diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 37b7bc2ca8c3a..397a106a4574b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -23,7 +23,7 @@ For more detailed information on contribution please read our [beginners guide]( * Unit/integration test coverage * Proposed [documentation](https://devdocs.magento.com) updates. Documentation contributions can be submitted via the [devdocs GitHub](https://github.com/magento/devdocs). 4. For larger features or changes, please [open an issue](https://github.com/magento/magento2/issues) to discuss the proposed changes prior to development. This may prevent duplicate or unnecessary effort and allow other contributors to provide input. -5. All automated tests must pass (all builds on [Travis CI](https://travis-ci.org/magento/magento2) must be green). +5. All automated tests must pass. ## Contribution process diff --git a/.htaccess b/.htaccess index e07a564bc0ab6..c5f3bf034d2fb 100644 --- a/.htaccess +++ b/.htaccess @@ -238,15 +238,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny diff --git a/.htaccess.sample b/.htaccess.sample index c9e83a53cc8bd..776f9046cf11d 100644 --- a/.htaccess.sample +++ b/.htaccess.sample @@ -238,15 +238,6 @@ Require all denied - - - order allow,deny - deny from all - - = 2.4> - Require all denied - - order allow,deny diff --git a/CHANGELOG.md b/CHANGELOG.md index 4661c4875737d..919f3f020088b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,817 @@ +2.4.0 +============= +* GitHub issues: + * [#24229](https://github.com/magento/magento2/issues/24229) -- Unable to enable maintenance mode when env.php is read only (fixed in [magento/magento2#25790](https://github.com/magento/magento2/pull/25790)) + * [#22416](https://github.com/magento/magento2/issues/22416) -- Coupling beetwen Magento_Checkout::js/view/shipping.js:validateShippingInformation() and layout definition or view. (fixed in [magento/magento2#25541](https://github.com/magento/magento2/pull/25541)) + * [#25739](https://github.com/magento/magento2/issues/25739) -- grunt clean does not clean generated folder (fixed in [magento/magento2#25765](https://github.com/magento/magento2/pull/25765)) + * [#25654](https://github.com/magento/magento2/issues/25654) -- Magento OpenGraph meta description / title content bleeding (fixed in [magento/magento2#25655](https://github.com/magento/magento2/pull/25655)) + * [#25731](https://github.com/magento/magento2/issues/25731) -- queue_consumer.xml doesn't allow numbers in handler class (fixed in [magento/magento2#25952](https://github.com/magento/magento2/pull/25952)) + * [#25935](https://github.com/magento/magento2/issues/25935) -- Email address mismatch with text in iPad(768) view (fixed in [magento/magento2#25942](https://github.com/magento/magento2/pull/25942)) + * [#25931](https://github.com/magento/magento2/issues/25931) -- Refresh Statistics: Updated At = Null should be display as "Never" instead of "undefined". (fixed in [magento/magento2#25932](https://github.com/magento/magento2/pull/25932)) + * [#25925](https://github.com/magento/magento2/issues/25925) -- Dupplicate Records when sorting column in Content->Themes Grid (fixed in [magento/magento2#25926](https://github.com/magento/magento2/pull/25926)) + * [#25917](https://github.com/magento/magento2/issues/25917) -- Admin confirm password input doesn't inherit needed styles (fixed in [magento/magento2#25918](https://github.com/magento/magento2/pull/25918)) + * [#25911](https://github.com/magento/magento2/issues/25911) -- Category - Notice on incorrect price filter GET param (fixed in [magento/magento2#25912](https://github.com/magento/magento2/pull/25912)) + * [#25893](https://github.com/magento/magento2/issues/25893) -- A "500 (Internal Server Error)" appears in Developer Console if Delete the image that is added to Page Content (fixed in [magento/magento2#25924](https://github.com/magento/magento2/pull/25924)) + * [#25896](https://github.com/magento/magento2/issues/25896) -- Cannot create folder using only letters (fixed in [magento/magento2#25904](https://github.com/magento/magento2/pull/25904)) + * [#24713](https://github.com/magento/magento2/issues/24713) -- Symbol of the Belarusian currency BYR is outdated (fixed in [magento/magento2#25723](https://github.com/magento/magento2/pull/25723)) + * [#19805](https://github.com/magento/magento2/issues/19805) -- Sales order Address Information edit form layout design improvement. (fixed in [magento/magento2#25699](https://github.com/magento/magento2/pull/25699)) + * [#23481](https://github.com/magento/magento2/issues/23481) -- Billing/Shipping Address edit form design update from order backend (fixed in [magento/magento2#25699](https://github.com/magento/magento2/pull/25699)) + * [#25972](https://github.com/magento/magento2/issues/25972) -- Not required spacing in submenu on hover desktop (fixed in [magento/magento2#25973](https://github.com/magento/magento2/pull/25973)) + * [#25586](https://github.com/magento/magento2/issues/25586) -- Mixins are not applied for advanced bundled modules (fixed in [magento/magento2#25587](https://github.com/magento/magento2/pull/25587)) + * [#20379](https://github.com/magento/magento2/issues/20379) -- calendar icon not aligned inside the textbox in Add Design Change page (fixed in [magento/magento2#26063](https://github.com/magento/magento2/pull/26063)) + * [#18687](https://github.com/magento/magento2/issues/18687) -- Left Side Back End Menu Design fix (fixed in [magento/magento2#26034](https://github.com/magento/magento2/pull/26034)) + * [#24025](https://github.com/magento/magento2/issues/24025) -- Slow Performance of ProductMetadata::getVersion (fixed in [magento/magento2#26001](https://github.com/magento/magento2/pull/26001)) + * [#100](https://github.com/magento/partners-magento2ee/issues/100) -- Users can see Negotiable Quotes from other Company (fixed in [magento/magento2#25940](https://github.com/magento/magento2/pull/25940) and [magento/partners-magento2ee#134](https://github.com/magento/partners-magento2ee/pull/134)) + * [#24357](https://github.com/magento/magento2/issues/24357) -- Eav sort order by attribute option_id (fixed in [magento/magento2#24360](https://github.com/magento/magento2/pull/24360)) + * [#25930](https://github.com/magento/magento2/issues/25930) -- Integration Success Message Text Overflow Issue in Admin (fixed in [magento/magento2#26011](https://github.com/magento/magento2/pull/26011)) + * [#25433](https://github.com/magento/magento2/issues/25433) -- Close (X) not working when error come for qty (fixed in [magento/magento2#25759](https://github.com/magento/magento2/pull/25759)) + * [#26155](https://github.com/magento/magento2/issues/26155) -- Table quote column customer_note uses wrong type (fixed in [magento/magento2#26160](https://github.com/magento/magento2/pull/26160)) + * [#761](https://github.com/magento/magento2/issues/761) -- A more verbose message when the db is not up to date. (fixed in [magento/magento2#25864](https://github.com/magento/magento2/pull/25864)) + * [#25974](https://github.com/magento/magento2/issues/25974) -- Amount of characters on a 'Area' Customizable Option counted differently on backend/frontend (fixed in [magento/magento2#26033](https://github.com/magento/magento2/pull/26033)) + * [#25674](https://github.com/magento/magento2/issues/25674) -- Elasticsearch version selections in admin are overly broad (fixed in [magento/magento2#25838](https://github.com/magento/magento2/pull/25838)) + * [#13136](https://github.com/magento/magento2/issues/13136) -- Error in vendor/magento/module-shipping/Model/Config/Source/Allmethods.php - public function toOptionArray (fixed in [magento/magento2#25315](https://github.com/magento/magento2/pull/25315)) + * [#22047](https://github.com/magento/magento2/issues/22047) -- Magento CRON Job Names are missing in NewRelic: "Transaction Names" (fixed in [magento/magento2#25957](https://github.com/magento/magento2/pull/25957)) + * [#26164](https://github.com/magento/magento2/issues/26164) -- Underline should not display on hover for delete icon at shopping cart Internet explorer browser (fixed in [magento/magento2#26173](https://github.com/magento/magento2/pull/26173)) + * [#24972](https://github.com/magento/magento2/issues/24972) -- Special Price class not added in configurable product page (fixed in [magento/magento2#26170](https://github.com/magento/magento2/pull/26170)) + * [#25659](https://github.com/magento/magento2/issues/25659) -- Paypal Payments Pro IPN keeping payments marked as Pending Payment (fixed in [magento/magento2#25876](https://github.com/magento/magento2/pull/25876)) + * [#18717](https://github.com/magento/magento2/issues/18717) -- UrlRewrite removes query string from url, if url has trailing slash (fixed in [magento/magento2#25603](https://github.com/magento/magento2/pull/25603)) + * [#26176](https://github.com/magento/magento2/issues/26176) -- Footer Newsletter input field width is not identical in Internet Explorer/EDGE browser compared with chrome (fixed in [magento/magento2#26182](https://github.com/magento/magento2/pull/26182)) + * [#25390](https://github.com/magento/magento2/issues/25390) -- UPS carrier model getting error when create plugin in to Magento 2.3.3 compatibility (fixed in [magento/magento2#26130](https://github.com/magento/magento2/pull/26130)) + * [#26083](https://github.com/magento/magento2/issues/26083) -- Problem when trying to unset additional data in payment method. (fixed in [magento/magento2#26084](https://github.com/magento/magento2/pull/26084)) + * [#26064](https://github.com/magento/magento2/issues/26064) -- Incorrect Error Message While Sharing Wish list more than Specified Email Address Value in Admin Configuration (fixed in [magento/magento2#26066](https://github.com/magento/magento2/pull/26066)) + * [#14663](https://github.com/magento/magento2/issues/14663) -- Updating Customer through rest/all/V1/customers/:id resets group_id if group_id not passed in payload (fixed in [magento/magento2#25958](https://github.com/magento/magento2/pull/25958)) + * [#20966](https://github.com/magento/magento2/issues/20966) -- Elastic Search 5 Indexing Performance Issue (fixed in [magento/magento2#25452](https://github.com/magento/magento2/pull/25452)) + * [#21684](https://github.com/magento/magento2/issues/21684) -- Currency sign for "Layered Navigation Price Step" is not according to default settings (fixed in [magento/magento2#24815](https://github.com/magento/magento2/pull/24815)) + * [#24468](https://github.com/magento/magento2/issues/24468) -- Export Coupon Code Grid redirect to DashBoard when create New Cart Price Rule (fixed in [magento/magento2#24471](https://github.com/magento/magento2/pull/24471)) + * [#22856](https://github.com/magento/magento2/issues/22856) -- Catalog pricerules are not working with custom options as expected in Magento 2.3.0 product details page (fixed in [magento/magento2#22917](https://github.com/magento/magento2/pull/22917)) + * [#14001](https://github.com/magento/magento2/issues/14001) -- M2.2.3 directory_country_region_name locale fix? 8bytes zh_Hans_CN(11bytes) ca_ES_VALENCIA(14bytes) (fixed in [magento/magento2#26268](https://github.com/magento/magento2/pull/26268)) + * [#23521](https://github.com/magento/magento2/issues/23521) -- Unable to run \Magento\Downloadable\Test\Unit\Helper\DownloadTest without internet connection / dns resolution (fixed in [magento/magento2#26264](https://github.com/magento/magento2/pull/26264)) + * [#25936](https://github.com/magento/magento2/issues/25936) -- Regular Price Label Alignment Issues in Frontend (fixed in [magento/magento2#26237](https://github.com/magento/magento2/pull/26237)) + * [#26227](https://github.com/magento/magento2/issues/26227) -- Need some space between input and update button Minicart (fixed in [magento/magento2#26234](https://github.com/magento/magento2/pull/26234)) + * [#26208](https://github.com/magento/magento2/issues/26208) -- Sorting issue for status column for Cache Management (fixed in [magento/magento2#26215](https://github.com/magento/magento2/pull/26215)) + * [#26206](https://github.com/magento/magento2/issues/26206) -- Missing information about currently reindexed index on failure (fixed in [magento/magento2#26207](https://github.com/magento/magento2/pull/26207)) + * [#26181](https://github.com/magento/magento2/issues/26181) -- Out of stock text is not aligned properly with add to cart button at list page in responsive (fixed in [magento/magento2#26183](https://github.com/magento/magento2/pull/26183)) + * [#26168](https://github.com/magento/magento2/issues/26168) -- Input Checkbox Alignment Issue at checkout page in Safari Browser (fixed in [magento/magento2#26169](https://github.com/magento/magento2/pull/26169)) + * [#19093](https://github.com/magento/magento2/issues/19093) -- API: salesOrderItemRepository does not include gift message (fixed in [magento/magento2#25946](https://github.com/magento/magento2/pull/25946)) + * [#23350](https://github.com/magento/magento2/issues/23350) -- Add support for catching throwables in App/Bootstrap (fixed in [magento/magento2#25250](https://github.com/magento/magento2/pull/25250)) + * [#26289](https://github.com/magento/magento2/issues/26289) -- Jump Datepicker in Catalog Price Rule (fixed in [magento/magento2#26290](https://github.com/magento/magento2/pull/26290)) + * [#22964](https://github.com/magento/magento2/issues/22964) -- Unable to save any dates if the user interface locale is not english (US) in 2.3.1 (fixed in [magento/magento2#26270](https://github.com/magento/magento2/pull/26270)) + * [#14913](https://github.com/magento/magento2/issues/14913) -- bookmark views become uneditable after deleting the first bookmark view. (fixed in [magento/magento2#26263](https://github.com/magento/magento2/pull/26263)) + * [#26217](https://github.com/magento/magento2/issues/26217) -- Wrong fields selection while using fragments on GraphQL products query (fixed in [magento/magento2#26218](https://github.com/magento/magento2/pull/26218)) + * [#23899](https://github.com/magento/magento2/issues/23899) -- system.xml file validation issue (fixed in [magento/magento2#25985](https://github.com/magento/magento2/pull/25985)) + * [#14971](https://github.com/magento/magento2/issues/14971) -- Improper Handling of Pagination SEO (fixed in [magento/magento2#25337](https://github.com/magento/magento2/pull/25337)) + * [#22988](https://github.com/magento/magento2/issues/22988) -- Wrong behavior of grid row and checkbox click (fixed in [magento/magento2#22990](https://github.com/magento/magento2/pull/22990)) + * [#7065](https://github.com/magento/magento2/issues/7065) -- page.main.title is translating title (fixed in [magento/magento2#26269](https://github.com/magento/magento2/pull/26269)) + * [#11209](https://github.com/magento/magento2/issues/11209) -- Wishlist Add grouped product Error (fixed in [magento/magento2#26258](https://github.com/magento/magento2/pull/26258)) + * [#26235](https://github.com/magento/magento2/issues/26235) -- Both Menu spacing should be same (fixed in [magento/magento2#26238](https://github.com/magento/magento2/pull/26238)) + * [#25130](https://github.com/magento/magento2/issues/25130) -- Issue with reorder when disabled reorder setting from admin (fixed in [magento/magento2#26051](https://github.com/magento/magento2/pull/26051)) + * [#25881](https://github.com/magento/magento2/issues/25881) -- Admin panel is not accessible after limited permissions set to at least one admin account (fixed in [magento/magento2#25909](https://github.com/magento/magento2/pull/25909)) + * [#25373](https://github.com/magento/magento2/issues/25373) -- The 'promotion' region of the minicart is never rendered (fixed in [magento/magento2#25375](https://github.com/magento/magento2/pull/25375)) + * [#25278](https://github.com/magento/magento2/issues/25278) -- Incorrect @return type at getSourceModel in Eav\Attribute (fixed in [magento/magento2#25333](https://github.com/magento/magento2/pull/25333)) + * [#25188](https://github.com/magento/magento2/issues/25188) -- Magento 2.3: Import fails if configurable attribute has an equal sign in its value (fixed in [magento/magento2#25194](https://github.com/magento/magento2/pull/25194)) + * [#22304](https://github.com/magento/magento2/issues/22304) -- [Grouped product] Can´t add simple products to cart if one other is out of stock (fixed in [magento/magento2#24955](https://github.com/magento/magento2/pull/24955)) + * [#26331](https://github.com/magento/magento2/issues/26331) -- [ MFTF ] Mess in ActionGroups: invalid names, multiple nodes. (fixed in [magento/partners-magento2ee#120](https://github.com/magento/partners-magento2ee/pull/120) and [magento/partners-magento2ee#108](https://github.com/magento/partners-magento2ee/pull/108) and [magento/partners-magento2ee#107](https://github.com/magento/partners-magento2ee/pull/107) and [magento/partners-magento2ee#106](https://github.com/magento/partners-magento2ee/pull/106) and [magento/partners-magento2ee#104](https://github.com/magento/partners-magento2ee/pull/104) and [magento/partners-magento2ee#105](https://github.com/magento/partners-magento2ee/pull/105) and [magento/partners-magento2ee#119](https://github.com/magento/partners-magento2ee/pull/119) and [magento/magento2#26323](https://github.com/magento/magento2/pull/26323) and [magento/magento2#26321](https://github.com/magento/magento2/pull/26321) and [magento/partners-magento2ee#111](https://github.com/magento/partners-magento2ee/pull/111) and [magento/magento2#26320](https://github.com/magento/magento2/pull/26320) and [magento/magento2#26319](https://github.com/magento/magento2/pull/26319) and [magento/partners-magento2ee#109](https://github.com/magento/partners-magento2ee/pull/109) and [magento/magento2#26322](https://github.com/magento/magento2/pull/26322) and [magento/partners-magento2ee#121](https://github.com/magento/partners-magento2ee/pull/121) and [magento/partners-magento2ee#117](https://github.com/magento/partners-magento2ee/pull/117) and [magento/partners-magento2ee#116](https://github.com/magento/partners-magento2ee/pull/116) and [magento/magento2#25828](https://github.com/magento/magento2/pull/25828) and [magento/magento2#26329](https://github.com/magento/magento2/pull/26329)) + * [#22909](https://github.com/magento/partners-magento2ee/issues/22909) -- requirejs/domReady.js can severely delay rendering of content (fixed in [magento/magento2#23313](https://github.com/magento/magento2/pull/23313) and [magento/partners-magento2ee#50](https://github.com/magento/partners-magento2ee/pull/50)) + * [#26396](https://github.com/magento/magento2/issues/26396) -- MFTF: Functional Tests are failing in Magento CI process (fixed in [magento/magento2#26407](https://github.com/magento/magento2/pull/26407) and [magento/magento2#26395](https://github.com/magento/magento2/pull/26395)) + * [#26364](https://github.com/magento/magento2/issues/26364) -- Add to Compare link not showing in mobile view under 640px (fixed in [magento/magento2#26424](https://github.com/magento/magento2/pull/26424) and [magento/magento2#26365](https://github.com/magento/magento2/pull/26365)) + * [#25968](https://github.com/magento/magento2/issues/25968) -- `getPrice()` returns a string when setting custom price in admin order (fixed in [magento/magento2#26313](https://github.com/magento/magento2/pull/26313)) + * [#26612](https://github.com/magento/magento2/issues/26612) -- MFTF: StorefrontApplyPromoCodeDuringCheckoutTest is failing in CI process (fixed in [magento/magento2#26614](https://github.com/magento/magento2/pull/26614)) + * [#26437](https://github.com/magento/magento2/issues/26437) -- Viewing customer shopping cart in admin shows all products in catalog when there is no active quote (fixed in [magento/magento2#26489](https://github.com/magento/magento2/pull/26489)) + * [#26479](https://github.com/magento/magento2/issues/26479) -- Bug: AutoloaderRegistry::getAutoloader returns array (fixed in [magento/magento2#26480](https://github.com/magento/magento2/pull/26480)) + * [#25162](https://github.com/magento/magento2/issues/25162) -- Message at Frontend has No HTML format (fixed in [magento/magento2#26455](https://github.com/magento/magento2/pull/26455)) + * [#25761](https://github.com/magento/magento2/issues/25761) -- Site map doesn't include home page (fixed in [magento/magento2#26445](https://github.com/magento/magento2/pull/26445)) + * [#18012](https://github.com/magento/magento2/issues/18012) -- Can not add string to underscore template using knockout (fixed in [magento/magento2#26435](https://github.com/magento/magento2/pull/26435)) + * [#25300](https://github.com/magento/magento2/issues/25300) -- Mobile view issue on category page - Sort By label overlaps with Shop By button (fixed in [magento/magento2#26381](https://github.com/magento/magento2/pull/26381)) + * [#26275](https://github.com/magento/magento2/issues/26275) -- Whitespace between label and required star on Checkout page (fixed in [magento/magento2#26285](https://github.com/magento/magento2/pull/26285)) + * [#26065](https://github.com/magento/magento2/issues/26065) -- Performance of isSalable method check on configurable product (fixed in [magento/magento2#26071](https://github.com/magento/magento2/pull/26071)) + * [#21014](https://github.com/magento/magento2/issues/21014) -- Gallery Thumbnail (left/right) Scroll Performance Android Chrome Sluggish and Unresponsive (fixed in [magento/magento2#25839](https://github.com/magento/magento2/pull/25839)) + * [#10518](https://github.com/magento/magento2/issues/10518) -- Mobile product page image jumps (fixed in [magento/magento2#25385](https://github.com/magento/magento2/pull/25385)) + * [#21717](https://github.com/magento/magento2/issues/21717) -- Product view page scrolls up randomly on mobile device (fixed in [magento/magento2#25385](https://github.com/magento/magento2/pull/25385)) + * [#25962](https://github.com/magento/magento2/issues/25962) -- Radio alignment issue (fixed in [magento/magento2#25966](https://github.com/magento/magento2/pull/25966)) + * [#9466](https://github.com/magento/magento2/issues/9466) -- Duplicating product copies product images couple of hundred times (fixed in [magento/magento2#25875](https://github.com/magento/magento2/pull/25875)) + * [#17125](https://github.com/magento/magento2/issues/17125) -- x-magento-init initialisation not bound to happen in the right order. (fixed in [magento/magento2#25764](https://github.com/magento/magento2/pull/25764)) + * [#26610](https://github.com/magento/magento2/issues/26610) -- MFTF: AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest is failing in CI process (fixed in [magento/magento2#26611](https://github.com/magento/magento2/pull/26611)) + * [#26240](https://github.com/magento/magento2/issues/26240) -- Minimum Advertised Price doesn't change for selected swatch option for configurable product (fixed in [magento/magento2#26241](https://github.com/magento/magento2/pull/26241) and [magento/magento2#26317](https://github.com/magento/magento2/pull/26317)) + * [#17847](https://github.com/magento/magento2/issues/17847) -- Wrong State Title, Displaying Status Label Rather than State (fixed in [magento/magento2#26569](https://github.com/magento/magento2/pull/26569)) + * [#21555](https://github.com/magento/magento2/issues/21555) -- Anonyomus classes in 2.3 (test data provider) (fixed in [magento/magento2#26533](https://github.com/magento/magento2/pull/26533)) + * [#26532](https://github.com/magento/magento2/issues/26532) -- di:setup:compile fails with anonymous classes (fixed in [magento/magento2#26533](https://github.com/magento/magento2/pull/26533)) + * [#26332](https://github.com/magento/magento2/issues/26332) -- BeforeOrderPaymentSaveObserver override payment insructions with wrong store view config (fixed in [magento/magento2#26399](https://github.com/magento/magento2/pull/26399)) + * [#25591](https://github.com/magento/magento2/issues/25591) -- & character in SKUs is shown as & in current variations list on configurable products (fixed in [magento/magento2#26007](https://github.com/magento/magento2/pull/26007)) + * [#13865](https://github.com/magento/magento2/issues/13865) -- Safari "Block all cookies" setting breaks JavaScript scripts (fixed in [magento/magento2#25324](https://github.com/magento/magento2/pull/25324)) + * [#26375](https://github.com/magento/magento2/issues/26375) -- Switching billing address causes Javascript function text to render in front-end checkout payment section (fixed in [magento/magento2#26378](https://github.com/magento/magento2/pull/26378)) + * [#25032](https://github.com/magento/magento2/issues/25032) -- Display some error "We can't update your Wish List right now." at wish list (fixed in [magento/magento2#25641](https://github.com/magento/magento2/pull/25641)) + * [#8691](https://github.com/magento/magento2/issues/8691) -- Language pack inheritance order is incorrect (fixed in [magento/magento2#26420](https://github.com/magento/magento2/pull/26420)) + * [#25195](https://github.com/magento/magento2/issues/25195) -- Issue with tier price 0 when saving product second time (fixed in [magento/magento2#26162](https://github.com/magento/magento2/pull/26162)) + * [#26622](https://github.com/magento/magento2/issues/26622) -- Fixed cart discount calculated incorrectly when product first added to cart. (fixed in [magento/magento2#26623](https://github.com/magento/magento2/pull/26623)) + * [#26543](https://github.com/magento/magento2/issues/26543) -- My Wish List Product not showing properly between >768px and <1023px (fixed in [magento/magento2#26546](https://github.com/magento/magento2/pull/26546)) + * [#25268](https://github.com/magento/magento2/issues/25268) -- $order->getCustomer() returns NULL for registered customer (fixed in [magento/magento2#26423](https://github.com/magento/magento2/pull/26423)) + * [#26338](https://github.com/magento/magento2/issues/26338) -- Code cleanup for module xml extra end tag removed (fixed in [magento/magento2#26339](https://github.com/magento/magento2/pull/26339)) + * [#26760](https://github.com/magento/magento2/issues/26760) -- Validate html error when enable critical css (fixed in [magento/magento2#26764](https://github.com/magento/magento2/pull/26764)) + * [#14885](https://github.com/magento/magento2/issues/14885) -- Refactoring: Code duplication EmailSender / ShipmentSender and so on (fixed in [magento/magento2#26714](https://github.com/magento/magento2/pull/26714)) + * [#863](https://github.com/magento/magento2/issues/863) -- How to switch base,thumbnail images in magento 2 back end (fixed in [magento/magento2#26502](https://github.com/magento/magento2/pull/26502)) + * [#26276](https://github.com/magento/magento2/issues/26276) -- Checkout. Quote Address Street cloning issue (fixed in [magento/magento2#26279](https://github.com/magento/magento2/pull/26279)) + * [#26245](https://github.com/magento/magento2/issues/26245) -- Magento does not send an email about a refunded grouped product (fixed in [magento/magento2#26246](https://github.com/magento/magento2/pull/26246)) + * [#26141](https://github.com/magento/magento2/issues/26141) -- Modal Popup and Custom subTitle erased (fixed in [magento/magento2#26142](https://github.com/magento/magento2/pull/26142)) + * [#25487](https://github.com/magento/magento2/issues/25487) -- Redis cache grows unilimmited (fixed in [magento/magento2#25488](https://github.com/magento/magento2/pull/25488)) + * [#25245](https://github.com/magento/magento2/issues/25245) -- Warning when Search Terms page is opened by clicking option at the footer (fixed in [magento/magento2#25246](https://github.com/magento/magento2/pull/25246)) + * [#24842](https://github.com/magento/magento2/issues/24842) -- Unable to delete custom option file in admin order create (fixed in [magento/magento2#24843](https://github.com/magento/magento2/pull/24843)) + * [#847](https://github.com/magento/magento2/issues/847) -- Use cursor: pointer for the product online switcher (fixed in [magento/magento2#25991](https://github.com/magento/magento2/pull/25991)) + * [#26843](https://github.com/magento/magento2/issues/26843) -- es_US Spanish (United States ) Locale is not supported in Magento 2.3.4 (fixed in [magento/magento2#26857](https://github.com/magento/magento2/pull/26857)) + * [#26054](https://github.com/magento/magento2/issues/26054) -- Do not duplicate SEO meta data when duplicating a product (fixed in [magento/magento2#26659](https://github.com/magento/magento2/pull/26659)) + * [#26314](https://github.com/magento/magento2/issues/26314) -- Minimum Advertised Prices duplicates for all configurable products with price from selected swatch (fixed in [magento/magento2#26317](https://github.com/magento/magento2/pull/26317)) + * [#24547](https://github.com/magento/magento2/issues/24547) -- Magento\Customer\Model\Account\Redirect::setRedirectCookie() not properly working (fixed in [magento/magento2#24612](https://github.com/magento/magento2/pull/24612)) + * [#26675](https://github.com/magento/magento2/issues/26675) -- Date incorrect on pdf invoice (fixed in [magento/magento2#26701](https://github.com/magento/magento2/pull/26701)) + * [#25675](https://github.com/magento/magento2/issues/25675) -- Unable add product to cart in Magento 2.3.3 backend when stock quantity is 1 - "The requested qty is not available" (fixed in [magento/magento2#26650](https://github.com/magento/magento2/pull/26650)) + * [#26583](https://github.com/magento/magento2/issues/26583) -- Product Detail Page - Tier price (fixed & discount) save percentage displaying wrong calculation (fixed in [magento/magento2#26584](https://github.com/magento/magento2/pull/26584)) + * [#25963](https://github.com/magento/magento2/issues/25963) -- Grid Export rendered data is not reflecting in the exported File, Displayed ID instead of Rendered Label (fixed in [magento/magento2#26523](https://github.com/magento/magento2/pull/26523)) + * [#26416](https://github.com/magento/magento2/issues/26416) -- Compare Products section not showing in mobile view under 767px (fixed in [magento/magento2#26418](https://github.com/magento/magento2/pull/26418)) + * [#25656](https://github.com/magento/magento2/issues/25656) -- M2.3.2 : Nullable getters in Service Contracts will throw a reflection error when used in the web API (fixed in [magento/magento2#25806](https://github.com/magento/magento2/pull/25806)) + * [#24971](https://github.com/magento/magento2/issues/24971) -- Incorrect @var reference in docBlock of a class member variable (fixed in [magento/magento2#24976](https://github.com/magento/magento2/pull/24976)) + * [#14958](https://github.com/magento/magento2/issues/14958) -- sale_sequence_* records are not removed on store view deletion (fixed in [magento/magento2#22296](https://github.com/magento/magento2/pull/22296)) + * [#26607](https://github.com/magento/partners-magento2ee/issues/26607) -- MFTF: AdminReorderWithCatalogPriceTest is failing in CI process (fixed in [magento/magento2#26608](https://github.com/magento/magento2/pull/26608) and [magento/partners-magento2ee#135](https://github.com/magento/partners-magento2ee/pull/135)) + * [#25856](https://github.com/magento/magento2/issues/25856) -- Ordered Products Report not grouping by configurable products variations (fixed in [magento/magento2#25858](https://github.com/magento/magento2/pull/25858)) + * [#26973](https://github.com/magento/magento2/issues/26973) -- Fatal error on calling ImageFactory::create() for product_page_image_large (fixed in [magento/magento2#26974](https://github.com/magento/magento2/pull/26974)) + * [#26917](https://github.com/magento/magento2/issues/26917) -- Tax rate Zip/Post range and check box alignment issue (fixed in [magento/magento2#26932](https://github.com/magento/magento2/pull/26932)) + * [#26838](https://github.com/magento/magento2/issues/26838) -- Low stock report showing disabled products (fixed in [magento/magento2#26862](https://github.com/magento/magento2/pull/26862)) + * [#26229](https://github.com/magento/magento2/issues/26229) -- Active menu is not set when opening admin path Marketing > User Content > Pending Reviews (fixed in [magento/magento2#26230](https://github.com/magento/magento2/pull/26230)) + * [#25910](https://github.com/magento/magento2/issues/25910) -- Choose drop down not close when open another for upload file for swatch (fixed in [magento/magento2#26090](https://github.com/magento/magento2/pull/26090)) + * [#13269](https://github.com/magento/magento2/issues/13269) -- Magento Framework Escaper - Critical log with special symbols (fixed in [magento/magento2#25895](https://github.com/magento/magento2/pull/25895)) + * [#25738](https://github.com/magento/magento2/issues/25738) -- DOMDocument::loadHTML(): Tag date invalid in Entity (fixed in [magento/magento2#25895](https://github.com/magento/magento2/pull/25895)) + * [#572](https://github.com/magento/magento2/issues/572) -- How do I: Bug tracking for Magento 1 + ideas for Magento 2 (fixed in [magento/magento2#25349](https://github.com/magento/magento2/pull/25349)) + * [#26800](https://github.com/magento/magento2/issues/26800) -- Undefined variable $type in Product-Link Management (fixed in [magento/magento2#26979](https://github.com/magento/magento2/pull/26979)) + * [#13252](https://github.com/magento/magento2/issues/13252) -- Fetching customer entity through API will not return 'is_subscribed' extension attribute (fixed in [magento/magento2#25311](https://github.com/magento/magento2/pull/25311)) + * [#27044](https://github.com/magento/magento2/issues/27044) -- BUG: Category Repository get()'s argument `store_id` does not work (fixed in [magento/magento2#27048](https://github.com/magento/magento2/pull/27048)) + * [#27040](https://github.com/magento/magento2/issues/27040) -- Images no longer responsive (fixed in [magento/magento2#27041](https://github.com/magento/magento2/pull/27041)) + * [#17933](https://github.com/magento/magento2/issues/17933) -- Bank Transer Payment Instuctions switch back to default (fixed in [magento/magento2#26765](https://github.com/magento/magento2/pull/26765)) + * [#23755](https://github.com/magento/magento2/issues/23755) -- Store view switcher is wrong , when each store views have different url. (fixed in [magento/magento2#26548](https://github.com/magento/magento2/pull/26548)) + * [#26384](https://github.com/magento/magento2/issues/26384) -- Store switcher redirects to homepage for multistore setup with different domains (fixed in [magento/magento2#26548](https://github.com/magento/magento2/pull/26548)) + * [#25243](https://github.com/magento/magento2/issues/25243) -- Numerical placeholder count in Phrase starts with %1, however js code assumes 0% (fixed in [magento/magento2#25359](https://github.com/magento/magento2/pull/25359)) + * [#23619](https://github.com/magento/magento2/issues/23619) -- Less compilation extend 'mixin' has no matches (fixed in [magento/magento2#24003](https://github.com/magento/magento2/pull/24003)) + * [#27032](https://github.com/magento/magento2/issues/27032) -- Add image lazy loading (fixed in [magento/magento2#27033](https://github.com/magento/magento2/pull/27033)) + * [#25834](https://github.com/magento/magento2/issues/25834) -- Discount fixed amount whole cart applied mutiple time when customer use Check Out with Multiple Addresses (fixed in [magento/magento2#26419](https://github.com/magento/magento2/pull/26419)) + * [#26989](https://github.com/magento/magento2/issues/26989) -- MFTF: Use Magento Cron for reindexing after creating data (fixed in [magento/magento2#26990](https://github.com/magento/magento2/pull/26990)) + * [#27027](https://github.com/magento/magento2/issues/27027) -- Admin date of birth doesn't factor in user locale set (fixed in [magento/magento2#27149](https://github.com/magento/magento2/pull/27149)) + * [#973](https://github.com/magento/magento2/issues/973) -- [Question] Add jenkins-ci ant build.xml and tool configurations to repository (fixed in [magento/magento2#27138](https://github.com/magento/magento2/pull/27138)) + * [#26758](https://github.com/magento/magento2/issues/26758) -- cms-page-specific layouts are not applied if FullActionName differs from page_view (fixed in [magento/magento2#27131](https://github.com/magento/magento2/pull/27131)) + * [#26847](https://github.com/magento/magento2/issues/26847) -- Hitting enter on create folder in media gallery refreshes the page (fixed in [magento/magento2#27029](https://github.com/magento/magento2/pull/27029)) + * [#27009](https://github.com/magento/magento2/issues/27009) -- Missing variable outside CATCH causing a double-fault in Renderer.php (fixed in [magento/magento2#27026](https://github.com/magento/magento2/pull/27026)) + * [#26992](https://github.com/magento/magento2/issues/26992) -- Add New ratings Is active and checkbox alignment issue (fixed in [magento/magento2#27014](https://github.com/magento/magento2/pull/27014)) + * [#20309](https://github.com/magento/magento2/issues/20309) -- URL Rewrites redirect loop (fixed in [magento/magento2#26902](https://github.com/magento/magento2/pull/26902)) + * [#26648](https://github.com/magento/magento2/issues/26648) -- Table bottom border color different then thead and tbody border color (fixed in [magento/magento2#26649](https://github.com/magento/magento2/pull/26649)) + * [#26590](https://github.com/magento/magento2/issues/26590) -- Customer registration multiple form submit (fixed in [magento/magento2#26642](https://github.com/magento/magento2/pull/26642)) + * [#24637](https://github.com/magento/magento2/issues/24637) -- Chinese input in tinymce 4 (fixed in [magento/magento2#25454](https://github.com/magento/magento2/pull/25454)) + * [#22609](https://github.com/magento/magento2/issues/22609) -- Since Magento 2.3 the wysiwyg image uploader incorrectly uses pub/media as storage root. (fixed in [magento/magento2#24878](https://github.com/magento/magento2/pull/24878)) + * [#24735](https://github.com/magento/magento2/issues/24735) -- Image in minicart is blurred on iPhone (fixed in [magento/magento2#24743](https://github.com/magento/magento2/pull/24743)) + * [#14086](https://github.com/magento/magento2/issues/14086) -- Guest cart API ignoring cartId in url for some methods (fixed in [magento/magento2#27172](https://github.com/magento/magento2/pull/27172)) + * [#25219](https://github.com/magento/magento2/issues/25219) -- Custom attributes of images generated by Block\Product\ImageFactory don't render correctly (fixed in [magento/magento2#26959](https://github.com/magento/magento2/pull/26959)) + * [#26499](https://github.com/magento/magento2/issues/26499) -- Product url key is not transliterated anymore if already set (fixed in [magento/magento2#26506](https://github.com/magento/magento2/pull/26506)) + * [#25669](https://github.com/magento/magento2/issues/25669) -- health_check.php fails if any database cache engine configured (fixed in [magento/magento2#25722](https://github.com/magento/magento2/pull/25722)) + * [#20472](https://github.com/magento/magento2/issues/20472) -- Special Price shown without currency symbol in magento backoffice (fixed in [magento/magento2#27261](https://github.com/magento/magento2/pull/27261)) + * [#20906](https://github.com/magento/magento2/issues/20906) -- Magento backend catalog "Cost" without currency symbol (fixed in [magento/magento2#27261](https://github.com/magento/magento2/pull/27261)) + * [#21910](https://github.com/magento/magento2/issues/21910) -- Magento backend catalog "MSRP" without currency symbol (fixed in [magento/magento2#27261](https://github.com/magento/magento2/pull/27261)) + * [#4112](https://github.com/magento/magento2/issues/4112) -- Wrong parent category url_key in URL (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#11615](https://github.com/magento/magento2/issues/11615) -- URL Rewrites vs multiple storeviews - a never ending battle (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#11616](https://github.com/magento/magento2/issues/11616) -- URL Rewrites vs multiple storeviews - too many rewrites are being generated (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#25124](https://github.com/magento/magento2/issues/25124) -- Magento 2.3 Wrong product url for anchor categories for multiple storeviews (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#26393](https://github.com/magento/magento2/issues/26393) -- Product category url rewrite missing storeview specific url_key (fixed in [magento/magento2#26784](https://github.com/magento/magento2/pull/26784)) + * [#26345](https://github.com/magento/magento2/issues/26345) -- Reorder in Admin panel leads to report page in case of changed product custom option max characters (fixed in [magento/magento2#26348](https://github.com/magento/magento2/pull/26348)) + * [#26117](https://github.com/magento/magento2/issues/26117) -- "Current user does not have an active cart" even when he actually has one (fixed in [magento/magento2#27187](https://github.com/magento/magento2/pull/27187)) + * [#26825](https://github.com/magento/magento2/issues/26825) -- Adding/updating image using API will not create thumbnail for admin products grid (fixed in [magento/magento2#27170](https://github.com/magento/magento2/pull/27170)) + * [#27117](https://github.com/magento/partners-magento2ee/issues/27117) -- MFTF: Test names are not following Best Practices (`Test` suffix) (fixed in [magento/magento2#27118](https://github.com/magento/magento2/pull/27118) and [magento/partners-magento2ee#151](https://github.com/magento/partners-magento2ee/pull/151)) + * [#26683](https://github.com/magento/magento2/issues/26683) -- Unable to execute addSimpleProduct mutation while other items out of stock (fixed in [magento/magento2#27015](https://github.com/magento/magento2/pull/27015)) + * [#26963](https://github.com/magento/magento2/issues/26963) -- Missing JS file (critical-css-loader) in Magento 2.3.4 (fixed in [magento/magento2#26987](https://github.com/magento/magento2/pull/26987)) + * [#26473](https://github.com/magento/magento2/issues/26473) -- BUG: Wrong selected product image when query url param configurable product (fixed in [magento/magento2#26560](https://github.com/magento/magento2/pull/26560)) + * [#26856](https://github.com/magento/magento2/issues/26856) -- Wrong gallery main image and active preview after updating for configurable products. (fixed in [magento/magento2#26560](https://github.com/magento/magento2/pull/26560)) + * [#26858](https://github.com/magento/magento2/issues/26858) -- Wrong gallery behavior when query url param configurable product (fixed in [magento/magento2#26560](https://github.com/magento/magento2/pull/26560)) + * [#22251](https://github.com/magento/magento2/issues/22251) -- Admin Order - Email is Now Required - Magento 2.2.6 (fixed in [magento/magento2#24479](https://github.com/magento/magento2/pull/24479)) + * [#24704](https://github.com/magento/magento2/issues/24704) -- Saving CMS Page Title from REST web API makes content empty (fixed in [magento/magento2#27237](https://github.com/magento/magento2/pull/27237)) + * [#26827](https://github.com/magento/magento2/issues/26827) -- 500 when creating new product after adding attribute via API and assigning to attribute set via UI (fixed in [magento/magento2#27191](https://github.com/magento/magento2/pull/27191)) + * [#27124](https://github.com/magento/magento2/issues/27124) -- Share Wishlist Email: Image Logic Issue (fixed in [magento/magento2#27125](https://github.com/magento/magento2/pull/27125)) + * [#27335](https://github.com/magento/magento2/issues/27335) -- My account Address Book Additional Address Entries table issue (fixed in [magento/magento2#27336](https://github.com/magento/magento2/pull/27336)) + * [#14080](https://github.com/magento/magento2/issues/14080) -- Category path is the same as key producing duplicate URL issue (fixed in [magento/magento2#27304](https://github.com/magento/magento2/pull/27304)) + * [#26708](https://github.com/magento/magento2/issues/26708) -- ORDER BY has two similar conditions (fixed in [magento/magento2#27263](https://github.com/magento/magento2/pull/27263)) + * [#26745](https://github.com/magento/magento2/issues/26745) -- OrderPaymentInterface is missing setAdditionalInformation() (fixed in [magento/magento2#26748](https://github.com/magento/magento2/pull/26748)) + * [#26335](https://github.com/magento/magento2/issues/26335) -- Update zendframework to laminas (fixed in [magento/magento2#26436](https://github.com/magento/magento2/pull/26436)) + * [#186](https://github.com/magento/magento2/issues/186) -- Indexing product (on save) should be done after committing the transaction (fixed in [magento/magento2#26923](https://github.com/magento/magento2/pull/26923)) + * [#26224](https://github.com/magento/magento2/issues/26224) -- Cache type without "instance" causes exception when disabling the module through "Cache Management" in the backend (fixed in [magento/magento2#27307](https://github.com/magento/magento2/pull/27307)) + * [#25540](https://github.com/magento/magento2/issues/25540) -- Products are not displaying infront end after updating product via importing CSV. (fixed in [magento/magento2#25664](https://github.com/magento/magento2/pull/25664)) + * [#22010](https://github.com/magento/magento2/issues/22010) -- 22010 -Updates AbstractExtensibleObject and AbstractExtensibleModel annotations (fixed in [magento/magento2#22011](https://github.com/magento/magento2/pull/22011)) + * [#22363](https://github.com/magento/magento2/issues/22363) -- Layered navigation breaks HTML5 Validation (fixed in [magento/magento2#26055](https://github.com/magento/magento2/pull/26055)) + * [#26884](https://github.com/magento/magento2/issues/26884) -- Customer address is duplicated after setBillingAddressOnCart GraphQL mutation. (fixed in [magento/magento2#27107](https://github.com/magento/magento2/pull/27107)) + * [#26742](https://github.com/magento/magento2/issues/26742) -- graphql mutation setShippingMethodsOnCart get wrong data in available_shipping_methods. (fixed in [magento/magento2#27004](https://github.com/magento/magento2/pull/27004)) + * [#13689](https://github.com/magento/magento2/issues/13689) -- Cannot create catagory's name with thai langauge (fixed in [magento/magento2#27412](https://github.com/magento/magento2/pull/27412)) + * [#27370](https://github.com/magento/magento2/issues/27370) -- Internet explorer issue:Default billing/shipping address not showing (fixed in [magento/magento2#27383](https://github.com/magento/magento2/pull/27383)) + * [#27086](https://github.com/magento/magento2/issues/27086) -- Report Value doesn't matching - "Year-To-Date Starts" (fixed in [magento/magento2#27088](https://github.com/magento/magento2/pull/27088)) + * [#22833](https://github.com/magento/magento2/issues/22833) -- Short-term admin accounts (fixed in [magento/magento2#22837](https://github.com/magento/magento2/pull/22837)) + * [#6310](https://github.com/magento/magento2/issues/6310) -- Changing products 'this item has weight' using 'Update Attributes' is not possible (fixed in [magento/magento2#26075](https://github.com/magento/magento2/pull/26075)) + * [#16315](https://github.com/magento/magento2/issues/16315) -- Product save with onthefly index ignores website assignments (fixed in [magento/magento2#27365](https://github.com/magento/magento2/pull/27365)) + * [#26762](https://github.com/magento/magento2/issues/26762) -- undefined index db-ssl-verify (fixed in [magento/magento2#26763](https://github.com/magento/magento2/pull/26763)) + * [#26652](https://github.com/magento/magento2/issues/26652) -- In the minicart edit and remove icon is not aligned. (fixed in [magento/magento2#27493](https://github.com/magento/magento2/pull/27493)) + * [#1002](https://github.com/magento/magento2/issues/1002) -- Database Schema: Incorrect Unique Indexes (fixed in [magento/magento2#27399](https://github.com/magento/magento2/pull/27399)) + * [#24990](https://github.com/magento/magento2/issues/24990) -- Admin Panel logo link is not directing to admin dashboard page (fixed in [magento/magento2#26100](https://github.com/magento/magento2/pull/26100)) + * [#27500](https://github.com/magento/magento2/issues/27500) -- Unit Tests incompatible with PHPUnit 8 (fixed in [magento/magento2#27521](https://github.com/magento/magento2/pull/27521) and [magento/magento2#27519](https://github.com/magento/magento2/pull/27519) and [magento/magento2#27627](https://github.com/magento/magento2/pull/27627) and [magento/magento2#27522](https://github.com/magento/magento2/pull/27522)) + * [#27496](https://github.com/magento/magento2/issues/27496) -- The store logo is missing when using the Magento_blank theme (fixed in [magento/magento2#27497](https://github.com/magento/magento2/pull/27497)) + * [#27169](https://github.com/magento/magento2/issues/27169) -- Not able to update value with "use default checkbox" for Downloadable Product's Sample and Links Title. (fixed in [magento/magento2#27295](https://github.com/magento/magento2/pull/27295)) + * [#27320](https://github.com/magento/magento2/issues/27320) -- MFTF: Functional Tests failing due to missing data in indexes (fixed in [magento/magento2#27322](https://github.com/magento/magento2/pull/27322) and [magento/magento2#27321](https://github.com/magento/magento2/pull/27321) and [magento/magento2#27323](https://github.com/magento/magento2/pull/27323)) + * [#171](https://github.com/magento/partners-magento2ee/issues/171) -- CMS Page modifications are not being reported in Action Log (fixed in [magento/magento2#27597](https://github.com/magento/magento2/pull/27597) and [magento/partners-magento2ee#172](https://github.com/magento/partners-magento2ee/pull/172)) + * [#13851](https://github.com/magento/magento2/issues/13851) -- Credit doesn't recognize amount after comma (fixed in [magento/magento2#27343](https://github.com/magento/magento2/pull/27343)) + * [#26986](https://github.com/magento/magento2/issues/26986) -- REST API Pagination Does not work as expected (fixed in [magento/magento2#26988](https://github.com/magento/magento2/pull/26988)) + * [#27638](https://github.com/magento/partners-magento2ee/issues/27638) -- PHPUnit Tests bundled with Magento fatal errors (fixed in [magento/magento2#27701](https://github.com/magento/magento2/pull/27701) and [magento/partners-magento2ee#178](https://github.com/magento/partners-magento2ee/pull/178)) + * [#27506](https://github.com/magento/magento2/issues/27506) -- Viewport resizing on search input focus on iphone (fixed in [magento/magento2#27603](https://github.com/magento/magento2/pull/27603)) + * [#27607](https://github.com/magento/magento2/issues/27607) -- Integration Tests: DOM Assertion class (fixed in [magento/magento2#27606](https://github.com/magento/magento2/pull/27606)) + * [#27299](https://github.com/magento/magento2/issues/27299) -- Integration Tests: Consecutive `dispatch($uri)` on Test AbstractController fails (fixed in [magento/magento2#27300](https://github.com/magento/magento2/pull/27300)) + * [#27920](https://github.com/magento/magento2/issues/27920) -- [2.3.5] Incorrect html structure after MC-30989 (fixed in [magento/magento2#27926](https://github.com/magento/magento2/pull/27926)) + * [#25769](https://github.com/magento/magento2/issues/25769) -- Arabic invoice pdf issue Magento 2.3.0 showing as Arabic letters but not correct (fixed in [magento/magento2#27887](https://github.com/magento/magento2/pull/27887)) + * [#27874](https://github.com/magento/magento2/issues/27874) -- Vat Validation URL for EU Vat numbers changed. (Vies Service) (fixed in [magento/magento2#27886](https://github.com/magento/magento2/pull/27886)) + * [#27089](https://github.com/magento/magento2/issues/27089) -- BUG: `getDefaultLimitPerPageValue` returns value that is not available (fixed in [magento/magento2#27093](https://github.com/magento/magento2/pull/27093)) + * [#1270](https://github.com/magento/magento2/issues/1270) -- back button not working in edit order status (fixed in [magento/magento2#27976](https://github.com/magento/magento2/pull/27976)) + * [#27897](https://github.com/magento/magento2/issues/27897) -- MFTF: Inconsistent case in Section name (fixed in [magento/magento2#27955](https://github.com/magento/magento2/pull/27955)) + * [#27503](https://github.com/magento/magento2/issues/27503) -- MFTF: Acceptance tests break the naming convention (fixed in [magento/magento2#27515](https://github.com/magento/magento2/pull/27515)) + * [#15](https://github.com/magento/magento2-login-as-customer/issues/15) -- Remove Login as Customer actions from admin grids (fixed in [magento/magento2-login-as-customer#23](https://github.com/magento/magento2-login-as-customer/pull/23)) + * [#34](https://github.com/magento/magento2-login-as-customer/issues/34) -- Remove option to merge guest cart (fixed in [magento/magento2-login-as-customer#49](https://github.com/magento/magento2-login-as-customer/pull/49)) + * [#110](https://github.com/magento/magento2-login-as-customer/issues/110) -- Need to add spinner/loader while Admin is logging in as Customer (fixed in [magento/magento2-login-as-customer#123](https://github.com/magento/magento2-login-as-customer/pull/123)) + * [#159](https://github.com/magento/magento2-login-as-customer/issues/159) -- Error is shown on the page if customer is not sign out from account (fixed in [magento/magento2-login-as-customer#164](https://github.com/magento/magento2-login-as-customer/pull/164)) + * [#102](https://github.com/magento/magento2-login-as-customer/issues/102) -- Admin user is logged into the default website if customer registered on second website (fixed in [magento/magento2-login-as-customer#148](https://github.com/magento/magento2-login-as-customer/pull/148)) + * [#59](https://github.com/magento/magento2-login-as-customer/issues/59) -- Customer data not invalidated private content after logged in (fixed in [magento/magento2-login-as-customer#68](https://github.com/magento/magento2-login-as-customer/pull/68)) + * [#33](https://github.com/magento/magento2-login-as-customer/issues/33) -- Update Readme.txt (fixed in [magento/magento2-login-as-customer#64](https://github.com/magento/magento2-login-as-customer/pull/64)) + * [#60](https://github.com/magento/magento2-login-as-customer/issues/60) -- Customer data sometimes not being cleared when logging in as customer (fixed in [magento/magento2-login-as-customer#75](https://github.com/magento/magento2-login-as-customer/pull/75)) + * [#73](https://github.com/magento/magento2-login-as-customer/issues/73) -- Page title is empty when admin login as customer (fixed in [magento/magento2-login-as-customer#92](https://github.com/magento/magento2-login-as-customer/pull/92)) + * [#55](https://github.com/magento/magento2-login-as-customer/issues/55) -- [DEV] Need to update/change titles for ACL resource tree related to Login as Customer (fixed in [magento/magento2-login-as-customer#69](https://github.com/magento/magento2-login-as-customer/pull/69)) + * [#8](https://github.com/magento/magento2-login-as-customer/issues/8) -- Merge initial module (fixed in [magento/magento2-login-as-customer#7](https://github.com/magento/magento2-login-as-customer/pull/7)) + * [#122](https://github.com/magento/magento2-login-as-customer/issues/122) -- Issue 96 (fixed in [magento/magento2-login-as-customer#123](https://github.com/magento/magento2-login-as-customer/pull/123)) + * [#71](https://github.com/magento/magento2-login-as-customer/issues/71) -- Login As Customer functionality is available when Login As Customer->Enable Extension=No (fixed in [magento/magento2-login-as-customer#121](https://github.com/magento/magento2-login-as-customer/pull/121)) + * [#16](https://github.com/magento/magento2-login-as-customer/issues/16) -- All System Configuration settings should be on Global level (fixed in [magento/magento2-login-as-customer#120](https://github.com/magento/magento2-login-as-customer/pull/120)) + * [#56](https://github.com/magento/magento2-login-as-customer/issues/56) -- [DEV] Confirmation pop-up window for "Login as Customer" if the setting "Store View To Log In" = "Manual Chooser" (fixed in [magento/magento2-login-as-customer#119](https://github.com/magento/magento2-login-as-customer/pull/119)) + * [#100](https://github.com/magento/magento2-login-as-customer/issues/100) -- Moved all UI from LoginAsCustomer to new LoginAsCustomerUi module (fixed in [magento/magento2-login-as-customer#101](https://github.com/magento/magento2-login-as-customer/pull/101)) + * [#97](https://github.com/magento/magento2-login-as-customer/issues/97) -- Refactor Magento\LoginAsCustomer\Model\Login Model (fixed in [magento/magento2-login-as-customer#99](https://github.com/magento/magento2-login-as-customer/pull/99)) + * [#17](https://github.com/magento/magento2-login-as-customer/issues/17) -- Notification banner on storefront (fixed in [magento/magento2-login-as-customer#87](https://github.com/magento/magento2-login-as-customer/pull/87)) + * [#10](https://github.com/magento/magento2-login-as-customer/issues/10) -- Controllers refactoring (fixed in [magento/magento2-login-as-customer#21](https://github.com/magento/magento2-login-as-customer/pull/21)) + +* GitHub pull requests: + * [magento/magento2#25905](https://github.com/magento/magento2/pull/25905) -- [Checkout] Cover DirectoryData by Unit Test (by @edenduong) + * [magento/magento2#25808](https://github.com/magento/magento2/pull/25808) -- No marginal white space validation added (by @ajithkumar-maragathavel) + * [magento/magento2#25790](https://github.com/magento/magento2/pull/25790) -- Don't disable FPC for maintenance, instead send "no cache" headers (by @Parakoopa) + * [magento/magento2#25774](https://github.com/magento/magento2/pull/25774) -- [Config] Giving the possibility to have a config dependency based on empty config value (by @eduard13) + * [magento/magento2#25604](https://github.com/magento/magento2/pull/25604) -- Moving Ui message.js hide speed and timeout into variables for easier… (by @edward-simpson) + * [magento/magento2#25541](https://github.com/magento/magento2/pull/25541) -- Removes hardcoded references to country selector component (by @krzksz) + * [magento/magento2#25939](https://github.com/magento/magento2/pull/25939) -- [ProductAlert] Cover Helper Data by Unit Test (by @edenduong) + * [magento/magento2#25928](https://github.com/magento/magento2/pull/25928) -- [Variable] Cover Variable Data Model by Unit Test (by @edenduong) + * [magento/magento2#25913](https://github.com/magento/magento2/pull/25913) -- [Backend] Covering the Backend Grid Decoding Helper by UnitTest (by @eduard13) + * [magento/magento2#25822](https://github.com/magento/magento2/pull/25822) -- MFTF: Extract Action Groups to separate files - magento/module-import-export (by @lbajsarowicz) + * [magento/magento2#25812](https://github.com/magento/magento2/pull/25812) -- MFTF: Extract Action Groups to separate files - magento/module-reports (by @lbajsarowicz) + * [magento/magento2#25803](https://github.com/magento/magento2/pull/25803) -- MFTF: Extract Action Groups to separate files - magento/module-shipping (by @lbajsarowicz) + * [magento/magento2#25791](https://github.com/magento/magento2/pull/25791) -- MFTF: Extract Action Groups to separate files - magento/module-widget (by @lbajsarowicz) + * [magento/magento2#25792](https://github.com/magento/magento2/pull/25792) -- MFTF: Extract Action Groups to separate files - magento/module-variable (by @lbajsarowicz) + * [magento/magento2#25765](https://github.com/magento/magento2/pull/25765) -- Magento#25739: fixed issue "grunt clean does not clean generated folder" (by @andrewbess) + * [magento/magento2#25655](https://github.com/magento/magento2/pull/25655) -- Add escaping on meta properties for open graph (by @NathMorgan) + * [magento/magento2#25952](https://github.com/magento/magento2/pull/25952) -- Resolve queue_consumer.xml doesn't allow numbers in handler class issue25731 (by @edenduong) + * [magento/magento2#25942](https://github.com/magento/magento2/pull/25942) -- Resolve Email address mismatch with text in iPad(768) view issue25935 (by @edenduong) + * [magento/magento2#25932](https://github.com/magento/magento2/pull/25932) -- Resolve Refresh Statistics: Updated At = Null should be display as "Never" instead of "undefined". issue25931 (by @edenduong) + * [magento/magento2#25926](https://github.com/magento/magento2/pull/25926) -- Resolve Duplicate Records when sorting column in Content->Themes Grid issue25925 (by @edenduong) + * [magento/magento2#25918](https://github.com/magento/magento2/pull/25918) -- [Ui] Adding admin class for password input type. (by @eduard13) + * [magento/magento2#25912](https://github.com/magento/magento2/pull/25912) -- Category filters - Fix notice on incorrect price param (by @ihor-sviziev) + * [magento/magento2#25995](https://github.com/magento/magento2/pull/25995) -- Updating wee -> weee in Magento_Weee README (by @MellenIO) + * [magento/magento2#25984](https://github.com/magento/magento2/pull/25984) -- [Customer] Cover CustomerData\Customer and CustomerData\JsLayoutDataProviderPool by Unit Test (by @edenduong) + * [magento/magento2#25982](https://github.com/magento/magento2/pull/25982) -- [Catalog] Cover Price Validation Result class by Unit Test (by @edenduong) + * [magento/magento2#25954](https://github.com/magento/magento2/pull/25954) -- Refactor: Add method hints to Tracking Status (by @lbajsarowicz) + * [magento/magento2#25924](https://github.com/magento/magento2/pull/25924) -- Resolve A "500 (Internal Server Error)" appears in Developer Console if Delete the image that is added to Page Content issue25893 (by @edenduong) + * [magento/magento2#25904](https://github.com/magento/magento2/pull/25904) -- Resolve issue 25896: Cannot create folder using only letters (by @edenduong) + * [magento/magento2#25723](https://github.com/magento/magento2/pull/25723) -- Fix #24713 - Symbol of the Belarusian currency BYR is outdated (by @Bartlomiejsz) + * [magento/magento2#25699](https://github.com/magento/magento2/pull/25699) -- magento/magento2#23481: Billing/Shipping Address edit form design update from order backend (by @alexey-rakitin) + * [magento/magento2#25262](https://github.com/magento/magento2/pull/25262) -- Allow autoplay for vimeo thumb click (by @philkun) + * [magento/magento2#26016](https://github.com/magento/magento2/pull/26016) -- [DownloadableImportExport] Cover Helper Data by Unit Test (by @edenduong) + * [magento/magento2#25997](https://github.com/magento/magento2/pull/25997) -- [Newsletter] Refactor code and Cover Model/Observer class by Unit Test (by @edenduong) + * [magento/magento2#25993](https://github.com/magento/magento2/pull/25993) -- [InstantPurchase] Cover Ui/CustomerAddressesFormatter and Ui/ShippingMethodFormatter by Unit Test (by @edenduong) + * [magento/magento2#25992](https://github.com/magento/magento2/pull/25992) -- Cover magento/magento2#25556 with jasmine test (by @Nazar65) + * [magento/magento2#25973](https://github.com/magento/magento2/pull/25973) -- [Removed spacing in submenu on hover desktop] (by @hitesh-wagento) + * [magento/magento2#25975](https://github.com/magento/magento2/pull/25975) -- phpdoc fix return type (by @maslii) + * [magento/magento2#25624](https://github.com/magento/magento2/pull/25624) -- Add right arrow to show some items have children (by @fredden) + * [magento/magento2#25114](https://github.com/magento/magento2/pull/25114) -- Added translate for strings and added missing node in existing translate attribute on xml. (by @sanganinamrata) + * [magento/magento2#25587](https://github.com/magento/magento2/pull/25587) -- Refactor JavaScript mixins module (by @krzksz) + * [magento/magento2#26069](https://github.com/magento/magento2/pull/26069) -- [CMS] Improving the test coverage for UrlBuilder ViewModel (by @eduard13) + * [magento/magento2#26067](https://github.com/magento/magento2/pull/26067) -- [Msrp] Cover MsrpPriceCalculator by Unit Test (by @edenduong) + * [magento/magento2#26063](https://github.com/magento/magento2/pull/26063) -- [Theme] Reverting removed container class (by @eduard13) + * [magento/magento2#26057](https://github.com/magento/magento2/pull/26057) -- [Contact] covered Model Config by Unit Test (by @srsathish92) + * [magento/magento2#26050](https://github.com/magento/magento2/pull/26050) -- [Catalog] covered product ViewModel AddToCompareAvailability by Unit Test (by @srsathish92) + * [magento/magento2#26044](https://github.com/magento/magento2/pull/26044) -- Set empty value to color picker when input is reset to update preview (by @gperis) + * [magento/magento2#26045](https://github.com/magento/magento2/pull/26045) -- [Downloadable] Cover Helper Data by Unit Test (by @edenduong) + * [magento/magento2#26042](https://github.com/magento/magento2/pull/26042) -- [Catalog] Cover Component/FilterFactory by Unit Test (by @edenduong) + * [magento/magento2#26043](https://github.com/magento/magento2/pull/26043) -- [Persistent] Cover CustomerData by Unit Test (by @edenduong) + * [magento/magento2#26037](https://github.com/magento/magento2/pull/26037) -- Fixes phpcs errors and warnings for Magento\Framework\View\Element (by @krisdante) + * [magento/magento2#26034](https://github.com/magento/magento2/pull/26034) -- MAGETWO-95866 Add horizontal scroll if elements extend menu's width (by @ptylek) + * [magento/magento2#26003](https://github.com/magento/magento2/pull/26003) -- [Directory] Cover action directory/json/countryRegion by Integration Test (by @edenduong) + * [magento/magento2#26001](https://github.com/magento/magento2/pull/26001) -- Fix caching Magento Metadata getVersion (by @luklewluk) + * [magento/magento2#25940](https://github.com/magento/magento2/pull/25940) -- Asynchronous operation validate (by @sedonik) + * [magento/magento2#25697](https://github.com/magento/magento2/pull/25697) -- [New Relic] Making system configs dependent by Enabled field (by @eduard13) + * [magento/magento2#25523](https://github.com/magento/magento2/pull/25523) -- Contact form > Adding ViewModel (by @rafaelstz) + * [magento/magento2#24360](https://github.com/magento/magento2/pull/24360) -- #24357 Eav sort order by attribute option_id (by @tnsezer) + * [magento/magento2#26060](https://github.com/magento/magento2/pull/26060) -- [Backend] Cover Dashboard Helper Data by Unit Test (by @edenduong) + * [magento/magento2#26059](https://github.com/magento/magento2/pull/26059) -- [Downloadable] Cover the Observer SetHasDownloadableProductsObserver by Unit Test (by @edenduong) + * [magento/magento2#26058](https://github.com/magento/magento2/pull/26058) -- Fixed typo: "reviwGrid" to "reviewGrid" (by @matheusgontijo) + * [magento/magento2#26011](https://github.com/magento/magento2/pull/26011) -- Fixed the issue 25930 (by @divyajyothi5321) + * [magento/magento2#26004](https://github.com/magento/magento2/pull/26004) -- [Backend] Cover action admin/dashboard/ajaxBlock by Integration Test (by @edenduong) + * [magento/magento2#25920](https://github.com/magento/magento2/pull/25920) -- Code refactor in Catalog ViewModel Breadcrumbs (by @srsathish92) + * [magento/magento2#26082](https://github.com/magento/magento2/pull/26082) -- [GiftMessage] Cover Observer SalesEventOrderItemToQuoteItemObserver by Unit Test (by @edenduong) + * [magento/magento2#26076](https://github.com/magento/magento2/pull/26076) -- [Search] Cover SynonymActions Column by Unit Test (by @edenduong) + * [magento/magento2#26068](https://github.com/magento/magento2/pull/26068) -- [GoogleAnalytics] covered Helper Data by Unit Test (by @srsathish92) + * [magento/magento2#26009](https://github.com/magento/magento2/pull/26009) -- Refactor: Add information about the path that is not allowed (by @lbajsarowicz) + * [magento/magento2#25759](https://github.com/magento/magento2/pull/25759) -- fixed issue 25433 (by @Ashna-Jahan) + * [magento/magento2#25854](https://github.com/magento/magento2/pull/25854) -- MFTF: Extract Action Groups to separate files - magento/module-catalog (by @lbajsarowicz) + * [magento/magento2#25846](https://github.com/magento/magento2/pull/25846) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-import-export (by @lbajsarowicz) + * [magento/magento2#25845](https://github.com/magento/magento2/pull/25845) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-inventory (by @lbajsarowicz) + * [magento/magento2#25844](https://github.com/magento/magento2/pull/25844) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-rule (by @lbajsarowicz) + * [magento/magento2#25842](https://github.com/magento/magento2/pull/25842) -- MFTF: Extract Action Groups to separate files - magento/module-catalog-search (by @lbajsarowicz) + * [magento/magento2#25841](https://github.com/magento/magento2/pull/25841) -- MFTF: Extract Action Groups to separate files - magento/module-checkout (by @lbajsarowicz) + * [magento/magento2#25831](https://github.com/magento/magento2/pull/25831) -- MFTF: Extract Action Groups to separate files - magento/module-config (by @lbajsarowicz) + * [magento/magento2#25836](https://github.com/magento/magento2/pull/25836) -- MFTF: Extract Action Groups to separate files - magento/module-cms (by @lbajsarowicz) + * [magento/magento2#25830](https://github.com/magento/magento2/pull/25830) -- MFTF: Extract Action Groups to separate files - magento/module-configurable-product (by @lbajsarowicz) + * [magento/magento2#25829](https://github.com/magento/magento2/pull/25829) -- MFTF: Extract Action Groups to separate files - magento/module-currency-symbol (by @lbajsarowicz) + * [magento/magento2#25825](https://github.com/magento/magento2/pull/25825) -- MFTF: Extract Action Groups to separate files - magento/module-downloadable (by @lbajsarowicz) + * [magento/magento2#25823](https://github.com/magento/magento2/pull/25823) -- MFTF: Extract Action Groups to separate files - magento/module-email (by @lbajsarowicz) + * [magento/magento2#25821](https://github.com/magento/magento2/pull/25821) -- MFTF: Extract Action Groups to separate files - magento/module-grouped-product (by @lbajsarowicz) + * [magento/magento2#25819](https://github.com/magento/magento2/pull/25819) -- MFTF: Extract Action Groups to separate files - magento/module-multishipping (by @lbajsarowicz) + * [magento/magento2#25820](https://github.com/magento/magento2/pull/25820) -- MFTF: Extract Action Groups to separate files - magento/module-indexer (by @lbajsarowicz) + * [magento/magento2#25818](https://github.com/magento/magento2/pull/25818) -- MFTF: Extract Action Groups to separate files - magento/module-newsletter (by @lbajsarowicz) + * [magento/magento2#25817](https://github.com/magento/magento2/pull/25817) -- MFTF: Replace redundant Action Group with proper one - magento/module-page-cache (by @lbajsarowicz) + * [magento/magento2#25816](https://github.com/magento/magento2/pull/25816) -- MFTF: Extract Action Groups to separate files - magento/module-paypal (by @lbajsarowicz) + * [magento/magento2#25815](https://github.com/magento/magento2/pull/25815) -- MFTF: Extract Action Groups to separate files - magento/module-persistent (by @lbajsarowicz) + * [magento/magento2#25813](https://github.com/magento/magento2/pull/25813) -- MFTF: Extract Action Groups to separate files - magento/module-product-video (by @lbajsarowicz) + * [magento/magento2#25811](https://github.com/magento/magento2/pull/25811) -- MFTF: Extract Action Groups to separate files - magento/module-sales (by @lbajsarowicz) + * [magento/magento2#25807](https://github.com/magento/magento2/pull/25807) -- MFTF: Extract Action Groups to separate files - magento/module-sales-rule (by @lbajsarowicz) + * [magento/magento2#25804](https://github.com/magento/magento2/pull/25804) -- MFTF: Extract Action Groups to separate files - magento/module-search (by @lbajsarowicz) + * [magento/magento2#25802](https://github.com/magento/magento2/pull/25802) -- MFTF: Extract Action Groups to separate files - magento/module-store (by @lbajsarowicz) + * [magento/magento2#25800](https://github.com/magento/magento2/pull/25800) -- MFTF: Extract Action Groups to separate files - magento/module-swatches (by @lbajsarowicz) + * [magento/magento2#25799](https://github.com/magento/magento2/pull/25799) -- MFTF: Extract Action Groups to separate files - magento/module-tax (by @lbajsarowicz) + * [magento/magento2#25797](https://github.com/magento/magento2/pull/25797) -- MFTF: Extract Action Groups to separate files - magento/module-ui (by @lbajsarowicz) + * [magento/magento2#25794](https://github.com/magento/magento2/pull/25794) -- MFTF: Extract Action Groups to separate files - magento/module-url-rewrite (by @lbajsarowicz) + * [magento/magento2#25793](https://github.com/magento/magento2/pull/25793) -- MFTF: Extract Action Groups to separate files - magento/module-user (by @lbajsarowicz) + * [magento/magento2#25788](https://github.com/magento/magento2/pull/25788) -- MFTF: Extract Action Groups to separate files - magento/module-wishlist (by @lbajsarowicz) + * [magento/magento2#25787](https://github.com/magento/magento2/pull/25787) -- MFTF: Extract Action Groups to separate files - magento/module-bundle (by @lbajsarowicz) + * [magento/magento2#25784](https://github.com/magento/magento2/pull/25784) -- MFTF: Extract Action Groups to separate files - magento/module-braintree (by @lbajsarowicz) + * [magento/magento2#25783](https://github.com/magento/magento2/pull/25783) -- MFTF: Extract Action Groups to separate files - magento/module-backup (by @lbajsarowicz) + * [magento/magento2#26157](https://github.com/magento/magento2/pull/26157) -- Remove blank space at the end of label (by @gihovani) + * [magento/magento2#26160](https://github.com/magento/magento2/pull/26160) -- Changing the data type for quote column customer_note (by @ravi-chandra3197) + * [magento/magento2#26154](https://github.com/magento/magento2/pull/26154) -- [LayeredNavigation] Covering the ProductAttributeGridBuildObserver for LayeredNavigation … (by @eduard13) + * [magento/magento2#26150](https://github.com/magento/magento2/pull/26150) -- [CatalogInventory] Covering the InvalidatePriceIndexUponConfigChangeObserver for Catalog… (by @eduard13) + * [magento/magento2#26148](https://github.com/magento/magento2/pull/26148) -- [Bundle] Covering the SetAttributeTabBlockObserver for Bundles by Unit Test (by @eduard13) + * [magento/magento2#26140](https://github.com/magento/magento2/pull/26140) -- [ImportExport] Cover Export Source Model by Unit Test (by @edenduong) + * [magento/magento2#26136](https://github.com/magento/magento2/pull/26136) -- Code refactor, updated Unit Test with JsonHexTag Serializer (by @srsathish92) + * [magento/magento2#26128](https://github.com/magento/magento2/pull/26128) -- Refactor Magento Version module (+ Unit Tests) (by @lbajsarowicz) + * [magento/magento2#26127](https://github.com/magento/magento2/pull/26127) -- [Weee] Cover Weee Plugin by Unit Test (by @edenduong) + * [magento/magento2#26096](https://github.com/magento/magento2/pull/26096) -- [Checkout] Covering the ResetQuoteAddresses by Unit Test (by @eduard13) + * [magento/magento2#26028](https://github.com/magento/magento2/pull/26028) -- Refactor: Remove deprecated methods (by @andrewbess) + * [magento/magento2#25864](https://github.com/magento/magento2/pull/25864) -- Adobe stock integration Issue-761: Highlight the selected image in the grid (by @serhiyzhovnir) + * [magento/magento2#24849](https://github.com/magento/magento2/pull/24849) -- Simplify some conditional checks (by @DanielRuf) + * [magento/magento2#26131](https://github.com/magento/magento2/pull/26131) -- Reduce sleep time for Unit Test of Consumer to 0 seconds (by @lbajsarowicz) + * [magento/magento2#26126](https://github.com/magento/magento2/pull/26126) -- [WeeeGraphQl] Covering the FixedProductTax (by @lbajsarowicz) + * [magento/magento2#26129](https://github.com/magento/magento2/pull/26129) -- Refactor / Cleanup: `use` section does not need leading backslash (by @lbajsarowicz) + * [magento/magento2#26125](https://github.com/magento/magento2/pull/26125) -- [WishlistGraphQL] Covering the CustomerWishlistResolver (by @lbajsarowicz) + * [magento/magento2#26033](https://github.com/magento/magento2/pull/26033) -- Normalize new line symbols in Product Text Option (type=area) (by @Leone) + * [magento/magento2#25915](https://github.com/magento/magento2/pull/25915) -- Tests for: magento/magento2#24907, magento/magento2#25051, magento/magento2#25149, magento/magento2#24973, magento/magento2#25666. (by @p-bystritsky) + * [magento/magento2#25838](https://github.com/magento/magento2/pull/25838) -- [FIXES] #25674: Elasticsearch version selections in admin are overly … (by @mautz-et-tong) + * [magento/magento2#25315](https://github.com/magento/magento2/pull/25315) -- Error in vendor/magento/module-shipping/Model/Config/Source/Allmethod… (by @mrodespin) + * [magento/magento2#25957](https://github.com/magento/magento2/pull/25957) -- Add Cron Jobs names to New Relic transactions (by @lbajsarowicz) + * [magento/magento2#24103](https://github.com/magento/magento2/pull/24103) -- Refactor AdminNotification Render Blocks (by @DavidLambauer) + * [magento/magento2#26173](https://github.com/magento/magento2/pull/26173) -- Added Fix for 26164 (by @divyajyothi5321) + * [magento/magento2#26170](https://github.com/magento/magento2/pull/26170) -- Fixed Special Price class not added in configurable product page (by @ravi-chandra3197) + * [magento/magento2#25876](https://github.com/magento/magento2/pull/25876) -- Advance the order state to processing when a capture notification is received (by @azambon) + * [magento/magento2#25428](https://github.com/magento/magento2/pull/25428) -- Fixed model save and ObjectManager usage (by @drpayyne) + * [magento/magento2#25125](https://github.com/magento/magento2/pull/25125) -- Performance optimizations (by @andrey-legayev) + * [magento/magento2#26225](https://github.com/magento/magento2/pull/26225) -- Hotfix for Invalid date format in Functional and hotfix for failing Integration Tests (by @lbajsarowicz) + * [magento/magento2#25603](https://github.com/magento/magento2/pull/25603) -- Fix removing query string from url after redirect (by @arendarenko) + * [magento/magento2#26182](https://github.com/magento/magento2/pull/26182) -- Fix for footer newsletter input field length in IE/Edge (by @divyajyothi5321) + * [magento/magento2#26130](https://github.com/magento/magento2/pull/26130) -- Fix #25390 - UPS carrier model getting error when creating plugin in to Magento 2.3.3 compatibility (by @Bartlomiejsz) + * [magento/magento2#26084](https://github.com/magento/magento2/pull/26084) -- Fix #26083 - problem with unsAdditionalInformation in \Magento\Payment\Model\Info (by @marcoaacoliveira) + * [magento/magento2#26066](https://github.com/magento/magento2/pull/26066) -- 26064 issuefix (by @divyajyothi5321) + * [magento/magento2#25958](https://github.com/magento/magento2/pull/25958) -- #14663 Updating Customer through rest/all/V1/customers/:id resets group_id if group_id not passed in payload (by @MaxRomanov4669) + * [magento/magento2#25479](https://github.com/magento/magento2/pull/25479) -- JSON fields support (by @akaplya) + * [magento/magento2#25640](https://github.com/magento/magento2/pull/25640) -- set correct pram like in BlockRepository implementation (by @torhoehn) + * [magento/magento2#25478](https://github.com/magento/magento2/pull/25478) -- Clearer PHPDocs comment for AbstractBlock and Escaper (by @edward-simpson) + * [magento/magento2#25452](https://github.com/magento/magento2/pull/25452) -- Elastic Search 5 Indexing Performance Issue with product mapper (by @behnamshayani) + * [magento/magento2#24815](https://github.com/magento/magento2/pull/24815) -- Fix #21684 - Currency sign for "Layered Navigation Price Step" is not according to default settings (by @Bartlomiejsz) + * [magento/magento2#24471](https://github.com/magento/magento2/pull/24471) -- Resolve Export Coupon Code Grid redirect to DashBoard when create New Cart Price Rule issue24468 (by @edenduong) + * [magento/magento2#22917](https://github.com/magento/magento2/pull/22917) -- magento/magento2#22856: Catalog price rules are not working with custom options as expected. (by @p-bystritsky) + * [magento/magento2#26274](https://github.com/magento/magento2/pull/26274) -- MFTF: Replace incorrect URLs in Tests and ActionGroups (by @lbajsarowicz) + * [magento/magento2#26273](https://github.com/magento/magento2/pull/26273) -- MFTF: Replace with for Admin log out (by @lbajsarowicz) + * [magento/magento2#26268](https://github.com/magento/magento2/pull/26268) -- Fix #14001 - M2.2.3 directory_country_region_name locale fix? 8bytes zh_Hans_CN(11bytes) ca_ES_VALENCIA(14bytes) (by @Bartlomiejsz) + * [magento/magento2#26264](https://github.com/magento/magento2/pull/26264) -- Issue 23521 (by @aleromano89) + * [magento/magento2#26259](https://github.com/magento/magento2/pull/26259) -- Fix invalid XML Schema location (by @lbajsarowicz) + * [magento/magento2#26237](https://github.com/magento/magento2/pull/26237) -- Added Fix for 25936 (by @divyajyothi5321) + * [magento/magento2#26234](https://github.com/magento/magento2/pull/26234) -- [Align some space between input and update button Minicart] (by @hitesh-wagento) + * [magento/magento2#26215](https://github.com/magento/magento2/pull/26215) -- Disabled the sorting option in status column on cache grid (by @srsathish92) + * [magento/magento2#26207](https://github.com/magento/magento2/pull/26207) -- #26206 Add information about currently reindexed index. (by @lbajsarowicz) + * [magento/magento2#26183](https://github.com/magento/magento2/pull/26183) -- Added Fix for - 26181 (by @divyajyothi5321) + * [magento/magento2#26169](https://github.com/magento/magento2/pull/26169) -- Added Fix for issue 26168 (by @divyajyothi5321) + * [magento/magento2#26029](https://github.com/magento/magento2/pull/26029) -- Fixed keyboard arrow keys behavior for number fields in AdobeStock grid (by @rogyar) + * [magento/magento2#25946](https://github.com/magento/magento2/pull/25946) -- Add plugin for SalesOrderItemRepository gift message (#19093) (by @lfolco) + * [magento/magento2#25250](https://github.com/magento/magento2/pull/25250) -- Implement catching for all Errors - ref Magento issue #23350 (by @miszyman) + * [magento/magento2#26290](https://github.com/magento/magento2/pull/26290) -- [Fixed Jump Datepicker issue in Catalog Price Rule] (by @hitesh-wagento) + * [magento/magento2#26270](https://github.com/magento/magento2/pull/26270) -- Fix #22964 (by @marcoaacoliveira) + * [magento/magento2#26263](https://github.com/magento/magento2/pull/26263) -- Fix #14913 - bookmark views become uneditable after deleting the first bookmark view (by @Bartlomiejsz) + * [magento/magento2#26251](https://github.com/magento/magento2/pull/26251) -- [Customer] Removing the delete buttons for default customer groups (by @eduard13) + * [magento/magento2#26218](https://github.com/magento/magento2/pull/26218) -- FIX issue#26217 - Wrong fields selection while using fragments on GraphQL (by @phoenix128) + * [magento/magento2#26048](https://github.com/magento/magento2/pull/26048) -- Fixed spelling and adjusted white spaces (by @pawankparmar) + * [magento/magento2#25985](https://github.com/magento/magento2/pull/25985) -- Fixed ability to save configuration in field without label (by @AndreyChorniy) + * [magento/magento2#25337](https://github.com/magento/magento2/pull/25337) -- #14971 Improper Handling of Pagination SEO (by @chickenland) + * [magento/magento2#22990](https://github.com/magento/magento2/pull/22990) -- [Catalog|Sales] Fix wrong behavior of grid row click event (by @Den4ik) + * [magento/magento2#26360](https://github.com/magento/magento2/pull/26360) -- System xml cleanup (by @Bartlomiejsz) + * [magento/magento2#26359](https://github.com/magento/magento2/pull/26359) -- Remove Filename Normalization in Delete Controller (by @pmclain) + * [magento/magento2#26354](https://github.com/magento/magento2/pull/26354) -- Make WYSIWYG configuration options depend on wysiwyg being enabled (by @Bartlomiejsz) + * [magento/magento2#26312](https://github.com/magento/magento2/pull/26312) -- magento/magento2#: Unit test for \Magento\Review\Observer\CatalogProductListCollectionAppendSummaryFieldsObserver (by @atwixfirster) + * [magento/magento2#26311](https://github.com/magento/magento2/pull/26311) -- [CurrencySymbol] Fixing the redirect after saving the currency symbols (by @eduard13) + * [magento/magento2#26305](https://github.com/magento/magento2/pull/26305) -- [GoogleAdWords] Conversion ID client validation (by @eduard13) + * [magento/magento2#26269](https://github.com/magento/magento2/pull/26269) -- Fix #7065 - page.main.title is translating title (by @Bartlomiejsz) + * [magento/magento2#26258](https://github.com/magento/magento2/pull/26258) -- #11209 Wishlist Add grouped product Error (by @MaxRomanov4669) + * [magento/magento2#26238](https://github.com/magento/magento2/pull/26238) -- [Correct both Menu spacing issue] (by @hitesh-wagento) + * [magento/magento2#26185](https://github.com/magento/magento2/pull/26185) -- Allow wishlist share when all items are out of stock (by @pmclain) + * [magento/magento2#26051](https://github.com/magento/magento2/pull/26051) -- Issue with reorder when disabled reorder setting from admin issue25130 (by @edenduong) + * [magento/magento2#25909](https://github.com/magento/magento2/pull/25909) -- Resolve Admin panel is not accessible after limited permissions set to at least one admin account issue25881 (by @edenduong) + * [magento/magento2#25718](https://github.com/magento/magento2/pull/25718) -- add the possibility to add display mode dependant layout handles (by @brosenberger) + * [magento/magento2#25716](https://github.com/magento/magento2/pull/25716) -- add check if attribute value is possible to be set as configurable option (by @brosenberger) + * [magento/magento2#25375](https://github.com/magento/magento2/pull/25375) -- Fix minicart promotion region not rendering #25373 (by @mattijv) + * [magento/magento2#25333](https://github.com/magento/magento2/pull/25333) -- Fixed issue magento#25278:Incorrect @return type at getSourceModel in… (by @mkalakailo) + * [magento/magento2#25194](https://github.com/magento/magento2/pull/25194) -- Limit the php explode to 2 to prevent extra '=' sign content in the a… (by @dhoang89) + * [magento/magento2#25083](https://github.com/magento/magento2/pull/25083) -- Cleanup search api di (by @thomas-kl1) + * [magento/magento2#24955](https://github.com/magento/magento2/pull/24955) -- Fix: add to cart grouped product when exists a sold out option (by @gihovani) + * [magento/magento2#23313](https://github.com/magento/magento2/pull/23313) -- Trigger page load listeners when no longer loading (by @johnhughes1984) + * [magento/magento2#26407](https://github.com/magento/magento2/pull/26407) -- MFTF: Set of fixes for failing Functional Tests (by @lbajsarowicz) + * [magento/magento2#26395](https://github.com/magento/magento2/pull/26395) -- HotFix: Failing Magento EE check on Layered Navigation (by @lbajsarowicz) + * [magento/magento2#26323](https://github.com/magento/magento2/pull/26323) -- MFTF: Extract Action Groups to separate files - magento/module-ui (by @lbajsarowicz) + * [magento/magento2#26321](https://github.com/magento/magento2/pull/26321) -- MFTF: Extract Action Groups to separate files - magento/module-shipping (by @lbajsarowicz) + * [magento/magento2#26320](https://github.com/magento/magento2/pull/26320) -- MFTF: Extract Action Groups to separate files - magento/module-sales (by @lbajsarowicz) + * [magento/magento2#26319](https://github.com/magento/magento2/pull/26319) -- MFTF: Extract Action Groups to separate files - magento/module-catalog (by @lbajsarowicz) + * [magento/magento2#26424](https://github.com/magento/magento2/pull/26424) -- Add to Compare link does not show in mobile view under 640px in blank theme (by @ptylek) + * [magento/magento2#26402](https://github.com/magento/magento2/pull/26402) -- magento/magento2#: Unit test for \Magento\AdminNotification\Observer\PredispatchAdminActionControllerObserver (by @atwixfirster) + * [magento/magento2#26365](https://github.com/magento/magento2/pull/26365) -- Add to Compare link not showing in mobile view under 640px (by @tejash-wagento) + * [magento/magento2#26313](https://github.com/magento/magento2/pull/26313) -- Issue-25968 - Added additional checking for returning needed variable… (by @AndreyChorniy) + * [magento/magento2#26495](https://github.com/magento/magento2/pull/26495) -- Fix confusing phpdoc in curl client (by @tdgroot) + * [magento/magento2#26464](https://github.com/magento/magento2/pull/26464) -- magento/magento2#: GraphQl. RevokeCustomerToken. Test coverage. (by @atwixfirster) + * [magento/magento2#26452](https://github.com/magento/magento2/pull/26452) -- magento/magento2#: GraphQl. DeletePaymentToken. Remove redundant validation logic. Test coverage. (by @atwixfirster) + * [magento/magento2#26322](https://github.com/magento/magento2/pull/26322) -- MFTF: Extract Action Groups to separate files for dev/tests (by @lbajsarowicz) + * [magento/magento2#26391](https://github.com/magento/magento2/pull/26391) -- MFTF: Add missing tests annotations (by @lbajsarowicz) + * [magento/magento2#26628](https://github.com/magento/magento2/pull/26628) -- Fixed #26513 (by @vikalps4) + * [magento/magento2#26614](https://github.com/magento/magento2/pull/26614) -- #26612 Fix failure on Coupon Apply procedure when loading mask still visible (by @lbajsarowicz) + * [magento/magento2#26558](https://github.com/magento/magento2/pull/26558) -- [Csp] Covering the model classes by Unit Tests (by @eduard13) + * [magento/magento2#26540](https://github.com/magento/magento2/pull/26540) -- Added action group for cms block duplication test (by @ajithkumar-maragathavel) + * [magento/magento2#26537](https://github.com/magento/magento2/pull/26537) -- Covered admin cms block creatation with MFTF test (by @ajithkumar-maragathavel) + * [magento/magento2#26512](https://github.com/magento/magento2/pull/26512) -- Extend exception message (by @oroskodias) + * [magento/magento2#26511](https://github.com/magento/magento2/pull/26511) -- Extend exception message (by @oroskodias) + * [magento/magento2#26509](https://github.com/magento/magento2/pull/26509) -- Update PHP Docs (by @oroskodias) + * [magento/magento2#26490](https://github.com/magento/magento2/pull/26490) -- Fixed type issue. Create unit test for customer data model (by @AndreyChorniy) + * [magento/magento2#26489](https://github.com/magento/magento2/pull/26489) -- Checked if quote object contains id before looking for quote items (by @rav-redchamps) + * [magento/magento2#26480](https://github.com/magento/magento2/pull/26480) -- Bugfix #26479 Exception when Autoloader was not registered properly (by @lbajsarowicz) + * [magento/magento2#26478](https://github.com/magento/magento2/pull/26478) -- Unit Test for Magento\Fedex\Plugin\Block\DataProviders\Tracking\ChangeTitle (by @karyna-tsymbal-atwix) + * [magento/magento2#26455](https://github.com/magento/magento2/pull/26455) -- 25162 fixed wrong format link (by @Usik2203) + * [magento/magento2#26445](https://github.com/magento/magento2/pull/26445) -- Fix #25761: Site map doesn't include home page (by @deepaksnair) + * [magento/magento2#26435](https://github.com/magento/magento2/pull/26435) -- #18012: added i18n wrapper to be used in underscore templates for translation (by @sergiy-v) + * [magento/magento2#26434](https://github.com/magento/magento2/pull/26434) -- Fix typo in sitemap product collection docblock (by @Tjitse-E) + * [magento/magento2#26381](https://github.com/magento/magento2/pull/26381) -- #25300 Mobile view issue on category page - Sort By label overlaps (by @akartavtsev) + * [magento/magento2#26327](https://github.com/magento/magento2/pull/26327) -- Fix the wrong behavior of validation scroll on the iPhone X (by @iGerchak) + * [magento/magento2#26285](https://github.com/magento/magento2/pull/26285) -- Remove extraneous whitespace - #26275 (by @DanielRuf) + * [magento/magento2#26071](https://github.com/magento/magento2/pull/26071) -- #26065 isSaleable cache and optimize result for configurable products (by @ilnytskyi) + * [magento/magento2#25994](https://github.com/magento/magento2/pull/25994) -- Extend exception messages (by @oroskodias) + * [magento/magento2#25839](https://github.com/magento/magento2/pull/25839) -- Fix gallery thumbs navigation scrolling (by @iGerchak) + * [magento/magento2#25385](https://github.com/magento/magento2/pull/25385) -- Prevent page scroll jumping when product gallery loads (by @krzksz) + * [magento/magento2#26355](https://github.com/magento/magento2/pull/26355) -- Performance: Getting rid of `array_merge` in loop (by @lbajsarowicz) + * [magento/magento2#26296](https://github.com/magento/magento2/pull/26296) -- Add Visual Code catalog generator (by @manuelcanepa) + * [magento/magento2#26000](https://github.com/magento/magento2/pull/26000) -- magento/magento2#: Remove unused “Default Email Domain” option and related to it code (by @atwixfirster) + * [magento/magento2#25966](https://github.com/magento/magento2/pull/25966) -- [Fixed Radio alignment issue] (by @hitesh-wagento) + * [magento/magento2#25875](https://github.com/magento/magento2/pull/25875) -- Prevent endless loop when duplicating product (by @JeroenVanLeusden) + * [magento/magento2#25764](https://github.com/magento/magento2/pull/25764) -- Cleanup, refactor and cover with tests section-config module (by @krzksz) + * [magento/magento2#24460](https://github.com/magento/magento2/pull/24460) -- Allow construction of products with custom_attributes in $data (by @Vinai) + * [magento/magento2#26634](https://github.com/magento/magento2/pull/26634) -- Xml fixes for Magento_AdvancedPricingImportExport module (by @sanganinamrata) + * [magento/magento2#26611](https://github.com/magento/magento2/pull/26611) -- #26610 Fix failing CI due to invalid variable handler (by @lbajsarowicz) + * [magento/magento2#26549](https://github.com/magento/magento2/pull/26549) -- [Fedex] covered Model/Source/Generic.php by unit test (by @srsathish92) + * [magento/magento2#26525](https://github.com/magento/magento2/pull/26525) -- Unit test for Magento\Reports\Observer\EventSaver (by @karyna-tsymbal-atwix) + * [magento/magento2#26487](https://github.com/magento/magento2/pull/26487) -- Unit test for Magento\Fedex\Plugin\Block\Tracking\PopupDeliveryDate (by @karyna-tsymbal-atwix) + * [magento/magento2#26439](https://github.com/magento/magento2/pull/26439) -- magento/magento2#: Unit test for \Magento\Bundle\Observer\InitOptionRendererObserver (by @atwixfirster) + * [magento/magento2#26429](https://github.com/magento/magento2/pull/26429) -- magento/magento2#: Unit test for \Magento\Bundle\Observer\AppendUpsellProductsObserver (by @atwixfirster) + * [magento/magento2#26241](https://github.com/magento/magento2/pull/26241) -- #26240: Fixed logic for getting option price index for selected swatch option (by @sergiy-v) + * [magento/magento2#26641](https://github.com/magento/magento2/pull/26641) -- Correct doc url added to README (by @rishatiwari) + * [magento/magento2#26579](https://github.com/magento/magento2/pull/26579) -- Unit test for Magento\Reports\Observer\CheckoutCartAddProductObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26574](https://github.com/magento/magento2/pull/26574) -- Cover Search Term Entity Redirect Works on Store Front by MFTF Test (by @DmitryTsymbal) + * [magento/magento2#26569](https://github.com/magento/magento2/pull/26569) -- 17847 Fixed wrong state title (by @Usik2203) + * [magento/magento2#26568](https://github.com/magento/magento2/pull/26568) -- Action group added for existing test (by @ajithkumar-maragathavel) + * [magento/magento2#26542](https://github.com/magento/magento2/pull/26542) -- Typo Mistake (by @mayankzalavadia) + * [magento/magento2#26533](https://github.com/magento/magento2/pull/26533) -- Github #26532: di:setup:compile fails with anonymous classes (by @joni-jones) + * [magento/magento2#26496](https://github.com/magento/magento2/pull/26496) -- [CurrencySymbol] Fixing the disabled currency inputs (by @eduard13) + * [magento/magento2#26476](https://github.com/magento/magento2/pull/26476) -- magento/magento2#: Remove a redundant call to DB for guest session (by @atwixfirster) + * [magento/magento2#26462](https://github.com/magento/magento2/pull/26462) -- Escape dollar sign for saving content into crontab (by @Erfans) + * [magento/magento2#26451](https://github.com/magento/magento2/pull/26451) -- Add frontend template hints status command (by @WaPoNe) + * [magento/magento2#26430](https://github.com/magento/magento2/pull/26430) -- Unit Test for Magento\Sitemap\Model\Config\Backend\Priority (by @karyna-tsymbal-atwix) + * [magento/magento2#26399](https://github.com/magento/magento2/pull/26399) -- Issue #26332 BeforeOrderPaymentSaveObserver override payment instructions (by @karyna-tsymbal-atwix) + * [magento/magento2#26213](https://github.com/magento/magento2/pull/26213) -- SEO: Do not follow links on filter options (by @paveq) + * [magento/magento2#26007](https://github.com/magento/magento2/pull/26007) -- #25591 & character in SKUs is shown as & in current variations li… (by @KaushikChavda) + * [magento/magento2#25860](https://github.com/magento/magento2/pull/25860) -- Add mass action to invalidate indexes via admin (by @fredden) + * [magento/magento2#25851](https://github.com/magento/magento2/pull/25851) -- Fix SearchResult isCacheable performance (by @wigman) + * [magento/magento2#25742](https://github.com/magento/magento2/pull/25742) -- Http adapter curl missing delete method (by @jimuld) + * [magento/magento2#25324](https://github.com/magento/magento2/pull/25324) -- 13865 safari block cookies breaks javascript scripts (by @raulvOnestic91) + * [magento/magento2#24648](https://github.com/magento/magento2/pull/24648) -- reduce reset data actions on DeploymentConfig (by @georgebabarus) + * [magento/magento2#24485](https://github.com/magento/magento2/pull/24485) -- Fix return type of price currency format method (by @avstudnitz) + * [magento/magento2#26378](https://github.com/magento/magento2/pull/26378) -- 26375 braintree payment address issue (by @chris-pook) + * [magento/magento2#25641](https://github.com/magento/magento2/pull/25641) -- M2C-21768 Validate product quantity on Wishlist update (by @ptylek) + * [magento/magento2#25285](https://github.com/magento/magento2/pull/25285) -- Add lib wrapper for UUID validation. (by @nikolaevas) + * [magento/magento2#26420](https://github.com/magento/magento2/pull/26420) -- #8691: improved language pack inheritance order (by @sergiy-v) + * [magento/magento2#26413](https://github.com/magento/magento2/pull/26413) -- #895 Fix for Media Gallery buttons are shifted to the left (by @diazwatson) + * [magento/magento2#26162](https://github.com/magento/magento2/pull/26162) -- Fixed Issue with tier price 0 when saving product second time (by @ravi-chandra3197) + * [magento/magento2#26623](https://github.com/magento/magento2/pull/26623) -- #26622 - Check quote item for parentItem instead of parentItemId (by @aligent-lturner) + * [magento/magento2#26621](https://github.com/magento/magento2/pull/26621) -- Set of fixes introduced during #CoreReview 31.01.2020 (by @lbajsarowicz) + * [magento/magento2#26546](https://github.com/magento/magento2/pull/26546) -- [fixed My Wish List Product not showing properly between >768px and <… (by @hitesh-wagento) + * [magento/magento2#26423](https://github.com/magento/magento2/pull/26423) -- Update getCustomer method in order class (by @sertlab) + * [magento/magento2#26339](https://github.com/magento/magento2/pull/26339) -- Module xml extra end tag removed (by @tejash-wagento) + * [magento/magento2#24691](https://github.com/magento/magento2/pull/24691) -- Allows additional payment checks in payment method list (by @jensscherbl) + * [magento/magento2#26782](https://github.com/magento/magento2/pull/26782) -- Module_Cms MFTF test improvements (by @ajithkumar-maragathavel) + * [magento/magento2#26781](https://github.com/magento/magento2/pull/26781) -- Code hygeine in bundle option graphql resolver (by @moloughlin) + * [magento/magento2#26770](https://github.com/magento/magento2/pull/26770) -- Unit tests for Magento\Csp\Model\Mode\ConfigManager and Magento\Csp\Observer\Render (by @karyna-tsymbal-atwix) + * [magento/magento2#26764](https://github.com/magento/magento2/pull/26764) -- LoadCssAsync html format fixed for critical css (by @srsathish92) + * [magento/magento2#26714](https://github.com/magento/magento2/pull/26714) -- Deprecated redundant class (by @drpayyne) + * [magento/magento2#26715](https://github.com/magento/magento2/pull/26715) -- Unit test for \Magento\Captcha\Observer\ResetAttemptForBackendObserver and ResetAttemptForFrontendObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26502](https://github.com/magento/magento2/pull/26502) -- {ASI} :- Error message to be cached for grid data storage component (by @konarshankar07) + * [magento/magento2#26279](https://github.com/magento/magento2/pull/26279) -- Fix issue #26276 with clonning quote billing address street (by @yutv) + * [magento/magento2#26246](https://github.com/magento/magento2/pull/26246) -- magento/magento2#26245: Magento does not send an email about a refunded grouped product (by @atwixfirster) + * [magento/magento2#26142](https://github.com/magento/magento2/pull/26142) -- Textarea patch 1 (by @textarea) + * [magento/magento2#25488](https://github.com/magento/magento2/pull/25488) -- Update composer dependency to fix Redis Key Expiery (by @toxix) + * [magento/magento2#25249](https://github.com/magento/magento2/pull/25249) -- upgrade method delete by ids to inject array skus (by @sarron93) + * [magento/magento2#25246](https://github.com/magento/magento2/pull/25246) -- Warning when Search Terms page is opened by clicking option at the footer (by @vishalverma279) + * [magento/magento2#24843](https://github.com/magento/magento2/pull/24843) -- Issue #24842: Unable to delete custom option file in admin order create (by @adrian-martinez-interactiv4) + * [magento/magento2#26820](https://github.com/magento/magento2/pull/26820) -- [Theme] Covered Unit Test for \Magento\Theme\Controller\Result\JsFooterPlugin (by @srsathish92) + * [magento/magento2#26816](https://github.com/magento/magento2/pull/26816) -- Unit Test for \Magento\Directory\Block\Adminhtml\Frontend\Currency\Base (by @karyna-tsymbal-atwix) + * [magento/magento2#26771](https://github.com/magento/magento2/pull/26771) -- Removed unnecessary function argument (by @ajithkumar-maragathavel) + * [magento/magento2#26684](https://github.com/magento/magento2/pull/26684) -- Move additional dependencies from private getters to constructor - Magento_PageCache (by @Bartlomiejsz) + * [magento/magento2#26674](https://github.com/magento/magento2/pull/26674) -- Comment add in translate. (by @pratikhmehta) + * [magento/magento2#26342](https://github.com/magento/magento2/pull/26342) -- Remove extra space before semicolon and remove extra comma in php, phtml and js files (by @tejash-wagento) + * [magento/magento2#25991](https://github.com/magento/magento2/pull/25991) -- Fixed issue when the preview images navigation is triggered by moving the input filed cursor using arrow keys (by @drpayyne) + * [magento/magento2#26857](https://github.com/magento/magento2/pull/26857) -- Issue/26843: Fix es_US Spanish (United States ) Locale is not support… (by @vincent-le89) + * [magento/magento2#26846](https://github.com/magento/magento2/pull/26846) -- magento/magento2#: GraphQL. setPaymentMethodOnCart mutation. Extend list of required parameters for testSetPaymentMethodWithoutRequiredParameters (by @atwixfirster) + * [magento/magento2#26844](https://github.com/magento/magento2/pull/26844) -- Unit Tests for observers from Magento_Reports (by @karyna-tsymbal-atwix) + * [magento/magento2#26835](https://github.com/magento/magento2/pull/26835) -- Unit test for Magento\Downloadable\Model\Sample\DeleteHandler (by @karyna-tsymbal-atwix) + * [magento/magento2#26839](https://github.com/magento/magento2/pull/26839) -- Unit test for \Magento\MediaGallery\Plugin\Wysiwyg\Images\Storage (by @karyna-tsymbal-atwix) + * [magento/magento2#26769](https://github.com/magento/magento2/pull/26769) -- Fix return type in ResetAttemptForFrontendObserver and ResetAttemptForBackendObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26768](https://github.com/magento/magento2/pull/26768) -- Marginal space validation (by @ajithkumar-maragathavel) + * [magento/magento2#26712](https://github.com/magento/magento2/pull/26712) -- Unit test for \Magento\Captcha\Observer\CheckUserForgotPasswordBackendObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26688](https://github.com/magento/magento2/pull/26688) -- Use '===' operator to check if file was written. (by @vovayatsyuk) + * [magento/magento2#26659](https://github.com/magento/magento2/pull/26659) -- [26054-Do not duplicate SEO meta data when duplicating a product] (by @dasharath-wagento) + * [magento/magento2#26398](https://github.com/magento/magento2/pull/26398) -- Move additional dependencies from private getters to constructor - Magento_Captcha (by @Bartlomiejsz) + * [magento/magento2#26317](https://github.com/magento/magento2/pull/26317) -- #26314: fixed logic for updating map price for selected swatch (by @sergiy-v) + * [magento/magento2#24612](https://github.com/magento/magento2/pull/24612) -- Fix for the issue #24547 Magento\Customer\Model\Account\Redirect::setRedirectCookie() not properly working (by @sashas777) + * [magento/magento2#26904](https://github.com/magento/magento2/pull/26904) -- [Layout] Adding 'article' as an additional supported layout tag (by @eduard13) + * [magento/magento2#26899](https://github.com/magento/magento2/pull/26899) -- Unit test for Magento\Vault\Plugin\PaymentVaultAttributesLoad (by @karyna-tsymbal-atwix) + * [magento/magento2#26897](https://github.com/magento/magento2/pull/26897) -- Swatches options: eliminate objects instantiation since only their data needed (by @ilnytskyi) + * [magento/magento2#26894](https://github.com/magento/magento2/pull/26894) -- Unit test for Magento\GiftMessage\Model\Plugin\MergeQuoteItems (by @karyna-tsymbal-atwix) + * [magento/magento2#26878](https://github.com/magento/magento2/pull/26878) -- [NewRelicReporting] Covering the New Relic plugins by Unit Tests (by @eduard13) + * [magento/magento2#26869](https://github.com/magento/magento2/pull/26869) -- Update MockObject class in Widget module (by @hws47a) + * [magento/magento2#26868](https://github.com/magento/magento2/pull/26868) -- Update MockObject class in Wishlist module (by @hws47a) + * [magento/magento2#26863](https://github.com/magento/magento2/pull/26863) -- TierPriceBox toHtml method should be return with string (by @tufahu) + * [magento/magento2#26790](https://github.com/magento/magento2/pull/26790) -- Disabled flat: cast $attributeId as (int) in selects (by @ilnytskyi) + * [magento/magento2#26761](https://github.com/magento/magento2/pull/26761) -- [Analytics] Code refactor & covered unit test in Analytics/ReportXml/QueryFactory (by @srsathish92) + * [magento/magento2#26710](https://github.com/magento/magento2/pull/26710) -- [Customer] Fixing the code style for account edit template and Integration test (by @eduard13) + * [magento/magento2#26701](https://github.com/magento/magento2/pull/26701) -- Date incorrect on pdf invoice issue26675 (by @edenduong) + * [magento/magento2#26650](https://github.com/magento/magento2/pull/26650) -- issue-#25675 Added fix for #25675 issue to the 2.4 Magento version (by @molneek) + * [magento/magento2#26617](https://github.com/magento/magento2/pull/26617) -- Unit Test for Magento\LayeredNavigation\Observer\Edit\Tab\Front\ProductAttributeFormBuildFrontTabObserver (by @karyna-tsymbal-atwix) + * [magento/magento2#26584](https://github.com/magento/magento2/pull/26584) -- #26583 Tier pricing save percent showing logic updated in product detail page (by @srsathish92) + * [magento/magento2#26523](https://github.com/magento/magento2/pull/26523) -- [#25963] Fixed grids export: option labels are taken from grid filters and columns now. (by @novikor) + * [magento/magento2#26418](https://github.com/magento/magento2/pull/26418) -- [Fixed Compare Products section not showing in mobile view under 767px] (by @hitesh-wagento) + * [magento/magento2#25806](https://github.com/magento/magento2/pull/25806) -- Reflection: Fix null as first return type (by @Parakoopa) + * [magento/magento2#25626](https://github.com/magento/magento2/pull/25626) -- Fix translation retrieval (by @brosenberger) + * [magento/magento2#25426](https://github.com/magento/magento2/pull/25426) -- Ui: log exception correctly (by @bchatard) + * [magento/magento2#25417](https://github.com/magento/magento2/pull/25417) -- README > Improving the apresentation (by @rafaelstz) + * [magento/magento2#25321](https://github.com/magento/magento2/pull/25321) -- Changing input phone type to tel in Contact, Error Report and Customer widget pages (by @rafaelstz) + * [magento/magento2#24976](https://github.com/magento/magento2/pull/24976) -- Fix doc block for $queueIterator Magento\Framework\MessageQueue\Topology\Config (by @UncleTioma) + * [magento/magento2#22296](https://github.com/magento/magento2/pull/22296) -- Fix #14958 - remove sales sequence data on store view delete (by @Bartlomiejsz) + * [magento/magento2#26833](https://github.com/magento/magento2/pull/26833) -- magento/magento2#: GraphQL. MergeCarts mutation. Add additional API-functional test cases (by @atwixfirster) + * [magento/magento2#26608](https://github.com/magento/magento2/pull/26608) -- HOTFIX: #26607 Fix failing CI due to Functional Tests (by @lbajsarowicz) + * [magento/magento2#26772](https://github.com/magento/magento2/pull/26772) -- Clean up 'isset' coding style (by @GraysonChiang) + * [magento/magento2#25858](https://github.com/magento/magento2/pull/25858) -- FIX #25856 / Group Ordered Products report by SKU (by @lbajsarowicz) + * [magento/magento2#23570](https://github.com/magento/magento2/pull/23570) -- Improve: [UrlRewrite] Move grid implementation to ui components (by @Den4ik) + * [magento/magento2#26995](https://github.com/magento/magento2/pull/26995) -- Fix typo in description node. (by @BorisovskiP) + * [magento/magento2#26982](https://github.com/magento/magento2/pull/26982) -- Remove app/functions.php (by @Bartlomiejsz) + * [magento/magento2#26974](https://github.com/magento/magento2/pull/26974) -- Fix: #26973 Fatal error when Product Image size is not defined (by @lbajsarowicz) + * [magento/magento2#26947](https://github.com/magento/magento2/pull/26947) -- Unit test for AssignCouponDataAfterOrderCustomerAssignObserver (by @mmezhensky) + * [magento/magento2#26944](https://github.com/magento/magento2/pull/26944) -- Unit test for CatalogProductCompareClearObserver (by @mmezhensky) + * [magento/magento2#26932](https://github.com/magento/magento2/pull/26932) -- Fix #26917 Tax rate zip/post range check box alignment issue (by @srsathish92) + * [magento/magento2#26928](https://github.com/magento/magento2/pull/26928) -- Module_Cms MFTF test improvements (by @nandhini-nagaraj) + * [magento/magento2#26916](https://github.com/magento/magento2/pull/26916) -- Move cache cleanup operation on state modification operation (by @kandy) + * [magento/magento2#26912](https://github.com/magento/magento2/pull/26912) -- Unit test for ProductProcessUrlRewriteRemovingObserver (by @mmezhensky) + * [magento/magento2#26862](https://github.com/magento/magento2/pull/26862) -- Removed disabled products from low stock report grid (by @Mohamed-Asar) + * [magento/magento2#26821](https://github.com/magento/magento2/pull/26821) -- Reduce requirements for parameter in catalog product type factory (by @hws47a) + * [magento/magento2#26755](https://github.com/magento/magento2/pull/26755) -- Fix wrong type of argument appendSummaryFieldsToCollection (by @Usik2203) + * [magento/magento2#26697](https://github.com/magento/magento2/pull/26697) -- Update the product model custom option methods PHPdoc (by @hws47a) + * [magento/magento2#26586](https://github.com/magento/magento2/pull/26586) -- Improve exception message (by @oroskodias) + * [magento/magento2#26230](https://github.com/magento/magento2/pull/26230) -- Activated "Pending Reviews" menu item when merchant opens 'Pending Reviews' section (by @rav-redchamps) + * [magento/magento2#26090](https://github.com/magento/magento2/pull/26090) -- Fixed issue 25910 choose drop down not close when open another (by @Usik2203) + * [magento/magento2#25895](https://github.com/magento/magento2/pull/25895) -- Change tag name (by @AndreyChorniy) + * [magento/magento2#25349](https://github.com/magento/magento2/pull/25349) -- Fixed issue when escape key is pressed to close prompt (by @konarshankar07) + * [magento/magento2#25161](https://github.com/magento/magento2/pull/25161) -- fixed confusing grammar in the backend formatDate() function (by @princefishthrower) + * [magento/magento2#26979](https://github.com/magento/magento2/pull/26979) -- #26800 Fixed Undefined variable in ProductLink/Management (by @srsathish92) + * [magento/magento2#26842](https://github.com/magento/magento2/pull/26842) -- Unit test for Magento\Catalog\Observer\SetSpecialPriceStartDate has added (by @mmezhensky) + * [magento/magento2#26615](https://github.com/magento/magento2/pull/26615) -- Add missing annotations to MFTF tests (by @sta1r) + * [magento/magento2#25828](https://github.com/magento/magento2/pull/25828) -- MFTF: Extract Action Groups to separate files - magento/module-customer (by @lbajsarowicz) + * [magento/magento2#25311](https://github.com/magento/magento2/pull/25311) -- Add afterGetList method in CustomerRepository plugin to retrieve is_s… (by @enriquei4) + * [magento/magento2#27054](https://github.com/magento/magento2/pull/27054) -- Cleanup ObjectManager usage - Magento_Authorization (by @Bartlomiejsz) + * [magento/magento2#27048](https://github.com/magento/magento2/pull/27048) -- #27044 Integration Test to cover `$storeId` on Category Repository `get()` (by @lbajsarowicz) + * [magento/magento2#27041](https://github.com/magento/magento2/pull/27041) -- FIX: responsiveness for images (by @GrimLink) + * [magento/magento2#27021](https://github.com/magento/magento2/pull/27021) -- Unit test for \Magento\MediaGallery\Plugin\Product\Gallery\Processor (by @karyna-tsymbal-atwix) + * [magento/magento2#27010](https://github.com/magento/magento2/pull/27010) -- Unit test for UpdateElementTypesObserver (by @mmezhensky) + * [magento/magento2#26779](https://github.com/magento/magento2/pull/26779) -- Fix failure for missing product on Storefront (by @lbajsarowicz) + * [magento/magento2#26765](https://github.com/magento/magento2/pull/26765) -- Fix #17933 - Bank Transfer Payment Instructions switch back to default (by @Bartlomiejsz) + * [magento/magento2#26548](https://github.com/magento/magento2/pull/26548) -- issue/26384 Fix store switcher when using different base url on stores (by @TobiasCodeNull) + * [magento/magento2#26329](https://github.com/magento/magento2/pull/26329) -- MFTF: Replace invalid ActionGroup for AdminLogin (by @lbajsarowicz) + * [magento/magento2#25359](https://github.com/magento/magento2/pull/25359) -- Fix #25243 (by @korostii) + * [magento/magento2#24003](https://github.com/magento/magento2/pull/24003) -- Fixes less compilation problems in the Magento/luma theme (by @hostep) + * [magento/magento2#27114](https://github.com/magento/magento2/pull/27114) -- magento/magento2#: Remove a redundant PHP5 directives from .htaccess (by @atwixfirster) + * [magento/magento2#27057](https://github.com/magento/magento2/pull/27057) -- Removed redundant method _beforeToHtml (by @Usik2203) + * [magento/magento2#27033](https://github.com/magento/magento2/pull/27033) -- Catalog image lazy load (by @tdgroot) + * [magento/magento2#26907](https://github.com/magento/magento2/pull/26907) -- Open separate page when click 'View' in CMS pages(Grid) (by @dominicfernando) + * [magento/magento2#26619](https://github.com/magento/magento2/pull/26619) -- Cover CartTotalRepositoryPlugin by unit test and correct docblock (by @mrtuvn) + * [magento/magento2#26778](https://github.com/magento/magento2/pull/26778) -- Eliminate the need for inheritance for action controllers. (by @lbajsarowicz) + * [magento/magento2#26990](https://github.com/magento/magento2/pull/26990) -- #26989 MFTF: Use for reindex (by @lbajsarowicz) + * [magento/magento2#27196](https://github.com/magento/magento2/pull/27196) -- Remove @author annotation from Magento framework (by @diazwatson) + * [magento/magento2#27149](https://github.com/magento/magento2/pull/27149) -- 27027 added date format adjustment for 'validate-dob' rule (by @sergiy-v) + * [magento/magento2#27138](https://github.com/magento/magento2/pull/27138) -- Removed unnecessary tabindex property (by @drpayyne) + * [magento/magento2#27131](https://github.com/magento/magento2/pull/27131) -- 26758 improved cms page custom layout update logic (by @sergiy-v) + * [magento/magento2#27084](https://github.com/magento/magento2/pull/27084) -- Cleanup ObjectManager usage - Magento_CacheInvalidate (by @Bartlomiejsz) + * [magento/magento2#27083](https://github.com/magento/magento2/pull/27083) -- Cleanup ObjectManager usage - Magento_AsynchronousOperations (by @Bartlomiejsz) + * [magento/magento2#27082](https://github.com/magento/magento2/pull/27082) -- Cleanup ObjectManager usage - Magento_Analytics (by @Bartlomiejsz) + * [magento/magento2#27080](https://github.com/magento/magento2/pull/27080) -- Cleanup ObjectManager usage - Magento_EncryptionKey (by @Bartlomiejsz) + * [magento/magento2#27029](https://github.com/magento/magento2/pull/27029) -- #26847: Added 'enterKey' event handler to prompt widget (by @sergiy-v) + * [magento/magento2#27026](https://github.com/magento/magento2/pull/27026) -- Issue 27009: Fix error fire on catch when create new theme (by @vincent-le89) + * [magento/magento2#27014](https://github.com/magento/magento2/pull/27014) -- Fix #26992 Add new rating is active checkbox alignment issue (by @srsathish92) + * [magento/magento2#26964](https://github.com/magento/magento2/pull/26964) -- Cleanup ObjectManager usage - Magento_Elasticsearch (by @Bartlomiejsz) + * [magento/magento2#26939](https://github.com/magento/magento2/pull/26939) -- ObjectManager cleanup - Remove usage from AdminNotification module (by @ihor-sviziev) + * [magento/magento2#26902](https://github.com/magento/magento2/pull/26902) -- Fix #20309 - URL Rewrites redirect loop (by @Bartlomiejsz) + * [magento/magento2#26649](https://github.com/magento/magento2/pull/26649) -- table bottom color different then thead and tbody border color (by @tejash-wagento) + * [magento/magento2#26642](https://github.com/magento/magento2/pull/26642) -- MAG-251090-26590: Fixed Customer registration multiple form submit (by @princeCB) + * [magento/magento2#26563](https://github.com/magento/magento2/pull/26563) -- magento/magento2#: Test Coverage. API functional tests. removeItemFromCart (by @atwixfirster) + * [magento/magento2#25454](https://github.com/magento/magento2/pull/25454) -- TinyMCE4 hard to input double byte characters on chrome (by @HirokazuNishi) + * [magento/magento2#24878](https://github.com/magento/magento2/pull/24878) -- Create missing directories in imageuploader tree if they don't alread… (by @hostep) + * [magento/magento2#24743](https://github.com/magento/magento2/pull/24743) -- fix issue 24735 (by @dmdanilchenko) + * [magento/magento2#23742](https://github.com/magento/magento2/pull/23742) -- Add Header (h1 - h6) tags to layout xml htmlTags Allowed types (by @furan917) + * [magento/magento2#22442](https://github.com/magento/magento2/pull/22442) -- Add support for char element to dto factory (by @wardcapp) + * [magento/magento2#27172](https://github.com/magento/magento2/pull/27172) -- magento/magento2#14086: Guest cart API ignoring cartId in url for some methods (by @engcom-Charlie) + * [magento/magento2#27179](https://github.com/magento/magento2/pull/27179) -- improve Magento\Catalog\Model\ImageUploader error handler (by @fsw) + * [magento/magento2#27145](https://github.com/magento/magento2/pull/27145) -- Cleanup ObjectManager usage - Magento_WebapiAsync (by @Bartlomiejsz) + * [magento/magento2#26959](https://github.com/magento/magento2/pull/26959) -- Correctly escape custom product image attributes (by @alexander-aleman) + * [magento/magento2#26506](https://github.com/magento/magento2/pull/26506) -- #26499 Always transliterate product url key (by @DanieliMi) + * [magento/magento2#25722](https://github.com/magento/magento2/pull/25722) -- Magento#25669: fixed issue "health_check.php fails if any database cache engine configured" (by @andrewbess) + * [magento/magento2#27284](https://github.com/magento/magento2/pull/27284) -- Fix static test failures for class annotaions (by @ihor-sviziev) + * [magento/magento2#27281](https://github.com/magento/magento2/pull/27281) -- TYPO: Fix annoying typo in Quantity word (by @lbajsarowicz) + * [magento/magento2#27277](https://github.com/magento/magento2/pull/27277) -- MFTF: Rename and rewrite Test that fake expired session (by @lbajsarowicz) + * [magento/magento2#27274](https://github.com/magento/magento2/pull/27274) -- MFTF: Create Account tests (Success & Failure) with `extend` (by @lbajsarowicz) + * [magento/magento2#27261](https://github.com/magento/magento2/pull/27261) -- 20472 added product list price modifier (by @sergiy-v) + * [magento/magento2#27249](https://github.com/magento/magento2/pull/27249) -- Update Frontend Development Workflow type's comment to be clearer (by @navarr) + * [magento/magento2#26784](https://github.com/magento/magento2/pull/26784) -- [Forward Port PR-14344] Fix generating product URL rewrites for anchor categories (by @hostep) + * [magento/magento2#26746](https://github.com/magento/magento2/pull/26746) -- In System/Export controlers use MessageManager instead of throwing exceptions (by @pmarki) + * [magento/magento2#26348](https://github.com/magento/magento2/pull/26348) -- Fixed #26345 Reorder in Admin panel leads to report page in case of changed product custom option max characters (by @cedmudit) + * [magento/magento2#27187](https://github.com/magento/magento2/pull/27187) -- 26117: "Current user does not have an active cart" even when he actually has one (by @engcom-Charlie) + * [magento/magento2#27170](https://github.com/magento/magento2/pull/27170) -- 26825 add all image roles for first product entity (by @sergiy-v) + * [magento/magento2#25733](https://github.com/magento/magento2/pull/25733) -- Resolve Mass Delete Widget should have "Confirmation Modal" (by @edenduong) + * [magento/magento2#27118](https://github.com/magento/magento2/pull/27118) -- FIX #27117 Invalid functional Test names (by @lbajsarowicz) + * [magento/magento2#27266](https://github.com/magento/magento2/pull/27266) -- MFTF: Enable Persistent Shopping Cart. Assert Options (by @DmitryTsymbal) + * [magento/magento2#27255](https://github.com/magento/magento2/pull/27255) -- MFTF: Replace fragile test `AdminLoginTest` with `AdminLoginSuccessfulTest` (by @lbajsarowicz) + * [magento/magento2#27165](https://github.com/magento/magento2/pull/27165) -- [feature] Display category filter item in layered navigation based on the system configuration from admin area (by @vasilii-b) + * [magento/magento2#27015](https://github.com/magento/magento2/pull/27015) -- MC-26683: Removed get errors of cart allowing to add product to cart (by @AleksLi) + * [magento/magento2#26987](https://github.com/magento/magento2/pull/26987) -- Remove unused requirejs alias defined (by @mrtuvn) + * [magento/magento2#26560](https://github.com/magento/magento2/pull/26560) -- #26473: Improved logic for product image updating for configurable products (by @sergiy-v) + * [magento/magento2#25297](https://github.com/magento/magento2/pull/25297) -- Add 'schedule status' column to admin indexer grid (by @fredden) + * [magento/magento2#24479](https://github.com/magento/magento2/pull/24479) -- 22251 admin order email is now required 1 (by @solwininfotech) + * [magento/magento2#27273](https://github.com/magento/magento2/pull/27273) -- MFTF: Test isolation, consistent naming (backwards-compatible) (by @lbajsarowicz) + * [magento/magento2#27237](https://github.com/magento/magento2/pull/27237) -- magento/magento2#26749: Saving CMS Page Title from REST web API makes content empty (by @engcom-Charlie) + * [magento/magento2#27215](https://github.com/magento/magento2/pull/27215) -- Cleanup ObjectManager usage - Magento_Translation (by @Bartlomiejsz) + * [magento/magento2#27191](https://github.com/magento/magento2/pull/27191) -- 26827 Added improvements to product attribute repository (save method) (by @sergiy-v) + * [magento/magento2#27125](https://github.com/magento/magento2/pull/27125) -- #27124: Update wishlist image logic to match logic on wishlist page (by @mtbottens) + * [magento/magento2#26015](https://github.com/magento/magento2/pull/26015) -- Remove media gallery assets metadata when a directory removed (by @rogyar) + * [magento/magento2#25734](https://github.com/magento/magento2/pull/25734) -- Experius 2.3 patch catalog flat (by @lewisvoncken) + * [magento/magento2#23191](https://github.com/magento/magento2/pull/23191) -- Refactor addlinks to own class take 3 (follows #21658) (by @amenk) + * [magento/magento2#27336](https://github.com/magento/magento2/pull/27336) -- fixed My account Address Book Additional Address Entries table issue #27335 (by @abrarpathan19) + * [magento/magento2#27304](https://github.com/magento/magento2/pull/27304) -- FIX #14080 Added improvements to Category repository (save method) (by @sergiy-v) + * [magento/magento2#27298](https://github.com/magento/magento2/pull/27298) -- Implement ActionInterface for `cms/page/view` (by @lbajsarowicz) + * [magento/magento2#27292](https://github.com/magento/magento2/pull/27292) -- Magento_Bundle / Remove `cache:flush` and extract Tests to separate files (by @lbajsarowicz) + * [magento/magento2#27263](https://github.com/magento/magento2/pull/27263) -- #26708 Fix: ORDER BY has two similar conditions in the SQL query (by @vasilii-b) + * [magento/magento2#27214](https://github.com/magento/magento2/pull/27214) -- Mark AbstractAccount as DEPRECATED for Magento_Customer controllers (by @lbajsarowicz) + * [magento/magento2#27116](https://github.com/magento/magento2/pull/27116) -- Add Italy States (by @WaPoNe) + * [magento/magento2#26748](https://github.com/magento/magento2/pull/26748) -- magento#26745 add method setAdditionalInformation to OrderPaymentInte… (by @antoninobonumore) + * [magento/magento2#26923](https://github.com/magento/magento2/pull/26923) -- Improve dashboard charts - migrate to chart.js (by @Bartlomiejsz) + * [magento/magento2#27390](https://github.com/magento/magento2/pull/27390) -- magento/magento2: fixes PHPDocs for module Magento_reports (by @andrewbess) + * [magento/magento2#27375](https://github.com/magento/magento2/pull/27375) -- Updating link to Adobe CLA in contributing.md (by @filmaj) + * [magento/magento2#27353](https://github.com/magento/magento2/pull/27353) -- Add xml declaration for catalog_widget_product_list.xml file (by @Usik2203) + * [magento/magento2#27334](https://github.com/magento/magento2/pull/27334) -- MFTF: Customer Subscribes To Newsletter Subscription On StoreFront (by @DmitryTsymbal) + * [magento/magento2#27319](https://github.com/magento/magento2/pull/27319) -- Cleanup ObjectManager usage - Magento_Catalog ViewModel,Plugin (by @Bartlomiejsz) + * [magento/magento2#27307](https://github.com/magento/magento2/pull/27307) -- magento/magento2: Fixes for the schema cache.xsd (by @andrewbess) + * [magento/magento2#27276](https://github.com/magento/magento2/pull/27276) -- Add "Admin" prefix to Test and ActionGroup (by @lbajsarowicz) + * [magento/magento2#27000](https://github.com/magento/magento2/pull/27000) -- MFTF FIX: Remove Customer by e-mail does not filter by e-mail (by @lbajsarowicz) + * [magento/magento2#26538](https://github.com/magento/magento2/pull/26538) -- Refactor datetime class (by @Tjitse-E) + * [magento/magento2#25664](https://github.com/magento/magento2/pull/25664) -- magento/magento2#25540: Products are not displaying infront end after updating product via importing CSV. (by @p-bystritsky) + * [magento/magento2#22011](https://github.com/magento/magento2/pull/22011) -- magento/magento2#22010: Updates AbstractExtensibleObject and AbstractExtensibleModel annotations (by @atwixfirster) + * [magento/magento2#27378](https://github.com/magento/magento2/pull/27378) -- MFTF: Refactor `amOnPage` for Admin product edit page (by @lbajsarowicz) + * [magento/magento2#26055](https://github.com/magento/magento2/pull/26055) -- [Fixed] - HTML Validation issue Replace Attribute with data-* attribute (by @niravkrish) + * [magento/magento2#27412](https://github.com/magento/magento2/pull/27412) -- Added improvements to category url key validation logic (by @sergiy-v) + * [magento/magento2#27393](https://github.com/magento/magento2/pull/27393) -- Implement ActionInterface for /robots/index/index (by @Bartlomiejsz) + * [magento/magento2#27385](https://github.com/magento/magento2/pull/27385) -- Cleanup ObjectManager usage - Magento_SendFriend (by @Bartlomiejsz) + * [magento/magento2#27384](https://github.com/magento/magento2/pull/27384) -- Cleanup ObjectManager usage - Magento_Sitemap (by @Bartlomiejsz) + * [magento/magento2#27383](https://github.com/magento/magento2/pull/27383) -- #27370 Internet explorer issue:Default billing/shipping address not showing (by @vasilii-b) + * [magento/magento2#27381](https://github.com/magento/magento2/pull/27381) -- Implement ActionInterface for /captcha/refresh (by @lbajsarowicz) + * [magento/magento2#27360](https://github.com/magento/magento2/pull/27360) -- Move JS module initialization to separate tasks (by @krzksz) + * [magento/magento2#27088](https://github.com/magento/magento2/pull/27088) -- Fix Report date doesn't matching in configuration setting (by @Priya-V-Panchal) + * [magento/magento2#22837](https://github.com/magento/magento2/pull/22837) -- Short-term admin accounts #22833 (by @lfolco) + * [magento/magento2#26075](https://github.com/magento/magento2/pull/26075) -- Fix #6310 - Changing products 'this item has weight' using 'Update Attributes' is not possible (by @Bartlomiejsz) + * [magento/magento2#27388](https://github.com/magento/magento2/pull/27388) -- {ASI} :- Image size is not passed to image-uploader when inserting an image from new media gallery (by @konarshankar07) + * [magento/magento2#26999](https://github.com/magento/magento2/pull/26999) -- Fixed URL Rewrite addition/removal on product website add/remove (by @gwharton) + * [magento/magento2#27371](https://github.com/magento/magento2/pull/27371) -- [Admin] Do not allow HTML tags for the Product Attribute labels on save (by @vasilii-b) + * [magento/magento2#27509](https://github.com/magento/magento2/pull/27509) -- [MFTF] fixed test `AdminLoginWithRestrictPermissionTest` (by @engcom-Charlie) + * [magento/magento2#27462](https://github.com/magento/magento2/pull/27462) -- Implement ActionInterface for /search/term/popular (by @Bartlomiejsz) + * [magento/magento2#27427](https://github.com/magento/magento2/pull/27427) -- Implement ActionInterface for /swagger/ (by @lbajsarowicz) + * [magento/magento2#27425](https://github.com/magento/magento2/pull/27425) -- Implement ActionInterface for /version/ (by @lbajsarowicz) + * [magento/magento2#27413](https://github.com/magento/magento2/pull/27413) -- Add follow symlinks to support linked folders (by @Nazar65) + * [magento/magento2#27365](https://github.com/magento/magento2/pull/27365) -- Fix issue 16315: Product save with onthefly index ignores website assignments (by @tna274) + * [magento/magento2#27257](https://github.com/magento/magento2/pull/27257) -- Save Asynchronous Operations with one Batch improvement (by @nuzil) + * [magento/magento2#26763](https://github.com/magento/magento2/pull/26763) -- fix: prevent undefined index error - closes #26762 (by @DanielRuf) + * [magento/magento2#26736](https://github.com/magento/magento2/pull/26736) -- {ASI} : SortBy component added (by @konarshankar07) + * [magento/magento2#26618](https://github.com/magento/magento2/pull/26618) -- Correct docblock CartTotalRepository get method (by @mrtuvn) + * [magento/magento2#26417](https://github.com/magento/magento2/pull/26417) -- translate.js Not shows empty values (by @ilnytskyi) + * [magento/magento2#27493](https://github.com/magento/magento2/pull/27493) -- Fix the minicart items actions alignment for tablet and desktop devices (by @vasilii-b) + * [magento/magento2#27492](https://github.com/magento/magento2/pull/27492) -- Fixed tests for Magento\Framework\Stdlib\DateTime\DateTime (by @andrewbess) + * [magento/magento2#27399](https://github.com/magento/magento2/pull/27399) -- Fixed the wrong behavior for a prompt modal when a user clicks on the modal overlay (by @serhiyzhovnir) + * [magento/magento2#26397](https://github.com/magento/magento2/pull/26397) -- Cleanup ObjectManager usage - Magento_Bundle (by @Bartlomiejsz) + * [magento/magento2#26100](https://github.com/magento/magento2/pull/26100) -- Fixed 24990: link doesn't redirect to dashboard (by @Usik2203) + * [magento/magento2#27545](https://github.com/magento/magento2/pull/27545) -- Fix XML Schema Location (by @sprankhub) + * [magento/magento2#27544](https://github.com/magento/magento2/pull/27544) -- Fix incorrect alignment element in login container theme blank (by @mrtuvn) + * [magento/magento2#27526](https://github.com/magento/magento2/pull/27526) -- [MFTF] using StorefrontOpenHomePageActionGroup for navigation to Home Page (by @Usik2203) + * [magento/magento2#27521](https://github.com/magento/magento2/pull/27521) -- PhpUnit 8 Migration - AdminNotification (by @ihor-sviziev) + * [magento/magento2#27497](https://github.com/magento/magento2/pull/27497) -- [bugfix] The store logo is missing when using the Magento_blank theme (by @vasilii-b) + * [magento/magento2#27495](https://github.com/magento/magento2/pull/27495) -- Make the header switcher styles more flexible (by @vasilii-b) + * [magento/magento2#27463](https://github.com/magento/magento2/pull/27463) -- Implement ActionInterface for /checkout/sidebar/removeItem (by @Bartlomiejsz) + * [magento/magento2#27295](https://github.com/magento/magento2/pull/27295) -- Fix the error that is wrong link title of a downloadable product when enabling "Use Default Value" (by @tna274) + * [magento/magento2#26900](https://github.com/magento/magento2/pull/26900) -- Removed references to '%context%' (dead code) (by @markshust) + * [magento/magento2#26801](https://github.com/magento/magento2/pull/26801) -- Prevent resizing an image if it was already resized before (by @hostep) + * [magento/magento2#27519](https://github.com/magento/magento2/pull/27519) -- PhpUnit 8 Migration - Framework & AdminAnalytics (by @ihor-sviziev) + * [magento/magento2#27322](https://github.com/magento/magento2/pull/27322) -- MFTF: Add ` - - Magento - -

-

-

+ + Magento Commerce + +
+
Open Source Helpers diff --git a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml b/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml deleted file mode 100644 index e02c34fd8868e..0000000000000 --- a/app/code/Magento/AdminAnalytics/Test/Mftf/Test/TrackingScriptTest.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - <description value="Checks to see if the tracking script is in the dom of admin and if setting is turned to no it checks if the tracking script in the dom was removed"/> - <severity value="CRITICAL"/> - <testCaseId value="MC-18192"/> - <group value="backend"/> - <group value="login"/> - </annotations> - - <!-- Logging in Magento admin --> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - </test> -</tests> \ No newline at end of file diff --git a/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml b/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..e937a3e18148a --- /dev/null +++ b/app/code/Magento/AdminAnalytics/etc/csp_whitelist.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="adobedtm" type="host">assets.adobedtm.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml index 4b1f971670184..6d56cd4452a91 100644 --- a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/notification.phtml @@ -4,13 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php +$isAnaliticsVisible = $block->getNotification()->isAnalyticsVisible() ? 1 : 0; +$isReleaseVisible = $block->getNotification()->isReleaseVisible() ? 1 : 0; +$scriptString = <<<script define('analyticsPopupConfig', function () { return { - analyticsVisible: <?= $block->getNotification()->isAnalyticsVisible() ? 1 : 0; ?>, - releaseVisible: <?= $block->getNotification()->isReleaseVisible() ? 1 : 0; ?>, + analyticsVisible: {$isAnaliticsVisible}, + releaseVisible: {$isReleaseVisible}, } }); -</script> +script; +?> + +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml index 0ea5c753c9337..bfe58de1eac5f 100644 --- a/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml +++ b/app/code/Magento/AdminAnalytics/view/adminhtml/templates/tracking.phtml @@ -3,13 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script src="<?= $block->escapeUrl($block->getTrackingUrl()) ?>" async></script> -<script> +<?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + [ + 'src' => $block->getTrackingUrl(), + 'async' => true, + ], + ' ', + false +) ?> + +<?php $scriptString = ' var adminAnalyticsMetadata = { - "version": "<?= $block->escapeJs($block->getMetadata()->getMagentoVersion()) ?>", - "user": "<?= $block->escapeJs($block->getMetadata()->getCurrentUser()) ?>", - "mode": "<?= $block->escapeJs($block->getMetadata()->getMode()) ?>" + "version": "' . $block->escapeJs($block->getMetadata()->getMagentoVersion()) . '", + "user": "' . $block->escapeJs($block->getMetadata()->getCurrentUser()) . '", + "mode": "' . $block->escapeJs($block->getMetadata()->getMode()) . '" }; -</script> +'; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/AdminNotification/Block/System/Messages.php b/app/code/Magento/AdminNotification/Block/System/Messages.php index c9b3a0b8844cc..c99a71a51e6ea 100644 --- a/app/code/Magento/AdminNotification/Block/System/Messages.php +++ b/app/code/Magento/AdminNotification/Block/System/Messages.php @@ -26,7 +26,7 @@ class Messages extends Template /** * @var JsonDataHelper - * @deprecated + * @deprecated 100.3.0 */ protected $jsonHelper; diff --git a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php index d58a7ec31f77d..f3d3cd64ddc64 100644 --- a/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php +++ b/app/code/Magento/AdminNotification/Controller/Adminhtml/System/Message/ListAction.php @@ -17,7 +17,7 @@ class ListAction extends \Magento\Backend\App\AbstractAction /** * @var \Magento\Framework\Json\Helper\Data - * @deprecated + * @deprecated 100.3.0 */ protected $jsonHelper; diff --git a/app/code/Magento/AdminNotification/etc/csp_whitelist.xml b/app/code/Magento/AdminNotification/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..c3327a716947b --- /dev/null +++ b/app/code/Magento/AdminNotification/etc/csp_whitelist.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="img-src"> + <values> + <value id="commerce_widgets" type="host">widgets.magentocommerce.com</value> + </values> + </policy> + </policies> +</csp_whitelist> + diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml index b4f19bda36cbf..f2e8e96fa2585 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/notification/window.phtml @@ -6,10 +6,10 @@ /** * @see \Magento\AdminNotification\Block\Window + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <ul class="message-system-list" - style="display: none;" data-mage-init='{ "Magento_Ui/js/modal/modal": { "autoOpen": true, @@ -25,3 +25,4 @@ </a> </li> </ul> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '.message-system-list'); ?> diff --git a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml index 494e60865623b..2217d441d96ad 100644 --- a/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml +++ b/app/code/Magento/AdminNotification/view/adminhtml/templates/system/messages/popup.phtml @@ -5,18 +5,20 @@ */ /** @var $block \Magento\AdminNotification\Block\System\Messages\UnreadMessagePopup */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div style="display:none" id="system_messages_list" data-role="system_messages_list" +<div id="system_messages_list" data-role="system_messages_list" title="<?= $block->escapeHtmlAttr($block->getPopupTitle()) ?>"> <ul class="message-system-list messages"> - <?php foreach ($block->getUnreadMessages() as $message) : ?> + <?php foreach ($block->getUnreadMessages() as $message): ?> <li class="message message-warning <?= $block->escapeHtmlAttr($block->getItemClass($message)) ?>"> <?= $block->escapeHtml($message->getText()) ?> </li> <?php endforeach;?> </ul> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#system_messages_list'); ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php index 39009e5c7b4e3..27e2713995653 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Export/AdvancedPricing.php @@ -381,7 +381,7 @@ private function prepareExportData( * @param array $exportData * @return array * @SuppressWarnings(PHPMD.UnusedLocalVariable) - * @deprecated + * @deprecated 100.3.0 * @see prepareExportData */ protected function correctExportData($exportData) @@ -510,7 +510,7 @@ private function fetchTierPrices(array $productIds): array * @return array|bool * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @deprecated + * @deprecated 100.3.0 * @see fetchTierPrices */ protected function getTierPrices(array $listSku, $table) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php index b1f99bb1fc05f..2ad96cfeab1d9 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPrice.php @@ -3,15 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; +use Magento\CatalogImportExport\Model\Import\Product; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product\StoreResolver; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractPrice; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; -class TierPrice extends \Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractPrice +class TierPrice extends AbstractPrice { /** - * @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver + * @var StoreResolver */ protected $storeResolver; @@ -27,21 +37,26 @@ class TierPrice extends \Magento\CatalogImportExport\Model\Import\Product\Valida ]; /** - * @param \Magento\Customer\Api\GroupRepositoryInterface $groupRepository - * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder - * @param \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver + * @param GroupRepositoryInterface $groupRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param StoreResolver $storeResolver */ public function __construct( - \Magento\Customer\Api\GroupRepositoryInterface $groupRepository, - \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, - \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver + GroupRepositoryInterface $groupRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + StoreResolver $storeResolver ) { $this->storeResolver = $storeResolver; parent::__construct($groupRepository, $searchCriteriaBuilder); } /** - * {@inheritdoc} + * Initialize method + * + * @param Product $context + * + * @return RowValidatorInterface|AbstractImportValidator|void + * @throws LocalizedException */ public function init($context) { @@ -52,7 +67,10 @@ public function init($context) } /** + * Add decimal error + * * @param string $attribute + * * @return void */ protected function addDecimalError($attribute) @@ -83,12 +101,12 @@ public function getCustomerGroups() } /** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * Validation * * @param mixed $value * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ public function isValid($value) { @@ -133,6 +151,7 @@ public function isValid($value) * Check if at list one value and length are valid * * @param array $value + * * @return bool */ protected function isValidValueAndLength(array $value) @@ -150,6 +169,7 @@ protected function isValidValueAndLength(array $value) * Check if value has empty columns * * @param array $value + * * @return bool */ protected function hasEmptyColumns(array $value) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php index 6aa59e6227a05..71b5271a90fa2 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/TierPriceType.php @@ -4,28 +4,24 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; /** * Class TierPriceType validates tier price type. */ -class TierPriceType extends \Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator +class TierPriceType extends AbstractImportValidator { - /** - * {@inheritdoc} - */ - public function init($context) - { - return parent::init($context); - } - /** * Validate tier price type. * * @param array $value + * * @return bool */ public function isValid($value) diff --git a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php index 0f3f8b3389c7d..93c63dcbcab28 100644 --- a/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php +++ b/app/code/Magento/AdvancedPricingImportExport/Model/Import/AdvancedPricing/Validator/Website.php @@ -3,49 +3,47 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing\Validator; use Magento\AdvancedPricingImportExport\Model\Import\AdvancedPricing; -use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; use Magento\CatalogImportExport\Model\Import\Product\RowValidatorInterface; +use Magento\CatalogImportExport\Model\Import\Product\StoreResolver; +use Magento\CatalogImportExport\Model\Import\Product\Validator\AbstractImportValidator; +use Magento\Store\Model\Website as WebsiteModel; class Website extends AbstractImportValidator implements RowValidatorInterface { /** - * @var \Magento\CatalogImportExport\Model\Import\Product\StoreResolver + * @var StoreResolver */ protected $storeResolver; /** - * @var \Magento\Store\Model\Website + * @var WebsiteModel */ protected $websiteModel; /** - * @param \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver - * @param \Magento\Store\Model\Website $websiteModel + * @param StoreResolver $storeResolver + * @param WebsiteModel $websiteModel */ public function __construct( - \Magento\CatalogImportExport\Model\Import\Product\StoreResolver $storeResolver, - \Magento\Store\Model\Website $websiteModel + StoreResolver $storeResolver, + WebsiteModel $websiteModel ) { $this->storeResolver = $storeResolver; $this->websiteModel = $websiteModel; } - /** - * {@inheritdoc} - */ - public function init($context) - { - return parent::init($context); - } - /** * Validate by website type * * @param array $value * @param string $websiteCode + * * @return bool */ protected function isWebsiteValid($value, $websiteCode) @@ -62,7 +60,8 @@ protected function isWebsiteValid($value, $websiteCode) /** * Validate value * - * @param mixed $value + * @param array $value + * * @return bool */ public function isValid($value) @@ -85,6 +84,7 @@ public function isValid($value) */ public function getAllWebsitesValue() { - return AdvancedPricing::VALUE_ALL_WEBSITES . ' ['.$this->websiteModel->getBaseCurrency()->getCurrencyCode().']'; + return AdvancedPricing::VALUE_ALL_WEBSITES . + ' [' . $this->websiteModel->getBaseCurrency()->getCurrencyCode() . ']'; } } diff --git a/app/code/Magento/AdvancedSearch/Model/Client/ClientResolver.php b/app/code/Magento/AdvancedSearch/Model/Client/ClientResolver.php index 84933c8584bb3..33e99a0a0d0ee 100644 --- a/app/code/Magento/AdvancedSearch/Model/Client/ClientResolver.php +++ b/app/code/Magento/AdvancedSearch/Model/Client/ClientResolver.php @@ -20,7 +20,7 @@ class ClientResolver * * @var ScopeConfigInterface * @since 100.1.0 - * @deprecated since it is not used anymore + * @deprecated 100.3.0 since it is not used anymore */ protected $scopeConfig; @@ -56,14 +56,14 @@ class ClientResolver * * @var string * @since 100.1.0 - * @deprecated since it is not used anymore + * @deprecated 100.3.0 since it is not used anymore */ protected $path; /** * Config Scope * @since 100.1.0 - * @deprecated since it is not used anymore + * @deprecated 100.3.0 since it is not used anymore */ protected $scope; diff --git a/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php b/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php index ddd9fcba21109..f6abb0f1ab2d1 100644 --- a/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php +++ b/app/code/Magento/Analytics/Model/Connector/Http/ConverterInterface.php @@ -9,6 +9,7 @@ * Represents converter interface for http request and response body. * * @api + * @since 100.2.0 */ interface ConverterInterface { @@ -16,6 +17,7 @@ interface ConverterInterface * @param string $body * * @return array + * @since 100.2.0 */ public function fromBody($body); @@ -23,16 +25,19 @@ public function fromBody($body); * @param array $data * * @return string + * @since 100.2.0 */ public function toBody(array $data); /** * @return string + * @since 100.2.0 */ public function getContentTypeHeader(); /** * @return string + * @since 100.3.0 */ public function getContentMediaType(): string; } diff --git a/app/code/Magento/Analytics/Model/ReportWriter.php b/app/code/Magento/Analytics/Model/ReportWriter.php index 7128658947908..d5bd36d068d20 100644 --- a/app/code/Magento/Analytics/Model/ReportWriter.php +++ b/app/code/Magento/Analytics/Model/ReportWriter.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Analytics\Model; use Magento\Analytics\ReportXml\DB\ReportValidator; @@ -10,7 +12,6 @@ /** * Writes reports in files in csv format - * @inheritdoc */ class ReportWriter implements ReportWriterInterface { @@ -54,7 +55,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function write(WriteInterface $directory, $path) { @@ -81,7 +82,7 @@ public function write(WriteInterface $directory, $path) $headers = array_keys($row); $stream->writeCsv($headers); } - $stream->writeCsv($row); + $stream->writeCsv($this->prepareRow($row)); } $stream->unlock(); $stream->close(); @@ -98,4 +99,18 @@ public function write(WriteInterface $directory, $path) return true; } + + /** + * Replace wrong symbols in row + * + * @param array $row + * @return array + */ + private function prepareRow(array $row): array + { + $row = preg_replace('/(?<!\\\\)"/', '\\"', $row); + $row = preg_replace('/[\\\\]+/', '\\', $row); + + return $row; + } } diff --git a/app/code/Magento/Analytics/ReportXml/Query.php b/app/code/Magento/Analytics/ReportXml/Query.php index edf5ed08ee55f..b7c31d4334e20 100644 --- a/app/code/Magento/Analytics/ReportXml/Query.php +++ b/app/code/Magento/Analytics/ReportXml/Query.php @@ -81,7 +81,6 @@ public function getConfig() * @link http://php.net/manual/en/jsonserializable.jsonserialize.php * @return mixed data which can be serialized by <b>json_encode</b>, * which is a value of any type other than a resource. - * @since 5.4.0 */ public function jsonSerialize() { diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php index 89595d21a10f0..074e27d75ea3c 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/CollectionTimeLabelTest.php @@ -59,7 +59,7 @@ class CollectionTimeLabelTest extends TestCase protected function setUp(): void { $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment']) + ->setMethods(['getComment', 'getElementHtml']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php index 0dc2671adf2d7..ac56f2b15fcd4 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/SubscriptionStatusLabelTest.php @@ -49,7 +49,7 @@ protected function setUp(): void $this->subscriptionStatusProviderMock = $this->createMock(SubscriptionStatusProvider::class); $this->contextMock = $this->createMock(Context::class); $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment']) + ->setMethods(['getComment', 'getElementHtml']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php index 6e315643ade1f..18b90df630c6b 100644 --- a/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Block/Adminhtml/System/Config/VerticalTest.php @@ -41,7 +41,7 @@ class VerticalTest extends TestCase protected function setUp(): void { $this->abstractElementMock = $this->getMockBuilder(AbstractElement::class) - ->setMethods(['getComment', 'getLabel', 'getHint']) + ->setMethods(['getComment', 'getLabel', 'getHint', 'getElementHtml']) ->disableOriginalConstructor() ->getMock(); diff --git a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php index b68e26f98a397..8fb135fb4d9ed 100644 --- a/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php +++ b/app/code/Magento/Analytics/Test/Unit/Model/ReportWriterTest.php @@ -12,7 +12,8 @@ use Magento\Analytics\Model\ReportWriter; use Magento\Analytics\ReportXml\DB\ReportValidator; use Magento\Analytics\ReportXml\ReportProvider; -use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Directory\WriteInterface as DirectoryWriteInterface; +use Magento\Framework\Filesystem\File\WriteInterface as FileWriteInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -48,7 +49,7 @@ class ReportWriterTest extends TestCase private $objectManagerHelper; /** - * @var WriteInterface|MockObject + * @var DirectoryWriteInterface|MockObject */ private $directoryMock; @@ -82,7 +83,7 @@ protected function setUp(): void $this->reportValidatorMock = $this->createMock(ReportValidator::class); $this->providerFactoryMock = $this->createMock(ProviderFactory::class); $this->reportProviderMock = $this->createMock(ReportProvider::class); - $this->directoryMock = $this->getMockBuilder(WriteInterface::class) + $this->directoryMock = $this->getMockBuilder(DirectoryWriteInterface::class) ->getMockForAbstractClass(); $this->objectManagerHelper = new ObjectManagerHelper($this); @@ -98,16 +99,15 @@ protected function setUp(): void /** * @param array $configData + * @param array $fileData + * @param array $expectedFileData * @return void * * @dataProvider configDataProvider */ - public function testWrite(array $configData) + public function testWrite(array $configData, array $fileData, array $expectedFileData): void { $errors = []; - $fileData = [ - ['number' => 1, 'type' => 'Shoes Usual'] - ]; $this->configInterfaceMock ->expects($this->once()) ->method('get') @@ -126,7 +126,7 @@ public function testWrite(array $configData) ->with($parameterName ?: null) ->willReturn($fileData); $errorStreamMock = $this->getMockBuilder( - \Magento\Framework\Filesystem\File\WriteInterface::class + FileWriteInterface::class )->getMockForAbstractClass(); $errorStreamMock ->expects($this->once()) @@ -136,8 +136,8 @@ public function testWrite(array $configData) ->expects($this->exactly(2)) ->method('writeCsv') ->withConsecutive( - [array_keys($fileData[0])], - [$fileData[0]] + [array_keys($expectedFileData[0])], + [$expectedFileData[0]] ); $errorStreamMock->expects($this->once())->method('unlock'); $errorStreamMock->expects($this->once())->method('close'); @@ -164,12 +164,12 @@ public function testWrite(array $configData) * * @dataProvider configDataProvider */ - public function testWriteErrorFile($configData) + public function testWriteErrorFile(array $configData): void { $errors = ['orders', 'SQL Error: test']; $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([$configData]); $errorStreamMock = $this->getMockBuilder( - \Magento\Framework\Filesystem\File\WriteInterface::class + FileWriteInterface::class )->getMockForAbstractClass(); $errorStreamMock->expects($this->once())->method('lock'); $errorStreamMock->expects($this->once())->method('writeCsv')->with($errors); @@ -184,7 +184,7 @@ public function testWriteErrorFile($configData) /** * @return void */ - public function testWriteEmptyReports() + public function testWriteEmptyReports(): void { $this->configInterfaceMock->expects($this->once())->method('get')->willReturn([]); $this->reportValidatorMock->expects($this->never())->method('validate'); @@ -195,11 +195,11 @@ public function testWriteEmptyReports() /** * @return array */ - public function configDataProvider() + public function configDataProvider(): array { return [ 'reportProvider' => [ - [ + 'configData' => [ 'providers' => [ [ 'name' => $this->providerName, @@ -209,6 +209,12 @@ public function configDataProvider() ], ] ] + ], + 'fileData' => [ + ['number' => 1, 'type' => 'Shoes\"" Usual\\\\"'] + ], + 'expectedFileData' => [ + ['number' => 1, 'type' => 'Shoes\"\" Usual\\"'] ] ], ]; diff --git a/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php index 76410794900e2..b40fdf19d466f 100644 --- a/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/BulkStatusInterface.php @@ -14,6 +14,7 @@ * Bulk summary data with list of operations items short data. * * @api + * @since 100.2.3 */ interface BulkStatusInterface extends \Magento\Framework\Bulk\BulkStatusInterface { @@ -23,6 +24,7 @@ interface BulkStatusInterface extends \Magento\Framework\Bulk\BulkStatusInterfac * @param string $bulkUuid * @return \Magento\AsynchronousOperations\Api\Data\DetailedBulkOperationsStatusInterface * @throws \Magento\Framework\Exception\NoSuchEntityException + * @since 100.2.3 */ public function getBulkDetailedStatus($bulkUuid); @@ -32,6 +34,7 @@ public function getBulkDetailedStatus($bulkUuid); * @param string $bulkUuid * @return \Magento\AsynchronousOperations\Api\Data\BulkOperationsStatusInterface * @throws \Magento\Framework\Exception\NoSuchEntityException + * @since 100.2.3 */ public function getBulkShortStatus($bulkUuid); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php index c7edd5c8ff9cd..c0390e40899e8 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/AsyncResponseInterface.php @@ -13,6 +13,7 @@ * Temporary data object to give response from webapi async router * * @api + * @since 100.2.3 */ interface AsyncResponseInterface { @@ -24,6 +25,7 @@ interface AsyncResponseInterface * Gets the bulk uuid. * * @return string Bulk Uuid. + * @since 100.2.3 */ public function getBulkUuid(); @@ -32,6 +34,7 @@ public function getBulkUuid(); * * @param string $bulkUuid * @return $this + * @since 100.2.3 */ public function setBulkUuid($bulkUuid); @@ -39,6 +42,7 @@ public function setBulkUuid($bulkUuid); * Gets the list of request items with status data. * * @return \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface[] + * @since 100.2.3 */ public function getRequestItems(); @@ -47,12 +51,14 @@ public function getRequestItems(); * * @param \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface[] $requestItems * @return $this + * @since 100.2.3 */ public function setRequestItems($requestItems); /** * @param bool $isErrors * @return $this + * @since 100.2.3 */ public function setErrors($isErrors = false); @@ -60,6 +66,7 @@ public function setErrors($isErrors = false); * Is there errors during processing bulk * * @return boolean + * @since 100.2.3 */ public function isErrors(); @@ -67,6 +74,7 @@ public function isErrors(); * Retrieve existing extension attributes object. * * @return \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface|null + * @since 100.2.3 */ public function getExtensionAttributes(); @@ -75,6 +83,7 @@ public function getExtensionAttributes(); * * @param \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface $extensionAttributes * @return $this + * @since 100.2.3 */ public function setExtensionAttributes( \Magento\AsynchronousOperations\Api\Data\AsyncResponseExtensionInterface $extensionAttributes diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php index f8b7e389d387d..5fedf675e5579 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkOperationsStatusInterface.php @@ -14,6 +14,7 @@ * Bulk summary data with list of operations items summary data. * * @api + * @since 100.2.3 */ interface BulkOperationsStatusInterface extends BulkSummaryInterface { @@ -24,6 +25,7 @@ interface BulkOperationsStatusInterface extends BulkSummaryInterface * Retrieve list of operation with statuses (short data). * * @return \Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface[] + * @since 100.2.3 */ public function getOperationsList(); @@ -32,6 +34,7 @@ public function getOperationsList(); * * @param \Magento\AsynchronousOperations\Api\Data\SummaryOperationStatusInterface[] $operationStatusList * @return $this + * @since 100.2.3 */ public function setOperationsList($operationStatusList); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php index a433ec0953a83..5e2cff0b6da3d 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/BulkSummaryInterface.php @@ -38,6 +38,7 @@ public function setExtensionAttributes( * Get user type * * @return int + * @since 100.3.0 */ public function getUserType(); @@ -46,6 +47,7 @@ public function getUserType(); * * @param int $userType * @return $this + * @since 100.3.0 */ public function setUserType($userType); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php index 6e39177630857..62bead9f9956e 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/DetailedBulkOperationsStatusInterface.php @@ -14,6 +14,7 @@ * Bulk summary data with list of operations items full data. * * @api + * @since 100.2.3 */ interface DetailedBulkOperationsStatusInterface extends BulkSummaryInterface { @@ -24,6 +25,7 @@ interface DetailedBulkOperationsStatusInterface extends BulkSummaryInterface * Retrieve operations list. * * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface[] + * @since 100.2.3 */ public function getOperationsList(); @@ -32,6 +34,7 @@ public function getOperationsList(); * * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface[] $operationStatusList * @return $this + * @since 100.2.3 */ public function setOperationsList($operationStatusList); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php index 3294078c2c1ea..8919e87c55bec 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/ItemStatusInterface.php @@ -14,6 +14,7 @@ * Indicate if entity param was Accepted|Rejected to bulk schedule * * @api + * @since 100.2.3 */ interface ItemStatusInterface { @@ -30,6 +31,7 @@ interface ItemStatusInterface * Get entity Id. * * @return int + * @since 100.2.3 */ public function getId(); @@ -38,6 +40,7 @@ public function getId(); * * @param int $entityId * @return $this + * @since 100.2.3 */ public function setId($entityId); @@ -45,6 +48,7 @@ public function setId($entityId); * Get hash of entity data. * * @return string md5 hash of entity params array. + * @since 100.2.3 */ public function getDataHash(); @@ -53,6 +57,7 @@ public function getDataHash(); * * @param string $hash md5 hash of entity params array. * @return $this + * @since 100.2.3 */ public function setDataHash($hash); @@ -60,6 +65,7 @@ public function setDataHash($hash); * Get status. * * @return string accepted|rejected + * @since 100.2.3 */ public function getStatus(); @@ -68,6 +74,7 @@ public function getStatus(); * * @param string $status accepted|rejected * @return $this + * @since 100.2.3 */ public function setStatus($status = self::STATUS_ACCEPTED); @@ -75,6 +82,7 @@ public function setStatus($status = self::STATUS_ACCEPTED); * Get error information. * * @return string|null + * @since 100.2.3 */ public function getErrorMessage(); @@ -83,6 +91,7 @@ public function getErrorMessage(); * * @param string|null|\Exception $error * @return $this + * @since 100.2.3 */ public function setErrorMessage($error = null); @@ -90,6 +99,7 @@ public function setErrorMessage($error = null); * Get error code. * * @return int|null + * @since 100.2.3 */ public function getErrorCode(); @@ -98,6 +108,7 @@ public function getErrorCode(); * * @param int|null|\Exception $errorCode Default: null * @return $this + * @since 100.2.3 */ public function setErrorCode($errorCode = null); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php index c3d221b7ef4f8..f8e1457366777 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/OperationSearchResultsInterface.php @@ -13,6 +13,7 @@ * * An bulk is a group of queue messages. An bulk operation item is a queue message. * @api + * @since 100.3.0 */ interface OperationSearchResultsInterface extends \Magento\Framework\Api\SearchResultsInterface { @@ -20,6 +21,7 @@ interface OperationSearchResultsInterface extends \Magento\Framework\Api\SearchR * Get list of operations. * * @return \Magento\AsynchronousOperations\Api\Data\OperationInterface[] + * @since 100.3.0 */ public function getItems(); @@ -28,6 +30,7 @@ public function getItems(); * * @param \Magento\AsynchronousOperations\Api\Data\OperationInterface[] $items * @return $this + * @since 100.3.0 */ public function setItems(array $items); } diff --git a/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php b/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php index 3b9f53b34162a..051dbd955c4a9 100644 --- a/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/Data/SummaryOperationStatusInterface.php @@ -15,6 +15,7 @@ * without serialized_data and result_serialized_data * * @api + * @since 100.2.3 */ interface SummaryOperationStatusInterface { @@ -22,6 +23,7 @@ interface SummaryOperationStatusInterface * Operation id * * @return int + * @since 100.2.3 */ public function getId(); @@ -31,6 +33,7 @@ public function getId(); * OPEN | COMPLETE | RETRIABLY_FAILED | NOT_RETRIABLY_FAILED * * @return int + * @since 100.2.3 */ public function getStatus(); @@ -38,6 +41,7 @@ public function getStatus(); * Get result message * * @return string + * @since 100.2.3 */ public function getResultMessage(); @@ -45,6 +49,7 @@ public function getResultMessage(); * Get error code * * @return int + * @since 100.2.3 */ public function getErrorCode(); } diff --git a/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php b/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php index 17547321b827f..6cb6a93143918 100644 --- a/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/OperationRepositoryInterface.php @@ -13,6 +13,7 @@ * * An bulk is a group of queue messages. An bulk operation item is a queue message. * @api + * @since 100.3.0 */ interface OperationRepositoryInterface { @@ -21,6 +22,7 @@ interface OperationRepositoryInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\AsynchronousOperations\Api\Data\OperationSearchResultsInterface + * @since 100.3.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); } diff --git a/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php b/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php index 8563ab6541a0c..12abdc04bb165 100644 --- a/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php +++ b/app/code/Magento/AsynchronousOperations/Api/SaveMultipleOperationsInterface.php @@ -14,6 +14,7 @@ * Interface for saving multiple operations * * @api + * @since 100.4.0 */ interface SaveMultipleOperationsInterface { @@ -22,6 +23,7 @@ interface SaveMultipleOperationsInterface * * @param OperationInterface[] $operations * @return void + * @since 100.4.0 */ public function execute(array $operations): void; } diff --git a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php index b47bb26985df0..6cf0611eb28ec 100644 --- a/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php +++ b/app/code/Magento/AsynchronousOperations/Model/BulkManagement.php @@ -140,8 +140,8 @@ public function scheduleBulk($bulkUuid, array $operations, $description, $userId public function retryBulk($bulkUuid, array $errorCodes) { $metadata = $this->metadataPool->getMetadata(BulkSummaryInterface::class); - $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); + $connection = $this->resourceConnection->getConnectionByName($metadata->getEntityConnectionName()); /** @var \Magento\AsynchronousOperations\Model\ResourceModel\Operation[] $retriablyFailedOperations */ $retriablyFailedOperations = $this->operationCollectionFactory->create() ->addFieldToFilter('error_code', ['in' => $errorCodes]) @@ -157,23 +157,27 @@ public function retryBulk($bulkUuid, array $errorCodes) /** @var OperationInterface $operation */ foreach ($retriablyFailedOperations as $operation) { if ($currentBatchSize === $maxBatchSize) { + $whereCondition = $connection->quoteInto('operation_key IN (?)', $operationIds) + . " AND " + . $connection->quoteInto('bulk_uuid = ?', $bulkUuid); $connection->delete( $this->resourceConnection->getTableName('magento_operation'), - $connection->quoteInto('id IN (?)', $operationIds) + $whereCondition ); $operationIds = []; $currentBatchSize = 0; } $currentBatchSize++; $operationIds[] = $operation->getId(); - // Rescheduled operations must be put in queue in 'open' state (i.e. without ID) - $operation->setId(null); } // remove operations from the last batch if (!empty($operationIds)) { + $whereCondition = $connection->quoteInto('operation_key IN (?)', $operationIds) + . " AND " + . $connection->quoteInto('bulk_uuid = ?', $bulkUuid); $connection->delete( $this->resourceConnection->getTableName('magento_operation'), - $connection->quoteInto('id IN (?)', $operationIds) + $whereCondition ); } diff --git a/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php b/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php index de0f89a71650a..593ab52bbdf29 100644 --- a/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php +++ b/app/code/Magento/AsynchronousOperations/Model/ConfigInterface.php @@ -15,6 +15,7 @@ * Class for accessing to Webapi_Async configuration. * * @api + * @since 100.2.3 */ interface ConfigInterface { @@ -45,6 +46,7 @@ interface ConfigInterface * Get array of generated topics name and related to this topic service class and methods * * @return array + * @since 100.2.3 */ public function getServices(); @@ -55,6 +57,7 @@ public function getServices(); * @param string $httpMethod GET|POST|PUT|DELETE * @return string * @throws \Magento\Framework\Exception\LocalizedException + * @since 100.2.3 */ public function getTopicName($routeUrl, $httpMethod); } diff --git a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php index e2f756a9e8fcd..4d83c03507f9c 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassConsumer.php @@ -69,11 +69,19 @@ public function process($maxNumberOfMessages = null) $this->registry->register('isSecureArea', true, true); $queue = $this->configuration->getQueue(); + $maxIdleTime = $this->configuration->getMaxIdleTime(); + $sleep = $this->configuration->getSleep(); if (!isset($maxNumberOfMessages)) { $queue->subscribe($this->getTransactionCallback($queue)); } else { - $this->invoker->invoke($queue, $maxNumberOfMessages, $this->getTransactionCallback($queue)); + $this->invoker->invoke( + $queue, + $maxNumberOfMessages, + $this->getTransactionCallback($queue), + $maxIdleTime, + $sleep + ); } $this->registry->unregister('isSecureArea'); diff --git a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php index 4dcaf7279a570..d8efed5562131 100644 --- a/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php +++ b/app/code/Magento/AsynchronousOperations/Model/MassSchedule.php @@ -11,7 +11,6 @@ use Magento\AsynchronousOperations\Api\Data\AsyncResponseInterfaceFactory; use Magento\AsynchronousOperations\Api\Data\ItemStatusInterface; use Magento\AsynchronousOperations\Api\Data\ItemStatusInterfaceFactory; -use Magento\AsynchronousOperations\Model\ResourceModel\Operation\OperationRepository; use Magento\Authorization\Model\UserContextInterface; use Magento\Framework\Bulk\BulkManagementInterface; use Magento\Framework\DataObject\IdentityGeneratorInterface; @@ -144,7 +143,6 @@ public function publishMass($topicName, array $entitiesArray, $groupId = null, $ foreach ($entitiesArray as $key => $entityParams) { /** @var \Magento\AsynchronousOperations\Api\Data\ItemStatusInterface $requestItem */ $requestItem = $this->itemStatusInterfaceFactory->create(); - try { $operation = $this->operationRepository->create($topicName, $entityParams, $groupId, $key); $operations[] = $operation; diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php b/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php index 74740cba9a6d8..7575257555fae 100644 --- a/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php +++ b/app/code/Magento/AsynchronousOperations/Model/OperationManagement.php @@ -7,17 +7,19 @@ namespace Magento\AsynchronousOperations\Model; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; -use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\App\ResourceConnection; +use Psr\Log\LoggerInterface; +use Magento\Framework\Bulk\OperationManagementInterface; /** * Class for managing Bulk Operations */ -class OperationManagement implements \Magento\Framework\Bulk\OperationManagementInterface +class OperationManagement implements OperationManagementInterface { /** - * @var EntityManager + * @var ResourceConnection */ - private $entityManager; + private $connection; /** * @var OperationInterfaceFactory @@ -32,25 +34,26 @@ class OperationManagement implements \Magento\Framework\Bulk\OperationManagement /** * OperationManagement constructor. * - * @param EntityManager $entityManager * @param OperationInterfaceFactory $operationFactory - * @param \Psr\Log\LoggerInterface $logger + * @param LoggerInterface $logger + * @param ResourceConnection $connection */ public function __construct( - EntityManager $entityManager, OperationInterfaceFactory $operationFactory, - \Psr\Log\LoggerInterface $logger + LoggerInterface $logger, + ResourceConnection $connection ) { - $this->entityManager = $entityManager; $this->operationFactory = $operationFactory; $this->logger = $logger; + $this->connection = $connection; } /** * @inheritDoc */ public function changeOperationStatus( - $operationId, + $bulkUuid, + $operationKey, $status, $errorCode = null, $message = null, @@ -58,14 +61,17 @@ public function changeOperationStatus( $resultData = null ) { try { - $operationEntity = $this->operationFactory->create(); - $this->entityManager->load($operationEntity, $operationId); - $operationEntity->setErrorCode($errorCode); - $operationEntity->setStatus($status); - $operationEntity->setResultMessage($message); - $operationEntity->setSerializedData($data); - $operationEntity->setResultSerializedData($resultData); - $this->entityManager->save($operationEntity); + $connection = $this->connection->getConnection(); + $table = $this->connection->getTableName('magento_operation'); + $bind = [ + 'error_code' => $errorCode, + 'status' => $status, + 'result_message' => $message, + 'serialized_data' => $data, + 'result_serialized_data' => $resultData + ]; + $where = ['bulk_uuid = ?' => $bulkUuid, 'operation_key = ?' => $operationKey]; + $connection->update($table, $bind, $where); } catch (\Exception $exception) { $this->logger->critical($exception->getMessage()); return false; diff --git a/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php b/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php index 453f786bdf47b..5c5619a4b41d1 100644 --- a/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php +++ b/app/code/Magento/AsynchronousOperations/Model/OperationProcessor.php @@ -163,6 +163,7 @@ public function process(string $encodedMessage) $serializedData = (isset($errorCode)) ? $operation->getSerializedData() : null; $this->operationManagement->changeOperationStatus( + $operation->getBulkUuid(), $operation->getId(), $status, $errorCode, diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php index 0eaa5315af614..b5c33af1470f3 100644 --- a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation.php @@ -6,10 +6,12 @@ namespace Magento\AsynchronousOperations\Model\ResourceModel; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; + /** * Resource class for Bulk Operations */ -class Operation extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb +class Operation extends AbstractDb { public const TABLE_NAME = "magento_operation"; diff --git a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php index b189d81d31636..6757b0c8f0a5c 100644 --- a/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php +++ b/app/code/Magento/AsynchronousOperations/Model/ResourceModel/Operation/OperationRepository.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - declare(strict_types=1); namespace Magento\AsynchronousOperations\Model\ResourceModel\Operation; @@ -11,6 +10,7 @@ use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; use Magento\AsynchronousOperations\Model\OperationRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\MessageQueue\MessageValidator; use Magento\Framework\MessageQueue\MessageEncoder; use Magento\Framework\Serialize\Serializer\Json; @@ -73,11 +73,13 @@ public function __construct( * @param string $topicName * @param array $entityParams * @param string $groupId + * @param string $operationId * @return OperationInterface - * @deprecated No longer used. + * @throws LocalizedException + * @deprecated 100.4.0 No longer used. * @see create() */ - public function createByTopic($topicName, $entityParams, $groupId) + public function createByTopic($topicName, $entityParams, $groupId, $operationId) { $this->messageValidator->validate($topicName, $entityParams); $encodedMessage = $this->messageEncoder->encode($topicName, $entityParams); @@ -89,10 +91,11 @@ public function createByTopic($topicName, $entityParams, $groupId) ]; $data = [ 'data' => [ - OperationInterface::BULK_ID => $groupId, - OperationInterface::TOPIC_NAME => $topicName, + OperationInterface::ID => $operationId, + OperationInterface::BULK_ID => $groupId, + OperationInterface::TOPIC_NAME => $topicName, OperationInterface::SERIALIZED_DATA => $this->jsonSerializer->serialize($serializedData), - OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, + OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, ], ]; @@ -103,9 +106,11 @@ public function createByTopic($topicName, $entityParams, $groupId) /** * @inheritDoc + * + * @throws LocalizedException */ public function create($topicName, $entityParams, $groupId, $operationId): OperationInterface { - return $this->createByTopic($topicName, $entityParams, $groupId); + return $this->createByTopic($topicName, $entityParams, $groupId, $operationId); } } diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php index 724871f216472..14abb41c77fc4 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/BulkManagementTest.php @@ -196,7 +196,7 @@ public function testRetryBulk() $bulkUuid = 'bulk-001'; $errorCodes = ['errorCode']; $connectionName = 'default'; - $operationId = 1; + $operationId = 0; $operationTable = 'magento_operation'; $topicName = 'topic.name'; $metadata = $this->getMockForAbstractClass(EntityMetadataInterface::class); @@ -216,13 +216,20 @@ public function testRetryBulk() $operationCollection->expects($this->once())->method('getItems')->willReturn([$operation]); $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); $operation->expects($this->once())->method('getId')->willReturn($operationId); - $operation->expects($this->once())->method('setId')->with(null)->willReturnSelf(); $this->resourceConnection->expects($this->once()) ->method('getTableName')->with($operationTable)->willReturn($operationTable); + $connection->expects($this->at(1)) + ->method('quoteInto') + ->with('operation_key IN (?)', [$operationId]) + ->willReturn('operation_key IN (' . $operationId . ')'); + $connection->expects($this->at(2)) + ->method('quoteInto') + ->with('bulk_uuid = ?', $bulkUuid) + ->willReturn("bulk_uuid = '$bulkUuid'"); $connection->expects($this->once()) - ->method('quoteInto')->with('id IN (?)', [$operationId])->willReturn('id IN (' . $operationId . ')'); - $connection->expects($this->once()) - ->method('delete')->with($operationTable, 'id IN (' . $operationId . ')')->willReturn(1); + ->method('delete') + ->with($operationTable, 'operation_key IN (' . $operationId . ') AND bulk_uuid = \'' . $bulkUuid . '\'') + ->willReturn(1); $connection->expects($this->once())->method('commit')->willReturnSelf(); $operation->expects($this->once())->method('getTopicName')->willReturn($topicName); $this->publisher->expects($this->once())->method('publish')->with($topicName, [$operation])->willReturn(null); @@ -239,7 +246,7 @@ public function testRetryBulkWithException() $bulkUuid = 'bulk-001'; $errorCodes = ['errorCode']; $connectionName = 'default'; - $operationId = 1; + $operationId = 0; $operationTable = 'magento_operation'; $exceptionMessage = 'Exception message'; $metadata = $this->getMockForAbstractClass(EntityMetadataInterface::class); @@ -259,13 +266,19 @@ public function testRetryBulkWithException() $operationCollection->expects($this->once())->method('getItems')->willReturn([$operation]); $connection->expects($this->once())->method('beginTransaction')->willReturnSelf(); $operation->expects($this->once())->method('getId')->willReturn($operationId); - $operation->expects($this->once())->method('setId')->with(null)->willReturnSelf(); $this->resourceConnection->expects($this->once()) ->method('getTableName')->with($operationTable)->willReturn($operationTable); + $connection->expects($this->at(1)) + ->method('quoteInto') + ->with('operation_key IN (?)', [$operationId]) + ->willReturn('operation_key IN (' . $operationId . ')'); + $connection->expects($this->at(2)) + ->method('quoteInto') + ->with('bulk_uuid = ?', $bulkUuid) + ->willReturn("bulk_uuid = '$bulkUuid'"); $connection->expects($this->once()) - ->method('quoteInto')->with('id IN (?)', [$operationId])->willReturn('id IN (' . $operationId . ')'); - $connection->expects($this->once()) - ->method('delete')->with($operationTable, 'id IN (' . $operationId . ')') + ->method('delete') + ->with($operationTable, 'operation_key IN (' . $operationId . ') AND bulk_uuid = \'' . $bulkUuid . '\'') ->willThrowException(new \Exception($exceptionMessage)); $connection->expects($this->once())->method('rollBack')->willReturnSelf(); $this->logger->expects($this->once())->method('critical')->with($exceptionMessage); diff --git a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php index 476bad2d0ee04..0f437cefd3fca 100644 --- a/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php +++ b/app/code/Magento/AsynchronousOperations/Test/Unit/Model/OperationManagementTest.php @@ -7,11 +7,10 @@ namespace Magento\AsynchronousOperations\Test\Unit\Model; -use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; use Magento\AsynchronousOperations\Model\OperationManagement; -use Magento\Framework\EntityManager\EntityManager; -use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Adapter\AdapterInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -24,79 +23,116 @@ class OperationManagementTest extends TestCase private $model; /** - * @var MockObject - */ - private $entityManagerMock; - - /** - * @var MockObject + * @var OperationInterfaceFactory|MockObject */ private $operationFactoryMock; /** - * @var MockObject - */ - private $operationMock; - - /** - * @var MockObject + * @var LoggerInterface|MockObject */ private $loggerMock; + /** - * @var MetadataPool|MockObject + * @var ResourceConnection|MockObject */ - private $metadataPoolMock; + private $resourceConnectionMock; protected function setUp(): void { - $this->entityManagerMock = $this->createMock(EntityManager::class); - $this->metadataPoolMock = $this->createMock(MetadataPool::class); $this->operationFactoryMock = $this->createPartialMock( OperationInterfaceFactory::class, ['create'] ); - $this->operationMock = - $this->getMockForAbstractClass(OperationInterface::class); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); + $this->resourceConnectionMock = $this->getMockBuilder(ResourceConnection::class) + ->disableOriginalConstructor() + ->setMethods(['getConnection', 'getTableName']) + ->getMock(); + $this->model = new OperationManagement( - $this->entityManagerMock, $this->operationFactoryMock, - $this->loggerMock + $this->loggerMock, + $this->resourceConnectionMock ); } + /** + * Test change operation status. + */ public function testChangeOperationStatus() { - $operationId = 1; + $operationKey = 1; $status = 1; $message = 'Message'; $data = 'data'; $errorCode = 101; - $this->operationFactoryMock->expects($this->once())->method('create')->willReturn($this->operationMock); - $this->entityManagerMock->expects($this->once())->method('load')->with($this->operationMock, $operationId); - $this->operationMock->expects($this->once())->method('setStatus')->with($status)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setResultMessage')->with($message)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setSerializedData')->with($data)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setErrorCode')->with($errorCode)->willReturnSelf(); - $this->entityManagerMock->expects($this->once())->method('save')->with($this->operationMock); - $this->assertTrue($this->model->changeOperationStatus($operationId, $status, $errorCode, $message, $data)); + $bulkUuid = '13f85e88-be1d-4ce7-8570-88637a589930'; + + $tableName = 'magento_operation'; + + $bind = [ + 'error_code' => $errorCode, + 'status' => $status, + 'result_message' => $message, + 'serialized_data' => $data, + 'result_serialized_data' => '' + ]; + $where = ['bulk_uuid = ?' => $bulkUuid, 'operation_key = ?' => $operationKey]; + + $connection = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->resourceConnectionMock->expects($this->atLeastOnce()) + ->method('getConnection')->with('default') + ->willReturn($connection); + $this->resourceConnectionMock->expects($this->once())->method('getTableName')->with($tableName) + ->willReturn($tableName); + + $connection->expects($this->once())->method('update')->with($tableName, $bind, $where) + ->willReturn(1); + $this->assertTrue( + $this->model->changeOperationStatus($bulkUuid, $operationKey, $status, $errorCode, $message, $data) + ); } + /** + * Test generic exception throw case. + */ public function testChangeOperationStatusIfExceptionWasThrown() { - $operationId = 1; + $operationKey = 1; $status = 1; $message = 'Message'; $data = 'data'; $errorCode = 101; - $this->operationFactoryMock->expects($this->once())->method('create')->willReturn($this->operationMock); - $this->entityManagerMock->expects($this->once())->method('load')->with($this->operationMock, $operationId); - $this->operationMock->expects($this->once())->method('setStatus')->with($status)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setResultMessage')->with($message)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setSerializedData')->with($data)->willReturnSelf(); - $this->operationMock->expects($this->once())->method('setErrorCode')->with($errorCode)->willReturnSelf(); - $this->entityManagerMock->expects($this->once())->method('save')->willThrowException(new \Exception()); + $bulkUuid = '13f85e88-be1d-4ce7-8570-88637a589930'; + + $tableName = 'magento_operation'; + + $bind = [ + 'error_code' => $errorCode, + 'status' => $status, + 'result_message' => $message, + 'serialized_data' => $data, + 'result_serialized_data' => '' + ]; + $where = ['bulk_uuid = ?' => $bulkUuid, 'operation_key = ?' => $operationKey]; + + $connection = $this->getMockBuilder(AdapterInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->resourceConnectionMock->expects($this->atLeastOnce()) + ->method('getConnection')->with('default') + ->willReturn($connection); + $this->resourceConnectionMock->expects($this->once()) + ->method('getTableName')->with($tableName) + ->willReturn($tableName); + + $connection->expects($this->once())->method('update')->with($tableName, $bind, $where) + ->willThrowException(new \Exception()); $this->loggerMock->expects($this->once())->method('critical'); - $this->assertFalse($this->model->changeOperationStatus($operationId, $status, $errorCode, $message, $data)); + $this->assertFalse( + $this->model->changeOperationStatus($bulkUuid, $operationKey, $status, $errorCode, $message, $data) + ); } } diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml index f287a368c72fb..5d49d71ee46b0 100644 --- a/app/code/Magento/AsynchronousOperations/etc/db_schema.xml +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema.xml @@ -9,15 +9,15 @@ xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> <table name="magento_bulk" resource="default" engine="innodb" comment="Bulk entity that represents set of related asynchronous operations"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Bulk Internal ID (must not be exposed)"/> <column xsi:type="varbinary" name="uuid" nullable="true" length="39" comment="Bulk UUID (can be exposed to reference bulk entity)"/> - <column xsi:type="int" name="user_id" unsigned="true" nullable="true" identity="false" + <column xsi:type="int" name="user_id" padding="10" unsigned="true" nullable="true" identity="false" comment="ID of the WebAPI user that performed an action"/> <column xsi:type="int" name="user_type" nullable="true" comment="Which type of user"/> <column xsi:type="varchar" name="description" nullable="true" length="255" comment="Bulk Description"/> - <column xsi:type="int" name="operation_count" unsigned="true" nullable="false" identity="false" + <column xsi:type="int" name="operation_count" padding="10" unsigned="true" nullable="false" identity="false" comment="Total number of operations scheduled within this bulk"/> <column xsi:type="timestamp" name="start_time" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Bulk start time"/> @@ -32,8 +32,10 @@ </index> </table> <table name="magento_operation" resource="default" engine="innodb" comment="Operation entity"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Operation ID"/> + <column xsi:type="int" name="operation_key" padding="10" unsigned="true" nullable="false" + comment="Operation Key"/> <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> <column xsi:type="varchar" name="topic_name" nullable="true" length="255" comment="Name of the related message queue topic"/> @@ -41,9 +43,9 @@ comment="Data (serialized) required to perform an operation"/> <column xsi:type="blob" name="result_serialized_data" nullable="true" comment="Result data (serialized) after perform an operation"/> - <column xsi:type="smallint" name="status" unsigned="false" nullable="true" identity="false" + <column xsi:type="smallint" name="status" padding="6" unsigned="false" nullable="true" identity="false" default="0" comment="Operation status (OPEN | COMPLETE | RETRIABLY_FAILED | NOT_RETRIABLY_FAILED)"/> - <column xsi:type="smallint" name="error_code" unsigned="false" nullable="true" identity="false" + <column xsi:type="smallint" name="error_code" padding="6" unsigned="false" nullable="true" identity="false" comment="Code of the error that appeared during operation execution (used to aggregate related failed operations)"/> <column xsi:type="varchar" name="result_message" nullable="true" length="255" comment="Operation result message"/> @@ -59,7 +61,7 @@ </table> <table name="magento_acknowledged_bulk" resource="default" engine="innodb" comment="Bulk that was viewed by user from notification area"> - <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" + <column xsi:type="int" name="id" padding="10" unsigned="true" nullable="false" identity="true" comment="Internal ID"/> <column xsi:type="varbinary" name="bulk_uuid" nullable="true" length="39" comment="Related Bulk UUID"/> <constraint xsi:type="primary" referenceId="PRIMARY"> diff --git a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json index 9b6c0709e1916..6cbb3c664a50f 100644 --- a/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json +++ b/app/code/Magento/AsynchronousOperations/etc/db_schema_whitelist.json @@ -22,6 +22,7 @@ "magento_operation": { "column": { "id": true, + "operation_key": true, "bulk_uuid": true, "topic_name": true, "serialized_data": true, @@ -35,7 +36,8 @@ }, "constraint": { "PRIMARY": true, - "MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID": true + "MAGENTO_OPERATION_BULK_UUID_MAGENTO_BULK_UUID": true, + "UUID": true } }, "magento_acknowledged_bulk": { @@ -49,4 +51,4 @@ "MAGENTO_ACKNOWLEDGED_BULK_BULK_UUID": true } } -} \ No newline at end of file +} diff --git a/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php b/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php index 6560b3be3b947..cbd9012e1a80d 100644 --- a/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php +++ b/app/code/Magento/Authorization/Test/Unit/Model/ResourceModel/RulesTest.php @@ -182,7 +182,7 @@ public function testSaveRelNoResources() /** * Test LocalizedException throw case. */ - public function testLocalizedExceptionOccurance() + public function testLocalizedExceptionOccurrence() { $this->expectException(LocalizedException::class); $this->expectExceptionMessage("TestException"); @@ -212,7 +212,7 @@ public function testLocalizedExceptionOccurance() /** * Test generic exception throw case. */ - public function testGenericExceptionOccurance() + public function testGenericExceptionOccurrence() { $exception = new \Exception('GenericException'); diff --git a/app/code/Magento/Backend/App/AbstractAction.php b/app/code/Magento/Backend/App/AbstractAction.php index 2f01700bdf51c..0e0b34f168c05 100644 --- a/app/code/Magento/Backend/App/AbstractAction.php +++ b/app/code/Magento/Backend/App/AbstractAction.php @@ -16,11 +16,12 @@ use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\View\Element\AbstractBlock; +use Magento\Framework\Encryption\Helper\Security; /** * Generic backend controller * - * @deprecated Use \Magento\Framework\App\ActionInterface + * @deprecated 102.0.0 Use \Magento\Framework\App\ActionInterface * * phpcs:disable Magento2.Classes.AbstractApi * @api @@ -386,7 +387,7 @@ protected function _validateSecretKey() } $secretKey = $this->getRequest()->getParam(UrlInterface::SECRET_KEY_PARAM_NAME, null); - if (!$secretKey || $secretKey != $this->_backendUrl->getSecretKey()) { + if (!$secretKey || !Security::compareStrings($secretKey, $this->_backendUrl->getSecretKey())) { return false; } return true; diff --git a/app/code/Magento/Backend/Block/AbstractBlock.php b/app/code/Magento/Backend/Block/AbstractBlock.php index fc91f99e3dbaf..bfac54f8c555c 100644 --- a/app/code/Magento/Backend/Block/AbstractBlock.php +++ b/app/code/Magento/Backend/Block/AbstractBlock.php @@ -22,10 +22,10 @@ class AbstractBlock extends \Magento\Framework\View\Element\AbstractBlock protected $_authorization; /** - * @param \Magento\Backend\Block\Context $context + * @param Context $context * @param array $data */ - public function __construct(\Magento\Backend\Block\Context $context, array $data = []) + public function __construct(Context $context, array $data = []) { $this->_authorization = $context->getAuthorization(); parent::__construct($context, $data); diff --git a/app/code/Magento/Backend/Block/Dashboard.php b/app/code/Magento/Backend/Block/Dashboard.php index 28d3eeae9a1c6..511e393610b1e 100644 --- a/app/code/Magento/Backend/Block/Dashboard.php +++ b/app/code/Magento/Backend/Block/Dashboard.php @@ -9,7 +9,7 @@ /** * Class used to initialize layout for MBO Dashboard - * @deprecated dashboard graphs were migrated to dynamic chart.js solution + * @deprecated 102.0.0 dashboard graphs were migrated to dynamic chart.js solution * @see dashboard in adminhtml_dashboard_index.xml * * @api diff --git a/app/code/Magento/Backend/Block/Dashboard/Graph.php b/app/code/Magento/Backend/Block/Dashboard/Graph.php index db95a64636c3a..7811ee948f763 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Graph.php +++ b/app/code/Magento/Backend/Block/Dashboard/Graph.php @@ -77,7 +77,7 @@ class Graph extends \Magento\Backend\Block\Dashboard\AbstractDashboard /** * Google chart api data encoding * - * @deprecated since the Google Image Charts API not accessible from March 14, 2019 + * @deprecated 101.0.2 since the Google Image Charts API not accessible from March 14, 2019 * @var string */ protected $_encoding = 'e'; diff --git a/app/code/Magento/Backend/Block/Dashboard/Grids.php b/app/code/Magento/Backend/Block/Dashboard/Grids.php index f40aaaf33fed7..9820d8b868d86 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Grids.php +++ b/app/code/Magento/Backend/Block/Dashboard/Grids.php @@ -15,6 +15,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Grids extends Tabs { diff --git a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php index 8b3574e223236..dd21a215ea6fe 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php +++ b/app/code/Magento/Backend/Block/Dashboard/Orders/Grid.php @@ -16,6 +16,7 @@ * @api * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.DepthOfInheritance) + * @since 100.0.2 */ class Grid extends \Magento\Backend\Block\Dashboard\Grid { diff --git a/app/code/Magento/Backend/Block/Dashboard/Sales.php b/app/code/Magento/Backend/Block/Dashboard/Sales.php index ebe0932c3fa3b..098580b1369e9 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Sales.php +++ b/app/code/Magento/Backend/Block/Dashboard/Sales.php @@ -16,6 +16,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Sales extends Bar { diff --git a/app/code/Magento/Backend/Block/Dashboard/Totals.php b/app/code/Magento/Backend/Block/Dashboard/Totals.php index 7da109c2fb602..73e6bc1ab9e8a 100644 --- a/app/code/Magento/Backend/Block/Dashboard/Totals.php +++ b/app/code/Magento/Backend/Block/Dashboard/Totals.php @@ -17,6 +17,7 @@ /** * Adminhtml dashboard totals bar * @api + * @since 100.0.2 */ class Totals extends Bar { diff --git a/app/code/Magento/Backend/Block/Media/Uploader.php b/app/code/Magento/Backend/Block/Media/Uploader.php index e95b6397cd244..40cc68e04bf51 100644 --- a/app/code/Magento/Backend/Block/Media/Uploader.php +++ b/app/code/Magento/Backend/Block/Media/Uploader.php @@ -46,7 +46,7 @@ class Uploader extends \Magento\Backend\Block\Widget /** * @var UploadConfigInterface - * @deprecated + * @deprecated 101.0.1 * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface */ private $imageConfig; @@ -120,6 +120,7 @@ public function getFileSizeService() * Get Image Upload Maximum Width Config. * * @return int + * @since 100.2.7 */ public function getImageUploadMaxWidth() { @@ -130,6 +131,7 @@ public function getImageUploadMaxWidth() * Get Image Upload Maximum Height Config. * * @return int + * @since 100.2.7 */ public function getImageUploadMaxHeight() { diff --git a/app/code/Magento/Backend/Block/Page/Footer.php b/app/code/Magento/Backend/Block/Page/Footer.php index e0c173a4cbfec..610d28b0f53e3 100644 --- a/app/code/Magento/Backend/Block/Page/Footer.php +++ b/app/code/Magento/Backend/Block/Page/Footer.php @@ -60,6 +60,7 @@ public function getMagentoVersion() /** * @inheritdoc + * @since 101.0.0 */ protected function getCacheLifetime() { diff --git a/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php b/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php index 2abb987db0723..d290b89b2a6bc 100644 --- a/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php +++ b/app/code/Magento/Backend/Block/Page/System/Config/Robots/Reset.php @@ -11,7 +11,7 @@ /** * "Reset to Defaults" button renderer * - * @deprecated 100.2.0 + * @deprecated 100.1.6 * @author Magento Core Team <core@magentocommerce.com> */ class Reset extends \Magento\Config\Block\System\Config\Form\Field diff --git a/app/code/Magento/Backend/Block/Template.php b/app/code/Magento/Backend/Block/Template.php index 3ae4451a2592f..b4c41645d7f65 100644 --- a/app/code/Magento/Backend/Block/Template.php +++ b/app/code/Magento/Backend/Block/Template.php @@ -8,6 +8,10 @@ namespace Magento\Backend\Block; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Directory\Helper\Data as DirectoryHelper; + /** * Standard admin block. Adds admin-specific behavior and event. * Should be used when you declare a block in admin layout handle. @@ -60,15 +64,23 @@ class Template extends \Magento\Framework\View\Element\Template /** * @param \Magento\Backend\Block\Template\Context $context * @param array $data + * @param JsonHelper|null $jsonHelper + * @param DirectoryHelper|null $directoryHelper */ - public function __construct(\Magento\Backend\Block\Template\Context $context, array $data = []) - { + public function __construct( + \Magento\Backend\Block\Template\Context $context, + array $data = [], + ?JsonHelper $jsonHelper = null, + ?DirectoryHelper $directoryHelper = null + ) { $this->_localeDate = $context->getLocaleDate(); $this->_authorization = $context->getAuthorization(); $this->mathRandom = $context->getMathRandom(); $this->_backendSession = $context->getBackendSession(); $this->formKey = $context->getFormKey(); $this->nameBuilder = $context->getNameBuilder(); + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); + $data['directoryHelper']= $directoryHelper ?? ObjectManager::getInstance()->get(DirectoryHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Backend/Block/Widget/Button.php b/app/code/Magento/Backend/Block/Widget/Button.php index 8385ecaa40a8b..3b5eca6a61779 100644 --- a/app/code/Magento/Backend/Block/Widget/Button.php +++ b/app/code/Magento/Backend/Block/Widget/Button.php @@ -5,6 +5,11 @@ */ namespace Magento\Backend\Block\Widget; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Button widget * @@ -15,6 +20,33 @@ */ class Button extends \Magento\Backend\Block\Widget { + /** + * @var Random + */ + private $random; + + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param Random|null $random + * @param SecureHtmlRenderer|null $htmlRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?Random $random = null, + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct($context, $data); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Define block template * @@ -90,11 +122,12 @@ protected function _prepareAttributes($title, $classes, $disabled) 'title' => $title, 'type' => $this->getType(), 'class' => join(' ', $classes), - 'onclick' => $this->getOnClick(), - 'style' => $this->getStyle(), 'value' => $this->getValue(), 'disabled' => $disabled, ]; + if ($this->hasData('backend_button_widget_hook_id')) { + $attributes['backend-button-widget-hook-id'] = $this->getData('backend_button_widget_hook_id'); + } if ($this->getDataAttribute()) { foreach ($this->getDataAttribute() as $key => $attr) { $attributes['data-' . $key] = is_scalar($attr) ? $attr : json_encode($attr); @@ -121,4 +154,30 @@ protected function _attributesToHtml($attributes) return $html; } + + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + $buttonId = 'buttonId' .$this->random->getRandomString(10); + $this->setData('backend_button_widget_hook_id', $buttonId); + + $afterHtml = $this->getAfterHtml(); + if ($this->getOnClick()) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + $this->getOnClick(), + "*[backend-button-widget-hook-id='$buttonId']" + ); + } + if ($this->getStyle()) { + $afterHtml .= $this->secureRenderer->renderStyleAsTag($this->getStyle(), "#{$this->getId()}"); + } + $this->setAfterHtml($afterHtml); + + return $this; + } } diff --git a/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php b/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php index db3f5466fbacb..8075139368ab1 100644 --- a/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php +++ b/app/code/Magento/Backend/Block/Widget/Button/SplitButton.php @@ -5,6 +5,11 @@ */ namespace Magento\Backend\Block\Widget\Button; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Split button widget * @@ -21,6 +26,33 @@ */ class SplitButton extends \Magento\Backend\Block\Widget { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + /** * Define block template * @@ -61,6 +93,16 @@ public function getAttributesHtml() return $html; } + /** + * Get main button's "id" attribute value. + * + * @return string + */ + private function getButtonId(): string + { + return $this->getId() .'-button'; + } + /** * Retrieve button attributes html * @@ -84,11 +126,10 @@ public function getButtonAttributesHtml() $classes[] = $disabled; } $attributes = [ - 'id' => $this->getId() . '-button', + 'id' => $this->getButtonId(), 'title' => $title, 'class' => join(' ', $classes), - 'disabled' => $disabled, - 'style' => $this->getStyle(), + 'disabled' => $disabled ]; //TODO perhaps we need to skip data-mage-init when disabled="disabled" @@ -180,7 +221,7 @@ public function hasSplit() * Add data attributes to $attributes array * * @param array $data - * @param array &$attributes + * @param array $attributes * @return void */ protected function _getDataAttributes($data, &$attributes) @@ -190,6 +231,21 @@ protected function _getDataAttributes($data, &$attributes) } } + /** + * Retrieve "id" attribute value for an option. + * + * @param array $option + * @return string + */ + private function identifyOption(array $option): string + { + return isset($option['id']) + ? $this->getId() .'-' .$option['id'] + : (isset($option['id_attribute']) ? + $option['id_attribute'] + : $this->getId() .'-optId' .$this->random->getRandomString(10)); + } + /** * Prepare option attributes * @@ -203,11 +259,9 @@ protected function _getDataAttributes($data, &$attributes) protected function _prepareOptionAttributes($option, $title, $classes, $disabled) { $attributes = [ - 'id' => isset($option['id']) ? $this->getId() . '-' . $option['id'] : '', + 'id' => $this->identifyOption($option), 'title' => $title, 'class' => join(' ', $classes), - 'onclick' => isset($option['onclick']) ? $option['onclick'] : '', - 'style' => isset($option['style']) ? $option['style'] : '', 'disabled' => $disabled, ]; @@ -235,4 +289,29 @@ protected function _getAttributesString($attributes) } return join(' ', $html); } + + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + $afterHtml = $this->getAfterHtml(); + /** @var array|null $options */ + $options = $this->getOptions() ?? []; + foreach ($options as &$option) { + $id = $option['id_attribute'] = $this->identifyOption($option); + if (!empty($option['onclick'])) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag('onclick', $option['onclick'], "#$id"); + } + if (!empty($option['style'])) { + $afterHtml .= $this->secureRenderer->renderStyleAsTag($option['style'], "#$id"); + } + } + $this->setOptions($options); + $this->setAfterHtml($afterHtml); + + return $this; + } } diff --git a/app/code/Magento/Backend/Block/Widget/Form/Container.php b/app/code/Magento/Backend/Block/Widget/Form/Container.php index febaae3861688..6d92d2bfb0396 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Container.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Container.php @@ -5,6 +5,10 @@ */ namespace Magento\Backend\Block\Widget\Form; +use Magento\Backend\Block\Widget\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Backend form container block * @@ -39,7 +43,7 @@ class Container extends \Magento\Backend\Block\Widget\Container * @var string */ protected $_blockGroup = 'Magento_Backend'; - + /** * @var string */ @@ -55,6 +59,25 @@ class Container extends \Magento\Backend\Block\Widget\Container */ protected $_template = 'Magento_Backend::widget/form/container.phtml'; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $data); + } + /** * Initialize form. * @@ -205,8 +228,14 @@ public function getFormHtml() public function getFormInitScripts() { if (!empty($this->_formInitScripts) && is_array($this->_formInitScripts)) { - return '<script>' . implode("\n", $this->_formInitScripts) . '</script>'; + return $this->secureRenderer->renderTag( + 'script', + [], + implode("\n", $this->_formInitScripts), + false + ); } + return ''; } @@ -218,8 +247,14 @@ public function getFormInitScripts() public function getFormScripts() { if (!empty($this->_formScripts) && is_array($this->_formScripts)) { - return '<script>' . implode("\n", $this->_formScripts) . '</script>'; + return $this->secureRenderer->renderTag( + 'script', + [], + implode("\n", $this->_formScripts), + false + ); } + return ''; } diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php index d599d5fbad5e0..5517cb8d4d617 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Dependence.php @@ -6,6 +6,9 @@ namespace Magento\Backend\Block\Widget\Form\Element; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Form element dependencies mapper * Assumes that one element may depend on other element values. @@ -52,21 +55,29 @@ class Dependence extends \Magento\Backend\Block\AbstractBlock */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Config\Model\Config\Structure\Element\Dependency\FieldFactory $fieldFactory * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Config\Model\Config\Structure\Element\Dependency\FieldFactory $fieldFactory, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_fieldFactory = $fieldFactory; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -131,11 +142,11 @@ protected function _toHtml() $params .= ', ' . $this->_jsonEncoder->encode($this->_configOptions); } - return "<script> -require(['mage/adminhtml/form'], function(){ - new FormElementDependenceController({$params}); -}); -</script>"; + $scriptString = 'require([\'mage/adminhtml/form\'], function(){ + new FormElementDependenceController(' . $params . '); +});'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php index b9cdd259796d0..1b89746b3a98a 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/ElementCreator.php @@ -14,7 +14,7 @@ /** * Class ElementCreator * - * @deprecated 100.3.0 in favour of UI component implementation + * @deprecated 101.0.1 in favour of UI component implementation * @package Magento\Backend\Block\Widget\Form\Element */ class ElementCreator diff --git a/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php b/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php index aa0b0c3352ebe..25ea5b6100e28 100644 --- a/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php +++ b/app/code/Magento/Backend/Block/Widget/Form/Element/Gallery.php @@ -6,7 +6,10 @@ namespace Magento\Backend\Block\Widget\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Backend\Block\Template\Context; /** * Backend image gallery item renderer @@ -27,6 +30,18 @@ class Gallery extends \Magento\Backend\Block\Template implements protected $_template = 'Magento_Backend::widget/form/element/gallery.phtml'; /** + * @param Context $context + * @param array $data + */ + public function __construct(Context $context, array $data = []) + { + $data['jsonHelper'] = ObjectManager::getInstance()->get(JsonHelper::class); + parent::__construct($context, $data); + } + + /** + * Renderer. + * * @param AbstractElement $element * @return string */ @@ -37,6 +52,8 @@ public function render(AbstractElement $element) } /** + * Set element. + * * @param AbstractElement $element * @return $this */ @@ -47,6 +64,8 @@ public function setElement(AbstractElement $element) } /** + * Get element. + * * @return AbstractElement|null */ public function getElement() @@ -55,6 +74,8 @@ public function getElement() } /** + * Get value. + * * @return array */ public function getValues() @@ -63,7 +84,7 @@ public function getValues() } /** - * {@inheritdoc} + * @inheritdoc */ protected function _prepareLayout() { @@ -82,6 +103,8 @@ protected function _prepareLayout() } /** + * Return add button. + * * @return string */ public function getAddButtonHtml() @@ -90,6 +113,8 @@ public function getAddButtonHtml() } /** + * Return delete button. + * * @param string $image * @return string|string[] */ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php index 632603d389d21..65c63c9689fc5 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Date.php @@ -7,6 +7,8 @@ namespace Magento\Backend\Block\Widget\Grid\Column\Filter; use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Date grid column filter @@ -30,6 +32,11 @@ class Date extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFilt */ protected $dateTimeFormatter; + /** + * @var SecureHtmlRenderer + */ + protected $secureHtmlRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Framework\DB\Helper $resourceHelper @@ -37,6 +44,7 @@ class Date extends \Magento\Backend\Block\Widget\Grid\Column\Filter\AbstractFilt * @param \Magento\Framework\Locale\ResolverInterface $localeResolver * @param DateTimeFormatterInterface $dateTimeFormatter * @param array $data + * @param SecureHtmlRenderer|null $secureHtmlRenderer */ public function __construct( \Magento\Backend\Block\Context $context, @@ -44,16 +52,18 @@ public function __construct( \Magento\Framework\Math\Random $mathRandom, \Magento\Framework\Locale\ResolverInterface $localeResolver, DateTimeFormatterInterface $dateTimeFormatter, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null ) { $this->mathRandom = $mathRandom; $this->localeResolver = $localeResolver; parent::__construct($context, $resourceHelper, $data); $this->dateTimeFormatter = $dateTimeFormatter; + $this->secureHtmlRenderer = $secureHtmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** - * @return string + * @inheritDoc */ public function getHtml() { @@ -99,7 +109,7 @@ public function getHtml() ' value="' . $this->localeResolver->getLocale() . '"/>'; - $html .= '<script> + $scriptString = ' require(["jquery", "mage/calendar"], function($){ $("#' . $htmlId . @@ -120,12 +130,15 @@ public function getHtml() '_to" } }) - }); - </script>'; + });'; + $html .= $this->secureHtmlRenderer->renderTag('script', [], $scriptString, false); + return $html; } /** + * Return escaped value. + * * @param string|null $index * @return array|string|int|float|null */ @@ -147,6 +160,8 @@ public function getEscapedValue($index = null) } /** + * Return value. + * * @param string|null $index * @return array|string|int|float|null */ @@ -166,6 +181,8 @@ public function getValue($index = null) } /** + * Return conditions. + * * @return array|string|int|float|null */ public function getCondition() @@ -176,6 +193,8 @@ public function getCondition() } /** + * Set value. + * * @param array|string|int|float $value * @return $this */ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php index 1d8d658267020..a139d20191b57 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Filter/Datetime.php @@ -20,7 +20,7 @@ class Datetime extends \Magento\Backend\Block\Widget\Grid\Column\Filter\Date const END_OF_DAY_IN_SECONDS = 86399; /** - * {@inheritdoc} + * @inheritdoc */ public function getValue($index = null) { @@ -117,8 +117,7 @@ public function getHtml() ) . '/>' . '</div></div>'; $html .= '<input type="hidden" name="' . $this->_getHtmlName() . '[locale]"' . ' value="' . $this->localeResolver->getLocale() . '"/>'; - $html .= '<script> - require(["jquery", "mage/calendar"],function($){ + $scriptString = 'require(["jquery", "mage/calendar"],function($){ $("#' . $htmlId . '_range").dateRange({ dateFormat: "' . $format . '", timeFormat: "' . $timeFormat . '", @@ -131,8 +130,9 @@ public function getHtml() id: "' . $htmlId . '_to" } }) - }); - </script>'; + });'; + $html .= $this->secureHtmlRenderer->renderTag('script', [], $scriptString, false); + return $html; } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php index a7d85a4cfef4c..0da7e4db9b983 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Action.php @@ -6,6 +6,10 @@ namespace Magento\Backend\Block\Widget\Grid\Column\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Grid column widget for rendering action grid cells * @@ -20,18 +24,34 @@ class Action extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + private $secureHtmlRenderer; + + /** + * @var Random + */ + private $random; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param array $data + * @param SecureHtmlRenderer|null $secureHtmlRenderer + * @param Random|null $random */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null, + ?Random $random = null ) { $this->_jsonEncoder = $jsonEncoder; parent::__construct($context, $data); + $this->secureHtmlRenderer = $secureHtmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); } /** @@ -111,8 +131,22 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) unset($action['confirm']); } + if (empty($action['id'])) { + $action['id'] = 'id' .$this->random->getRandomString(10); + } $actionAttributes->setData($action); - return '<a ' . $actionAttributes->serialize() . '>' . $actionCaption . '</a>'; + $onclick = $actionAttributes->getData('onclick'); + $style = $actionAttributes->getData('style'); + $actionAttributes->unsetData(['onclick', 'style']); + $html = '<a ' . $actionAttributes->serialize() . '>' . $actionCaption . '</a>'; + if ($onclick) { + $html .= $this->secureHtmlRenderer->renderEventListenerAsTag('onclick', $onclick, "#{$action['id']}"); + } + if ($style) { + $html .= $this->secureHtmlRenderer->renderStyleAsTag($style, "#{$action['id']}"); + } + + return $html; } /** diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php index 1297f5cd330b8..013c3b7e105f5 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Column/Renderer/Checkbox.php @@ -5,6 +5,10 @@ */ namespace Magento\Backend\Block\Widget\Grid\Column\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Grid checkbox column renderer * @@ -29,18 +33,34 @@ class Checkbox extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Abstra */ protected $_converter; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param \Magento\Backend\Block\Context $context - * @param \Magento\Backend\Block\Widget\Grid\Column\Renderer\Options\Converter $converter + * @param Options\Converter $converter * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Backend\Block\Widget\Grid\Column\Renderer\Options\Converter $converter, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { parent::__construct($context, $data); $this->_converter = $converter; + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); } /** @@ -112,6 +132,8 @@ public function render(\Magento\Framework\DataObject $row) } /** + * Render checkbox HTML. + * * @param string $value Value of the element * @param bool $checked Whether it is checked * @return string @@ -154,11 +176,18 @@ public function renderHeader() if ($this->getColumn()->getDisabled()) { $disabled = ' disabled="disabled"'; } + $id = 'id' .$this->random->getRandomString(10); $html = '<th class="data-grid-th data-grid-actions-cell"><input type="checkbox" '; + $html .= 'id="' .$id .'" '; $html .= 'name="' . $this->getColumn()->getFieldName() . '" '; - $html .= 'onclick="' . $this->getColumn()->getGrid()->getJsObjectName() . '.checkCheckboxes(this)" '; $html .= 'class="admin__control-checkbox"' . $checked . $disabled . ' '; $html .= 'title="' . __('Select All') . '"/><label></label></th>'; + $html .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + $this->getColumn()->getGrid()->getJsObjectName() . '.checkCheckboxes(this)', + "#$id" + ); + return $html; } } diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Export.php b/app/code/Magento/Backend/Block/Widget/Grid/Export.php index 7b7f6cc14799c..11b24539e54f5 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Export.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Export.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** + * Class Export for exporting grid data as CSV file or MS Excel 2003 XML Document file + * * @api * @deprecated 100.2.0 in favour of UI component implementation * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -69,6 +71,8 @@ public function __construct( } /** + * Internal constructor, that is called from real constructor + * * @return void * @throws \Magento\Framework\Exception\LocalizedException */ @@ -242,6 +246,7 @@ protected function _getExportTotals() /** * Iterate collection and call callback method per item + * * For callback method first argument always is item object * * @param string $callback @@ -273,7 +278,12 @@ public function _exportIterateCollection($callback, array $args) $collection = $this->_getRowCollection($originalCollection); foreach ($collection as $item) { - call_user_func_array([$this, $callback], array_merge([$item], $args)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array( + [$this, $callback], + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + array_merge([$item], $args) + ); } } } @@ -307,7 +317,7 @@ protected function _exportCsvItem( */ public function getCsvFile() { - $name = md5(microtime()); + $name = hash('sha256', microtime()); $file = $this->_path . '/' . $name . '.csv'; $this->_directory->create($this->_path); @@ -432,11 +442,11 @@ public function getRowRecord(\Magento\Framework\DataObject $data) */ public function getExcelFile($sheetName = '') { - $collection = $this->_getRowCollection(); + $collection = $this->_getPreparedCollection(); $convert = new \Magento\Framework\Convert\Excel($collection->getIterator(), [$this, 'getRowRecord']); - $name = md5(microtime()); + $name = hash('sha256', microtime()); $file = $this->_path . '/' . $name . '.xml'; $this->_directory->create($this->_path); @@ -551,6 +561,8 @@ public function _getPreparedCollection() } /** + * Get export page size + * * @return int */ public function getExportPageSize() diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php index 40e87171e82cc..539b208436e87 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Extended.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Extended.php @@ -8,6 +8,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; /** + * Extended Grid Widget + * * @api * @deprecated 100.2.0 in favour of UI component implementation * @SuppressWarnings(PHPMD.ExcessivePublicCount) @@ -177,7 +179,10 @@ class Extended extends \Magento\Backend\Block\Widget\Grid implements \Magento\Ba protected $_path = 'export'; /** + * Initialization + * * @return void + * @throws \Magento\Framework\Exception\FileSystemException */ protected function _construct() { @@ -297,6 +302,7 @@ public function addColumn($columnId, $column) ); $this->getColumnSet()->getChildBlock($columnId)->setGrid($this); } else { + // phpcs:ignore Magento2.Exceptions.DirectThrow throw new \Exception(__('Please correct the column format and try again.')); } @@ -471,10 +477,6 @@ protected function _prepareMassactionColumn() protected function _prepareCollection() { if ($this->getCollection()) { - if ($this->getCollection()->isLoaded()) { - $this->getCollection()->clear(); - } - parent::_prepareCollection(); if (!$this->_isExport) { @@ -663,6 +665,7 @@ public function setEmptyCellLabel($label) */ public function getRowUrl($item) { + // phpstan:ignore "Call to an undefined static method" $res = parent::getRowUrl($item); return $res ? $res : '#'; } @@ -680,6 +683,7 @@ public function getMultipleRows($item) /** * Retrieve columns for multiple rows + * * @return array */ public function getMultipleRowColumns() @@ -943,6 +947,7 @@ protected function _getExportTotals() /** * Iterate collection and call callback method per item + * * For callback method first argument always is item object * * @param string $callback @@ -972,7 +977,12 @@ public function _exportIterateCollection($callback, array $args) $page++; foreach ($collection as $item) { - call_user_func_array([$this, $callback], array_merge([$item], $args)); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + call_user_func_array( + [$this, $callback], + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + array_merge([$item], $args) + ); } } } @@ -1009,6 +1019,7 @@ public function getCsvFile() $this->_isExport = true; $this->_prepareGrid(); + // phpcs:ignore Magento2.Security.InsecureFunction $name = md5(microtime()); $file = $this->_path . '/' . $name . '.csv'; @@ -1153,6 +1164,7 @@ public function getExcelFile($sheetName = '') [$this, 'getRowRecord'] ); + // phpcs:ignore Magento2.Security.InsecureFunction $name = md5(microtime()); $file = $this->_path . '/' . $name . '.xml'; @@ -1244,7 +1256,7 @@ public function setCollection($collection) } /** - * get collection object + * Get collection object * * @return \Magento\Framework\Data\Collection */ diff --git a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php index 662cbedaed8db..53e52fc7252b3 100644 --- a/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php +++ b/app/code/Magento/Backend/Block/Widget/Grid/Massaction.php @@ -64,6 +64,7 @@ public function __construct( * @param array|DataObject $item * * @return $this + * @since 100.2.3 */ public function addItem($itemId, $item) { diff --git a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php index a9be14b77b29c..7bef74862f029 100644 --- a/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php +++ b/app/code/Magento/Backend/Controller/Adminhtml/System/Store.php @@ -88,7 +88,7 @@ protected function createPage() * * @return bool * - * @deprecated Backup module is to be removed. + * @deprecated 100.2.7 Backup module is to be removed. */ protected function _backupDatabase() { diff --git a/app/code/Magento/Backend/Helper/Dashboard/Data.php b/app/code/Magento/Backend/Helper/Dashboard/Data.php index f691d2b7cd4b9..39ea634320b41 100644 --- a/app/code/Magento/Backend/Helper/Dashboard/Data.php +++ b/app/code/Magento/Backend/Helper/Dashboard/Data.php @@ -88,7 +88,7 @@ public function countStores() /** * Prepare array with periods for dashboard graphs * - * @deprecated periods were moved to it's own class + * @deprecated 102.0.0 periods were moved to it's own class * @see Period::getDatePeriods() * * @return array diff --git a/app/code/Magento/Backend/Model/Url.php b/app/code/Magento/Backend/Model/Url.php index 97f82647d9445..8948961be8875 100644 --- a/app/code/Magento/Backend/Model/Url.php +++ b/app/code/Magento/Backend/Model/Url.php @@ -372,6 +372,7 @@ protected function _getMenu() * * @param mixed $scopeId * @return \Magento\Framework\UrlInterface + * @since 101.0.3 */ public function setScope($scopeId) { diff --git a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml index e6782dca897d7..f9d3c49d509e9 100644 --- a/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml +++ b/app/code/Magento/Backend/Test/Mftf/Section/AdminMenuSection.xml @@ -17,11 +17,11 @@ <element name="widgets" type="button" selector="#nav li[data-ui-id='menu-magento-widget-cms-widget-instance']"/> <element name="stores" type="button" selector="#menu-magento-backend-stores"/> <element name="configuration" type="button" selector="#nav li[data-ui-id='menu-magento-config-system-config']"/> - <element name="dashboard" type="button" selector="//li[@id='menu-magento-backend-dashboard']"/> - <element name="sales" type="button" selector="//li[@id='menu-magento-sales-sales']"/> - <element name="marketing" type="button" selector="//li[@id='menu-magento-backend-marketing']"/> - <element name="system" type="button" selector="//li[@id='menu-magento-backend-system']"/> - <element name="findPartners" type="button" selector="//li[@id='menu-magento-marketplace-partners']"/> + <element name="dashboard" type="button" selector="#menu-magento-backend-dashboard"/> + <element name="sales" type="button" selector="#menu-magento-sales-sales"/> + <element name="marketing" type="button" selector="#menu-magento-backend-marketing"/> + <element name="system" type="button" selector="#menu-magento-backend-system"/> + <element name="findPartners" type="button" selector="#menu-magento-marketplace-partners"/> <!-- Navigate menu selectors --> <element name="menuItem" type="button" selector="li[data-ui-id='menu-{{dataUiId}}']" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml index 8ac7af096da0a..ef69764a87833 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminAttributeTextSwatchesCanBeFiledTest.xml @@ -94,8 +94,7 @@ </actionGroup> <!--Navigate to Product attribute page--> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> <fillField userInput="test_label" selector="{{AttributePropertiesSection.DefaultLabel}}" stepKey="fillDefaultLabel"/> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="Text Swatch" stepKey="selectInputType"/> <click selector="{{AttributePropertiesSection.addSwatch}}" stepKey="clickAddSwatch"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml index 972947656cd3d..c120b210d37e5 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminDashboardWithChartsTest.xml @@ -89,12 +89,11 @@ <waitForPageLoad stepKey="waitForInvoicePageToLoad"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seeNewInvoiceInPageTitle" after="clickInvoiceButton"/> <see selector="{{AdminInvoiceTotalSection.total('Subtotal')}}" userInput="$150.00" stepKey="seeCorrectGrandTotal"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessInvoiceMessage"/> <!--Create Shipment for the order--> <comment userInput="Create Shipment for the order" stepKey="createShipmentForOrder"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage2"/> - <waitForPageLoad time="30" stepKey="waitForOrderListPageLoading"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage2"/> <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="openOrderPageForShip"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> <waitForPageLoad stepKey="waitForShipmentPagePage"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml index be734205e1f5b..c5b4e8c34bfec 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterChangeCookieDomainTest.xml @@ -21,11 +21,15 @@ </annotations> <before> <magentoCLI command="config:set {{ChangedCookieDomainForMainWebsiteConfigData.path}} --scope={{ChangedCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{ChangedCookieDomainForMainWebsiteConfigData.scope_code}} {{ChangedCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteBeforeTestRun"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBeforeTestRun"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{EmptyCookieDomainForMainWebsiteConfigData.path}} --scope={{EmptyCookieDomainForMainWebsiteConfigData.scope}} --scope-code={{EmptyCookieDomainForMainWebsiteConfigData.scope_code}} {{EmptyCookieDomainForMainWebsiteConfigData.value}}" stepKey="changeDomainForMainWebsiteAfterTestComplete"/> - <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestComplete"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTestComplete"> + <argument name="tags" value="config"/> + </actionGroup> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AssertAdminDashboardPageIsVisibleActionGroup" stepKey="seeDashboardPage"/> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml index d2c628ed13701..fb58b59b0ccaa 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminLoginAfterJSMinificationTest.xml @@ -21,7 +21,9 @@ </annotations> <before> <magentoCLI command="config:set {{MinifyJavaScriptFilesEnableConfigData.path}} {{MinifyJavaScriptFilesEnableConfigData.value}}" stepKey="enableJsMinification"/> - <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml index 812158948d85f..aa246cb5f9d22 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminMenuNavigationWithSecretKeysTest.xml @@ -20,12 +20,16 @@ </annotations> <before> <magentoCLI command="config:set admin/security/use_form_key 1" stepKey="enableUrlSecretKeys"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches1"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> <magentoCLI command="config:set admin/security/use_form_key 0" stepKey="disableUrlSecretKeys"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches2"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches2"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml b/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml index 387e81cb71546..bb69aa218e77a 100644 --- a/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml +++ b/app/code/Magento/Backend/Test/Mftf/Test/AdminPersistentShoppingCartSettingsTest.xml @@ -21,11 +21,15 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <magentoCLI stepKey="enablePersistentShoppingCart" command="config:set persistent/options/enabled 1"/> - <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <magentoCLI stepKey="disablePersistentShoppingCart" command="config:set persistent/options/enabled 0"/> - <magentoCLI stepKey="cacheClean" command="cache:clean config"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php index deb9c300f41b8..b841ad271ac43 100644 --- a/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php +++ b/app/code/Magento/Backend/Test/Unit/Block/Widget/Grid/ExtendedTest.php @@ -41,8 +41,8 @@ public function testPrepareLoadedCollection() $layout->expects($this->any())->method('getBlock')->willReturn($columnSet); $collection = $this->createMock(Collection::class); - $collection->expects($this->atLeastOnce())->method('isLoaded')->willReturn(true); - $collection->expects($this->atLeastOnce())->method('clear'); + $collection->expects($this->never())->method('isLoaded'); + $collection->expects($this->never())->method('clear'); $collection->expects($this->atLeastOnce())->method('load'); /** @var Extended $block */ diff --git a/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php b/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php index 3f4f3669ab75b..d3c177fa907ab 100644 --- a/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php +++ b/app/code/Magento/Backend/Ui/Component/Control/DeleteButton.php @@ -17,6 +17,7 @@ * Provide an ability to show confirmation message on click on the "Delete" button * * @api + * @since 101.0.0 */ class DeleteButton implements ButtonProviderInterface { @@ -84,6 +85,7 @@ public function __construct( /** * {@inheritdoc} + * @since 101.0.0 */ public function getButtonData() { diff --git a/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php b/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php index 75d6bad06e239..f85264e532057 100644 --- a/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php +++ b/app/code/Magento/Backend/Ui/Component/Control/SaveSplitButton.php @@ -13,6 +13,7 @@ * Provide an ability to show drop-down list with options clicking on the "Save" button * * @api + * @since 101.0.0 */ class SaveSplitButton implements ButtonProviderInterface { @@ -31,6 +32,7 @@ public function __construct(string $targetName) /** * {@inheritdoc} + * @since 101.0.0 */ public function getButtonData() { diff --git a/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php b/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php index fb0aa6987f4d9..1769bd7b3bb64 100644 --- a/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php +++ b/app/code/Magento/Backend/Ui/Component/Listing/Column/EditAction.php @@ -14,6 +14,7 @@ * Represents Edit link in grid for entity by its identifier field * * @api + * @since 101.0.0 */ class EditAction extends Column { @@ -43,6 +44,7 @@ public function __construct( /** * @param array $dataSource * @return array + * @since 101.0.0 */ public function prepareDataSource(array $dataSource) { diff --git a/app/code/Magento/Backend/etc/adminhtml/system.xml b/app/code/Magento/Backend/etc/adminhtml/system.xml index 45ed50fd49b7e..57cc36da95cfe 100644 --- a/app/code/Magento/Backend/etc/adminhtml/system.xml +++ b/app/code/Magento/Backend/etc/adminhtml/system.xml @@ -146,7 +146,7 @@ <label>Allow Symlinks</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment> - <![CDATA[<strong style="color:red">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.]]> + <![CDATA[<strong class="colorRed">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.]]> </comment> </field> <field id="minify_html" translate="label comment" type="select" sortOrder="25" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> @@ -459,7 +459,9 @@ <label>Add Store Code to Urls</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <backend_model>Magento\Config\Model\Config\Backend\Store</backend_model> - <comment><![CDATA[<strong style="color:red">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).]]></comment> + <comment> + <![CDATA[<strong class="colorRed">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).]]> + </comment> </field> <field id="redirect_to_base" translate="label comment" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Auto-redirect to Base URL</label> diff --git a/app/code/Magento/Backend/i18n/en_US.csv b/app/code/Magento/Backend/i18n/en_US.csv index 59bbe7f69985a..74633141c89fe 100644 --- a/app/code/Magento/Backend/i18n/en_US.csv +++ b/app/code/Magento/Backend/i18n/en_US.csv @@ -332,7 +332,7 @@ Debug,Debug "Add Block Names to Hints","Add Block Names to Hints" "Template Settings","Template Settings" "Allow Symlinks","Allow Symlinks" -"<strong style=""color:red"">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.","<strong style=""color:red"">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk." +"<strong class=""colorRed">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk.","<strong class=""colorRed"">Warning!</strong> Enabling this feature is not recommended on production environments because it represents a potential security risk." "Minify Html","Minify Html" "Minification is not applied in developer mode.","Minification is not applied in developer mode." "Translate Inline","Translate Inline" @@ -403,6 +403,11 @@ Security,Security Web,Web "Url Options","Url Options" "Add Store Code to Urls","Add Store Code to Urls" +" + <strong class=""colorRed">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.). + "," + <strong class=""colorRed">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.). + " "<strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.).","<strong style=""color:red"">Warning!</strong> When using Store Code in URLs, in some cases system may not work properly if URLs without Store Codes are specified in the third-party services (e.g. PayPal etc.)." "Enable Frontend Resize","Enable Frontend Resize" "Resize performed via javascript before file upload.","Resize performed via javascript before file upload." diff --git a/app/code/Magento/Backend/view/adminhtml/layout/default.xml b/app/code/Magento/Backend/view/adminhtml/layout/default.xml index a7faab0bc4673..0d629e31d6d91 100644 --- a/app/code/Magento/Backend/view/adminhtml/layout/default.xml +++ b/app/code/Magento/Backend/view/adminhtml/layout/default.xml @@ -70,7 +70,6 @@ <argument name="bugreport_url" xsi:type="string">https://github.com/magento/magento2/issues</argument> </arguments> </block> - </container> </container> </referenceContainer> diff --git a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js index e886f28cd158b..ae0e84e2d27f8 100644 --- a/app/code/Magento/Backend/view/adminhtml/requirejs-config.js +++ b/app/code/Magento/Backend/view/adminhtml/requirejs-config.js @@ -6,8 +6,7 @@ var config = { map: { '*': { - 'mediaUploader': 'Magento_Backend/js/media-uploader', - 'mage/translate': 'Magento_Backend/js/translate' + 'mediaUploader': 'Magento_Backend/js/media-uploader' } } }; diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml index 65c0d292ee187..56131fa622321 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/chart.phtml @@ -7,18 +7,23 @@ use Magento\Backend\ViewModel\ChartsPeriod; use Magento\Framework\Escaper; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @var Template $block * @var Escaper $escaper * @var ChartsPeriod $viewModel + * @var SecureHtmlRenderer $secureRenderer */ $viewModel = $block->getViewModel(); ?> <div class="dashboard-diagram"> <div class="dashboard-diagram-graph"> - <canvas id="chart_<?= $escaper->escapeHtmlAttr($block->getData('html_id')) ?>_period" - style="display: none;"></canvas> + <canvas id="chart_<?= $escaper->escapeHtmlAttr($block->getData('html_id')) ?>_period"/> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#chart_' . $escaper->escapeJs($block->getData('html_id')) . '_period' + ) ?> <div class="dashboard-diagram-nodata"> <span><?= $escaper->escapeHtml(__('No Data Found')) ?></span> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml index 7c05335642ba7..a2eb24476726e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/grid.phtml @@ -3,89 +3,113 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $numColumns = count($block->getColumns()); ?> -<?php if ($block->getCollection()) : ?> -<div class="dashboard-item-content"> - <?php if ($block->getCollection()->getSize() > 0) : ?> - <table class="admin__table-primary dashboard-data" id="<?= $block->escapeHtmlAttr($block->getId()) ?>_table"> - <?php - /* This part is commented to remove all <col> tags from the code. */ - /* foreach ($block->getColumns() as $_column): ?> - <col <?= $_column->getHtmlProperty() ?> /> - <?php endforeach; */ ?> - <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()) : ?> - <thead> - <?php if ($block->getHeadersVisibility()) : ?> - <tr> - <?php foreach ($block->getColumns() as $_column) : ?> - <?= $_column->getHeaderHtml() ?> - <?php endforeach; ?> - </tr> +<?php if ($block->getCollection()): ?> + <div class="dashboard-item-content"> + <?php if ($block->getCollection()->getSize() > 0): ?> + <table class="admin__table-primary dashboard-data" + id="<?= $block->escapeHtmlAttr($block->getId()) ?>_table"> + <?php + /* This part is commented to remove all <col> tags from the code. */ + /* foreach ($block->getColumns() as $_column): ?> + <col <?= $_column->getHtmlProperty() ?> /> + <?php endforeach; */ ?> + <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()): ?> + <thead> + <?php if ($block->getHeadersVisibility()): ?> + <tr> + <?php foreach ($block->getColumns() as $_column): ?> + <?= $_column->getHeaderHtml() ?> + <?php endforeach; ?> + </tr> + <?php endif; ?> + </thead> <?php endif; ?> - </thead> - <?php endif; ?> - <?php if (!$block->getIsCollapsed()) : ?> - <tbody> - <?php foreach ($block->getCollection() as $_index => $_item) : ?> - <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"> - <?php $i = 0; foreach ($block->getColumns() as $_column) : ?> - <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> <?= /* @noEscape */ ++$i == $numColumns ? 'last' : '' ?>"><?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?></td> + <?php if (!$block->getIsCollapsed()): ?> + <tbody> + <?php foreach ($block->getCollection() as $_index => $_item): ?> + <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"> + <?php $i = 0; foreach ($block->getColumns() as $_column): ?> + <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()); + ?> <?= /* @noEscape */ ++$i == $numColumns ? 'last' : ''; +?>"><?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? + $_html : ' ') ?></td> + <?php endforeach; ?> + </tr> <?php endforeach; ?> - </tr> - <?php endforeach; ?> - </tbody> - <?php endif; ?> - </table> - <?php else : ?> - <div class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></div> - <?php endif; ?> -</div> - <?php if ($block->canDisplayContainer()) : ?> -<script> -var deps = []; - - <?php if ($block->getDependencyJsObject()) : ?> -deps.push('uiRegistry'); + </tbody> + <?php endif; ?> + </table> + <?php else: ?> + <div class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()); + ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></div> <?php endif; ?> + </div> + <?php if ($block->canDisplayContainer()): ?> + <?php $scriptString = 'var deps = [];' . PHP_EOL; + if ($block->getDependencyJsObject()) { + $scriptString .= 'deps.push(\'uiRegistry\');' . PHP_EOL; + } - <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> -deps.push('Magento_Sales/order/create/form'); - <?php endif; ?> + if (strpos($block->getRowClickCallback(), 'order.') !== false) { + $scriptString .= 'deps.push(\'Magento_Sales/order/create/form\');' . PHP_EOL; + } -deps.push('mage/adminhtml/grid'); + $scriptString .= 'deps.push(\'mage/adminhtml/grid\');' . PHP_EOL; -require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> + $scriptString .= 'require(deps, function('. ($block->getDependencyJsObject() ? 'registry' : '') .'){' . + PHP_EOL . + '//TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed' . PHP_EOL; - <?php if ($block->getDependencyJsObject()) : ?> - registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { - <?php endif; ?> + if ($block->getDependencyJsObject()) { + $scriptString .= 'registry.get(\'' . $block->escapeJs($block->getDependencyJsObject()) . + '\', function ('. $block->escapeJs($block->getDependencyJsObject()) . ') {' . PHP_EOL; + } - <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid('<?= $block->escapeJs($block->getId()) ?>', '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); - <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = '<?= $block->escapeJs($block->getUseAjax()) ?>'; - <?php if ($block->getRowClickCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; - <?php endif; ?> - <?php if ($block->getCheckboxCheckCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; - <?php endif; ?> - <?php if ($block->getRowInitCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; - <?= $block->escapeJs($block->getJsObjectName()) ?>.rows.each(function(row){<?= /* @noEscape */ $block->getRowInitCallback() ?>(<?= $block->escapeJs($block->getJsObjectName()) ?>, row)}); - <?php endif; ?> - <?php if ($block->getMassactionBlock()->isAvailable()) : ?> - <?= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() ?> - <?php endif ?> + $scriptString .= $block->escapeJs($block->getJsObjectName()) . ' = new varienGrid(\'' . + $block->escapeJs($block->getId()) . '\', \'' . $block->escapeJs($block->getGridUrl()) . '\', \'' . + $block->escapeJs($block->getVarNamePage()) .'\', \'' . + $block->escapeJs($block->getVarNameSort()) . '\', \'' . + $block->escapeJs($block->getVarNameDir()) . '\', \'' . + $block->escapeJs($block->getVarNameFilter()) .'\');' . PHP_EOL; - <?php if ($block->getDependencyJsObject()) : ?> - }); - <?php endif; ?> + $scriptString .= $block->escapeJs($block->getJsObjectName()) .'.useAjax = \'' . + $block->escapeJs($block->getUseAjax()) . '\';' . PHP_EOL; + if ($block->getRowClickCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.rowClickCallback = ' . + /* @noEscape */ $block->getRowClickCallback() . ';' . PHP_EOL; + } + + if ($block->getCheckboxCheckCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.checkboxCheckCallback = ' . + /* @noEscape */ $block->getCheckboxCheckCallback() . ';' . PHP_EOL; + } + + if ($block->getRowInitCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.initRowCallback = ' . + /* @noEscape */ $block->getRowInitCallback() . ';' . PHP_EOL; + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.rows.each(function(row){' . + /* @noEscape */ $block->getRowInitCallback() . '(' . $block->escapeJs($block->getJsObjectName()) . + ', row)});' . PHP_EOL; + } -}); -</script> -<?php endif; ?> + if ($block->getMassactionBlock()->isAvailable()) { + $scriptString .= /* @noEscape */ $block->getMassactionBlock()->getJavaScript(); + } + + if ($block->getDependencyJsObject()) { + $scriptString .= '});' . PHP_EOL; + } + + $scriptString .= '});' . PHP_EOL; + + echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); + ?> + <?php endif; ?> <?php endif ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml index 87e5399ddda44..7cc9b781f579e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/dashboard/store/switcher.phtml @@ -3,35 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <p class="switcher"><label for="store_switcher"><?= $block->escapeHtml(__('View Statistics For:')) ?></label> <?= $block->getHintHtml() ?> -<select name="store_switcher" id="store_switcher" class="left-col-block" onchange="return switchStore(this);"> +<select name="store_switcher" id="store_switcher" class="left-col-block"> <option value=""><?= $block->escapeHtml(__('All Websites')) ?></option> - <?php foreach ($block->getWebsiteCollection() as $_website) : ?> + <?php foreach ($block->getWebsiteCollection() as $_website): ?> <?php $showWebsite = false; ?> - <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> + <?php foreach ($block->getGroupCollection($_website) as $_group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStoreCollection($_group) as $_store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> - <option website="true" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>"<?php if ($block->getRequest()->getParam('website') == $_website->getId()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_website->getName()) ?></option> + <option website="true" + value="<?= $block->escapeHtmlAttr($_website->getId()) ?>"<?php + if ($block->getRequest()->getParam('website') == $_website->getId()): + ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_website->getName()) ?> + </option> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> <!--optgroup label="   <?= $block->escapeHtmlAttr($_group->getName()) ?>"--> - <option group="true" value="<?= $block->escapeHtmlAttr($_group->getId()) ?>"<?php if ($block->getRequest()->getParam('group') == $_group->getId()) : ?> selected="selected"<?php endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> + <option group="true" value="<?= $block->escapeHtmlAttr($_group->getId()) ?>"<?php + if ($block->getRequest()->getParam('group') == $_group->getId()): ?> selected="selected"<?php + endif; ?>>   <?= $block->escapeHtml($_group->getName()) ?></option> <?php endif; ?> - <option value="<?= $block->escapeHtmlAttr($_store->getId()) ?>"<?php if ($block->getStoreId() == $_store->getId()) : ?> selected="selected"<?php endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> + <option value="<?= $block->escapeHtmlAttr($_store->getId()) ?>"<?php + if ($block->getStoreId() == $_store->getId()): ?> selected="selected"<?php + endif; ?>>      <?= $block->escapeHtml($_store->getName()) ?></option> <?php endforeach; ?> - <?php if ($showGroup) : ?> + <?php if ($showGroup): ?> <!--</optgroup>--> <?php endif; ?> <?php endforeach; ?> <?php endforeach; ?> </select> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'return switchStore(this);', + 'select#store_switcher' +) ?> </p> -<script> +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -54,7 +69,9 @@ var select = $('order_amounts_period'); } var periodParam = select.value ? 'period/' + select.value + '/' : ''; - setLocation('<?= $block->escapeJs($block->getSwitchUrl()) ?>' + storeParam + periodParam); + setLocation('{$block->escapeJs($block->getSwitchUrl())}' + storeParam + periodParam); } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml index 94df9ef9eb872..1ea5e225c8402 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/js/calendar.phtml @@ -11,9 +11,10 @@ * * @see \Magento\Framework\View\Element\Html\Calendar */ -?> -<script> +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$scriptString = ' require([ "jquery", "jquery/ui" @@ -21,20 +22,20 @@ require([ $.extend(true, $, { calendarConfig: { - dayNames: <?= /* @noEscape */ $days['wide'] ?>, - dayNamesMin: <?= /* @noEscape */ $days['abbreviated'] ?>, - monthNames: <?= /* @noEscape */ $months['wide'] ?>, - monthNamesShort: <?= /* @noEscape */ $months['abbreviated'] ?>, - infoTitle: "<?= $block->escapeJs(__('About the calendar')) ?>", - firstDay: <?= /* @noEscape */ $firstDay ?>, - closeText: "<?= $block->escapeJs(__('Close')) ?>", - currentText: "<?= $block->escapeJs(__('Go Today')) ?>", - prevText: "<?= $block->escapeJs(__('Previous')) ?>", - nextText: "<?= $block->escapeJs(__('Next')) ?>", - weekHeader: "<?= $block->escapeJs(__('WK')) ?>", - timeText: "<?= $block->escapeJs(__('Time')) ?>", - hourText: "<?= $block->escapeJs(__('Hour')) ?>", - minuteText: "<?= $block->escapeJs(__('Minute')) ?>", + dayNames: ' . /* @noEscape */ $days['wide'] . ', + dayNamesMin: ' . /* @noEscape */ $days['abbreviated'] . ', + monthNames: ' . /* @noEscape */ $months['wide'] . ', + monthNamesShort: ' . /* @noEscape */ $months['abbreviated'] . ', + infoTitle: "' . $block->escapeJs(__('About the calendar')) . '", + firstDay: ' . /* @noEscape */ $firstDay . ', + closeText: "' . $block->escapeJs(__('Close')) . '", + currentText: "' . $block->escapeJs(__('Go Today')) . '", + prevText: "' . $block->escapeJs(__('Previous')) . '", + nextText: "' . $block->escapeJs(__('Next')) . '", + weekHeader: "' . $block->escapeJs(__('WK')) . '", + timeText: "' . $block->escapeJs(__('Time')) . '", + hourText: "' . $block->escapeJs(__('Hour')) . '", + minuteText: "' . $block->escapeJs(__('Minute')) . '", dateFormat: $.datepicker.RFC_2822, showOn: "button", showAnim: "", @@ -45,17 +46,18 @@ require([ showButtonPanel: true, showOtherMonths: true, showWeek: false, - timeFormat: '', + timeFormat: \'\', showTime: false, showHour: false, showMinute: false, - serverTimezoneSeconds: <?= (int) $block->getStoreTimestamp() ?>, - serverTimezoneOffset: <?= (int) $block->getTimezoneOffsetSeconds() ?>, - yearRange: '<?= $block->escapeJs($block->getYearRange()) ?>' + serverTimezoneSeconds: ' . (int) $block->getStoreTimestamp() . ', + serverTimezoneOffset: ' . (int) $block->getTimezoneOffsetSeconds() . ', + yearRange: \'' . $block->escapeJs($block->getYearRange()) . '\' } }); -enUS = <?= /* @noEscape */ $enUS ?>; // en_US locale reference +enUS = ' . /* @noEscape */ $enUS . '; // en_US locale reference + +});'; -}); -</script> +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml b/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml index 68453d9ff8ff2..6fa41e1079950 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/page/js/require_js.phtml @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<script> - var BASE_URL = '<?= /* @noEscape */ $block->getUrl('*') ?>'; - var FORM_KEY = '<?= /* @noEscape */ $block->getFormKey() ?>'; + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$scriptString = ' + var BASE_URL = \'' . /* @noEscape */ $block->getUrl('*') . '\'; + var FORM_KEY = \'' . /* @noEscape */ $block->getFormKey() . '\'; var require = { - "baseUrl": "<?= /* @noEscape */ $block->getViewFileUrl('/') ?>" - }; -</script> + \'baseUrl\': \'' . /* @noEscape */ $block->getViewFileUrl('/') . '\' + };'; + +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml index df9323a7276df..c6fcaff9cd877 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher.phtml @@ -5,24 +5,39 @@ */ /* @var $block \Magento\Backend\Block\Store\Switcher */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($websites = $block->getWebsites()) : ?> - +<?php if ($websites = $block->getWebsites()): ?> <div class="store-switcher store-view"> <span class="store-switcher-label"><?= $block->escapeHtml(__('Scope:')) ?></span> <div class="actions dropdown closable"> <input type="hidden" name="store_switcher" id="store_switcher" data-role="store-view-id" data-param="<?= $block->escapeHtmlAttr($block->getStoreVarName()) ?>" value="<?= $block->escapeHtml($block->getStoreId()) ?>" - onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'switchScope(this);', + '#store_switcher' + ) ?> <input type="hidden" name="store_group_switcher" id="store_group_switcher" data-role="store-group-id" data-param="<?= $block->escapeHtmlAttr($block->getStoreGroupVarName()) ?>" value="<?= $block->escapeHtml($block->getStoreGroupId()) ?>" - onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'switchScope(this);', + '#store_group_switcher' + ) ?> <input type="hidden" name="website_switcher" id="website_switcher" data-role="website-id" data-param="<?= $block->escapeHtmlAttr($block->getWebsiteVarName()) ?>" value="<?= $block->escapeHtml($block->getWebsiteId()) ?>" - onchange="switchScope(this);"<?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $block->getUiId() ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'switchScope(this);', + '#website_switcher' + ) ?> <button type="button" class="admin__action-dropdown" @@ -33,61 +48,75 @@ <?= $block->escapeHtml($block->getCurrentSelectionName()) ?> </button> <ul class="dropdown-menu" data-role="stores-list"> - <?php if ($block->hasDefaultOption()) : ?> - <li class="store-switcher-all <?php if (!($block->getDefaultSelectionName() != $block->getCurrentSelectionName())) : ?>disabled<?php endif; ?> <?php if (!$block->hasScopeSelected()) : ?>current<?php endif; ?>"> - <?php if ($block->getDefaultSelectionName() != $block->getCurrentSelectionName()) : ?> + <?php if ($block->hasDefaultOption()): ?> + <li class="store-switcher-all <?php + if (!($block->getDefaultSelectionName() != $block->getCurrentSelectionName())): ?>disabled<?php endif; + ?> <?php if (!$block->hasScopeSelected()): ?>current<?php endif; ?>"> + <?php if ($block->getDefaultSelectionName() != $block->getCurrentSelectionName()): ?> <a data-role="store-view-id" data-value="" href="#"> <?= $block->escapeHtml($block->getDefaultSelectionName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($block->getDefaultSelectionName()) ?></span> <?php endif; ?> </li> <?php endif; ?> - <?php foreach ($websites as $website) : ?> + <?php foreach ($websites as $website): ?> <?php $showWebsite = false; ?> - <?php foreach ($website->getGroups() as $group) : ?> + <?php foreach ($website->getGroups() as $group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStores($group) as $store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStores($group) as $store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> - <li class="store-switcher-website <?php if (!($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website))) : ?>disabled<?php endif; ?> <?php if ($block->isWebsiteSelected($website)) : ?>current<?php endif; ?>"> - <?php if ($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website)) : ?> - <a data-role="website-id" data-value="<?= $block->escapeHtml($website->getId()) ?>" href="#"> + <li class="store-switcher-website <?php if (!($block->isWebsiteSwitchEnabled() && + ! $block->isWebsiteSelected($website))): ?>disabled<?php endif; ?> <?php +if ($block->isWebsiteSelected($website)): ?>current<?php endif; ?>"> + <?php if ($block->isWebsiteSwitchEnabled() && ! $block->isWebsiteSelected($website)): ?> + <a data-role="website-id" data-value="<?= $block->escapeHtmlAttr($website->getId()); + ?>" href="#"> <?= $block->escapeHtml($website->getName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($website->getName()) ?></span> <?php endif; ?> </li> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> - <li class="store-switcher-store <?php if (!($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group))) : ?>disabled<?php endif; ?> <?php if ($block->isStoreGroupSelected($group)) : ?>current<?php endif; ?>"> - <?php if ($block->isStoreGroupSwitchEnabled() && ! $block->isStoreGroupSelected($group)) : ?> - <a data-role="store-group-id" data-value="<?= $block->escapeHtml($group->getId()) ?>" href="#"> + <li class="store-switcher-store <?php if (!($block->isStoreGroupSwitchEnabled() && + ! $block->isStoreGroupSelected($group))): ?>disabled<?php endif; ?> <?php +if ($block->isStoreGroupSelected($group)): ?>current<?php endif; ?>"> + <?php if ($block->isStoreGroupSwitchEnabled() && + ! $block->isStoreGroupSelected($group)): ?> + <a data-role="store-group-id" + data-value="<?= $block->escapeHtmlAttr($group->getId()) ?>" href="#"> <?= $block->escapeHtml($group->getName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($group->getName()) ?></span> <?php endif; ?> </li> <?php endif; ?> - <li class="store-switcher-store-view <?php if (!($block->isStoreSwitchEnabled() && !$block->isStoreSelected($store))) : ?>disabled<?php endif; ?> <?php if ($block->isStoreSelected($store)) :?>current<?php endif; ?>"> - <?php if ($block->isStoreSwitchEnabled() && ! $block->isStoreSelected($store)) : ?> - <a data-role="store-view-id" data-value="<?= $block->escapeHtml($store->getId()) ?>" href="#"> + <li class="store-switcher-store-view <?php if (!($block->isStoreSwitchEnabled() && + !$block->isStoreSelected($store))): ?>disabled<?php endif; ?> <?php +if ($block->isStoreSelected($store)):?>current<?php endif; ?>"> + <?php if ($block->isStoreSwitchEnabled() && ! $block->isStoreSelected($store)): ?> + <a data-role="store-view-id" + data-value="<?= $block->escapeHtmlAttr($store->getId()) ?>" href="#"> <?= $block->escapeHtml($store->getName()) ?> </a> - <?php else : ?> + <?php else: ?> <span><?= $block->escapeHtml($store->getName()) ?></span> <?php endif; ?> </li> <?php endforeach; ?> <?php endforeach; ?> <?php endforeach; ?> - <?php if ($block->getShowManageStoresLink() && $block->getAuthorization()->isAllowed('Magento_Backend::store')) : ?> + <?php if ($block->getShowManageStoresLink() && + $block->getAuthorization()->isAllowed('Magento_Backend::store')): ?> <li class="dropdown-toolbar"> - <a href="<?= /* @noEscape */ $block->getUrl('*/system_store') ?>"><?= $block->escapeHtml(__('Stores Configuration')) ?></a> + <a href="<?= /* @noEscape */ $block->getUrl('*/system_store'); + ?>"><?= $block->escapeHtml(__('Stores Configuration')) ?></a> </li> <?php endif; ?> </ul> @@ -95,15 +124,17 @@ <?= $block->getHintHtml() ?> </div> -<script> + <?php + $useConfirm = (int)$block->getUseConfirm(); + $scriptString = <<<script require([ 'jquery', 'Magento_Ui/js/modal/confirm' ], function(jQuery, confirm){ (function($) { - var $storesList = $('[data-role=stores-list]'); - $storesList.on('click', '[data-value]', function(event) { + var storesList = $('[data-role=stores-list]'); + storesList.on('click', '[data-value]', function(event) { var val = $(event.target).data('value'); var role = $(event.target).data('role'); var switcher = $('[data-role='+role+']'); @@ -134,35 +165,42 @@ require([ var switcherParams = { scopeId: scopeId, scopeParams: scopeParams, - useConfirm: <?= (int)$block->getUseConfirm() ?> + useConfirm: {$useConfirm} }; scopeSwitcherHandler(switcherParams); } else { - - <?php if ($block->getUseConfirm()) : ?> - +script; + if ($block->getUseConfirm()) { + $scriptString .= ' confirm({ - content: "<?= $block->escapeJs(__('Please confirm scope switching. All data that hasn\'t been saved will be lost.')) ?>", + content: "' . $block->escapeJs(__( + 'Please confirm scope switching. All data that hasn\'t been saved will be lost.' + )) . '", actions: { confirm: function() { reload(); }, cancel: function() { - obj.value = '<?= $block->escapeHtml($block->getStoreId()) ?>'; + obj.value = \'' . $block->escapeJs($block->getStoreId()) . '\'; } } }); - - <?php else : ?> - reload(); - <?php endif; ?> +'; + } else { + $scriptString .= 'reload();'; + } + $scriptString .= ' } function reload() { - <?php if (!$block->isUsingIframe()) : ?> - var url = '<?= $block->escapeJs($block->getSwitchUrl()) ?>' + scopeParams; - setLocation(url); - <?php else : ?> + '; + if (!$block->isUsingIframe()) { + $scriptString .= ' + var url = \'' . $block->escapeJs($block->getSwitchUrl()) . '\' + scopeParams; + setLocation(url); +'; + } else { + $scriptString .= <<<script jQuery('#preview_selected_store').val(scopeId); jQuery('#preview_form').submit(); @@ -175,7 +213,9 @@ require([ }); jQuery('#store-change-button').click(); - <?php endif; ?> +script; + } + $scriptString .= <<<script } } @@ -183,5 +223,7 @@ require([ window.switchScope = switchScope; }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml index 959a27279e5c2..eb64cc602eaf9 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/store/switcher/form/renderer/fieldset/element.phtml @@ -21,19 +21,25 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' . $block->getUiId('form-field', $element->getId()); ?> -<?php if (!$element->getNoDisplay()) : ?> - <?php if ($element->getType() == 'hidden') : ?> +<?php if (!$element->getNoDisplay()): ?> + <?php if ($element->getType() == 'hidden'): ?> <?= $element->getElementHtml() ?> - <?php else : ?> - <div<?= /* @noEscape */ $fieldAttributes ?>> - <?php if ($elementBeforeLabel) : ?> + <?php else: ?> + <div <?= /* @noEscape */ $fieldAttributes ?>> + <?php if ($elementBeforeLabel): ?> <?= $element->getElementHtml() ?> <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> <?= /* @noEscape */ $note ?> - <?php else : ?> + <?php else: ?> <?= $element->getLabelHtml('', $element->getScopeLabel()) ?> <div class="admin__field-control control"> - <?= /* @noEscape */ ($addOn) ? '<div class="addon">' . $element->getElementHtml() . '</div>' : $element->getElementHtml() ?> + <?php if ($addOn): ?> + <div class="addon"> + <?php endif; ?> + <?= /* @noEscape */ $element->getElementHtml() ?> + <?php if ($addOn): ?> + </div> + <?php endif; ?> <?= $block->getHintHtml() ?> <?= /* @noEscape */ $note ?> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml index c392ebf3883d2..4b80d2863ad93 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/additional.phtml @@ -5,34 +5,51 @@ */ /** @var \Magento\Backend\Block\Cache\Permissions|null $permissions */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $permissions = $block->getData('permissions'); ?> -<?php if ($permissions && $permissions->hasAccessToAdditionalActions()) : ?> +<?php if ($permissions && $permissions->hasAccessToAdditionalActions()): ?> <div class="additional-cache-management"> <h2> <span><?= $block->escapeHtml(__('Additional Cache Management')); ?></span> </h2> - <?php if ($permissions->hasAccessToFlushCatalogImages()) : ?> + <?php if ($permissions->hasAccessToFlushCatalogImages()): ?> <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanImagesUrl()); ?>')" type="button"> + <button id="flushCatalogImages" type="button"> <?= $block->escapeHtml(__('Flush Catalog Images Cache')); ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'setLocation(\'' . $block->escapeJs($block->getCleanImagesUrl()) . '\')', + 'button#flushCatalogImages' + ) ?> <span><?= $block->escapeHtml(__('Pregenerated product images files')); ?></span> </p> <?php endif; ?> - <?php if ($permissions->hasAccessToFlushJsCss()) : ?> + <?php if ($permissions->hasAccessToFlushJsCss()): ?> <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanMediaUrl()); ?>')" type="button"> + <button id="flushJsCss" type="button"> <?= $block->escapeHtml(__('Flush JavaScript/CSS Cache')); ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'setLocation(\'' . $block->escapeJs($block->getCleanMediaUrl()) . '\')', + 'button#flushJsCss' + ) ?> <span><?= $block->escapeHtml(__('Themes JavaScript and CSS files combined to one file')) ?></span> </p> <?php endif; ?> - <?php if (!$block->isInProductionMode() && $permissions->hasAccessToFlushStaticFiles()) : ?> + <?php if (!$block->isInProductionMode() && $permissions->hasAccessToFlushStaticFiles()): ?> <p> - <button onclick="setLocation('<?= $block->escapeJs($block->getCleanStaticFilesUrl()); ?>')" type="button"> + <button id="flushStaticFiles" type="button"> <?= $block->escapeHtml(__('Flush Static Files Cache')); ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'setLocation(\'' . $block->escapeJs($block->getCleanStaticFilesUrl()) . '\')', + 'button#flushStaticFiles' + ) ?> <span><?= $block->escapeHtml(__('Preprocessed view files and static files')); ?></span> </p> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml index d1c51f0755a72..753cc7ceee356 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/cache/edit.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /** @@ -14,16 +16,19 @@ */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"><?= $block->getSaveButtonHtml() ?></div> -<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> +<form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" + enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> - <script> + <?php $scriptString = <<<script window.setCacheAction = function(id, button) { document.getElementById(id).value = button.id; configForm.submit(); } - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <input type="hidden" id="catalog_action" name="catalog_action" value="" /> <input type="hidden" id="jscss_action" name="jscss_action" value="" /> @@ -36,7 +41,7 @@ <fieldset id="catalog"> <table class="form-list"> <tbody> - <?php foreach ($block->getCatalogData() as $_item) : ?> + <?php foreach ($block->getCatalogData() as $_item): ?> <?php /* disable reindex buttons. functionality moved to index management*/?> <?php if ($_item['buttons'][0]['name'] != 'clear_images_cache') { @@ -46,15 +51,31 @@ <tr> <td class="label"><label><?= $block->escapeHtml($_item['label']) ?></label></td> <td class="value"> - <?php foreach ($_item['buttons'] as $_button) : ?> + <?php foreach ($_item['buttons'] as $_button): ?> <?php $clickAction = "setCacheAction('catalog_action',this)"; ?> - <?php if (isset($_button['warning']) && $_button['warning']) : ?> + <?php if (isset($_button['warning']) && $_button['warning']): ?> <?php //phpcs:disable ?> - <?php $clickAction = "if (confirm('" . addslashes($_button['warning']) . "')) {{$clickAction}}"; ?> + <?php $clickAction = "if (confirm('" . addslashes($_button['warning']) . + "')) {{$clickAction}}"; ?> <?php //phpcs:enable ?> <?php endif; ?> - <button <?php if (!isset($_button['disabled']) || !$_button['disabled']) :?>onclick="<?= /* @noEscape */ $clickAction ?>"<?php endif; ?> id="<?= $block->escapeHtmlAttr($_button['name']) ?>" type="button" class="scalable <?php if (isset($_button['disabled']) && $_button['disabled']) :?>disabled<?php endif; ?>" style=""><span><span><span><?= $block->escapeHtml($_button['action']) ?></span></span></span></button> - <?php if (isset($_button['comment'])) : ?> <br /> <small><?= $block->escapeHtml($_button['comment']) ?></small> <?php endif; ?> + <button + id="<?= $block->escapeHtmlAttr($_button['name']) ?>" + type="button" + class="scalable + <?php if (isset($_button['disabled']) && $_button['disabled']):?>disabled<?php endif;?>" + ><span><span><span><?= $block->escapeHtml($_button['action']) ?></span></span></span> + </button> + <?php if (!isset($_button['disabled']) || !$_button['disabled']):?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $clickAction, + '#' . $block->escapeJs($_button['name']) + ) ?> + <?php endif; ?> + <?php if (isset($_button['comment'])): ?> <br /> + <small><?= $block->escapeHtml($_button['comment']) ?></small> + <?php endif; ?> <?php endforeach; ?> </td> <td><small> </small></td> @@ -76,7 +97,16 @@ <tr> <td class="label"><label><?= $block->escapeHtml(__('JavaScript/CSS Cache')) ?></label></td> <td class="value"> - <button onclick="setCacheAction('jscss_action', this)" id='jscss_action' type="button" class="scalable"><span><span><span><?= $block->escapeHtml(__('Clear')) ?></span></span></span></button> + <button id='jscss_action' + type="button" + class="scalable"> + <span><span><span><?= $block->escapeHtml(__('Clear')) ?></span></span></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "setCacheAction('jscss_action', this)", + '#jscss_action' + ) ?> </td> </tr> </tbody> @@ -84,8 +114,11 @@ </fieldset> </div> </form> -<script> +<?php $scriptString = <<<script require(["jquery","mage/mage"],function($){ $('#config-edit-form').mage('form').mage('validation'); }); -</script> +script; +?> + +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml index c9cd765de35be..2d68012b2c5dc 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/design/edit.phtml @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="design-edit-form"> <?= $block->getBlockHtml('formkey') ?> </form> -<script> + +<?php $scriptString = <<<script require([ "jquery", "mage/mage" @@ -16,4 +19,6 @@ require([ $('#design-edit-form').mage('form').mage('validation'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml b/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml index 2a43baa4e24c8..0846fd2020b36 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/system/shipping/applicable_country.phtml @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -115,4 +120,6 @@ CountryModel.prototype = { } countryApply = new CountryModel(); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml index fecf5365544e0..dfee490379dd1 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/accordion.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /** @@ -10,17 +12,20 @@ */ $items = $block->getItems(); ?> -<?php if (!empty($items)) : ?> +<?php if (!empty($items)): ?> <dl id="tab_content_<?= $block->getHtmlId() ?>" name="tab_content_<?= $block->getHtmlId() ?>" class="accordion"> - <?php foreach ($items as $_item) : ?> + <?php foreach ($items as $_item): ?> <?= $block->getChildHtml($_item->getId()) ?> <?php endforeach ?> </dl> - <script> + <?php $scriptString = <<<script require([ 'mage/adminhtml/accordion' ], function(){ - tab_content_<?= $block->getHtmlId() ?>AccordionJs = new varienAccordion('tab_content_<?= $block->getHtmlId() ?>', '<?= $block->escapeJs($block->getShowOnlyOne()) ?>'); + tab_content_{$block->getHtmlId()}AccordionJs = new varienAccordion('tab_content_{$block->getHtmlId()}', + '{$block->escapeJs($block->getShowOnlyOne())}'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml index 0123de098a9e0..6ebbd4118e7a0 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/button/split.phtml @@ -12,19 +12,19 @@ <button <?= $block->getButtonAttributesHtml() ?>> <span><?= $block->escapeHtml($block->getLabel()) ?></span> </button> - <?php if ($block->hasSplit()) : ?> + <?php if ($block->hasSplit()): ?> <button <?= $block->getToggleAttributesHtml() ?>> <span>Select</span> </button> - <?php if (!$block->getDisabled()) : ?> + <?php if (!$block->getDisabled()): ?> <ul class="dropdown-menu" <?= /* @noEscape */ $block->getUiId("dropdown-menu") ?>> - <?php foreach ($block->getOptions() as $key => $option) : ?> + <?php foreach ($block->getOptions() as $key => $option): ?> <li> <span <?= $block->getOptionAttributesHtml($key, $option) ?>> <?= $block->escapeHtml($option['label']) ?> </span> - <?php if (isset($option['hint'])) : ?> + <?php if (isset($option['hint'])): ?> <div class="tooltip" <?= /* @noEscape */ $block->getUiId('item', $key, 'tooltip') ?>> <a href="<?= $block->escapeHtml($option['hint']['href']) ?>" class="help"> <?= $block->escapeHtml($option['hint']['label']) ?> @@ -37,6 +37,7 @@ <?php endif; ?> <?php endif; ?> </div> +<?= /* @noEscape */$block->getAfterHtml() ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml index aa289dbf1eb0f..08ec331e37b7d 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/container.phtml @@ -5,12 +5,15 @@ */ /** @var $block \Magento\Backend\Block\Widget\Form\Container */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= /* @noEscape */ $block->getFormInitScripts() ?> -<?php if ($block->getButtonsHtml('header')) : ?> - <div class="page-form-actions" <?= /* @noEscape */ $block->getUiId('content-header') ?>><?= $block->getButtonsHtml('header') ?></div> +<?php if ($block->getButtonsHtml('header')): ?> + <div class="page-form-actions" <?= /* @noEscape */ $block->getUiId('content-header') ?>> + <?= $block->getButtonsHtml('header') ?> + </div> <?php endif; ?> -<?php if ($block->getButtonsHtml('toolbar')) : ?> +<?php if ($block->getButtonsHtml('toolbar')): ?> <div class="page-main-actions"> <div class="page-actions"> <div class="page-actions-buttons"> @@ -20,12 +23,13 @@ </div> <?php endif; ?> <?= $block->getFormHtml() ?> -<?php if ($block->hasFooterButtons()) : ?> +<?php if ($block->hasFooterButtons()): ?> <div class="content-footer"> <p class="form-buttons"><?= $block->getButtonsHtml('footer') ?></p> </div> <?php endif; ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'mage/backend/form', @@ -34,7 +38,7 @@ require([ $('#edit_form').form() .validation({ - validationUrl: '<?= $block->escapeJs($block->getValidationUrl()) ?>', + validationUrl: '{$block->escapeJs($block->getValidationUrl())}', highlight: function(element) { var detailsElement = $(element).closest('details'); if (detailsElement.length && detailsElement.is('.details')) { @@ -48,5 +52,8 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= /* @noEscape */ $block->getFormScripts() ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml index ec53f7e5c74ce..299f53dc9e3ef 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element.phtml @@ -4,47 +4,87 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $type = $element->getType(); +$htmlId = $element->getHtmlId(); ?> -<?php if ($type === 'fieldset') : ?> +<?php if ($type === 'fieldset'): ?> <fieldset> <legend><?= $block->escapeHtml($element->getLegend()) ?></legend><br /> - <?php foreach ($element->getElements() as $_element) : ?> + <?php foreach ($element->getElements() as $_element): ?> <?= /* @noEscape */ $formBlock->drawElement($_element) ?> <?php endforeach; ?> </fieldset> -<?php elseif ($type === 'hidden') : ?> - <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>"> - <?php elseif ($type === 'select') : ?> +<?php elseif ($type === 'hidden'): ?> + <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" + name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + value="<?= $block->escapeHtmlAttr($element->getValue()) ?>"> + <?php elseif ($type === 'select'): ?> <span class="form_row"> - <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> - <select name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" class="select<?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"> - <?php foreach ($element->getValues() as $_value) : ?> - <option <?= /* @noEscape */ $_value->serialize() ?><?php if ($_value->getValue() == $element->getValue()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_value->getLabel()) ?></option> + <?php if ($element->getLabel()): ?> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php endif; ?> + <select name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + class="select<?= $block->escapeHtmlAttr($element->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"> + <?php foreach ($element->getValues() as $_value): ?> + <option <?= /* @noEscape */ $_value->serialize() ?> + <?php if ($_value->getValue() == $element->getValue()): ?> selected="selected"<?php endif; ?>> + <?= $block->escapeHtml($_value->getLabel()) ?> + </option> <?php endforeach; ?> </select> </span> -<?php elseif ($type === 'text' || $type === 'button' || $type === 'password') : ?> +<?php elseif ($type === 'text' || $type === 'button' || $type === 'password'): ?> <span class="form_row"> - <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>" <?= /* @noEscape */ $block->getUiId('label') ?>><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> - <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= /* @noEscape */ $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" <?= /* @noEscape */ ($element->getOnClick() ? 'onClick="' . $element->getOnClick() . '"' : '') ?>/> + <?php if ($element->getLabel()): ?> + <label for="<?= /* @noEscape */ $htmlId ?>" <?= /* @noEscape */ $block->getUiId('label') ?>> + <?= $block->escapeHtml($element->getLabel()) ?>: + </label> + <?php endif; ?> + <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" + name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" + class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" /> + <?php if ($listener = $element->getOnclick()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag('onclick', $listener, "#{$htmlId}") ?> + <?php endif; ?> </span> -<?php elseif ($type === 'radio') : ?> +<?php elseif ($type === 'radio'): ?> <span class="form_row"> - <?php if ($element->getLabel()) : ?><label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label><?php endif; ?> - <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" name="<?= $block->escapeHtmlAttr($element->getName()) ?>" id="<?= $element->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"/> + <?php if ($element->getLabel()): ?> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php endif; ?> + <input type="<?= $block->escapeHtmlAttr($element->getType()) ?>" + name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + value="<?= $block->escapeHtmlAttr($element->getValue()) ?>" + class="input-text <?= $block->escapeHtmlAttr($element->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>"/> </span> -<?php elseif ($type === 'radios') : ?> +<?php elseif ($type === 'radios'): ?> <span class="form_row"> - <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> - <?php foreach ($element->getRadios() as $_radio) : ?> - <input type="radio" name="<?= $block->escapeHtmlAttr($_radio->getName()) ?>" id="<?= $_radio->getHtmlId() ?>" value="<?= $block->escapeHtmlAttr($_radio->getValue()) ?>" class="input-radio <?= $block->escapeHtmlAttr($_radio->getClass()) ?>" title="<?= $block->escapeHtmlAttr($_radio->getTitle()) ?>" <?= ($_radio->getValue() == $element->getChecked()) ? 'checked="true"' : '' ?> > <?= $block->escapeHtml($_radio->getLabel()) ?> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php foreach ($element->getRadios() as $_radio): ?> + <input type="radio" + name="<?= $block->escapeHtmlAttr($_radio->getName()) ?>" + id="<?= $_radio->getHtmlId() ?>" + value="<?= $block->escapeHtmlAttr($_radio->getValue()) ?>" + class="input-radio <?= $block->escapeHtmlAttr($_radio->getClass()) ?>" + title="<?= $block->escapeHtmlAttr($_radio->getTitle()) ?>" + <?= ($_radio->getValue() == $element->getChecked()) ? 'checked="true"' : '' ?> > + <?= $block->escapeHtml($_radio->getLabel()) ?> <?php endforeach; ?> </span> -<?php elseif ($type === 'wysiwyg') : ?> +<?php elseif ($type === 'wysiwyg'): ?> <span class="form_row"> - <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> - <script> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <?php $scriptString = <<<script require([ "wysiwygAdapter" ], function(tinyMCE){ @@ -52,35 +92,59 @@ $type = $element->getType(); tinyMCE.init({ mode : "exact", theme : "advanced", - elements : "<?= $block->escapeJs($element->getName()) ?>", - plugins : "inlinepopups,style,layer,table,save,advhr,advimage,advlink,emotions,iespell,insertdatetime,preview,zoom,media,searchreplace,print,contextmenu,paste,directionality,fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras", - theme_advanced_buttons1 : "newdocument,|,bold,italic,underline,strikethrough,|,justifyleft,justifycenter,justifyright,justifyfull,|,styleselect,formatselect,fontselect,fontsizeselect", - theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|,outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|,insertdate,inserttime,preview,|,forecolor,backcolor", - theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|,charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen", - theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,|,cite,abbr,acronym,del,ins,|,visualchars,nonbreaking", + elements : "{$block->escapeJs($element->getName())}", + plugins : "inlinepopups,style,layer,table,save,advhr,advimage,advlink,emotions,iespell," + + "insertdatetime,preview,zoom,media,searchreplace,print,contextmenu,paste,directionality," + + "fullscreen,noneditable,visualchars,nonbreaking,xhtmlxtras", + theme_advanced_buttons1 : "newdocument,|,bold,italic,underline,strikethrough,|" + + ",justifyleft,justifycenter,justifyright,justifyfull,|" + + ",styleselect,formatselect,fontselect,fontsizeselect", + theme_advanced_buttons2 : "cut,copy,paste,pastetext,pasteword,|,search,replace,|,bullist,numlist,|" + + ",outdent,indent,|,undo,redo,|,link,unlink,anchor,image,cleanup,help,code,|" + + ",insertdate,inserttime,preview,|,forecolor,backcolor", + theme_advanced_buttons3 : "tablecontrols,|,hr,removeformat,visualaid,|,sub,sup,|" + + ",charmap,emotions,iespell,media,advhr,|,print,|,ltr,rtl,|,fullscreen", + theme_advanced_buttons4 : "insertlayer,moveforward,movebackward,absolute,|,styleprops,|" + + ",cite,abbr,acronym,del,ins,|,visualchars,nonbreaking", theme_advanced_toolbar_location : "top", theme_advanced_toolbar_align : "left", theme_advanced_path_location : "bottom", - extended_valid_elements : "a[name|href|target|title|onclick],img[class|src|border=0|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name],hr[class|width|size|noshade],font[face|size|color|style],span[class|align|style]", + extended_valid_elements : "a[name|href|target|title|onclick],img[class|src|border=0|alt|title|hspace" + + "|vspace|width|height|align|onmouseover|onmouseout|name],hr[class|width|size" + + "|noshade],font[face|size|color|style],span[class|align|style]", theme_advanced_resize_horizontal : 'false', theme_advanced_resizing : 'true', apply_source_formatting : 'true', convert_urls : 'false', force_br_newlines : 'true', - doctype : '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' + doctype : '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"' + + ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">' }); }); -</script> - <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" cols="80" rows="20"><?= $block->escapeHtml($element->getValue()) ?></textarea> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" + cols="80" rows="20"> + <?= $block->escapeHtml($element->getValue()) ?> + </textarea> </span> -<?php elseif ($type === 'textarea') : ?> +<?php elseif ($type === 'textarea'): ?> <span class="form_row"> - <label for="<?= $element->getHtmlId() ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> - <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" id="<?= $element->getHtmlId() ?>" class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" cols="15" rows="2"><?= $block->escapeHtml($element->getValue()) ?></textarea> + <label for="<?= /* @noEscape */ $htmlId ?>"><?= $block->escapeHtml($element->getLabel()) ?>:</label> + <textarea name="<?= $block->escapeHtmlAttr($element->getName()) ?>" + title="<?= $block->escapeHtmlAttr($element->getTitle()) ?>" + id="<?= /* @noEscape */ $htmlId ?>" + class="textarea <?= $block->escapeHtmlAttr($element->getClass()) ?>" + cols="15" + rows="2"> + <?= $block->escapeHtml($element->getValue()) ?> + </textarea> </span> <?php endif; ?> -<?php if ($element->getScript()) : ?> -<script> - <?= /* @noEscape */ $element->getScript() ?> -</script> +<?php if ($element->getScript()): ?> + <?php /* @noEscape */ $secureRenderer->renderTag('script', [], $element->getScript(), false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml index 5c07b35e72a19..c2abd6069dd5d 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/form/element/gallery.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <tr> <td colspan="2"> @@ -25,35 +27,79 @@ <tbody class="gallery"> -<?php $i = 0; if ($block->getValues() !== null) : ?> - <?php foreach ($block->getValues() as $image) : $i++; ?> - <tr id="<?= $block->getElement()->getHtmlId() ?>_tr_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" class="gallery"> - <?php foreach ($block->getValues()->getAttributeBackend()->getImageTypes() as $type) : ?> - <td class="gallery" align="center" style="vertical-align:bottom;"> - <a href="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>" target="_blank" onclick="imagePreview('<?= $block->getElement()->getHtmlId() ?>_image_<?= $block->escapeHtmlAttr($block->escapeJs($type)) ?>_<?= $block->escapeHtmlAttr($block->escapeJs($image->getValueId())) ?>');return false;"> - <img id="<?= $block->getElement()->getHtmlId() ?>_image_<?= $block->escapeHtmlAttr($type) ?>_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" src="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>?<?= /* @noEscape */ time() ?>" alt="<?= $block->escapeHtmlAttr($image->getValue()) ?>" title="<?= $block->escapeHtmlAttr($image->getValue()) ?>" height="25" class="small-image-preview v-middle"/></a><br/> - <input type="file" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>_<?= $block->escapeHtmlAttr($type) ?>[<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" size="1"></td> +<?php $i = 0; if ($block->getValues() !== null): ?> + <?php foreach ($block->getValues() as $image): $i++; ?> + <?php $trId = $block->getElement()->getHtmlId() . '_tr_' . $block->escapeHtmlAttr($image->getValueId()); ?> + <tr id="<?= /* @noEscape */ $trId ?>" class="gallery"> + <?php foreach ($block->getValues()->getAttributeBackend()->getImageTypes() as $type): ?> + <?php $typeId = $block->getElement()->getHtmlId() . '_image_' . $block->escapeHtmlAttr($type); + $imgId = $typeId . '_' . $block->escapeHtmlAttr($image->getValueId()); ?> + <td class="gallery" align="center" id="<?= /* @noEscape */ $typeId ?>"> + <a href="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>" + id = <?= /* @noEscape */ 'a_' . $imgId ?> + target="_blank" + <img id="<?= /* @noEscape */ $imgId ?>" + src="<?= $block->escapeUrl($image->setType($type)->getSourceUrl()) ?>?<?= /* @noEscape */ time() ?>" + alt="<?= $block->escapeHtmlAttr($image->getValue()) ?>" + title="<?= $block->escapeHtmlAttr($image->getValue()) ?>" + height="25" + class="small-image-preview v-middle"/> + </a><br/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "imagePreview('<?= $imgId ?>');event.preventDefault()", + '#a_' . $imgId + ) ?> + <input type="file" + name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) + ?>_<?= $block->escapeHtmlAttr($type) ?>[<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" + size="1"> + </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('vertical-align:bottom;', 'td#' . $typeId) ?> <?php endforeach; ?> - <td class="gallery" align="center" style="vertical-align:bottom;"><input type="input" name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) ?>[position][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" value="<?= $block->escapeHtmlAttr($image->getPosition()) ?>" id="<?= $block->getElement()->getHtmlId() ?>_position_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" size="3"/></td> - <td class="gallery" align="center" style="vertical-align:bottom;"><?= $block->getDeleteButtonHtml($image->getValueId()) ?><input type="hidden" name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) ?>[delete][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" id="<?= $block->getElement()->getHtmlId() ?>_delete_<?= $block->escapeHtmlAttr($image->getValueId()) ?>"/></td> + <td class="gallery" align="center" id="<?= /* @noEscape */ $trId . '_td_input' ?>"> + <input type="input" + name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) + ?>[position][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" + value="<?= $block->escapeHtmlAttr($image->getPosition()) ?>" + id="<?= $block->getElement()->getHtmlId() + ?>_position_<?= $block->escapeHtmlAttr($image->getValueId()) ?>" + size="3"/> + </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('vertical-align:bottom;', $trId . '_td_input') ?> + <td class="gallery" align="center" id="<?= /* @noEscape */ $trId . '_td_delete' ?>"> + <?= $block->getDeleteButtonHtml($image->getValueId()) ?> + <input type="hidden" + name="<?= $block->escapeHtmlAttr($block->getElement()->getParentName()) + ?>[delete][<?= $block->escapeHtmlAttr($image->getValueId()) ?>]" + id="<?= $block->getElement()->getHtmlId() + ?>_delete_<?= $block->escapeHtmlAttr($image->getValueId()) ?>"/> + </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('vertical-align:bottom;', $trId . '_td_delete') ?> </tr> <?php endforeach; ?> <?php endif; ?> -<?php if ($i == 0) : ?> - <script> +<?php if ($i == 0): ?> + <?php $scriptString = <<<script document.getElementById("gallery_thead").style.visibility="hidden"; -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif; ?> </tbody></table> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); + +$scriptString = <<<script require([ 'prototype' ], function () { id = 0; -num_of_images = <?= /* @noEscape */ $i ?>; +num_of_images = {$i}; window.addNewImage = function() { @@ -62,19 +108,24 @@ window.addNewImage = function() id--; num_of_images++; - new_file_input = '<input type="file" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>_%j%[%id%]" size="1">'; +script; + + $elementName = $block->escapeHtmlAttr($block->getElement()->getName()); + $parentName = $block->escapeJs($block->getElement()->getParentName()); + $deleteButton = /* @noEscape */ $jsonHelper->jsonEncode($block->getDeleteButtonHtml("this")); + $elementNameDel = $block->escapeJs($block->getElement()->getName()); + $scriptString .= <<<script + new_file_input = '<input type="file" name="{$elementName}_%j%[%id%]" size="1">'; // Sort order input var new_row_input = document.createElement( 'input' ); new_row_input.type = 'text'; - new_row_input.name = '<?= $block->escapeJs($block->getElement()->getParentName()) ?>[position]['+id+']'; + new_row_input.name = '{$parentName}[position]['+id+']'; new_row_input.size = '3'; new_row_input.value = '0'; // Delete button - <?php //phpcs:disable ?> - new_row_button = <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getDeleteButtonHtml("this")) ?>; - <?php // phpcs:enable ?> + new_row_button = {$deleteButton}; table = document.getElementById( "gallery" ); @@ -113,13 +164,15 @@ window.deleteImage = function(image) document.getElementById("gallery_thead").style.visibility="hidden"; } if (image>0) { - document.getElementById('<?= $block->escapeJs($block->getElement()->getName()) ?>_delete_'+image).value=image; - document.getElementById('<?= $block->escapeJs($block->getElement()->getName()) ?>_tr_'+image).style.display='none'; + document.getElementById('{$elementNameDel}_delete_'+image).value=image; + document.getElementById('{$elementNameDel}_tr_'+image).style.display='none'; } else { image.parentNode.parentNode.parentNode.removeChild( image.parentNode.parentNode ); } } }); -</script> +script; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </td> </tr> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml index 7f6f2bbd13fa5..6f9344d7e1d77 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid.phtml @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + ?> <?php /** @@ -16,63 +17,73 @@ * */ /* @var $block \Magento\Backend\Block\Widget\Grid */ -$numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$numColumns = $block->getColumns() !== null ? count($block->getColumns()): 0; ?> -<?php if ($block->getCollection()) : ?> +<?php if ($block->getCollection()): ?> - <?php if ($block->canDisplayContainer()) : ?> + <?php if ($block->canDisplayContainer()): ?> <div id="<?= $block->escapeHtml($block->getId()) ?>" data-grid-id="<?= $block->escapeHtml($block->getId()) ?>"> - <?php else : ?> + <?php else: ?> <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> <?php endif; ?> <div class="admin__data-grid-header admin__data-grid-toolbar"> - <?php $massActionAvailable = $block->getChildBlock('grid.massaction') && $block->getChildBlock('grid.massaction')->isAvailable() ?> - <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getChildBlock('grid.columnSet')->getFilterVisibility() || $massActionAvailable) : ?> + <?php $massActionAvailable = $block->getChildBlock('grid.massaction') && + $block->getChildBlock('grid.massaction')->isAvailable() ?> + <?php if ($block->getPagerVisibility() || $block->getExportTypes() || + $block->getChildBlock('grid.columnSet')->getFilterVisibility() || $massActionAvailable): ?> <div class="admin__data-grid-header-row"> - <?php if ($massActionAvailable) : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php if ($massActionAvailable): ?> + <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . + $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> - <?php if ($block->getChildBlock('grid.export')) : ?> + <?php if ($block->getChildBlock('grid.export')): ?> <?= $block->getChildHtml('grid.export') ?> <?php endif; ?> </div> <?php endif; ?> <div class="<?php if ($massActionAvailable) { echo '_massaction ';} ?>admin__data-grid-header-row"> - <?php if ($massActionAvailable) : ?> + <?php if ($massActionAvailable): ?> <?= $block->getChildHtml('grid.massaction') ?> - <?php else : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php else: ?> + <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . + $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> <?php $countRecords = $block->getCollection()->getSize(); ?> <div class="admin__control-support-text"> - <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @noEscape */ $block->getUiId('total-count') ?>> + <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" + <?= /* @noEscape */ $block->getUiId('total-count') ?>> <?= /* @noEscape */ $countRecords ?> </span> <?= $block->escapeHtml(__('records found')) ?> <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>_massaction-count" - class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= $block->escapeHtml(__('selected')) ?></span></span> + class="mass-select-info _empty"><strong data-role="counter">0</strong> + <span><?= $block->escapeHtml(__('selected')) ?></span> + </span> </div> - <?php if ($block->getPagerVisibility()) : ?> + <?php if ($block->getPagerVisibility()): ?> <div class="admin__data-grid-pager-wrap"> <select name="<?= $block->escapeHtmlAttr($block->getVarNameLimit()) ?>" id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" - onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" <?= /* @noEscape */ $block->getUiId('per-page') ?> + onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" + <?= /* @noEscape */ $block->getUiId('per-page') ?> class="admin__control-select"> - <option value="20"<?php if ($block->getCollection()->getPageSize() == 20) : ?> + <option value="20"<?php if ($block->getCollection()->getPageSize() == 20): ?> selected="selected"<?php endif; ?>>20 </option> - <option value="30"<?php if ($block->getCollection()->getPageSize() == 30) : ?> + <option value="30"<?php if ($block->getCollection()->getPageSize() == 30): ?> selected="selected"<?php endif; ?>>30 </option> - <option value="50"<?php if ($block->getCollection()->getPageSize() == 50) : ?> + <option value="50"<?php if ($block->getCollection()->getPageSize() == 50): ?> selected="selected"<?php endif; ?>>50 </option> - <option value="100"<?php if ($block->getCollection()->getPageSize() == 100) : ?> + <option value="100"<?php if ($block->getCollection()->getPageSize() == 100): ?> selected="selected"<?php endif; ?>>100 </option> - <option value="200"<?php if ($block->getCollection()->getPageSize() == 200) : ?> + <option value="200"<?php if ($block->getCollection()->getPageSize() == 200): ?> selected="selected"<?php endif; ?>>200 </option> </select> @@ -82,14 +93,21 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; <?php $_curPage = $block->getCollection()->getCurPage() ?> <?php $_lastPage = $block->getCollection()->getLastPageNumber() ?> - <?php if ($_curPage > 1) : ?> - <button class="action-previous" - type="button" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage - 1) ?>');return false;"> - <span><?= $block->escapeHtml(__('Previous page')) ?></span> + <?php if ($_curPage > 1): ?> + <button class="action-previous" type="button"> + <span><?= $block->escapeHtml(__('Previous page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage - 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-previous:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-previous disabled"> + <span><?= $block->escapeHtml(__('Previous page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-previous disabled"><span><?= $block->escapeHtml(__('Previous page')) ?></span></button> <?php endif; ?> <input type="text" @@ -97,20 +115,36 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>" value="<?= $block->escapeHtmlAttr($_curPage) ?>" class="admin__control-text" - onkeypress="<?= /* @noEscape */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @noEscape */ $_lastPage ?>')" <?= /* @noEscape */ $block->getUiId('current-page') ?> /> + <?= /* @noEscape */ $block->getUiId('current-page') ?> /> + + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onkeypress', + /* @noEscape */ $block->getJsObjectName() . '.inputPage(event, \'' . + /* @noEscape */ $_lastPage . '\')', + '#' . $block->escapeHtml($block->getHtmlId()) . '_page-current' + ) ?> <label class="admin__control-support-text" for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> - <?= /* @noEscape */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> + <?= /* @noEscape */ __('of %1', '<span>' . + $block->getCollection()->getLastPageNumber() . '</span>') ?> </label> - <?php if ($_curPage < $_lastPage) : ?> + <?php if ($_curPage < $_lastPage): ?> <button type="button" title="<?= $block->escapeHtmlAttr(__('Next page')) ?>" - class="action-next" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage + 1) ?>');return false;"> + class="action-next"> + <span><?= $block->escapeHtml(__('Next page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage + 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-next:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-next disabled"> <span><?= $block->escapeHtml(__('Next page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-next disabled"><span><?= $block->escapeHtml(__('Next page')) ?></span></button> <?php endif; ?> </div> </div> @@ -118,79 +152,104 @@ $numColumns = $block->getColumns() !== null ? count($block->getColumns()) : 0; </div> </div> <div class="admin__data-grid-wrap admin__data-grid-wrap-static"> - <?php if ($block->getGridCssClass()) : ?> - <table class="<?= $block->escapeHtmlAttr($block->getGridCssClass()) ?> data-grid" id="<?= $block->escapeHtml($block->getId()) ?>_table"> + <?php if ($block->getGridCssClass()): ?> + <table class="<?= $block->escapeHtmlAttr($block->getGridCssClass()) ?> data-grid" + id="<?= $block->escapeHtml($block->getId()) ?>_table"> <!-- Rendering column set --> <?= $block->getChildHtml('grid.columnSet') ?> </table> - <?php else : ?> + <?php else: ?> <table class="data-grid" id="<?= $block->escapeHtml($block->getId()) ?>_table"> <!-- Rendering column set --> <?= $block->getChildHtml('grid.columnSet') ?> </table> - <?php if ($block->getChildBlock('grid.bottom.links')) : ?> + <?php if ($block->getChildBlock('grid.bottom.links')): ?> <?= $block->getChildHtml('grid.bottom.links') ?> <?php endif; ?> <?php endif ?> </div> - <?php if ($block->canDisplayContainer()) : ?> + <?php if ($block->canDisplayContainer()): ?> </div> -<script> - var deps = []; - - <?php if ($block->getDependencyJsObject()) : ?> - deps.push('uiRegistry'); - <?php endif; ?> - - <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> - deps.push('Magento_Sales/order/create/form'); - deps.push('jquery'); - <?php endif; ?> - - deps.push('mage/adminhtml/grid'); - - require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> - - <?php if ($block->getDependencyJsObject()) : ?> - registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { - <?php endif; ?> - - <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid('<?= $block->escapeHtml($block->getId()) ?>', '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); - <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = <?= /* @noEscape */ $block->getUseAjax() ? 'true' : 'false' ?>; - <?php if ($block->getRowClickCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; - <?php endif; ?> - <?php if ($block->getCheckboxCheckCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; - <?php endif; ?> - <?php if ($block->getSortableUpdateCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.sortableUpdateCallback = <?= /* @noEscape */ $block->getSortableUpdateCallback() ?>; - <?php endif; ?> - <?php if ($block->getFilterKeyPressCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; - <?php endif; ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.bindSortable(); - <?php if ($block->getRowInitCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; - <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); - <?php endif; ?> - <?php if ($block->getChildBlock('grid.massaction') && $block->getChildBlock('grid.massaction')->isAvailable()) : ?> - <?= /* @noEscape */ $block->getChildBlock('grid.massaction')->getJavaScript() ?> - <?php endif ?> - <?= /* @noEscape */ $block->getAdditionalJavaScript() ?> + <?php + $scriptString = 'var deps = [];' . PHP_EOL; + + if ($block->getDependencyJsObject()) { + $scriptString .= 'deps.push(\'uiRegistry\');' . PHP_EOL; + } + + if (strpos($block->getRowClickCallback(), 'order.') !== false) { + $scriptString .= 'deps.push(\'Magento_Sales/order/create/form\');' . PHP_EOL; + $scriptString .= 'deps.push(\'jquery\');' . PHP_EOL; + } + + $scriptString .= 'deps.push(\'mage/adminhtml/grid\');' . PHP_EOL; + + $scriptString .= ' +require(deps, function('. ($block->getDependencyJsObject() ? 'registry' : '') .'){' . PHP_EOL; + //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed + if ($block->getDependencyJsObject()) { + $scriptString .= 'registry.get(\'' . $block->escapeJs($block->getDependencyJsObject()) . + '\', function ('. $block->escapeJs($block->getDependencyJsObject()) . ') {' . PHP_EOL; + } + + $scriptString .= $block->escapeJs($block->getJsObjectName()) . ' = new varienGrid(\'' . + $block->escapeJs($block->getId()) . '\', \'' . $block->escapeJs($block->getGridUrl()) . '\', \'' . + $block->escapeJs($block->getVarNamePage()) .'\', \'' . + $block->escapeJs($block->getVarNameSort()) . '\', \'' . + $block->escapeJs($block->getVarNameDir()) . '\', \'' . $block->escapeJs($block->getVarNameFilter()) .'\'); +' . PHP_EOL; + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.useAjax = ' . + (/* @noEscape */ $block->escapeJs($block->getUseAjax()) ? 'true' : 'false') . ';' . PHP_EOL; + if ($block->getRowClickCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.rowClickCallback = ' . + /* @noEscape */ $block->getRowClickCallback() . ';' . PHP_EOL; + } + + if ($block->getCheckboxCheckCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.checkboxCheckCallback = ' . + /* @noEscape */ $block->getCheckboxCheckCallback() . ';' . PHP_EOL; + } + + if ($block->getSortableUpdateCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.sortableUpdateCallback = ' . + /* @noEscape */ $block->getSortableUpdateCallback() . ';' . PHP_EOL; + } + + if ($block->getFilterKeyPressCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.filterKeyPressCallback = ' . + /* @noEscape */ $block->getFilterKeyPressCallback() . ';' . PHP_EOL; + } + + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.bindSortable();' . PHP_EOL; + + if ($block->getRowInitCallback()) { + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '.initRowCallback = ' . + /* @noEscape */ $block->getRowInitCallback() . ';' . PHP_EOL; + $scriptString .= $block->escapeJs($block->getJsObjectName()) . '..initGridRows();' . PHP_EOL; + } + + if ($block->getChildBlock('grid.massaction') && + $block->getChildBlock('grid.massaction')->isAvailable()) { + $scriptString .= /* @noEscape */ $block->getChildBlock('grid.massaction')->getJavaScript() . PHP_EOL; + } + + $scriptString .= /* @noEscape */ $block->getAdditionalJavaScript() . PHP_EOL; + + if ($block->getDependencyJsObject()) { + $scriptString .= '});' . PHP_EOL; + } + + $scriptString .= '});' . PHP_EOL; + + echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); + ?> - <?php if ($block->getDependencyJsObject()) : ?> - }); - <?php endif; ?> - }); -</script> <?php endif; ?> - <?php if ($block->getChildBlock('grid.js')) : ?> + <?php if ($block->getChildBlock('grid.js')): ?> <?= $block->getChildHtml('grid.js') ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml index 527ddc436207f..d4aa14250837f 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/extended.phtml @@ -18,31 +18,39 @@ $numColumns = count($block->getColumns()); /** * @var \Magento\Backend\Block\Widget\Grid\Extended $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getCollection()) : ?> - <?php if ($block->canDisplayContainer()) : ?> +<?php if ($block->getCollection()): ?> + <?php if ($block->canDisplayContainer()): ?> <div id="<?= $block->escapeHtml($block->getId()) ?>" data-grid-id="<?= $block->escapeHtml($block->getId()) ?>"> - <?php else : ?> + <?php else: ?> <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> <?php endif; ?> <?php $massActionAvailable = $block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable() ?> - <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getFilterVisibility() || $massActionAvailable) : ?> + <?php if ($block->getPagerVisibility() || $block->getExportTypes() || $block->getFilterVisibility() || + $massActionAvailable): ?> <div class="admin__data-grid-header admin__data-grid-toolbar"> <div class="admin__data-grid-header-row"> - <?php if ($massActionAvailable) : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php if ($massActionAvailable): ?> + <?= $block->getMainButtonsHtml() ? + '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> - <?php if ($block->getExportTypes()) : ?> + <?php if ($block->getExportTypes()): ?> <div class="admin__data-grid-export"> <label class="admin__control-support-text" - for="<?= $block->escapeHtml($block->getId()) ?>_export"><?= $block->escapeHtml(__('Export to:')) ?></label> - <select name="<?= $block->escapeHtml($block->getId()) ?>_export" id="<?= $block->escapeHtml($block->getId()) ?>_export" + for="<?= $block->escapeHtml($block->getId()) ?>_export"> + <?= $block->escapeHtml(__('Export to:')) ?> + </label> + <select name="<?= $block->escapeHtml($block->getId()) ?>_export" + id="<?= $block->escapeHtml($block->getId()) ?>_export" class="admin__control-select"> - <?php foreach ($block->getExportTypes() as $_type) : ?> - <option value="<?= $block->escapeHtmlAttr($_type->getUrl()) ?>"><?= $block->escapeHtml($_type->getLabel()) ?></option> + <?php foreach ($block->getExportTypes() as $_type): ?> + <option value="<?= $block->escapeHtmlAttr($_type->getUrl()) ?>"> + <?= $block->escapeHtml($_type->getLabel()) ?> + </option> <?php endforeach; ?> </select> <?= $block->getExportButtonHtml() ?> @@ -51,76 +59,105 @@ $numColumns = count($block->getColumns()); </div> <div class="admin__data-grid-header-row <?= $massActionAvailable ? '_massaction' : '' ?>"> - <?php if ($massActionAvailable) : ?> + <?php if ($massActionAvailable): ?> <?= $block->getMassactionBlockHtml() ?> - <?php else : ?> - <?= $block->getMainButtonsHtml() ? '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> + <?php else: ?> + <?= $block->getMainButtonsHtml() ? + '<div class="admin__filter-actions">' . $block->getMainButtonsHtml() . '</div>' : '' ?> <?php endif; ?> <?php $countRecords = $block->getCollection()->getSize(); ?> <div class="admin__control-support-text"> - <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" <?= /* @noEscape */ $block->getUiId('total-count') ?>> + <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>-total-count" + <?= /* @noEscape */ $block->getUiId('total-count') ?>> <?= /* @noEscape */ $countRecords ?> </span> <?= $block->escapeHtml(__('records found')) ?> <span id="<?= $block->escapeHtml($block->getHtmlId()) ?>_massaction-count" - class="mass-select-info _empty"><strong data-role="counter">0</strong> <span><?= $block->escapeHtml(__('selected')) ?></span></span> + class="mass-select-info _empty"> + <strong data-role="counter">0</strong> + <span><?= $block->escapeHtml(__('selected')) ?></span> + </span> </div> - <?php if ($block->getPagerVisibility()) : ?> + <?php if ($block->getPagerVisibility()): ?> <div class="admin__data-grid-pager-wrap"> <select name="<?= $block->escapeHtmlAttr($block->getVarNameLimit()) ?>" id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" onchange="<?= /* @noEscape */ $block->getJsObjectName() ?>.loadByElement(this)" class="admin__control-select"> - <option value="20"<?php if ($block->getCollection()->getPageSize() == 20) : ?> + <option value="20"<?php if ($block->getCollection()->getPageSize() == 20): ?> selected="selected"<?php endif; ?>>20 </option> - <option value="30"<?php if ($block->getCollection()->getPageSize() == 30) : ?> + <option value="30"<?php if ($block->getCollection()->getPageSize() == 30): ?> selected="selected"<?php endif; ?>>30 </option> - <option value="50"<?php if ($block->getCollection()->getPageSize() == 50) : ?> + <option value="50"<?php if ($block->getCollection()->getPageSize() == 50): ?> selected="selected"<?php endif; ?>>50 </option> - <option value="100"<?php if ($block->getCollection()->getPageSize() == 100) : ?> + <option value="100"<?php if ($block->getCollection()->getPageSize() == 100): ?> selected="selected"<?php endif; ?>>100 </option> - <option value="200"<?php if ($block->getCollection()->getPageSize() == 200) : ?> + <option value="200"<?php if ($block->getCollection()->getPageSize() == 200): ?> selected="selected"<?php endif; ?>>200 </option> </select> - <label for="<?= $block->escapeHtml($block->getHtmlId()) ?><?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" + <label for="<?= $block->escapeHtml($block->getHtmlId()) + ?><?= $block->escapeHtml($block->getHtmlId()) ?>_page-limit" class="admin__control-support-text"><?= $block->escapeHtml(__('per page')) ?></label> <div class="admin__data-grid-pager"> <?php $_curPage = $block->getCollection()->getCurPage() ?> <?php $_lastPage = $block->getCollection()->getLastPageNumber() ?> - <?php if ($_curPage > 1) : ?> - <button class="action-previous" - type="button" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage - 1) ?>');return false;"> + <?php if ($_curPage > 1): ?> + <button class="action-previous" type="button"> + <span><?= $block->escapeHtml(__('Previous page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage - 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-previous:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-previous disabled"> <span><?= $block->escapeHtml(__('Previous page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-previous disabled"><span><?= $block->escapeHtml(__('Previous page')) ?></span></button> <?php endif; ?> <input type="text" id="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current" name="<?= $block->escapeHtmlAttr($block->getVarNamePage()) ?>" value="<?= $block->escapeHtmlAttr($_curPage) ?>" class="admin__control-text" - onkeypress="<?= /* @noEscape */ $block->getJsObjectName() ?>.inputPage(event, '<?= /* @noEscape */ $_lastPage ?>')" <?= /* @noEscape */ $block->getUiId('current-page') ?> /> - <label class="admin__control-support-text" for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> - <?= /* @noEscape */ __('of %1', '<span>' . $block->getCollection()->getLastPageNumber() . '</span>') ?> + <?= /* @noEscape */ $block->getUiId('current-page') ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onkeypress', + /* @noEscape */ $block->getJsObjectName() . '.inputPage(event, \'' . + /* @noEscape */ $_lastPage . '\')', + '#' . $block->escapeHtml($block->getHtmlId()) . '_page-current' + ) ?> + <label class="admin__control-support-text" + for="<?= $block->escapeHtml($block->getHtmlId()) ?>_page-current"> + <?= /* @noEscape */ __('of %1', '<span>' . + $block->getCollection()->getLastPageNumber() . '</span>') ?> </label> - <?php if ($_curPage < $_lastPage) : ?> + <?php if ($_curPage < $_lastPage): ?> <button type="button" title="<?= $block->escapeHtmlAttr(__('Next page')) ?>" - class="action-next" - onclick="<?= /* @noEscape */ $block->getJsObjectName() ?>.setPage('<?= /* @noEscape */ ($_curPage + 1) ?>');return false;"> + class="action-next"> + <span><?= $block->escapeHtml(__('Next page')) ?></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + /* @noEscape */ $block->getJsObjectName() . '.setPage(\'' . + /* @noEscape */ ($_curPage + 1) . '\');event.preventDefault();', + 'div#' . $block->escapeJs($block->getId()) . + ' .admin__data-grid-pager button.action-next:not(.disabled)' + ) ?> + <?php else: ?> + <button type="button" class="action-next disabled"> <span><?= $block->escapeHtml(__('Next page')) ?></span> </button> - <?php else : ?> - <button type="button" class="action-next disabled"><span><?= $block->escapeHtml(__('Next page')) ?></span></button> <?php endif; ?> </div> </div> @@ -137,25 +174,26 @@ $numColumns = count($block->getColumns()); <col <?= $_column->getHtmlProperty() ?> /> <?php endforeach; */ ?> - <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()) : ?> + <?php if ($block->getHeadersVisibility() || $block->getFilterVisibility()): ?> <thead> - <?php if ($block->getHeadersVisibility()) : ?> + <?php if ($block->getHeadersVisibility()): ?> <tr> - <?php foreach ($block->getColumns() as $_column) : ?> - <?php if ($_column->getHeaderHtml() == ' ') : ?> + <?php foreach ($block->getColumns() as $_column): ?> + <?php if ($_column->getHeaderHtml() == ' '): ?> <th class="data-grid-th" data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" <?= $_column->getHeaderHtmlProperty() ?>> </th> - <?php else : ?> + <?php else: ?> <?= $_column->getHeaderHtml() ?> <?php endif; ?> <?php endforeach; ?> </tr> <?php endif; ?> - <?php if ($block->getFilterVisibility()) : ?> + <?php if ($block->getFilterVisibility()): ?> <tr class="data-grid-filters" data-role="filter-form"> <?php $i = 0; - foreach ($block->getColumns() as $_column) : ?> - <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" <?= $_column->getHeaderHtmlProperty() ?>> + foreach ($block->getColumns() as $_column): ?> + <td data-column="<?= $block->escapeHtmlAttr($_column->getId()) ?>" + <?= $_column->getHeaderHtmlProperty() ?>> <?= $_column->getFilterHtml() ?> </td> <?php endforeach; ?> @@ -163,12 +201,14 @@ $numColumns = count($block->getColumns()); <?php endif ?> </thead> <?php endif; ?> - <?php if ($block->getCountTotals()) : ?> + <?php if ($block->getCountTotals()): ?> <tfoot> <tr class="totals"> - <?php foreach ($block->getColumns() as $_column) : ?> + <?php foreach ($block->getColumns() as $_column): ?> <th class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?>"> - <?= /* @noEscape */ ($_column->hasTotalsLabel()) ? $block->escapeHtml($_column->getTotalsLabel()) : $_column->getRowField($_column->getGrid()->getTotals()) ?> + <?= /* @noEscape */ ($_column->hasTotalsLabel()) ? + $block->escapeHtml($_column->getTotalsLabel()) : + $_column->getRowField($_column->getGrid()->getTotals()) ?> </th> <?php endforeach; ?> </tr> @@ -176,21 +216,26 @@ $numColumns = count($block->getColumns()); <?php endif; ?> <tbody> - <?php if (($block->getCollection()->getSize() > 0) && (!$block->getIsCollapsed())) : ?> - <?php foreach ($block->getCollection() as $_index => $_item) : ?> - <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>"<?php if ($_class = $block->getRowClass($_item)) : ?> - class="<?= $block->escapeHtmlAttr($_class) ?>"<?php endif; ?> ><?php + <?php if (($block->getCollection()->getSize() > 0) && (!$block->getIsCollapsed())): ?> + <?php foreach ($block->getCollection() as $_index => $_item): ?> + <tr title="<?= $block->escapeHtmlAttr($block->getRowUrl($_item)) ?>" + <?php if ($_class = $block->getRowClass($_item)): ?> + class="<?= $block->escapeHtmlAttr($_class) ?>" + <?php endif; ?>> + <?php $i = 0; - foreach ($block->getColumns() as $_column) : - if ($block->shouldRenderCell($_item, $_column)) : + foreach ($block->getColumns() as $_column): + if ($block->shouldRenderCell($_item, $_column)): $_rowspan = $block->getRowspan($_item, $_column); ?> <td <?= /* @noEscape */ ($_rowspan ? 'rowspan="' . $_rowspan . '" ' : '') ?> class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> - <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> - <?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? $_html : ' ') ?> + <?= /* @noEscape */ $_column->getId() == 'massaction' ? + 'data-grid-checkbox-cell': '' ?>"> + <?= /* @noEscape */ (($_html = $_column->getRowField($_item)) != '' ? + $_html : ' ') ?> </td><?php - if ($block->shouldRenderEmptyCell($_item, $_column)) : + if ($block->shouldRenderEmptyCell($_item, $_column)): ?> <td colspan="<?= $block->escapeHtmlAttr($block->getEmptyCellColspan($_item)) ?>" class="last"><?= $block->escapeHtml($block->getEmptyCellLabel()) ?></td><?php @@ -198,98 +243,164 @@ $numColumns = count($block->getColumns()); endif; endforeach; ?> </tr> - <?php if ($_multipleRows = $block->getMultipleRows($_item)) : ?> - <?php foreach ($_multipleRows as $_i) : ?> + <?php if ($_multipleRows = $block->getMultipleRows($_item)): ?> + <?php foreach ($_multipleRows as $_i): ?> <tr> <?php $i = 0; - foreach ($block->getMultipleRowColumns($_i) as $_column) : ?> + foreach ($block->getMultipleRowColumns($_i) as $_column): ?> <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> - <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> - <?= /* @noEscape */ (($_html = $_column->getRowField($_i)) != '' ? $_html : ' ') ?> + <?= /* @noEscape */ $_column->getId() == 'massaction' ? + 'data-grid-checkbox-cell': '' ?>"> + <?= /* @noEscape */ (($_html = $_column->getRowField($_i)) != '' ? + $_html : ' ') ?> </td> <?php endforeach; ?> </tr> <?php endforeach; ?> <?php endif; ?> - <?php if ($block->shouldRenderSubTotal($_item)) : ?> + <?php if ($block->shouldRenderSubTotal($_item)): ?> <tr class="subtotals"> <?php $i = 0; - foreach ($block->getSubTotalColumns() as $_column) : ?> + foreach ($block->getSubTotalColumns() as $_column): ?> <td class="<?= $block->escapeHtmlAttr($_column->getCssProperty()) ?> - <?= /* @noEscape */ $_column->getId() == 'massaction' ? 'data-grid-checkbox-cell': '' ?>"> - <?= /* @noEscape */ $_column->hasSubtotalsLabel() ? $block->escapeHtml($_column->getSubtotalsLabel()) : $_column->getRowField($block->getSubTotalItem($_item)) ?> + <?= /* @noEscape */ $_column->getId() == 'massaction' ? + 'data-grid-checkbox-cell': '' ?>"> + <?= /* @noEscape */ $_column->hasSubtotalsLabel() ? + $block->escapeHtml($_column->getSubtotalsLabel()) : + $_column->getRowField($block->getSubTotalItem($_item)) ?> </td> <?php endforeach; ?> </tr> <?php endif; ?> <?php endforeach; ?> - <?php elseif ($block->getEmptyText()) : ?> + <?php elseif ($block->getEmptyText()): ?> <tr class="data-grid-tr-no-data"> <td class="<?= $block->escapeHtmlAttr($block->getEmptyTextClass()) ?>" - colspan="<?= $block->escapeHtmlAttr($numColumns) ?>"><?= $block->escapeHtml($block->getEmptyText()) ?></td> + colspan="<?= $block->escapeHtmlAttr($numColumns) ?>"><?= + $block->escapeHtml($block->getEmptyText()) ?></td> </tr> <?php endif; ?> </tbody> </table> </div> - <?php if ($block->canDisplayContainer()) : ?> </div> -<script> + <?php + /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ + $jsonHelper = $block->getData('jsonHelper'); + if ($block->canDisplayContainer()): + $scriptString = <<<script + var deps = []; +script; + if ($block->getDependencyJsObject()): + $scriptString .= <<<script - <?php if ($block->getDependencyJsObject()) : ?> deps.push('uiRegistry'); - <?php endif; ?> +script; + endif; + + if (strpos($block->getRowClickCallback(), 'order.') !== false): + $scriptString .= <<<script - <?php if (strpos($block->getRowClickCallback(), 'order.') !== false) : ?> deps.push('Magento_Sales/order/create/form') - <?php endif; ?> +script; + endif; + $scriptString .= <<<script deps.push('mage/adminhtml/grid'); +script; + if (is_array($block->getRequireJsDependencies())): + foreach ($block->getRequireJsDependencies() as $dependency): + $scriptString .= <<<script - <?php if (is_array($block->getRequireJsDependencies())) : ?> - <?php foreach ($block->getRequireJsDependencies() as $dependency) : ?> - deps.push('<?= $block->escapeJs($dependency) ?>'); - <?php endforeach; ?> - <?php endif; ?> + deps.push('{$block->escapeJs($dependency)}'); +script; + endforeach; + endif; + $dependencyJsObject = ($block->getDependencyJsObject() ? 'registry' : ''); + $scriptString .= <<<script - require(deps, function(<?= ($block->getDependencyJsObject() ? 'registry' : '') ?>){ - <?php //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed ?> + require(deps, function({$dependencyJsObject}){ + +script; + //TODO: getJsObjectName and getRowClickCallback has unexpected behavior. Should be removed + $scriptString .= <<<script //<![CDATA[ - <?php if ($block->getDependencyJsObject()) : ?> - registry.get('<?= $block->escapeJs($block->getDependencyJsObject()) ?>', function (<?= $block->escapeJs($block->getDependencyJsObject()) ?>) { - <?php endif; ?> - <?php // phpcs:disable ?> - <?= $block->escapeJs($block->getJsObjectName()) ?> = new varienGrid(<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getId()) ?>, '<?= $block->escapeJs($block->getGridUrl()) ?>', '<?= $block->escapeJs($block->getVarNamePage()) ?>', '<?= $block->escapeJs($block->getVarNameSort()) ?>', '<?= $block->escapeJs($block->getVarNameDir()) ?>', '<?= $block->escapeJs($block->getVarNameFilter()) ?>'); - <?php //phpcs:enable ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.useAjax = '<?= $block->escapeJs($block->getUseAjax()) ?>'; - <?php if ($block->getRowClickCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.rowClickCallback = <?= /* @noEscape */ $block->getRowClickCallback() ?>; - <?php endif; ?> - <?php if ($block->getCheckboxCheckCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.checkboxCheckCallback = <?= /* @noEscape */ $block->getCheckboxCheckCallback() ?>; - <?php endif; ?> - <?php if ($block->getFilterKeyPressCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.filterKeyPressCallback = <?= /* @noEscape */ $block->getFilterKeyPressCallback() ?>; - <?php endif; ?> - <?php if ($block->getRowInitCallback()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.initRowCallback = <?= /* @noEscape */ $block->getRowInitCallback() ?>; - <?= $block->escapeJs($block->getJsObjectName()) ?>.initGridRows(); - <?php endif; ?> - <?php if ($block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable()) : ?> - <?= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() ?> - <?php endif ?> - <?= /* @noEscape */ $block->getAdditionalJavaScript() ?> - - <?php if ($block->getDependencyJsObject()) : ?> + +script; + if ($block->getDependencyJsObject()): + $scriptString .= <<<script + + registry.get('{$block->escapeJs($block->getDependencyJsObject())}', + function ({$block->escapeJs($block->getDependencyJsObject())}) { +script; + endif; + $encodedId = /* @noEscape */ $jsonHelper->jsonEncode($block->getId()); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())} = new varienGrid({$encodedId}, + '{$block->escapeJs($block->getGridUrl())}', + '{$block->escapeJs($block->getVarNamePage())}', + '{$block->escapeJs($block->getVarNameSort())}', + '{$block->escapeJs($block->getVarNameDir())}', + '{$block->escapeJs($block->getVarNameFilter())}' + ); + + {$block->escapeJs($block->getJsObjectName())}.useAjax = '{$block->escapeJs($block->getUseAjax())}'; + +script; + if ($block->getRowClickCallback()): + $rowClickCallback = /* @noEscape */ $block->getRowClickCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.rowClickCallback = {$rowClickCallback}; +script; + endif; + if ($block->getCheckboxCheckCallback()): + $checkboxCheckCallback = /* @noEscape */ $block->getCheckboxCheckCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.checkboxCheckCallback = {$checkboxCheckCallback}; +script; + endif; + if ($block->getFilterKeyPressCallback()): + $filterKeyPressCallback = /* @noEscape */ $block->getFilterKeyPressCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.filterKeyPressCallback = {$filterKeyPressCallback}; +script; + endif; + if ($block->getRowInitCallback()): + $rowInitCallback = /* @noEscape */ $block->getRowInitCallback(); + $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.initRowCallback = {$rowInitCallback}; + {$block->escapeJs($block->getJsObjectName())}.initGridRows(); + +script; + endif; + if ($block->getMassactionBlock() && $block->getMassactionBlock()->isAvailable()): + $scriptString .= /* @noEscape */ $block->getMassactionBlock()->getJavaScript() . PHP_EOL; + endif; + $scriptString .= /* @noEscape */ $block->getAdditionalJavaScript() . PHP_EOL; + + if ($block->getDependencyJsObject()): + $scriptString .= <<<script + }); - <?php endif; ?> + +script; + endif; + $scriptString .= <<<script + //]]> }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml index 9a21cd4ef71a1..179557c2984e5 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction.phtml @@ -3,11 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= /* @noEscape */ $block->getSomething() ?> <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - <?php if ($block->getHideFormElement() !== true) : ?> + <?php if ($block->getHideFormElement() !== true): ?> <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> <?php endif ?> <div class="admin__grid-massaction-form"> @@ -16,22 +18,26 @@ id="<?= $block->getHtmlId() ?>-select" class="required-entry local-validation admin__control-select" <?= /* @noEscape */ $block->getUiId('select') ?>> - <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> - <?php foreach ($block->getItems() as $_item) : ?> - <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> + <option class="admin__control-select-placeholder" value="" selected> + <?= $block->escapeHtml(__('Actions')) ?></option> + <?php foreach ($block->getItems() as $_item): ?> + <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" + <?= ($_item->getSelected() ? ' selected="selected"' : '') ?>> + <?= $block->escapeHtml($_item->getLabel()) ?> + </option> <?php endforeach; ?> </select> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> <?= $block->getApplyButtonHtml() ?> </div> - <?php if ($block->getHideFormElement() !== true) :?> + <?php if ($block->getHideFormElement() !== true):?> </form> <?php endif ?> <div class="no-display"> - <?php foreach ($block->getItems() as $_item) : ?> + <?php foreach ($block->getItems() as $_item): ?> <div id="<?= $block->getHtmlId() ?>-item-<?= /* @noEscape */ $_item->getId() ?>-block"> - <?php if ('' != $_item->getBlockName()) :?> + <?php if ('' != $_item->getBlockName()):?> <?= $block->getChildHtml($_item->getBlockName()) ?> <?php endif;?> </div> @@ -46,7 +52,7 @@ data-menu="grid-mass-select"> <optgroup label="<?= $block->escapeHtmlAttr(__('Mass Actions')) ?>"> <option disabled selected></option> - <?php if ($block->getUseSelectAll()) :?> + <?php if ($block->getUseSelectAll()):?> <option value="selectAll"> <?= $block->escapeHtml(__('Select All')) ?> </option> @@ -65,35 +71,43 @@ <label for="<?= $block->getHtmlId() ?>-mass-select"></label> </div> -<script> +<?php $scriptString = <<<script require(['jquery', 'domReady!'], function($){ 'use strict'; - $('#<?= $block->getHtmlId() ?>-mass-select') +script; +$scriptString .= '$(\'#' . $block->getHtmlId() . '-mass-select\')'; +$scriptString .= <<<script .removeClass('_disabled') .prop('disabled', false) .change(function () { var massAction = $('option:selected', this).val(); this.blur(); switch (massAction) { - <?php if ($block->getUseSelectAll()) : ?> - case 'selectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); - break; - case 'unselectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); +script; +if ($block->getUseSelectAll()): + $scriptString .= ' + case \'selectAll\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.selectAll(); break; - <?php endif; ?> - case 'selectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); + case \'unselectAll\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.unselectAll(); + break;'; +endif; + $scriptString .= ' + case \'selectVisible\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.selectVisible(); break; - case 'unselectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); + case \'unselectVisible\': + return ' . $block->escapeJs($block->getJsObjectName()) . '.unselectVisible(); break; } }); - }); - <?php if (!$block->getParentBlock()->canDisplayContainer()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= $block->escapeJs($block->getGridIdsJson()) ?>'); - <?php endif; ?> -</script> + });'; + +if (!$block->getParentBlock()->canDisplayContainer()): + $scriptString .= $block->escapeJs($block->getJsObjectName()) . + '.setGridIds(\'' . $block->escapeJs($block->getGridIdsJson()) .'\');'; +endif; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml index c0f30fc282f38..495cb572fe125 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/massaction_extended.phtml @@ -3,10 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - <?php if ($block->getHideFormElement() !== true) : ?> + <?php if ($block->getHideFormElement() !== true): ?> <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> <?php endif ?> <div class="admin__grid-massaction-form"> @@ -14,20 +16,25 @@ <select id="<?= $block->getHtmlId() ?>-select" class="required-entry local-validation admin__control-select"> - <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> - <?php foreach ($block->getItems() as $_item) : ?> - <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> + <option class="admin__control-select-placeholder" value="" selected> + <?= $block->escapeHtml(__('Actions')) ?> + </option> + <?php foreach ($block->getItems() as $_item): ?> + <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" + <?= ($_item->getSelected() ? ' selected="selected"' : '') ?>> + <?= $block->escapeHtml($_item->getLabel()) ?> + </option> <?php endforeach; ?> </select> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> <?= $block->getApplyButtonHtml() ?> </div> - <?php if ($block->getHideFormElement() !== true) : ?> + <?php if ($block->getHideFormElement() !== true): ?> </form> <?php endif ?> <div class="no-display"> - <?php foreach ($block->getItems() as $_item) : ?> + <?php foreach ($block->getItems() as $_item): ?> <div id="<?= $block->getHtmlId() ?>-item-<?= /* @noEscape */ $_item->getId() ?>-block"> <?= $_item->getAdditionalActionBlockHtml() ?> </div> @@ -40,7 +47,7 @@ data-menu="grid-mass-select"> <optgroup label="<?= $block->escapeHtml(__('Mass Actions')) ?>"> <option disabled selected></option> - <?php if ($block->getUseSelectAll()) : ?> + <?php if ($block->getUseSelectAll()): ?> <option value="selectAll"> <?= $block->escapeHtml(__('Select All')) ?> </option> @@ -58,33 +65,41 @@ </select> <label for="<?= $block->getHtmlId() ?>-mass-select"></label> </div> -<script> + <?php $scriptString = <<<script require(['jquery'], function($){ 'use strict'; - $('#<?= $block->getHtmlId() ?>-mass-select').change(function () { + $('#{$block->getHtmlId()}-mass-select').change(function () { var massAction = $('option:selected', this).val(); switch (massAction) { - <?php if ($block->getUseSelectAll()) : ?> +script; + if ($block->getUseSelectAll()): + $scriptString .= <<<script case 'selectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); + return {$block->escapeJs($block->getJsObjectName())}.selectAll(); break; case 'unselectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); + return {$block->escapeJs($block->getJsObjectName())}.unselectAll(); break; - <?php endif; ?> +script; +endif; + $scriptString .= <<<script case 'selectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.selectVisible(); break; case 'unselectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.unselectVisible(); break; } this.blur(); }); }); - - <?php if (!$block->getParentBlock()->canDisplayContainer()) : ?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= /* @noEscape */ $block->getGridIdsJson() ?>'); - <?php endif; ?> -</script> +script; + if (!$block->getParentBlock()->canDisplayContainer()): + $gridIdsJson = /* @noEscape */ $block->getGridIdsJson(); + $scriptString .= <<<script + {$block->escapeJs($block->getJsObjectName())}.setGridIds('{$gridIdsJson}'); +script; + endif; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml index 2208a00929592..9ddf3ea5df3c8 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/grid/serializer.phtml @@ -7,6 +7,7 @@ <?php /** * @var $block \Magento\Backend\Block\Widget\Grid\Serializer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -14,8 +15,8 @@ $_id = 'id_' . md5(microtime()); ?> <?php $formId = $block->getFormId()?> -<?php if (!empty($formId)) : ?> -<script> +<?php if (!empty($formId)): ?> + <?php $scriptString = <<<script require([ 'prototype', 'mage/adminhtml/grid' @@ -23,24 +24,33 @@ $_id = 'id_' . md5(microtime()); Event.observe(window, "load", function(){ var serializeInput = document.createElement('input'); serializeInput.type = 'hidden'; - serializeInput.name = '<?= $block->escapeJs($block->getInputElementName()) ?>'; - serializeInput.id = '<?= /* @noEscape */ $_id ?>'; + serializeInput.name = '{$block->escapeJs($block->getInputElementName())}'; + serializeInput.id = '{$_id}'; try { - document.getElementById('<?= $block->escapeJs($formId) ?>').appendChild(serializeInput); - new serializerController('<?= /* @noEscape */ $_id ?>', <?= /* @noEscape */ $block->getDataAsJSON() ?>, <?= /* @noEscape */ $block->getColumnInputNames(true) ?>, <?= $block->escapeJs($block->getGridBlock()->getJsObjectName()) ?>, '<?= $block->escapeJs($block->getReloadParamName()) ?>'); + document.getElementById('{$block->escapeJs($formId)}').appendChild(serializeInput); + new serializerController('{$_id}', {$block->getDataAsJSON()}, {$block->getColumnInputNames(true)}, + {$block->escapeJs($block->getGridBlock()->getJsObjectName())}, + '{$block->escapeJs($block->getReloadParamName())}'); } catch(e) { //Error add serializer } }); }); -</script> -<?php else :?> -<input type="hidden" name="<?= $block->escapeHtmlAttr($block->getInputElementName()) ?>" value="" id="<?= /* @noEscape */ $_id ?>" /> -<script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php else:?> +<input type="hidden" name="<?= $block->escapeHtmlAttr($block->getInputElementName()) ?>" value="" + id="<?= /* @noEscape */ $_id ?>" /> + <?php $scriptString = <<<script require([ 'mage/adminhtml/grid' ], function(){ - new serializerController('<?= /* @noEscape */ $_id ?>', <?= /* @noEscape */ $block->getDataAsJSON() ?>, <?= /* @noEscape */ $block->getColumnInputNames(true) ?>, <?= $block->escapeJs($block->getGridBlock()->getJsObjectName()) ?>, '<?= $block->escapeJs($block->getReloadParamName()) ?>'); + new serializerController('{$_id}', {$block->getDataAsJSON()}, {$block->getColumnInputNames(true)}, + {$block->escapeJs($block->getGridBlock()->getJsObjectName())}, + '{$block->escapeJs($block->getReloadParamName())}'); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml index 5246aac088a5b..51183f733434e 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabs.phtml @@ -5,29 +5,38 @@ */ /** @var $block \Magento\Backend\Block\Widget\Tabs */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if (!empty($tabs)) : ?> +<?php if (!empty($tabs)): ?> <div class="admin__page-nav" data-role="container" id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> - <?php if ($block->getTitle()) : ?> + <?php if ($block->getTitle()): ?> <div class="admin__page-nav-title" data-role="title" <?= /* @noEscape */ $block->getUiId('title') ?>> <strong><?= $block->escapeHtml($block->getTitle()) ?></strong> <span data-role="title-messages" class="admin__page-nav-title-messages"></span> </div> <?php endif ?> - <ul <?= /* @noEscape */ $block->getUiId('tab', $block->getId()) ?> class="<?= /* @noEscape */ $block->getIsHoriz() ? 'tabs-horiz' : 'tabs admin__page-nav-items' ?>"> - <?php foreach ($tabs as $_tab) : ?> + <ul <?= /* @noEscape */ $block->getUiId('tab', $block->getId()) ?> + class="<?= /* @noEscape */ $block->getIsHoriz() ? 'tabs-horiz' : 'tabs admin__page-nav-items' ?>"> + <?php foreach ($tabs as $_tab): ?> <?php - if (!$block->canShowTab($_tab)) : + if (!$block->canShowTab($_tab)): continue; endif; ?> - <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> - <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> - <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> + <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . + (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> + <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? + 'link' : '' ?> + <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : + $block->getTabUrl($_tab) ?> - <li class="admin__page-nav-item" <?php if ($block->getTabIsHidden($_tab)) : ?> style="display:none"<?php endif; ?><?= /* @noEscape */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> - <a href="<?= $block->escapeUrl($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" name="<?= $block->escapeHtmlAttr($block->getTabId($_tab, false)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" + <li class="admin__page-nav-item no-display" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" + <?= /* @noEscape */ $block->getUiId('tab', 'item', $_tab->getId()) ?>> + <a href="<?= $block->escapeUrl($_tabHref) ?>" + id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" + name="<?= $block->escapeHtmlAttr($block->getTabId($_tab, false)) ?>" + title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" class="admin__page-nav-link <?= $block->escapeHtmlAttr($_tabClass) ?>" data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>" <?= /* @noEscape */ $block->getUiId('tab', 'link', $_tab->getId()) ?>> @@ -38,13 +47,17 @@ <span class="admin__page-nav-item-message _changed"> <span class="admin__page-nav-item-message-icon"></span> <span class="admin__page-nav-item-message-tooltip"> - <?= $block->escapeHtml(__('Changes have been made to this section that have not been saved.')) ?> + <?= $block->escapeHtml(__( + 'Changes have been made to this section that have not been saved.' + )) ?> </span> </span> <span class="admin__page-nav-item-message _error"> <span class="admin__page-nav-item-message-icon"></span> <span class="admin__page-nav-item-message-tooltip"> - <?= $block->escapeHtml(__('This tab contains invalid data. Please resolve this before saving.')) ?> + <?= $block->escapeHtml(__( + 'This tab contains invalid data. Please resolve this before saving.' + )) ?> </span> </span> <span class="admin__page-nav-item-message-loader"> @@ -55,23 +68,49 @@ </span> </span> </a> - <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" style="display:none;"<?= /* @noEscape */ $block->getUiId('tab', 'content', $_tab->getId()) ?>><?= /* @noEscape */ $block->getTabContent($_tab) ?></div> + <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" + <?= /* @noEscape */ $block->getUiId('tab', 'content', $_tab->getId()) ?>> + <?= /* @noEscape */ $block->getTabContent($_tab) ?> + </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#' . $block->escapeJs($block->getTabId($_tab)) . '_content' + ); ?> </li> + <?php $scriptString = <<<script + require(['jquery'], function($){ + 'use strict'; +script; + if ($block->getTabIsHidden($_tab)): + $scriptString .= <<<script + $('li.admin__page-nav-item#{$block->escapeJs($block->getTabId($_tab))}').css('display', 'none'); +script; + endif; + + $scriptString .= <<<script + $('li.admin__page-nav-item#{$block->escapeJs($block->getTabId($_tab))}').removeClass('no-display'); + }) +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endforeach; ?> </ul> </div> - -<script> -require(['jquery',"mage/backend/tabs"], function($){ + <?php $scriptString = <<<script +require(['jquery','mage/backend/tabs'], function($){ $(function() { - $('#<?= /* @noEscape */ $block->getId() ?>').tabs({ - active: '<?= /* @noEscape */ $block->getActiveTabId() ?>', - destination: '#<?= /* @noEscape */ $block->getDestElementId() ?>', - shadowTabs: <?= /* @noEscape */ $block->getAllShadowTabs() ?>, - tabsBlockPrefix: '<?= /* @noEscape */ $block->getId() ?>_', +script; + $scriptString .= '$(\'#' . /* @noEscape */ $block->getId() . '\').tabs({' . PHP_EOL . + 'active: \'' . /* @noEscape */ $block->getActiveTabId() . '\',' . PHP_EOL . + 'destination: \'#' . /* @noEscape */ $block->getDestElementId() . '\',' . PHP_EOL . + 'shadowTabs: ' . /* @noEscape */ $block->getAllShadowTabs() . ',' . PHP_EOL . + 'tabsBlockPrefix: \'' . /* @noEscape */ $block->getId() . '_\',' . PHP_EOL; + $scriptString .= <<<script tabIdArgument: 'active_tab' }); }); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml index 747dc577d2348..c51b357091bda 100644 --- a/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml +++ b/app/code/Magento/Backend/view/adminhtml/templates/widget/tabshoriz.phtml @@ -3,40 +3,62 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<!-- <?php if ($block->getTitle()) : ?> +<!-- <?php if ($block->getTitle()): ?> <h3><?= $block->escapeHtml($block->getTitle()) ?></h3> <?php endif ?> --> -<?php if (!empty($tabs)) : ?> +<?php if (!empty($tabs)): ?> <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> <ul class="tabs-horiz"> - <?php foreach ($tabs as $_tab) : ?> - <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> + <?php foreach ($tabs as $_tab): ?> + <?php $_tabClass = 'tab-item-link ' . $block->getTabClass($_tab) . ' ' . + (preg_match('/\s?ajax\s?/', $_tab->getClass()) ? 'notloaded' : '') ?> <?php $_tabType = (!preg_match('/\s?ajax\s?/', $_tabClass) && $block->getTabUrl($_tab) != '#') ? 'link' : '' ?> - <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? '#' . $block->getTabId($_tab) . '_content' : $block->getTabUrl($_tab) ?> + <?php $_tabHref = $block->getTabUrl($_tab) == '#' ? + '#' . $block->getTabId($_tab) . '_content' : + $block->getTabUrl($_tab) ?> <li> - <a href="<?= $block->escapeHtmlAttr($_tabHref) ?>" id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" class="<?= $block->escapeHtmlAttr($_tabClass) ?>" data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>"> + <a href="<?= $block->escapeUrl($_tabHref) ?>" + id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>" + title="<?= $block->escapeHtmlAttr($block->getTabTitle($_tab)) ?>" + class="<?= $block->escapeHtmlAttr($_tabClass) ?>" + data-tab-type="<?= $block->escapeHtmlAttr($_tabType) ?>"> <span> - <span class="changed" title="<?= $block->escapeHtmlAttr(__('The information in this tab has been changed.')) ?>"></span> - <span class="error" title="<?= $block->escapeHtmlAttr(__('This tab contains invalid data. Please resolve this before saving.')) ?>"></span> - <span class="loader" title="<?= $block->escapeHtmlAttr(__('Loading...')) ?>"></span> + <span class="changed" + title="<?= $block->escapeHtmlAttr(__('The information in this tab has been changed.')) ?>"></span> + <span class="error" + title="<?= $block->escapeHtmlAttr(__( + 'This tab contains invalid data. Please resolve this before saving.' + )) ?>"></span> + <span class="loader" + title="<?= $block->escapeHtmlAttr(__('Loading...')) ?>"></span> <?= $block->escapeHtml($block->getTabLabel($_tab)) ?> </span> </a> - <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content" style="display:none"><?= /* @noEscape */ $block->getTabContent($_tab) ?></div> + <div id="<?= $block->escapeHtmlAttr($block->getTabId($_tab)) ?>_content"> + <?= /* @noEscape */ $block->getTabContent($_tab) ?> + </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#' . $block->escapeJs($block->getTabId($_tab)) . '_content' + ); ?> </li> <?php endforeach; ?> </ul> </div> -<script> + <?php $scriptString = <<<script require(["jquery","mage/backend/tabs"], function($){ $(function() { - $('#<?= /* @noEscape */ $block->getId() ?>').tabs({ - active: '<?= /* @noEscape */ $block->getActiveTabId() ?>', - destination: '#<?= /* @noEscape */ $block->getDestElementId() ?>', - shadowTabs: <?= /* @noEscape */ $block->getAllShadowTabs() ?> + $('#{$block->getId()}').tabs({ + active: '{$block->getActiveTabId()}', + destination: '#{$block->getDestElementId()}', + shadowTabs: {$block->getAllShadowTabs()} }); }); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Backup/Controller/Adminhtml/Index.php b/app/code/Magento/Backup/Controller/Adminhtml/Index.php index b62963947d7bf..64052254f5233 100644 --- a/app/code/Magento/Backup/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Backup/Controller/Adminhtml/Index.php @@ -87,6 +87,7 @@ public function __construct( /** * @inheritDoc + * @since 100.2.6 */ public function dispatch(\Magento\Framework\App\RequestInterface $request) { diff --git a/app/code/Magento/Backup/Helper/Data.php b/app/code/Magento/Backup/Helper/Data.php index c6df6a7366852..a29aa01e64d46 100644 --- a/app/code/Magento/Backup/Helper/Data.php +++ b/app/code/Magento/Backup/Helper/Data.php @@ -293,6 +293,7 @@ public function extractDataFromFilename($filename) * Is backup functionality enabled. * * @return bool + * @since 100.2.6 */ public function isEnabled(): bool { diff --git a/app/code/Magento/Backup/Model/Db.php b/app/code/Magento/Backup/Model/Db.php index 084b35448a823..0d117a7dff818 100644 --- a/app/code/Magento/Backup/Model/Db.php +++ b/app/code/Magento/Backup/Model/Db.php @@ -16,7 +16,7 @@ * * @api * @since 100.0.2 - * @deprecated Backup module is to be removed. + * @deprecated 100.2.6 Backup module is to be removed. */ class Db implements \Magento\Framework\Backup\Db\BackupDbInterface { diff --git a/app/code/Magento/Backup/Model/ResourceModel/Db.php b/app/code/Magento/Backup/Model/ResourceModel/Db.php index 6e7d6f9863f33..c38a7b3005e21 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Db.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Db.php @@ -120,6 +120,7 @@ public function getTableForeignKeysSql($tableName = null) * @param string|null $tableName * @param bool $addDropIfExists * @return string + * @since 100.2.3 */ public function getTableTriggersSql($tableName = null, $addDropIfExists = true) { diff --git a/app/code/Magento/Backup/Model/ResourceModel/Helper.php b/app/code/Magento/Backup/Model/ResourceModel/Helper.php index dace56fd60688..b5a5faae1fde4 100644 --- a/app/code/Magento/Backup/Model/ResourceModel/Helper.php +++ b/app/code/Magento/Backup/Model/ResourceModel/Helper.php @@ -345,6 +345,7 @@ public function restoreTransactionIsolationLevel() * @param boolean $addDropIfExists * @param boolean $stripDefiner * @return string + * @since 100.2.3 */ public function getTableTriggersSql($tableName, $addDropIfExists = false, $stripDefiner = true) { diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/AdminAssertBackupLinkAbsentInMenuActionGroup.xml b/app/code/Magento/Backup/Test/Mftf/ActionGroup/AdminAssertBackupLinkAbsentInMenuActionGroup.xml new file mode 100644 index 0000000000000..094c6292684f6 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/ActionGroup/AdminAssertBackupLinkAbsentInMenuActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertBackupLinkAbsentInMenuActionGroup"> + <annotations> + <description>Verify 'Backup' link is absent in admin menu.</description> + </annotations> + + <click selector="{{AdminMenuSection.menuItem('magento-backend-system')}}" stepKey="clickSystem"/> + <dontSeeElement selector="{{AdminMenuSection.menuItem('magento-backup-system-tools-backup')}}" stepKey="dontSeeBackup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/Test/AdminSystemBackupMenuTest.xml b/app/code/Magento/Backup/Test/Mftf/Test/AdminSystemBackupMenuTest.xml new file mode 100644 index 0000000000000..af86163fbfe64 --- /dev/null +++ b/app/code/Magento/Backup/Test/Mftf/Test/AdminSystemBackupMenuTest.xml @@ -0,0 +1,30 @@ +<?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="AdminSystemBackupMenuTest"> + <annotations> + <features value="Backup"/> + <stories value="Backup menu not visible if config disabled"/> + <title value="Backup menu not visible if backup config disabled"/> + <description value="Disable backup config and check backup menu isn't visible"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-36292"/> + <group value="backup"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminAssertBackupLinkAbsentInMenuActionGroup" stepKey="verifyBackupLinkAbsentInMenu"/> + </test> +</tests> diff --git a/app/code/Magento/Backup/etc/adminhtml/menu.xml b/app/code/Magento/Backup/etc/adminhtml/menu.xml index 27991e57b8485..d6967a6a7932e 100644 --- a/app/code/Magento/Backup/etc/adminhtml/menu.xml +++ b/app/code/Magento/Backup/etc/adminhtml/menu.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> <menu> - <add id="Magento_Backup::system_tools_backup" title="Backups" translate="title" module="Magento_Backup" sortOrder="30" parent="Magento_Backend::system_tools" action="backup/index" resource="Magento_Backup::backup"/> + <add id="Magento_Backup::system_tools_backup" title="Backups" translate="title" module="Magento_Backup" sortOrder="30" parent="Magento_Backend::system_tools" action="backup/index" resource="Magento_Backup::backup" dependsOnConfig="system/backup/functionality_enabled"/> </menu> </config> diff --git a/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml b/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml index 81aa49efd11e8..33313e71d8c6a 100644 --- a/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml +++ b/app/code/Magento/Backup/view/adminhtml/templates/backup/dialogs.phtml @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <!-- TODO: refactor form styles and js --> <script type="text/x-magento-template" id="rollback-warning-template"> -<p><?= $block->escapeHtml(__('You will lose any data created since the backup was made, including admin users, customers and orders.')) ?></p> +<p><?= $block->escapeHtml(__( + 'You will lose any data created since the backup was made, including admin users, customers and orders.' +)) ?></p> <p><?= $block->escapeHtml(__('Are you sure you want to continue?')) ?></p> </script> <script type="text/x-magento-template" id="backup-options-template"> - <div class="backup-messages" style="display: none;"> + <div class="backup-messages no-display"> <div class="messages"></div> </div> <div class="messages"> @@ -21,33 +25,56 @@ <form action="" method="post" id="backup-form" class="form-inline"> <fieldset class="admin__fieldset form-list question"> <div class="admin__field field _required"> - <label for="backup_name" class="admin__field-label"><span><?= $block->escapeHtml(__('Backup Name')) ?></span></label> + <label for="backup_name" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Backup Name')) ?></span> + </label> <div class="admin__field-control"> <input type="text" name="backup_name" id="backup_name" - class="admin__control-text required-entry validate-alphanum-with-spaces validate-length maximum-length-50" + class="admin__control-text required-entry validate-alphanum-with-spaces validate-length + maximum-length-50" maxlength="50" /> <div class="admin__field-note"> - <?= $block->escapeHtml(__('Please use only letters (a-z or A-Z), numbers (0-9) or spaces in this field.')) ?> + <?= $block->escapeHtml(__( + 'Please use only letters (a-z or A-Z), numbers (0-9) or spaces in this field.' + )) ?> </div> </div> </div> <div class="admin__field field maintenance-checkbox-container"> - <label for="backup_maintenance_mode" class="admin__field-label"><span><?= $block->escapeHtml(__('Maintenance mode')) ?></span></label> + <label for="backup_maintenance_mode" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Maintenance mode')) ?></span> + </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="maintenance_mode" value="1" id="backup_maintenance_mode"/> - <label class="admin__field-label" for="backup_maintenance_mode"><?= $block->escapeHtml(__('Please put your store into maintenance mode during backup.')) ?></label> + <input class="admin__control-checkbox" + type="checkbox" + name="maintenance_mode" + value="1" + id="backup_maintenance_mode"/> + <label class="admin__field-label" + for="backup_maintenance_mode"><?= $block->escapeHtml(__( + 'Please put your store into maintenance mode during backup.' + )) ?></label> </div> </div> </div> - <div class="admin__field field maintenance-checkbox-container" id="exclude-media-checkbox-container" style="display: none;"> - <label for="exclude_media" class="admin__field-label"><span><?= $block->escapeHtml(__('Exclude')) ?></span></label> + <div class="admin__field field maintenance-checkbox-container no-display" + id="exclude-media-checkbox-container"> + <label for="exclude_media" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Exclude')) ?></span> + </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="exclude_media" value="1" id="exclude_media"/> - <label class="admin__field-label" for="exclude_media"><?= $block->escapeHtml(__('Exclude media folder from backup')) ?></label> + <input class="admin__control-checkbox" + type="checkbox" + name="exclude_media" + value="1" + id="exclude_media"/> + <label class="admin__field-label" + for="exclude_media"><?= $block->escapeHtml(__('Exclude media folder from backup')) ?> + </label> </div> </div> </div> @@ -56,7 +83,7 @@ </script> <script type="text/x-magento-template" id="rollback-request-password-template"> - <div class="backup-messages" style="display: none;"> + <div class="backup-messages no-display"> <div class="messages"></div> </div> <div class="messages"> @@ -69,44 +96,63 @@ <form action="" method="post" id="rollback-form" class="form-inline"> <fieldset class="admin__fieldset password-box-container"> <div class="admin__field field _required"> - <label for="password" class="admin__field-label"><span><?= $block->escapeHtml(__('User Password')) ?></span></label> - <div class="admin__field-control"><input type="password" name="password" id="password" class="admin__control-text required-entry" autocomplete="new-password"></div> + <label for="password" class="admin__field-label"> + <span><?= $block->escapeHtml(__('User Password')) ?></span> + </label> + <div class="admin__field-control"> + <input type="password" name="password" id="password" class="admin__control-text required-entry" + autocomplete="new-password"> + </div> </div> <div class="admin__field field maintenance-checkbox-container"> - <label for="rollback_maintenance_mode" class="admin__field-label"><span><?= $block->escapeHtml(__('Maintenance mode')) ?></span></label> + <label for="rollback_maintenance_mode" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Maintenance mode')) ?></span> + </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="maintenance_mode" value="1" id="rollback_maintenance_mode"/> - <label class="admin__field-label" for="rollback_maintenance_mode"><?= $block->escapeHtml(__('Please put your store into maintenance mode during rollback processing.')) ?></label> - </div> + <input class="admin__control-checkbox" type="checkbox" name="maintenance_mode" value="1" + id="rollback_maintenance_mode"/> + <label class="admin__field-label" for="rollback_maintenance_mode"> + <?= $block->escapeHtml(__( + 'Please put your store into maintenance mode during rollback processing.' + )) ?></label> + </div> </div> </div> - <div class="admin__field field maintenance-checkbox-container" id="use-ftp-checkbox-row" style="display: none;"> + <div class="admin__field field maintenance-checkbox-container" id="use-ftp-checkbox-row"> <label for="use_ftp" class="admin__field-label"> <span><?= $block->escapeHtml(__('FTP')) ?></span> </label> <div class="admin__field-control"> <div class="admin__field-option"> - <input class="admin__control-checkbox" type="checkbox" name="use_ftp" value="1" id="use_ftp" onclick="backup.toggleFtpCredentialsForm(event)"/> - <label class="admin__field-label" for="use_ftp"><?= $block->escapeHtml(__('Use FTP Connection')) ?></label> + <input class="admin__control-checkbox" type="checkbox" name="use_ftp" value="1" id="use_ftp"/> + <label class="admin__field-label" for="use_ftp"> + <?= $block->escapeHtml(__('Use FTP Connection')) ?> + </label> </div> </div> </div> </fieldset> - <div class="entry-edit" id="ftp-credentials-container" style="display: none;"> + <div class="entry-edit no-display" id="ftp-credentials-container"> <fieldset class="admin__fieldset"> - <legend class="admin__legend legend"><span><?= $block->escapeHtml(__('FTP credentials')) ?></span></legend><br /> + <legend class="admin__legend legend"> + <span><?= $block->escapeHtml(__('FTP credentials')) ?></span> + </legend><br /> <div class="admin__field field _required"> - <label class="admin__field-label" for="ftp_host"><span><?= $block->escapeHtml(__('FTP Host')) ?></span></label> + <label class="admin__field-label" for="ftp_host"> + <span><?= $block->escapeHtml(__('FTP Host')) ?></span> + </label> <div class="admin__field-control"> <input type="text" class="admin__control-text" name="ftp_host" id="ftp_host"> </div> </div> <div class="admin__field field _required"> - <label class="admin__field-label" for="ftp_user"><span><?= $block->escapeHtml(__('FTP Login')) ?></span></label> + <label class="admin__field-label" for="ftp_user"> + <span><?= $block->escapeHtml(__('FTP Login')) ?></span> + </label> <div class="admin__field-control"> <input type="text" class="admin__control-text" name="ftp_user" id="ftp_user"> </div> @@ -116,7 +162,8 @@ <span><?= $block->escapeHtml(__('FTP Password')) ?></span> </label> <div class="admin__field-control"> - <input type="password" class="admin__control-text" name="ftp_pass" id="ftp_pass" autocomplete="new-password"> + <input type="password" class="admin__control-text" name="ftp_pass" id="ftp_pass" + autocomplete="new-password"> </div> </div> <div class="admin__field field"> @@ -136,17 +183,25 @@ $backupUrl = $block->getUrl('*/*/create'); ?> -<script> +<?php $scriptString = <<<script + require([ - "prototype", - "mage/adminhtml/backup" + 'prototype', + 'mage/adminhtml/backup' ], function(){ //<![CDATA[ backup = new AdminBackup(); - backup.rollbackUrl = '<?= $block->escapeUrl($rollbackUrl) ?>'; - backup.backupUrl = '<?= $block->escapeUrl($backupUrl) ?>'; + backup.rollbackUrl = '{$block->escapeJs($rollbackUrl)}'; + backup.backupUrl = '{$block->escapeJs($backupUrl)}'; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?=/* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'backup.toggleFtpCredentialsForm(event)', + '#use_ftp' +) ?> diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php index 46db8a9907341..05a2a51c51213 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Checkbox.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option checkbox type renderer * @@ -19,22 +22,71 @@ class Checkbox extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Op */ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/checkbox.phtml'; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** * @inheritdoc + * @since 100.3.1 */ public function getSelectionPrice($selection) { diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php index 629f08dc75106..af3642995a6c4 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Multi.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option multi select type renderer * @@ -19,22 +22,71 @@ class Multi extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio */ protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/multi.phtml'; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** * @inheritdoc + * @since 100.3.1 */ public function getSelectionPrice($selection) { diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php index 1519b3a67ac97..a9b8f7880cac3 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Radio.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option radiobox type renderer * @@ -20,18 +23,64 @@ class Radio extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Optio protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/radio.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + + /** + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return $this->secureRenderer->renderTag('script', [], $scriptString, false); } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php index 502dfa32044a3..948d0c4a84c92 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/Select.php @@ -5,6 +5,9 @@ */ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle option dropdown type renderer * @@ -20,18 +23,63 @@ class Select extends \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Opti protected $_template = 'Magento_Bundle::product/composite/fieldset/options/type/select.phtml'; /** - * @param string $elementId - * @param string $containerId - * @return string + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\Catalog\Helper\Data $catalogData + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Stdlib\StringUtils $string + * @param \Magento\Framework\Math\Random $mathRandom + * @param \Magento\Checkout\Helper\Cart $cartHelper + * @param \Magento\Tax\Helper\Data $taxData + * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\Catalog\Helper\Data $catalogData, + \Magento\Framework\Registry $registry, + \Magento\Framework\Stdlib\StringUtils $string, + \Magento\Framework\Math\Random $mathRandom, + \Magento\Checkout\Helper\Cart $cartHelper, + \Magento\Tax\Helper\Data $taxData, + \Magento\Framework\Pricing\Helper\Data $pricingHelper, + array $data = [], + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $jsonEncoder, + $catalogData, + $registry, + $string, + $mathRandom, + $cartHelper, + $taxData, + $pricingHelper, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + + /** + * @inheritdoc */ public function setValidationContainer($elementId, $containerId) { - return '<script> - document.getElementById(\'' . + $scriptString = 'document.getElementById(\'' . $elementId . '\').advaiceContainer = \'' . $containerId . - '\'; - </script>'; + '\';'; + + return $this->secureRenderer->renderTag('script', [], $scriptString, false); } } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php index 0fe8c38cc4992..cc28fab403fa4 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Attributes.php @@ -6,12 +6,43 @@ namespace Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Bundle product attributes tab * @SuppressWarnings(PHPMD.DepthOfInheritance) */ class Attributes extends \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes { + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Framework\Data\FormFactory $formFactory + * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Framework\Data\FormFactory $formFactory, + array $data = [], + SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct( + $context, + $registry, + $formFactory, + $data + ); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Prepare attributes form of bundle product * @@ -69,9 +100,7 @@ protected function _prepareForm() $tax = $this->getForm()->getElement('tax_class_id'); if ($tax) { - $tax->setAfterElementHtml( - '<script>' . - " + $scriptString = " require(['prototype'], function(){ function changeTaxClassId() { if ($('price_type').value == '" . @@ -96,9 +125,9 @@ function changeTaxClassId() { changeTaxClassId(); } }); - " . - '</script>' - ); + "; + + $tax->setAfterElementHtml($this->secureRenderer->renderTag('script', [], $scriptString, false)); } $weight = $this->getForm()->getElement('weight'); diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php index 491f6c3fb1096..befa5794bfb69 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option.php @@ -9,6 +9,8 @@ use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Store\Model\Store; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** * Block for rendering option of bundle product @@ -59,17 +61,20 @@ class Option extends \Magento\Backend\Block\Widget * @param \Magento\Bundle\Model\Source\Option\Type $optionTypes * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Config\Model\Config\Source\Yesno $yesno, \Magento\Bundle\Model\Source\Option\Type $optionTypes, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_coreRegistry = $registry; $this->_optionTypes = $optionTypes; $this->_yesno = $yesno; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php index b4134e7e3a97e..29c85fcf455f9 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/Items/Renderer.php @@ -7,6 +7,8 @@ use Magento\Catalog\Model\Product\Type\AbstractType; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Adminhtml sales order item renderer @@ -30,6 +32,7 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Items\Renderer\DefaultRend * @param \Magento\Framework\Registry $registry * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param CatalogHelper|null $catalogHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -37,11 +40,11 @@ public function __construct( \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, \Magento\Framework\Registry $registry, array $data = [], - Json $serializer = null + Json $serializer = null, + ?CatalogHelper $catalogHelper = null ) { - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(Json::class); - + $this->serializer = $serializer ?: ObjectManager::getInstance()->get(Json::class); + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct($context, $stockRegistry, $stockConfiguration, $registry, $data); } diff --git a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php index 9fe8891254a5a..dee924ae3cf5e 100644 --- a/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php +++ b/app/code/Magento/Bundle/Block/Adminhtml/Sales/Order/View/Items/Renderer.php @@ -6,7 +6,9 @@ namespace Magento\Bundle\Block\Adminhtml\Sales\Order\View\Items; use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Adminhtml sales order item renderer @@ -32,6 +34,7 @@ class Renderer extends \Magento\Sales\Block\Adminhtml\Order\View\Items\Renderer\ * @param \Magento\Checkout\Helper\Data $checkoutHelper * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json $serializer + * @param CatalogHelper|null $catalogHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -41,10 +44,11 @@ public function __construct( \Magento\GiftMessage\Helper\Message $messageHelper, \Magento\Checkout\Helper\Data $checkoutHelper, array $data = [], - Json $serializer = null + Json $serializer = null, + ?CatalogHelper $catalogHelper = null ) { - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(Json::class); + $this->serializer = $serializer ?? ObjectManager::getInstance()->get(Json::class); + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct( $context, @@ -63,7 +67,7 @@ public function __construct( * @param string $value * @param int $length * @param string $etc - * @param string &$remainder + * @param string $remainder * @param bool $breakWords * @return string */ @@ -76,6 +80,8 @@ public function truncateString($value, $length = 80, $etc = '...', &$remainder = } /** + * Get is shipment separately. + * * @param null|object $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -109,6 +115,8 @@ public function isShipmentSeparately($item = null) } /** + * Get is child calculated. + * * @param null|object $item * @return bool * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -144,6 +152,8 @@ public function isChildCalculated($item = null) } /** + * Return selection attributes. + * * @param mixed $item * @return mixed */ @@ -161,6 +171,8 @@ public function getSelectionAttributes($item) } /** + * Return order options. + * * @return array */ public function getOrderOptions() @@ -182,6 +194,8 @@ public function getOrderOptions() } /** + * Return value html. + * * @param object $item * @return string */ @@ -204,6 +218,8 @@ public function getValueHtml($item) } /** + * Return can show price. + * * @param object $item * @return bool */ diff --git a/app/code/Magento/Bundle/Model/Product/Type.php b/app/code/Magento/Bundle/Model/Product/Type.php index 2dc519dbf1540..fe120e9a179dd 100644 --- a/app/code/Magento/Bundle/Model/Product/Type.php +++ b/app/code/Magento/Bundle/Model/Product/Type.php @@ -6,6 +6,8 @@ namespace Magento\Bundle\Model\Product; +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\ResourceModel\Option\Collection; use Magento\Bundle\Model\ResourceModel\Selection\Collection as Selections; use Magento\Bundle\Model\ResourceModel\Selection\Collection\FilterApplier as SelectionCollectionFilterApplier; use Magento\Catalog\Api\ProductRepositoryInterface; @@ -414,16 +416,13 @@ public function beforeSave($product) if ($product->getCanSaveBundleSelections()) { $product->canAffectOptions(true); $selections = $product->getBundleSelectionsData(); - if ($selections && !empty($selections)) { - $options = $product->getBundleOptionsData(); - if ($options) { - foreach ($options as $option) { - if (empty($option['delete']) || 1 != (int)$option['delete']) { - $product->setTypeHasOptions(true); - if (1 == (int)$option['required']) { - $product->setTypeHasRequiredOptions(true); - break; - } + if (!empty($selections) && $options = $product->getBundleOptionsData()) { + foreach ($options as $option) { + if (empty($option['delete']) || 1 != (int)$option['delete']) { + $product->setTypeHasOptions(true); + if (1 == (int)$option['required']) { + $product->setTypeHasRequiredOptions(true); + break; } } } @@ -464,7 +463,7 @@ public function getOptionsIds($product) public function getOptionsCollection($product) { if (!$product->hasData($this->_keyOptionsCollection)) { - /** @var \Magento\Bundle\Model\ResourceModel\Option\Collection $optionsCollection */ + /** @var Collection $optionsCollection */ $optionsCollection = $this->_bundleOption->create() ->getResourceCollection(); $optionsCollection->setProductIdFilter($product->getEntityId()); @@ -530,10 +529,10 @@ public function getSelectionsCollection($optionIds, $product) * Example: the catalog inventory validation of decimal qty can change qty to int, * so need to change quote item qty option value too. * - * @param array $options - * @param \Magento\Framework\DataObject $option - * @param mixed $value - * @param \Magento\Catalog\Model\Product $product + * @param array $options + * @param \Magento\Framework\DataObject $option + * @param mixed $value + * @param \Magento\Catalog\Model\Product $product * @return $this */ public function updateQtyOption($options, \Magento\Framework\DataObject $option, $value, $product) @@ -682,6 +681,11 @@ protected function _prepareProduct(\Magento\Framework\DataObject $buyRequest, $p $options ); + $this->validateRadioAndSelectOptions( + $optionsCollection, + $options + ); + $selectionIds = array_values($this->arrayUtility->flatten($options)); // If product has not been configured yet then $selections array should be empty if (!empty($selectionIds)) { @@ -1184,9 +1188,11 @@ public function canConfigure($product) * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ + // @codingStandardsIgnoreStart public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product) { } + // @codingStandardsIgnoreEnd /** * Return array of specific to type product entities @@ -1196,18 +1202,19 @@ public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $product) */ public function getIdentities(\Magento\Catalog\Model\Product $product) { - $identities = parent::getIdentities($product); + $identities = []; + $identities[] = parent::getIdentities($product); /** @var \Magento\Bundle\Model\Option $option */ foreach ($this->getOptions($product) as $option) { if ($option->getSelections()) { /** @var \Magento\Catalog\Model\Product $selection */ foreach ($option->getSelections() as $selection) { - $identities = array_merge($identities, $selection->getIdentities()); + $identities[] = $selection->getIdentities(); } } } - return $identities; + return array_merge([], ...$identities); } /** @@ -1272,6 +1279,53 @@ protected function checkIsAllRequiredOptions($product, $isStrictProcessMode, $op } } + /** + * Validate Options for Radio and Select input types + * + * @param Collection $optionsCollection + * @param int[] $options + * @return void + * @throws \Magento\Framework\Exception\LocalizedException + */ + private function validateRadioAndSelectOptions($optionsCollection, $options): void + { + $errorTypes = []; + + if (is_array($optionsCollection->getItems())) { + foreach ($optionsCollection->getItems() as $option) { + if ($this->isSelectedOptionValid($option, $options)) { + $errorTypes[] = $option->getType(); + } + } + } + + if (!empty($errorTypes)) { + throw new \Magento\Framework\Exception\LocalizedException( + __( + 'Option type (%types) should have only one element.', + ['types' => implode(", ", $errorTypes)] + ) + ); + } + } + + /** + * Check if selected option is valid + * + * @param Option $option + * @param array $options + * @return bool + */ + private function isSelectedOptionValid($option, $options): bool + { + return ( + ($option->getType() == 'radio' || $option->getType() == 'select') && + isset($options[$option->getOptionId()]) && + is_array($options[$option->getOptionId()]) && + count($options[$option->getOptionId()]) > 1 + ); + } + /** * Check if selection is salable * @@ -1333,16 +1387,18 @@ protected function checkIsResult($_result) */ protected function mergeSelectionsWithOptions($options, $selections) { + $selections = []; + foreach ($options as $option) { $optionSelections = $option->getSelections(); if ($option->getRequired() && is_array($optionSelections) && count($optionSelections) == 1) { - $selections = array_merge($selections, $optionSelections); + $selections[] = $optionSelections; } else { $selections = []; break; } } - return $selections; + return array_merge([], ...$selections); } } diff --git a/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php index 11f7e2f3d1f15..4b5ec32bf61aa 100644 --- a/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php +++ b/app/code/Magento/Bundle/Pricing/Price/ConfiguredPrice.php @@ -18,6 +18,7 @@ /** * Configured price model * @api + * @since 100.0.2 */ class ConfiguredPrice extends CatalogPrice\FinalPrice implements ConfiguredPriceInterface { diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductSetAdvancedPricingActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductSetAdvancedPricingActionGroup.xml new file mode 100644 index 0000000000000..16b07a49fabda --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AdminBundleProductSetAdvancedPricingActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminBundleProductSetAdvancedPricingActionGroup" extends="ProductSetAdvancedPricingActionGroup"> + <annotations> + <description>Sets the provided Advanced Pricing on the Admin Bundle Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="priceView" type="string" defaultValue="Price Range"/> + </arguments> + <remove keyForRemoval="selectProductCustomGroupValue"/> + <selectOption selector="{{AdminProductFormAdvancedPricingSection.bundleAdvancedPriceView}}" userInput="{{priceView}}" stepKey="selectPriceView" before="clickDoneButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml new file mode 100644 index 0000000000000..a37bb443224b4 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessageActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontBundleValidationMessageActionGroup"> + <annotations> + <description>Check error message in validation message box</description> + </annotations> + <arguments> + <argument name="message" type="string"/> + </arguments> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <see selector="{{StorefrontBundledSection.validationMessageBox}}" userInput="{{message}}" stepKey="seeErrorHoldMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml new file mode 100644 index 0000000000000..35ac68b602a5e --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/AssertStorefrontBundleValidationMessagesCountActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontBundleValidationMessagesCountActionGroup"> + <annotations> + <description>Check if there's a validation message box on page and asserts the validation messages number</description> + </annotations> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeElement selector="{{StorefrontBundledSection.validationMessageBox}}" stepKey="seeErrorBox"/> + <seeNumberOfElements selector="{{StorefrontBundledSection.validationMessageBox}}" userInput="1" stepKey="seeOneErrorBox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml new file mode 100644 index 0000000000000..f0afcffca816c --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/ActionGroup/StorefrontAddToTheCartButtonActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAddToTheCartButtonActionGroup"> + <annotations> + <description>Clicks 'Add to Cart' on a Storefront Bundled Product page.</description> + </annotations> + + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="waitForAddToCartButton"/> + <click selector="{{StorefrontBundleProductActionSection.addToCartButton}}" stepKey="clickOnAddToCartButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.xml new file mode 100644 index 0000000000000..b30b599f4a034 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormAdvancedPricingSection.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="AdminProductFormAdvancedPricingSection"> + <element name="bundleAdvancedPriceView" type="select" selector="div[data-index='advanced-pricing'] select[name='product[price_view]']"/> + </section> +</sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml index 967cf5ac49ed5..6ad83ba1105f4 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/AdminProductFormBundleSection.xml @@ -20,6 +20,7 @@ <element name="bundleOptionXInputType" type="select" selector="[name='bundle_options[bundle_options][{{x}}][type]']" parameterized="true"/> <element name="bundleOptionXRequired" type="checkbox" selector="[name='bundle_options[bundle_options][{{x}}][required]']" parameterized="true"/> <element name="bundleOptionXProductYQuantity" type="input" selector="[name='bundle_options[bundle_options][{{x}}][bundle_selections][{{y}}][selection_qty]']" parameterized="true"/> + <element name="bundleOptionXProductYPrice" type="input" selector="[name='bundle_options[bundle_options][{{x}}][bundle_selections][{{y}}][selection_price_value]']" parameterized="true"/> <element name="addProductsToOption" type="button" selector="[data-index='modal_set']" timeout="30"/> <element name="nthAddProductsToOption" type="button" selector="//tr[{{var}}]//button[@data-index='modal_set']" timeout="30" parameterized="true"/> <element name="bundlePriceType" type="select" selector="bundle_options[bundle_options][0][bundle_selections][0][selection_price_type]"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml index 7a188fd58e1af..739c2839e990d 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/BundleStorefrontSection.xml @@ -14,8 +14,8 @@ <element name="bundleOptionSelection" type="checkbox" selector="//div[@class='nested options-list']/div[{{optionNumber}}]/label[@class='label']" parameterized="true"/> <!--Description--> <!--CE exclusively--> - <element name="longDescriptionText" type="text" selector="//*[@id='description']/div/div" timeout="30"/> - <element name="shortDescriptionText" type="text" selector="//div[@class='product attribute overview']" timeout="30"/> + <element name="longDescriptionText" type="text" selector="#description>div>div" timeout="30"/> + <element name="shortDescriptionText" type="text" selector="div.product.attribute.overview" timeout="30"/> <!--NameOfProductOnProductPage--> <element name="bundleProductName" type="text" selector="//*[@id='maincontent']//span[@itemprop='name']"/> <!--PageNotFoundErrorMessage--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml index c47cf6095c777..1dea8958c3552 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontBundledSection.xml @@ -17,7 +17,7 @@ <element name="updateCart" type="button" selector="#product-updatecart-button" timeout="30"/> <element name="configuredPrice" type="block" selector=".price-configured_price .price"/> <element name="fixedPricing" type="text" selector="//div[@class='price-box price-final_price']//span[@id]//..//span[contains(text(),'{{var1}}')]" parameterized="true"/> - <element name="customizeProduct" type="button" selector="//*[@id='bundle-slide']"/> + <element name="customizeProduct" type="button" selector="#bundle-slide"/> <element name="customizableBundleItemOption" type="text" selector="//div[@class='field choice'][1]//input[@type='checkbox']"/> <element name="customizableBundleItemOption2" type="text" selector="//div[@class='field choice'][2]//input[@type='checkbox']"/> <element name="nthOptionDiv" type="block" selector="#product-options-wrapper div.field.option:nth-of-type({{var}})" parameterized="true"/> @@ -38,5 +38,6 @@ <element name="currencyTrigger" type="select" selector="#switcher-currency-trigger" timeout="30"/> <element name="currency" type="select" selector="//a[text()='{{arg}}']" parameterized="true"/> <element name="multiSelectOption" type="select" selector="//div[@class='field option required']//select"/> + <element name="validationMessageBox" type="block" selector="#validation-message-box"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index fae1ec331b667..23b541273a861 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -13,6 +13,8 @@ <element name="priceTo" type="text" selector=".product-info-price .price-to"/> <element name="minPrice" type="text" selector="span[data-price-type='minPrice']"/> <element name="maxPrice" type="text" selector="span[data-price-type='minPrice']"/> + <element name="asLowAsFinalPrice" type="text" selector="div.price-box.price-final_price p.minimal-price > span.price-final_price span.price"/> + <element name="fixedFinalPrice" type="text" selector="div.price-box.price-final_price > span.price-final_price span.price"/> <element name="productBundleOptionsCheckbox" type="checkbox" selector="//*[@id='product-options-wrapper']//div[@class='fieldset']//label[contains(.,'{{childName}}')]/../input" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml index 4a78aeb752ca7..debf023ea0a65 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddBundleItemsTest.xml @@ -70,7 +70,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Checking on admin side--> @@ -117,7 +117,7 @@ <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '3')}}" userInput="{{BundleProduct.defaultQuantity}}" stepKey="fillNewProductDefaultQty2"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAgain"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--Checking on admin side--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml index 55038b0c68c44..0ba1cf50d9b59 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAddDefaultImageBundleProductTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml index 30922839a191d..f73941c375a41 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAssociateBundleProductToWebsitesTest.xml @@ -43,7 +43,9 @@ </createData> <!-- Reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml index 06a05e7a29cd9..33bfa455e2bdf 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminAttributeSetSelectionTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- Create a new attribute set --> <amOnPage url="{{AdminProductAttributeSetGridPage.url}}" stepKey="goToAttributeSets"/> @@ -49,7 +49,7 @@ <fillField selector="{{AdminProductFormBundleSection.productSku}}" userInput="{{BundleProduct.sku}}" stepKey="fillProductSku"/> <!--save the product/published by default--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Testing that price appears correctly in admin catalog--> @@ -70,7 +70,7 @@ <click selector="{{AdminProductFormSection.attributeSetFilterResult}}" stepKey="selectAttrSet2"/> <!--save the product/published by default--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown2"/> <!--Testing that price appears correctly in admin catalog--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml index b9fb1c72e079f..41b372cf150a0 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminBasicBundleProductAttributesTest.xml @@ -21,7 +21,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create attribute set--> <actionGroup ref="CreateDefaultAttributeSetActionGroup" stepKey="createDefaultAttributeSet"> @@ -143,7 +143,7 @@ <selectOption selector="{{AdminProductFormBundleSection.countryOfManufactureDropDown}}" userInput="France" stepKey="countryOfManufactureDropDown"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Verify form was filled out correctly after edit--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml index 82da228e040dc..5a7c1beeea706 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminCreateAndEditBundleProductOptionsNegativeTest.xml @@ -111,7 +111,7 @@ <!-- Save product form --> <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveWithTwoOptions"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveWithTwoOptions"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!-- Delete created bundle product --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml index 51c30ef86242c..9716791985970 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteABundleProductTest.xml @@ -25,7 +25,7 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> @@ -57,7 +57,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Go to catalog deletion page--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml index 5b603ef2f0a44..a2f26e235fc23 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleDynamicProductTest.xml @@ -40,10 +40,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createDynamicBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDynamicBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createDynamicBundleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createDynamicBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml index 46244603f2868..edde81f338437 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminDeleteBundleFixedProductTest.xml @@ -37,10 +37,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createFixedBundleProduct.name$$)}}" stepKey="amOnBundleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createFixedBundleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createFixedBundleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createFixedBundleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml index 4ba5d0f66e096..ac0c3e7b5b791 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminEditRelatedBundleProductTest.xml @@ -31,14 +31,13 @@ <argument name="product" value="BundleProduct"/> </actionGroup> <!--Logging out--> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct0" stepKey="deleteSimpleProduct0"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -52,7 +51,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <actionGroup ref="AddRelatedProductBySkuActionGroup" stepKey="addRelatedProduct1"> @@ -63,7 +62,7 @@ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.removeRelatedProduct($$simpleProduct0.sku$$)}}" stepKey="removeRelatedProduct"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAfterEdit"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--See related product in admin--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml index f8914656cc32b..b7d2edf2ec080 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminFilterProductListByBundleProductTest.xml @@ -25,7 +25,7 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> @@ -57,7 +57,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Apply Bundle Product Filter--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml index 2c1fcb6d7de42..ef84ebd6fafea 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminMassDeleteBundleProductsTest.xml @@ -29,7 +29,7 @@ <after> <!--Clear Filters--> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> @@ -67,7 +67,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Creating Second bundle product--> @@ -107,7 +107,7 @@ <fillField userInput="{{BundleProduct.urlKey2}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension2"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown2"/> <!--Mass delete bundle products--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml index 17235c531de8f..d05a4707d6fa0 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminProductBundleCreationTest.xml @@ -66,7 +66,7 @@ </actionGroup> <!--save the product/published by default--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!-- go to page--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml index ab1d4bb5ce68a..e6c6c9322fdb2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/AdminRemoveDefaultImageBundleProductTest.xml @@ -29,14 +29,13 @@ <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteBundleProduct"> <argument name="sku" value="{{BundleProduct.sku}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml index b8eef5c1b406f..888c857ac61f7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductFixedPricingTest.xml @@ -73,7 +73,7 @@ <fillField userInput="{{BundleProduct.fixedPrice}}" selector="{{AdminProductFormBundleSection.priceField}}" stepKey="fillPrice"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Testing that price appears correctly in admin catalog--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml new file mode 100644 index 0000000000000..c56e09562d49a --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithDynamicTierPriceInCartTest.xml @@ -0,0 +1,41 @@ +<?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="BundleProductWithDynamicTierPriceInCartTest" extends="BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest"> + <annotations> + <stories value="Add bundle product to cart on storefront"/> + <title value="Customer should get the right subtotal in cart when the bundle product with dynamic tier price added to the cart"/> + <description value="Customer should be able to add bundle product with dynamic tier price to the cart and get the right price"/> + <severity value="CRITICAL"/> + </annotations> + + <before> + <createData entity="VirtualProduct" stepKey="createProductForBundleItem1"> + <field key="price">50.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem2"> + <field key="price">100.00</field> + </createData> + </before> + + <remove keyForRemoval="clickDynamicPriceSwitcher"/> + <remove keyForRemoval="fillBundlePrice"/> + <remove keyForRemoval="disableDynamicSku"/> + <remove keyForRemoval="fillBundleOption1Price"/> + <remove keyForRemoval="selectPercentPrice"/> + <remove keyForRemoval="fillBundleOption2Price"/> + <assertEquals message="ExpectedPrice" stepKey="assertBundleProductPrice"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">$75.00</expectedResult> + </assertEquals> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$75.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml new file mode 100644 index 0000000000000..1b33bb08b1b03 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithOptionTierPriceInCartTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="BundleProductWithOptionTierPriceInCartTest" extends="BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest"> + <annotations> + <stories value="Add bundle product to cart on storefront"/> + <title value="Customer should get the right subtotal in cart when the bundle product with tier price for sub-item added to the cart"/> + <description value="Customer should be able to add bundle product with tier price for sub-item price to the cart and get the right price"/> + <severity value="CRITICAL"/> + </annotations> + + <before> + <createData entity="VirtualProduct" stepKey="createProductForBundleItem1"> + <field key="price">50.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem2"> + <field key="price">100.00</field> + </createData> + <createData entity="TierProductPrice50PercentDiscount" stepKey="addTierPrice"> + <requiredEntity createDataKey="createProductForBundleItem2"/> + </createData> + </before> + + <remove keyForRemoval="clickDynamicPriceSwitcher"/> + <remove keyForRemoval="fillBundlePrice"/> + <remove keyForRemoval="disableDynamicSku"/> + <remove keyForRemoval="fillBundleOption1Price"/> + <remove keyForRemoval="selectPercentPrice"/> + <remove keyForRemoval="fillBundleOption2Price"/> + <remove keyForRemoval="addProductTierPrice"/> + <actionGroup ref="SaveProductFormActionGroup" after="addBundleOption2" stepKey="saveBundleProduct"/> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.fixedFinalPrice}}" stepKey="grabProductPrice"/> + <assertEquals message="ExpectedPrice" stepKey="assertBundleProductPrice"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">$100.00</expectedResult> + </assertEquals> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$100.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml index b5812817b5640..def24c86e1730 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceInCartTest.xml @@ -34,7 +34,7 @@ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="CustomerEntityOne.email"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> <waitForPageLoad stepKey="waitForBundleProductCreatePageToLoad"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml new file mode 100644 index 0000000000000..59a6869747444 --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="BundleProductWithTierPriceWithFixedAndPercentOptionsInCartTest"> + <annotations> + <features value="Bundle"/> + <stories value="Add bundle product to cart on storefront"/> + <title value="Customer should get the right subtotal in cart when the bundle product with tier price and bundle items with fixed and percent price added to the cart"/> + <description value="Customer should be able to add bundle product with tier price and bundle items with fixed and percent price to the cart and get the right price"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-26543"/> + <group value="bundle"/> + <group value="catalog"/> + </annotations> + + <before> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem1"> + <field key="price">100.00</field> + </createData> + <createData entity="SimpleProduct2" stepKey="createProductForBundleItem2"> + <field key="price">100.00</field> + </createData> + <magentoCron groups="index" stepKey="runCronIndex"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <after> + <deleteData createDataKey="createProductForBundleItem1" stepKey="deleteProductForBundleItem1"/> + <deleteData createDataKey="createProductForBundleItem2" stepKey="deleteProductForBundleItem2"/> + <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteBundle"> + <argument name="product" value="BundleProduct"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductsGridFilters"/> + <waitForPageLoad stepKey="waitForClearProductsGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <amOnPage url="{{AdminProductCreatePage.url(BundleProduct.set, BundleProduct.type)}}" stepKey="goToBundleProductCreationPage"/> + <waitForPageLoad stepKey="waitForBundleProductCreatePageToLoad"/> + <click selector="{{AdminProductFormBundleSection.dynamicSkuToggle}}" stepKey="disableDynamicSku"/> + <click selector="{{AdminProductFormBundleSection.dynamicPrice}}" stepKey="clickDynamicPriceSwitcher"/> + <fillField selector="{{AdminProductFormBundleSection.priceField}}" userInput="100" stepKey="fillBundlePrice"/> + <actionGroup ref="FillMainBundleProductFormActionGroup" stepKey="fillMainFieldsForBundle"/> + <actionGroup ref="AddBundleOptionWithOneProductActionGroup" stepKey="addBundleOption1"> + <argument name="x" value="0"/> + <argument name="n" value="1"/> + <argument name="prodOneSku" value="$createProductForBundleItem1.sku$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option1"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYPrice('0', '0')}}" userInput="100" stepKey="fillBundleOption1Price"/> + <selectOption selector="{{AdminProductFormBundleSection.bundlePriceType}}" userInput="Percent" stepKey="selectPercentPrice"/> + <actionGroup ref="AddBundleOptionWithOneProductActionGroup" stepKey="addBundleOption2"> + <argument name="x" value="1"/> + <argument name="n" value="2"/> + <argument name="prodOneSku" value="$createProductForBundleItem2.sku$"/> + <argument name="prodTwoSku" value=""/> + <argument name="optionTitle" value="Option2"/> + <argument name="inputType" value="checkbox"/> + </actionGroup> + <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYPrice('1', '0')}}" userInput="100" stepKey="fillBundleOption2Price"/> + <scrollToTopOfPage stepKey="scrollToTopOfTheProductPage"/> + <actionGroup ref="AdminBundleProductSetAdvancedPricingActionGroup" stepKey="addProductTierPrice"> + <argument name="quantity" value="1"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + <argument name="priceView" value="As Low as"/> + </actionGroup> + <amOnPage url="{{StorefrontProductPage.url(BundleProduct.urlKey)}}" stepKey="goToStorefront"/> + <waitForPageLoad stepKey="waitForStorefront"/> + <!--Assert Bundle Product Price--> + <grabTextFrom selector="{{StorefrontProductInfoMainSection.asLowAsFinalPrice}}" stepKey="grabProductPrice"/> + <assertEquals message="ExpectedPrice" stepKey="assertBundleProductPrice"> + <actualResult type="variable">grabProductPrice</actualResult> + <expectedResult type="string">$150.00</expectedResult> + </assertEquals> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="clickOnCustomizeAndAddToCartButton"/> + <actionGroup ref="StorefrontEnterProductQuantityAndAddToTheCartActionGroup" stepKey="enterProductQuantityAndAddToTheCart"> + <argument name="quantity" value="1"/> + </actionGroup> + <actionGroup ref="AssertSubTotalOnStorefrontMiniCartActionGroup" stepKey="assertSubTotalOnStorefrontMiniCart"> + <argument name="subTotal" value="$150.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml index ada91d068efcf..b25139835de59 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/CurrencyChangingBundleProductInCartTest.xml @@ -40,7 +40,7 @@ <click selector="{{CurrencySetupSection.currencyOptions}}" stepKey="closeOptions"/> <waitForPageLoad stepKey="waitForCloseOptions"/> <click stepKey="saveUnselectedConfigs" selector="{{AdminConfigSection.saveButton}}"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml index 90619eeeadae9..edd89f64ab5d0 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/EnableDisableBundleProductStatusTest.xml @@ -65,7 +65,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Testing enabled view--> @@ -76,15 +76,13 @@ <seeElement stepKey="LookingForNameOfProduct" selector="{{StorefrontBundledSection.bundleProductName}}"/> <!--Testing disabled view--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="GoToProductCatalog"/> - <waitForPageLoad stepKey="WaitForCatalogProductPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="GoToProductCatalog"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="FindProductEditPage"> <argument name="product" value="BundleProduct"/> </actionGroup> <click selector="{{AdminDataGridTableSection.rowViewAction('1')}}" stepKey="ClickProductInGrid"/> <click stepKey="ClickOnEnableDisableToggle" selector="{{AdminProductFormBundleSection.enableDisableToggle}}"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAgain"/> - <waitForPageLoad stepKey="PauseForSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAgain"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <amOnPage url="{{BundleProduct.urlKey}}.html" stepKey="GoToProductPageAgain"/> <waitForPageLoad stepKey="WaitForProductPageToLoadToShowElement"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml index fd94ca93b1600..126d965c23423 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/MassEnableDisableBundleProductsTest.xml @@ -29,7 +29,7 @@ <after> <!--Clear Filters--> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="ClearFiltersAfter"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> @@ -64,7 +64,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Creating Second bundle product--> @@ -105,7 +105,7 @@ <fillField userInput="{{BundleProduct.urlKey2}}" selector="{{AdminProductFormBundleSection.urlKey}}" stepKey="FillsinSEOlinkExtension2"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown2"/> <!--Clear Filters--> @@ -130,16 +130,19 @@ <dontSeeElement stepKey="LookingForNameOfProductDisabled" selector="{{StorefrontBundledSection.bundleProductName}}"/> <!--Enabling bundle products--> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="GoToCatalogPageChangingView"/> - <waitForPageLoad stepKey="WaitForPageToLoadFullyChangingView"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToCatalogPageChangingView"/> <click selector="{{AdminProductFiltersSection.allCheckbox}}" stepKey="ClickOnSelectAllCheckBoxChangingView"/> <click selector="{{AdminProductFiltersSection.actions}}" stepKey="ClickOnActionsChangingView"/> <click selector="{{AdminProductFiltersSection.changeStatus}}" stepKey="ClickOnChangeStatusChangingView"/> <click selector="{{AdminProductFiltersSection.enable}}" stepKey="ClickOnEnable"/> <!--Clear Cache - reindex - resets products according to enabled/disabled view--> - <magentoCLI command="indexer:reindex" stepKey="reindex2"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Confirm bundle products have been enabled--> <amOnPage url="{{BundleProduct.urlKey2}}.html" stepKey="GoToProductPageEnabled"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml index efef033f9d974..e722caaf090c5 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewBundleProductSelectionTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <amOnPage url="{{AdminCatalogProductPage.url}}" stepKey="GoToCatalogProductPage"/> <waitForPageLoad stepKey="WaitForPageToLoad"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml index cc2aeb0602d36..4b9b046363caa 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/NewProductsListWidgetBundleProductTest.xml @@ -35,8 +35,7 @@ <!-- Create a product to appear in the widget, fill in basic info first --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <click selector="{{AdminProductGridActionSection.addBundleProduct}}" stepKey="clickAddBundleProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> @@ -64,7 +63,7 @@ <click selector="{{AdminAddProductsToOptionPanel.addSelectedProducts}}" stepKey="clickAddSelectedBundleProducts"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '0')}}" userInput="1" stepKey="fillProductDefaultQty1"/> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXProductYQuantity('0', '1')}}" userInput="1" stepKey="fillProductDefaultQty2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml index 8e0197697e691..5f9697f666e7b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleOptionsToCartTest.xml @@ -53,8 +53,7 @@ </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml index b3c542af7bbc9..93fac3171e9fb 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAddBundleProductWithZeroPriceToShoppingCartTest.xml @@ -40,7 +40,9 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="apiSimple"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShipping"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml index 8e8df1f4f16f0..6436f8be98d7b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontAdminEditDataTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> @@ -84,15 +83,13 @@ <grabTextFrom selector="{{CheckoutCartProductSection.nthBundleOptionName('1')}}" stepKey="grabTotalBefore"/> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> <waitForPageLoad stepKey="waitForProductFilterLoad"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Change the product option title --> <fillField selector="{{AdminProductFormBundleSection.bundleOptionXTitle('0')}}" userInput="BundleOption2" stepKey="fillOptionTitle2"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml index e6f8834336683..194a3972ed934 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleAddToCartSuccessTest.xml @@ -24,14 +24,13 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct2"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml index 966082739aa68..7883cc4faf00b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCartTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.xml new file mode 100644 index 0000000000000..91cc58ee0119b --- /dev/null +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleCheckBoxOptionValidationTest.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="StorefrontBundleCheckBoxOptionValidationTest"> + <annotations> + <features value="Bundle"/> + <stories value="Bundle product validation before add to cart"/> + <title value="Customer should be able to see only one validation message for checkbox option group"/> + <description value="Customer should be able to see only one validation message for checkbox option group"/> + <testCaseId value="MC-35133"/> + <severity value="MINOR"/> + <group value="Bundle"/> + </annotations> + <before> + <createData entity="ApiProductWithDescription" stepKey="simpleProduct1" before="bundleProduct"/> + <createData entity="ApiProductWithDescription" stepKey="simpleProduct2" after="simpleProduct1"/> + <createData entity="ApiBundleProduct" stepKey="bundleProduct"/> + <createData entity="CheckboxOption" stepKey="checkboxBundleOption"> + <requiredEntity createDataKey="bundleProduct"/> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink1"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simpleProduct1"/> + <field key="qty">2</field> + </createData> + <createData entity="ApiBundleLink" stepKey="createBundleLink2"> + <requiredEntity createDataKey="bundleProduct"/> + <requiredEntity createDataKey="checkboxBundleOption"/> + <requiredEntity createDataKey="simpleProduct2"/> + <field key="qty">4</field> + </createData> + <magentoCron stepKey="runCronIndex" groups="index"/> + </before> + <after> + <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> + <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductStorefront"> + <argument name="productUrl" value="$$bundleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontSelectCustomizeAndAddToTheCartButtonActionGroup" stepKey="customizeBundleProduct"/> + <actionGroup ref="StorefrontAddToTheCartButtonActionGroup" stepKey="addToCartBundleProduct"/> + <actionGroup ref="AssertStorefrontBundleValidationMessagesCountActionGroup" stepKey="assertBundleValidationCount"/> + <actionGroup ref="AssertStorefrontBundleValidationMessageActionGroup" stepKey="assertBundleValidationMessage"> + <argument name="message" value="Please select one of the options."/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml index 786040d16b7e2..43b11549d0824 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductDetailsTest.xml @@ -73,7 +73,7 @@ </actionGroup> <!--save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAgain"/> <see userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--Checking details--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml index 871bf71d1f876..2a962f6ef71b4 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontBundleProductShownInCategoryListAndGridTest.xml @@ -29,7 +29,7 @@ </before> <after> <!--Logging out--> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="simpleProduct3" stepKey="deleteSimpleProduct3"/> @@ -73,7 +73,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <magentoCron stepKey="runCronReindex" groups="index"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml index de6718dfd9f31..5997cdc14ade8 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSearchBundleProductsByKeywordsTest.xml @@ -39,7 +39,9 @@ <requiredEntity createDataKey="fixedBundleOption"/> <requiredEntity createDataKey="createSimpleProductTwo"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createDynamicBundle" stepKey="deleteDynamicBundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml index 77c561f311280..97d466964fbd7 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontCustomerSelectAndSetBundleOptionsTest.xml @@ -30,14 +30,13 @@ <actionGroup stepKey="deleteBundle" ref="DeleteProductUsingProductGridActionGroup"> <argument name="product" value="BundleProduct"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Start creating a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml index 161d308044b4a..8b72dc7ed05cc 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontEditBundleProductTest.xml @@ -25,14 +25,13 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> </after> <!-- Create a bundle product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageBundle"/> - <waitForPageLoad stepKey="waitForProductPageLoadBundle"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageBundle"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateBundleProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml index add819e2d3f14..b3cbdb93fb830 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontGoToDetailsPageWhenAddingToCartTest.xml @@ -26,7 +26,7 @@ <magentoCron stepKey="runCronIndex" groups="index"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -63,7 +63,7 @@ <actionGroup ref="AncillaryPrepBundleProductActionGroup" stepKey="createBundledProductForTwoSimpleProducts"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="messageYouSavedTheProductIsShown"/> <!--Go to category page--> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml index 1c7cb39d7746f..7049299987dff 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSortBundleProductsByPriceTest.xml @@ -95,7 +95,9 @@ </createData> <!-- Perform CLI reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete all created data --> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml index 32662321a611e..58fae75a6fc6b 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontSpecialPriceBundleProductInCartTest.xml @@ -36,7 +36,9 @@ <requiredEntity createDataKey="simpleProduct"/> </createData> <!-- Run reindex stock status --> - <magentoCLI command="indexer:reindex" arguments="cataloginventory_stock" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> </before> <after> <deleteData createDataKey="bundleProduct" stepKey="deleteBundleProduct"/> diff --git a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml index ac2ab4806fd44..24c481c9ddcb2 100644 --- a/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml +++ b/app/code/Magento/Bundle/Test/Mftf/Test/StorefrontVerifyDynamicBundleProductPricesForCombinationOfOptionsTest.xml @@ -163,11 +163,13 @@ <!-- Save the settings --> <scrollToTopOfPage stepKey="scrollToTop"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveTaxOptions"/> - <waitForPageLoad stepKey="waitForTaxSaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveTaxOptions"/> + <see userInput="You saved the configuration." selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccess"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- navigate to the tax configuration page --> @@ -190,8 +192,7 @@ <!-- Save the settings --> <scrollToTopOfPage stepKey="scrollToTop"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveTaxOptions"/> - <waitForPageLoad stepKey="waitForTaxSaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveTaxOptions"/> <see userInput="You saved the configuration." selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccess"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php index bf3860fd322eb..79c9c28df708a 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/CheckboxTest.php @@ -8,8 +8,10 @@ namespace Magento\Bundle\Test\Unit\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type; use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Checkbox; +use Magento\Framework\DataObject; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class CheckboxTest extends TestCase { @@ -20,9 +22,20 @@ class CheckboxTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) ->getObject( - Checkbox::class + Checkbox::class, + ['htmlRenderer' => $secureRendererMock] ); } diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php index 5173c034e79e2..6c37827d100f0 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/MultiTest.php @@ -10,6 +10,8 @@ use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Multi; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\DataObject; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class MultiTest extends TestCase { @@ -20,8 +22,18 @@ class MultiTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) - ->getObject(Multi::class); + ->getObject(Multi::class, ['htmlRenderer' => $secureRendererMock]); } public function testSetValidationContainer() diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php index cf6fadd3affa2..1cbea7fc85a2f 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/RadioTest.php @@ -10,6 +10,8 @@ use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Radio; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\DataObject; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class RadioTest extends TestCase { @@ -20,8 +22,18 @@ class RadioTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) - ->getObject(Radio::class); + ->getObject(Radio::class, ['htmlRenderer' => $secureRendererMock]); } public function testSetValidationContainer() diff --git a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php index 7fde7647e72d7..b289081d2a59a 100644 --- a/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Options/Type/SelectTest.php @@ -10,6 +10,8 @@ use Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Options\Type\Select; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\DataObject; class SelectTest extends TestCase { @@ -20,8 +22,18 @@ class SelectTest extends TestCase protected function setUp(): void { + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $this->block = (new ObjectManager($this)) - ->getObject(Select::class); + ->getObject(Select::class, ['htmlRenderer' => $secureRendererMock]); } public function testSetValidationContainer() diff --git a/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php b/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php index 2d86f130767c8..0092c894ac44a 100644 --- a/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Controller/Adminhtml/Product/Initialization/Helper/Plugin/BundleTest.php @@ -150,13 +150,13 @@ public function testAfterInitializeIfBundleAnsCustomOptionsAndBundleSelectionsEx $this->productMock->expects($this->once()) ->method('getBundleOptionsData') ->willReturn(['option_1' => ['delete' => 1]]); - $extentionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) + $extensionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) ->disableOriginalConstructor() ->setMethods(['setBundleProductOptions']) ->getMockForAbstractClass(); - $extentionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); - $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extentionAttribute); - $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extentionAttribute); + $extensionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); + $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extensionAttribute); + $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extensionAttribute); $this->model->afterInitialize($this->subjectMock, $this->productMock); } @@ -191,14 +191,14 @@ public function testAfterInitializeIfBundleOptionsNotExist(): void ['affect_bundle_product_selections', null, false], ]; $this->requestMock->expects($this->any())->method('getPost')->willReturnMap($valueMap); - $extentionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) + $extensionAttribute = $this->getMockBuilder(ProductExtensionInterface::class) ->disableOriginalConstructor() ->setMethods(['setBundleProductOptions']) ->getMockForAbstractClass(); - $extentionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); + $extensionAttribute->expects($this->once())->method('setBundleProductOptions')->with([]); $this->productMock->expects($this->any())->method('getCompositeReadonly')->willReturn(false); - $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extentionAttribute); - $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extentionAttribute); + $this->productMock->expects($this->once())->method('getExtensionAttributes')->willReturn($extensionAttribute); + $this->productMock->expects($this->once())->method('setExtensionAttributes')->with($extensionAttribute); $this->productMock->expects($this->once())->method('setCanSaveBundleSelections')->with(false); $this->model->afterInitialize($this->subjectMock, $this->productMock); diff --git a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php index 771b5c53b3347..b7041051591d8 100644 --- a/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php +++ b/app/code/Magento/Bundle/Test/Unit/Model/Product/TypeTest.php @@ -11,6 +11,7 @@ use Magento\Bundle\Model\Product\Type; use Magento\Bundle\Model\ResourceModel\BundleFactory; use Magento\Bundle\Model\ResourceModel\Option\Collection; +use Magento\CatalogRule\Model\ResourceModel\Product\CollectionProcessor; use Magento\Bundle\Model\ResourceModel\Selection\Collection as SelectionCollection; use Magento\Bundle\Model\ResourceModel\Selection\CollectionFactory; use Magento\Bundle\Model\Selection; @@ -42,6 +43,8 @@ use PHPUnit\Framework\TestCase; /** + * Test for bundle product type + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class TypeTest extends TestCase @@ -116,6 +119,11 @@ class TypeTest extends TestCase */ private $arrayUtility; + /** + * @var CollectionProcessor|MockObject + */ + private $catalogRuleProcessor; + /** * @return void */ @@ -172,20 +180,20 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->serializer = $this->getMockBuilder(Json::class) ->setMethods(null) ->disableOriginalConstructor() ->getMock(); - $this->metadataPool = $this->getMockBuilder(MetadataPool::class) ->disableOriginalConstructor() ->getMock(); - $this->arrayUtility = $this->getMockBuilder(ArrayUtils::class) ->setMethods(['flatten']) ->disableOriginalConstructor() ->getMock(); + $this->catalogRuleProcessor = $this->getMockBuilder(CollectionProcessor::class) + ->disableOriginalConstructor() + ->getMock(); $objectHelper = new ObjectManager($this); $this->model = $objectHelper->getObject( @@ -1542,7 +1550,7 @@ public function testPrepareForCartAdvancedSpecifyProductOptions() $this->parentClass($group, $option, $buyRequest, $product); - $product->expects($this->once()) + $product->expects($this->any()) ->method('getSkipCheckRequiredOption') ->willReturn(true); $buyRequest->expects($this->once()) @@ -2424,9 +2432,6 @@ protected function parentClass($group, $option, $buyRequest, $product) $group->expects($this->once()) ->method('setProcessMode') ->willReturnSelf(); - $group->expects($this->once()) - ->method('validateUserValue') - ->willReturnSelf(); $group->expects($this->once()) ->method('prepareForCart') ->willReturn('someString'); diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml index f028c7013df90..81a6034b9218d 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/catalog/product/edit/tab/attributes/extend.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Attributes\Extend */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $elementHtml = $block->getParentElementHtml(); $attributeCode = $block->getAttribute() @@ -18,18 +19,19 @@ $isElementReadonly = $block->getElement() ->getReadonly(); ?> -<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)) : ?> +<?php if (!($attributeCode === 'price' && $block->getCanReadPrice() === false)): ?> <div class="<?= $block->escapeHtmlAttr($attributeCode) ?> "><?= /* @noEscape */ $elementHtml ?></div> <?php endif; ?> <?= $block->getExtendedElement($switchAttributeCode)->toHtml() ?> -<?php if (!$isElementReadonly && $block->getDisableChild()) { ?> - <script> +<?php if (!$isElementReadonly && $block->getDisableChild()) { + $switchAttributeCode = /* @noEscape */ $switchAttributeCode; + $scriptString = <<<script require(['prototype'], function () { - function <?= /* @noEscape */ $switchAttributeCode ?>_change() { - var $attribute = $('<?= $block->escapeJs($attributeCode) ?>'); - if ($('<?= /* @noEscape */ $switchAttributeCode ?>').value == '<?= $block->escapeJs($block::DYNAMIC) ?>') { + function {$switchAttributeCode}_change() { + var $attribute = $('{$block->escapeJs($attributeCode)}'); + if ($('{$switchAttributeCode}').value == '{$block->escapeJs($block::DYNAMIC)}') { if ($attribute) { $attribute.disabled = true; $attribute.value = ''; @@ -40,28 +42,36 @@ $isElementReadonly = $block->getElement() } } else { if ($attribute) { - <?php if ($attributeCode === 'price' && !$block->getCanEditPrice() && $block->getCanReadPrice() - && $block->getProduct()->isObjectNew()) : ?> - <?php $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; ?> - $attribute.value = <?= /* @noEscape */ (string)$defaultProductPrice ?>; - <?php else : ?> - $attribute.disabled = false; - $attribute.addClassName('required-entry'); - <?php endif; ?> - } - if ($('dynamic-price-warning')) { - $('dynamic-price-warning').hide(); - } +script; + if ($attributeCode === 'price' && !$block->getCanEditPrice() && + $block->getCanReadPrice() && $block->getProduct()->isObjectNew()): + $defaultProductPrice = $block->getDefaultProductPrice() ?: "''"; + $scriptString .= '$attribute.value = ' . /* @noEscape */ (string)$defaultProductPrice . ';'; + else: + $scriptString = <<<script + $attribute.disabled = false; + $attribute.addClassName('required-entry'); +script; + endif; + $scriptString .= <<<script + } + if ($('dynamic-price-warning')) { + $('dynamic-price-warning').hide(); } } - - <?php if (!($attributeCode === 'price' && !$block->getCanEditPrice() - && !$block->getProduct()->isObjectNew())) : ?> - $('<?= /* @noEscape */ $switchAttributeCode ?>').observe('change', <?= /* @noEscape */ $switchAttributeCode ?>_change); - <?php endif; ?> + } +script; + if (!($attributeCode === 'price' && !$block->getCanEditPrice() && !$block->getProduct()->isObjectNew())): + $scriptString .= <<<script + $('{$switchAttributeCode}').observe('change', {$switchAttributeCode}_change); +script; + endif; + $scriptString .= <<<script Event.observe(window, 'load', function(){ - <?= /* @noEscape */ $switchAttributeCode ?>_change(); + {$switchAttributeCode}_change(); }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php } ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml index 53ad0a963244d..91517cf8284dd 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/composite/fieldset/options/bundle.phtml @@ -5,22 +5,26 @@ */ ?> -<?php /* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Bundle */ ?> +<?php +/* @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Composite\Fieldset\Bundle */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + <?php $options = $block->decorateArray($block->getOptions(true)); ?> -<?php if (count($options)) : ?> +<?php if (count($options)): ?> <fieldset id="catalog_product_composite_configure_fields_bundle" class="fieldset admin__fieldset composite-bundle<?= $block->getIsLastFieldset() ? ' last-fieldset' : '' ?>"> <legend class="legend admin__legend"> <span><?= $block->escapeHtml(__('Bundle Items')) ?></span> </legend><br /> - <?php foreach ($options as $option) : ?> - <?php if ($option->getSelections()) : ?> + <?php foreach ($options as $option): ?> + <?php if ($option->getSelections()): ?> <?= $block->getOptionHtml($option) ?> <?php endif; ?> <?php endforeach; ?> </fieldset> -<script> + <?php $scriptString = <<<script require([ "Magento_Catalog/catalog/product/composite/configure" ], function(){ @@ -70,8 +74,12 @@ require([ } } }; - ProductConfigure.bundleControl = new BundleControl(<?= /* @noEscape */ $block->getJsonConfig() ?>); +script; + $scriptString .= 'ProductConfigure.bundleControl = new BundleControl(' . /* @noEscape */ $block->getJsonConfig() . + ');'; + $scriptString .= <<<script }); -</script> - +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml index c8ab6cc5b98d2..89c0f930e21c2 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle.phtml @@ -5,27 +5,29 @@ */ /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script if(typeof Bundle=='undefined') { Bundle = {}; } - -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="bundle_product_container" class="entry-edit form-inline"> <fieldset class="fieldset"> <div class="field field-ship-bundle-items"> <label for="shipment_type" class="label"><?= $block->escapeHtml(__('Ship Bundle Items')) ?></label> <div class="control"> - <select <?php if ($block->isReadonly()) : ?>disabled="disabled" <?php endif;?> + <select <?php if ($block->isReadonly()): ?>disabled="disabled" <?php endif;?> id="shipment_type" name="<?= $block->escapeHtmlAttr($block->getFieldSuffix()) ?>[shipment_type]" class="select"> <option value="1"><?= $block->escapeHtml(__('Separately')) ?></option> <option value="0" - <?php if ($block->getProduct()->getShipmentType() == 0) : ?> + <?php if ($block->getProduct()->getShipmentType() == 0): ?> selected="selected" <?php endif; ?> > @@ -47,18 +49,24 @@ if(typeof Bundle=='undefined') { <input type="hidden" name="affect_bundle_product_selections" value="1" /> -<script> +<?php $scriptString = <<<script + require(["prototype", "mage/adminhtml/form"], function(){ // re-bind form elements onchange varienWindowOnload(true); - - <?php if ($block->isReadonly()) :?> +script; +if ($block->isReadonly()): + $scriptString .= <<<script $('product_bundle_container').select('input', 'select', 'textarea', 'button').each(function(input){ input.disabled = true; if (input.tagName.toLowerCase() == 'button') { input.addClassName('disabled'); } }); - <?php endif; ?> +script; +endif; +$scriptString .= <<<script }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml index 4d68d363b7484..d6637401cff9f 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option.phtml @@ -5,12 +5,15 @@ */ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <script id="bundle-option-template" type="text/x-magento-template"> <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>" class="option-box"> - <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-wrapper"> + <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" + id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-wrapper"> <div class="fieldset-wrapper-title"> - <strong class="admin__collapsible-title" data-toggle="collapse" data-target="#<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> + <strong class="admin__collapsible-title" data-toggle="collapse" + data-target="#<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> <span><%- data.default_title %></span> </strong> <div class="actions"> @@ -18,55 +21,67 @@ </div> <div data-role="draggable-handle" class="draggable-handle"></div> </div> - <div class="fieldset-wrapper-content in collapse" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> + <div class="fieldset-wrapper-content in collapse" + id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>-content"> <fieldset class="fieldset"> <fieldset class="fieldset-alt"> <div class="field field-option-title required"> - <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title"> + <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title"> <?= $block->escapeHtml(__('Option Title')) ?> </label> <div class="control"> - <?php if ($block->isDefaultStore()) : ?> + <?php if ($block->isDefaultStore()): ?> <input class="input-text required-entry" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" - id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title" value="<%- data.title %>" data-original-value="<%- data.title %>" /> - <?php else : ?> + <?php else: ?> <input class="input-text required-entry" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][default_title]" - id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_default_title" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][default_title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_default_title" value="<%- data.default_title %>" data-original-value="<%- data.default_title %>" /> <?php endif; ?> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id_<%- data.index %>" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][option_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][option_id]" value="<%- data.option_id %>" /> <input type="hidden" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][delete]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][delete]" value="" data-state="deleted" /> </div> </div> - <?php if (!$block->isDefaultStore()) : ?> + <?php if (!$block->isDefaultStore()): ?> <div class="field field-option-store-view required"> - <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store"> + <label class="label" for="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title_store"> <?= $block->escapeHtml(__('Store View Title')) ?> </label> <div class="control"> <input class="input-text required-entry" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][title]" - id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) ?>_<%- data.index %>_title_store" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][title]" + id="id_<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>_<%- data.index %>_title_store" value="<%- data.title %>" /> </div> </div> <?php endif; ?> <div class="field field-option-input-type required"> - <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId() . '_<%- data.index %>_type') ?>"> + <label class="label" for="<?= $block->escapeHtmlAttr($block->getFieldId() . + '_<%- data.index %>_type') ?>"> <?= $block->escapeHtml(__('Input Type')) ?> </label> <div class="control"> @@ -82,9 +97,13 @@ <label for="field-option-req"> <?= $block->escapeHtml(__('Required')) ?> </label> - <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> + <span><?= $block->getRequireSelectHtml() ?></span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div.field.field-option-req span' + ) ?> <div class="field field-option-position no-display"> <label class="label" for="field-option-position"> <?= $block->escapeHtml(__('Position')) ?> @@ -92,7 +111,8 @@ <div class="control"> <input class="input-text validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.index %>][position]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.index %>][position]" value="<%- data.position %>" id="field-option-position" /> </div> @@ -106,13 +126,18 @@ </fieldset> </div> </div> - <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_search_<%- data.index %>" class="selection-search"></div> + <div id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_search_<%- data.index %>" + class="selection-search"> + </div> </div> </script> <?= $block->getSelectionHtml() ?> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $helper */ +$helper = $block->getData('jsonHelper'); +$scriptString = <<<script require([ 'jquery', 'mage/template', @@ -140,7 +165,7 @@ function changeInputType(oldObject, oType) { Bundle.Option = Class.create(); Bundle.Option.prototype = { - idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', + idLabel : '{$block->escapeJs($block->getFieldId())}', templateText : '', itemsCount : 0, initialize : function(template) { @@ -149,7 +174,9 @@ Bundle.Option.prototype = { add : function(data) { if (!data) { - data = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode(['default_title' => __('New Option')]) ?>; +script; +$scriptString .= 'data = ' . $helper->jsonEncode(['default_title' => __('New Option')]) . ';'; +$scriptString .= <<<script } else { data.title = data.title.replace(/</g, "<"); data.title = data.title.replace(/"/g, """); @@ -168,14 +195,14 @@ Bundle.Option.prototype = { //set selected type if (data.type) { - $A($(this.idLabel + '_'+data.index+'_type').options).each(function(option){ + \$A($(this.idLabel + '_'+data.index+'_type').options).each(function(option){ if (option.value==data.type) option.selected = true; }); } //set selected is_require if (data.required) { - $A($(this.idLabel + '_'+data.index+'_required').options).each(function(option){ + \$A($(this.idLabel + '_'+data.index+'_required').options).each(function(option){ if (option.value==data.required) option.selected = true; }); } @@ -215,7 +242,7 @@ Bundle.Option.prototype = { parts = element.id.split('_'); i = parts[2]; if (element.value == 'multi' || element.value == 'checkbox') { - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); inputs.each( function(elem){ //elem.type = "checkbox"; @@ -225,7 +252,7 @@ Bundle.Option.prototype = { /** * Hide not needed elements (user defined qty select box) */ - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); inputs.each( function(elem){ elem.hide(); @@ -233,7 +260,7 @@ Bundle.Option.prototype = { ); } else { - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' tr.selection input.default')); have = false; for (j=0; j< inputs.length; j++) { //inputs[j].type = "radio"; @@ -248,7 +275,7 @@ Bundle.Option.prototype = { /** * Show user defined select box */ - inputs = $A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); + inputs = \$A($$('#' + bSelection.idLabel + '_box_' + i + ' .qty-box')); inputs.each( function(elem){ elem.show(); @@ -258,7 +285,7 @@ Bundle.Option.prototype = { }, priceTypeFixed : function() { - inputs = $A($$('.price-type-box')); + inputs = \$A($$('.price-type-box')); inputs.each( function(elem){ elem.show(); @@ -267,7 +294,7 @@ Bundle.Option.prototype = { }, priceTypeDynamic : function() { - inputs = $A($$('.price-type-box')); + inputs = \$A($$('.price-type-box')); inputs.each( function(elem){ elem.hide(); @@ -278,19 +305,21 @@ Bundle.Option.prototype = { var optionIndex = 0; bOption = new Bundle.Option(optionTemplate); -<?php +script; + foreach ($block->getOptions() as $_option) { /** @var $_option \Magento\Bundle\Model\Option */ - /* @noEscape */ echo 'optionIndex = bOption.add(', $_option->toJson(), ');', PHP_EOL; + $scriptString .= /* @noEscape */ 'optionIndex = bOption.add('. $_option->toJson() . ');' . PHP_EOL; if ($_option->getSelections()) { foreach ($_option->getSelections() as $_selection) { /** @var $_selection \Magento\Catalog\Model\Product */ $_selection->setName($block->escapeHtml($_selection->getName())); - /* @noEscape */ echo 'bSelection.addRow(optionIndex,', $_selection->toJson(), ');', PHP_EOL; + $scriptString .= /* @noEscape */ 'bSelection.addRow(optionIndex,' . $_selection->toJson() . ');' . PHP_EOL; } } } -?> + +$scriptString .= <<<script function togglePriceType() { bOption['priceType' + ($('price_type').value == '1' ? 'Fixed' : 'Dynamic')](); } @@ -304,7 +333,10 @@ jQuery(window).on('load', function() { }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <script type="text/x-magento-init"> { "*": { diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml index 0f1167f3d3eaa..4ada5496ab5a7 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/edit/bundle/option/selection.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Bundle\Block\Adminhtml\Catalog\Product\Edit\Tab\Bundle\Option\Selection */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <script id="bundle-option-selection-box-template" type="text/x-magento-template"> <table class="admin__control-table"> @@ -14,15 +15,16 @@ <th class="col-default"><?= $block->escapeHtml(__('Default')) ?></th> <th class="col-name"><?= $block->escapeHtml(__('Name')) ?></th> <th class="col-sku"><?= $block->escapeHtml(__('SKU')) ?></th> - <?php if ($block->getCanReadPrice() !== false) : ?> + <?php if ($block->getCanReadPrice() !== false): ?> <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price')) ?></th> <th class="col-price price-type-box"><?= $block->escapeHtml(__('Price Type')) ?></th> <?php endif; ?> <th class="col-qty"><?= $block->escapeHtml(__('Default Quantity')) ?></th> <th class="col-uqty qty-box"><?= $block->escapeHtml(__('User Defined')) ?></th> - <th class="col-order type-order" style="display:none"><?= $block->escapeHtml(__('Position')) ?></th> + <th class="col-order type-order"><?= $block->escapeHtml(__('Position')) ?></th> <th class="col-actions"></th> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'th.col-order.type-order') ?> </thead> <tbody> </tbody> @@ -33,16 +35,20 @@ <span data-role="draggable-handle" class="draggable-handle"></span> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_id<%- data.index %>" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_id]" value="<%- data.selection_id %>"/> <input type="hidden" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][option_id]" value="<%- data.option_id %>"/> <input type="hidden" class="product" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][product_id]" value="<%- data.product_id %>"/> - <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" + <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][delete]" value="" class="delete"/> </td> @@ -50,19 +56,21 @@ <input onclick="bSelection.checkGroup(event)" type="<%- data.option_type %>" class="default" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][is_default]" value="1" <%- data.checked %> /> </td> <td class="col-name"><%- data.name %></td> <td class="col-sku"><%- data.sku %></td> -<?php if ($block->getCanReadPrice() !== false) : ?> +<?php if ($block->getCanReadPrice() !== false): ?> <td class="col-price price-type-box"> <input id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" class="input-text required-entry validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="<%- data.selection_price_value %>" - <?php if ($block->getCanEditPrice() === false) : ?> + <?php if ($block->getCanEditPrice() === false): ?> disabled="disabled" <?php endif; ?>/> </td> @@ -70,42 +78,51 @@ <?= $block->getPriceTypeSelectHtml() ?> <div><?= $block->getCheckboxScopeHtml() ?></div> </td> -<?php else : ?> +<?php else: ?> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_value" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_value]" value="0" /> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_type" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> - <?php if ($block->isUsedWebsitePrice()) : ?> + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_price_type]" value="0" /> + <?php if ($block->isUsedWebsitePrice()): ?> <input type="hidden" id="<?= $block->escapeHtmlAttr($block->getFieldId()) ?>_<%- data.index %>_price_scope" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][default_price_scope]" value="1" /> <?php endif; ?> <?php endif; ?> <td class="col-qty"> <input class="input-text required-entry validate-greater-zero-based-on-option validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][selection_qty]" value="<%- data.selection_qty %>" /> </td> <td class="col-uqty qty-box"> <input type="checkbox" class="is-user-defined-qty" checked="checked" /> - <span style="display:none"><?= $block->getQtyTypeSelectHtml() ?></span> + <span><?= $block->getQtyTypeSelectHtml() ?></span> </td> - <td class="col-order type-order" style="display:none"> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'td.col-uqty.qty-box span') ?> + <td class="col-order type-order""> <input class="input-text required-entry validate-zero-or-greater" type="text" - name="<?= $block->escapeHtmlAttr($block->getFieldName()) ?>[<%- data.parentIndex %>][<%- data.index %>][position]" + name="<?= $block->escapeHtmlAttr($block->getFieldName()) + ?>[<%- data.parentIndex %>][<%- data.index %>][position]" value="<%- data.position %>" /> </td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'td.col-order.type-order') ?> <td class="col-actions"> <span title="Delete Row"> <?= $block->getSelectionDeleteButtonHtml() ?> </span> </td> </script> -<script> + +<?php $isUsedWebsitePrice = (int)$block->isUsedWebsitePrice(); +$scriptString = <<<script require([ 'jquery', 'mage/template', @@ -116,8 +133,8 @@ var bundleTemplateBox = jQuery('#bundle-option-selection-box-template').html(), Bundle.Selection = Class.create(); Bundle.Selection.prototype = { - idLabel : '<?= $block->escapeJs($block->getFieldId()) ?>', - scopePrice : <?= (int)$block->isUsedWebsitePrice() ?>, + idLabel : '{$block->escapeJs($block->getFieldId())}', + scopePrice : {$isUsedWebsitePrice}, templateBox : '', templateRow : '', itemsCount : 0, @@ -125,12 +142,14 @@ Bundle.Selection.prototype = { gridSelection: new Hash(), gridRemoval: new Hash(), gridSelectedProductSkus: [], - selectionSearchUrl: '<?= $block->escapeUrl($block->getSelectionSearchUrl()) ?>', + selectionSearchUrl: '{$block->escapeJs($block->getSelectionSearchUrl())}', initialize : function() { - this.templateBox = '<div class="tier form-list" id="' + this.idLabel + '_box_<%- data.parentIndex %>">' + bundleTemplateBox + '</div>'; + this.templateBox = '<div class="tier form-list" + id="' + this.idLabel + '_box_<%- data.parentIndex %>">' + bundleTemplateBox + '</div>'; - this.templateRow = '<tr class="selection" id="' + this.idLabel + '_row_<%- data.index %>">' + bundleTemplateRow + '</tr>'; + this.templateRow = '<tr class="selection" + id="' + this.idLabel + '_row_<%- data.index %>">' + bundleTemplateRow + '</tr>'; }, gridUpdateCallback: function () { @@ -197,7 +216,7 @@ Bundle.Selection.prototype = { var escapedHTML = this.template({ data: data }).replace(/<(\/?)script/g, '<$1script'); - + Element.insert(tbody[0], {bottom: escapedHTML}); if (data.selection_price_type) { @@ -366,4 +385,6 @@ Bundle.Selection.prototype = { bSelection = new Bundle.Selection(); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml index b540b59ada343..14cb5f29be69a 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/product/stock/disabler.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-tab-panel=product-details]').on('stockbeforedisable', function(e) { if (e.productType === 'bundle') { @@ -13,4 +15,7 @@ require(['jquery'], function($){ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml index c480d9b126da6..f32fa87c0ac11 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/create/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -45,160 +50,165 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-ordered-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> <tr> <th><?= $block->escapeHtml(__('Invoiced')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped() && + $block->isShipmentSeparately($_item)): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> <tr> <th><?= $block->escapeHtml(__('Refunded')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> <tr> <th><?= $block->escapeHtml(__('Canceled')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)) : ?> + <?php elseif ($block->isShipmentSeparately($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> - <?php if ($block->canParentReturnToStock($_item)) : ?> + <?php if ($block->canParentReturnToStock($_item)): ?> <td class="col-return-to-stock"> - <?php if ($block->canShowPriceInfo($_item)) : ?> - <?php if ($block->canReturnItemToStock($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canReturnItemToStock($_item)): ?> <input type="checkbox" class="admin__control-checkbox" - name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][back_to_stock]" - value="1"<?php if ($_item->getBackToStock()) :?> checked="checked"<?php endif;?> /> + name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) + ?>][back_to_stock]" + value="1"<?php if ($_item->getBackToStock()):?> checked="checked"<?php endif;?> /> <label class="admin__field-label"></label> <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <?php endif; ?> <td class="col-refund col-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> - <?php if ($block->canEditQty()) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> + <?php if ($block->canEditQty()): ?> <input type="text" class="input-text admin__control-text qty-input" name="creditmemo[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>][qty]" value="<?= (float)$_item->getQty() * 1 ?>" /> - <?php else : ?> + <?php else: ?> <?= (float)$_item->getQty() * 1 ?> <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax-amount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discont"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml index 0d54e1528dfe9..12a27be743875 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/creditmemo/view/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -43,83 +48,86 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= (float)$_item->getQty() * 1 ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option) : ?> + <?php foreach ($block->getOrderOptions() as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml index a7d49b4b3530a..23e7ef27fa78d 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/create/items/renderer.phtml @@ -3,44 +3,49 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $shipTogether = ($_item->getOrderItem()->getProductType() == \Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) ? !$_item->getOrderItem()->isShipSeparately() : !$_item->getOrderItem()->getParentItem()->isShipSeparately() ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php if ($shipTogether) { continue; } ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -53,148 +58,151 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><span><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></span></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyInvoiced()): ?> <tr> <th><?= $block->escapeHtml(__('Invoiced')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped() && + $block->isShipmentSeparately($_item)): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyRefunded()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyRefunded()): ?> <tr> <th><?= $block->escapeHtml(__('Refunded')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getOrderItem()->getQtyCanceled()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyCanceled()): ?> <tr> <th><?= $block->escapeHtml(__('Canceled')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)) : ?> + <?php elseif ($block->isShipmentSeparately($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getOrderItem()->getQtyShipped()) : ?> + <?php if ((float) $_item->getOrderItem()->getQtyShipped()): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getOrderItem()->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty-invoice"> - <?php if ($block->canShowPriceInfo($_item) || $shipTogether) : ?> - <?php if ($block->canEditQty()) : ?> + <?php if ($block->canShowPriceInfo($_item) || $shipTogether): ?> + <?php if ($block->canEditQty()): ?> <input type="text" class="input-text admin__control-text qty-input" name="invoice[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" value="<?= (float)$_item->getQty() * 1 ?>" /> - <?php else : ?> + <?php else: ?> <?= (float)$_item->getQty() * 1 ?> <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); - + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml index e29bb5dbc9479..004c484bb9ba5 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/invoice/view/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -43,84 +48,87 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> - <?php else : ?> + <?php else: ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= (float)$_item->getQty() * 1 ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option) : ?> + <?php foreach ($block->getOrderOptions() as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml index 233e57a003397..9620f648ae3b8 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/order/view/items/renderer.phtml @@ -3,35 +3,40 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php + /** * @see \Magento\Bundle\Block\Adminhtml\Sales\Order\View\Items\Renderer */ /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\View\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = array_merge([$_item], $_item->getChildrenItems()); ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription() || $block->canDisplayGiftmessage()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_item->getParentItem()) : ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_item->getParentItem()): ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td> </td> <td> </td> <td> </td> @@ -46,156 +51,159 @@ <?php endif; ?> <?php endif; ?> <tr<?= (++$_index==$_count && !$_showlastRow)?' class="border"':'' ?>> - <?php if (!$_item->getParentItem()) : ?> + <?php if (!$_item->getParentItem()): ?> <td class="col-product"> <div class="product-title" id="order_item_<?= $block->escapeHtmlAttr($_item->getId()) ?>_title"> <?= $block->escapeHtml($_item->getName()) ?> </div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"> <div class="option-value"><?= $block->getValueHtml($_item) ?></div> </td> <?php endif; ?> <td class="col-status"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->escapeHtml($_item->getStatus()) ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-price-original"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('original_price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-price"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'price') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-ordered-qty"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getQtyInvoiced()) : ?> + <?php if ((float) $_item->getQtyInvoiced()): ?> <tr> <th><?= $block->escapeHtml(__('Invoiced')) ?></th> <td><?= (float)$_item->getQtyInvoiced() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)) : ?> + <?php if ((float) $_item->getQtyShipped() && $block->isShipmentSeparately($_item)): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyRefunded()) : ?> + <?php if ((float) $_item->getQtyRefunded()): ?> <tr> <th><?= $block->escapeHtml(__('Refunded')) ?></th> <td><?= (float)$_item->getQtyRefunded() * 1 ?></td> </tr> <?php endif; ?> - <?php if ((float) $_item->getQtyCanceled()) : ?> + <?php if ((float) $_item->getQtyCanceled()): ?> <tr> <th><?= $block->escapeHtml(__('Canceled')) ?></th> <td><?= (float)$_item->getQtyCanceled() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php elseif ($block->isShipmentSeparately($_item)) : ?> + <?php elseif ($block->isShipmentSeparately($_item)): ?> <table class="qty-table"> <tr> <th><?= $block->escapeHtml(__('Ordered')) ?></th> <td><?= (float)$_item->getQtyOrdered() * 1 ?></td> </tr> - <?php if ((float) $_item->getQtyShipped()) : ?> + <?php if ((float) $_item->getQtyShipped()): ?> <tr> <th><?= $block->escapeHtml(__('Shipped')) ?></th> <td><?= (float)$_item->getQtyShipped() * 1 ?></td> </tr> <?php endif; ?> </table> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-subtotal"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'subtotal') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax-amount"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('tax_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-tax-percent"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayTaxPercent($_item) ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-discont"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= /* @noEscape */ $block->displayPriceAttribute('discount_amount') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-total last"> - <?php if ($block->canShowPriceInfo($_item)) : ?> + <?php if ($block->canShowPriceInfo($_item)): ?> <?= $block->getColumnHtml($_item, 'total') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr<?php if (!$block->canDisplayGiftmessage()) { echo ' class="border"'; } ?>> <td class="col-product"> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $option) : ?> + <?php foreach ($block->getOrderOptions() as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?>:</dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml index 2e52ed906626b..9c3cdcd97bf52 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/create/items/renderer.phtml @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> +/** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_item = $block->getItem() ?> <?php $items = $block->getChildren($_item); ?> <?php $_count = count($items) ?> @@ -15,19 +19,21 @@ <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getOrderItem()->getParentItem()) : ?> + <?php if ($_item->getOrderItem()->getParentItem()): ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td class="col-product"> </td> <td class="col-qty last"> </td> </tr> @@ -35,64 +41,68 @@ <?php endif; ?> <?php endif; ?> <tr class="<?= (++$_index == $_count && !$_showlastRow) ? 'border' : '' ?>"> - <?php if (!$_item->getOrderItem()->getParentItem()) : ?> + <?php if (!$_item->getOrderItem()->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-ordered-qty"> - <?php if ($block->isShipmentSeparately($_item)) : ?> + <?php if ($block->isShipmentSeparately($_item)): ?> <?= $block->getColumnHtml($_item, 'qty') ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> <td class="col-qty last"> - <?php if ($block->isShipmentSeparately($_item)) : ?> + <?php if ($block->isShipmentSeparately($_item)): ?> <input type="text" class="input-text admin__control-text" name="shipment[items][<?= $block->escapeHtmlAttr($_item->getOrderItemId()) ?>]" value="<?= (float)$_item->getQty() * 1 ?>" /> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> - <?php else : ?> + <?php else: ?>   <?php endif; ?> <?= $block->escapeHtml($_item->getDescription()) ?> diff --git a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml index 521669700e10a..73efa4bddcc1c 100644 --- a/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml +++ b/app/code/Magento/Bundle/view/adminhtml/templates/sales/shipment/view/items/renderer.phtml @@ -3,85 +3,96 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /** @var $block \Magento\Bundle\Block\Adminhtml\Sales\Order\Items\Renderer */ ?> <?php $_item = $block->getItem() ?> <?php $items = array_merge([$_item->getOrderItem()], $_item->getOrderItem()->getChildrenItems()) ?> <?php $shipItems = $block->getChildren($_item) ?> <?php $_count = count($items) ?> <?php $_index = 0 ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +?> <?php $_prevOptionId = '' ?> -<?php if ($block->getOrderOptions() || $_item->getDescription()) : ?> +<?php if ($block->getOrderOptions() || $_item->getDescription()): ?> <?php $_showlastRow = true ?> -<?php else : ?> +<?php else: ?> <?php $_showlastRow = false ?> <?php endif; ?> -<?php foreach ($items as $_item) : ?> +<?php foreach ($items as $_item): ?> <?php $block->setPriceDataObject($_item) ?> - <?php if ($_item->getParentItem()) : ?> + <?php if ($_item->getParentItem()): ?> <?php $attributes = $block->getSelectionAttributes($_item) ?> - <?php if ($_prevOptionId != $attributes['option_id']) : ?> + <?php if ($_prevOptionId != $attributes['option_id']): ?> <tr> - <td class="col-product"><div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div></td> + <td class="col-product"> + <div class="option-label"><?= $block->escapeHtml($attributes['option_label']) ?></div> + </td> <td class="col-qty last"> </td> </tr> <?php $_prevOptionId = $attributes['option_id'] ?> <?php endif; ?> <?php endif; ?> <tr<?= (++$_index == $_count && !$_showlastRow) ? ' class="border"' : '' ?>> - <?php if (!$_item->getParentItem()) : ?> + <?php if (!$_item->getParentItem()): ?> <td class="col-product"> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($_item->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($_item->getSku())) ?> </div> </td> - <?php else : ?> + <?php else: ?> <td class="col-product"><div class="option-value"><?= $block->getValueHtml($_item) ?></div></td> <?php endif; ?> <td class="col-qty last"> - <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || (!$block->isShipmentSeparately() && !$_item->getParentItem())) : ?> - <?php if (isset($shipItems[$_item->getId()])) : ?> + <?php if (($block->isShipmentSeparately() && $_item->getParentItem()) || + (!$block->isShipmentSeparately() && !$_item->getParentItem())): ?> + <?php if (isset($shipItems[$_item->getId()])): ?> <?= (float)$shipItems[$_item->getId()]->getQty() * 1 ?> - <?php elseif ($_item->getIsVirtual()) : ?> + <?php elseif ($_item->getIsVirtual()): ?> <?= $block->escapeHtml(__('N/A')) ?> - <?php else : ?> + <?php else: ?> 0 <?php endif; ?> - <?php else : ?> + <?php else: ?>   <?php endif; ?> </td> </tr> <?php endforeach; ?> -<?php if ($_showlastRow) : ?> +<?php if ($_showlastRow): ?> <tr class="border"> <td class="col-product"> - <?php if ($block->getOrderOptions($_item->getOrderItem())) : ?> + <?php if ($block->getOrderOptions($_item->getOrderItem())): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option) : ?> + <?php foreach ($block->getOrderOptions($_item->getOrderItem()) as $option): ?> <dt><?= $block->escapeHtml($option['label']) ?></dt> <dd> - <?php if (isset($option['custom_view']) && $option['custom_view']) : ?> + <?php if (isset($option['custom_view']) && $option['custom_view']): ?> <?= $block->escapeHtml($option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) + ?>"><?= $block->escapeHtml($_remainder) ?></span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){\$('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){\$('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> diff --git a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml index 5b56598dc58e2..4ba6fd6183653 100644 --- a/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml +++ b/app/code/Magento/Bundle/view/frontend/templates/catalog/product/view/type/bundle/option/checkbox.phtml @@ -8,40 +8,55 @@ <?php /* @var $block \Magento\Bundle\Block\Catalog\Product\View\Type\Bundle\Option\Checkbox */ ?> <?php $_option = $block->getOption() ?> <?php $_selections = $_option->getSelections() ?> +<?php $inputClass = 'checkbox product bundle option bundle-option-' . $block->escapeHtmlAttr($_option->getId()) ?> +<?php $inputId = 'bundle-option-' . $block->escapeHtmlAttr($_option->getId()) ?> +<?php $inputName = 'bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']' ?> +<?php $dataValidation = 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . + $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"' ?> + <div class="field option <?= ($_option->getRequired()) ? ' required': '' ?>"> <label class="label"> <span><?= $block->escapeHtml($_option->getTitle()) ?></span> </label> <div class="control"> <div class="nested options-list"> - <?php if ($block->showSingle()) : ?> + <?php if ($block->showSingle()): ?> <?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selections[0]) ?> <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selections[0]) ?> <input type="hidden" class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> product bundle option" name="bundle_option[<?= $block->escapeHtml($_option->getId()) ?>]" value="<?= $block->escapeHtmlAttr($_selections[0]->getSelectionId()) ?>"/> - <?php else :?> - <?php foreach ($_selections as $_selection) : ?> + <?php else: ?> + <?php foreach ($_selections as $selection): ?> + <?php $sectionId = $selection->getSelectionId() ?> <div class="field choice"> - <input class="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?> checkbox product bundle option change-container-classname" - id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>" + <input class="<?=/* @noEscape */ $inputClass ?> change-container-classname" + id="<?=/* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId)?>" type="checkbox" - <?php if ($_option->getRequired()) { echo 'data-validate="{\'validate-one-required-by-name\':\'input[name^="bundle_option[' . $block->escapeHtmlAttr($_option->getId()) . ']"]:checked\'}"'; } ?> - name="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" - data-selector="bundle_option[<?= $block->escapeHtmlAttr($_option->getId()) ?>][<?= $block->escapeHtmlAttr($_selection->getId()) ?>]" - <?php if ($block->isSelected($_selection)) { echo ' checked="checked"'; } ?> - <?php if (!$_selection->isSaleable()) { echo ' disabled="disabled"'; } ?> - value="<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"/> + <?php if ($_option->getRequired()): ?> + <?= /* @noEscape */ $dataValidation ?> + <?php endif;?> + name="<?=/* @noEscape */ $inputName .'['. $block->escapeHtmlAttr($sectionId)?>]" + data-selector="<?= /* @noEscape */ $inputName.'['.$block->escapeHtmlAttr($sectionId)?>]" + <?php if ($block->isSelected($selection)): ?> + <?= ' checked="checked"' ?> + <?php endif; ?> + <?php if (!$selection->isSaleable()): ?> + <?= ' disabled="disabled"' ?> + <?php endif; ?> + value="<?= $block->escapeHtmlAttr($sectionId) ?>" + data-errors-message-box="#validation-message-box"/> <label class="label" - for="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-<?= $block->escapeHtmlAttr($_selection->getSelectionId()) ?>"> - <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($_selection) ?></span> + for="<?= /* @noEscape */ $inputId . '-' . $block->escapeHtmlAttr($sectionId) ?>"> + <span><?= /* @noEscape */ $block->getSelectionQtyTitlePrice($selection) ?></span> <br/> - <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($_selection) ?> + <?= /* @noEscape */ $block->getTierPriceRenderer()->renderTierPrice($selection) ?> </label> </div> <?php endforeach; ?> <div id="bundle-option-<?= $block->escapeHtmlAttr($_option->getId()) ?>-container"></div> + <div id="validation-message-box"></div> <?php endif; ?> </div> </div> diff --git a/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php b/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php new file mode 100644 index 0000000000000..8e678cdb12d24 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Order/Shipment/BundleShipmentItemFormatter.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Order\Shipment; + +use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\SalesGraphQl\Model\Shipment\Item\ShipmentItemFormatter; +use Magento\SalesGraphQl\Model\Shipment\Item\FormatterInterface; + +/** + * Format Bundle shipment items for GraphQl output + */ +class BundleShipmentItemFormatter implements FormatterInterface +{ + /** + * @var ShipmentItemFormatter + */ + private $itemFormatter; + + /** + * @param ShipmentItemFormatter $itemFormatter + */ + public function __construct(ShipmentItemFormatter $itemFormatter) + { + $this->itemFormatter = $itemFormatter; + } + + /** + * Format bundle product shipment item + * + * @param ShipmentInterface $shipment + * @param ShipmentItemInterface $item + * @return array|null + */ + public function formatShipmentItem(ShipmentInterface $shipment, ShipmentItemInterface $item): ?array + { + $orderItem = $item->getOrderItem(); + $shippingType = $orderItem->getProductOptions()['shipment_type'] ?? null; + if ($shippingType == AbstractType::SHIPMENT_SEPARATELY && !$orderItem->getParentItemId()) { + //When bundle items are shipped separately the children are treated as their own items + return null; + } + return $this->itemFormatter->formatShipmentItem($shipment, $item); + } +} diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php new file mode 100644 index 0000000000000..ce5c12ce69675 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Options/BundleItemOptionUid.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver\Options; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for entered bundle options + */ +class BundleItemOptionUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'bundle'; + + /** + * Create a option uid for entered option in "<option-type>/<option-id>/<option-value-id>/<quantity>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + if (!isset($value['selection_id']) || empty($value['selection_id'])) { + throw new GraphQlInputException(__('"selection_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'], + $value['selection_id'], + (int) $value['selection_qty'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php new file mode 100644 index 0000000000000..a21bbbb84d735 --- /dev/null +++ b/app/code/Magento/BundleGraphQl/Model/Resolver/Order/Item/BundleOptions.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\BundleGraphQl\Model\Resolver\Order\Item; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Sales\Api\Data\InvoiceItemInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\Sales\Api\Data\CreditmemoItemInterface; + +/** + * Resolve bundle options items for order item + */ +class BundleOptions implements ResolverInterface +{ + /** + * Serializer + * + * @var Json + */ + private $serializer; + + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @param ValueFactory $valueFactory + * @param Json $serializer + */ + public function __construct( + ValueFactory $valueFactory, + Json $serializer + ) { + $this->valueFactory = $valueFactory; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + return $this->valueFactory->create(function () use ($value) { + if (!isset($value['model'])) { + throw new LocalizedException(__('"model" value should be specified')); + } + if ($value['model'] instanceof OrderItemInterface) { + $item = $value['model']; + return $this->getBundleOptions($item, $value); + } + if ($value['model'] instanceof InvoiceItemInterface + || $value['model'] instanceof ShipmentItemInterface + || $value['model'] instanceof CreditmemoItemInterface) { + $item = $value['model']; + // Have to pass down order and item to map to avoid refetching all data + return $this->getBundleOptions($item->getOrderItem(), $value); + } + return null; + }); + } + + /** + * Format bundle options and values from a parent bundle order item + * + * @param OrderItemInterface $item + * @param array $formattedItem + * @return array + */ + private function getBundleOptions( + OrderItemInterface $item, + array $formattedItem + ): array { + $bundleOptions = []; + if ($item->getProductType() === 'bundle') { + $options = $item->getProductOptions(); + //loop through options + foreach ($options['bundle_options'] ?? [] as $bundleOptionId => $bundleOption) { + $bundleOptions[$bundleOptionId]['label'] = $bundleOption['label'] ?? ''; + $bundleOptions[$bundleOptionId]['id'] = isset($bundleOption['option_id']) ? + base64_encode($bundleOption['option_id']) : null; + if (isset($bundleOption['option_id'])) { + $bundleOptions[$bundleOptionId]['values'] = $this->formatBundleOptionItems( + $item, + $formattedItem, + $bundleOption['option_id'] + ); + } else { + $bundleOptions[$bundleOptionId]['values'] = []; + } + } + } + return $bundleOptions; + } + + /** + * Format Bundle items + * + * @param OrderItemInterface $item + * @param array $formattedItem + * @param string $bundleOptionId + * @return array + */ + private function formatBundleOptionItems( + OrderItemInterface $item, + array $formattedItem, + string $bundleOptionId + ) { + $optionItems = []; + // Find the item assign to the option + /** @var OrderItemInterface $childrenOrderItem */ + foreach ($item->getChildrenItems() ?? [] as $childrenOrderItem) { + $childOrderItemOptions = $childrenOrderItem->getProductOptions(); + $bundleChildAttributes = $this->serializer + ->unserialize($childOrderItemOptions['bundle_selection_attributes'] ?? ''); + // Value Id is missing from parent, so we have to match the child to parent option + if (isset($bundleChildAttributes['option_id']) + && $bundleChildAttributes['option_id'] == $bundleOptionId) { + $optionItems[$childrenOrderItem->getItemId()] = [ + 'id' => base64_encode($childrenOrderItem->getItemId()), + 'product_name' => $childrenOrderItem->getName(), + 'product_sku' => $childrenOrderItem->getSku(), + 'quantity' => $bundleChildAttributes['qty'], + 'price' => [ + //use options price, not child price + 'value' => $bundleChildAttributes['price'], + //use currency from order + 'currency' => $formattedItem['product_sale_price']['currency'] ?? null, + ] + ]; + } + } + + return $optionItems; + } +} diff --git a/app/code/Magento/BundleGraphQl/composer.json b/app/code/Magento/BundleGraphQl/composer.json index cb49ab78588b3..e3c54719f4d0e 100644 --- a/app/code/Magento/BundleGraphQl/composer.json +++ b/app/code/Magento/BundleGraphQl/composer.json @@ -10,6 +10,8 @@ "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", "magento/module-store": "*", + "magento/module-sales": "*", + "magento/module-sales-graph-ql": "*", "magento/framework": "*" }, "license": [ diff --git a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml index b847a6672e046..863e152fbe177 100644 --- a/app/code/Magento/BundleGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/BundleGraphQl/etc/graphql/di.xml @@ -65,4 +65,39 @@ </argument> </arguments> </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\OrderItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleOrderItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\InvoiceItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleInvoiceItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\ShipmentItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleShipmentItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\CreditMemoItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="bundle" xsi:type="string">BundleCreditMemoItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\Shipment\ItemProvider"> + <arguments> + <argument name="formatters" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\BundleGraphQl\Model\Order\Shipment\BundleShipmentItemFormatter\Proxy</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/BundleGraphQl/etc/schema.graphqls b/app/code/Magento/BundleGraphQl/etc/schema.graphqls index 0eff0e086180e..a66fa397020a7 100644 --- a/app/code/Magento/BundleGraphQl/etc/schema.graphqls +++ b/app/code/Magento/BundleGraphQl/etc/schema.graphqls @@ -66,6 +66,7 @@ type BundleItemOption @doc(description: "BundleItemOption defines characteristic price_type: PriceTypeEnum @doc(description: "One of FIXED, PERCENT, or DYNAMIC.") can_change_quantity: Boolean @doc(description: "Indicates whether the customer can change the number of items for this option.") product: ProductInterface @doc(description: "Contains details about this product option.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Options\\BundleItemOptionUid") # A Base64 string that encodes option details. } type BundleProduct implements ProductInterface, PhysicalProductInterface, CustomizableProductInterface @doc(description: "BundleProduct defines basic features of a bundle product and contains multiple BundleItems.") { @@ -86,3 +87,33 @@ enum ShipBundleItemsEnum @doc(description: "This enumeration defines whether bun TOGETHER SEPARATELY } + +type BundleOrderItem implements OrderItemInterface { + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type BundleInvoiceItem implements InvoiceItemInterface{ + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type BundleShipmentItem implements ShipmentItemInterface { + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type BundleCreditMemoItem implements CreditMemoItemInterface { + bundle_options: [ItemSelectedBundleOption] @doc(description: "A list of bundle options that are assigned to the bundle product") @resolver(class: "Magento\\BundleGraphQl\\Model\\Resolver\\Order\\Item\\BundleOptions") +} + +type ItemSelectedBundleOption @doc(description: "A list of options of the selected bundle product") { + id: ID! @doc(description: "The unique identifier of the option") + label: String! @doc(description: "The label of the option") + values: [ItemSelectedBundleOptionValue] @doc(description: "A list of products that represent the values of the parent option") +} + +type ItemSelectedBundleOptionValue @doc(description: "A list of values for the selected bundle product") { + id: ID! @doc(description: "The unique identifier of the value") + product_name: String! @doc(description: "The name of the child bundle product") + product_sku: String! @doc(description: "The SKU of the child bundle product") + quantity: Float! @doc(description: "Indicates how many of this bundle product were ordered") + price: Money! @doc(description: "The price of the child bundle product") +} diff --git a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php index e6522054d9f94..49881f67f5c9a 100644 --- a/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php +++ b/app/code/Magento/BundleImportExport/Model/Import/Product/Type/Bundle.php @@ -339,7 +339,7 @@ protected function populateSelectionTemplate($selection, $optionId, $parentId, $ /** * Deprecated method for retrieving mapping between skus and products. * - * @deprecated Misspelled method + * @deprecated 100.3.0 Misspelled method * @see retrieveProductsByCachedSkus */ protected function retrieveProducsByCachedSkus() diff --git a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml index 9bd9c784b39f2..515c2bc56f067 100644 --- a/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml +++ b/app/code/Magento/BundleImportExport/Test/Mftf/Test/UpdateBundleProductViaImportTest.xml @@ -38,8 +38,12 @@ <argument name="importFile" value="catalog_product_import_bundle.csv"/> <argument name="importNoticeMessage" value="Created: 2, Updated: 0, Deleted: 0"/> </actionGroup> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCacheAfterCreate"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindexAfterCreate"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterCreate"> + <argument name="tags" value="full_page"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterCreate"> + <argument name="indices" value=""/> + </actionGroup> <!-- Check Bundle product is visible on the storefront--> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPageAfterCreation"> @@ -56,8 +60,12 @@ <argument name="importFile" value="catalog_product_import_bundle.csv"/> <argument name="importNoticeMessage" value="Created: 0, Updated: 2, Deleted: 0"/> </actionGroup> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCacheAfterUpdate"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindexAfterUpdate"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterUpdate"> + <argument name="tags" value="full_page"/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindexAfterUpdate"> + <argument name="indices" value=""/> + </actionGroup> <!-- Check Bundle product is still visible on the storefront--> <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPageAfterUpdate"> diff --git a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml index 9103c4191544c..030c9f5efcf50 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Section/CaptchaFormsDisplayingSection.xml @@ -14,7 +14,7 @@ <element name="customer" type="button" selector="//div[@class='admin__page-nav-title title _collapsible']//strong[text()='Customers']"/> <element name="customerConfig" type="text" selector="//span[text()='Customer Configuration']"/> <element name="captcha" type="button" selector="#customer_captcha-head"/> - <element name="dependent" type="button" selector="//a[@id='customer_captcha-head' and @class='open']"/> + <element name="dependent" type="button" selector="a#customer_captcha-head.open"/> <element name="forms" type="multiselect" selector="#customer_captcha_forms"/> <element name="createUser" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_create']"/> <element name="forgotpassword" type="multiselect" selector="//select[@id='customer_captcha_forms']/option[@value='user_forgotpassword']"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml index 28253fb3c00ef..58cfd7aacd631 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/AdminLoginWithCaptchaTest.xml @@ -23,12 +23,16 @@ <before> <magentoCLI command="config:set {{AdminCaptchaLength3ConfigData.path}} {{AdminCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{AdminCaptchaSymbols1ConfigData.path}} {{AdminCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{AdminCaptchaDefaultLengthConfigData.path}} {{AdminCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{AdminCaptchaDefaultSymbolsConfigData.path}} {{AdminCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdminWithWrongCredentialsFirstAttempt"> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml index bfea4e99996c3..9e99fa96ee766 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/CaptchaFormsDisplayingTest/CaptchaWithDisabledGuestCheckoutTest.xml @@ -34,9 +34,7 @@ </after> <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.sku$$)}}" stepKey="openProductPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <waitForText userInput="You added $$createSimpleProduct.name$$ to your shopping cart." stepKey="waitForText"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickCart"/> <click selector="{{StorefrontMinicartSection.goToCheckout}}" stepKey="goToCheckout"/> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml index 54237087227d8..2736888154483 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaEditCustomerEmailTest.xml @@ -24,7 +24,9 @@ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerChangePasswordConfigData.path}} {{StorefrontCaptchaOnCustomerChangePasswordConfigData.value}}" stepKey="enableUserEditCaptcha"/> <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <createData entity="Simple_US_Customer" stepKey="customer"/> <!-- Sign in as customer --> @@ -37,7 +39,9 @@ <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml index 0c6a3f31c1df2..22f1ed1af3e28 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnContactUsTest.xml @@ -23,13 +23,17 @@ <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> <magentoCLI command="config:set {{StorefrontCaptchaOnContactUsFormConfigData.path}} {{StorefrontCaptchaOnContactUsFormConfigData.value}}" stepKey="enableUserEditCaptcha"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> <magentoCLI command="config:set {{StorefrontCaptchaOnCustomerLoginConfigData.path}} {{StorefrontCaptchaOnCustomerLoginConfigData.value}},{{StorefrontCaptchaOnCustomerForgotPasswordConfigData.value}}" stepKey="enableCaptchaOnDefaultForms" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Open storefront contact us form --> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml index 5a1be68d3f251..332d7eb6067b5 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaOnCustomerLoginTest.xml @@ -22,13 +22,17 @@ <before> <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <createData entity="Simple_US_Customer" stepKey="customer"/> </before> <after> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> diff --git a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml index 2c331f958e467..b7d5b60ddc632 100644 --- a/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml +++ b/app/code/Magento/Captcha/Test/Mftf/Test/StorefrontCaptchaRegisterNewCustomerTest.xml @@ -25,7 +25,9 @@ <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAlwaysConfigData.path}} {{StorefrontCustomerCaptchaModeAlwaysConfigData.value}}" stepKey="alwaysEnableCaptcha" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaLength3ConfigData.path}} {{StorefrontCustomerCaptchaLength3ConfigData.value}}" stepKey="setCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaSymbols1ConfigData.path}} {{StorefrontCustomerCaptchaSymbols1ConfigData.value}}" stepKey="setCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <!-- Set default configuration. --> @@ -33,7 +35,9 @@ <magentoCLI command="config:set {{StorefrontCustomerCaptchaModeAfterFailConfigData.path}} {{StorefrontCustomerCaptchaModeAfterFailConfigData.value}}" stepKey="defaultCaptchaMode" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultLengthConfigData.path}} {{StorefrontCustomerCaptchaDefaultLengthConfigData.value}}" stepKey="setDefaultCaptchaLength" /> <magentoCLI command="config:set {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.path}} {{StorefrontCustomerCaptchaDefaultSymbolsConfigData.value}}" stepKey="setDefaultCaptchaSymbols" /> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <!-- Open Customer registration page --> diff --git a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php index f243718952f4f..3c5aed076a653 100644 --- a/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php +++ b/app/code/Magento/Captcha/Test/Unit/Model/Customer/Plugin/AjaxLoginTest.php @@ -63,7 +63,7 @@ class AjaxLoginTest extends TestCase /** * @var array */ - protected $formIds; + protected $formIds = ['user_login']; /** * @var AjaxLogin @@ -97,7 +97,6 @@ protected function setUp(): void ->method('getCaptcha') ->willReturn($this->captchaMock); - $this->formIds = ['user_login']; $this->serializerMock = $this->createMock(Json::class); $this->model = new AjaxLogin( @@ -194,7 +193,10 @@ public function testAroundExecuteCaptchaIsNotRequired($username, $requestContent $this->captchaMock->expects($this->once())->method('isRequired')->with($username) ->willReturn(false); - $this->captchaMock->expects($this->never())->method('logAttempt')->with($username); + $expectLogAttempt = $requestContent['captcha_form_id'] ?? false; + $this->captchaMock + ->expects($expectLogAttempt ? $this->once() : $this->never()) + ->method('logAttempt')->with($username); $this->captchaMock->expects($this->never())->method('isCorrect'); $closure = function () { diff --git a/app/code/Magento/Captcha/etc/adminhtml/system.xml b/app/code/Magento/Captcha/etc/adminhtml/system.xml index ac4197c976ea0..bc3874989435c 100644 --- a/app/code/Magento/Captcha/etc/adminhtml/system.xml +++ b/app/code/Magento/Captcha/etc/adminhtml/system.xml @@ -57,7 +57,7 @@ <depends> <field id="enable">1</field> </depends> - <frontend_class>required-entry</frontend_class> + <frontend_class>required-entry validate-range range-1-8</frontend_class> </field> <field id="symbols" translate="label comment" type="text" sortOrder="8" showInDefault="1" canRestore="1"> <label>Symbols Used in CAPTCHA</label> diff --git a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml index 88e0d5edc2a7d..e73174d3768df 100644 --- a/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml +++ b/app/code/Magento/Captcha/view/adminhtml/templates/default.phtml @@ -5,8 +5,9 @@ */ /** @var \Magento\Captcha\Block\Captcha\DefaultCaptcha $block */ - /** @var \Magento\Captcha\Model\DefaultModel $captcha */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $captcha = $block->getCaptchaModel(); ?> <div class="admin__field _required"> @@ -18,11 +19,13 @@ $captcha = $block->getCaptchaModel(); id="captcha" class="admin__control-text" type="text" - name="<?= $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE) ?>[<?= $block->escapeHtml($block->getFormId()) ?>]" + name="<?= $block->escapeHtmlAttr(\Magento\Captcha\Helper\Data::INPUT_NAME_FIELD_VALUE) + ?>[<?= $block->escapeHtml($block->getFormId()) ?>]" data-validate="{required:true}"/> - <?php if ($captcha->isCaseSensitive()) :?> + <?php if ($captcha->isCaseSensitive()):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('<strong>Attention</strong>: Captcha is case sensitive.'), ['strong']) ?></span> + <span><?= $block->escapeHtml(__('<strong>Attention</strong>: Captcha is case sensitive.'), ['strong']) + ?></span> </div> <?php endif; ?> </div> @@ -39,11 +42,16 @@ $captcha = $block->getCaptchaModel(); height="<?= /* @noEscape */ (float) $block->getImgHeight() ?>" src="<?= $block->escapeUrl($captcha->getImgSrc()) ?>" /> </div> -<script> + +<?php +$url = $block->escapeJs($block->getRefreshUrl()); +$formId = $block->escapeJs($block->escapeHtml($block->getFormId())); +$scriptString = <<<script + require(["prototype", "mage/captcha"], function(){ //<![CDATA[ - var captcha = new Captcha('<?= $block->escapeJs($block->escapeUrl($block->getRefreshUrl())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getFormId())) ?>'); + var captcha = new Captcha('{$url}', '{$formId}'); $('captcha-reload').observe('click', function () { captcha.refresh(this); @@ -52,4 +60,6 @@ $captcha = $block->getCaptchaModel(); //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php index 953af1751fd65..076e122e8e819 100644 --- a/app/code/Magento/CardinalCommerce/Model/JwtManagement.php +++ b/app/code/Magento/CardinalCommerce/Model/JwtManagement.php @@ -8,6 +8,7 @@ namespace Magento\CardinalCommerce\Model; use Magento\Framework\Serialize\Serializer\Json; +use Magento\Framework\Encryption\Helper\Security; /** * JSON Web Token management. @@ -62,7 +63,8 @@ public function decode(string $jwt, string $key): array $payload = $this->json->unserialize($payloadJson); $signature = $this->urlSafeB64Decode($signatureB64); - if ($signature !== $this->sign($headB64 . '.' . $payloadB64, $key, $header['alg'])) { + + if (!Security::compareStrings($signature, $this->sign($headB64 . '.' . $payloadB64, $key, $header['alg']))) { throw new \InvalidArgumentException('JWT signature verification failed'); } diff --git a/app/code/Magento/CardinalCommerce/etc/csp_whitelist.xml b/app/code/Magento/CardinalCommerce/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..946f22b219909 --- /dev/null +++ b/app/code/Magento/CardinalCommerce/etc/csp_whitelist.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geoapi.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eafapi.cardinalcommerce.com</value> + <value id="cardinal_commerce_songbird" type="host">songbird.cardinalcommerce.com</value> + <value id="cardinal_commerce_test" type="host">includestest.ccdc02.com</value> + </values> + </policy> + <policy id="connect-src"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geo.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eaf.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent_stag" type="host">centinelapistag.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent" type="host">centinelapi.cardinalcommerce.com</value> + </values> + </policy> + <policy id="frame-src"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geo.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eaf.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent_stag" type="host">centinelapistag.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent" type="host">centinelapi.cardinalcommerce.com</value> + </values> + </policy> + <policy id="form-action"> + <values> + <value id="cardinal_commerce_geo_stag" type="host">geostag.cardinalcommerce.com</value> + <value id="cardinal_commerce_geo" type="host">geo.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf_stag" type="host">1eafstag.cardinalcommerce.com</value> + <value id="cardinal_commerce_1eaf" type="host">1eaf.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent_stag" type="host">centinelapistag.cardinalcommerce.com</value> + <value id="cardinal_commerce_cent" type="host">centinelapi.cardinalcommerce.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php index 05e5106b287a0..2afa14d874e4b 100644 --- a/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/BasePriceStorageInterface.php @@ -9,7 +9,7 @@ /** * Base prices storage. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface BasePriceStorageInterface { @@ -18,7 +18,7 @@ interface BasePriceStorageInterface * * @param string[] $skus * @return \Magento\Catalog\Api\Data\BasePriceInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * @param \Magento\Catalog\Api\Data\BasePriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); } diff --git a/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php b/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php index 62eba5987c35d..08265d6583090 100644 --- a/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php +++ b/app/code/Magento/Catalog/Api/CategoryListDeleteBySkuInterface.php @@ -8,7 +8,7 @@ /** * @api - * @since 100.0.2 + * @since 104.0.0 */ interface CategoryListDeleteBySkuInterface { @@ -22,6 +22,7 @@ interface CategoryListDeleteBySkuInterface * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\InputException + * @since 104.0.0 */ public function deleteBySkus(int $categoryId, array $productSkuList): bool; } diff --git a/app/code/Magento/Catalog/Api/CategoryListInterface.php b/app/code/Magento/Catalog/Api/CategoryListInterface.php index 22a9da00eaffc..2f6cd1f38730a 100644 --- a/app/code/Magento/Catalog/Api/CategoryListInterface.php +++ b/app/code/Magento/Catalog/Api/CategoryListInterface.php @@ -7,7 +7,7 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CategoryListInterface { @@ -16,7 +16,7 @@ interface CategoryListInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\Catalog\Api\Data\CategorySearchResultsInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria); } diff --git a/app/code/Magento/Catalog/Api/CostStorageInterface.php b/app/code/Magento/Catalog/Api/CostStorageInterface.php index a52d290bd46d8..debb791a7b756 100644 --- a/app/code/Magento/Catalog/Api/CostStorageInterface.php +++ b/app/code/Magento/Catalog/Api/CostStorageInterface.php @@ -9,7 +9,7 @@ /** * Product cost storage. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CostStorageInterface { @@ -19,7 +19,7 @@ interface CostStorageInterface * @param string[] $skus * @return \Magento\Catalog\Api\Data\CostInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * * @param \Magento\Catalog\Api\Data\CostInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -45,7 +45,7 @@ public function update(array $prices); * @return bool Will return True if deleted. * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotDeleteException - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $skus); } diff --git a/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php b/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php index a527bbfe947ab..1bce067650313 100644 --- a/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php +++ b/app/code/Magento/Catalog/Api/Data/BasePriceInterface.php @@ -9,7 +9,7 @@ /** * Price interface. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface BasePriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -26,7 +26,7 @@ interface BasePriceInterface extends \Magento\Framework\Api\ExtensibleDataInterf * * @param float $price * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPrice($price); @@ -34,7 +34,7 @@ public function setPrice($price); * Get price. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getPrice(); @@ -43,7 +43,7 @@ public function getPrice(); * * @param int $storeId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -51,7 +51,7 @@ public function setStoreId($storeId); * Get store id. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -60,7 +60,7 @@ public function getStoreId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -68,7 +68,7 @@ public function setSku($sku); * Get SKU. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -76,7 +76,7 @@ public function getSku(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\BasePriceExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -85,7 +85,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\BasePriceExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\BasePriceExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php b/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php index e9c0e04c4f746..c9ca57dc1eff1 100644 --- a/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategoryLinkInterface.php @@ -10,20 +10,20 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CategoryLinkInterface extends ExtensibleDataInterface { /** * @return int|null - * @since 101.1.0 + * @since 102.0.0 */ public function getPosition(); /** * @param int $position * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPosition($position); @@ -31,7 +31,7 @@ public function setPosition($position); * Get category id * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCategoryId(); @@ -40,7 +40,7 @@ public function getCategoryId(); * * @param string $categoryId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setCategoryId($categoryId); @@ -48,7 +48,7 @@ public function setCategoryId($categoryId); * Retrieve existing extension attributes object. * * @return \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -57,7 +57,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\CategoryLinkExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php b/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php index 38f3f89d6a0c5..0ed03c2d56519 100644 --- a/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CategorySearchResultsInterface.php @@ -9,7 +9,7 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CategorySearchResultsInterface extends SearchResultsInterface { @@ -17,7 +17,7 @@ interface CategorySearchResultsInterface extends SearchResultsInterface * Get categories * * @return \Magento\Catalog\Api\Data\CategoryInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function getItems(); @@ -26,7 +26,7 @@ public function getItems(); * * @param \Magento\Catalog\Api\Data\CategoryInterface[] $items * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setItems(array $items); } diff --git a/app/code/Magento/Catalog/Api/Data/CostInterface.php b/app/code/Magento/Catalog/Api/Data/CostInterface.php index a9966f56bafa3..ed5e8cd8a2bb2 100644 --- a/app/code/Magento/Catalog/Api/Data/CostInterface.php +++ b/app/code/Magento/Catalog/Api/Data/CostInterface.php @@ -9,7 +9,7 @@ /** * Cost interface. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CostInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -26,7 +26,7 @@ interface CostInterface extends \Magento\Framework\Api\ExtensibleDataInterface * * @param float $cost * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setCost($cost); @@ -34,7 +34,7 @@ public function setCost($cost); * Get cost value. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getCost(); @@ -43,7 +43,7 @@ public function getCost(); * * @param int $storeId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -51,7 +51,7 @@ public function setStoreId($storeId); * Get store id. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -60,7 +60,7 @@ public function getStoreId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -68,7 +68,7 @@ public function setSku($sku); * Get SKU. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -76,7 +76,7 @@ public function getSku(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\CostExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -85,7 +85,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\CostExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\CostExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php b/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php index dabd3234c6dab..08696156fa11a 100644 --- a/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php +++ b/app/code/Magento/Catalog/Api/Data/EavAttributeInterface.php @@ -145,7 +145,7 @@ public function getIsFilterableInGrid(); * * @param bool|null $isUsedInGrid * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setIsUsedInGrid($isUsedInGrid); @@ -154,7 +154,7 @@ public function setIsUsedInGrid($isUsedInGrid); * * @param bool|null $isVisibleInGrid * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setIsVisibleInGrid($isVisibleInGrid); @@ -163,7 +163,7 @@ public function setIsVisibleInGrid($isVisibleInGrid); * * @param bool|null $isFilterableInGrid * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setIsFilterableInGrid($isFilterableInGrid); diff --git a/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php b/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php index 426c5becc7a24..25bd9eeb438c9 100644 --- a/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php +++ b/app/code/Magento/Catalog/Api/Data/PriceUpdateResultInterface.php @@ -9,7 +9,7 @@ /** * Interface returned in case of incorrect price passed to efficient price API. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface PriceUpdateResultInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -24,7 +24,7 @@ interface PriceUpdateResultInterface extends \Magento\Framework\Api\ExtensibleDa * Get error message, that contains description of error occurred during price update. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMessage(); @@ -33,7 +33,7 @@ public function getMessage(); * * @param string $message * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setMessage($message); @@ -41,7 +41,7 @@ public function setMessage($message); * Get parameters, that could be displayed in error message placeholders. * * @return string[] - * @since 101.1.0 + * @since 102.0.0 */ public function getParameters(); @@ -50,7 +50,7 @@ public function getParameters(); * * @param string[] $parameters * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setParameters(array $parameters); @@ -59,7 +59,7 @@ public function setParameters(array $parameters); * If extension attributes do not exist return null. * * @return \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -68,7 +68,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\PriceUpdateResultExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php b/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php index 590c23a0aa0b1..15fd17f41e158 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductAttributeInterface.php @@ -35,6 +35,7 @@ interface ProductAttributeInterface extends \Magento\Catalog\Api\Data\EavAttribu /** * @return \Magento\Eav\Api\Data\AttributeExtensionInterface|null + * @since 103.0.0 */ public function getExtensionAttributes(); } diff --git a/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php b/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php index 9a6cfebc71fa0..610c457215853 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductFrontendActionInterface.php @@ -9,7 +9,7 @@ * Represents Data Object for a Product Frontend Action like Product View or Comparison * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductFrontendActionInterface { @@ -17,7 +17,7 @@ interface ProductFrontendActionInterface * Gets Identifier of a Product Frontend Action * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getActionId(); @@ -26,7 +26,7 @@ public function getActionId(); * * @param int $actionId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setActionId($actionId); @@ -34,7 +34,7 @@ public function setActionId($actionId); * Gets Identifier of Visitor who performs a Product Frontend Action * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getVisitorId(); @@ -43,7 +43,7 @@ public function getVisitorId(); * * @param int $visitorId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setVisitorId($visitorId); @@ -51,7 +51,7 @@ public function setVisitorId($visitorId); * Gets Identifier of Customer who performs a Product Frontend Action * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getCustomerId(); @@ -60,7 +60,7 @@ public function getCustomerId(); * * @param int $customerId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setCustomerId($customerId); @@ -68,7 +68,7 @@ public function setCustomerId($customerId); * Gets Identifier of Product a Product Frontend Action is performed on * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getProductId(); @@ -77,7 +77,7 @@ public function getProductId(); * * @param int $productId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setProductId($productId); @@ -85,7 +85,7 @@ public function setProductId($productId); * Gets Identifier of Type of a Product Frontend Action * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getTypeId(); @@ -94,7 +94,7 @@ public function getTypeId(); * * @param string $typeId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setTypeId($typeId); @@ -102,7 +102,7 @@ public function setTypeId($typeId); * Gets JS timestamp of a Product Frontend Action (in microseconds) * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getAddedAt(); @@ -111,7 +111,7 @@ public function getAddedAt(); * * @param int $addedAt * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setAddedAt($addedAt); } diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php index e2f4dfa201593..592b47d216ae9 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/ButtonInterface.php @@ -14,7 +14,7 @@ * This interface represents all manner of product buttons: add to cart, add to compare, etc... * The buttons describes by this interface should have interaction with backend * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ButtonInterface extends ExtensibleDataInterface { @@ -22,7 +22,7 @@ interface ButtonInterface extends ExtensibleDataInterface * @param string $postData Post data should be serialized (JSON/serialized) string * Post data can be empty * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setPostData($postData); @@ -33,7 +33,7 @@ public function setPostData($postData); * to handle product action * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPostData(); @@ -44,7 +44,7 @@ public function getPostData(); * * @param string $url * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setUrl($url); @@ -52,7 +52,7 @@ public function setUrl($url); * Retrieve url, needed to add product to cart * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getUrl(); @@ -62,7 +62,7 @@ public function getUrl(); * * @param bool $requiredOptions * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setRequiredOptions($requiredOptions); @@ -70,7 +70,7 @@ public function setRequiredOptions($requiredOptions); * Retrieve flag whether a product has options or not * * @return bool - * @since 101.1.0 + * @since 102.0.0 */ public function hasRequiredOptions(); @@ -78,7 +78,7 @@ public function hasRequiredOptions(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\ButtonExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -87,7 +87,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\ButtonExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\ButtonExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php index d111de1b04b94..6022c5198769a 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/FormattedPriceInfoInterface.php @@ -15,7 +15,7 @@ * Consider currency, rounding and html * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface FormattedPriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -23,7 +23,7 @@ interface FormattedPriceInfoInterface extends \Magento\Framework\Api\ExtensibleD * Retrieve html with final price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getFinalPrice(); @@ -34,7 +34,7 @@ public function getFinalPrice(); * * @param string $finalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setFinalPrice($finalPrice); @@ -44,7 +44,7 @@ public function setFinalPrice($finalPrice); * E.g. for product with custom options is price with the most expensive custom option * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxPrice(); @@ -53,7 +53,7 @@ public function getMaxPrice(); * * @param string $maxPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxPrice($maxPrice); @@ -63,7 +63,7 @@ public function setMaxPrice($maxPrice); * The minimal price is for example, the lowest price of all variations for complex product * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalPrice(); @@ -74,7 +74,7 @@ public function getMinimalPrice(); * * @param string $maxRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxRegularPrice($maxRegularPrice); @@ -82,7 +82,7 @@ public function setMaxRegularPrice($maxRegularPrice); * Retrieve max regular price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxRegularPrice(); @@ -91,7 +91,7 @@ public function getMaxRegularPrice(); * * @param string $minRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalRegularPrice($minRegularPrice); @@ -99,7 +99,7 @@ public function setMinimalRegularPrice($minRegularPrice); * Retrieve minimal regular price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalRegularPrice(); @@ -110,7 +110,7 @@ public function getMinimalRegularPrice(); * * @param string $specialPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setSpecialPrice($specialPrice); @@ -118,7 +118,7 @@ public function setSpecialPrice($specialPrice); * Retrieve special price * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSpecialPrice(); @@ -127,7 +127,7 @@ public function getSpecialPrice(); * * @param string $minimalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalPrice($minimalPrice); @@ -137,7 +137,7 @@ public function setMinimalPrice($minimalPrice); * Usually this price is corresponding to price in admin panel of product * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getRegularPrice(); @@ -146,7 +146,7 @@ public function getRegularPrice(); * * @param string $regularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setRegularPrice($regularPrice); @@ -154,7 +154,7 @@ public function setRegularPrice($regularPrice); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -163,7 +163,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php index 45b070d2706dc..49789e5ce9ed7 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/ImageInterface.php @@ -14,7 +14,7 @@ * Represents physical characteristics of image, that can be used in product listing or product view * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ImageInterface extends ExtensibleDataInterface { @@ -24,7 +24,7 @@ interface ImageInterface extends ExtensibleDataInterface * * @param string $url * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setUrl($url); @@ -32,7 +32,7 @@ public function setUrl($url); * Retrieve image url * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getUrl(); @@ -43,7 +43,7 @@ public function getUrl(); * What size should this image have, etc... * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCode(); @@ -52,7 +52,7 @@ public function getCode(); * * @param string $code * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setCode($code); @@ -61,7 +61,7 @@ public function setCode($code); * * @param string $height * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setHeight($height); @@ -69,7 +69,7 @@ public function setHeight($height); * Retrieve image height * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getHeight(); @@ -77,7 +77,7 @@ public function getHeight(); * Set image width in px * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getWidth(); @@ -86,7 +86,7 @@ public function getWidth(); * * @param string $width * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setWidth($width); @@ -96,7 +96,7 @@ public function setWidth($width); * Image label is short description of this image * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getLabel(); @@ -105,7 +105,7 @@ public function getLabel(); * * @param string $label * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setLabel($label); @@ -115,7 +115,7 @@ public function setLabel($label); * This width is image dimension, which represents the width, that can be used for performance improvements * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getResizedWidth(); @@ -124,7 +124,7 @@ public function getResizedWidth(); * * @param string $width * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setResizedWidth($width); @@ -133,7 +133,7 @@ public function setResizedWidth($width); * * @param string $height * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setResizedHeight($height); @@ -141,7 +141,7 @@ public function setResizedHeight($height); * Retrieve resize height * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getResizedHeight(); @@ -149,7 +149,7 @@ public function getResizedHeight(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -158,7 +158,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\ImageExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php index 9768b3c08c8ab..0e9b1c53fcd14 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRender/PriceInfoInterface.php @@ -10,7 +10,7 @@ * Price interface. * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -18,7 +18,7 @@ interface PriceInfoInterface extends \Magento\Framework\Api\ExtensibleDataInterf * Retrieve final price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getFinalPrice(); @@ -29,7 +29,7 @@ public function getFinalPrice(); * * @param float $finalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setFinalPrice($finalPrice); @@ -39,7 +39,7 @@ public function setFinalPrice($finalPrice); * E.g. for product with custom options is price with the most expensive custom option * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxPrice(); @@ -48,7 +48,7 @@ public function getMaxPrice(); * * @param float $maxPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxPrice($maxPrice); @@ -60,7 +60,7 @@ public function setMaxPrice($maxPrice); * * @param float $maxRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMaxRegularPrice($maxRegularPrice); @@ -68,7 +68,7 @@ public function setMaxRegularPrice($maxRegularPrice); * Retrieve max regular price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMaxRegularPrice(); @@ -77,7 +77,7 @@ public function getMaxRegularPrice(); * * @param float $minRegularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalRegularPrice($minRegularPrice); @@ -85,7 +85,7 @@ public function setMinimalRegularPrice($minRegularPrice); * Retrieve minimal regular price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalRegularPrice(); @@ -96,7 +96,7 @@ public function getMinimalRegularPrice(); * * @param float $specialPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setSpecialPrice($specialPrice); @@ -104,7 +104,7 @@ public function setSpecialPrice($specialPrice); * Retrieve special price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getSpecialPrice(); @@ -112,7 +112,7 @@ public function getSpecialPrice(); * Retrieve minimal price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getMinimalPrice(); @@ -121,7 +121,7 @@ public function getMinimalPrice(); * * @param float $minimalPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setMinimalPrice($minimalPrice); @@ -129,7 +129,7 @@ public function setMinimalPrice($minimalPrice); * Retrieve regular price * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getRegularPrice(); @@ -140,7 +140,7 @@ public function getRegularPrice(); * * @param float $regularPrice * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setRegularPrice($regularPrice); @@ -148,7 +148,7 @@ public function setRegularPrice($regularPrice); * Retrieve dto with formatted prices * * @return \Magento\Catalog\Api\Data\ProductRender\FormattedPriceInfoInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getFormattedPrices(); @@ -157,7 +157,7 @@ public function getFormattedPrices(); * * @param FormattedPriceInfoInterface $formattedPriceInfo * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setFormattedPrices(FormattedPriceInfoInterface $formattedPriceInfo); @@ -165,7 +165,7 @@ public function setFormattedPrices(FormattedPriceInfoInterface $formattedPriceIn * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRender\PriceInfoExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -174,7 +174,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRender\PriceInfoExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRender\PriceInfoExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php index 166a1aba76b61..e1bd1a7899b67 100644 --- a/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php +++ b/app/code/Magento/Catalog/Api/Data/ProductRenderInterface.php @@ -15,7 +15,7 @@ * This information is put into part as Add To Cart or Add to Compare Data or Price Data * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductRenderInterface extends ExtensibleDataInterface { @@ -23,7 +23,7 @@ interface ProductRenderInterface extends ExtensibleDataInterface * Provide information needed for render "Add To Cart" button on front * * @return \Magento\Catalog\Api\Data\ProductRender\ButtonInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getAddToCartButton(); @@ -32,7 +32,7 @@ public function getAddToCartButton(); * * @param ButtonInterface $cartAddToCartButton * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setAddToCartButton(ButtonInterface $cartAddToCartButton); @@ -40,7 +40,7 @@ public function setAddToCartButton(ButtonInterface $cartAddToCartButton); * Provide information needed for render "Add To Compare" button on front * * @return \Magento\Catalog\Api\Data\ProductRender\ButtonInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getAddToCompareButton(); @@ -49,7 +49,7 @@ public function getAddToCompareButton(); * * @param ButtonInterface $compareButton * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function setAddToCompareButton(ButtonInterface $compareButton); @@ -59,7 +59,7 @@ public function setAddToCompareButton(ButtonInterface $compareButton); * Prices are represented in raw format and in current currency * * @return \Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceInfo(); @@ -68,7 +68,7 @@ public function getPriceInfo(); * * @param \Magento\Catalog\Api\Data\ProductRender\PriceInfoInterface $priceInfo * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceInfo(PriceInfoInterface $priceInfo); @@ -78,7 +78,7 @@ public function setPriceInfo(PriceInfoInterface $priceInfo); * Images can be separated by image codes * * @return \Magento\Catalog\Api\Data\ProductRender\ImageInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function getImages(); @@ -87,7 +87,7 @@ public function getImages(); * * @param \Magento\Catalog\Api\Data\ProductRender\ImageInterface[] $images * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setImages(array $images); @@ -95,7 +95,7 @@ public function setImages(array $images); * Provide product url * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getUrl(); @@ -104,7 +104,7 @@ public function getUrl(); * * @param string $url * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setUrl($url); @@ -112,7 +112,7 @@ public function setUrl($url); * Provide product identifier * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getId(); @@ -121,7 +121,7 @@ public function getId(); * * @param int $id * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setId($id); @@ -129,7 +129,7 @@ public function setId($id); * Provide product name * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getName(); @@ -138,7 +138,7 @@ public function getName(); * * @param string $name * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setName($name); @@ -146,7 +146,7 @@ public function setName($name); * Provide product type. Such as bundle, grouped, simple, etc... * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getType(); @@ -155,7 +155,7 @@ public function getType(); * * @param string $productType * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setType($productType); @@ -163,7 +163,7 @@ public function setType($productType); * Provide information about product saleability (In Stock) * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getIsSalable(); @@ -175,7 +175,7 @@ public function getIsSalable(); * * @param string $isSalable * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setIsSalable($isSalable); @@ -186,7 +186,7 @@ public function setIsSalable($isSalable); * This setting affect store scope attributes * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -195,7 +195,7 @@ public function getStoreId(); * * @param int $storeId * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -205,7 +205,7 @@ public function setStoreId($storeId); * This setting affect formatted prices* * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCurrencyCode(); @@ -214,7 +214,7 @@ public function getCurrencyCode(); * * @param string $currencyCode * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function setCurrencyCode($currencyCode); @@ -222,7 +222,7 @@ public function setCurrencyCode($currencyCode); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\ProductRenderExtensionInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -231,7 +231,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\ProductRenderExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php b/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php index 62028ed788dd5..ec4feb5076238 100644 --- a/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php +++ b/app/code/Magento/Catalog/Api/Data/SpecialPriceInterface.php @@ -9,7 +9,7 @@ /** * Product Special Price Interface is used to encapsulate data that can be processed by efficient price API. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface SpecialPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -28,7 +28,7 @@ interface SpecialPriceInterface extends \Magento\Framework\Api\ExtensibleDataInt * * @param float $price * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPrice($price); @@ -36,7 +36,7 @@ public function setPrice($price); * Get product special price value. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getPrice(); @@ -45,7 +45,7 @@ public function getPrice(); * * @param int $storeId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setStoreId($storeId); @@ -53,7 +53,7 @@ public function setStoreId($storeId); * Get ID of store, that contains special price value. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getStoreId(); @@ -62,7 +62,7 @@ public function getStoreId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -70,7 +70,7 @@ public function setSku($sku); * Get SKU of product, that contains special price value. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -79,7 +79,7 @@ public function getSku(); * * @param string $datetime * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceFrom($datetime); @@ -87,7 +87,7 @@ public function setPriceFrom($datetime); * Get start date for special price in Y-m-d H:i:s format. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceFrom(); @@ -96,7 +96,7 @@ public function getPriceFrom(); * * @param string $datetime * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceTo($datetime); @@ -104,7 +104,7 @@ public function setPriceTo($datetime); * Get end date for special price in Y-m-d H:i:s format. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceTo(); @@ -113,7 +113,7 @@ public function getPriceTo(); * If extension attributes do not exist return null. * * @return \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -122,7 +122,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\SpecialPriceExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php b/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php index eaa1d22726d7c..dae43722bf42c 100644 --- a/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php +++ b/app/code/Magento/Catalog/Api/Data/TierPriceInterface.php @@ -9,7 +9,7 @@ /** * Tier price interface. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface TierPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterface { @@ -31,7 +31,7 @@ interface TierPriceInterface extends \Magento\Framework\Api\ExtensibleDataInterf * * @param float $price * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPrice($price); @@ -39,7 +39,7 @@ public function setPrice($price); * Get tier price. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getPrice(); @@ -48,7 +48,7 @@ public function getPrice(); * * @param string $type * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setPriceType($type); @@ -56,7 +56,7 @@ public function setPriceType($type); * Get tier price type. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getPriceType(); @@ -65,7 +65,7 @@ public function getPriceType(); * * @param int $websiteId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setWebsiteId($websiteId); @@ -73,7 +73,7 @@ public function setWebsiteId($websiteId); * Get website id. * * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function getWebsiteId(); @@ -82,7 +82,7 @@ public function getWebsiteId(); * * @param string $sku * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setSku($sku); @@ -90,7 +90,7 @@ public function setSku($sku); * Get SKU. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getSku(); @@ -99,7 +99,7 @@ public function getSku(); * * @param string $group * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setCustomerGroup($group); @@ -107,7 +107,7 @@ public function setCustomerGroup($group); * Get customer group. * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getCustomerGroup(); @@ -116,7 +116,7 @@ public function getCustomerGroup(); * * @param float $quantity * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setQuantity($quantity); @@ -124,7 +124,7 @@ public function setQuantity($quantity); * Get quantity. * * @return float - * @since 101.1.0 + * @since 102.0.0 */ public function getQuantity(); @@ -132,7 +132,7 @@ public function getQuantity(); * Retrieve existing extension attributes object or create a new one. * * @return \Magento\Catalog\Api\Data\TierPriceExtensionInterface|null - * @since 101.1.0 + * @since 102.0.0 */ public function getExtensionAttributes(); @@ -141,7 +141,7 @@ public function getExtensionAttributes(); * * @param \Magento\Catalog\Api\Data\TierPriceExtensionInterface $extensionAttributes * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Catalog\Api\Data\TierPriceExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Catalog/Api/ProductAttributeOptionUpdateInterface.php b/app/code/Magento/Catalog/Api/ProductAttributeOptionUpdateInterface.php new file mode 100644 index 0000000000000..c783033b6d7b7 --- /dev/null +++ b/app/code/Magento/Catalog/Api/ProductAttributeOptionUpdateInterface.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Api; + +/** + * Interface to update product attribute option + * + * @api + */ +interface ProductAttributeOptionUpdateInterface +{ + /** + * Update attribute option + * + * @param string $attributeCode + * @param int $optionId + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @return bool + * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws \Magento\Framework\Exception\InputException + */ + public function update( + string $attributeCode, + int $optionId, + \Magento\Eav\Api\Data\AttributeOptionInterface $option + ): bool; +} diff --git a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php index 954acd35a07db..ddd0f1406f68e 100644 --- a/app/code/Magento/Catalog/Api/ProductRenderListInterface.php +++ b/app/code/Magento/Catalog/Api/ProductRenderListInterface.php @@ -11,7 +11,7 @@ * Interface which provides product renders information for products. * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductRenderListInterface { @@ -26,7 +26,7 @@ interface ProductRenderListInterface * @param int $storeId * @param string $currencyCode * @return \Magento\Catalog\Api\Data\ProductRenderSearchResultsInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria, $storeId, $currencyCode); } diff --git a/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php b/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php index a6c3e975bfd79..dcb17a6f467f8 100644 --- a/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php +++ b/app/code/Magento/Catalog/Api/ProductTierPriceManagementInterface.php @@ -8,7 +8,7 @@ /** * @api - * @deprecated 101.1.0 use ScopedProductTierPriceManagementInterface instead + * @deprecated 102.0.0 use ScopedProductTierPriceManagementInterface instead * @since 100.0.2 */ interface ProductTierPriceManagementInterface diff --git a/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php b/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php index 1a3d05de5bcd1..2b005bc685b23 100644 --- a/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php +++ b/app/code/Magento/Catalog/Api/ScopedProductTierPriceManagementInterface.php @@ -8,7 +8,7 @@ /** * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ScopedProductTierPriceManagementInterface { @@ -20,7 +20,7 @@ interface ScopedProductTierPriceManagementInterface * @return boolean * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function add($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice); @@ -32,7 +32,7 @@ public function add($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface $t * @return boolean * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function remove($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface $tierPrice); @@ -43,7 +43,7 @@ public function remove($sku, \Magento\Catalog\Api\Data\ProductTierPriceInterface * @param string $customerGroupId 'all' can be used to specify 'ALL GROUPS' * @return \Magento\Catalog\Api\Data\ProductTierPriceInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function getList($sku, $customerGroupId); } diff --git a/app/code/Magento/Catalog/Api/SpecialPriceInterface.php b/app/code/Magento/Catalog/Api/SpecialPriceInterface.php index 86dca59004132..543eab2263cbe 100644 --- a/app/code/Magento/Catalog/Api/SpecialPriceInterface.php +++ b/app/code/Magento/Catalog/Api/SpecialPriceInterface.php @@ -9,7 +9,7 @@ /** * Special prices resource model. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface SpecialPriceInterface { @@ -30,6 +30,7 @@ interface SpecialPriceInterface * 'price_to' => (string) Special price to date value in UTC. * ] * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -47,7 +48,7 @@ public function get(array $skus); * ]; * @return bool * @throws \Magento\Framework\Exception\CouldNotSaveException Thrown if error occurred during price save. - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -65,7 +66,7 @@ public function update(array $prices); * ]; * @return bool * @throws \Magento\Framework\Exception\CouldNotDeleteException Thrown if error occurred during price delete. - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $prices); } diff --git a/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php b/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php index 2442af103a4e9..6c2d89b51278c 100644 --- a/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/SpecialPriceStorageInterface.php @@ -9,7 +9,7 @@ /** * Special price storage presents efficient price API and is used to retrieve, update or delete special prices. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface SpecialPriceStorageInterface { @@ -19,7 +19,7 @@ interface SpecialPriceStorageInterface * @param string[] $skus * @return \Magento\Catalog\Api\Data\SpecialPriceInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] * @throws \Magento\Framework\Exception\CouldNotSaveException - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -47,7 +47,7 @@ public function update(array $prices); * @param \Magento\Catalog\Api\Data\SpecialPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] * @throws \Magento\Framework\Exception\CouldNotDeleteException - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $prices); } diff --git a/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php index 584daa9864588..b9102fcfc075c 100644 --- a/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php +++ b/app/code/Magento/Catalog/Api/TierPriceStorageInterface.php @@ -9,7 +9,7 @@ /** * Tier prices storage. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface TierPriceStorageInterface { @@ -19,7 +19,7 @@ interface TierPriceStorageInterface * @param string[] $skus * @return \Magento\Catalog\Api\Data\TierPriceInterface[] * @throws \Magento\Framework\Exception\NoSuchEntityException - * @since 101.1.0 + * @since 102.0.0 */ public function get(array $skus); @@ -33,7 +33,7 @@ public function get(array $skus); * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function update(array $prices); @@ -47,7 +47,7 @@ public function update(array $prices); * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function replace(array $prices); @@ -61,7 +61,7 @@ public function replace(array $prices); * * @param \Magento\Catalog\Api\Data\TierPriceInterface[] $prices * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function delete(array $prices); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php index 3266922d116ec..acffce3ca0b8c 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Pricestep.php @@ -11,11 +11,45 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Pricestep Helper */ class Pricestep extends \Magento\Framework\Data\Form\Element\Text { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + parent::__construct( + $factoryElement, + $factoryCollection, + $escaper, + $data + ); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Returns js code that is used instead of default toggle code for "Use default config" checkbox * @@ -53,18 +87,23 @@ public function getElementHtml() $html .= ' disabled="disabled"'; } - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '" class="normal">' . __('Use Config Settings') . '</label>'; - $html .= '<script>' . + $scriptString = 'require(["prototype"], function(){'. 'toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . '\').parentNode);' . - '});'. - '</script>'; + '});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php index 2a88d0f4d4f15..b0f00d0f2b04b 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/Available.php @@ -11,8 +11,41 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Available extends \Magento\Framework\Data\Form\Element\Multiselect { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); + $this->secureRenderer = $secureRenderer; + } + /** * Returns js code that is used instead of default toggle code for "Use default config" checkbox * @@ -49,13 +82,19 @@ public function getElementHtml() $html .= ' disabled="disabled"'; } - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '" class="normal">' . __('Use All Available Attributes') . '</label>'; - $html .= '<script>require(["prototype"], function(){toggleValueElements($(\'' . + $scriptString = 'require(["prototype"], function(){toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . - '\').parentNode);});</script>'; + '\').parentNode);});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php index 2d887d05f62ad..e0836a0d7cb25 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Helper/Sortby/DefaultSortby.php @@ -11,8 +11,41 @@ */ namespace Magento\Catalog\Block\Adminhtml\Category\Helper\Sortby; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class DefaultSortby extends \Magento\Framework\Data\Form\Element\Select { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); + $this->secureRenderer = $secureRenderer; + } + /** * Returns js code that is used instead of default toggle code for "Use default config" checkbox * @@ -49,13 +82,19 @@ public function getElementHtml() $html .= ' disabled="disabled"'; } - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '" class="normal">' . __('Use Config Settings') . '</label>'; - $html .= '<script>require(["prototype"], function(){toggleValueElements($(\'' . + $scriptString = 'require(["prototype"], function(){toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . - '\').parentNode);});</script>'; + '\').parentNode);});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php index 929c181bf820c..a66dcece2bef0 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Category/Tree.php @@ -10,16 +10,18 @@ namespace Magento\Catalog\Block\Adminhtml\Category; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Tree\Node; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Store\Model\Store; /** - * Class Tree + * Class Category Tree * * @api - * @package Magento\Catalog\Block\Adminhtml\Category * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @since 100.0.2 */ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory @@ -44,6 +46,11 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Catalog\Model\ResourceModel\Category\Tree $categoryTree @@ -53,6 +60,7 @@ class Tree extends \Magento\Catalog\Block\Adminhtml\Category\AbstractCategory * @param \Magento\Framework\DB\Helper $resourceHelper * @param \Magento\Backend\Model\Auth\Session $backendSession * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -62,12 +70,14 @@ public function __construct( \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Framework\DB\Helper $resourceHelper, \Magento\Backend\Model\Auth\Session $backendSession, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_resourceHelper = $resourceHelper; $this->_backendSession = $backendSession; parent::__construct($context, $categoryTree, $registry, $categoryFactory, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -336,12 +346,14 @@ public function getBreadcrumbsJavascript($path, $javascriptVarName) foreach ($categories as $key => $category) { $categories[$key] = $this->_getNodeJson($category); } - return '<script>require(["prototype"], function(){' . $javascriptVarName . ' = ' . $this->_jsonEncoder->encode( + $scriptString = 'require(["prototype"], function(){' . $javascriptVarName . ' = ' . $this->_jsonEncoder->encode( $categories ) . ';' . ($this->canAddSubCategory() ? '$("add_subcategory_button").show();' : '$("add_subcategory_button").hide();') - . '});</script>'; + . '});'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php index c58ed58370e3a..48753bfd6efb4 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Helper/Form/Wysiwyg.php @@ -11,8 +11,13 @@ */ namespace Magento\Catalog\Block\Adminhtml\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Wysiwyg helper. + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea { @@ -40,6 +45,11 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea */ protected $_layout; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Framework\Data\Form\Element\Factory $factoryElement * @param \Magento\Framework\Data\Form\Element\CollectionFactory $factoryCollection @@ -49,6 +59,7 @@ class Wysiwyg extends \Magento\Framework\Data\Form\Element\Textarea * @param \Magento\Framework\Module\Manager $moduleManager * @param \Magento\Backend\Helper\Data $backendData * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Framework\Data\Form\Element\Factory $factoryElement, @@ -58,13 +69,15 @@ public function __construct( \Magento\Framework\View\LayoutInterface $layout, \Magento\Framework\Module\Manager $moduleManager, \Magento\Backend\Helper\Data $backendData, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_wysiwygConfig = $wysiwygConfig; $this->_layout = $layout; $this->_moduleManager = $moduleManager; $this->_backendData = $backendData; parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -95,8 +108,7 @@ public function getAfterElementHtml() ] ] )->toHtml(); - $html .= <<<HTML -<script> + $scriptString = <<<HTML require([ 'jquery', 'mage/adminhtml/wysiwyg/tiny_mce/setup' @@ -119,9 +131,10 @@ public function getAfterElementHtml() editor ); }); -</script> HTML; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } + return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php index e6afc41ebebac..822580801c4e4 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit.php @@ -12,10 +12,12 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** - * Class Edit + * Class for Product Edit. */ class Edit extends \Magento\Backend\Block\Widget { @@ -59,6 +61,7 @@ class Edit extends \Magento\Backend\Block\Widget * @param \Magento\Catalog\Helper\Product $productHelper * @param Escaper $escaper * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -67,13 +70,15 @@ public function __construct( \Magento\Framework\Registry $registry, \Magento\Catalog\Helper\Product $productHelper, Escaper $escaper, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_productHelper = $productHelper; $this->_attributeSetFactory = $attributeSetFactory; $this->_coreRegistry = $registry; $this->jsonEncoder = $jsonEncoder; $this->escaper = $escaper; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } @@ -288,7 +293,7 @@ public function getDuplicateUrl() /** * Retrieve product header * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return string */ public function getHeader() diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php index 1ebfa14200364..287a24e0aaca0 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Action/Attribute/Tab/Attributes.php @@ -22,9 +22,11 @@ use Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Weight; use Magento\Catalog\Helper\Product\Edit\Action\Attribute; use Magento\Catalog\Model\ProductFactory; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\Data\FormFactory; use Magento\Framework\Registry; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Attributes tab block @@ -51,6 +53,11 @@ class Attributes extends Form implements TabInterface */ private $excludeFields; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param Context $context * @param Registry $registry @@ -59,6 +66,7 @@ class Attributes extends Form implements TabInterface * @param Attribute $attributeAction * @param array $data * @param array|null $excludeFields + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( Context $context, @@ -67,13 +75,15 @@ public function __construct( ProductFactory $productFactory, Attribute $attributeAction, array $data = [], - array $excludeFields = null + array $excludeFields = null, + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_attributeAction = $attributeAction; $this->_productFactory = $productFactory; $this->excludeFields = $excludeFields ?: []; parent::__construct($context, $registry, $formFactory, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -146,13 +156,20 @@ protected function _getAdditionalElementHtml($element) // @codingStandardsIgnoreStart $html = <<<HTML <span class="attribute-change-checkbox"> - <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" class="checkbox" $nameAttributeHtml onclick="toogleFieldEditMode(this, '{$elementId}')" $dataAttribute /> + <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" + class="checkbox" $nameAttributeHtml $dataAttribute /> <label class="label" for="$dataCheckboxName"> {$checkboxLabel} </label> </span> HTML; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toogleFieldEditMode(this, '{$elementId}')", + "#". $dataCheckboxName + ); + // @codingStandardsIgnoreEnd return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php index d95ee7f8f2cf9..6419ae2d70588 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/AttributeSet.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Admin AttributeSet block */ @@ -27,13 +30,16 @@ class AttributeSet extends \Magento\Backend\Block\Widget\Form * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php index 9712c0e03d609..2620fd345c667 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Js.php @@ -9,6 +9,8 @@ use Magento\Customer\Helper\Session\CurrentCustomer; use Magento\Tax\Api\TaxCalculationInterface; use Magento\Tax\Model\TaxClass\Source\Product as ProductTaxClassSource; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; class Js extends \Magento\Backend\Block\Template { @@ -51,6 +53,7 @@ class Js extends \Magento\Backend\Block\Template * @param TaxCalculationInterface $calculationService * @param ProductTaxClassSource $productTaxClassSource * @param array $data + * @param TaxHelper|null $taxHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -59,13 +62,15 @@ public function __construct( \Magento\Framework\Json\Helper\Data $jsonHelper, TaxCalculationInterface $calculationService, ProductTaxClassSource $productTaxClassSource, - array $data = [] + array $data = [], + ?TaxHelper $taxHelper = null ) { $this->coreRegistry = $registry; $this->currentCustomer = $currentCustomer; $this->jsonHelper = $jsonHelper; $this->calculationService = $calculationService; $this->productTaxClassSource = $productTaxClassSource; + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php index 0a766bc4c0cb3..ebc269f85d054 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/NewCategory.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * @SuppressWarnings(PHPMD.DepthOfInheritance) */ @@ -26,13 +29,19 @@ class NewCategory extends \Magento\Backend\Block\Widget\Form\Generic */ protected $_categoryFactory; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context - * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Framework\Registry $registry * @param \Magento\Framework\Data\FormFactory $formFactory + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Catalog\Model\CategoryFactory $categoryFactory * @param array $data + * @param SecureHtmlRenderer|null $htmlRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -40,12 +49,14 @@ public function __construct( \Magento\Framework\Data\FormFactory $formFactory, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Catalog\Model\CategoryFactory $categoryFactory, - array $data = [] + array $data = [], + SecureHtmlRenderer $htmlRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_categoryFactory = $categoryFactory; parent::__construct($context, $registry, $formFactory, $data); $this->setUseContainer(true); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -153,14 +164,13 @@ public function getAfterElementHtml() ] ); //TODO: JavaScript logic should be moved to separate file or reviewed - return <<<HTML -<script> + $scriptString = <<<HTML require(["jquery","mage/mage"],function($) { // waiting for dependencies at first $(function(){ // waiting for page to load to have '#category_ids-template' available - $('#new-category').mage('newCategoryDialog', $widgetOptions); + $('#new-category').mage('newCategoryDialog', {$widgetOptions}); }); }); -</script> HTML; + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php index 3d131a6e08810..2962e63313cdc 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Ajax/Serializer.php @@ -10,7 +10,7 @@ /** * Class Serializer * @package Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Ajax - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ class Serializer extends \Magento\Framework\View\Element\Template { @@ -47,7 +47,7 @@ public function _construct() /** * @return string - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ public function getProductsJSON() { diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php index 42463354926dd..702c77e3a5595 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Attributes/Search.php @@ -11,6 +11,9 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Admin product attribute search block */ @@ -39,17 +42,20 @@ class Search extends \Magento\Backend\Block\Widget * @param \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $collectionFactory * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\DB\Helper $resourceHelper, \Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory $collectionFactory, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->_resourceHelper = $resourceHelper; $this->_collectionFactory = $collectionFactory; $this->_coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php index e5ce59c550af1..40e7136da5bf6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Crosssell.php @@ -15,7 +15,7 @@ * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 - * @deprecated Not used since cross-sell products grid moved to UI components. + * @deprecated 103.0.1 Not used since cross-sell products grid moved to UI components. * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider */ class Crosssell extends Extended diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php index ccf207938ab06..0a9434768737d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Options/Option.php @@ -14,6 +14,9 @@ use Magento\Backend\Block\Widget; use Magento\Catalog\Model\Product; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Store\Model\Store; /** @@ -72,6 +75,11 @@ class Option extends Widget */ protected $_optionType; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Config\Model\Config\Source\Yesno $configYesNo @@ -80,6 +88,8 @@ class Option extends Widget * @param \Magento\Framework\Registry $registry * @param \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig * @param array $data + * @param JsonHelper|null $jsonHelper + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -88,14 +98,18 @@ public function __construct( Product $product, \Magento\Framework\Registry $registry, \Magento\Catalog\Model\ProductOptions\ConfigInterface $productOptionConfig, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null, + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_optionType = $optionType; $this->_configYesNo = $configYesNo; $this->_product = $product; $this->_productOptionConfig = $productOptionConfig; $this->_coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -460,8 +474,12 @@ public function getCheckboxScopeHtml($id, $name, $checked = true, $select_id = ' . ' name="' . $localName . '"' . 'id="' . $localId . '"' . ' value=""' . $checkedHtml - . ' onchange="toggleSeveralValueElements(this, [' . $containers . ']);" ' . ' />' + . $this->secureRenderer->renderEventListenerAsTag( + 'onchange', + "toggleSeveralValueElements(this, [' . $containers . ']);", + '#' . $localId + ) . '<label for="' . $localId . '" class="use-default">' . '<span class="use-default-label">' . __('Use Default') . '</span></label></div>'; @@ -482,6 +500,8 @@ public function getPriceValue($value, $type) } elseif ($type == 'fixed') { return number_format((float)$value, 2, null, ''); } + + return ''; } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php index 7cb1c2c9e4263..00cd020e4f525 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Price/Tier.php @@ -5,8 +5,15 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Price; +use Magento\Customer\Api\GroupManagementInterface; +use Magento\Customer\Api\GroupRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Adminhtml tier price item renderer + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ class Tier extends Group\AbstractGroup { @@ -15,6 +22,44 @@ class Tier extends Group\AbstractGroup */ protected $_template = 'Magento_Catalog::catalog/product/edit/price/tier.phtml'; + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param GroupRepositoryInterface $groupRepository + * @param \Magento\Directory\Helper\Data $directoryHelper + * @param \Magento\Framework\Module\Manager $moduleManager + * @param \Magento\Framework\Registry $registry + * @param GroupManagementInterface $groupManagement + * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder + * @param \Magento\Framework\Locale\CurrencyInterface $localeCurrency + * @param array $data + * @param JsonHelper|null $jsonHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + GroupRepositoryInterface $groupRepository, + \Magento\Directory\Helper\Data $directoryHelper, + \Magento\Framework\Module\Manager $moduleManager, + \Magento\Framework\Registry $registry, + GroupManagementInterface $groupManagement, + \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, + \Magento\Framework\Locale\CurrencyInterface $localeCurrency, + array $data = [], + ?JsonHelper $jsonHelper = null + ) { + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); + parent::__construct( + $context, + $groupRepository, + $directoryHelper, + $moduleManager, + $registry, + $groupManagement, + $searchCriteriaBuilder, + $localeCurrency, + $data + ); + } + /** * Retrieve list of initial customer groups * @@ -62,6 +107,7 @@ protected function _sortTierPrices($a, $b) /** * Prepare global layout + * * Add "Add tier" button to layout * * @return $this diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php index 23b927598e8e7..c73ffe5764dfb 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Related.php @@ -13,7 +13,7 @@ * * @api * @since 100.0.2 - * @deprecated Not used since related products grid moved to UI components. + * @deprecated 103.0.1 Not used since related products grid moved to UI components. * @see \Magento\Catalog\Ui\DataProvider\Product\Related\RelatedDataProvider */ class Related extends Extended diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php index 41ad72ca39e53..d196f82f8b48d 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Edit/Tab/Upsell.php @@ -10,7 +10,7 @@ * * @api * @since 100.0.2 - * @deprecated Not used since upsell products grid moved to UI components. + * @deprecated 103.0.1 Not used since upsell products grid moved to UI components. * @see \Magento\Catalog\Ui\DataProvider\Product\Related\CrossSellDataProvider */ class Upsell extends \Magento\Backend\Block\Widget\Grid\Extended diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php index 86ed3d09d5728..101daec2b4906 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Apply.php @@ -11,9 +11,18 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Apply extends \Magento\Framework\Data\Form\Element\Multiselect { /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * Return html of the element. + * * @return string */ public function getElementHtml() @@ -28,12 +37,19 @@ public function getElementHtml() $elementAttributeHtml = $elementAttributeHtml . ' disabled="disabled"'; } - $html = '<select onchange="toggleApplyVisibility(this)"' . $elementAttributeHtml . '>' + $html = '<select id="' . $this->getHtmlId() . '"' . $elementAttributeHtml . '>' . '<option value="0">' . $this->getModeLabels('all') . '</option>' . '<option value="1" ' . ($this->getValue() == null ? '' : 'selected') . '>' . $this->getModeLabels('custom') . '</option>' . '</select><br /><br />'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onchange', + "toggleApplyVisibility(this)", + 'select#' . $this->getHtmlId() + ); + $html .= parent::getElementHtml(); + return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php index df372312613f4..698bb12022bc6 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Category.php @@ -7,7 +7,9 @@ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; use Magento\Catalog\Model\ResourceModel\Category\Collection; +use Magento\Framework\App\ObjectManager; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Product form category field helper @@ -41,6 +43,11 @@ class Category extends \Magento\Framework\Data\Form\Element\Multiselect */ protected $authorization; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Framework\Data\Form\Element\Factory $factoryElement * @param \Magento\Framework\Data\Form\Element\CollectionFactory $factoryCollection @@ -51,6 +58,8 @@ class Category extends \Magento\Framework\Data\Form\Element\Multiselect * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param AuthorizationInterface $authorization * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Framework\Data\Form\Element\Factory $factoryElement, @@ -61,7 +70,8 @@ public function __construct( \Magento\Framework\View\LayoutInterface $layout, \Magento\Framework\Json\EncoderInterface $jsonEncoder, AuthorizationInterface $authorization, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_collectionFactory = $collectionFactory; @@ -69,6 +79,7 @@ public function __construct( $this->authorization = $authorization; parent::__construct($factoryElement, $factoryCollection, $escaper, $data); $this->_layout = $layout; + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); if (!$this->isAllowed()) { $this->setType('hidden'); $this->addClass('hidden'); @@ -136,13 +147,15 @@ public function getAfterElementHtml() ); $return = <<<HTML <input id="{$htmlId}-suggest" placeholder="$suggestPlaceholder" /> - <script> +HTML; + $scriptString = <<<script require(["jquery", "mage/mage"], function($){ $('#{$htmlId}-suggest').mage('treeSuggest', {$selectorOptions}); }); - </script> -HTML; - return $return . $button->toHtml(); +script; + + return $return . $button->toHtml() . + /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } /** diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php index 0c82ac537689f..16c00aee6beaa 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Config.php @@ -11,8 +11,38 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Config extends \Magento\Framework\Data\Form\Element\Select { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + /** * Retrieve element html * @@ -31,13 +61,19 @@ public function getElementHtml() $disabled = $this->getReadonly() ? ' disabled="disabled"' : ''; $html .= '<input id="' . $htmlId . '" name="product[' . $htmlId . ']" ' . $disabled . ' value="1" ' . $checked; - $html .= ' onclick="toggleValueElements(this, this.parentNode);" class="checkbox" type="checkbox" />'; + $html .= ' class="checkbox" type="checkbox" />'; $html .= ' <label for="' . $htmlId . '">' . __('Use Config Settings') . '</label>'; - $html .= '<script>require(["prototype"], function(){toggleValueElements($(\'' . + $scriptString = 'require(["prototype"], function(){toggleValueElements($(\'' . $htmlId . '\'), $(\'' . $htmlId . - '\').parentNode);});</script>'; + '\').parentNode);});'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements($('#' . $htmlId), $('#' . $htmlId).parentNode);", + '#' . $htmlId + ); return $html; } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php index 8e6011c09a27f..57cea59bee207 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Gallery/Content.php @@ -15,6 +15,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Backend\Block\Media\Uploader; +use Magento\Framework\Json\Helper\Data as JsonHelper; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\FileSystemException; @@ -23,6 +24,8 @@ /** * Block for gallery content. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Content extends \Magento\Backend\Block\Widget { @@ -63,6 +66,7 @@ class Content extends \Magento\Backend\Block\Widget * @param array $data * @param ImageUploadConfigDataProvider $imageUploadConfigDataProvider * @param Database $fileStorageDatabase + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -70,10 +74,12 @@ public function __construct( \Magento\Catalog\Model\Product\Media\Config $mediaConfig, array $data = [], ImageUploadConfigDataProvider $imageUploadConfigDataProvider = null, - Database $fileStorageDatabase = null + Database $fileStorageDatabase = null, + ?JsonHelper $jsonHelper = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_mediaConfig = $mediaConfig; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); $this->imageUploadConfigDataProvider = $imageUploadConfigDataProvider ?: ObjectManager::getInstance()->get(ImageUploadConfigDataProvider::class); diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php index 1cc6a5b56bf2c..57e82581d83b2 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Image.php @@ -11,9 +11,43 @@ */ namespace Magento\Catalog\Block\Adminhtml\Product\Helper\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Image extends \Magento\Framework\Data\Form\Element\Image { /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param \Magento\Framework\Escaper $escaper + * @param UrlInterface $urlBuilder + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + \Magento\Framework\Escaper $escaper, + UrlInterface $urlBuilder, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $urlBuilder, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + + /** + * Return generated url. + * * @return bool|string */ protected function _getUrl() @@ -24,10 +58,13 @@ protected function _getUrl() ['_type' => \Magento\Framework\UrlInterface::URL_TYPE_MEDIA] ) . 'catalog/product/' . $this->getValue(); } + return $url; } /** + * Return generated delete checkbox. + * * @return string */ protected function _getDeleteCheckbox() @@ -39,18 +76,19 @@ protected function _getDeleteCheckbox() } else { $inputField = '<input value="%s" id="%s_hidden" type="hidden" class="required-entry" />'; $html .= sprintf($inputField, $this->getValue(), $this->getHtmlId()); - $html .= '<script>require(["prototype"], function(){ + $scriptString = 'require(["prototype"], function(){ syncOnchangeValue(\'' . $this->getHtmlId() . '\', \'' . $this->getHtmlId() . '_hidden\'); - }); - </script>'; + });'; + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } } else { $html .= parent::_getDeleteCheckbox(); } + return $html; } } diff --git a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php index 70b2948501d2d..7fe51a7327d64 100644 --- a/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php +++ b/app/code/Magento/Catalog/Block/Adminhtml/Product/Helper/Form/Weight.php @@ -9,6 +9,7 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Directory\Helper\Data; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form; use Magento\Catalog\Model\Product\Edit\WeightResolver; use Magento\Framework\Data\Form\Element\CollectionFactory; @@ -17,6 +18,7 @@ use Magento\Framework\Data\Form\Element\Text; use Magento\Framework\Escaper; use Magento\Framework\Locale\Format; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Product form weight field helper @@ -40,6 +42,11 @@ class Weight extends Text */ protected $directoryHelper; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection @@ -47,6 +54,7 @@ class Weight extends Text * @param Format $localeFormat * @param Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( Factory $factoryElement, @@ -54,7 +62,8 @@ public function __construct( Escaper $escaper, Format $localeFormat, Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->directoryHelper = $directoryHelper; $this->localeFormat = $localeFormat; @@ -75,6 +84,7 @@ public function __construct( ); parent::__construct($factoryElement, $factoryCollection, $escaper, $data); $this->addClass('validate-zero-or-greater'); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -199,8 +209,7 @@ private function getHtmlForWeightSwitcher() $checkboxLabel = __('Change'); $html .= <<<HTML <span class="attribute-change-checkbox"> - <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" class="checkbox" $nameAttributeHtml - onclick="toogleFieldEditMode(this, 'weight-switcher1'); toogleFieldEditMode(this, 'weight-switcher0');" /> + <input type="checkbox" id="$dataCheckboxName" name="$dataCheckboxName" class="checkbox" $nameAttributeHtml/> <label class="label" for="$dataCheckboxName"> {$checkboxLabel} </label> @@ -209,6 +218,12 @@ private function getHtmlForWeightSwitcher() $html .= '</label></div></div>'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toogleFieldEditMode(this, 'weight-switcher1'); toogleFieldEditMode(this, 'weight-switcher0');", + "#". $dataCheckboxName + ); + return $html; } } diff --git a/app/code/Magento/Catalog/Block/FrontendStorageManager.php b/app/code/Magento/Catalog/Block/FrontendStorageManager.php index 0c826b95cbb49..112058baf4e05 100644 --- a/app/code/Magento/Catalog/Block/FrontendStorageManager.php +++ b/app/code/Magento/Catalog/Block/FrontendStorageManager.php @@ -15,7 +15,7 @@ * Provide information to frontend storage manager * * @api - * @since 101.1.0 + * @since 102.0.0 */ class FrontendStorageManager extends \Magento\Framework\View\Element\Template { @@ -51,7 +51,7 @@ public function __construct( * in json format * * @return string - * @since 101.1.0 + * @since 102.0.0 */ public function getConfigurationJson() { diff --git a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php index c8da0f70f73b6..26af19fb85bcb 100644 --- a/app/code/Magento/Catalog/Block/Product/AbstractProduct.php +++ b/app/code/Magento/Catalog/Block/Product/AbstractProduct.php @@ -8,7 +8,7 @@ /** * Class AbstractProduct * @api - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 @@ -99,7 +99,7 @@ class AbstractProduct extends \Magento\Framework\View\Element\Template /** * @var ImageBuilder - * @since 101.1.0 + * @since 102.0.0 */ protected $imageBuilder; diff --git a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php index 523efe08c6a4e..15e8d41809631 100644 --- a/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php +++ b/app/code/Magento/Catalog/Block/Product/Compare/ListCompare.php @@ -213,6 +213,7 @@ public function getProductAttributeValue($product, $attribute) * * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute * @return bool + * @since 102.0.6 */ public function hasAttributeValueForProducts($attribute) { diff --git a/app/code/Magento/Catalog/Block/Product/Context.php b/app/code/Magento/Catalog/Block/Product/Context.php index db18eb2bc8a7d..36a3214cab079 100644 --- a/app/code/Magento/Catalog/Block/Product/Context.php +++ b/app/code/Magento/Catalog/Block/Product/Context.php @@ -18,7 +18,7 @@ * As Magento moves from inheritance-based APIs all such classes will be deprecated together with * the classes they were introduced for. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @SuppressWarnings(PHPMD) */ class Context extends \Magento\Framework\View\Element\Template\Context diff --git a/app/code/Magento/Catalog/Block/Product/Image.php b/app/code/Magento/Catalog/Block/Product/Image.php index ccc37029bedf7..8fc7ba483d980 100644 --- a/app/code/Magento/Catalog/Block/Product/Image.php +++ b/app/code/Magento/Catalog/Block/Product/Image.php @@ -21,19 +21,19 @@ class Image extends \Magento\Framework\View\Element\Template { /** - * @deprecated Property isn't used + * @deprecated 102.0.5 Property isn't used * @var \Magento\Catalog\Helper\Image */ protected $imageHelper; /** - * @deprecated Property isn't used + * @deprecated 102.0.5 Property isn't used * @var \Magento\Catalog\Model\Product */ protected $product; /** - * @deprecated Property isn't used + * @deprecated 102.0.5 Property isn't used * @var array */ protected $attributes = []; diff --git a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php index 06d4fb39109d8..702410a530ea4 100644 --- a/app/code/Magento/Catalog/Block/Product/ImageBuilder.php +++ b/app/code/Magento/Catalog/Block/Product/ImageBuilder.php @@ -11,7 +11,7 @@ use Magento\Catalog\Model\Product; /** - * @deprecated + * @deprecated 103.0.0 * @see ImageFactory */ class ImageBuilder diff --git a/app/code/Magento/Catalog/Block/Product/ListProduct.php b/app/code/Magento/Catalog/Block/Product/ListProduct.php index 76fcdfbf232e5..6cec9bf3ef88a 100644 --- a/app/code/Magento/Catalog/Block/Product/ListProduct.php +++ b/app/code/Magento/Catalog/Block/Product/ListProduct.php @@ -23,6 +23,8 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Pricing\Render; use Magento\Framework\Url\Helper\Data; +use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Helper\Output as OutputHelper; /** * Product list @@ -75,6 +77,7 @@ class ListProduct extends AbstractProduct implements IdentityInterface * @param CategoryRepositoryInterface $categoryRepository * @param Data $urlHelper * @param array $data + * @param OutputHelper|null $outputHelper */ public function __construct( Context $context, @@ -82,12 +85,14 @@ public function __construct( Resolver $layerResolver, CategoryRepositoryInterface $categoryRepository, Data $urlHelper, - array $data = [] + array $data = [], + ?OutputHelper $outputHelper = null ) { $this->_catalogLayer = $layerResolver->get(); $this->_postDataHelper = $postDataHelper; $this->categoryRepository = $categoryRepository; $this->urlHelper = $urlHelper; + $data['outputHelper'] = $outputHelper ?? ObjectManager::getInstance()->get(OutputHelper::class); parent::__construct( $context, $data @@ -353,18 +358,16 @@ public function getIdentities() $category = $this->getLayer()->getCurrentCategory(); if ($category) { - $identities[] = Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $category->getId(); + $identities[] = [Product::CACHE_PRODUCT_CATEGORY_TAG . '_' . $category->getId()]; } //Check if category page shows only static block (No products) - if ($category->getData('display_mode') == Category::DM_PAGE) { - return $identities; - } - - foreach ($this->_getProductCollection() as $item) { - // phpcs:ignore Magento2.Performance.ForeachArrayMerge - $identities = array_merge($identities, $item->getIdentities()); + if ($category->getData('display_mode') != Category::DM_PAGE) { + foreach ($this->_getProductCollection() as $item) { + $identities[] = $item->getIdentities(); + } } + $identities = array_merge(...$identities); return $identities; } @@ -377,7 +380,7 @@ public function getIdentities() */ public function getAddToCartPostParams(Product $product) { - $url = $this->getAddToCartUrl($product); + $url = $this->getAddToCartUrl($product, ['_escape' => false]); return [ 'action' => $url, 'data' => [ diff --git a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php index 48725331b27da..5b5a7cd2d342a 100644 --- a/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php +++ b/app/code/Magento/Catalog/Block/Product/ProductList/Toolbar.php @@ -79,7 +79,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template /** * @var bool $_paramsMemorizeAllowed - * @deprecated + * @deprecated 103.0.1 */ protected $_paramsMemorizeAllowed = true; @@ -99,7 +99,7 @@ class Toolbar extends \Magento\Framework\View\Element\Template * Catalog session * * @var \Magento\Catalog\Model\Session - * @deprecated + * @deprecated 103.0.1 */ protected $_catalogSession; @@ -188,7 +188,7 @@ public function __construct( * Disable list state params memorizing * * @return $this - * @deprecated + * @deprecated 103.0.1 */ public function disableParamsMemorizing() { @@ -202,7 +202,7 @@ public function disableParamsMemorizing() * @param string $param parameter name * @param mixed $value parameter value * @return $this - * @deprecated + * @deprecated 103.0.1 */ protected function _memorizeParam($param, $value) { diff --git a/app/code/Magento/Catalog/Block/Product/View.php b/app/code/Magento/Catalog/Block/Product/View.php index 437171bcb4bc6..a25501d9ef150 100644 --- a/app/code/Magento/Catalog/Block/Product/View.php +++ b/app/code/Magento/Catalog/Block/Product/View.php @@ -30,7 +30,7 @@ class View extends AbstractProduct implements \Magento\Framework\DataObject\Iden /** * @var \Magento\Framework\Pricing\PriceCurrencyInterface - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected $priceCurrency; diff --git a/app/code/Magento/Catalog/Block/Product/View/AbstractView.php b/app/code/Magento/Catalog/Block/Product/View/AbstractView.php index ec16bc1d2334f..9c569725d98de 100644 --- a/app/code/Magento/Catalog/Block/Product/View/AbstractView.php +++ b/app/code/Magento/Catalog/Block/Product/View/AbstractView.php @@ -9,7 +9,7 @@ * Product view abstract block * * @api - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @since 100.0.2 */ abstract class AbstractView extends \Magento\Catalog\Block\Product\AbstractProduct diff --git a/app/code/Magento/Catalog/Block/Product/View/Attributes.php b/app/code/Magento/Catalog/Block/Product/View/Attributes.php index 5b9777cbfd1e7..9fd840b264085 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Attributes.php +++ b/app/code/Magento/Catalog/Block/Product/View/Attributes.php @@ -110,6 +110,7 @@ public function getAdditionalData(array $excludeAttr = []) * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute * @param array $excludeAttr * @return bool + * @since 103.0.0 */ protected function isVisibleOnFrontend( \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, diff --git a/app/code/Magento/Catalog/Block/Product/View/Details.php b/app/code/Magento/Catalog/Block/Product/View/Details.php index 38925e9ae3cd7..67303d177e71e 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Details.php +++ b/app/code/Magento/Catalog/Block/Product/View/Details.php @@ -14,6 +14,7 @@ * Holds a group of blocks to show as tabs. * * @api + * @since 103.0.1 */ class Details extends \Magento\Framework\View\Element\Template { @@ -25,6 +26,7 @@ class Details extends \Magento\Framework\View\Element\Template * @throws \Magento\Framework\Exception\LocalizedException * * @return array + * @since 103.0.1 */ public function getGroupSortedChildNames(string $groupName, string $callback): array { diff --git a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php index 0bfdcc678e9f7..1dcbf60db15c3 100644 --- a/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php +++ b/app/code/Magento/Catalog/Block/Product/View/Options/AbstractOptions.php @@ -3,14 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); + +/** + * Product options abstract type block + * + * @author Magento Core Team <core@magentocommerce.com> + */ namespace Magento\Catalog\Block\Product\View\Options; -use Magento\Catalog\Pricing\Price\BasePrice; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; use Magento\Catalog\Pricing\Price\CustomOptionPriceInterface; -use Magento\Framework\App\ObjectManager; /** * Product options section abstract block. @@ -45,29 +47,20 @@ abstract class AbstractOptions extends \Magento\Framework\View\Element\Template */ protected $_catalogHelper; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Pricing\Helper\Data $pricingHelper * @param \Magento\Catalog\Helper\Data $catalogData * @param array $data - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Pricing\Helper\Data $pricingHelper, \Magento\Catalog\Helper\Data $catalogData, - array $data = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $data = [] ) { $this->pricingHelper = $pricingHelper; $this->_catalogHelper = $catalogData; - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule - ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); parent::__construct($context, $data); } @@ -119,6 +112,7 @@ public function getOption() * Retrieve formatted price * * @return string + * @since 102.0.6 */ public function getFormattedPrice() { @@ -138,7 +132,7 @@ public function getFormattedPrice() * * @return string * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedPrice() */ public function getFormatedPrice() @@ -168,15 +162,6 @@ protected function _formatPrice($value, $flag = true) $priceStr = $sign; $customOptionPrice = $this->getProduct()->getPriceInfo()->getPrice('custom_option_price'); - - if (!$value['is_percent']) { - $value['pricing_value'] = $this->calculateCustomOptionCatalogRule->execute( - $this->getProduct(), - (float)$value['pricing_value'], - (bool)$value['is_percent'] - ); - } - $context = [CustomOptionPriceInterface::CONFIGURATION_OPTION_FLAG => true]; $optionAmount = $customOptionPrice->getCustomAmount($value['pricing_value'], null, $context); $priceStr .= $this->getLayout()->getBlock('product.price.render.default')->renderAmount( diff --git a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php index 6d96ba8e1880e..c5c08a0552f42 100644 --- a/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php +++ b/app/code/Magento/Catalog/Block/Ui/ProductViewCounter.php @@ -26,7 +26,7 @@ * by customer on frontend and data to synchronize this tracks with backend * * @api - * @since 101.1.0 + * @since 102.0.0 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductViewCounter extends Template @@ -122,7 +122,7 @@ public function __construct( * requests and will be flushed with full page cache * * @return string {JSON encoded data} - * @since 101.1.0 + * @since 102.0.0 * @throws \Magento\Framework\Exception\LocalizedException * @throws \Magento\Framework\Exception\NoSuchEntityException */ diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php index 696401e5430d6..3651a9bc6adea 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Action/Attribute/Save.php @@ -7,15 +7,17 @@ namespace Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute; use Magento\AsynchronousOperations\Api\Data\OperationInterface; +use Magento\Catalog\Model\ProductFactory; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Eav\Model\Config; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Backend\App\Action; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; /** - * Class used for saving mass updated products attributes. + * Class responsible for saving product attributes. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribute implements HttpPostActionInterface @@ -60,6 +62,11 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut */ private $eavConfig; + /** + * @var ProductFactory + */ + private $productFactory; + /** * @param Action\Context $context * @param \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper @@ -71,6 +78,7 @@ class Save extends \Magento\Catalog\Controller\Adminhtml\Product\Action\Attribut * @param int $bulkSize * @param TimezoneInterface $timezone * @param Config $eavConfig + * @param ProductFactory $productFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -83,7 +91,8 @@ public function __construct( \Magento\Authorization\Model\UserContextInterface $userContext, int $bulkSize = 100, TimezoneInterface $timezone = null, - Config $eavConfig = null + Config $eavConfig = null, + ProductFactory $productFactory = null ) { parent::__construct($context, $attributeHelper); $this->bulkManagement = $bulkManagement; @@ -96,6 +105,7 @@ public function __construct( ->get(TimezoneInterface::class); $this->eavConfig = $eavConfig ?: ObjectManager::getInstance() ->get(Config::class); + $this->productFactory = $productFactory ?? ObjectManager::getInstance()->get(ProductFactory::class); } /** @@ -121,9 +131,10 @@ public function execute() $attributesData = $this->sanitizeProductAttributes($attributesData); try { + $this->validateProductAttributes($attributesData); $this->publish($attributesData, $websiteRemoveData, $websiteAddData, $storeId, $websiteId, $productIds); $this->messageManager->addSuccessMessage(__('Message is added to queue')); - } catch (\Magento\Framework\Exception\LocalizedException $e) { + } catch (LocalizedException $e) { $this->messageManager->addErrorMessage($e->getMessage()); } catch (\Exception $e) { $this->messageManager->addExceptionMessage( @@ -152,10 +163,12 @@ private function sanitizeProductAttributes($attributesData) } $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + if (!$attribute->getAttributeId()) { unset($attributesData[$attributeCode]); continue; } + if ($attribute->getBackendType() === 'datetime') { if (!empty($value)) { $filterInput = new \Zend_Filter_LocalizedToNormalized(['date_format' => $dateFormat]); @@ -183,6 +196,25 @@ private function sanitizeProductAttributes($attributesData) return $attributesData; } + /** + * Validate product attributes data. + * + * @param array $attributesData + * + * @return void + * @throws LocalizedException + */ + private function validateProductAttributes(array $attributesData): void + { + $product = $this->productFactory->create(); + $product->setData($attributesData); + + foreach (array_keys($attributesData) as $attributeCode) { + $attribute = $this->eavConfig->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $attributeCode); + $attribute->getBackend()->validate($product); + } + } + /** * Schedule new bulk * @@ -192,7 +224,7 @@ private function sanitizeProductAttributes($attributesData) * @param int $storeId * @param int $websiteId * @param array $productIds - * @throws \Magento\Framework\Exception\LocalizedException + * @throws LocalizedException * * @return void */ @@ -246,7 +278,7 @@ private function publish( $this->userContext->getUserId() ); if (!$result) { - throw new \Magento\Framework\Exception\LocalizedException( + throw new LocalizedException( __('Something went wrong while processing the request.') ); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php index cf12e332be86d..4ca9d4b0d0606 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Save.php @@ -224,7 +224,7 @@ public function execute() return $this->returnResult('catalog/*/', [], ['error' => true]); } // entity type check - if ($model->getEntityTypeId() != $this->_entityTypeId) { + if ($model->getEntityTypeId() != $this->_entityTypeId || array_key_exists('backend_model', $data)) { $this->messageManager->addErrorMessage(__('We can\'t update the attribute.')); $this->_session->setAttributeData($data); return $this->returnResult('catalog/*/', [], ['error' => true]); @@ -261,6 +261,12 @@ public function execute() unset($data['apply_to']); } + if ($model->getBackendType() == 'static' && !$model->getIsUserDefined()) { + $data['frontend_class'] = $model->getFrontendClass(); + } + + unset($data['entity_type_id']); + $model->addData($data); if (!$attributeId) { diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php index dcb7074c0d036..a2be7db7e62be 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Attribute/Validate.php @@ -3,17 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Catalog\Controller\Adminhtml\Product\Attribute; +use Magento\Backend\App\Action\Context; use Magento\Catalog\Controller\Adminhtml\Product\Attribute as AttributeAction; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute; +use Magento\Eav\Model\Entity\Attribute\Set; use Magento\Eav\Model\Validator\Attribute\Code as AttributeCodeValidator; use Magento\Framework\App\Action\HttpGetActionInterface; use Magento\Framework\App\Action\HttpPostActionInterface as HttpPostActionInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Cache\FrontendInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; use Magento\Framework\DataObject; use Magento\Framework\Escaper; +use Magento\Framework\Registry; use Magento\Framework\Serialize\Serializer\FormData; +use Magento\Framework\View\LayoutFactory; +use Magento\Framework\View\Result\PageFactory; /** * Product attribute validate controller. @@ -25,12 +35,12 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo const DEFAULT_MESSAGE_KEY = 'message'; /** - * @var \Magento\Framework\Controller\Result\JsonFactory + * @var JsonFactory */ protected $resultJsonFactory; /** - * @var \Magento\Framework\View\LayoutFactory + * @var LayoutFactory */ protected $layoutFactory; @@ -57,12 +67,12 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo /** * Constructor * - * @param \Magento\Backend\App\Action\Context $context - * @param \Magento\Framework\Cache\FrontendInterface $attributeLabelCache - * @param \Magento\Framework\Registry $coreRegistry - * @param \Magento\Framework\View\Result\PageFactory $resultPageFactory - * @param \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory - * @param \Magento\Framework\View\LayoutFactory $layoutFactory + * @param Context $context + * @param FrontendInterface $attributeLabelCache + * @param Registry $coreRegistry + * @param PageFactory $resultPageFactory + * @param JsonFactory $resultJsonFactory + * @param LayoutFactory $layoutFactory * @param array $multipleAttributeList * @param FormData|null $formDataSerializer * @param AttributeCodeValidator|null $attributeCodeValidator @@ -70,12 +80,12 @@ class Validate extends AttributeAction implements HttpGetActionInterface, HttpPo * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Backend\App\Action\Context $context, - \Magento\Framework\Cache\FrontendInterface $attributeLabelCache, - \Magento\Framework\Registry $coreRegistry, - \Magento\Framework\View\Result\PageFactory $resultPageFactory, - \Magento\Framework\Controller\Result\JsonFactory $resultJsonFactory, - \Magento\Framework\View\LayoutFactory $layoutFactory, + Context $context, + FrontendInterface $attributeLabelCache, + Registry $coreRegistry, + PageFactory $resultPageFactory, + JsonFactory $resultJsonFactory, + LayoutFactory $layoutFactory, array $multipleAttributeList = [], FormData $formDataSerializer = null, AttributeCodeValidator $attributeCodeValidator = null, @@ -96,7 +106,7 @@ public function __construct( /** * @inheritdoc * - * @return \Magento\Framework\Controller\ResultInterface + * @return ResultInterface * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ @@ -118,14 +128,22 @@ public function execute() $attributeCode = $this->getRequest()->getParam('attribute_code'); $frontendLabel = $this->getRequest()->getParam('frontend_label'); - $attributeCode = $attributeCode ?: $this->generateCode($frontendLabel[0]); $attributeId = $this->getRequest()->getParam('attribute_id'); - $attribute = $this->_objectManager->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - )->loadByCode( - $this->_entityTypeId, - $attributeCode - ); + + if ($attributeId) { + $attribute = $this->_objectManager->create( + Attribute::class + )->load($attributeId); + $attributeCode = $attribute->getAttributeCode(); + } else { + $attributeCode = $attributeCode ?: $this->generateCode($frontendLabel[0]); + $attribute = $this->_objectManager->create( + Attribute::class + )->loadByCode( + $this->_entityTypeId, + $attributeCode + ); + } if ($attribute->getId() && !$attributeId || $attributeCode === 'product_type' || $attributeCode === 'type_id') { $message = strlen($this->getRequest()->getParam('attribute_code')) @@ -145,8 +163,8 @@ public function execute() if ($this->getRequest()->has('new_attribute_set_name')) { $setName = $this->getRequest()->getParam('new_attribute_set_name'); - /** @var $attributeSet \Magento\Eav\Model\Entity\Attribute\Set */ - $attributeSet = $this->_objectManager->create(\Magento\Eav\Model\Entity\Attribute\Set::class); + /** @var $attributeSet Set */ + $attributeSet = $this->_objectManager->create(Set::class); $attributeSet->setEntityTypeId($this->_entityTypeId)->load($setName, 'attribute_set_name'); if ($attributeSet->getId()) { $setName = $this->escaper->escapeHtml($setName); @@ -252,7 +270,7 @@ private function checkUniqueOption(DataObject $response, array $options = null) private function checkEmptyOption(DataObject $response, array $optionsForCheck = null) { foreach ($optionsForCheck as $optionValues) { - if (isset($optionValues[0]) && trim($optionValues[0]) == '') { + if (isset($optionValues[0]) && trim((string)$optionValues[0]) == '') { $this->setMessageToResponse($response, [__("The value of Admin scope can't be empty.")]); $response->setError(true); } diff --git a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php index 2ae97223d6359..d948daed1c7d9 100644 --- a/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php +++ b/app/code/Magento/Catalog/Controller/Adminhtml/Product/Initialization/Helper.php @@ -400,7 +400,7 @@ private function overwriteValue($optionId, $option, $overwriteOptions) * Get link resolver instance * * @return LinkResolver - * @deprecated 101.0.0 + * @deprecated 102.0.0 */ private function getLinkResolver() { diff --git a/app/code/Magento/Catalog/Controller/Category/View.php b/app/code/Magento/Catalog/Controller/Category/View.php index 552af244f0097..e448be9a1df21 100644 --- a/app/code/Magento/Catalog/Controller/Category/View.php +++ b/app/code/Magento/Catalog/Controller/Category/View.php @@ -205,10 +205,9 @@ protected function _initCategory() /** * Category view action * - * @return ResultInterface * @throws NoSuchEntityException */ - public function execute(): ?ResultInterface + public function execute() { $result = null; diff --git a/app/code/Magento/Catalog/Helper/Data.php b/app/code/Magento/Catalog/Helper/Data.php index 3e96763632830..3a55164aa33ef 100644 --- a/app/code/Magento/Catalog/Helper/Data.php +++ b/app/code/Magento/Catalog/Helper/Data.php @@ -451,7 +451,7 @@ public function isUsingStaticUrlsAllowed() * Check if the parsing of URL directives is allowed for the catalog * * @return bool - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Catalog\Helper\Output::isDirectivesExists */ public function isUrlDirectivesParsingAllowed() diff --git a/app/code/Magento/Catalog/Helper/Image.php b/app/code/Magento/Catalog/Helper/Image.php index 110b798df9df9..a06266037d05c 100644 --- a/app/code/Magento/Catalog/Helper/Image.php +++ b/app/code/Magento/Catalog/Helper/Image.php @@ -297,7 +297,7 @@ public function resize($width, $height = null) * * @param int $quality * @return $this - * @deprecated + * @deprecated 103.0.1 */ public function setQuality($quality) { @@ -446,7 +446,7 @@ public function placeholder($fileName) * @param null|string $placeholder * @return string * - * @deprecated 101.1.0 Returns only default placeholder. + * @deprecated 102.0.0 Returns only default placeholder. * Does not take into account custom placeholders set in Configuration. */ public function getPlaceholder($placeholder = null) diff --git a/app/code/Magento/Catalog/Model/AbstractModel.php b/app/code/Magento/Catalog/Model/AbstractModel.php index 78a49cd1e8b14..851055c1bf810 100644 --- a/app/code/Magento/Catalog/Model/AbstractModel.php +++ b/app/code/Magento/Catalog/Model/AbstractModel.php @@ -223,7 +223,7 @@ public function unsetData($key = null) * Get collection instance * * @return \Magento\Catalog\Model\ResourceModel\Collection\AbstractCollection - * @deprecated 101.1.0 because collections should be used directly via factory + * @deprecated 102.0.0 because collections should be used directly via factory */ public function getResourceCollection() { diff --git a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php index b5aa5e2035100..c3c331ccf7ef6 100644 --- a/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php +++ b/app/code/Magento/Catalog/Model/Attribute/Backend/Customlayoutupdate.php @@ -22,7 +22,7 @@ class Customlayoutupdate extends AbstractBackend { /** * @var ValidatorFactory - * @deprecated Is not used anymore. + * @deprecated 103.0.4 Is not used anymore. */ protected $_layoutUpdateValidatorFactory; @@ -117,6 +117,7 @@ private function putValue(AbstractModel $object, ?string $value): void * * @param AbstractModel $object * @throws LocalizedException + * @since 103.0.4 */ public function beforeSave($object) { diff --git a/app/code/Magento/Catalog/Model/Category.php b/app/code/Magento/Catalog/Model/Category.php index 330debdc32469..538a721d356d7 100644 --- a/app/code/Magento/Catalog/Model/Category.php +++ b/app/code/Magento/Catalog/Model/Category.php @@ -87,7 +87,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements * * @var string */ - protected $_cacheTag = self::CACHE_TAG; + protected $_cacheTag = false; /** * URL Model instance @@ -98,6 +98,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements /** * @var ResourceModel\Category + * @since 102.0.6 */ protected $_resource; @@ -105,7 +106,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements * URL rewrite model * * @var \Magento\UrlRewrite\Model\UrlRewrite - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected $_urlRewrite; @@ -135,7 +136,7 @@ class Category extends \Magento\Catalog\Model\AbstractModel implements /** * Attributes are that part of interface * - * @deprecated + * @deprecated 103.0.0 * @see CategoryInterface::ATTRIBUTES * @var array */ @@ -319,8 +320,9 @@ protected function getCustomAttributesCodes() * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Category - * @deprecated because resource models should be used directly + * @deprecated 102.0.6 because resource models should be used directly * phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod + * @since 102.0.6 */ protected function _getResource() { @@ -1111,6 +1113,17 @@ public function afterSave() return $result; } + /** + * @inheritDoc + */ + public function getCacheTags() + { + $identities = $this->getIdentities(); + $cacheTags = !empty($identities) ? (array) $identities : parent::getCacheTags(); + + return $cacheTags; + } + /** * Init indexing process after category save * diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php index 057933c55e6de..c1cfbdade3403 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Backend/Sortby.php @@ -102,6 +102,11 @@ public function beforeSave($object) } $object->setData($attributeCode, implode(',', $data) ?: null); } + if ($attributeCode == 'default_sort_by') { + $data = $object->getData($attributeCode); + $attributeValue = (is_array($data) ? reset($data) : (!empty($data))) ? $data : null; + $object->setData($attributeCode, $attributeValue); + } if (!$object->hasData($attributeCode)) { $object->setData($attributeCode, null); } diff --git a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php index 20ea899a3d0d7..486263f3de5a6 100644 --- a/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Category/Attribute/Source/Layout.php @@ -19,7 +19,7 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource /** * @inheritdoc - * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + * @deprecated 103.0.1 since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $_options = null; diff --git a/app/code/Magento/Catalog/Model/Category/DataProvider.php b/app/code/Magento/Catalog/Model/Category/DataProvider.php index d8c79c485e3e5..0a562a9a80c89 100644 --- a/app/code/Magento/Catalog/Model/Category/DataProvider.php +++ b/app/code/Magento/Catalog/Model/Category/DataProvider.php @@ -246,7 +246,7 @@ public function __construct( /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function getMeta() { @@ -495,7 +495,7 @@ protected function addUseConfigSettings($categoryData) * @param Category $category * @param array $categoryData * @return array - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @since 101.0.0 */ protected function addUseDefaultSettings($category, $categoryData) diff --git a/app/code/Magento/Catalog/Model/CategoryList.php b/app/code/Magento/Catalog/Model/CategoryList.php index cc8920203526f..e7c755b379b91 100644 --- a/app/code/Magento/Catalog/Model/CategoryList.php +++ b/app/code/Magento/Catalog/Model/CategoryList.php @@ -97,7 +97,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php index 497ed2fd49953..a928ddea03a70 100644 --- a/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php +++ b/app/code/Magento/Catalog/Model/FilterProductCustomAttribute.php @@ -8,21 +8,21 @@ namespace Magento\Catalog\Model; /** - * Filter custom attributes for product using the blacklist + * Filter custom attributes for product using the excluded list */ class FilterProductCustomAttribute { /** * @var array */ - private $blackList; + private $excludedList; /** - * @param array $blackList + * @param array $excludedList */ - public function __construct(array $blackList = []) + public function __construct(array $excludedList = []) { - $this->blackList = $blackList; + $this->excludedList = $excludedList; } /** @@ -33,6 +33,6 @@ public function __construct(array $blackList = []) */ public function execute(array $attributes): array { - return array_diff_key($attributes, array_flip($this->blackList)); + return array_diff_key($attributes, array_flip($this->excludedList)); } } diff --git a/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php b/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php index dac9e03e0b753..c61511d1ed49f 100644 --- a/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php +++ b/app/code/Magento/Catalog/Model/FrontendStorageConfigurationInterface.php @@ -9,7 +9,7 @@ /** * @api * Storage, which provide information for frontend storages, as product-storage, ids-storage - * @since 101.1.0 + * @since 102.0.0 */ interface FrontendStorageConfigurationInterface { @@ -23,7 +23,7 @@ interface FrontendStorageConfigurationInterface * Prepare dynamic data which will be used in Storage Configuration (e.g. data from App/Config) * * @return array - * @since 101.1.0 + * @since 102.0.0 */ public function get(); } diff --git a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php index 178f4172ce6fa..6b6ad2bfc726a 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Category/Product/AbstractAction.php @@ -109,6 +109,7 @@ abstract class AbstractAction /** * @var TableMaintainer + * @since 102.0.5 */ protected $tableMaintainer; @@ -195,7 +196,7 @@ protected function getTable($table) * The name is switched between 'catalog_category_product_index' and 'catalog_category_product_index_replica' * * @return string - * @deprecated + * @deprecated 102.0.5 */ protected function getMainTable() { @@ -206,7 +207,7 @@ protected function getMainTable() * Return temporary index table name * * @return string - * @deprecated + * @deprecated 102.0.5 */ protected function getMainTmpTable() { @@ -220,6 +221,7 @@ protected function getMainTmpTable() * * @param int $storeId * @return string + * @since 102.0.5 */ protected function getIndexTable($storeId) { diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php index c0722901e3b1c..0897ae0d74c0b 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Flat/TableBuilder.php @@ -368,7 +368,7 @@ protected function _fillTemporaryTable( * Get Metadata Pool * * @return \Magento\Framework\EntityManager\MetadataPool - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ private function getMetadataPool() { diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php index f010536f06ee5..41e72ecf880a5 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/AbstractAction.php @@ -154,7 +154,7 @@ abstract public function execute($ids); * @param array $processIds * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated Used only for backward compatibility for indexer, which not support indexation by dimensions + * @deprecated 102.0.6 Used only for backward compatibility for indexer, which not support indexation by dimensions */ protected function _syncData(array $processIds = []) { @@ -368,14 +368,20 @@ protected function _reindexRows($changedIds = []) $productsTypes = $this->getProductsTypes($changedIds); $parentProductsTypes = $this->getParentProductsTypes($changedIds); - $changedIds = array_merge($changedIds, ...array_values($parentProductsTypes)); + $changedIds = array_unique(array_merge($changedIds, ...array_values($parentProductsTypes))); $productsTypes = array_merge_recursive($productsTypes, $parentProductsTypes); if ($changedIds) { $this->deleteIndexData($changedIds); } - foreach ($productsTypes as $productType => $entityIds) { - $indexer = $this->_getIndexer($productType); + + $typeIndexers = $this->getTypeIndexers(); + foreach ($typeIndexers as $productType => $indexer) { + $entityIds = $productsTypes[$productType] ?? []; + if (empty($entityIds)) { + continue; + } + if ($indexer instanceof DimensionalIndexerInterface) { foreach ($this->dimensionCollectionFactory->create() as $dimensions) { $this->tableMaintainer->createMainTmpTable($dimensions); @@ -424,7 +430,7 @@ private function deleteIndexData(array $entityIds) * @param null|array $parentIds * @param array $excludeIds * @return \Magento\Catalog\Model\Indexer\Product\Price\AbstractAction - * @deprecated Used only for backward compatibility for do not broke custom indexer implementation + * @deprecated 102.0.6 Used only for backward compatibility for do not broke custom indexer implementation * which do not work by dimensions. * For indexers, which support dimensions all composite products read data directly from main price indexer table * or replica table for partial or full reindex correspondingly. diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php index 390c9784b50de..d36c7507afa8a 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/Action/Full.php @@ -488,7 +488,7 @@ private function moveDataFromReplicaTableToReplicaTables(array $dimensions): voi /** * Retrieves the index table that should be used * - * @deprecated + * @deprecated 102.0.6 */ protected function getIndexTargetTable(): string { diff --git a/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php b/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php index 3d1809997bd61..091131508ef66 100644 --- a/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php +++ b/app/code/Magento/Catalog/Model/Indexer/Product/Price/UpdateIndexInterface.php @@ -11,7 +11,7 @@ * Defines strategy for updating price index * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface UpdateIndexInterface { @@ -21,7 +21,7 @@ interface UpdateIndexInterface * @param GroupInterface $group * @param bool $isGroupNew * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function update(GroupInterface $group, $isGroupNew); } diff --git a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php index 77dedb9eb0121..3494fd00a8b6c 100644 --- a/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php +++ b/app/code/Magento/Catalog/Model/Layer/Filter/Price/Render.php @@ -72,6 +72,8 @@ public function renderRangeLabel($fromPrice, $toPrice) } /** + * Prepare range data + * * @param int $range * @param int[] $dbRanges * @return array @@ -81,12 +83,10 @@ public function renderRangeData($range, $dbRanges) if (empty($dbRanges)) { return []; } - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $this->itemDataBuilder->addItemData( $this->renderRangeLabel($fromPrice, $toPrice), $fromPrice . '-' . $toPrice, diff --git a/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php b/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php index b812de1dfc2ae..640a6539f0041 100644 --- a/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php +++ b/app/code/Magento/Catalog/Model/Plugin/SetPageLayoutDefaultValue.php @@ -11,7 +11,10 @@ namespace Magento\Catalog\Model\Plugin; use Magento\Catalog\Model\Category\DataProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Sets the default value for Category Design Layout if provided @@ -21,11 +24,28 @@ class SetPageLayoutDefaultValue private $defaultValue; /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager * @param string $defaultValue */ - public function __construct(string $defaultValue = "") - { + public function __construct( + ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, + string $defaultValue = "" + ) { $this->defaultValue = $defaultValue; + $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; } /** @@ -42,7 +62,15 @@ public function afterGetDefaultMetaData(DataProvider $subject, array $result): a $currentCategory = $subject->getCurrentCategory(); if ($currentCategory && !$currentCategory->getId() && array_key_exists('page_layout', $result)) { - $result['page_layout']['default'] = $this->defaultValue ?: null; + $defaultAdminValue = $this->scopeConfig->getValue( + 'web/default_layouts/default_category_layout', + ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore()->getId() + ); + + $defaultValue = $defaultAdminValue ?: $this->defaultValue; + + $result['page_layout']['default'] = $defaultValue ?: null; } return $result; diff --git a/app/code/Magento/Catalog/Model/Product.php b/app/code/Magento/Catalog/Model/Product.php index bc8d274fb6e63..7c463267e5a58 100644 --- a/app/code/Magento/Catalog/Model/Product.php +++ b/app/code/Magento/Catalog/Model/Product.php @@ -11,7 +11,6 @@ use Magento\Catalog\Api\ProductLinkRepositoryInterface; use Magento\Catalog\Model\Product\Attribute\Backend\Media\EntryConverterPool; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; -use Magento\Catalog\Model\FilterProductCustomAttribute; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; @@ -122,6 +121,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * @var ResourceModel\Product + * @since 102.0.6 */ protected $_resource; @@ -278,7 +278,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface - * @deprecated Not used anymore due to performance issue (loaded all product attributes) + * @deprecated 102.0.6 Not used anymore due to performance issue (loaded all product attributes) */ protected $metadataService; @@ -315,7 +315,7 @@ class Product extends \Magento\Catalog\Model\AbstractModel implements /** * List of attributes in ProductInterface * - * @deprecated + * @deprecated 103.0.0 * @see ProductInterface::ATTRIBUTES * @var array */ @@ -493,7 +493,8 @@ protected function _construct() * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Catalog\Model\ResourceModel\Product - * @deprecated because resource models should be used directly + * @deprecated 102.0.6 because resource models should be used directly + * @since 102.0.6 */ protected function _getResource() { @@ -640,7 +641,7 @@ public function getUpdatedAt() * * @param bool $calculate * @return void - * @deprecated + * @deprecated 102.0.4 */ public function setPriceCalculation($calculate = true) { @@ -977,6 +978,17 @@ public function afterSave() return $result; } + /** + * @inheritDoc + */ + public function getCacheTags() + { + $identities = $this->getIdentities(); + $cacheTags = !empty($identities) ? (array) $identities : parent::getCacheTags(); + + return $cacheTags; + } + /** * Set quantity for product * @@ -1168,6 +1180,7 @@ public function getTierPrice($qty = null) * Get formatted by currency product price * * @return array|double + * @since 102.0.6 */ public function getFormattedPrice() { @@ -1179,7 +1192,7 @@ public function getFormattedPrice() * * @return array|double * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedPrice() */ public function getFormatedPrice() @@ -2152,13 +2165,13 @@ public function reset() /** * Get cache tags associated with object id * - * @deprecated + * @deprecated 102.0.5 * @see \Magento\Catalog\Model\Product::getIdentities * @return string[] */ public function getCacheIdTags() { - // phpstan:ignore + // phpstan:ignore "Call to an undefined static method" $tags = parent::getCacheIdTags(); $affectedCategoryIds = $this->getAffectedCategoryIds(); if (!$affectedCategoryIds) { @@ -2339,7 +2352,8 @@ public function isDisabled() public function getImage() { $this->getTypeInstance()->setImageFromChildProduct($this); - // phpstan:ignore + + // phpstan:ignore "Call to an undefined static method" return parent::getImage(); } @@ -2403,6 +2417,8 @@ public function reloadPriceInfo() } } + //phpcs:disable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore + /** * Return Data Object data in array format. * @@ -2430,6 +2446,8 @@ public function __toArray() //phpcs:ignore PHPCompatibility.FunctionNameRestrict return $data; } + //phpcs:enable PHPCompatibility.FunctionNameRestrictions.ReservedFunctionNames.MethodDoubleUnderscore + /** * Convert Category model into flat array. * @@ -2726,11 +2744,11 @@ public function setAssociatedProductIds(array $productIds) * * @return array|null * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function getQuantityAndStockStatus() { @@ -2743,11 +2761,11 @@ public function getQuantityAndStockStatus() * @param array $quantityAndStockStatusData * @return $this * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function setQuantityAndStockStatus($quantityAndStockStatusData) { @@ -2760,11 +2778,11 @@ public function setQuantityAndStockStatus($quantityAndStockStatusData) * * @return array|null * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function getStockData() { @@ -2777,11 +2795,11 @@ public function getStockData() * @param array $stockData * @return $this * - * @deprecated 101.1.0 as Product model shouldn't be responsible for stock status + * @deprecated 102.0.0 as Product model shouldn't be responsible for stock status * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process - * @since 101.1.0 + * @since 102.0.0 */ public function setStockData($stockData) { diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php index 994ff98dee217..713a0a35abec7 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Backend/Stock.php @@ -10,7 +10,7 @@ /** * Quantity and Stock Status attribute processing * - * @deprecated 101.1.0 as this attribute should be removed + * @deprecated 102.0.0 as this attribute should be removed * @see StockItemInterface when you want to change the stock data * @see StockStatusInterface when you want to read the stock data for representation layer (storefront) * @see StockItemRepositoryInterface::save as extension point for customization of saving process diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php index b797308c30fb0..1554293661c02 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/OptionManagement.php @@ -6,25 +6,39 @@ */ namespace Magento\Catalog\Model\Product\Attribute; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeOptionManagementInterface; +use Magento\Catalog\Api\ProductAttributeOptionUpdateInterface; +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeOptionUpdateInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Framework\Exception\InputException; /** * Option management model for product attribute. */ -class OptionManagement implements \Magento\Catalog\Api\ProductAttributeOptionManagementInterface +class OptionManagement implements ProductAttributeOptionManagementInterface, ProductAttributeOptionUpdateInterface { /** - * @var \Magento\Eav\Api\AttributeOptionManagementInterface + * @var AttributeOptionManagementInterface */ protected $eavOptionManagement; /** - * @param \Magento\Eav\Api\AttributeOptionManagementInterface $eavOptionManagement + * @var AttributeOptionUpdateInterface + */ + private $eavOptionUpdate; + + /** + * @param AttributeOptionManagementInterface $eavOptionManagement + * @param AttributeOptionUpdateInterface $eavOptionUpdate */ public function __construct( - \Magento\Eav\Api\AttributeOptionManagementInterface $eavOptionManagement + AttributeOptionManagementInterface $eavOptionManagement, + AttributeOptionUpdateInterface $eavOptionUpdate ) { $this->eavOptionManagement = $eavOptionManagement; + $this->eavOptionUpdate = $eavOptionUpdate; } /** @@ -33,7 +47,7 @@ public function __construct( public function getItems($attributeCode) { return $this->eavOptionManagement->getItems( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode ); } @@ -44,8 +58,21 @@ public function getItems($attributeCode) public function add($attributeCode, $option) { return $this->eavOptionManagement->add( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode, + $option + ); + } + + /** + * @inheritdoc + */ + public function update(string $attributeCode, int $optionId, AttributeOptionInterface $option): bool + { + return $this->eavOptionUpdate->update( + ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode, + $optionId, $option ); } @@ -60,7 +87,7 @@ public function delete($attributeCode, $optionId) } return $this->eavOptionManagement->delete( - \Magento\Catalog\Api\Data\ProductAttributeInterface::ENTITY_TYPE_CODE, + ProductAttributeInterface::ENTITY_TYPE_CODE, $attributeCode, $optionId ); diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php index bc362430089c4..c0a13aa8b934a 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Countryofmanufacture.php @@ -82,7 +82,7 @@ public function getAllOptions() * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ private function getSerializer() { diff --git a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php index dbc7535dccfa9..333e8021d30b5 100644 --- a/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php +++ b/app/code/Magento/Catalog/Model/Product/Attribute/Source/Layout.php @@ -19,7 +19,7 @@ class Layout extends \Magento\Eav\Model\Entity\Attribute\Source\AbstractSource /** * @inheritdoc - * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + * @deprecated 103.0.1 since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $_options = null; diff --git a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php index 35c0a7835cb6c..b340e5dea5eb8 100644 --- a/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php +++ b/app/code/Magento/Catalog/Model/Product/Configuration/Item/ItemResolverInterface.php @@ -13,6 +13,7 @@ * Resolves the product from a configured item. * * @api + * @since 102.0.7 */ interface ItemResolverInterface { @@ -21,6 +22,7 @@ interface ItemResolverInterface * * @param ItemInterface $item * @return ProductInterface + * @since 102.0.7 */ public function getFinalProduct(ItemInterface $item) : ProductInterface; } diff --git a/app/code/Magento/Catalog/Model/Product/Image.php b/app/code/Magento/Catalog/Model/Product/Image.php index a0be36c5a327c..3c60d81e9a4d8 100644 --- a/app/code/Magento/Catalog/Model/Product/Image.php +++ b/app/code/Magento/Catalog/Model/Product/Image.php @@ -45,7 +45,7 @@ class Image extends \Magento\Framework\Model\AbstractModel * Default quality value (for JPEG images only). * * @var int - * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY + * @deprecated 103.0.1 use config setting with path self::XML_PATH_JPEG_QUALITY */ protected $_quality = null; @@ -305,7 +305,7 @@ public function getHeight() * * @param int $quality * @return $this - * @deprecated use config setting with path self::XML_PATH_JPEG_QUALITY + * @deprecated 103.0.1 use config setting with path self::XML_PATH_JPEG_QUALITY */ public function setQuality($quality) { @@ -454,7 +454,7 @@ public function getBaseFile() /** * Get new file * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return bool|string */ public function getNewFile() diff --git a/app/code/Magento/Catalog/Model/Product/Link.php b/app/code/Magento/Catalog/Model/Product/Link.php index 5c07d3d32b257..f2b07bad8891c 100644 --- a/app/code/Magento/Catalog/Model/Product/Link.php +++ b/app/code/Magento/Catalog/Model/Product/Link.php @@ -58,7 +58,7 @@ class Link extends \Magento\Framework\Model\AbstractModel /** * @var \Magento\CatalogInventory\Helper\Stock - * @deprecated 101.0.1 + * @deprecated 101.0.0 */ protected $stockHelper; diff --git a/app/code/Magento/Catalog/Model/Product/Option.php b/app/code/Magento/Catalog/Model/Product/Option.php index 128f420e033c2..e83982b8ce672 100644 --- a/app/code/Magento/Catalog/Model/Product/Option.php +++ b/app/code/Magento/Catalog/Model/Product/Option.php @@ -3,7 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product; @@ -17,10 +16,8 @@ use Magento\Catalog\Model\Product\Option\Type\File; use Magento\Catalog\Model\Product\Option\Type\Select; use Magento\Catalog\Model\Product\Option\Type\Text; -use Magento\Catalog\Model\Product\Option\Value; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; +use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Model\AbstractExtensibleModel; @@ -126,11 +123,6 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter */ private $customOptionValuesFactory; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -146,7 +138,6 @@ class Option extends AbstractExtensibleModel implements ProductCustomOptionInter * @param ProductCustomOptionValuesInterfaceFactory|null $customOptionValuesFactory * @param array $optionGroups * @param array $optionTypesToGroups - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -163,17 +154,14 @@ public function __construct( array $data = [], ProductCustomOptionValuesInterfaceFactory $customOptionValuesFactory = null, array $optionGroups = [], - array $optionTypesToGroups = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $optionTypesToGroups = [] ) { $this->productOptionValue = $productOptionValue; $this->optionTypeFactory = $optionFactory; $this->string = $string; $this->validatorPool = $validatorPool; $this->customOptionValuesFactory = $customOptionValuesFactory ?: - ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? - ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); + \Magento\Framework\App\ObjectManager::getInstance()->get(ProductCustomOptionValuesInterfaceFactory::class); $this->optionGroups = $optionGroups ?: [ self::OPTION_GROUP_DATE => Date::class, self::OPTION_GROUP_FILE => File::class, @@ -208,7 +196,7 @@ public function __construct( * Get resource instance * * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - * @deprecated 101.1.0 because resource models should be used directly + * @deprecated 102.0.0 because resource models should be used directly */ protected function _getResource() { @@ -258,7 +246,7 @@ public function getValueById($valueId) * * @param string $type * @return bool - * @since 101.1.0 + * @since 102.0.0 */ public function hasValues($type = null) { @@ -474,12 +462,10 @@ public function afterSave() */ public function getPrice($flag = false) { - if ($flag) { - return $this->calculateCustomOptionCatalogRule->execute( - $this->getProduct(), - (float)$this->getData(self::KEY_PRICE), - $this->getPriceType() === Value::TYPE_PERCENT - ); + if ($flag && $this->getPriceType() == self::$typePercent) { + $basePrice = $this->getProduct()->getPriceInfo()->getPrice(BasePrice::PRICE_CODE)->getValue(); + $price = $basePrice * ($this->_getData(self::KEY_PRICE) / 100); + return $price; } return $this->_getData(self::KEY_PRICE); } @@ -966,7 +952,7 @@ public function setExtensionAttributes( private function getOptionRepository() { if (null === $this->optionRepository) { - $this->optionRepository = ObjectManager::getInstance() + $this->optionRepository = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Catalog\Model\Product\Option\Repository::class); } return $this->optionRepository; @@ -980,7 +966,7 @@ private function getOptionRepository() private function getMetadataPool() { if (null === $this->metadataPool) { - $this->metadataPool = ObjectManager::getInstance() + $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\EntityManager\MetadataPool::class); } return $this->metadataPool; diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php index be7f1921afccf..16fdd4cdeeb1c 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/DefaultType.php @@ -3,19 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product\Option\Type; +use Magento\Framework\Exception\LocalizedException; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; -use Magento\Catalog\Model\Product\Option; +use Magento\Catalog\Model\Product\Configuration\Item\ItemInterface; use Magento\Catalog\Model\Product\Option\Value; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Exception\LocalizedException; /** * Catalog product option default type @@ -63,30 +60,21 @@ class DefaultType extends \Magento\Framework\DataObject */ protected $_checkoutSession; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * Construct * * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig * @param array $data - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - array $data = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $data = [] ) { $this->_checkoutSession = $checkoutSession; parent::__construct($data); $this->_scopeConfig = $scopeConfig; - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance() - ->get(CalculateCustomOptionCatalogRule::class); } /** @@ -104,12 +92,12 @@ public function setOption($option) /** * Option Instance getter * - * @return Option * @throws \Magento\Framework\Exception\LocalizedException + * @return Option */ public function getOption() { - if ($this->_option instanceof Option) { + if ($this->_option instanceof \Magento\Catalog\Model\Product\Option) { return $this->_option; } throw new LocalizedException(__('The option instance type in options group is incorrect.')); @@ -130,8 +118,8 @@ public function setProduct($product) /** * Product Instance getter * - * @return Product * @throws \Magento\Framework\Exception\LocalizedException + * @return Product */ public function getProduct() { @@ -169,8 +157,7 @@ public function getConfigurationItemOption() */ public function getConfigurationItem() { - if ($this->_getData('configuration_item') instanceof ItemInterface - ) { + if ($this->_getData('configuration_item') instanceof ItemInterface) { return $this->_getData('configuration_item'); } @@ -354,11 +341,7 @@ public function getOptionPrice($optionValue, $basePrice) { $option = $this->getOption(); - return $this->calculateCustomOptionCatalogRule->execute( - $option->getProduct(), - (float)$option->getPrice(), - $option->getPriceType() === Value::TYPE_PERCENT - ); + return $this->_getChargeableOptionPrice($option->getPrice(), $option->getPriceType() == 'percent', $basePrice); } /** @@ -418,8 +401,8 @@ public function getProductOptions() * @param boolean $isPercent Price type - percent or fixed * @param float $basePrice For percent price type * @return float - * @deprecated 102.0.4 typo in method name - * @see CalculateCustomOptionCatalogRule::execute + * @deprecated 102.0.6 typo in method name + * @see _getChargeableOptionPrice */ protected function _getChargableOptionPrice($price, $isPercent, $basePrice) { @@ -433,8 +416,7 @@ protected function _getChargableOptionPrice($price, $isPercent, $basePrice) * @param boolean $isPercent Price type - percent or fixed * @param float $basePrice For percent price type * @return float - * @deprecated - * @see CalculateCustomOptionCatalogRule::execute + * @since 102.0.6 */ protected function _getChargeableOptionPrice($price, $isPercent, $basePrice) { diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php index 9f1eae207e116..77ef8ef4853e1 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/File.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/File.php @@ -16,8 +16,9 @@ /** * Catalog product option file type * - * @author Magento Core Team <core@magentocommerce.com> + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class File extends \Magento\Catalog\Model\Product\Option\Type\DefaultType { @@ -181,6 +182,7 @@ protected function _getProcessingParams() /** * Returns file info array if we need to get file from already existing file. + * * Or returns null, if we need to get file from uploaded array. * * @return null|array @@ -262,7 +264,6 @@ public function validateUserValue($values) . "Make sure the options are entered and try again." ) ); - break; default: $this->setUserValue(null); break; @@ -330,7 +331,11 @@ public function prepareForCart() public function getFormattedOptionValue($optionValue) { if ($this->_formattedOptionValue === null) { - $value = $this->serializer->unserialize($optionValue); + try { + $value = $this->serializer->unserialize($optionValue); + } catch (\InvalidArgumentException $e) { + return $optionValue; + } if ($value === null) { return $optionValue; } @@ -411,7 +416,7 @@ public function getPrintableOptionValue($optionValue) * @param string $optionValue Prepared for cart option value * @return string * - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ public function getEditableOptionValue($optionValue) { @@ -435,7 +440,7 @@ public function getEditableOptionValue($optionValue) * * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ public function parseOptionValue($optionValue, $productOptionValues) { @@ -476,13 +481,13 @@ public function copyQuoteToOrder() try { $value = $this->serializer->unserialize($quoteOption->getValue()); if (!isset($value['quote_path'])) { - throw new \Exception(); + return $this; } $quotePath = $value['quote_path']; $orderPath = $value['order_path']; if (!$this->mediaDirectory->isFile($quotePath) || !$this->mediaDirectory->isReadable($quotePath)) { - throw new \Exception(); + return $this; } if ($this->_coreFileStorageDatabase->checkDbUsage()) { @@ -524,6 +529,8 @@ protected function _getOptionDownloadUrl($route, $params) } /** + * Prepare size + * * @param array $value * @return string */ diff --git a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php index 8eebd3e91c2ee..d2766b1bbb054 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Type/Select.php @@ -3,13 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product\Option\Type; -use Magento\Catalog\Model\Product\Option\Value; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; /** @@ -41,11 +37,6 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType */ private $singleSelectionTypes; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Checkout\Model\Session $checkoutSession * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig @@ -53,7 +44,6 @@ class Select extends \Magento\Catalog\Model\Product\Option\Type\DefaultType * @param \Magento\Framework\Escaper $escaper * @param array $data * @param array $singleSelectionTypes - * @param CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule */ public function __construct( \Magento\Checkout\Model\Session $checkoutSession, @@ -61,8 +51,7 @@ public function __construct( \Magento\Framework\Stdlib\StringUtils $string, \Magento\Framework\Escaper $escaper, array $data = [], - array $singleSelectionTypes = [], - CalculateCustomOptionCatalogRule $calculateCustomOptionCatalogRule = null + array $singleSelectionTypes = [] ) { $this->string = $string; $this->_escaper = $escaper; @@ -72,8 +61,6 @@ public function __construct( 'drop_down' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DROP_DOWN, 'radio' => \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_RADIO, ]; - $this->calculateCustomOptionCatalogRule = $calculateCustomOptionCatalogRule ?? ObjectManager::getInstance() - ->get(CalculateCustomOptionCatalogRule::class); } /** @@ -261,10 +248,10 @@ public function getOptionPrice($optionValue, $basePrice) foreach (explode(',', $optionValue) as $value) { $_result = $option->getValueById($value); if ($_result) { - $result += $this->calculateCustomOptionCatalogRule->execute( - $option->getProduct(), - (float)$_result->getPrice(), - $_result->getPriceType() === Value::TYPE_PERCENT + $result += $this->_getChargeableOptionPrice( + $_result->getPrice(), + $_result->getPriceType() == 'percent', + $basePrice ); } else { if ($this->getListener()) { @@ -276,10 +263,10 @@ public function getOptionPrice($optionValue, $basePrice) } elseif ($this->_isSingleSelection()) { $_result = $option->getValueById($optionValue); if ($_result) { - $result = $this->calculateCustomOptionCatalogRule->execute( - $option->getProduct(), - (float)$_result->getPrice(), - $_result->getPriceType() === Value::TYPE_PERCENT + $result = $this->_getChargeableOptionPrice( + $_result->getPrice(), + $_result->getPriceType() == 'percent', + $basePrice ); } else { if ($this->getListener()) { diff --git a/app/code/Magento/Catalog/Model/Product/Option/Value.php b/app/code/Magento/Catalog/Model/Product/Option/Value.php index 783bda4699792..313513a9151dc 100644 --- a/app/code/Magento/Catalog/Model/Product/Option/Value.php +++ b/app/code/Magento/Catalog/Model/Product/Option/Value.php @@ -3,16 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Option; -use Magento\Catalog\Pricing\Price\BasePrice; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Framework\App\ObjectManager; use Magento\Framework\Model\AbstractModel; +use Magento\Catalog\Pricing\Price\BasePrice; use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; use Magento\Catalog\Pricing\Price\RegularPrice; @@ -72,11 +69,6 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu */ private $customOptionPriceCalculator; - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - /** * @param \Magento\Framework\Model\Context $context * @param \Magento\Framework\Registry $registry @@ -85,7 +77,6 @@ class Value extends AbstractModel implements \Magento\Catalog\Api\Data\ProductCu * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection * @param array $data * @param CustomOptionPriceCalculator|null $customOptionPriceCalculator - * @param CalculateCustomOptionCatalogRule|null $CalculateCustomOptionCatalogRule */ public function __construct( \Magento\Framework\Model\Context $context, @@ -94,14 +85,11 @@ public function __construct( \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null, \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null, array $data = [], - CustomOptionPriceCalculator $customOptionPriceCalculator = null, - CalculateCustomOptionCatalogRule $CalculateCustomOptionCatalogRule = null + CustomOptionPriceCalculator $customOptionPriceCalculator = null ) { $this->_valueCollectionFactory = $valueCollectionFactory; $this->customOptionPriceCalculator = $customOptionPriceCalculator - ?? ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); - $this->calculateCustomOptionCatalogRule = $CalculateCustomOptionCatalogRule - ?? ObjectManager::getInstance()->get(CalculateCustomOptionCatalogRule::class); + ?? \Magento\Framework\App\ObjectManager::getInstance()->get(CustomOptionPriceCalculator::class); parent::__construct( $context, @@ -123,7 +111,7 @@ protected function _construct() } /** - * Add value. + * Add value to values array * * @codeCoverageIgnoreStart * @param mixed $value @@ -136,7 +124,7 @@ public function addValue($value) } /** - * Get values. + * Returns array of values * * @return array */ @@ -146,7 +134,7 @@ public function getValues() } /** - * Set values. + * Set values array * * @param array $values * @return $this @@ -158,7 +146,7 @@ public function setValues($values) } /** - * Unset values. + * Unset all from values array * * @return $this */ @@ -169,7 +157,7 @@ public function unsetValues() } /** - * Set option. + * Set option * * @param Option $option * @return $this @@ -181,7 +169,7 @@ public function setOption(Option $option) } /** - * Unset option. + * Unset option * * @return $this */ @@ -192,7 +180,7 @@ public function unsetOption() } /** - * Get option. + * Enter description here... * * @return Option */ @@ -202,7 +190,7 @@ public function getOption() } /** - * Set product. + * Set product * * @param Product $product * @return $this @@ -216,7 +204,7 @@ public function setProduct($product) //@codeCoverageIgnoreEnd /** - * Get product. + * Get product * * @return Product */ @@ -229,10 +217,9 @@ public function getProduct() } /** - * Save values. + * Save array of values * * @return $this - * @throws \Exception */ public function saveValues() { @@ -258,9 +245,7 @@ public function saveValues() } /** - * Return price. - * - * If $flag is true and price is percent return converted percent to price + * Return price. If $flag is true and price is percent return converted percent to price * * @param bool $flag * @return float|int @@ -268,11 +253,7 @@ public function saveValues() public function getPrice($flag = false) { if ($flag) { - return $this->calculateCustomOptionCatalogRule->execute( - $this->getProduct(), - (float)$this->getData(self::KEY_PRICE), - $this->getPriceType() === self::TYPE_PERCENT - ); + return $this->customOptionPriceCalculator->getOptionPriceByPriceCode($this, BasePrice::PRICE_CODE); } return $this->_getData(self::KEY_PRICE); } @@ -288,7 +269,7 @@ public function getRegularPrice() } /** - * Get values collection. + * Enter description here... * * @param Option $option * @return \Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection @@ -306,7 +287,7 @@ public function getValuesCollection(Option $option) } /** - * Get values by option. + * Returns values by option * * @param array $optionIds * @param int $option_id @@ -327,7 +308,7 @@ public function getValuesByOption($optionIds, $option_id, $store_id) } /** - * Delete value. + * Delete value by option * * @param int $option_id * @return $this @@ -339,7 +320,7 @@ public function deleteValue($option_id) } /** - * Delete values. + * Delete values by option * * @param int $option_type_id * @return $this diff --git a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php index 3d4d9f607da48..5b9b1c5e4816a 100644 --- a/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php +++ b/app/code/Magento/Catalog/Model/Product/Price/Validation/Result.php @@ -10,7 +10,7 @@ * Validation Result is used to aggregate errors that occurred during price update. * * @api - * @since 101.1.0 + * @since 102.0.0 */ class Result { @@ -43,7 +43,7 @@ public function __construct( * @param array $parameters (optional). Placeholder values in ['placeholder key' => 'placeholder value'] format * for failure reason message. * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function addFailedItem($id, $message, array $parameters = []) { @@ -57,7 +57,7 @@ public function addFailedItem($id, $message, array $parameters = []) * Get ids of rows, that contained errors during price update. * * @return int[] - * @since 101.1.0 + * @since 102.0.0 */ public function getFailedRowIds() { @@ -68,7 +68,7 @@ public function getFailedRowIds() * Get price update errors, that occurred during price update. * * @return \Magento\Catalog\Api\Data\PriceUpdateResultInterface[] - * @since 101.1.0 + * @since 102.0.0 */ public function getFailedItems() { @@ -83,6 +83,12 @@ public function getFailedItems() } } + /** + * Clear validation messages to prevent wrong validation for subsequent price update. + * Work around for backward compatible changes. + */ + $this->failedItems = []; + return $failedItems; } } diff --git a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php b/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php deleted file mode 100644 index 404760a51eff5..0000000000000 --- a/app/code/Magento/Catalog/Model/Product/SalabilityChecker.php +++ /dev/null @@ -1,57 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Model\Product; - -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Store\Model\StoreManagerInterface; - -/** - * Class to check that product is saleable. - */ -class SalabilityChecker -{ - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - - /** - * @var StoreManagerInterface - */ - private $storeManager; - - /** - * @param ProductRepositoryInterface $productRepository - * @param StoreManagerInterface $storeManager - */ - public function __construct( - ProductRepositoryInterface $productRepository, - StoreManagerInterface $storeManager - ) { - $this->productRepository = $productRepository; - $this->storeManager = $storeManager; - } - - /** - * Check if product is salable. - * - * @param int|string $productId - * @param int|null $storeId - * @return bool - */ - public function isSalable($productId, $storeId = null): bool - { - if ($storeId === null) { - $storeId = $this->storeManager->getStore()->getId(); - } - /** @var \Magento\Catalog\Model\Product $product */ - $product = $this->productRepository->getById($productId, false, $storeId); - - return $product->isSalable(); - } -} diff --git a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php index e6804d9246faa..eb4a71cb90a8c 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php +++ b/app/code/Magento/Catalog/Model/Product/Type/AbstractType.php @@ -9,15 +9,18 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\ObjectManager; /** - * @api * Abstract model for product type implementation + * + * phpcs:disable Magento2.Classes.AbstractApi + * @api + * @since 100.0.2 * @SuppressWarnings(PHPMD.ExcessivePublicCount) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.0.2 */ abstract class AbstractType { @@ -167,7 +170,7 @@ abstract public function deleteTypeSpecificData(\Magento\Catalog\Model\Product $ * Serializer interface instance. * * @var \Magento\Framework\Serialize\Serializer\Json - * @since 101.1.0 + * @since 102.0.0 */ protected $serializer; @@ -207,7 +210,7 @@ public function __construct( $this->_filesystem = $filesystem; $this->_logger = $logger; $this->productRepository = $productRepository; - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); } @@ -355,6 +358,7 @@ public function isSalable($product) /** * Prepare product and its configuration to be added to some products list. + * * Perform standard preparation process and then prepare options belonging to specific product type. * * @param \Magento\Framework\DataObject $buyRequest @@ -440,6 +444,7 @@ public function processConfiguration( /** * Initialize product(s) for add to cart process. + * * Advanced version of func to prepare product for cart - processMode can be specified there. * * @param \Magento\Framework\DataObject $buyRequest @@ -476,6 +481,7 @@ public function prepareForCart(\Magento\Framework\DataObject $buyRequest, $produ * @throws \Magento\Framework\Exception\LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * phpcs:disable Generic.Metrics.NestingLevel */ public function processFileQueue() { @@ -492,6 +498,7 @@ public function processFileQueue() /** @var $uploader \Zend_File_Transfer_Adapter_Http */ $uploader = isset($queueOptions['uploader']) ? $queueOptions['uploader'] : null; + // phpcs:ignore Magento2.Functions.DiscouragedFunction $path = dirname($dst); try { @@ -529,9 +536,11 @@ public function processFileQueue() return $this; } + //phpcs:enable /** * Add file to File Queue + * * @param array $queueOptions Array of File Queue * (eg. ['operation'=>'move', * 'src_name'=>'filename', @@ -572,6 +581,7 @@ public function getSpecifyOptionMessage() * @param string $processMode * @return array * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $product, $processMode) { @@ -583,6 +593,7 @@ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $p } if ($options !== null) { $results = []; + $optionsFromRequest = $buyRequest->getOptions(); foreach ($options as $option) { /* @var $option \Magento\Catalog\Model\Product\Option */ try { @@ -590,8 +601,14 @@ protected function _prepareOptions(\Magento\Framework\DataObject $buyRequest, $p ->setOption($option) ->setProduct($product) ->setRequest($buyRequest) - ->setProcessMode($processMode) - ->validateUserValue($buyRequest->getOptions()); + ->setProcessMode($processMode); + + if ($product->getSkipCheckRequiredOption() !== true) { + $group->validateUserValue($optionsFromRequest); + } elseif ($optionsFromRequest !== null && isset($optionsFromRequest[$option->getId()])) { + $transport->options[$option->getId()] = $optionsFromRequest[$option->getId()]; + } + } catch (LocalizedException $e) { $results[] = $e->getMessage(); continue; @@ -643,8 +660,7 @@ public function checkProductBuyState($product) } /** - * Prepare additional options/information for order item which will be - * created from this product + * Prepare additional options/information for order item which will be created from this product * * @param \Magento\Catalog\Model\Product $product * @return array @@ -900,7 +916,7 @@ public function getStoreFilter($product) /** * Set store filter for associated products * - * @param $store int|\Magento\Store\Model\Store + * @param int|\Magento\Store\Model\Store $store * @param \Magento\Catalog\Model\Product $product * @return $this */ @@ -913,6 +929,7 @@ public function setStoreFilter($store, $product) /** * Allow for updates of children qty's + * * (applicable for complicated product types. As default returns false) * * @param \Magento\Catalog\Model\Product $product @@ -940,6 +957,7 @@ public function prepareQuoteItemQty($qty, $product) /** * Implementation of product specify logic of which product needs to be assigned to option. + * * For example if product which was added to option already removed from catalog. * * @param \Magento\Catalog\Model\Product $optionProduct @@ -979,6 +997,7 @@ public function setConfig($config) /** * Retrieve additional searchable data from type instance + * * Using based on product id and store_id data * * @param \Magento\Catalog\Model\Product $product @@ -999,6 +1018,7 @@ public function getSearchableData($product) /** * Retrieve products divided into groups required to purchase + * * At least one product in each group has to be purchased * * @param \Magento\Catalog\Model\Product $product @@ -1092,6 +1112,8 @@ public function getIdentities(\Magento\Catalog\Model\Product $product) } /** + * Get Associated Products + * * @param \Magento\Catalog\Model\Product\Type\AbstractType $product * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -1106,7 +1128,7 @@ public function getAssociatedProducts($product) * * @param \Magento\Catalog\Model\Product $product * @return bool - * @since 101.1.0 + * @since 101.0.11 */ public function isPossibleBuyFromList($product) { diff --git a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php index dabfdb74f0118..a692ec5d463d0 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php +++ b/app/code/Magento/Catalog/Model/Product/Type/FrontSpecialPrice.php @@ -18,7 +18,7 @@ * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * - * @deprecated + * @deprecated 103.0.2 * @see \Magento\Catalog\Model\Product\Type\Price */ class FrontSpecialPrice extends Price @@ -70,7 +70,7 @@ public function __construct( /** * @inheritdoc * - * @deprecated + * @deprecated 103.0.2 */ protected function _applySpecialPrice($product, $finalPrice) { diff --git a/app/code/Magento/Catalog/Model/Product/Type/Price.php b/app/code/Magento/Catalog/Model/Product/Type/Price.php index e702965270639..74a6c7f634f81 100644 --- a/app/code/Magento/Catalog/Model/Product/Type/Price.php +++ b/app/code/Magento/Catalog/Model/Product/Type/Price.php @@ -484,6 +484,7 @@ public function getTierPriceCount($product) * @param Product $product * * @return array|float + * @since 102.0.6 */ public function getFormattedTierPrice($qty, $product) { @@ -509,7 +510,7 @@ public function getFormattedTierPrice($qty, $product) * * @return array|float * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedTierPrice() */ public function getFormatedTierPrice($qty, $product) @@ -522,6 +523,7 @@ public function getFormatedTierPrice($qty, $product) * * @param Product $product * @return array|float + * @since 102.0.6 */ public function getFormattedPrice($product) { @@ -534,7 +536,7 @@ public function getFormattedPrice($product) * @param Product $product * @return array || float * - * @deprecated + * @deprecated 102.0.6 * @see getFormattedPrice() */ public function getFormatedPrice($product) diff --git a/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php b/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php index 91570b58b7328..521d53629d99c 100644 --- a/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php +++ b/app/code/Magento/Catalog/Model/ProductIdLocatorInterface.php @@ -8,7 +8,7 @@ /** * Product ID locator provides all product IDs by SKU. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductIdLocatorInterface { @@ -17,7 +17,7 @@ interface ProductIdLocatorInterface * * @param array $skus * @return array - * @since 101.1.0 + * @since 102.0.0 */ public function retrieveProductIdsBySkus(array $skus); } diff --git a/app/code/Magento/Catalog/Model/ProductLink/Repository.php b/app/code/Magento/Catalog/Model/ProductLink/Repository.php index 960044efbc2ec..7a0b224a6153a 100644 --- a/app/code/Magento/Catalog/Model/ProductLink/Repository.php +++ b/app/code/Magento/Catalog/Model/ProductLink/Repository.php @@ -54,14 +54,14 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface /** * @var CollectionProvider - * @deprecated Not used anymore. + * @deprecated 103.0.4 Not used anymore. * @see query */ protected $entityCollectionProvider; /** * @var LinksInitializer - * @deprecated Not used. + * @deprecated 103.0.4 Not used. */ protected $linkInitializer; @@ -77,14 +77,14 @@ class Repository implements \Magento\Catalog\Api\ProductLinkRepositoryInterface /** * @var ProductLinkInterfaceFactory - * @deprecated Not used anymore, search delegated. + * @deprecated 103.0.4 Not used anymore, search delegated. * @see getList() */ protected $productLinkFactory; /** * @var ProductLinkExtensionFactory - * @deprecated Not used anymore, search delegated. + * @deprecated 103.0.4 Not used anymore, search delegated. * @see getList() */ protected $productLinkExtensionFactory; diff --git a/app/code/Magento/Catalog/Model/ProductNotFoundPageCacheTags.php b/app/code/Magento/Catalog/Model/ProductNotFoundPageCacheTags.php new file mode 100644 index 0000000000000..685e9a69a0f8a --- /dev/null +++ b/app/code/Magento/Catalog/Model/ProductNotFoundPageCacheTags.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Add product identities to "noroute" page + * + * Ensure that "noroute" page has necessary product tags + * so it can be invalidated once the product becomes visible again + */ +class ProductNotFoundPageCacheTags implements PageCacheTagsPreprocessorInterface +{ + private const NOROUTE_ACTION_NAME = 'cms_noroute_index'; + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + * @param ProductRepositoryInterface $productRepository + * @param StoreManagerInterface $storeManager + */ + public function __construct( + RequestInterface $request, + ProductRepositoryInterface $productRepository, + StoreManagerInterface $storeManager + ) { + $this->productRepository = $productRepository; + $this->storeManager = $storeManager; + $this->request = $request; + } + + /** + * @inheritDoc + */ + public function process(array $tags): array + { + if ($this->request->getFullActionName() === self::NOROUTE_ACTION_NAME) { + try { + $productId = (int) $this->request->getParam('id'); + $product = $this->productRepository->getById( + $productId, + false, + $this->storeManager->getStore()->getId() + ); + } catch (NoSuchEntityException $e) { + $product = null; + } + if ($product) { + $tags = array_merge($tags, $product->getIdentities()); + } + } + return $tags; + } +} diff --git a/app/code/Magento/Catalog/Model/ProductRepository.php b/app/code/Magento/Catalog/Model/ProductRepository.php index d656a0a9ac5b4..59a92656abf84 100644 --- a/app/code/Magento/Catalog/Model/ProductRepository.php +++ b/app/code/Magento/Catalog/Model/ProductRepository.php @@ -122,14 +122,14 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $fileSystem; /** - * @deprecated + * @deprecated 103.0.2 * * @var ImageContentInterfaceFactory */ protected $contentFactory; /** - * @deprecated + * @deprecated 103.0.2 * * @var ImageProcessorInterface */ @@ -141,7 +141,7 @@ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterfa protected $extensionAttributesJoinProcessor; /** - * @deprecated + * @deprecated 103.0.2 * * @var \Magento\Catalog\Model\Product\Gallery\Processor */ @@ -404,7 +404,7 @@ private function assignProductToWebsites(\Magento\Catalog\Model\Product $product /** * Process new gallery media entry. * - * @deprecated + * @deprecated 103.0.2 * @see MediaGalleryProcessor::processNewMediaGalleryEntry() * * @param ProductInterface $product @@ -669,7 +669,7 @@ private function addExtensionAttributes(Collection $collection) : Collection /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup * @param Collection $collection * @return void @@ -728,7 +728,7 @@ private function getMediaGalleryProcessor() /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php index fab2441db26c9..939f9d354af85 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/AggregateCount.php @@ -8,11 +8,15 @@ use Magento\Catalog\Model\Category; /** + * Aggregate count for parent category after deleting child category + * * Class AggregateCount */ class AggregateCount { /** + * Reduces children count for parent categories + * * @param Category $category * @return void */ @@ -25,9 +29,7 @@ public function processDelete(Category $category) */ $parentIds = $category->getParentIds(); if ($parentIds) { - $childDecrease = $category->getChildrenCount() + 1; - // +1 is itself - $data = ['children_count' => new \Zend_Db_Expr('children_count - ' . $childDecrease)]; + $data = ['children_count' => new \Zend_Db_Expr('children_count - 1')]; $where = ['entity_id IN(?)' => $parentIds]; $resourceModel->getConnection()->update($resourceModel->getEntityTable(), $data, $where); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php index 4711828d8f78d..351e3314c9fb4 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Collection.php @@ -456,6 +456,7 @@ public function addRootLevelFilter() * Add navigation max depth filter * * @return $this + * @since 103.0.0 */ public function addNavigationMaxDepthFilter() { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php index 05950531e2178..cd1b8c8924552 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Category/Flat.php @@ -69,7 +69,7 @@ class Flat extends \Magento\Indexer\Model\ResourceModel\AbstractResource * Category collection factory * * @var \Magento\Catalog\Model\ResourceModel\Category\CollectionFactory - * @deprecated 100.0.12 + * @deprecated 100.0.2 */ protected $_categoryCollectionFactory; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php index e1c90017327cd..d1769ded93d29 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Eav/Attribute.php @@ -258,6 +258,7 @@ public function afterSave() * Is attribute enabled for flat indexing * * @return bool + * @since 103.0.0 */ public function isEnabledInFlat() { @@ -874,7 +875,7 @@ public function __wakeup() /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function setIsUsedInGrid($isUsedInGrid) { @@ -884,7 +885,7 @@ public function setIsUsedInGrid($isUsedInGrid) /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function setIsVisibleInGrid($isVisibleInGrid) { @@ -894,7 +895,7 @@ public function setIsVisibleInGrid($isVisibleInGrid) /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function setIsFilterableInGrid($isFilterableInGrid) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php index 585da2af529a4..ab9f0e76854a6 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Layer/Filter/Price.php @@ -410,6 +410,7 @@ protected function _construct() /** * {@inheritdoc} * @return string + * @since 102.0.6 */ public function getMainTable() { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product.php b/app/code/Magento/Catalog/Model/ResourceModel/Product.php index c5587d3b25665..c3f1ee28b19fe 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product.php @@ -180,7 +180,7 @@ public function getProductWebsiteTable() /** * Product Category table name getter * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return string */ public function getProductCategoryTable() @@ -204,7 +204,7 @@ protected function _getDefaultAttributes() /** * Retrieve product website identifiers * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @param \Magento\Catalog\Model\Product|int $product * @return array */ @@ -379,7 +379,7 @@ public function delete($object) /** * Save product website relations * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @param \Magento\Catalog\Model\Product $product * @return $this */ @@ -408,7 +408,7 @@ protected function _saveWebsiteIds($product) * @param DataObject $object * @return $this * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected function _saveCategories(DataObject $object) { @@ -776,7 +776,7 @@ private function getEntityManager() /** * Retrieve ProductWebsiteLink instance. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return ProductWebsiteLink */ private function getProductWebsiteLink() @@ -787,7 +787,7 @@ private function getProductWebsiteLink() /** * Retrieve CategoryLink instance. * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return Product\CategoryLink */ private function getProductCategoryLink() @@ -805,7 +805,7 @@ private function getProductCategoryLink() * Store id is required to correctly identify attribute value we are working with. * * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ protected function getAttributeRow($entity, $object, $attribute) { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php index cf5760b0c33a9..8d03eb3ccafc9 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/CategoryLink.php @@ -147,7 +147,7 @@ private function processCategoryLinks($newCategoryPositions, &$oldCategoryPositi * @param bool $insert * @return array */ - private function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false) + public function updateCategoryLinks(ProductInterface $product, array $insertLinks, $insert = false) { if (empty($insertLinks)) { return []; diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php index 6e1efbc9db003..0cc3090100e8b 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Collection.php @@ -731,6 +731,7 @@ protected function _afterLoad() * Add Store ID to products from collection. * * @return $this + * @since 102.0.8 */ protected function prepareStoreId() { @@ -2220,7 +2221,7 @@ public function addTierPriceData() * * @param int $customerGroupId * @return $this - * @since 101.1.0 + * @since 102.0.0 */ public function addTierPriceDataByGroupId($customerGroupId) { @@ -2400,7 +2401,7 @@ function ($item) use ($linkField) { * Get product entity metadata * * @return \Magento\Framework\EntityManager\EntityMetadataInterface - * @since 101.1.0 + * @since 102.0.0 */ public function getProductEntityMetadata() { diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductBatchSizeAdjusterInterface.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductBatchSizeAdjusterInterface.php index c0e8858b4cfa8..2d282c5bf9741 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductBatchSizeAdjusterInterface.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/CompositeProductBatchSizeAdjusterInterface.php @@ -9,7 +9,7 @@ /** * Correct batch size according to number of composite related items. * @api - * @since 101.1.0 + * @since 102.0.0 */ interface CompositeProductBatchSizeAdjusterInterface { @@ -18,7 +18,7 @@ interface CompositeProductBatchSizeAdjusterInterface * * @param int $batchSize * @return int - * @since 101.1.0 + * @since 102.0.0 */ public function adjust($batchSize); } diff --git a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php index b64cca4ff1b26..747b06266cce0 100644 --- a/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php +++ b/app/code/Magento/Catalog/Model/ResourceModel/Product/Indexer/Price/DefaultPrice.php @@ -18,7 +18,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 - * @deprecated Not used anymore for price indexation. Class left for backward compatibility + * @deprecated 102.0.6 Not used anymore for price indexation. Class left for backward compatibility * @see DimensionalIndexerInterface */ class DefaultPrice extends AbstractIndexer implements PriceInterface @@ -240,7 +240,7 @@ protected function _getDefaultFinalPriceTable() * Prepare final price temporary index table * * @return $this - * @deprecated + * @deprecated 102.0.5 * @see prepareFinalPriceTable() */ protected function _prepareDefaultFinalPriceTable() diff --git a/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php b/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php index 0b49ef8796ce6..d398c9c14787f 100644 --- a/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php +++ b/app/code/Magento/Catalog/Model/System/Config/Backend/Catalog/Url/Rewrite/Suffix.php @@ -98,7 +98,7 @@ public function __construct( * Get instance of ScopePool * * @return \Magento\Framework\App\Config - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ private function getAppConfig() { @@ -140,7 +140,7 @@ public function afterSave() /** * {@inheritdoc} - * @since 101.1.0 + * @since 102.0.0 */ public function afterDeleteCommit() { diff --git a/app/code/Magento/Catalog/Model/Template/Filter.php b/app/code/Magento/Catalog/Model/Template/Filter.php index 8cd61415b958a..0a46af3ef021d 100644 --- a/app/code/Magento/Catalog/Model/Template/Filter.php +++ b/app/code/Magento/Catalog/Model/Template/Filter.php @@ -14,8 +14,6 @@ /** * Work with catalog(store, website) urls - * - * @package Magento\Catalog\Model\Template */ class Filter extends \Magento\Framework\Filter\Template { @@ -30,6 +28,7 @@ class Filter extends \Magento\Framework\Filter\Template * Whether to allow SID in store directive: NO * * @var bool + * @deprecated SID query parameter is not used in URLs anymore. */ protected $_useSessionInUrl = false; @@ -81,10 +80,14 @@ public function setUseAbsoluteLinks($flag) * * @param bool $flag * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @deprecated SID query parameter is not used in URLs anymore. */ public function setUseSessionInUrl($flag) { - $this->_useSessionInUrl = $flag; + // phpcs:disable Magento2.Functions.DiscouragedFunction + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return $this; } @@ -126,6 +129,7 @@ public function viewDirective($construction) */ public function mediaDirective($construction) { + // phpcs:disable Magento2.Functions.DiscouragedFunction $params = $this->getParameters(html_entity_decode($construction[2], ENT_QUOTES)); return $this->_storeManager->getStore() ->getBaseUrl(\Magento\Framework\UrlInterface::URL_TYPE_MEDIA) . $params['url']; diff --git a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php b/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php deleted file mode 100644 index b3f3ac7bf68ef..0000000000000 --- a/app/code/Magento/Catalog/Pricing/Price/CalculateCustomOptionCatalogRule.php +++ /dev/null @@ -1,119 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Pricing\Price; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\PriceModifierInterface; -use Magento\CatalogRule\Pricing\Price\CatalogRulePrice; -use Magento\Framework\Pricing\Price\BasePriceProviderInterface; -use Magento\Framework\Pricing\PriceCurrencyInterface; - -/** - * Calculates prices of custom options of the product with catalog rules applied. - */ -class CalculateCustomOptionCatalogRule -{ - /** - * @var PriceCurrencyInterface - */ - private $priceCurrency; - - /** - * @var PriceModifierInterface - */ - private $priceModifier; - - /** - * @param PriceCurrencyInterface $priceCurrency - * @param PriceModifierInterface $priceModifier - */ - public function __construct( - PriceCurrencyInterface $priceCurrency, - PriceModifierInterface $priceModifier - ) { - $this->priceModifier = $priceModifier; - $this->priceCurrency = $priceCurrency; - } - - /** - * Calculate prices of custom options of the product with catalog rules applied. - * - * @param Product $product - * @param float $optionPriceValue - * @param bool $isPercent - * @return float - */ - public function execute( - Product $product, - float $optionPriceValue, - bool $isPercent - ): float { - $regularPrice = (float)$product->getPriceInfo() - ->getPrice(RegularPrice::PRICE_CODE) - ->getValue(); - $catalogRulePrice = $this->priceModifier->modifyPrice( - $regularPrice, - $product - ); - $basePriceWithOutCatalogRules = (float)$this->getGetBasePriceWithOutCatalogRules($product); - // Apply catalog price rules to product options only if catalog price rules are applied to product. - if ($catalogRulePrice < $basePriceWithOutCatalogRules) { - $optionPrice = $this->getOptionPriceWithoutPriceRule($optionPriceValue, $isPercent, $regularPrice); - $totalCatalogRulePrice = $this->priceModifier->modifyPrice( - $regularPrice + $optionPrice, - $product - ); - $finalOptionPrice = $totalCatalogRulePrice - $catalogRulePrice; - } else { - $finalOptionPrice = $this->getOptionPriceWithoutPriceRule( - $optionPriceValue, - $isPercent, - $this->getGetBasePriceWithOutCatalogRules($product) - ); - } - - return $this->priceCurrency->convertAndRound($finalOptionPrice); - } - - /** - * Get product base price without catalog rules applied. - * - * @param Product $product - * @return float - */ - private function getGetBasePriceWithOutCatalogRules(Product $product): float - { - $basePrice = null; - foreach ($product->getPriceInfo()->getPrices() as $price) { - if ($price instanceof BasePriceProviderInterface - && $price->getPriceCode() !== CatalogRulePrice::PRICE_CODE - && $price->getValue() !== false - ) { - $basePrice = min( - $price->getValue(), - $basePrice ?? $price->getValue() - ); - } - } - - return $basePrice ?? $product->getPrice(); - } - - /** - * Calculate option price without catalog price rule discount. - * - * @param float $optionPriceValue - * @param bool $isPercent - * @param float $basePrice - * @return float - */ - private function getOptionPriceWithoutPriceRule(float $optionPriceValue, bool $isPercent, float $basePrice): float - { - return $isPercent ? $basePrice * $optionPriceValue / 100 : $optionPriceValue; - } -} diff --git a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php index 6ec282e45a1a0..c1af0b41741df 100644 --- a/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/ConfiguredPrice.php @@ -65,7 +65,7 @@ public function setItem(ItemInterface $item) /** * Get value of configured options. * - * @deprecated ConfiguredOptions::getItemOptionsValue is used instead + * @deprecated 102.0.4 ConfiguredOptions::getItemOptionsValue is used instead * @return float */ protected function getOptionsValue(): float diff --git a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php index f250927889c29..1aa43a39af442 100644 --- a/app/code/Magento/Catalog/Pricing/Price/TierPrice.php +++ b/app/code/Magento/Catalog/Pricing/Price/TierPrice.php @@ -29,7 +29,7 @@ class TierPrice extends AbstractPrice implements TierPriceInterface, BasePricePr /** * @var Session - * @deprecated 101.1.0 + * @deprecated 102.0.0 */ protected $customerSession; diff --git a/app/code/Magento/Catalog/Setup/Patch/Data/UpdateProductDescriptionOrder.php b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateProductDescriptionOrder.php new file mode 100644 index 0000000000000..db113448eb238 --- /dev/null +++ b/app/code/Magento/Catalog/Setup/Patch/Data/UpdateProductDescriptionOrder.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Catalog\Setup\Patch\Data; + +use Magento\Catalog\Setup\CategorySetup; +use Magento\Catalog\Setup\CategorySetupFactory; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Reorder Short Description/Description Product Attributes + */ +class UpdateProductDescriptionOrder implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var CategorySetupFactory + */ + private $categorySetupFactory; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param CategorySetupFactory $categorySetupFactory + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + CategorySetupFactory $categorySetupFactory + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->categorySetupFactory = $categorySetupFactory; + } + + /** + * @inheritdoc + */ + public function apply() + { + /** @var CategorySetup $categorySetup */ + $categorySetup = $this->categorySetupFactory->create(['setup' => $this->moduleDataSetup]); + $entityTypeId = $categorySetup->getEntityTypeId(\Magento\Catalog\Model\Product::ENTITY); + + // Content + $categorySetup->updateAttribute( + $entityTypeId, + 'short_description', + 'frontend_label', + 'Short Description', + 100 + ); + $categorySetup->updateAttribute( + $entityTypeId, + 'description', + 'frontend_label', + 'Description', + 110 + ); + + return $this; + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return [ + UpdateMediaAttributesBackendTypes::class, + ]; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml new file mode 100644 index 0000000000000..020fb27063be7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminChangeCategoryNameActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string" defaultValue="{{_defaultCategory.name}}"/> + </arguments> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="updateCategoryName"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml new file mode 100644 index 0000000000000..14a7967422332 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminChangeCategoryNameOnStoreViewLevelActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminChangeCategoryNameOnStoreViewLevelActionGroup"> + <annotations> + <description>Updates the Category Name for proper Store View.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> + <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{categoryName}}" stepKey="changeNameField"/> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCategoryWithInactiveIncludeInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCategoryWithInactiveIncludeInMenuActionGroup.xml new file mode 100644 index 0000000000000..c407e9ba829d7 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateCategoryWithInactiveIncludeInMenuActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCategoryWithInactiveIncludeInMenuActionGroup" extends="CreateCategoryActionGroup"> + <annotations> + <description>EXTENDS: CreateCategory. Add "disableIncludeInMenuOption" step.</description> + </annotations> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + + <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenuOption" + after="seeCategoryPageTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateInactiveCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateInactiveCategoryActionGroup.xml new file mode 100644 index 0000000000000..b16ff59b91329 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminCreateInactiveCategoryActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> + <!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> +<actionGroup name="AdminCreateInactiveCategoryActionGroup" extends="CreateCategoryActionGroup"> + <annotations> + <description>EXTENDS: CreateCategory. Add "disableCategory" step.</description> + </annotations> + <arguments> + <argument name="categoryEntity" defaultValue="_defaultCategory"/> + </arguments> + + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory" + after="seeCategoryPageTitle"/> +</actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml new file mode 100644 index 0000000000000..bd7eb664819dd --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminEnableCategoryActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnableCategoryActionGroup"> + <annotations> + <description>Enable the category</description> + </annotations> + <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="enableCategory"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml index ca1303f180ca4..153227e462f32 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminOpenProductIndexPageActionGroup.xml @@ -8,6 +8,9 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminOpenProductIndexPageActionGroup"> + <annotations> + <description>Go to products grid page.</description> + </annotations> <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndexPage"/> <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSaveActionGroup.xml new file mode 100644 index 0000000000000..57d4a6c702c89 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductAttributeSaveActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductAttributeSaveActionGroup"> + <annotations> + <description>Clicks on Save button to save the attribute and check success message.</description> + </annotations> + + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" stepKey="waitForSaveButton"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="clickSaveButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the product attribute." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCatalogPageOpenActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCatalogPageOpenActionGroup.xml new file mode 100644 index 0000000000000..f25f73977bf4e --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductCatalogPageOpenActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductCatalogPageOpenActionGroup"> + <annotations> + <description>Goes to the Admin Product Catalog Page grid page.</description> + </annotations> + + <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> + <waitForPageLoad stepKey="waitForProductCatalogPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveActionGroup.xml new file mode 100644 index 0000000000000..3db88bf3054bf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductFormSaveActionGroup"> + <annotations> + <description>Click save button for saving product.</description> + </annotations> + + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <waitForPageLoad stepKey="waitForProductSaving"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveButtonClickActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveButtonClickActionGroup.xml new file mode 100644 index 0000000000000..cb481c43c5484 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductFormSaveButtonClickActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductFormSaveButtonClickActionGroup"> + <annotations> + <description>Click Save button of product form.</description> + </annotations> + + <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> + <waitForPageLoad stepKey="waitForProductSaved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridSectionClickFirstRowActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridSectionClickFirstRowActionGroup.xml new file mode 100644 index 0000000000000..da71e7816ef0f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminProductGridSectionClickFirstRowActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminProductGridSectionClickFirstRowActionGroup"> + <annotations> + <description>Click first row on the product grid page.</description> + </annotations> + + <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml index 956dc3bf6fa52..66e08b222b60e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSaveProductAttributeActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminSaveProductAttributeActionGroup"> <annotations> - <description>Clicks on Save button to save the attribute.</description> + <description>DEPRECATED. Use AdminProductAttributeSaveActionGroup instead. Clicks on Save button to save the attribute.</description> </annotations> <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml new file mode 100644 index 0000000000000..8ecef0df400be --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetManageStockConfigActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetManageStockConfigActionGroup"> + <annotations> + <description>Set "Manage Stock" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="{{value}}" + stepKey="setManageStockConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml new file mode 100644 index 0000000000000..0f6a8df1ebf8c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMaxAllowedQtyForProductActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetMaxAllowedQtyForProductActionGroup"> + <annotations> + <description>Fills in the "Maximum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="{{qty}}" + stepKey="fillMaxAllowedQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml new file mode 100644 index 0000000000000..abbfdacc15395 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetMinAllowedQtyForProductActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetMinAllowedQtyForProductActionGroup"> + <annotations> + <description>Fills in the "Minimum Qty Allowed in Shopping Cart" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="{{qty}}" + stepKey="fillMinAllowedQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml new file mode 100644 index 0000000000000..4ecfa0762db9f --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetNotifyBelowQtyValueActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetNotifyBelowQtyValueActionGroup"> + <annotations> + <description>Fills in the "Notify for Quantity Below" option in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="qty" type="string"/> + </arguments> + <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" + stepKey="uncheckNotifyBelowQtyCheckBox"/> + <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="{{qty}}" + stepKey="fillNotifyBelowQty"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup.xml new file mode 100644 index 0000000000000..37947ffca7c5d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup"> + <arguments> + <argument name="useInLayeredNavigationValue" type="string" defaultValue="Filterable (with results)"/> + </arguments> + <conditionalClick selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" dependentSelector="{{AttributePropertiesSection.useInLayeredNavigation}}" visible="false" stepKey="clickStoreFrontTab"/> + <waitForElementVisible selector="{{AttributePropertiesSection.useInLayeredNavigation}}" stepKey="waitForStorefrontTabLoad"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="{{useInLayeredNavigationValue}}" stepKey="selectUseInLayeredNavigationOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml new file mode 100644 index 0000000000000..7846689a8d643 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetQtyUsesDecimalsConfigActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetQtyUsesDecimalsConfigActionGroup"> + <annotations> + <description>Set "Qty Uses Decimals" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="value" type="string"/> + </arguments> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="{{value}}" + stepKey="setQtyUsesDecimalsConfig"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml new file mode 100644 index 0000000000000..98156eb1ad9b1 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSetStockStatusConfigActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetStockStatusConfigActionGroup"> + <annotations> + <description>Set "Stock status" config in 'Advanced Inventory' panel on the Admin Product creation/edit page.</description> + </annotations> + <arguments> + <argument name="stockStatus" type="string"/> + </arguments> + <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" + userInput="{{stockStatus}}" stepKey="selectStockStatus"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml index b859ed2ea5942..f94bec1789068 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitAdvancedInventoryFormActionGroup.xml @@ -16,5 +16,6 @@ </annotations> <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> + <waitForPageLoad stepKey="waitForProductPageToLoad"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml index 8905643658cd8..59958f4266084 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AdminSubmitCategoriesPopupActionGroup.xml @@ -14,5 +14,6 @@ </annotations> <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneButton" /> + <waitForPageLoad stepKey="waitForCategoryApply"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsInactiveActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsInactiveActionGroup.xml new file mode 100644 index 0000000000000..cec6d42fc2dc4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsInactiveActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminCategoryIsInactiveActionGroup"> + <annotations> + <description>Verify the category is disabled</description> + </annotations> + + <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="seeCategoryIsDisabled"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..3a75b0a3cd361 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsListedInCategoriesTreeActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminCategoryIsListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <seeElement selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="seeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml new file mode 100644 index 0000000000000..e0a98a8932d4d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup"> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(categoryName)}}" stepKey="doNotSeeCategoryInTree"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml new file mode 100644 index 0000000000000..84e14269d24c2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/AssertStorefrontCategoryCurrentPageIsNthActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertStorefrontCategoryCurrentPageIsNthActionGroup"> + <arguments> + <argument name="expectedPage" type="string"/> + </arguments> + + <grabTextFrom selector="{{StorefrontCategoryBottomToolbarSection.currentPage}}" stepKey="currentPageText"/> + <assertEquals stepKey="assertIsPageNth"> + <expectedResult type="string">{{expectedPage}}</expectedResult> + <actualResult type="variable">currentPageText</actualResult> + </assertEquals> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml index e1bf2dea21318..796577bf84b65 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SaveProductAttributeActionGroup"> <annotations> - <description>Clicks on Save. Validates that the Success Message is present.</description> + <description>DEPRECATED. Use AdminProductAttributeSaveActionGroup instead. Clicks on Save. Validates that the Success Message is present.</description> </annotations> <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml index 4da8232e8405d..660bd314c4bf3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SaveProductAttributeInUseActionGroup.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="SaveProductAttributeInUseActionGroup"> <annotations> - <description>Clicks on Save. Validates that the Success Message is present.</description> + <description>DEPRECATED. Use AdminProductAttributeSaveActionGroup instead. Clicks on Save. Validates that the Success Message is present.</description> </annotations> <waitForElementVisible selector="{{AttributePropertiesSection.Save}}" stepKey="waitForSaveButton"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml index e3370864e7f61..6dd7f45dd0e64 100644 --- a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/SearchProductGridByKeywordActionGroup.xml @@ -19,5 +19,6 @@ <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{keyword}}" stepKey="fillKeywordSearchField"/> <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearch"/> + <waitForPageLoad stepKey="waitForProductSearch"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml new file mode 100644 index 0000000000000..cead98091d268 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsNotShownInMenuActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup"> + <annotations> + <description>Validate that the Category is not present in menu on Frontend.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" + stepKey="doNotSeeCatergoryInStoreFront"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml new file mode 100644 index 0000000000000..c56a18b4895a4 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertCategoryNameIsShownInMenuActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertCategoryNameIsShownInMenuActionGroup"> + <annotations> + <description>Validate that the Category is present in menu on Frontend.</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryName)}}" + stepKey="seeCatergoryInStoreFront"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml new file mode 100644 index 0000000000000..65858be673dfa --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup"> + <dontSee userInput="Add to Wish List" selector="{{StorefrontProductPageSection.addToWishlist}}" stepKey="dontSeeElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProperUrlIsShownActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProperUrlIsShownActionGroup.xml new file mode 100644 index 0000000000000..6fb7f68f64320 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontAssertProperUrlIsShownActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertProperUrlIsShownActionGroup"> + <annotations> + <description>Validate that the URL path is correct</description> + </annotations> + <arguments> + <argument name="urlPath" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{urlPath}}" stepKey="checkUrl"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml new file mode 100644 index 0000000000000..5b7dd3026a905 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontClickOnProductFromSidebarCompareListActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClickOnProductFromSidebarCompareListActionGroup"> + <annotations> + <description>Click on the product item from the sidebar comparing list.</description> + </annotations> + + <arguments> + <argument name="product" type="entity"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontComparisonSidebarSection.ProductTitleByName((product.name)}}" stepKey="waitForAddedCompareProduct"/> + <click selector="{{StorefrontComparisonSidebarSection.ProductTitleByName((product.name))}}" stepKey="clickOnProductLink"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml new file mode 100644 index 0000000000000..4776c9d32a34d --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontNavigateCategoryNextPageActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontNavigateCategoryNextPageActionGroup"> + <annotations> + <description>Navigates storefront category next page from toolbar</description> + </annotations> + <scrollTo selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="scrollToNextButton"/> + <click selector="{{StorefrontCategoryBottomToolbarSection.nextPage}}" stepKey="clickOnNextPage"/> + <waitForPageLoad stepKey="waitForNextCategoryPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml new file mode 100644 index 0000000000000..4a403364a91e3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSwitchStoreActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="storeName" type="string"/> + </arguments> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickOnSwitchStoreButton"/> + <click selector="{{StorefrontFooterSection.storeLink(storeName)}}" stepKey="selectStoreToSwitchOn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml index c9b67e0db4398..1d6bb970ea4d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CatalogInventoryConfigData.xml @@ -30,4 +30,9 @@ <data key="label">No</data> <data key="value">0</data> </entity> + <entity name="CatalogInventoryOptionsOnlyXleftThreshold"> + <!-- Magento default value --> + <data key="path">cataloginventory/options/stock_threshold_qty</data> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml index daf4809a4781a..9639bc39b45f4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/CategoryData.xml @@ -265,4 +265,8 @@ <data key="level">0</data> <var key="parent_id" entityType="category" entityKey="id"/> </entity> + <entity name="AssignProductToCategory" type="category_product_link"> + <var key="category_id" entityKey="id" entityType="category"/> + <var key="sku" entityKey="sku" entityType="product"/> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml index 7aabedbf1c3f7..716c4b07d2f1d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/ProductData.xml @@ -284,6 +284,9 @@ <requiredEntity type="product_extension_attribute">EavStockItem</requiredEntity> <requiredEntity type="custom_attribute">CustomAttributeProductAttribute</requiredEntity> </entity> + <entity name="ApiSimpleProductWithCategory" type="product2" extends="ApiSimpleOne"> + <requiredEntity type="custom_attribute">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="ApiSimpleProductWithShortSKU" type="product2" extends="ApiSimpleOne"> <data key="sku" unique="suffix">pr</data> </entity> @@ -1249,6 +1252,20 @@ <requiredEntity type="product_extension_attribute">EavStock777</requiredEntity> <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> </entity> + <entity name="SimpleProduct_zero" type="product"> + <data key="sku" unique="suffix">testSku</data> + <data key="type_id">simple</data> + <data key="attribute_set_id">4</data> + <data key="visibility">4</data> + <data key="name" unique="suffix">testProductName</data> + <data key="price">0.00</data> + <data key="urlKey" unique="suffix">testurlkey</data> + <data key="status">1</data> + <data key="quantity">777</data> + <data key="weight">1</data> + <requiredEntity type="product_extension_attribute">EavStock777</requiredEntity> + <requiredEntity type="custom_attribute_array">CustomAttributeCategoryIds</requiredEntity> + </entity> <entity name="ApiSimpleOneQty10" type="product2"> <data key="sku" unique="suffix">api-simple-product</data> <data key="type_id">simple</data> diff --git a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml index c79756507794a..731754ef01959 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Data/TierPriceData.xml @@ -72,4 +72,12 @@ <data key="quantity">3</data> <var key="sku" entityType="product" entityKey="sku" /> </entity> + <entity name="TierProductPrice50PercentDiscount" type="catalogTierPrice"> + <data key="price">50</data> + <data key="price_type">discount</data> + <data key="website_id">0</data> + <data key="customer_group">ALL GROUPS</data> + <data key="quantity">1</data> + <var key="sku" entityType="product" entityKey="sku" /> + </entity> </entities> diff --git a/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml b/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml index ae491aefc10cf..c0a92b7e1d1ad 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Metadata/CategoryMeta.xml @@ -57,4 +57,12 @@ <operation name="DeleteCategory" dataType="category" type="delete" auth="adminOauth" url="/V1/categories/{id}" method="DELETE"> <contentType>application/json</contentType> </operation> + + <operation name="AssignProductToCategory" dataType="category_product_link" type="create" auth="adminOauth" url="/V1/categories/{id}/products" method="POST"> + <contentType>application/json</contentType> + <object key="productLink" dataType="category_product_link"> + <field key="sku">string</field> + <field key="category_id">string</field> + </object> + </operation> </operations> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml index fafae5d535546..4b4aa20300d14 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductContentSection.xml @@ -15,5 +15,6 @@ <element name="shortDescriptionTextArea" type="textarea" selector="#product_form_short_description"/> <element name="sectionHeaderIfNotShowing" type="button" selector="//div[@data-index='content']//div[contains(@class, '_hide')]"/> <element name="pageHeader" type="textarea" selector="//*[@class='page-header row']"/> + <element name="attributeInput" type="input" selector="input[name='product[{{attributeCode}}]']" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml index 26946692ce050..7a829a5475758 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductDescriptionWYSIWYGToolbarSection.xml @@ -8,7 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductDescriptionWYSIWYGToolbarSection"> - <element name="TinyMCE4" type="button" selector="//div[@id='editorproduct_form_description']//*[contains(@class,'mce-branding')]"/> + <element name="TinyMCE4" type="button" selector="div#editorproduct_form_description .mce-branding"/> <element name="showHideBtn" type="button" selector="#toggleproduct_form_description"/> <element name="InsertImageBtn" type="button" selector="#buttonsproduct_form_description > .scalable.action-add-image.plugin"/> <element name="Style" type="button" selector="//div[@id='editorproduct_form_description']//span[text()='Paragraph']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml index 544bdf85681c9..f4d5d20bcfb83 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductFormSection/ProductWYSIWYGSection.xml @@ -8,12 +8,13 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="ProductWYSIWYGSection"> - <element name="Switcher" type="button" selector="//select[@id='dropdown-switcher']"/> - <element name="v436" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.3.6']"/> - <element name="v3" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 3.6(Deprecated)']"/> + <element name="Switcher" type="button" selector="select#dropdown-switcher"/> + <element name="v436" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.3.6']" deprecated="New element was introduced. Please use 'ProductWYSIWYGSection.v4910'"/> + <element name="v3" type="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 3.6(Deprecated)']" deprecated="New element was introduced. Please use 'ProductWYSIWYGSection.v4910'"/> + <element name="v4910" type ="button" selector="//select[@id='dropdown-switcher']/option[text()='TinyMCE 4.9.10']" /> <element name="TinymceDescription3" type="button" selector="//span[text()='Description']"/> <element name="SaveConfig" type="button" selector="#save"/> <element name="v4" type="button" selector="#category_form_description_v4"/> - <element name="WYSIWYGBtn" type="button" selector=".//button[@class='action-default scalable action-wysiwyg']"/> + <element name="WYSIWYGBtn" type="button" selector="button.action-default.scalable.action-wysiwyg"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml index 8685e84a347f2..f2cd9f4de3570 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/AdminProductSEOSection.xml @@ -12,6 +12,7 @@ <element name="sectionHeader" type="button" selector="div[data-index='search-engine-optimization']" timeout="30"/> <element name="urlKeyInput" type="input" selector="input[name='product[url_key]']"/> <element name="useDefaultUrl" type="checkbox" selector="input[name='use_default[url_key]']"/> + <element name="urlKeyRedirectCheckbox" type="checkbox" selector="input[name='product[url_key_create_redirect]']"/> <element name="metaTitleInput" type="input" selector="input[name='product[meta_title]']"/> <element name="metaKeywordsInput" type="textarea" selector="textarea[name='product[meta_keyword]']"/> <element name="metaDescriptionInput" type="textarea" selector="textarea[name='product[meta_description]']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml index 09eb4ad954274..c27a6107e5e35 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategoryBottomToolbarSection.xml @@ -12,6 +12,6 @@ <element name="previousPage" type="button" selector=".//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'previous')]" timeout="30"/> <element name="pageNumber" type="text" selector="//*[@class='toolbar toolbar-products'][2]//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> <element name="perPage" type="select" selector="//*[@class='toolbar toolbar-products'][2]//select[@id='limiter']"/> - <element name="currentPage" type="text" selector=".products.wrapper + .toolbar-products .pages .current span:nth-of-type(2)"/> + <element name="currentPage" type="text" selector=".//*[@class='toolbar toolbar-products'][2]//li[contains(@class, 'current')]//span[2]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml index 186d0cf313d96..5ec493aef0cea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontCategorySidebarSection/StorefrontCategorySidebarSection.xml @@ -17,5 +17,7 @@ <element name="removeFilter" type="button" selector="div.filter-current .remove"/> <element name="activeFilterOptions" type="text" selector=".filter-options-item.active .items"/> <element name="activeFilterOptionItemByPosition" type="text" selector=".filter-options-item.active .items li:nth-child({{itemPosition}}) a" parameterized="true"/> + <element name="enabledFilterOptionItemByLabel" type="text" selector="//div[@class='filter-options']//li[@class='item']//a[contains(text(), '{{optionLabel}}')]" parameterized="true"/> + <element name="disabledFilterOptionItemByLabel" type="text" selector="//div[@class='filter-options']//li[@class='item' and contains(text(), '{{optionLabel}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml index 5ee754904b702..13ced1c0263e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductActionSection.xml @@ -10,6 +10,7 @@ <section name="StorefrontProductActionSection"> <element name="quantity" type="input" selector="#qty"/> <element name="addToCart" type="button" selector="#product-addtocart-button" timeout="60"/> + <element name="addToCartEnabledWithTranslation" type="button" selector="button#product-addtocart-button[data-translate]:enabled" timeout="60"/> <element name="addToCartButtonTitleIsAdding" type="text" selector="//button/span[text()='Adding...']"/> <element name="addToCartButtonTitleIsAdded" type="text" selector="//button/span[text()='Added']"/> <element name="addToCartButtonTitleIsAddToCart" type="text" selector="//button/span[text()='Add to Cart']"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml index 78818dd37a5d4..7be02126e3a0f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Section/StorefrontProductPageSection.xml @@ -25,5 +25,6 @@ <element name="customOptionDropDown" type="select" selector="//*[@id='product-options-wrapper']//select[contains(@class, 'product-custom-option admin__control-select')]"/> <element name="qtyInputWithProduct" type="input" selector="//tr//strong[contains(.,'{{productName}}')]/../../td[@class='col qty']//input" parameterized="true"/> <element name="customOptionRadio" type="input" selector="//span[contains(text(),'{{customOption}}')]/../../input" parameterized="true"/> + <element name="onlyProductsLeft" type="block" selector="//div[@class='product-info-price']//div[@class='product-info-stock-sku']//div[@class='availability only']"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml index e42dd8b8ab12e..92be79fdfe720 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddOutOfStockProductToCompareListTest.xml @@ -22,7 +22,9 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="SimpleSubCategory" stepKey="category"/> <createData entity="SimpleProduct4" stepKey="product"> <requiredEntity createDataKey="category"/> @@ -30,7 +32,9 @@ </before> <after> <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> @@ -47,8 +51,12 @@ <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockEnable.path}} {{CatalogInventoryOptionsShowOutOfStockEnable.value}}" stepKey="setConfigShowOutOfStockTrue"/> <!--Clear cache and reindex--> <comment userInput="Clear cache and reindex" stepKey="cleanCache"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> +</actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> +</actionGroup> <!--Open product page--> <comment userInput="Open product page" stepKey="openProductPage"/> <amOnPage url="{{StorefrontProductPage.url($$product.custom_attributes[url_key]$$)}}" stepKey="goToSimpleProductPage2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml index e00b3fe2994eb..b677fae5e58ea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AddToCartCrossSellTest.xml @@ -53,8 +53,7 @@ <actionGroup ref="AddCrossSellProductBySkuActionGroup" stepKey="addProduct3ToSimp1"> <argument name="sku" value="$simpleProduct3.sku$"/> </actionGroup> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <!-- Go to simpleProduct3, add simpleProduct1 and simpleProduct2 as cross-sell--> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProduct3"> @@ -68,8 +67,7 @@ <actionGroup ref="AddCrossSellProductBySkuActionGroup" stepKey="addProduct2ToSimp3"> <argument name="sku" value="$simpleProduct2.sku$"/> </actionGroup> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave2"/> - <waitForPageLoad stepKey="waitForPageLoad2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave2"/> <!-- Go to frontend, add simpleProduct1 to cart--> <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimp1ToCart"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml index 92f24fe76502d..2dc840b60f3b8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageSimpleProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml index 7cf388914207b..d1e292ff56444 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddDefaultImageVirtualProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="defaultVirtualProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml index 6eb4de39726f0..bcdf6ad39124a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGCatalogTest.xml @@ -32,8 +32,11 @@ <waitForPageLoad stepKey="waitForPageLoad" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> - <argument name="ImageFolder" value="ImageFolder"/> + <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> <actionGroup ref="AttachImageActionGroup" stepKey="attachImage1"> <argument name="Image" value="ImageUpload3"/> @@ -44,12 +47,16 @@ </actionGroup> <actionGroup ref="SaveImageActionGroup" stepKey="insertImage"/> <actionGroup ref="FillOutUploadImagePopupActionGroup" stepKey="fillOutUploadImagePopup" /> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCatalog"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCatalog"/> <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> <waitForPageLoad stepKey="waitForPageLoad2"/> <seeElement selector="{{StorefrontCategoryMainSection.mediaDescription(ImageUpload3.content)}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontCategoryMainSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <actionGroup ref="DeleteCategoryActionGroup" stepKey="DeleteCategory"> <argument name="categoryEntity" value="SimpleSubCategory"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml index d86a696880bae..acc22f2e611d6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddImageToWYSIWYGProductTest.xml @@ -32,6 +32,7 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillBasicProductInfo" /> <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab" /> + <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDescription" /> <waitForElementVisible selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="waitForDescription" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse1" /> @@ -67,7 +68,7 @@ <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.ImageDescription}}" userInput="{{ImageUpload1.content}}" stepKey="fillImageDescription1" /> <fillField selector="{{ProductDescriptionWYSIWYGToolbarSection.Height}}" userInput="{{ImageUpload1.height}}" stepKey="fillImageHeight1" /> <click selector="{{ProductDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="clickOkBtn1" /> - <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.TinyMCE4}}" stepKey="scrollToTinyMCE4" /> + <scrollTo selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToTinyMCE4" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageIcon}}" stepKey="clickInsertImageIcon2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.Browse}}" stepKey="clickBrowse2" /> <waitForLoadingMaskToDisappear stepKey="waitForLoading13"/> @@ -98,8 +99,7 @@ <fillField selector="{{ProductShortDescriptionWYSIWYGToolbarSection.Height}}" userInput="{{ImageUpload3.height}}" stepKey="fillImageHeight2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.OkBtn}}" stepKey="clickOkBtn2" /> <waitForPageLoad stepKey="waitForPageLoad6"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading12" /> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <amOnPage url="{{_defaultProduct.name}}.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForPageLoad7"/> <seeElement selector="{{StorefrontProductInfoMainSection.mediaDescription}}" stepKey="assertMediaDescription"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml index 52da8c70a3bc8..94d3b46aaa5f1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminAddInStockProductToTheCartTest.xml @@ -34,16 +34,14 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory setting --> <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> @@ -54,15 +52,19 @@ <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="In Stock" stepKey="selectOutOfStock"/> - <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> - <waitForPageLoad stepKey="waitForProductPageToLoad"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="In Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Clear cache and reindex--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Verify product is visible in category front page --> <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> @@ -74,13 +76,13 @@ <waitForPageLoad stepKey="waitForProductFrontPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="{{SimpleProduct.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> <!--Add Product to the cart--> <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> - <waitForPageLoad stepKey="waitForProductToAddInCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml index cbbd496e8cb34..1ed079b12d1fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductTest.xml @@ -31,8 +31,7 @@ <deleteData createDataKey="createSimpleUSCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> - <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex1"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> @@ -69,8 +68,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_2"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmount_2"/> <!--Case: Tier Price for General Customer Group--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex2"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct2"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -94,8 +92,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.productPriceLabel('Regular Price')}}" stepKey="assertRegularPriceLabel_4"/> <seeElement selector="{{StorefrontCategoryProductSection.productPriceOld('100')}}" stepKey="assertRegularPriceAmount_3"/> <!--Case: Tier Price applied if Product quantity meets Tier Price Condition--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex3"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex3"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct3"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -160,8 +157,7 @@ <actualResult type="variable">grabTextFromSubtotalField3</actualResult> </assertEquals> <!--Tier Price is changed in Shopping Cart and is changed on Product page if Tier Price parameters are changed in Admin--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex4"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoa4"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex4"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct4"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -225,8 +221,7 @@ <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceAmount('2', '75')}}" stepKey="assertProductTierPriceAmountForSecondRow2"/> <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('1', '10')}}" stepKey="assertProductTierPriceSavePercentageAmountForFirstRow2"/> <seeElement selector="{{StorefrontProductInfoMainSection.productTierPriceSavePercentageAmount('2', '25')}}" stepKey="assertProductTierPriceSavePercentageAmountForSecondRow2"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex5"/> - <waitForPageLoad time="30" stepKey="waitForProductPageToLoad3"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex5"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct5"> <argument name="product" value="$$createSimpleProduct$$"/> </actionGroup> @@ -246,7 +241,7 @@ <click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="closeAdvancedPricingPopup"/> <waitForElementVisible selector="{{AdminProductFormSection.productPrice}}" stepKey="waitForAdminProductFormSectionProductPriceInput"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="200" stepKey="fillProductPrice200"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage4"/> <grabTextFrom selector="{{CheckoutCartProductSection.productSubtotalByName($$createSimpleProduct.name$$)}}" stepKey="grabTextFromSubtotalField7"/> <assertEquals message="Shopping cart should contain subtotal $4,000" stepKey="assertSubtotalField7"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml index fd8c0ba29fdfa..45284e69a54e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminApplyTierPriceToProductTest/AdminApplyTierPriceToProductWithPercentageDiscountTest.xml @@ -27,8 +27,7 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad time="30" stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml index 96d0c209aba34..077765bd5c15a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminBackorderAllowedAddProductToCartTest.xml @@ -31,7 +31,9 @@ <!-- Set Magento back to default configuration --> <magentoCLI command="config:set {{CatalogInventoryOptionsShowOutOfStockDisable.path}} {{CatalogInventoryOptionsShowOutOfStockDisable.value}}" stepKey="setConfigShowOutOfStockFalse"/> <magentoCLI command="config:set {{CatalogInventoryItemOptionsBackordersDisable.path}} {{CatalogInventoryItemOptionsBackordersDisable.value}}" stepKey="setConfigAllowBackordersFalse"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml new file mode 100644 index 0000000000000..68e6040277247 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminChangeProductAttributeGroupTest.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminChangeProductAttributeGroupTest"> + <annotations> + <stories value="Preserving attribute value after attribute group is changed"/> + <title value="Preserving attribute value after attribute group is changed"/> + <description value="Attribute value should be preserved after changing attribute group"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-35612"/> + <useCaseId value="MC-31892"/> + <group value="catalog"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <createData entity="productAttributeText" stepKey="createProductAttribute"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="CatalogAttributeSet" stepKey="createSecondAttributeSet"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createAttributeSet.attribute_set_id$/" + stepKey="onAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createProductAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createSecondAttributeSet.attribute_set_id$/" + stepKey="onSecondAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToContentGroup"> + <argument name="group" value="Content"/> + <argument name="attribute" value="$createProductAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveSecondAttributeSet"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createSecondAttributeSet" stepKey="deleteSecondAttributeSet"/> + <actionGroup ref="ClearProductsFilterActionGroup" stepKey="clearProductsFilter"/> + + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForSimpleProduct"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct1"> + <argument name="product" value="$createSimpleProduct$"/> + </actionGroup> + + <actionGroup ref="AdminProductPageSelectAttributeSetActionGroup" stepKey="selectAttributeSet"> + <argument name="attributeSetName" value="$createAttributeSet.attribute_set_name$"/> + </actionGroup> + <waitForText userInput="$createProductAttribute.default_frontend_label$" stepKey="seeAttributeInForm"/> + <fillField selector="{{AdminProductFormSection.attributeRequiredInput($createProductAttribute.attribute_code$)}}" + userInput="test" + stepKey="fillProductAttributeValue"/> + <actionGroup ref="AdminProductPageSelectAttributeSetActionGroup" stepKey="selectSecondAttributeSet"> + <argument name="attributeSetName" value="$createSecondAttributeSet.attribute_set_name$"/> + </actionGroup> + <actionGroup ref="ExpandAdminProductSectionActionGroup" stepKey="expandContentSection"/> + <waitForText userInput="$createProductAttribute.default_frontend_label$" stepKey="seeAttributeInSection"/> + <grabValueFrom selector="{{AdminProductContentSection.attributeInput($createProductAttribute.attribute_code$)}}" + stepKey="attributeValue"/> + <assertEquals stepKey="assertAttributeValue"> + <expectedResult type="string">test</expectedResult> + <actualResult type="variable">attributeValue</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml index 39351539d14a6..a72af673c009a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckConfigurableProductPriceWithDisabledChildProductTest.xml @@ -131,7 +131,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct1.price$$" stepKey="seeInitialPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> <!-- Verify First Child Product attribute option is displayed --> @@ -146,8 +148,7 @@ <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct3.price$$" stepKey="seeChildProduct3PriceInStoreFront"/> <!-- Open Product Index Page and Filter First Child product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="ApiSimpleOne"/> </actionGroup> @@ -156,8 +157,7 @@ <!-- Disable the product --> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!-- Open Product Store Front Page --> @@ -168,7 +168,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigChildProduct2.price$$" stepKey="seeUpdatedProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront1"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront1"/> <!-- Verify product Attribute Option1 is not displayed --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml index b3f7f0e6eb42a..0bdf19c9b8950 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckCustomAttributeValuesAfterProductSaveTest.xml @@ -33,7 +33,9 @@ </createData> <!-- Create simple product --> <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="reindexCatalogSearch"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCatalogSearch"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> <!-- Login to Admin page --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index de8110f995606..b52d18f3c0203 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveAndNotIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -32,16 +32,14 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Verify Parent Category and Sub category is not visible in navigation menu --> <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml index fd8093d8d3b52..a4a42a9999a5c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveCategoryAndSubcategoryIsNotVisibleInNavigationMenuTest.xml @@ -31,16 +31,14 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Verify Parent Category and Sub category is not visible in navigation menu --> <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml index e6cbe156698e7..99fd1cf3caf3f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckInactiveIncludeInMenuCategoryAndSubcategoryIsNotVisibleInNavigationTest.xml @@ -32,16 +32,14 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Verify Parent Category and Sub category is not visible in navigation menu --> <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml index a94610abf0918..c15cedadb4460 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsNotVisibleInCategoryTest.xml @@ -34,42 +34,47 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory Setting --> - <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuatityUsesDecimal"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> - <waitForPageLoad stepKey="waitForProductPageToSave"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminFillAdvancedInventoryQtyActionGroup" stepKey="fillProductQty"> + <argument name="qty" value="5"/> + </actionGroup> + <actionGroup ref="AdminSetMinAllowedQtyForProductActionGroup" stepKey="fillMiniAllowedQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetMaxAllowedQtyForProductActionGroup" stepKey="fillMaxAllowedQty"> + <argument name="qty" value="1000"/> + </actionGroup> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetNotifyBelowQtyValueActionGroup" stepKey="fillNotifyBelowQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Verify product is not visible in category store front page --> - <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> - <dontSee selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="dontSeeProductInCategoryPage"/> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductInCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="{{SimpleProduct.name}}"/> + </actionGroup> <!--Verify Product In Store Front--> - <amOnPage url="$$createSimpleProduct.name$$.html" stepKey="goToProductStorefrontPage"/> - <waitForPageLoad stepKey="waitForProductPageTobeLoaded"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="Out of stock" stepKey="seeProductStatusIsOutOfStock"/> + <actionGroup ref="StorefrontCheckProductStockStatus" stepKey="seeProductOnStorefront"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="stockStatus" value="Out of stock"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml index e64707a895fd4..9c1ff43587a27 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckOutOfStockProductIsVisibleInCategoryTest.xml @@ -37,41 +37,53 @@ <magentoCLI stepKey="setDisplayOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0" /> </after> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> <!-- Update product Advanced Inventory Setting --> - <click stepKey="openSelectedProduct" selector="{{AdminProductGridSection.productRowBySku($$createSimpleProduct.sku$$)}}"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageToLoad"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="uncheckConfigSetting"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="Yes" stepKey="clickOnManageStock"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryQty}}" userInput="5" stepKey="fillProductQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.miniQtyConfigSetting}}" stepKey="uncheckMiniQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.miniQtyAllowedInCart}}" userInput="1" stepKey="fillMiniAllowedQty"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.maxiQtyConfigSetting}}" stepKey="uncheckMaxQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.maxiQtyAllowedInCart}}" userInput="10000" stepKey="fillMaxAllowedQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" userInput="Yes" stepKey="selectQuantityUsesDecimal"/> - <uncheckOption selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQtyConfigSetting}}" stepKey="uncheckNotifyBelowQtyCheckBox"/> - <fillField selector="{{AdminProductFormAdvancedInventorySection.notifyBelowQty}}" userInput="1" stepKey="fillNotifyBelowQty"/> - <selectOption selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryStockStatus}}" userInput="Out of Stock" stepKey="selectOutOfStock"/> - <click stepKey="clickOnDoneButton" selector="{{AdminProductFormAdvancedInventorySection.doneButton}}"/> - <waitForPageLoad stepKey="waitForProductPageToLoad"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> - <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> - + <actionGroup ref="OpenProductForEditByClickingRowXColumnYInProductGridActionGroup" stepKey="openProduct"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminSetManageStockConfigActionGroup" stepKey="setManageStockConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminFillAdvancedInventoryQtyActionGroup" stepKey="fillProductQty"> + <argument name="qty" value="5"/> + </actionGroup> + <actionGroup ref="AdminSetMinAllowedQtyForProductActionGroup" stepKey="fillMiniAllowedQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetMaxAllowedQtyForProductActionGroup" stepKey="fillMaxAllowedQty"> + <argument name="qty" value="1000"/> + </actionGroup> + <actionGroup ref="AdminSetQtyUsesDecimalsConfigActionGroup" stepKey="setQtyUsesDecimalsConfig"> + <argument name="value" value="Yes"/> + </actionGroup> + <actionGroup ref="AdminSetNotifyBelowQtyValueActionGroup" stepKey="fillNotifyBelowQty"> + <argument name="qty" value="1"/> + </actionGroup> + <actionGroup ref="AdminSetStockStatusConfigActionGroup" stepKey="selectOutOfStock"> + <argument name="stockStatus" value="Out of Stock"/> + </actionGroup> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify product is visible in category front page --> - <amOnPage url="$$createCategory.name$$.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryInFrontPage"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="clickOnCategory"/> - <see selector="{{StorefrontCategoryMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInCategoryPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="selectCategory"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeProductName"> + <argument name="productName" value="{{SimpleProduct.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml index 6ac71c4a7982d..3fff2c118ae6d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckPaginationInStorefrontTest.xml @@ -100,8 +100,7 @@ <!--Open Category Page and select created category--> <comment userInput="Open Category Page and select created category" stepKey="commentOpenCategoryPage"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForPageToLoad0"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForPageToLoaded2"/> <!--Select Products--> @@ -120,8 +119,7 @@ <waitForPageLoad stepKey="waitFroPageToLoad2"/> <see selector="{{AdminProductGridFilterSection.productCount}}" userInput="30" stepKey="seeNumberOfProductsFound"/> <click selector="{{AdminCategoryProductsGridSection.productSelectAll}}" stepKey="selectSelectAll"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> <!--Open Category Store Front Page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml index 192bab7c6d126..2cdec1405e9f9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCheckSubCategoryIsNotVisibleInNavigationMenuTest.xml @@ -31,21 +31,19 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create subcategory under parent category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createCategory.name$$)}}" stepKey="selectCategory"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> <!-- Verify Parent Category is visible in navigation menu and Sub category is not visible in navigation menu --> - <amOnPage url="$$createCategory.name_lwr$$/{{SimpleSubCategory.name_lwr}}.html" stepKey="openCategoryStoreFrontPage"/> - <waitForPageLoad stepKey="waitForCategoryStoreFrontPageToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryOnStoreNavigationBar"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryOnStoreNavigation"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCategoryOnStoreNavigationBar"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeSubCategoryOnStoreNavigation"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml index 65e67020e4532..3c7900f37d36f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToVirtualTest.xml @@ -54,7 +54,11 @@ <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="searchForProduct"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Virtual Product"/> + </actionGroup> <actionGroup ref="AssertProductInStorefrontProductPageActionGroup" stepKey="AssertProductInStorefrontProductPage"> <argument name="product" value="_defaultProduct"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml index f79072582035b..7191f1971b319 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToSimpleTest.xml @@ -25,6 +25,10 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillProductForm"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Simple Product"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml index 04d032511ded0..9b9f7cf468985 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateAttributeSetEntityTest.xml @@ -57,8 +57,7 @@ <see userInput="$$createProductAttribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="seeAttributeInGroup2"/> <!-- Assert attribute can be used in product creation --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForPageLoad3"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml index b94c12d1d7a39..dba1ea040c825 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryFromProductPageTest.xml @@ -27,20 +27,18 @@ <after> <!-- Delete the created category --> <actionGroup ref="DeleteMostRecentCategoryActionGroup" stepKey="getRidOfCreatedCategory"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> </after> <!-- Find the product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="SimpleTwo"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Fill out the form for the new category --> <actionGroup ref="FillNewProductCategoryActionGroup" stepKey="FillNewProductCategory"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml index b8e58eae8a98a..83404391abca9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryTest/AdminCreateCategoryTest.xml @@ -23,16 +23,13 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategory"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="enterCategoryName"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSEO"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="enterURLKey"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategory"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccess"/> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> - <!-- Literal URL below, need to refactor line + StorefrontCategoryPage when support for variable URL is implemented--> - <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> - <seeInTitle userInput="{{SimpleSubCategory.name}}" stepKey="assertTitle"/> - <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{SimpleSubCategory.name_lwr}}" stepKey="assertInfo1"/> + <!--Go to storefront and verify created category on frontend--> + <actionGroup ref="CheckCategoryOnStorefrontActionGroup" stepKey="checkCreatedCategoryOnFrontend"> + <argument name="categoryEntity" value="SimpleSubCategory"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml index 4c1993eb803b3..3332bc66653e5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithAnchorFieldTest.xml @@ -57,8 +57,7 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> <!--Verify the Category Title--> @@ -72,8 +71,12 @@ </actionGroup> <!--Clear cache and reindex--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Verify Product in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name_lwr)}}" stepKey="amOnCategoryPage"/> <waitForPageLoad stepKey="waitForPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml index 4b0774d2307dd..e66984dda4427 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithCustomRootCategoryTest.xml @@ -21,10 +21,8 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> </before> <after> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> - <waitForPageLoad stepKey="waitStoreIndexPageLoad" /> <actionGroup ref="DeleteCustomStoreActionGroup" stepKey="deleteCustomStore"> - <argument name="storeGroupName" value="customStore.name"/> + <argument name="storeGroupName" value="customStoreGroup.name"/> </actionGroup> <actionGroup ref="DeleteCategoryActionGroup" stepKey="deleteCreatedNewRootCategory"> <argument name="categoryEntity" value="NewRootCategory"/> @@ -37,39 +35,32 @@ <argument name="categoryEntity" value="NewRootCategory"/> </actionGroup> <!--Create subcategory--> - <scrollToTopOfPage stepKey="scrollToTopOfPage"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name)}}" stepKey="clickOnCreatedNewRootCategory"/> - <scrollToTopOfPage stepKey="scrollToTopOfPage1"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedCategory"> + <argument name="Category" value="NewRootCategory"/> + </actionGroup> + <actionGroup ref="CreateCategoryActionGroup" stepKey="createSubcategory"> <argument name="categoryEntity" value="SimpleSubCategory"/> </actionGroup> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go to store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front page--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectCustomStore"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeSubCategoryInStoreFrontPage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToCustomStore"> + <argument name="storeName" value="{{customStoreGroup.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml index b27d9239c53e1..f61a97219903f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithFiveNestingTest.xml @@ -30,7 +30,7 @@ <click selector="{{AdminCategoryModalSection.ok}}" stepKey="confirmDelete"/> <waitForPageLoad time="60" stepKey="waitForDeleteToFinish"/> <see selector="You deleted the category." stepKey="seeDeleteSuccess"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandToSeeAllCategories"/> <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="dontSeeCategoryInTree"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> @@ -40,36 +40,31 @@ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillFirstSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstSubCategory"/> - <waitForPageLoad stepKey="waitForSFirstSubCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFirstSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!--Create Nested Second Sub Category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSecondSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage1"/> <!--Create Nested Third Sub Category/>--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton2"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{ThirdLevelSubCat.name}}" stepKey="fillThirdSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveThirdSubCategory"/> - <waitForPageLoad stepKey="waitForThirdCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveThirdSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage2"/> <!--Create Nested fourth Sub Category />--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton3"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FourthLevelSubCat.name}}" stepKey="fillFourthSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFourthSubCategory"/> - <waitForPageLoad stepKey="waitForFourthCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFourthSubCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage3"/> <!--Create Nested fifth Sub Category />--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton4"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FifthLevelCat.name}}" stepKey="fillFifthSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFifthLevelCategory"/> - <waitForPageLoad stepKey="waitForFifthCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFifthLevelCategory"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage4"/> <amOnPage url="/{{FirstLevelSubCat.name}}/{{SecondLevelSubCat.name}}/{{ThirdLevelSubCat.name}}/{{FourthLevelSubCat.name}}/{{FifthLevelCat.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml index a7dab57173377..6d7d56861b731 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveCategoryTest.xml @@ -26,20 +26,12 @@ </after> <!-- Create In active Category --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.IncludeInMenu}}" stepKey="enableIncludeInMenu"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> - <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> - <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> - <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> - <!--Verify InActive Category is created--> - <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> + <actionGroup ref="AdminCreateInactiveCategoryActionGroup" stepKey="createInactiveCategory"/> + <actionGroup ref="AssertAdminCategoryIsInactiveActionGroup" stepKey="seeDisabledCategory"/> <!--Verify Category is not listed store front page--> - <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> - <waitForPageLoad stepKey="waitForPageToBeLoaded"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnStoreFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStoreFront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeCategoryNameInMenu"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml index d3a766be2c99f..f60312f19a7e0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithInactiveIncludeInMenuTest.xml @@ -26,21 +26,11 @@ </after> <!--Create Category with not included in menu Subcategory --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> - <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIncludeInMenu"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> - <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> - <waitForPageLoad stepKey="waitForPageSaved"/> - <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> - <!--Verify Category is created/>--> - <seeElement selector="{{AdminCategoryContentSection.activeCategoryInTree(_defaultCategory.name)}}" stepKey="seeCategoryInTree" /> + <actionGroup ref="AdminCreateCategoryWithInactiveIncludeInMenuActionGroup" stepKey="createNotIncludedInMenuCategory"/> <!--Verify Category in store front page menu/>--> - <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="amOnCategoryPage"/> - <waitForPageLoad stepKey="waitForPageToBeLoaded"/> - <see selector="{{StorefrontCategoryMainSection.CategoryTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="dontSeeCategoryOnNavigation"/> + <actionGroup ref="CheckCategoryOnStorefrontActionGroup" stepKey="CheckCategoryOnStorefront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeCategoryOnNavigation"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml index 3273fb62e7d9c..4173254c66fc3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithNoAnchorFieldTest.xml @@ -59,8 +59,7 @@ <actionGroup ref="AdminAddProductToCategoryActionGroup" stepKey="addProductToCategory"> <argument name="product" value="$$simpleProduct$$"/> </actionGroup> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml index 0b269749c5dd6..21256342986ba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithProductsGridFilterTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="NavigateToAndResetProductGridToDefaultViewActionGroup" stepKey="NavigateToAndResetProductGridToDefaultView"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <!--Create Default Product--> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddDefaultProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillDefaultProductName"/> @@ -41,12 +40,10 @@ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngine"/> <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization"/> <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{SimpleProduct.urlKey}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveDefaultProduct"/> - <waitForPageLoad stepKey="waitForPDefaultProductSaved"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveDefaultProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="successMessageYouSavedTheProductIsShown"/> <!--Create product with grid filter Not Visible Individually--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="ProductList"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="ProductList"/> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddFilterProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{defaultSimpleProduct.sku}}" stepKey="fillProductSku"/> @@ -55,8 +52,7 @@ <scrollTo selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="scrollToSearchEngineOptimization"/> <click selector="{{AdminProductFormBundleSection.seoDropdown}}" stepKey="selectSearchEngineOptimization1"/> <fillField selector="{{AdminProductFormBundleSection.urlKey}}" userInput="{{defaultSimpleProduct.urlKey}}" stepKey="fillUrlKey1"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> - <waitForPageLoad stepKey="waitForProductSaved"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Create sub category--> @@ -72,8 +68,7 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="{{defaultSimpleProduct.name}}" stepKey="selectDefaultProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton1"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectDefaultProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="WaitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="successMessageYouSavedTheCategory"/> <!--Verify product with grid filter is not not visible--> <amOnPage url="{{StorefrontProductPage.url(defaultSimpleProduct.urlKey)}}" stepKey="seeOnProductPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml index 19552ddaab729..af72dba3f8051 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCategoryWithRequiredFieldsTest.xml @@ -29,8 +29,7 @@ <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="fillCategoryName"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <!-- Verify success message --> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!-- Verify subcategory created with required fields --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml index 7b2c67b205ea8..758dcee69525e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateCustomProductAttributeWithDropdownFieldTest.xml @@ -46,8 +46,7 @@ </after> <!-- Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!-- Select Created Product--> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> @@ -94,8 +93,7 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> - <waitForPageLoad stepKey="waitForProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Verify product attribute added in product form --> @@ -105,7 +103,7 @@ <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> <!--Verify Product Attribute in Attribute Form --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> <waitForPageLoad stepKey="waitForPageLoad" /> @@ -124,7 +122,9 @@ <waitForPageLoad stepKey="waitForProductToLoad1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml index 37dc7de910917..4c57504b60ad7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDatetimeProductAttributeTest.xml @@ -29,8 +29,7 @@ <!-- Generate the datetime default value --> <generateDate date="now" format="n/j/y g:i A" stepKey="generateDefaultValue"/> <!-- Create new datetime product attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForPageLoadAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <actionGroup ref="CreateProductAttributeWithDatetimeFieldActionGroup" stepKey="createAttribute"> <argument name="attribute" value="DatetimeProductAttribute"/> <argument name="date" value="{$generateDefaultValue}"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml index 88b1c874caadc..b8fec6d5bc001 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> <!-- Set attribute properties --> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index fef69edde23e8..9db8e74b6ae7a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateDropdownProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -60,7 +60,7 @@ <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> <!-- Go to Product Attribute Grid page --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> @@ -83,7 +83,9 @@ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to store's advanced catalog search page --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml index f8346f5a9dd5c..dfa289f18711b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryAndUpdateAsInactiveTest.xml @@ -31,20 +31,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -56,17 +61,20 @@ </after> <!-- Select created category and make category inactive--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotActive.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{CatNotActive.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveCategory"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Index Management Page --> <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml index 0aa89bdfd45b6..d0c40ec276abb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveFlatCategoryTest.xml @@ -31,20 +31,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -56,18 +61,21 @@ </after> <!-- Select created category and make category inactive--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableActiveCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="verifyInactiveIncludeInMenu"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Index Management Page --> <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml index 171d15fe6ed4f..8256661f8c525 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateInactiveInMenuFlatCategoryTest.xml @@ -31,20 +31,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron1"/> - <magentoCLI command="cron:run" arguments="--group=index" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runIndexCronJobs" groups="index"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> </actionGroup> @@ -56,19 +61,22 @@ </after> <!-- Select created category and disable Include In Menu option--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="disableIcludeInMenuOption"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <!--Verify category is saved and Include In Menu Option is disabled in Category Page --> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="verifyInactiveIncludeInMenu"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Index Management Page --> <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml index caacfde89d1cb..7b555aa84be05 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateMultipleSelectProductAttributeVisibleInStorefrontAdvancedSearchFormTest.xml @@ -64,7 +64,7 @@ <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> <!-- Go to Product Attribute Grid page --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="$$attribute.attribute_code$$" stepKey="fillAttrCodeField" /> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearchBtn" /> <click selector="{{AdminProductAttributeGridSection.FirstRow}}" stepKey="chooseFirstRow" /> @@ -87,7 +87,9 @@ <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSuccessMessage"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to store's advanced catalog search page --> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml index 573fc1f83a5a8..fc5fa60f754c4 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateNewGroupForAttributeSetTest.xml @@ -91,8 +91,7 @@ <dontSeeElement selector="{{AdminProductAttributeSetEditSection.attributesInGroup(emptyGroup.name)}}" stepKey="seeNoAttributes"/> <!-- Navigate to Catalog > Products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductPage"/> <!-- Start to create a new simple product with the custom attribute set from the preconditions --> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml index 7fdab11d0a050..61ef389c7909e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeFromProductPageTest.xml @@ -43,8 +43,7 @@ </after> <!-- Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!-- Select Created Product--> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> @@ -86,12 +85,13 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> - <waitForPageLoad stepKey="waitForProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Run Re-Index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify product attribute added in product form --> <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> @@ -100,7 +100,7 @@ <seeElement selector="{{AdminProductFormSection.attributeLabelByText(ProductAttributeFrontendLabel.label)}}" stepKey="seeAttributeLabelInProductForm"/> <!--Verify Product Attribute in Attribute Form --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newProductAttribute.attribute_code}}" stepKey="setAttributeCode"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> <waitForPageLoad stepKey="waitForPageLoad" /> @@ -119,7 +119,9 @@ <waitForPageLoad stepKey="waitForProductToLoad1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{SimpleProduct.price}}" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProduct.sku}}" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="{{SimpleProduct.sku}}"/> + </actionGroup> <scrollTo selector="{{StorefrontProductMoreInformationSection.moreInformation}}" stepKey="scrollToMoreInformation"/> <see selector="{{StorefrontProductMoreInformationSection.attributeLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="seeAttributeLabel"/> <see selector="{{StorefrontProductMoreInformationSection.attributeValue}}" userInput="{{ProductAttributeOption8.value}}" stepKey="seeAttributeValue"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml index 274a560d343d8..9a9d64617f7b5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductAttributeRequiredTextFieldTest.xml @@ -41,9 +41,7 @@ </after> <!-- Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> - + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!-- Select Created Product--> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$createSimpleProduct$$"/> @@ -74,8 +72,7 @@ <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminCreateNewProductAttributeSection.saveAttribute}}" stepKey="clickOnSaveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct"/> - <waitForPageLoad stepKey="waitForProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct"/> <!--Verify product attribute added in product form and Is Required message displayed--> <scrollTo selector="{{AdminProductFormSection.contentTab}}" stepKey="scrollToContentTab"/> @@ -85,8 +82,7 @@ <!--Fill the Required field and save the product --> <fillField selector="{{AdminProductFormSection.attributeRequiredInput(newProductAttribute.attribute_code)}}" userInput="attribute" stepKey="fillTheAttributeRequiredInputField"/> <scrollToTopOfPage stepKey="scrollToTopOfProductFormPage"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="saveTheProduct1"/> - <waitForPageLoad stepKey="waitForProductToSave1"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="saveTheProduct1"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml index c461aa8bfcf18..a9bc656870ff7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateProductDuplicateUrlkeyTest/AdminCreateProductDuplicateUrlkeyTest.xml @@ -27,7 +27,7 @@ </after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> <fillField userInput="$$simpleProduct.name$$new" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> @@ -36,7 +36,7 @@ <fillField userInput="$$simpleProduct.quantity$$" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> <fillField userInput="$$simpleProduct.custom_attributes[url_key]$$" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <see userInput="The value specified in the URL Key field would generate a URL that already exists" selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="assertErrorMessage"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml index 40ca511e1f7bc..1202052c492fc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryAndSubcategoriesTest.xml @@ -21,8 +21,7 @@ <!--Delete all created data during the test execution and assign Default Root Category to Store--> <after> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin2"/> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> - <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnPageAdminSystemStore"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> <waitForPageLoad time="10" stepKey="waitForPageAdminStoresGridLoadAfterResetButton"/> <fillField selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store" stepKey="fillFieldOnWebsiteStore"/> @@ -58,8 +57,7 @@ <argument name="categoryEntity" value="SubCategoryWithParent"/> </actionGroup> <!--Assign new created root category to store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnPageAdminSystemStore"/> - <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnPageAdminSystemStore"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="clickOnResetButton"/> <waitForPageLoad time="10" stepKey="waitForPageAdminStoresGridLoadAfterResetButton"/> <fillField selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store" stepKey="fillFieldOnWebsiteStore"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml index 29c7bc6828662..8d947fba5f368 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateRootCategoryRequiredFieldsTest.xml @@ -33,8 +33,7 @@ <click selector="{{AdminCategorySidebarActionSection.AddRootCategoryButton}}" stepKey="ClickOnAddRootButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="FillCategoryField"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="EnableCheckOption"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="ClickSaveButton"/> - <waitForPageLoad stepKey="WaitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="ClickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="AssertSuccessMessage"/> <seeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="SeeCheckBoxisSelected"/> <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="SeedFieldInput"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml index a8cc66243d73e..9f51d6227aa1d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductNegativePriceTest.xml @@ -22,7 +22,7 @@ <waitForPageLoad stepKey="wait1"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillName"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="-42" stepKey="fillPrice"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <see selector="{{AdminProductFormSection.priceFieldError}}" userInput="Please enter a number 0 or greater in this field." stepKey="seePriceValidationError"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml index f4878f2948e9d..24f87cca958ab 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductTest/AdminCreateSimpleProductZeroPriceTest.xml @@ -22,7 +22,7 @@ <waitForPageLoad stepKey="wait1"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillName"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="0" stepKey="fillPrice"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <amOnPage url="{{StorefrontProductPage.url(SimpleProduct.name)}}" stepKey="viewProduct"/> <waitForPageLoad stepKey="wait2"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$0.00" stepKey="seeZeroPrice"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml index c378ca5b2c27a..48f1f9bbb4e5c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithCountryOfManufactureAttributeSKUMaskTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectSimpleProduct"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickSimpleProductFromDropDownList"/> @@ -43,14 +42,12 @@ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.price}}" stepKey="fillSimpleProductPrice"/> <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.weight}}" stepKey="fillSimpleProductWeight"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.quantity}}" stepKey="fillSimpleProductQuantity"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductToSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search created simple product(from above step) in the grid page to verify sku masked as name and country of manufacture --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchCreatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchCreatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.skuFilter}}" userInput="{{nameAndAttributeSkuMaskSimpleProduct.name}}-{{nameAndAttributeSkuMaskSimpleProduct.country_of_manufacture_label}}" stepKey="fillSkuFilterFieldWithNameAndCountryOfManufactureInput" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml index 2141f44113057..13efee209a556 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithDatetimeAttributeTest.xml @@ -49,7 +49,9 @@ <!-- Save the product --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Flush config cache to reset product attributes in attribute set --> - <magentoCLI command="cache:flush" arguments="config" stepKey="flushConfigCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> <reloadPage stepKey="reloadProductEditPage"/> <!-- Check default value --> <waitForElementVisible selector="{{AdminProductAttributesSection.sectionHeader}}" stepKey="waitAttributesSectionAppears"/> @@ -58,7 +60,7 @@ <waitForElementVisible selector="{{AdminProductAttributesSection.attributeTextInputByCode($createDatetimeAttribute.attribute_code$)}}" stepKey="waitForSlideOutAttributes"/> <seeInField selector="{{AdminProductAttributesSection.attributeTextInputByCode($createDatetimeAttribute.attribute_code$)}}" userInput="$generateDefaultValue" stepKey="checkDefaultValue"/> <!-- Check datetime grid filter --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToAdminProductIndexPage"/> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clickClearFilters"/> <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openProductFilters"/> <fillField selector="{{AdminProductGridFilterSection.inputByCodeRangeFrom($createDatetimeAttribute.attribute_code$)}}" userInput="{$generateDefaultValue}" stepKey="fillProductDatetimeFromFilter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml index 8de84867241a8..54c3a05651c44 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateSimpleProductWithUnicodeTest.xml @@ -31,8 +31,12 @@ <argument name="category" value="$$createPreReqCategory$$"/> <argument name="simpleProduct" value="ProductWithUnicode"/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AssertProductInStorefrontCategoryPage" stepKey="assertProductInStorefront1"> <argument name="category" value="$$createPreReqCategory$$"/> <argument name="product" value="ProductWithUnicode"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml index 9d3a47cd115aa..1144804bb34ff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductFillingRequiredFieldsOnlyTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -35,15 +34,13 @@ <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{virtualProductWithRequiredFields.sku}}" stepKey="fillProductSku"/> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{virtualProductWithRequiredFields.price}}" stepKey="fillProductPrice"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved" /> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> <!-- Verify we see created virtual product(from the above step) on the product grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickSelector"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFilter"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{virtualProductWithRequiredFields.name}}" stepKey="fillProductName1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml index 842f93b49c14a..12fa50c8eed99 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductOutOfStockWithTierPriceTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -58,12 +57,11 @@ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductOutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> @@ -72,7 +70,9 @@ <amOnPage url="{{StorefrontProductPage.url(virtualProductOutOfStock.urlKey)}}" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{virtualProductOutOfStock.sku}}"/> + </actionGroup> <!-- Verify customer see product tier price on product page --> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productTierPriceByForTextLabel('1', tierPriceOnDefault.qty_0)}}" stepKey="firstTierPriceText"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml index 8bb3391b5240b..12dd30872123f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithCustomOptionsSuiteAndImportOptionsTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -46,7 +45,7 @@ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductCustomImportOptions.urlKey}}" stepKey="fillUrlKey"/> <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> @@ -112,13 +111,16 @@ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify customer see created virtual product with custom options suite and import options(from above step) on storefront page and is searchable by sku --> <amOnPage url="{{StorefrontProductPage.url(virtualProductCustomImportOptions.urlKey)}}" stepKey="goToProductPage"/> @@ -132,7 +134,9 @@ <!-- Verify we see created virtual product with custom options suite and import options on the storefront page --> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductCustomImportOptions.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductCustomImportOptions.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{virtualProductCustomImportOptions.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{virtualProductCustomImportOptions.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml index 5076ab2515332..378264141bf20 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceForGeneralGroupTest.xml @@ -86,7 +86,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$categoryEntity.name$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductGeneralGroup.visibility}}" stepKey="seeVisibility"/> <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.useDefaultUrl}}" visible="false" stepKey="openSearchEngineOptimizationSection"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="scrollToAdminProductSEOSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml index faae6a371db24..0f800419f0456 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithTierPriceTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -49,23 +48,20 @@ <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductBigQty.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="checkRetailCustomerTaxClass" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="{{virtualProductBigQty.name}}" stepKey="fillProductName1"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="virtualProductBigQty.name"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened" /> @@ -86,7 +82,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{virtualProductBigQty.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -98,8 +94,12 @@ <see selector="{{StorefrontCategoryMainSection.productLink}}" userInput="{{virtualProductBigQty.name}}" stepKey="seeVirtualProductNameOnCategoryPage"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify customer see created virtual product with tier price(from above step) on storefront page and is searchable by sku --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml index 3b5a8d8e753da..d644281c9fb44 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminCreateVirtualProductWithoutManageStockTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductToggle"/> <waitForPageLoad stepKey="waitForProductToggleToSelectProduct"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickVirtualProduct"/> @@ -41,19 +40,18 @@ <fillField selector="{{AdminProductFormAdvancedPricingSection.specialPrice}}" userInput="{{virtualProductWithoutManageStock.special_price}}" stepKey="fillSpecialPrice"/> <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickDoneButton"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{virtualProductWithoutManageStock.quantity}}" stepKey="fillProductQuantity"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickAdvancedInventoryLink"/> <click selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" stepKey="clickManageStock"/> <checkOption selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" stepKey="CheckUseConfigSettingsCheckBox"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection"/> <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{virtualProductWithoutManageStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSuccessMessage"/> @@ -62,7 +60,9 @@ <amOnPage url="{{StorefrontProductPage.url(virtualProductWithoutManageStock.urlKey)}}" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForStoreFrontPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{virtualProductWithoutManageStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{virtualProductWithoutManageStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{virtualProductWithoutManageStock.sku}}"/> + </actionGroup> <!-- Verify customer see product special price on the storefront page --> <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml index 3dfeea2c33af0..b82c6ba13550c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteAttributeSetTest.xml @@ -45,8 +45,7 @@ <click selector="{{AdminProductAttributeSetGridSection.searchBtn}}" stepKey="clickSearch2"/> <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> <!-- Search for the product by sku and name on the product page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToAdminProductIndex"/> - <waitForPageLoad stepKey="waitForAdminProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToAdminProductIndex"/> <actionGroup ref="FilterProductGridBySkuAndNameActionGroup" stepKey="filerProductsBySkuAndName"> <argument name="product" value="SimpleProductWithCustomAttributeSet"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml index 7748d4bf4db6f..bf0d5d99a23bb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteConfigurableChildProductsTest.xml @@ -93,7 +93,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="seeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStatusInStoreFront"/> <see selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="seeProductAttributeLabel"/> <seeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="seeProductAttributeOptions"/> @@ -116,7 +118,9 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName($$createCategory.name$$)}}" stepKey="seeCategoryInFrontPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createConfigProduct.name$$" stepKey="seeProductNameInStoreFront1"/> <dontSee selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$$createConfigProduct.price$$" stepKey="dontSeeProductPriceInStoreFront"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createConfigProduct.sku$$" stepKey="seeProductSkuInStoreFront1"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSkuInStoreFront1"> + <argument name="productSku" value="$$createConfigProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="OUT OF STOCK" stepKey="seeProductStatusInStoreFront1"/> <dontSee selector="{{StorefrontProductInfoMainSection.productAttributeTitle1}}" userInput="$$createConfigProductAttribute.default_value$$" stepKey="dontSeeProductAttributeLabel"/> <dontSeeElement selector="{{StorefrontProductInfoMainSection.productAttributeOptions1}}" stepKey="dontSeeProductAttributeOptions"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml index c0cbb44ebc681..30bc0315bcf13 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductAttributeTest.xml @@ -45,8 +45,7 @@ </actionGroup> <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage2"/> <!--Go to the Catalog > Products page and create Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductBtn"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="chooseAddSimpleProduct"/> <waitForPageLoad stepKey="waitForProductAdded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml index 22e1d7d7c5d9e..4599d0c275214 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductWithCustomOptionTest.xml @@ -38,10 +38,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml index e0375728f316f..7e5ee977d679b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteProductsImageInCaseOfMultipleStoresTest.xml @@ -65,8 +65,12 @@ <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> <argument name="websiteName" value="{{NewWebSiteData.name}}"/> </actionGroup> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="createSubCategory" stepKey="deleteSubCategory"/> <deleteData createDataKey="createRootCategory" stepKey="deleteRootCategory"/> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> @@ -74,8 +78,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Grab new store view code--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToNewWebsitePage"/> - <waitForPageLoad stepKey="waitForStoresPageLoad"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="navigateToNewWebsitePage"/> <fillField userInput="{{NewWebSiteData.name}}" selector="{{AdminStoresGridSection.websiteFilterTextField}}" stepKey="fillSearchWebsiteField"/> <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearchButton"/> <click selector="{{AdminStoresGridSection.storeNameInFirstRow}}" stepKey="clickFirstRow"/> @@ -94,8 +97,12 @@ </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> <!--Reindex and flush cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Switch to 'Default Store View' scope and open product page--> <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="SwitchDefaultStoreView"> <argument name="storeViewName" value="'Default Store View'"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml index 2fa91604e1776..84ada79c9ec86 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryAssignedToStoreTest.xml @@ -29,8 +29,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnAdminSystemStorePage"/> <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> @@ -41,7 +40,7 @@ <!--Verify Delete Root Category can not be deleted--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> <scrollToTopOfPage stepKey="scrollToTopOfPage2"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandToSeeAllCategories"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(NewRootCategory.name))}}" stepKey="clickRootCategoryInTree"/> <!--Verify Delete button is not displayed--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml index 40bd3bdcfea20..4979b06a1051e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootCategoryTest.xml @@ -27,16 +27,19 @@ <!--Verify Created root Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminCategoryBasicFieldSection.CategoryNameInput(NewRootCategory.name)}}" stepKey="seeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsListedInCategoriesTreeActionGroup" stepKey="seeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Delete Root Category--> <deleteData createDataKey="rootCategory" stepKey="deleteRootCategory"/> <!--Verify Root Category is not listed in backend--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories1"/> - <dontSee selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{NewRootCategory.name}}" stepKey="dontSeeRootCategory"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{NewRootCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml index fe07360d6b9ca..4310c6f06219a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteRootSubCategoryTest.xml @@ -33,59 +33,48 @@ </after> <!--Create a Store--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> - <see userInput="You saved the store." stepKey="seeSaveMessage"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="{{NewRootCategory.name}}"/> + </actionGroup> <!--Create a Store View--> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="selectCreateStoreView"/> - <click selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="clickDropDown"/> - <selectOption userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreViewStatus"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="Enabled" stepKey="enableStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreViewButton"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal" /> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning" /> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal" /> - <waitForElementNotVisible selector="{{AdminNewStoreViewActionsSection.loadingMask}}" stepKey="waitForElementVisible"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage1"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> + <argument name="StoreGroup" value="customStore"/> + <argument name="customStore" value="customStore"/> + </actionGroup> <!--Go To store front page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> <!--Verify subcategory displayed in store front--> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="selectMainWebsite"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="selectMainWebsite1"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="selectCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Delete SubCategory--> <deleteData createDataKey="category" stepKey="deleteCategory"/> <!--Verify Sub Category is absent in backend --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories2"/> - <dontSee selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="dontSeeCategoryInTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTheCategoryTree"/> + <actionGroup ref="AssertAdminCategoryIsNotListedInCategoriesTreeActionGroup" stepKey="doNotSeeRootCategory"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify Sub Category is not present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage1"/> - <waitForPageLoad time="60" stepKey="waitForStoreFrontPageLoad2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="dontSeeSubCategoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeOldCategoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleSubCategory.name}}"/> + </actionGroup> <!--Verify in Category is not in Url Rewrite grid--> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="openUrlRewriteIndexPage"/> - <waitForPageLoad stepKey="waitForUrlRewritePageTopLoad"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillRequestPath"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="seeEmptyRow"/> + <actionGroup ref="AdminSearchDeletedUrlRewriteActionGroup" stepKey="searchingCategoryUrlRewrite"> + <argument name="requestPath" value="{{SimpleRootSubCategory.url_key}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml index 390002f5d9498..bdd1a4b4c70fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSimpleProductTest.xml @@ -37,10 +37,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createSimpleProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createSimpleProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml index 22c6bf061f274..b7e037b323ee2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteSystemProductAttributeTest.xml @@ -23,8 +23,7 @@ <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="{{newsFromDate.attribute_code}}" stepKey="setAttributeCode"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml index 36001bd0b570a..a6cd3c8b52b23 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteTextFieldProductAttributeFromAttributeSetTest.xml @@ -48,8 +48,7 @@ <waitForPageLoad stepKey="waitForAttributeSetEditPageToLoad"/> <see selector="{{AdminProductAttributeSetEditSection.groupTree}}" userInput="$$attribute.attribute_code$$" stepKey="seeAttributeInAttributeGroupTree"/> <!--Open Product Index Page and filter the product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="SimpleProduct2"/> </actionGroup> @@ -82,8 +81,7 @@ <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.groupTree}}" stepKey="dontSeeAttributeInAttributeGroupTree"/> <dontSee userInput="$$attribute.attribute_code$$" selector="{{AdminProductAttributeSetEditSection.unassignedAttributesTree}}" stepKey="dontSeeAttributeInUnassignedAttributeTree"/> <!--Verify Product Attribute is not present in Product Index Page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductIndexPage"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductIndexPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct1"> <argument name="product" value="SimpleProduct2"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml index f49e1142315eb..dcfcbd699fc6b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminDeleteVirtualProductTest.xml @@ -38,10 +38,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createVirtualProduct.name$$)}}" stepKey="amOnVirtualProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createVirtualProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createVirtualProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createVirtualProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml index ebbfdc4d72f40..b437d5fb0c868 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminEditTextEditorProductAttributeTest.xml @@ -44,14 +44,12 @@ <fillField userInput="{{_defaultProduct.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> <dontSeeElement selector="{{ProductAttributeWYSIWYGSection.TinyMCE4($$myProductAttributeCreation.attribute_code$$)}}" stepKey="dontSeeTinyMCE4" /> <fillField selector="{{ProductAttributeWYSIWYGSection.TextArea($$myProductAttributeCreation.attribute_code$$)}}" userInput="Text Area" stepKey="fillContentTextarea" /> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoading1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Go to storefront product page, assert product content --> <amOnPage url="{{_defaultProduct.name}}.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForPageLoad5"/> <see userInput="Text Area" stepKey="seeText2" /> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid2"/> - <waitForPageLoad stepKey="waitForPageLoad6"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid2"/> <click selector="{{AdminProductAttributeGridSection.AttributeCode($$myProductAttributeCreation.attribute_code$$)}}" stepKey="navigateToAttributeEditPage2" /> <waitForPageLoad stepKey="waitForPageLoad7" /> <seeOptionIsSelected selector="{{AttributePropertiesSection.InputType}}" userInput="Text Area" stepKey="seeTextAreaSelected" /> @@ -62,8 +60,7 @@ <dontSeeElement selector="{{StorefrontPropertiesSection.EnableWYSIWYG}}" stepKey="dontSeeWYSIWYGEnableField2" /> <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute8" /> <waitForPageLoad stepKey="waitForPageLoad8"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGrid" /> - <waitForPageLoad stepKey="waitForPageLoad9"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGrid"/> <actionGroup ref="SortByIdDescendingActionGroup" stepKey="sortByIdDescending" /> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.enabledFilters}}" visible="true" stepKey="clearAllExistingFilter"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingAfterFilterIsCleared"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml index 843221782ebd9..eb9fe693f8b3b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminFilterByNameByStoreViewOnProductGridTest.xml @@ -38,7 +38,7 @@ <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{SimpleProduct.name}}" stepKey="fillNewName"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterGridByName"> <argument name="product" value="SimpleProduct"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml index f6b2a74eca0f0..f10288bea36d9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminGridPageNumberAfterSaveAndCloseActionTest.xml @@ -23,8 +23,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Clear product grid--> <comment userInput="Clear product grid" stepKey="commentClearProductGrid"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridToDefaultView"/> <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProductIfTheyExist"/> <createData stepKey="category1" entity="SimpleSubCategory"/> @@ -37,8 +36,8 @@ </createData> </before> <after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <click selector="{{AdminDataGridPaginationSection.previousPage}}" stepKey="clickPrevPageOrderGrid"/> <actionGroup ref="AdminDataGridDeleteCustomPerPageActionGroup" stepKey="deleteCustomAddedPerPage"> <argument name="perPage" value="ProductPerPage.productCount"/> @@ -49,8 +48,7 @@ <deleteData stepKey="deleteProduct2" createDataKey="product2"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <actionGroup ref="AdminDataGridSelectCustomPerPageActionGroup" stepKey="select1OrderPerPage"> <argument name="perPage" value="ProductPerPage.productCount"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml index c319116bf075c..af31b7c1d5c07 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminImportCustomizableOptionToProductWithSKUTest.xml @@ -32,8 +32,12 @@ </createData> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml index 0214f9141b903..b7a55a90a08d3 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassChangeProductsStatusTest.xml @@ -30,15 +30,14 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml index 845ce340451d1..070c07d9feb7d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassProductPriceUpdateTest.xml @@ -30,8 +30,7 @@ </after> <!--Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!--Search products using keyword --> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml index cd34741b6a68c..35a3a39422185 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesGlobalScopeTest.xml @@ -44,8 +44,7 @@ </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml index e4d69e9169613..479c9e5e5bb19 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesMissingRequiredFieldTest.xml @@ -30,15 +30,14 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml index 08b2d924e2a5e..30ab17f65f3c8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductAttributesStoreViewScopeTest/AdminMassUpdateProductAttributesStoreViewScopeTest.xml @@ -31,12 +31,11 @@ <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createProductThree" stepKey="deleteProductThree"/> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="AdminDeleteStoreViewActionGroup"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="api-simple-product"/> </actionGroup> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml index 54921d3fc2dda..b1dad54edcae9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMassUpdateProductStatusStoreViewScopeTest/AdminMassUpdateProductStatusStoreViewScopeTest.xml @@ -14,7 +14,7 @@ <title value="Admin should be able to mass update product statuses in store view scope"/> <description value="Admin should be able to mass update product statuses in store view scope"/> <severity value="AVERAGE"/> - <testCaseId value="MAGETWO-59361"/> + <testCaseId value="MC-28538"/> <group value="Catalog"/> <group value="Product Attributes"/> <group value="SearchEngineElasticsearch"/> @@ -22,69 +22,51 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!--Create Website --> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - - <!--Create Store --> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> - - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminNewStoreViewActionsSection.saveButton}}" stepKey="clickSaveStoreView"/> - <waitForElementVisible selector="{{AdminConfirmationModalSection.ok}}" stepKey="waitForModal"/> - <see selector="{{AdminConfirmationModalSection.title}}" userInput="Warning message" stepKey="seeWarning"/> - <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="dismissModal"/> - <waitForPageLoad stepKey="waitForPageLoad2" time="180"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" time="150" stepKey="waitForPageReolad"/> - <see userInput="You saved the store view." stepKey="seeSavedMessage"/> <!--Create a Simple Product 1 --> <actionGroup ref="CreateSimpleProductAndAddToWebsiteActionGroup" stepKey="createSimpleProduct1"> <argument name="product" value="simpleProductForMassUpdate"/> - <argument name="website" value="Second Website"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <!--Create a Simple Product 2 --> <actionGroup ref="CreateSimpleProductAndAddToWebsiteActionGroup" stepKey="createSimpleProduct2"> <argument name="product" value="simpleProductForMassUpdate2"/> - <argument name="website" value="Second Website"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> </before> <after> <!--Delete website --> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <!--Delete Products --> - <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> - <argument name="productName" value="simpleProductForMassUpdate.name"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteProduct1"> + <argument name="sku" value="{{simpleProductForMassUpdate.sku}}"/> </actionGroup> - <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct2"> - <argument name="productName" value="simpleProductForMassUpdate2.name"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteProduct2"> + <argument name="sku" value="{{simpleProductForMassUpdate2.sku}}"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilter"/> </after> <!-- Search and select products --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="SearchProductGridByKeyword2ActionGroup" stepKey="searchByKeyword"> <argument name="keyword" value="{{simpleProductForMassUpdate.keyword}}"/> </actionGroup> @@ -92,7 +74,7 @@ <!-- Filter to Second Store View --> <actionGroup ref="AdminFilterStoreViewActionGroup" stepKey="filterStoreView"> - <argument name="customStore" value="'Second Store View'"/> + <argument name="customStore" value="customStore.name"/> </actionGroup> <!-- Select Product 2 --> @@ -136,8 +118,7 @@ <see userInput="1 item" selector="{{StorefrontCatalogSearchAdvancedResultMainSection.itemFound}}" stepKey="seeInDefaultSecondProductResults"/> <!--Enable the product in Default store view--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex2"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex2"/> <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('1')}}" stepKey="clickCheckboxDefaultStoreView"/> <click selector="{{AdminProductGridSection.productGridCheckboxOnRow('2')}}" stepKey="clickCheckboxDefaultStoreView2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml index bf5fde3b85bba..809a015369ea9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveAnchoredCategoryToDefaultCategoryTest.xml @@ -30,8 +30,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -39,8 +38,8 @@ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> + <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Enable Anchor for FirstLevelSubCat Category--> @@ -49,8 +48,7 @@ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting1"/> <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting1"/> <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> <!--Enable Anchor for SimpleSubCategory Category and add products to the Category--> @@ -64,13 +62,16 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Category in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{FirstLevelSubCat.name}}/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> @@ -92,8 +93,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree2"/> <!--Move SubCategory under Default Category--> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml index 4dbbdc8f4399e..9100e6027a52f 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryAndCheckUrlRewritesTest.xml @@ -23,8 +23,12 @@ <field key="is_active">true</field> </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createDefaultCategory" stepKey="deleteCategory"/> @@ -33,23 +37,20 @@ <!--Open category page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(FirstLevelSubCat.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <!--Create second level category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave1"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage1"/> <!--Create third level category under second level category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton1"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory2"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave2"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory2"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage2"/> <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> @@ -74,8 +75,7 @@ <!--Open Category Page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree2"/> <!--Move the third level category under first level category --> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree($$createDefaultCategory.name$$)}}" stepKey="m0oveCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml index 116df566f2bd0..0e056e4bb7078 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryFromParentAnchoredCategoryTest.xml @@ -33,8 +33,7 @@ <!--Open Category page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -42,7 +41,7 @@ <scrollTo selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" x="0" y="-80" stepKey="scrollToDisplaySetting"/> <click selector="{{CategoryDisplaySettingsSection.DisplaySettingTab}}" stepKey="selectDisplaySetting"/> <checkOption selector="{{CategoryDisplaySettingsSection.anchor}}" stepKey="enableAnchor"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <!--Create a Subcategory under _defaultCategory category--> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> @@ -54,12 +53,13 @@ <fillField selector="{{AdminCategoryContentSection.productTableColumnName}}" userInput="$$simpleProduct.name$$" stepKey="selectProduct"/> <click selector="{{AdminCategoryContentSection.productSearch}}" stepKey="clickSearchButton"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify category displayed in store front page--> <amOnPage url="/$$createDefaultCategory.name$$/{{SimpleSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFrontPage"/> @@ -81,8 +81,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree2"/> <!--Move SubCategory under Default Category--> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="moveCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml index fd9e50928d748..654ddb4d8d872 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveCategoryToAnotherPositionInCategoryTreeTest.xml @@ -33,20 +33,17 @@ <!-- Open Category Page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <!-- Create three level deep sub Category --> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{FirstLevelSubCat.name}}" stepKey="fillSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveFirstLevelSubCategory"/> - <waitForPageLoad stepKey="waitForFirstLevelCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveFirstLevelSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButtonAgain"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SecondLevelSubCat.name}}" stepKey="fillSecondLevelSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSecondLevelSubCategory"/> - <waitForPageLoad stepKey="waitForSecondLevelCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSecondLevelSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> <grabFromCurrentUrl stepKey="categoryId" regex="#\/([0-9]*)?\/$#" /> @@ -68,8 +65,7 @@ <!-- Move Category to another position in category tree and click ok button--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openTheAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SecondLevelSubCat.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryInTree('Default Category')}}" stepKey="DragCategory"/> <see selector="{{AdminCategoryModalSection.message}}" userInput="This operation can take a long time" stepKey="seeWarningMessageForOneMoreTime"/> <click selector="{{AdminCategoryModalSection.ok}}" stepKey="clickOkButtonOnWarningPopup"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml index 055f4e23cd9e7..059a3a321b16a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMoveProductBetweenCategoriesTest.xml @@ -63,8 +63,7 @@ <!-- Create subcategory <Sub1> of the anchored category --> <click selector="{{AdminCategorySidebarActionSection.AddSubcategoryButton}}" stepKey="clickOnAddSubCategoryButton"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="addSubCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory1"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory1"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSaveSuccessMessage"/> <!-- Assign <product1> to the <Sub1> --> @@ -76,10 +75,8 @@ <fillField userInput="{{SimpleSubCategory.name}}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearch"/> <waitForPageLoad stepKey="waitForSubCategory"/> <click selector="{{AdminProductFormSection.selectCategory(SimpleSubCategory.name)}}" stepKey="selectSub1Category"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickDone"/> - <waitForPageLoad stepKey="waitForApplyCategory"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="waitForSavingChanges"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickDone"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSave"/> <!-- Enable `Use Categories Path for Product URLs` on Stores -> Configuration -> Catalog -> Catalog -> Search Engine Optimization --> <amOnPage url="{{AdminCatalogSearchConfigurationPage.url}}" stepKey="onConfigPage"/> @@ -107,10 +104,8 @@ <click selector="{{AdminProductFormSection.unselectCategories(SimpleSubCategory.name)}}" stepKey="removeCategory"/> <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="openDropDown"/> <checkOption selector="{{AdminProductFormSection.selectCategory($$createSecondCategory.name$$)}}" stepKey="selectCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="pressButtonDone"/> - <waitForPageLoad stepKey="waitForApplyCategory2"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="pushButtonSave"/> - <waitForPageLoad stepKey="waitForSavingProduct"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="pressButtonDone"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="pushButtonSave"/> <!--Product is saved --> <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSuccessMessage"/> @@ -179,10 +174,8 @@ <fillField userInput="{$grabNameSubCategory}" selector="{{AdminProductFormSection.searchCategory}}" stepKey="fillSearchField"/> <waitForPageLoad stepKey="waitForSearchSubCategory"/> <click selector="{{AdminProductFormSection.selectCategory({$grabNameSubCategory})}}" stepKey="selectSubCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickButtonDone"/> - <waitForPageLoad stepKey="waitForCategoryApply"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickButtonSave"/> - <waitForPageLoad stepKey="waitForSaveChanges"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickButtonDone"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickButtonSave"/> <!-- Product is saved successfully --> <see userInput="You saved the product." selector="{{CatalogProductsSection.messageSuccessSavedProduct}}" stepKey="seeSaveMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml index c1cfcf7ebe10f..4c076c9a495fc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminMultipleWebsitesUseDefaultValuesTest.xml @@ -15,45 +15,38 @@ <title value="Use Default Value checkboxes should be checked for new website scope"/> <description value="Use Default Value checkboxes for product attribute should be checked for new website scope"/> <severity value="BLOCKER"/> - <testCaseId value="MAGETWO-92454"/> + <testCaseId value="MC-25783"/> <group value="Catalog"/> </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> + </before> + <after> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> - </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> - </actionGroup> - - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <waitForElementVisible selector="//legend[contains(., 'Store View Information')]" stepKey="waitForNewStorePageToOpen"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption selector="{{AdminNewStoreSection.statusDropdown}}" userInput="1" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickStoreViewSaveButton"/> - <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationModal" /> - <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="AcceptNewStoreViewCreation"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReload"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage"/> <!--Create a Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillProductName"/> @@ -63,17 +56,17 @@ <!-- Add product to second website and save the product --> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsites"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> <!-- switch to the second store view --> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> <!-- Check if Use Default Value checkboxes are checked --> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="seeProductStatusCheckboxChecked"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml index 659521ed9e467..94d7ea14b096c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminNavigateMultipleUpSellProductsTest.xml @@ -96,8 +96,7 @@ </after> <!--Open Product Index Page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <!--Select SimpleProduct --> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml index 1c536df7c2efb..8e728fc6e1f27 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductCustomURLKeyPreservedWhenAssignedToCategoryWithoutCustomURLKeyTest.xml @@ -35,8 +35,12 @@ </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProductFirst" stepKey="deleteFirstSimpleProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml index 9536ee030cdf8..d677eda5b0920 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByCustomAttributeTest.xml @@ -94,8 +94,7 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductGridFilters"/> <!--Sort by custom attribute DESC using grabbed value--> <conditionalClick selector="{{AdminProductGridSection.columnHeader($$createDropdownAttribute.attribute[frontend_labels][0][label]$$)}}" dependentSelector="{{AdminProductGridSection.columnHeader($$createDropdownAttribute.attribute[frontend_labels][0][label]$$)}}" visible="true" stepKey="ascendSortByCustomAttribute"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml index d47730a99308b..449d201393206 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridFilteringByDateAttributeTest.xml @@ -26,8 +26,7 @@ <deleteData createDataKey="createSimpleProductWithDate" stepKey="deleteProduct"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="Set Product as New from Date" stepKey="setAttributeLabel"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromGrid"/> @@ -38,16 +37,14 @@ <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> <waitForPageLoad stepKey="waitForSaveAttribute"/> <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad time="30" stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="openColumnsdropDown1"/> <checkOption selector="{{AdminProductGridFilterSection.viewColumnOption('Set Product as New from Date')}}" stepKey="showProductAsNewColumn"/> <click selector="{{AdminProductGridFilterSection.columnsDropdown}}" stepKey="closeColumnsDropdown1"/> <seeElement selector="{{AdminProductGridSection.columnHeader('Set Product as New from Date')}}" stepKey="seeNewFromDateColumn"/> <waitForPageLoad stepKey="waitforFiltersToApply"/> <actionGroup ref="FilterProductGridBySetNewFromDateActionGroup" stepKey="filterProductGridToCheckSetAsNewColumn"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnFirstRowProductGrid"/> - <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnFirstRowProductGrid"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveAndCloseProductForm"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="expandFilters"/> <seeInField selector="{{AdminProductGridFilterSection.newFromDateFilter}}" userInput="05/16/2018" stepKey="checkForNewFromDate"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml new file mode 100644 index 0000000000000..fea4436446da2 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductGridUrlFilterApplierTest.xml @@ -0,0 +1,41 @@ +<?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="AdminProductGridUrlFilterApplierTest"> + <annotations> + <features value="Catalog"/> + <stories value="Filter product using GET URL parameter"/> + <title value="Verify that filter is applied on product grid when filters parameter is set on url"/> + <description value="Accessing product grid url with filters parameter"/> + <severity value="MAJOR"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> + <group value="product"/> + </annotations> + + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"/> + </before> + + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + + <amOnPage url="{{AdminProductIndexPage.url}}?filters[name]=$createSimpleProduct.name$" stepKey="navigateToProductGridWithFilters"/> + <waitForPageLoad stepKey="waitForProductGrid"/> + <see selector="{{AdminProductGridSection.productGridNameProduct($createSimpleProduct.name$)}}" userInput="$createSimpleProduct.name$" stepKey="seeProduct"/> + <waitForElementVisible selector="{{AdminProductGridFilterSection.enabledFilters}}" stepKey="waitForEnabledFilters"/> + <seeElement selector="{{AdminProductGridFilterSection.enabledFilters}}" stepKey="seeEnabledFilters"/> + <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="Name: $createSimpleProduct.name$" stepKey="seeProductNameFilter"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml index ae63158990b96..081eceede0b35 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductStatusAttributeDisabledByDefaultTest.xml @@ -23,8 +23,7 @@ </before> <after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid1"/> <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="Enable Product" stepKey="setAttributeLabel1"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid1"/> @@ -37,8 +36,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttribute"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttribute"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> <fillField selector="{{AdminProductAttributeGridSection.GridFilterFrontEndLabel}}" userInput="Enable Product" stepKey="setAttributeLabel"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="searchForAttributeFromTheGrid"/> @@ -48,8 +46,7 @@ <click selector="{{AttributePropertiesSection.Save}}" stepKey="saveAttribute"/> <waitForPageLoad stepKey="waitForAttributeToSave"/> <actionGroup ref="ClearCacheActionGroup" stepKey="clearCache"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad time="30" stepKey="waitForProductGridPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickOnAddSimpleProduct"/> <waitForPageLoad stepKey="waitForProductEditToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml index 9d82edd0fb50c..6d7de64b47434 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminDownloadableProductTypeSwitchingToSimpleProductTest.xml @@ -33,8 +33,16 @@ <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterSimpleProductGridBySku"> <argument name="sku" value="$$createProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeSimpleProductNameInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeSimpleProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeSimpleProductNameInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeSimpleProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Simple Product"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearSimpleProductFilters"/> <!--Assert simple product on storefront--> <comment userInput="Assert simple product on storefront" stepKey="commentAssertSimpleProductOnStorefront"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml index 12d654508d7d7..de99933c78933 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToDownloadableProductTest.xml @@ -53,8 +53,16 @@ <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> <argument name="sku" value="$$createProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeDownloadableProductNameInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Downloadable Product" stepKey="seeDownloadableProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeDownloadableProductNameInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeDownloadableProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Downloadable Product"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearDownloadableProductFilters"/> <!--Assert downloadable product on storefront--> <comment userInput="Assert downloadable product on storefront" stepKey="commentAssertDownloadableProductOnStorefront"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml index 96ee795998459..99fe4dd0c135d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveCustomOptionsFromProductTest.xml @@ -26,7 +26,7 @@ </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProductWithOptions"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductFilter"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml index 00eaa623e2bca..521256cf57dd5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageSimpleProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct3"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml index 6cc1b256e5ec9..4a544b60f15b6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveDefaultImageVirtualProductTest.xml @@ -22,12 +22,11 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="defaultVirtualProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml index 6cd76c4cc06b8..1707fda9e3edb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRemoveImageAffectsAllScopesTest.xml @@ -26,53 +26,56 @@ <requiredEntity createDataKey="category"/> </createData> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> - <argument name="newWebsiteName" value="FirstWebSite"/> - <argument name="websiteCode" value="FirstWebSiteCode"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore" after="createWebsite"> - <argument name="website" value="FirstWebSite"/> - <argument name="storeGroupName" value="NewStore"/> - <argument name="storeGroupCode" value="Base1"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> </actionGroup> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView" after="createNewStore"> - <argument name="StoreGroup" value="staticFirstStoreGroup"/> - <argument name="customStore" value="staticStore"/> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createSecondWebsite" after="createCustomStoreView"> - <argument name="newWebsiteName" value="SecondWebSite"/> - <argument name="websiteCode" value="SecondWebSiteCode"/> + <argument name="newWebsiteName" value="{{secondCustomWebsite.name}}"/> + <argument name="websiteCode" value="{{secondCustomWebsite.code}}"/> </actionGroup> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createSecondStore" after="createSecondWebsite"> - <argument name="website" value="SecondWebSite"/> - <argument name="storeGroupName" value="SecondStore"/> - <argument name="storeGroupCode" value="Base2"/> + <argument name="website" value="{{secondCustomWebsite.name}}"/> + <argument name="storeGroupName" value="{{SecondStoreGroupUnique.name}}"/> + <argument name="storeGroupCode" value="{{SecondStoreGroupUnique.code}}"/> </actionGroup> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView2" after="createSecondStore"> - <argument name="StoreGroup" value="staticStoreGroup"/> - <argument name="customStore" value="staticSecondStore"/> + <argument name="StoreGroup" value="SecondStoreGroupUnique"/> + <argument name="customStore" value="SecondStoreUnique"/> </actionGroup> </before> <after> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> - <argument name="websiteName" value="FirstWebSite"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="SecondWebSite"/> + <argument name="websiteName" value="{{secondCustomWebsite.name}}"/> </actionGroup> <deleteData createDataKey="category" stepKey="deletePreReqCategory"/> <deleteData createDataKey="product" stepKey="deleteFirstProduct"/> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <!--Open created product--> @@ -90,15 +93,14 @@ </actionGroup> <!--"Product in Websites": select both Websites--> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite1"> - <argument name="website" value="FirstWebSite"/> + <argument name="website" value="{{customWebsite.name}}"/> </actionGroup> <actionGroup ref="ProductSetWebsiteActionGroup" stepKey="ProductSetWebsite2"> - <argument name="website" value="SecondWebSite"/> + <argument name="website" value="{{secondCustomWebsite.name}}"/> </actionGroup> <!--Go to "Catalog" -> "Products". Open created product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoaded"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductPage"/> <click selector="{{AdminProductGridSection.productGridNameProduct($$product.name$$)}}" stepKey="openCreatedProduct"/> <waitForPageLoad stepKey="waitForCreatedProductOpened"/> @@ -110,7 +112,7 @@ <!--Switch to "Store view 1"--> <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="selectStoreView"> - <argument name="storeViewName" value="Store View"/> + <argument name="storeViewName" value="{{customStore.name}}"/> </actionGroup> <!-- Assert product first image not in admin product form --> @@ -120,7 +122,7 @@ <!--Switch to "Store view 2"--> <actionGroup ref="SwitchToTheNewStoreViewActionGroup" stepKey="selectSecondStoreView"> - <argument name="storeViewName" value="Second Store View"/> + <argument name="storeViewName" value="{{SecondStoreUnique.name}}"/> </actionGroup> <!-- Verify that Image 1 is deleted from the Second Store View list --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml index 2444165fa1b39..394e16b8412a5 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminRequiredFieldsHaveRequiredFieldIndicatorTest.xml @@ -39,7 +39,7 @@ <expectedResult type="string">rgb(226, 38, 38)</expectedResult> </assertEquals> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndexPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="addProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="addSimpleProduct"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml index 9819890ed3751..de116b26d1414 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductImagesTest.xml @@ -37,8 +37,7 @@ </after> <!-- Go to the first product edit page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$firstProduct$$"/> @@ -106,7 +105,7 @@ <dontSeeElement selector="{{AdminProductMessagesSection.errorMessage}}" stepKey="dontSeeErrorPng"/> <!-- Save the first product and go to the storefront --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <amOnPage url="$$firstProduct.name$$.html" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForStorefront"/> @@ -123,8 +122,7 @@ <seeElement selector=".products-grid img[src*='placeholder/small_image.jpg']" stepKey="seePlaceholder"/> <!-- Go to the second product edit page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex2"/> - <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex2"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku2"> <argument name="product" value="$$secondProduct$$"/> @@ -143,12 +141,15 @@ <!-- Save the second product --> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to the admin grid and see the uploaded image --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex3"/> - <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex3"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid3"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku3"> <argument name="product" value="$$secondProduct$$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml index ec82bdcf5bc94..e1c97b205d4fa 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductImagesTest/AdminSimpleProductRemoveImagesTest.xml @@ -33,8 +33,7 @@ </after> <!-- Go to the product edit page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$product$$"/> @@ -85,7 +84,7 @@ <waitForPageLoad stepKey="waitForHide3"/> <!-- Save the product with all 3 images --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Go to the product page and see the Base image --> <amOnPage url="$$product.name$$.html" stepKey="goToProductPage"/> @@ -98,8 +97,7 @@ <seeElement selector="{{StorefrontCategoryProductSection.ProductImageBySrc('/adobe-small')}}" stepKey="seeThumb"/> <!-- Go to the admin grid and see the Thumbnail image --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex2"/> - <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex2"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku2"> <argument name="product" value="$$product$$"/> @@ -107,8 +105,7 @@ <seeElement selector="{{AdminProductGridSection.productThumbnailBySrc('/adobe-thumb')}}" stepKey="seeBaseInGrid"/> <!-- Go to the product edit page again --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex3"/> - <waitForPageLoad stepKey="wait5"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex3"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid3"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku3"> <argument name="product" value="$$product$$"/> @@ -120,11 +117,10 @@ <click selector="{{AdminProductImagesSection.nthRemoveImageBtn('1')}}" stepKey="removeImage1"/> <click selector="{{AdminProductImagesSection.nthRemoveImageBtn('2')}}" stepKey="removeImage2"/> <click selector="{{AdminProductImagesSection.nthRemoveImageBtn('3')}}" stepKey="removeImage3"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct2"/> <!-- Check admin grid for placeholder --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex4"/> - <waitForPageLoad stepKey="wait6"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex4"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid4"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku4"> <argument name="product" value="$$product$$"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml index 51a91a17ff41a..fc18531eca350 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleProductSetEditContentTest.xml @@ -34,8 +34,7 @@ </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="SimpleProduct"/> </actionGroup> @@ -51,7 +50,7 @@ <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="This is the short description" stepKey="fillShortDescription"/> <!--save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Edit content--> @@ -61,7 +60,7 @@ <fillField selector="{{AdminProductContentSection.shortDescriptionTextArea}}" userInput="EDIT ~ This is the short description ~ EDIT" stepKey="editShortDescription"/> <!--save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAfterEdit"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--Checking content admin--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml index 534924e0f70c9..3c90de572988c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSimpleSetEditRelatedProductsTest.xml @@ -35,7 +35,7 @@ <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct0" stepKey="deleteSimpleProduct0"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> @@ -46,8 +46,7 @@ </after> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> @@ -61,7 +60,7 @@ </actionGroup> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Add another related product--> @@ -73,7 +72,7 @@ <click selector="{{AdminProductFormRelatedUpSellCrossSellSection.removeRelatedProduct($$simpleProduct0.sku$$)}}" stepKey="removeRelatedProduct"/> <!--Save the product--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButtonAfterEdit"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButtonAfterEdit"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShownAgain"/> <!--See related product in admin--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml index 71e827a64ae2d..f5fb33afd4617 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminSortingByWebsitesTest.xml @@ -31,7 +31,9 @@ <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnableWebUrlOptions"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableWebUrlOptions"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -43,14 +45,17 @@ <actionGroup ref="GoToProductCatalogPageActionGroup" stepKey="goToProductCatalogPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Assign Custom Website to Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForCatalogProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="assignCustomWebsiteToProduct"> @@ -62,13 +67,11 @@ <uncheckOption selector="{{ProductInWebsitesSection.website(_defaultWebsite.name)}}" stepKey="deselectMainWebsite"/> <checkOption selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectWebsite"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForLoadingMaskToDisappear stepKey="waitForProductPageToSaveAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> <!--Navigate To Product Grid To Check Website Sorting--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGridToSortByWebsite"/> - <waitForPageLoad stepKey="waitForCatalogProductGridLoaded"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGridToSortByWebsite"/> <!--Sorting works (By Websites) ASC--> <click selector="{{AdminProductGridSection.columnHeader('Websites')}}" stepKey="clickWebsitesHeaderToSortAsc"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml index e0e517defdeac..e562faf523929 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndCheckDefaultUrlKeyOnStoreViewTest.xml @@ -33,8 +33,7 @@ </after> <!--Open Store Page --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="amOnAdminSystemStorePage"/> <!--Create Custom Store --> <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> @@ -51,12 +50,11 @@ <!--Update Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCateforyToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveUpdatedCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <waitForPageLoad stepKey="waitForPageToLoad1"/> <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" x="0" y="-80" stepKey="scrollToSearchEngineOptimization1"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml index a4ba859714982..21f2b622c2ebd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryAndMakeInactiveTest.xml @@ -30,11 +30,10 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Update category and make category inactive--> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="disableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> <dontSeeCheckboxIsChecked selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="dontCategoryIsChecked"/> @@ -47,7 +46,7 @@ <!--Verify Inactive Category in category page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree1"/> <seeElement selector="{{AdminCategoryContentSection.categoryInTree(_defaultCategory.name)}}" stepKey="assertCategoryInTree" /> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory1"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seeCategoryPageTitle1" /> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml index 0ca8e74c4e59e..f4d464455491b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryNameWithStoreViewTest.xml @@ -32,16 +32,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!--Open store page --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <!--Create Custom Store --> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> <!--Create Store View--> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> @@ -50,32 +46,40 @@ </actionGroup> <!--Verify created SubCAtegory is present on Store Front --> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="seeCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="goToCategoryPage"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <!--Update Category--> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandToSeeAllCategories"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTreeUnderRoot(SimpleRootSubCategory.name)}}" stepKey="clickOnSubcategoryIsUndeRootCategory"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{_defaultCategory.name}}" stepKey="updateCategoryName"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCateforyToSave"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandCategoryTree"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminChangeCategoryNameActionGroup" stepKey="updateCategoryName"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!--Verify the Category is not present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> - <waitForPageLoad stepKey="waitForPageToLoaded2"/> - <dontSeeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="dontSeeCatergoryInStoreFront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsNotShownInMenuActionGroup" stepKey="doNotSeeOldCategoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify the Updated Category is present in Store Front--> - <amOnPage url="/{{NewRootCategory.name}}/{{_defaultCategory.name}}.html" stepKey="seeTheUpdatedCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForPageToLoaded3"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(_defaultCategory.name)}}" stepKey="seeUpdatedCatergoryInStoreFront"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeUpdatedCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{_defaultCategory.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml index 87d7f91431dc3..4389bf4bd6383 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryStoreUrlKeyTest.xml @@ -46,7 +46,7 @@ <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyDefaultValueCheckbox}}" stepKey="uncheckUseDefaultUrlKey"/> <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}-hattest" stepKey="enterURLKey"/> <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckRedirect1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryAfterFirstSeoUpdate"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> <amOnPage url="" stepKey="goToStorefront"/> <waitForPageLoad stepKey="waitForFrontendLoad"/> @@ -64,7 +64,7 @@ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection2"/> <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{_defaultCategory.name_lwr}}" stepKey="enterOriginalURLKey"/> <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckRedirect2"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterOriginalSeoKey"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryAfterOriginalSeoKey"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterOriginalSeoKey"/> <amOnPage url="" stepKey="goToStorefrontAfterOriginalSeoKey"/> <waitForPageLoad stepKey="waitForFrontendLoadAfterOriginalSeoKey"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml index 6a12b991bd225..3bbe8722d8bfc 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryUrlKeyWithStoreViewTest.xml @@ -32,16 +32,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <!--Open Store Page --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForSystemStorePage"/> - <!--Create Custom Store --> - <click selector="{{AdminStoresMainActionsSection.createStoreButton}}" stepKey="selectCreateStore"/> - <fillField userInput="{{customStore.name}}" selector="{{AdminNewStoreGroupSection.storeGrpNameTextField}}" stepKey="fillStoreName"/> - <fillField userInput="{{customStore.code}}" selector="{{AdminNewStoreGroupSection.storeGrpCodeTextField}}" stepKey="fillStoreCode"/> - <selectOption userInput="{{NewRootCategory.name}}" selector="{{AdminNewStoreGroupSection.storeRootCategoryDropdown}}" stepKey="selectStoreStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickSaveStoreButton"/> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{_defaultWebsite.name}}"/> + <argument name="store" value="{{customStore.name}}"/> + <argument name="rootCategory" value="$$rootCategory.name$$"/> + </actionGroup> <!--Create Store View--> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"> @@ -50,34 +46,37 @@ </actionGroup> <!--Verify Category in Store View--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront"/> - <waitForPageLoad stepKey="waitForSystemStorePage1"/> - <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="ClickSwitchStoreButtonOnDefaultStore"/> - <click selector="{{StorefrontFooterSection.storeLink(customStore.name)}}" stepKey="SelectSecondStoreToSwitchOn"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCatergoryInStoreFront"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory"/> - <waitForPageLoad stepKey="waitForProductToLoad"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontSwitchStoreActionGroup" stepKey="switchToCustomStore"> + <argument name="storeName" value="{{customStore.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="selectCategory"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> <!--Update URL Key--> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCategory1"/> - <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSearchEngineOptimization"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection"/> - <clearField selector="{{AdminCategorySEOSection.UrlKeyInput}}" stepKey="clearUrlKeyField"/> - <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="newurlkey" stepKey="enterURLKey"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryAfterFirstSeoUpdate"/> - <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessage"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="ChangeSeoUrlKeyActionGroup" stepKey="changeSeoUrlKey"> + <argument name="value" value="newurlkey"/> + </actionGroup> <!--Open Category Store Front Page--> - <amOnPage url="/{{NewRootCategory.name}}/{{SimpleRootSubCategory.name}}.html" stepKey="seeTheCategoryInStoreFront1"/> - <waitForPageLoad stepKey="waitForSystemStorePage3"/> - <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> - <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleRootSubCategory.name)}}" stepKey="selectCategory2"/> - <waitForPageLoad stepKey="waitForProductToLoad1"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openHomepage"/> + <actionGroup ref="StorefrontAssertCategoryNameIsShownInMenuActionGroup" stepKey="seeCatergoryNameInStoreFront"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openCategory"> + <argument name="categoryName" value="{{SimpleRootSubCategory.name}}"/> + </actionGroup> <!--Verify Updated URLKey is present--> - <seeInCurrentUrl stepKey="verifyUpdatedUrlKey" url="newurlkey.html"/> + <actionGroup ref="StorefrontAssertProperUrlIsShownActionGroup" stepKey="seeUpdatedUrlkey"> + <argument name="urlPath" value="newurlkey.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml index db6cfce167bce..373a14c6bb0e7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithInactiveIncludeInMenuTest.xml @@ -31,7 +31,7 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!--Update Category name,description, urlKey, meta title and disable Include in Menu--> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillCategoryName"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> @@ -44,8 +44,7 @@ <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization"/> <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="{{SimpleRootSubCategory.url_key}}" stepKey="fillUpdatedUrlKey"/> <fillField selector="{{AdminCategorySEOSection.MetaTitleInput}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="fillUpdatedMetaTitle"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <!--Open UrlRewrite Page--> @@ -65,7 +64,7 @@ <!--Verify Updated fields in Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree1"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleRootSubCategory.name)}}" stepKey="selectCreatedCategory1"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleRootSubCategory.name}}" stepKey="seeUpdatedCategoryTitle"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml index 9b827550a6817..b0829d96db4fd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateCategoryWithProductsTest.xml @@ -30,7 +30,7 @@ <!--Open Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <checkOption selector="{{AdminCategoryBasicFieldSection.EnableCategory}}" stepKey="enableCategory"/> @@ -55,8 +55,7 @@ <waitForPageLoad stepKey="waitFroPageToLoad1"/> <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProduct1FromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="clickSaveButton"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="assertSuccessMessage"/> <waitForPageLoad stepKey="waitForPageTitleToBeSaved"/> @@ -64,8 +63,12 @@ <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{_defaultCategory.name}}" stepKey="seePageTitle" /> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Verify Category in store front page--> <amOnPage url="{{StorefrontCategoryPage.url(_defaultCategory.name)}}" stepKey="seeDefaultProductPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml index 1950b385c4a68..b6c508df121df 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryAndAddProductsTest.xml @@ -32,20 +32,25 @@ <argument name="storeView" value="customStoreFR"/> </actionGroup> <!--Run full reindex and clear caches --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Enable Flat Catalog Category --> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 1"/> <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="schedule" /> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> + <!-- Run cron --> + <magentoCron stepKey="runAllCronJobs"/> </before> <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexersMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> </actionGroup> @@ -57,9 +62,11 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Select Created Category--> - <magentoCLI command="indexer:reindex" stepKey="reindexBeforeFlow"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexBeforeFlow"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForTheCategoryPageToLoaded"/> <!--Add Products in Category--> @@ -73,12 +80,15 @@ <waitForPageLoad stepKey="waitFroPageToLoad1"/> <scrollTo selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="scrollToTableRow"/> <click selector="{{AdminCategoryContentSection.productTableRow}}" stepKey="selectProductFromTableRow"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Open Index Management Page and verify flat categoryIndex status--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Index Management Page --> <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> <waitForPageLoad stepKey="waitForIndexPageToLoad"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml index 1214ba879f211..8eb7813b6203e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryIncludeInNavigationTest.xml @@ -58,16 +58,19 @@ <dontSee selector="{{StorefrontHeaderSection.NavigationCategoryByName(CatNotIncludeInMenu.name)}}" stepKey="dontSeeCategoryOnNavigation"/> <!-- Select created category and enable Include In Menu option--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(CatNotIncludeInMenu.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForCategoryPageToLoad"/> <click selector="{{AdminCategoryBasicFieldSection.includeInMenuLabel}}" stepKey="enableIncludeInMenuOption"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Index Management Page --> <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> <waitForPageLoad stepKey="waitForIndexPageToBeLoaded"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml index 490f8dbdc4f81..94f8d0e1dc523 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateFlatCategoryNameAndDescriptionTest.xml @@ -45,7 +45,9 @@ <after> <magentoCLI stepKey="setFlatCatalogCategory" command="config:set catalog/frontend/flat_catalog_category 0 "/> <magentoCLI stepKey="setIndexerMode" command="indexer:set-mode" arguments="realtime" /> - <magentoCLI stepKey="indexerReindex" command="indexer:reindex" /> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> <deleteData stepKey="deleteCategory" createDataKey="createCategory" /> <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteStoreViewEn"> <argument name="customStore" value="customStoreEN"/> @@ -57,7 +59,7 @@ </after> <!-- Select Created Category--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(_defaultCategory.name)}}" stepKey="selectCreatedCategory"/> <waitForPageLoad stepKey="waitForPageToLoaded"/> <!--Update Category Name and Description --> @@ -65,12 +67,15 @@ <scrollTo selector="{{AdminCategoryContentSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToContent"/> <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="selectContent"/> <fillField selector="{{AdminCategoryContentSection.description}}" userInput="Updated category Description Fields" stepKey="fillUpdatedDescription"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveSubCategory"/> - <waitForPageLoad stepKey="waitForSecondCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveSubCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!--Run full reindex and clear caches --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Open Index Management Page --> <amOnPage url="{{AdminIndexManagementPage.url}}" stepKey="openIndexManagementPage"/> <waitForPageLoad stepKey="waitForIndexPageToLoad"/> @@ -90,7 +95,7 @@ <seeElement selector="{{StorefrontHeaderSection.NavigationCategoryByName(SimpleSubCategory.name)}}" stepKey="seeCategoryOnNavigation1"/> <!-- Verify Updated Category Name and description on Category Page--> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage1"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree1"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree1"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" stepKey="selectUpdatedCategory"/> <waitForPageLoad stepKey="waitForUpdatedCategoryPageToLoad"/> <seeInField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeUpdatedSubCategoryName"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml index 6edffb923d540..5f7cecfde188a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductNameToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,8 +42,7 @@ </after> <!-- Search default simple product in grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -64,8 +63,7 @@ <!-- Update default simple product with name --> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{simpleProductDataOverriding.name}}" stepKey="fillSimpleProductName"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickButtonSave"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml index e954de90ef542..8a05ed9d64b8b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductPriceToVerifyDataOverridingOnStoreViewLevelTest.xml @@ -42,8 +42,7 @@ </after> <!-- Search default simple product in grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -62,8 +61,7 @@ <!-- Update default simple product with price --> <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductDataOverriding.price}}" stepKey="fillSimpleProductPrice"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickButtonSave"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml index f5b0fb8054dc1..300b312612253 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductTieredPriceTest.xml @@ -27,8 +27,12 @@ <createData entity="SimpleSubCategory" stepKey="categoryEntity"/> <!--TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <deleteData stepKey="deleteSimpleSubCategory" createDataKey="initialCategoryEntity"/> @@ -40,8 +44,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -74,19 +77,17 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductTierPrice300InStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -125,7 +126,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductTierPrice300InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductTierPrice300InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductTierPrice300InStock.sku}}" stepKey="seeProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSku"> + <argument name="productSku" value="{{simpleProductTierPrice300InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductTierPrice300InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml index d20594461173b..a9630aba467c6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockDisabledProductTest.xml @@ -34,8 +34,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -53,15 +52,13 @@ <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductDisabled.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="clickEnableProductLabelToDisableProduct"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductDisabled.name}}" stepKey="fillSimpleProductNameInNameFilter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml index 5fa7acbeb8de9..aa1b1ae702914 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockEnabledFlatTest.xml @@ -38,8 +38,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -52,11 +51,10 @@ <fillField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="fillSimpleProductPrice"/> <selectOption selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="selectProductTaxClass"/> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="fillSimpleProductQuantity"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPage"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickAdvancedInventoryLink"/> <conditionalClick selector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" dependentSelector="{{AdminProductFormAdvancedInventorySection.useConfigSettings}}" visible="true" stepKey="checkUseConfigSettingsCheckBox"/> <selectOption selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="selectManageStock"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickDoneButtonOnAdvancedInventorySection"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickDoneButtonOnAdvancedInventorySection"/> <selectOption selector="{{AdminProductFormSection.stockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="selectStockStatusInStock"/> <fillField selector="{{AdminProductFormSection.productWeight}}" userInput="{{simpleProductEnabledFlat.weight}}" stepKey="fillSimpleProductWeight"/> <selectOption selector="{{AdminProductFormSection.productWeightSelect}}" userInput="{{simpleProductEnabledFlat.weightSelect}}" stepKey="selectProductWeight"/> @@ -67,20 +65,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductEnabledFlat.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductEnabledFlat.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -95,8 +91,7 @@ <seeInField selector="{{AdminProductFormSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPrice"/> <seeInField selector="{{AdminProductFormSection.productTaxClass}}" userInput="{{simpleProductEnabledFlat.productTaxClass}}" stepKey="seeProductTaxClass"/> <seeInField selector="{{AdminProductFormSection.productQuantity}}" userInput="{{simpleProductEnabledFlat.quantity}}" stepKey="seeSimpleProductQuantity"/> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickTheAdvancedInventoryLink"/> - <waitForPageLoad stepKey="waitForAdvancedInventoryPageLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickTheAdvancedInventoryLink"/> <see selector="{{AdminProductFormAdvancedInventorySection.manageStock}}" userInput="No" stepKey="seeManageStock"/> <click selector="{{AdminProductFormAdvancedInventorySection.advancedInventoryCloseButton}}" stepKey="clickDoneButtonOnAdvancedInventory"/> <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="{{simpleProductEnabledFlat.status}}" stepKey="seeSimpleProductStockStatus"/> @@ -119,7 +114,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductEnabledFlat.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductEnabledFlat.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductEnabledFlat.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductEnabledFlat.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductEnabledFlat.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml index 4b21d1337e9b7..86fac835ce44d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockNotVisibleIndividuallyTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductNotVisibleIndividually.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductNotVisibleIndividually.urlKey}}" stepKey="fillSimpleProductUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductNotVisibleIndividually.name}}" stepKey="fillSimpleProductNameInNameFilter"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml index 4256f93ea41d1..af3861e4e0b64 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockUnassignFromCategoryTest.xml @@ -34,8 +34,7 @@ </after> <!--Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -46,18 +45,16 @@ <scrollTo selector="{{AdminProductFormSection.productStockStatus}}" stepKey="scroll"/> <click selector="{{AdminProductFormSection.categoriesDropdown}}" stepKey="clickCategoriesDropDown"/> <click selector="{{AdminProductFormSection.unselectCategories($$initialCategoryEntity.name$$)}}" stepKey="unselectCategories"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategory"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategory"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!--Search default simple product in the grid page --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="OpenCategoryCatalogPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$initialCategoryEntity.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> <click selector="{{AdminCategoryProductsSection.sectionHeader}}" stepKey="clickAdminCategoryProductSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml index 58db163bed720..320edba5feeff 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogAndSearchTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice245InStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="fillSimpleProductUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -95,7 +92,9 @@ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice245InStock.urlKey}}" stepKey="seeUrlKey"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> @@ -107,7 +106,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice245InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice245InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice245InStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductRegularPrice245InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice245InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml index 5e9a48f659d6b..77c3e7548a3cf 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInCatalogOnlyTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice32501InStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -95,7 +92,9 @@ <seeInField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32501InStock.urlKey}}" stepKey="seeUrlKey"/> <!--Run re-index task --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Verify customer see updated simple product link on category page --> <amOnPage url="{{StorefrontCategoryPage.url($$categoryEntity.name$$)}}" stepKey="openCategoryPage"/> @@ -107,7 +106,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32501InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32501InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32501InStock.sku}}" stepKey="seeProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSku"> + <argument name="productSku" value="{{simpleProductRegularPrice32501InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice32501InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml index 3d37b54dfa439..39dab0b08915c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockVisibleInSearchOnlyTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,20 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{simpleProductRegularPrice325InStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice325InStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -104,7 +101,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice325InStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice325InStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice325InStock.sku}}" stepKey="seeProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeProductSku"> + <argument name="productSku" value="{{simpleProductRegularPrice325InStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice325InStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml index 855a2b1d9b0cc..670030d1d98ea 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceInStockWithCustomOptionsTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,7 +57,7 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPriceCustomOptions.urlKey}}" stepKey="fillUrlKey"/> <click selector="{{AdminProductCustomizableOptionsSection.checkIfCustomizableOptionsTabOpen}}" stepKey="clickAdminProductCustomizableOption"/> @@ -76,15 +75,13 @@ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_price_type}}" stepKey="selectOptionPriceType"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(simpleProductCustomizableOption.title,'0')}}" userInput="{{simpleProductCustomizableOption.option_0_sku}}" stepKey="fillOptionSku"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!--Verify customer see success message--> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!--Search updated simple product(from above step) in the grid page--> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -124,7 +121,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPriceCustomOptions.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPriceCustomOptions.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPriceCustomOptions.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductRegularPriceCustomOptions.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPriceCustomOptions.storefrontStatus}}</expectedResult> @@ -150,9 +149,7 @@ <!-- Verify added Product in cart --> <selectOption selector="{{StorefrontProductPageSection.customOptionDropDown}}" userInput="{{simpleProductCustomizableOption.option_0_title}} +$98.00" stepKey="selectCustomOption"/> <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="1" stepKey="fillProductQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="clickOnAddToCartButton"/> - <waitForPageLoad stepKey="waitForProductToAddInCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeYouAddedSimpleprod4ToYourShoppingCartSuccessSaveMessage"/> <seeElement selector="{{StorefrontMinicartSection.quantity(1)}}" stepKey="seeAddedProductQuantityInCart"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml index af836efcf6be6..441bc9b8f8005 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateSimpleProductWithRegularPriceOutOfStockTest.xml @@ -36,8 +36,7 @@ </after> <!-- Search default simple product in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGrid"> <argument name="sku" value="$$initialSimpleProduct.sku$$"/> </actionGroup> @@ -58,19 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory"/> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{simpleProductRegularPrice32503OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForSimpleProductSave"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify customer see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertSimpleProductSaveSuccessMessage"/> <!-- Search updated simple product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedSimpleProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedSimpleProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="fillSimpleProductNameInNameFilter"/> @@ -102,7 +100,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{simpleProductRegularPrice32503OutOfStock.name}}" stepKey="seeSimpleProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{simpleProductRegularPrice32503OutOfStock.price}}" stepKey="seeSimpleProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{simpleProductRegularPrice32503OutOfStock.sku}}" stepKey="seeSimpleProductSkuOnStoreFrontPage"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeSimpleProductSkuOnStoreFrontPage"> + <argument name="productSku" value="{{simpleProductRegularPrice32503OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{simpleProductRegularPrice32503OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml index 5221510fd4dce..616c38e326a62 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithNoRedirectTest.xml @@ -43,8 +43,7 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!-- Open 3rd Level category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -55,8 +54,7 @@ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updatedurl" stepKey="updateUrlKey"/> <uncheckOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="uncheckPermanentRedirectCheckBox"/> <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveUpdatedCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Get Category Id --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml index 505ca583da3f4..9d4bc5f184c9c 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateTopCategoryUrlWithRedirectTest.xml @@ -41,8 +41,7 @@ <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openAdminCategoryIndexPage"/> <!-- Open 3rd Level category --> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="clickOnExpandTree"/> - <waitForPageLoad stepKey="waitForCategoryToLoad"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="clickOnExpandTree"/> <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$createThreeLevelNestedCategories.name$$)}}" stepKey="selectCategory"/> <waitForPageLoad stepKey="waitForPageToLoad"/> @@ -53,8 +52,7 @@ <fillField selector="{{AdminCategorySEOSection.UrlKeyInput}}" userInput="updateredirecturl" stepKey="updateUrlKey"/> <checkOption selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="checkPermanentRedirectCheckBox"/> <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="selectSearchEngineOptimization1"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveUpdatedCategory"/> - <waitForPageLoad stepKey="waitForCategoryToSave"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveUpdatedCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="seeSuccessMessage"/> <!-- Get Category ID --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml index 595f9bcd489ec..f4375ad499dfd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockVisibleInCategoryOnlyTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -70,19 +68,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -110,7 +107,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -140,7 +137,9 @@ <waitForPageLoad stepKey="waitForStoreFrontProductPageToLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <grabTextFrom selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="tierPriceText"/> <assertEquals stepKey="assertTierPriceTextOnProductPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml index 458d02d61426d..0cf0d22094cb6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceInStockWithCustomOptionsVisibleInSearchOnlyTest.xml @@ -34,12 +34,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -57,7 +55,7 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPriceInStock.urlKey}}" stepKey="fillUrlKey"/> @@ -120,14 +118,13 @@ <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_price_type}}" stepKey="selectOptionPriceTypeForFourthDataSetSecondRow"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueSku(virtualProductCustomizableOption4.title,'1')}}" userInput="{{virtualProductCustomizableOption4.option_1_sku}}" stepKey="fillOptionSkuForFourthDataSetSecondRow"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -149,7 +146,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPriceInStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -219,7 +216,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPriceInStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPriceInStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 6d6ff0b3b1b89..7f3df4be87bb9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -56,14 +54,13 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -88,7 +85,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice5OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml index d5ae971d87695..343326547254a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInCategoryOnlyTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickclearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -59,19 +57,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -92,7 +89,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -108,7 +105,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice5OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice5OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPrice5OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml index 314df67d43d00..0d6a1c87c62fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithRegularPriceOutOfStockVisibleInSearchOnlyTest.xml @@ -35,12 +35,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -54,14 +52,13 @@ <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -86,7 +83,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductRegularPrice99OutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductRegularPrice99OutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductRegularPrice99OutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml index d0f4fc8882e3f..f423cd6c77807 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceInStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -65,19 +63,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPrice.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -101,7 +98,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPrice.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -125,7 +122,9 @@ <amOnPage url="{{StorefrontProductPage.url(updateVirtualProductSpecialPrice.urlKey)}}" stepKey="goToProductStorefrontPage"/> <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPrice.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPrice.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductSpecialPrice.sku}}"/> + </actionGroup> <!-- Verify customer see virtual product special price on the storefront page --> <grabTextFrom selector="{{StorefrontProductInfoMainSection.specialPriceAmount}}" stepKey="specialPriceAmount"/> <assertEquals stepKey="assertSpecialPriceTextOnProductPage"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml index 2234d6f338b62..78a15ee7eb195 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithSpecialPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -64,19 +62,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product with special price(out of stock) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -99,7 +96,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <see selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -110,7 +107,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductSpecialPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductSpecialPriceOutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductSpecialPriceOutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml index ab5d23f0f875e..79a1bf96671d9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryAndSearchTest.xml @@ -97,7 +97,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$categoryEntity.name$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductTierPriceInStock.visibility}}" stepKey="seeVisibility"/> <conditionalClick selector="{{AdminProductSEOSection.sectionHeader}}" dependentSelector="{{AdminProductSEOSection.useDefaultUrl}}" visible="false" stepKey="openSearchEngineOptimizationSection"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml index 8f0861fe33371..f64e628c0edb9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceInStockVisibleInCategoryOnlyTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -70,19 +68,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualProductWithTierPriceInStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -110,7 +107,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualProductWithTierPriceInStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -126,7 +123,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualProductWithTierPriceInStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualProductWithTierPriceInStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualProductWithTierPriceInStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualProductWithTierPriceInStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualProductWithTierPriceInStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml index f7f5385381590..6e835f2e5e98b 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/AdminUpdateVirtualProductWithTierPriceOutOfStockVisibleInCategoryAndSearchTest.xml @@ -37,12 +37,10 @@ </after> <!-- Search default virtual product in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPage1"/> - <waitForPageLoad stepKey="waitForProductCatalogPage1"/> - <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAllFilter" /> - <fillField selector="{{AdminProductGridFilterSection.keywordSearch}}" userInput="$$initialVirtualProduct.name$$" stepKey="fillVirtualProductNameInKeywordSearch"/> - <click selector="{{AdminProductGridFilterSection.keywordSearchButton}}" stepKey="clickKeywordSearchButton"/> - <waitForPageLoad stepKey="waitForProductSearch"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage1"/> + <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> + <argument name="keyword" value="$$initialVirtualProduct.name$$"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.nthRow('1')}}" stepKey="clickFirstRowToVerifyCreatedVirtualProduct"/> <waitForPageLoad stepKey="waitUntilProductIsOpened"/> @@ -70,19 +68,18 @@ <fillField selector="{{AdminProductFormSection.searchCategory}}" userInput="$$categoryEntity.name$$" stepKey="fillSearchCategory" /> <waitForPageLoad stepKey="waitForCategory2"/> <click selector="{{AdminProductFormSection.selectCategory($$categoryEntity.name$$)}}" stepKey="clickOnCategory"/> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneAdvancedCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneAdvancedCategorySelect"/> <selectOption selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="selectVisibility"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection"/> <fillField selector="{{AdminProductSEOSection.urlKeyInput}}" userInput="{{updateVirtualTierPriceOutOfStock.urlKey}}" stepKey="fillUrlKey"/> <scrollToTopOfPage stepKey="scrollToTopOfAdminProductFormSection"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSaveButton"/> - <waitForPageLoad stepKey="waitForVirtualProductSaved"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSaveButton"/> <!-- Verify we see success message --> <see selector="{{AdminProductFormSection.successMessage}}" userInput="You saved the product." stepKey="seeAssertVirtualProductSaveSuccessMessage"/> <!-- Search updated virtual product(from above step) in the grid page --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="OpenProductCatalogPageToSearchUpdatedVirtualProduct"/> - <waitForPageLoad stepKey="waitForProductCatalogPageToLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPageToSearchUpdatedVirtualProduct"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clickClearAll"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickFiltersButton"/> <fillField selector="{{AdminProductGridFilterSection.nameFilter}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="fillVirtualProductNameInNameFilter"/> @@ -110,7 +107,7 @@ <actualResult type="variable">selectedCategories</actualResult> <expectedResult type="array">[$$categoryEntity.name$$]</expectedResult> </assertEquals> - <click selector="{{AdminProductFormSection.done}}" stepKey="clickOnDoneOnCategorySelect"/> + <actionGroup ref="AdminSubmitCategoriesPopupActionGroup" stepKey="clickOnDoneOnCategorySelect"/> <seeInField selector="{{AdminProductFormSection.visibility}}" userInput="{{updateVirtualTierPriceOutOfStock.visibility}}" stepKey="seeVisibility"/> <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" x="0" y="-80" stepKey="scrollToAdminProductSEOSection1"/> <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="clickAdminProductSEOSection1"/> @@ -126,7 +123,9 @@ <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{updateVirtualTierPriceOutOfStock.name}}" stepKey="seeVirtualProductNameOnStoreFrontPage"/> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="{{updateVirtualTierPriceOutOfStock.price}}" stepKey="seeVirtualProductPriceOnStoreFrontPage"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{updateVirtualTierPriceOutOfStock.sku}}" stepKey="seeVirtualProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeVirtualProductSku"> + <argument name="productSku" value="{{updateVirtualTierPriceOutOfStock.sku}}"/> + </actionGroup> <grabTextFrom selector="{{StorefrontProductInfoMainSection.productStockStatus}}" stepKey="productStockAvailableStatus"/> <assertEquals stepKey="assertStockAvailableOnProductPage"> <expectedResult type="string">{{updateVirtualTierPriceOutOfStock.storefrontStatus}}</expectedResult> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml index 3eaae60d789f5..55d697e35deba 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CheckTierPricingOfProductsTest.xml @@ -110,7 +110,9 @@ <actionGroup ref="ClearProductsFilterActionGroup" stepKey="ClearProductsFilterActionGroup"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Edit customer info--> @@ -333,8 +335,12 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!--Do reindex and flush cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml index 437532b9baebf..26ff1bc45be9d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDateTest.xml @@ -39,7 +39,7 @@ <generateDate date="now" format="m/j/Y" stepKey="generateDefaultDate"/> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeWithDateFieldActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml index 580a5bd4939bb..61787dcff0b91 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml index e24bf0d7b1115..73c8bafba0625 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityDropdownWithSingleQuoteTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml index 0a84d9af3c918..9952f6a4a85fe 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityMultiSelectTest.xml @@ -32,7 +32,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml index 97eff20b2d560..760cd5e0e488a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityPriceTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute with Price--> <actionGroup ref="CreateProductAttributeActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml index c0cff7b0b2bc9..ea94fc58400a6 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/CreateProductAttributeEntityTest/CreateProductAttributeEntityTextFieldTest.xml @@ -33,7 +33,7 @@ </after> <!--Navigate to Stores > Attributes > Product.--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <!--Create new Product Attribute as TextField, with code and default value.--> <actionGroup ref="CreateProductAttributeWithTextFieldActionGroup" stepKey="createAttribute"> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml index ce9ff3af18607..ac855cdbf94af 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DeleteCategoriesTest.xml @@ -56,8 +56,7 @@ <argument name="parentCategory" value="$$createNewRootCategoryA.name$$"/> </actionGroup> <!-- Change root category for Main Website Store. --> - <amOnPage stepKey="s1" url="{{AdminSystemStorePage.url}}"/> - <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="s1"/> <click stepKey="s2" selector="{{AdminStoresGridSection.resetButton}}"/> <waitForPageLoad stepKey="waitForPageAdminStoresGridLoadAfterResetButton" time="10"/> <fillField stepKey="s4" selector="{{AdminStoresGridSection.storeGrpFilterTextField}}" userInput="Main Website Store"/> @@ -75,7 +74,9 @@ <!-- @TODO: Uncomment commented below code after MQE-903 is fixed --> <!-- Perform cli reindex. --> - <!--<magentoCLI command="indexer:reindex" stepKey="magentoCli"/>--> + <!--<actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex">--> + <!-- <argument name="indices" value=""/>--> + <!--</actionGroup>--> <!-- Delete Default Root Category. --> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="navigateToCategoryPageAfterCLIReindexCommand"/> @@ -152,7 +153,7 @@ <click selector="{{AdminCategorySidebarTreeSection.categoryInTree('$$createNewRootCategoryA.name$$')}}" stepKey="clickOnNewRootCategoryA"/> <waitForPageLoad stepKey="waitForPageNewRootCategoryALoad" /> <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="Default Category" stepKey="enterCategoryNameAsDefaultCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryDefaultCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryDefaultCategory"/> <seeElement selector="{{AdminCategoryMessagesSection.SuccessMessage}}" stepKey="assertSuccessMessageAfterSaveDefaultCategory"/> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml index f6ede46578f33..e679402740398 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/DisplayRefreshCacheAfterChangingCategoryPageLayoutTest.xml @@ -24,14 +24,18 @@ <comment userInput="Create category, flush cache and log in" stepKey="createCategoryAndLogIn"/> <createData entity="SimpleSubCategory" stepKey="simpleCategory"/> <actionGroup ref="AdminLoginActionGroup" stepKey="logInAsAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete category and log out --> <comment userInput="Delete category and log out" stepKey="deleteCategoryAndLogOut"/> <deleteData createDataKey="simpleCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Navigate to category details page --> <comment userInput="Navigate to category details page" stepKey="navigateToAdminCategoryPage"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml index 5c3f79694e79a..110b4167cfc80 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CAdminTest.xml @@ -29,8 +29,7 @@ <!--Admin creates product--> <!--Create Simple Product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageSimple"/> - <waitForPageLoad time="30" stepKey="waitForProductPageLoadSimple"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageSimple"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct"/> @@ -57,8 +56,7 @@ </actionGroup> <!--Create Virtual Product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageVirtual"/> - <waitForPageLoad time="30" stepKey="waitForProductPageLoadVirtual"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageVirtual"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateVirtualProduct"> <argument name="product" value="VirtualProduct"/> </actionGroup> @@ -73,8 +71,7 @@ <!--Admin uses product grid--> <!--Start with default view--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPageGrid"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPageGrid"/> <!--Search by keyword--> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> @@ -82,7 +79,11 @@ <argument name="keyword" value="SimpleProduct.name"/> </actionGroup> <seeNumberOfElements selector="{{AdminProductGridSection.productGridRows}}" userInput="1" stepKey="seeOnlyOneProductInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="{{SimpleProduct.name}}" stepKey="seeOnlySimpleProductInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeOnlySimpleProductInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="{{SimpleProduct.name}}"/> + </actionGroup> <!--Paging works--> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="setProductGridToDefaultPagination"/> @@ -108,7 +109,11 @@ <argument name="product" value="GroupedProduct"/> </actionGroup> <seeNumberOfElements selector="{{AdminProductGridSection.productGridRows}}" userInput="1" stepKey="seeOneMatchingSkuInProductGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1','SKU')}}" userInput="{{GroupedProduct.sku}}" stepKey="seeProductInFilteredGridSku"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductInFilteredGridSku"> + <argument name="row" value="1"/> + <argument name="column" value="SKU"/> + <argument name="value" value="{{GroupedProduct.sku}}"/> + </actionGroup> <!--Filter by price--> <actionGroup ref="FilterProductGridByPriceRangeActionGroup" stepKey="filterProductGridByPrice"> <argument name="filter" value="PriceFilterRange"/> @@ -197,7 +202,11 @@ <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridToCheckWeightColumn"> <argument name="product" value="SimpleProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1','Weight')}}" userInput="{{SimpleProduct.weight}}" stepKey="seeCorrectProductWeightInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeCorrectProductWeightInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Weight"/> + <argument name="value" value="{{SimpleProduct.weight}}"/> + </actionGroup> <!--END Admin uses product grid--> <!--Admin creates category--> @@ -218,7 +227,7 @@ <!--Admin moves category--> <comment userInput="Admin moves category." stepKey="adminMovesCategoryComment" before="onCategoryPageToMoveCategory"/> <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="onCategoryPageToMoveCategory"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandTree"/> + <actionGroup ref="AdminExpandCategoryTreeActionGroup" stepKey="expandTree"/> <dragAndDrop selector1="{{AdminCategorySidebarTreeSection.categoryInTree(SimpleSubCategory.name)}}" selector2="{{AdminCategorySidebarTreeSection.categoryTreeRoot}}" stepKey="dragAndDropCategory"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml index 441c9cd5eab8b..ff68bba78cae8 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/EndToEndB2CGuestUserTest/EndToEndB2CGuestUserTest.xml @@ -57,7 +57,9 @@ </after> <!--Re-index--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Step 1: User browses catalog --> <comment userInput="Start of browsing catalog" stepKey="startOfBrowsingCatalog"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml index 9ee56c02c7710..845299b3e33bb 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetSimpleProductTest.xml @@ -23,8 +23,7 @@ <!-- A Cms page containing the New Products Widget gets created here via extends --> <!-- Create a Simple Product to appear in the widget --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductBtn}}" stepKey="clickAddProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> <fillField selector="{{AdminProductFormSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="fillProductSku"/> @@ -32,7 +31,7 @@ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQuantity"/> <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here via merge --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml index a4e0d8708eb49..9f49b2abcea7a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/NewProductsListWidgetVirtualProductTest.xml @@ -25,8 +25,7 @@ <!-- Create a Virtual Product to appear in the widget --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductButton"/> <click selector="{{AdminProductGridActionSection.addVirtualProduct}}" stepKey="clickAddVirtualProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> @@ -35,7 +34,7 @@ <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="fillProductQuantity"/> <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml index 9b5fa25085e1a..7fd752d7df98d 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/ProductAvailableAfterEnablingSubCategoriesTest.xml @@ -37,23 +37,28 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront2"/> - <waitForPageLoad stepKey="waitForCategoryStorefront"/> - <dontSeeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="dontSeeCreatedProduct"/> - <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="onCategoryIndexPage"/> - <click selector="{{AdminCategorySidebarTreeSection.expandAll}}" stepKey="expandAll"/> - <click selector="{{AdminCategorySidebarTreeSection.categoryInTree($$simpleSubCategory.name$$)}}" stepKey="clickOnCreatedSimpleSubCategoryBeforeDelete"/> - <waitForPageLoad stepKey="AdminCategoryEditPageLoad"/> - <click selector="{{AdminCategoryBasicFieldSection.enableCategoryLabel}}" stepKey="EnableCategory"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AssertStorefrontProductAbsentOnCategoryPageActionGroup" stepKey="doNotSeeProductOnCategoryPage"> + <argument name="categoryUrlKey" value="$$createCategory.name$$"/> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$simpleSubCategory$$"/> + </actionGroup> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="enableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="seeSuccessMessage"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> - <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryStorefront"/> - <waitForPageLoad stepKey="waitForCategoryStorefrontPage"/> - <seeElement selector="{{StorefrontCategoryProductSection.ProductImageByName($$createSimpleProduct.name$$)}}" stepKey="seeCreatedProduct"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomepage"/> + <actionGroup ref="StorefrontGoToCategoryPageActionGroup" stepKey="openEnabledCategory"> + <argument name="categoryName" value="$$createCategory.name$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductNameOnProductMainPageActionGroup" stepKey="seeCreatedProduct"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml index b206a33ebde88..2ff5f0cadcfe7 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SaveProductWithCustomOptionsSecondWebsiteTest.xml @@ -15,51 +15,40 @@ <title value="You should be able to save a product with custom options assigned to a different website"/> <description value="Custom Options should not be split when saving the product after assigning to a different website"/> <severity value="BLOCKER"/> - <testCaseId value="MAGETWO-91436"/> + <testCaseId value="MC-25687"/> <group value="product"/> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <!--Create new website --> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - - <!--Create new Store Group --> <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <waitForPageLoad stepKey="waitForAdminSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption userInput="1" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickStoreViewSaveButton"/> - <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationModal" /> - <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="AcceptNewStoreViewCreation"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReolad"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage" /> + <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> </before> + <after> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteTestWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> <!--Create a Simple Product with Custom Options --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToCatalogProductGrid"/> - <waitForPageLoad stepKey="waitForCatalogProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToCatalogProductGrid"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProductDropdown"/> <click selector="{{AdminProductGridActionSection.addSimpleProduct}}" stepKey="clickAddSimpleProduct"/> <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> @@ -88,19 +77,17 @@ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Radio Option', '2')}}" userInput="7" stepKey="fillOptionValuePrice3"/> <!--Save the product with custom options --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> - <waitForLoadingMaskToDisappear stepKey="waitProductPageSave"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeProductSavedMessage"/> <!-- Add this product to second website --> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsitesSection1"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> <waitForLoadingMaskToDisappear stepKey="waitForProductPagetoSaveAgain"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessageAgain"/> <click selector="{{AdminProductCustomizableOptionsSection.customizableOptions}}" stepKey="openCustomOptionsSection2"/> <seeNumberOfElements selector=".admin__dynamic-rows[data-index='values'] tr.data-row" userInput="3" stepKey="see4RowsOfOptions"/> - </test> </tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml index 7b2e004495fea..f6292a3a96c40 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/SimpleProductTwoCustomOptionsTest.xml @@ -23,8 +23,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Create product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateSimpleProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> @@ -37,7 +36,7 @@ <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> <argument name="product" value="SimpleProduct3"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- opens the custom option panel and clicks add options --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml index e109dcb0deea5..cde7b14614f8e 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontProductsDisplayUsingElasticSearchTest.xml @@ -114,8 +114,12 @@ </createData> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="performReindex"/> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="performReindex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml index 489be97a9927a..e1b5aca6382e9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreLevelTest.xml @@ -74,8 +74,12 @@ </actionGroup> <!-- Logout Admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterDeletion"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDeletion"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create widget for recently viewed products--> <actionGroup ref="AdminEditCMSPageContentActionGroup" stepKey="clearRecentlyViewedWidgetsFromCMSContentBefore"> @@ -95,7 +99,9 @@ <argument name="buttonToShowSection2" value="3"/> </actionGroup> <!-- Warm up cache --> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterWidgetCreated"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterWidgetCreated"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to product 3 on store front --> <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct2.name$)}}" stepKey="goToStoreOneProductPageTwo"/> <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct3.name$)}}" stepKey="goToStoreOneProductPageThree"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml index bc93b3e6e3c45..0117493906de1 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StoreFrontRecentlyViewedAtStoreViewLevelTest.xml @@ -66,8 +66,12 @@ <!-- Logout Admin --> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterDeletion"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDeletion"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create widget for recently viewed products--> @@ -88,7 +92,9 @@ <argument name="buttonToShowSection2" value="3"/> </actionGroup> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterWidgetCreated"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterWidgetCreated"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to product 3 on store front --> <amOnPage url="{{StorefrontProductPage.url($createSimpleProduct2.name$)}}" stepKey="goToStore1ProductPage2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml index 8955f43e1b335..3ff477070cc30 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithCategoryFilterTest.xml @@ -44,7 +44,9 @@ <!-- Set the category filter to be present on the category page layered navigation --> <magentoCLI command="config:set {{EnableCategoryFilterOnCategoryPageConfigData.path}} {{EnableCategoryFilterOnCategoryPageConfigData.value}}" stepKey="setCategoryFilterVisibleOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="clearCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml index 7900a712e0664..a2316efb3a743 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCategoryPageWithoutCategoryFilterTest.xml @@ -44,7 +44,9 @@ <!-- Set the category filter to NOT be present on the category page layered navigation --> <magentoCLI command="config:set {{DisableCategoryFilterOnCategoryPageConfigData.path}} {{DisableCategoryFilterOnCategoryPageConfigData.value}}" stepKey="hideCategoryFilterOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml index b13c3827c6727..a73bd5a533ad0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckDefaultNumberProductsToDisplayTest.xml @@ -188,8 +188,12 @@ <seeInField selector="{{AdminCatalogStorefrontConfigSection.productsPerPageDefaultValue}}" userInput="12" stepKey="seeDefaultValueProductPerPage"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Open storefront on the category page --> <comment userInput="Open storefront on the category page" stepKey="commentOpenStorefront"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml new file mode 100644 index 0000000000000..507e4ae14e83c --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontCheckNoAppearDefaultOptionConfigurableProductTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckNoAppearDefaultOptionConfigurableProductTest"> + <annotations> + <stories value="Configurable Product"/> + <title value="Check for Configurable Product the default option doesn't appear."/> + <description value="Check for Configurable Product the default option doesn't appear on the list options product when an option use."/> + <testCaseId value="MC-35074"/> + <severity value="CRITICAL"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute"> + <argument name="productAttributeLabel" value="{{colorProductAttribute.default_label}}" /> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AdminFillBasicValueConfigurableProductActionGroup" stepKey="fillBasicValue"> + <argument name="product" value="_defaultProduct"/> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup" stepKey="createOptions"/> + <actionGroup ref="AdminGotoSelectValueAttributePageActionGroup" stepKey="gotoSelectValuePage"> + <argument name="defaultLabelAttribute" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <actionGroup ref="AdminSelectValueFromAttributeActionGroup" stepKey="selectColorProductAttribute2"> + <argument name="option" value="colorProductAttribute2"/> + </actionGroup> + <actionGroup ref="AdminSelectValueFromAttributeActionGroup" stepKey="selectColorProductAttribute3"> + <argument name="option" value="colorProductAttribute3"/> + </actionGroup> + <actionGroup ref="AdminSetQuantityToEachSkusConfigurableProductActionGroup" stepKey="saveConfigurable"/> + <grabValueFrom selector="{{NewProductPageSection.sku}}" stepKey="grabSkuProduct"/> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + + <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="expandOption"> + <argument name="categoryName" value="$$createCategory.name$$"/> + <argument name="attributeDefaultLabel" value="{{colorProductAttribute.default_label}}"/> + </actionGroup> + <dontSeeElement selector="{{LayeredNavigationSection.filterOptionContent(colorProductAttribute.default_label,colorProductAttribute1.name)}}" stepKey="dontSeeCaptchaField"/> + <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteConfigurableProduct"> + <argument name="sku" value="$grabSkuProduct"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml new file mode 100644 index 0000000000000..dc608a7f12dd3 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontOnlyXProductLeftForSimpleProductsTest.xml @@ -0,0 +1,38 @@ +<?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="StorefrontOnlyXProductLeftForSimpleProductsTest"> + <annotations> + <features value="Catalog"/> + <title value="See Only * Left block"/> + <stories value="See Only * Left on product page if Only X left Threshold was set"/> + <description value="See Only * Left on product page if Only X left Threshold was set"/> + <testCaseId value="MC-35235"/> + <severity value="MINOR"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} 10000" stepKey="setStockThresholdQty"/> + <magentoCLI command="cache:flush config" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{CatalogInventoryOptionsOnlyXleftThreshold.path}} {{CatalogInventoryOptionsOnlyXleftThreshold.value}}" stepKey="removedStockThresholdQty"/> + </after> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <seeElement selector="{{StorefrontProductPageSection.onlyProductsLeft}}" stepKey="seeOnlyLeftBlock"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml index bcd5d7b851db3..67ca04a0a4594 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithDoubleQuoteTest.xml @@ -26,8 +26,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!--Create product via admin--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToProductCreatePage"> <argument name="product" value="SimpleProductNameWithDoubleQuote"/> </actionGroup> @@ -41,7 +40,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="goToCategoryPage"/> @@ -52,7 +53,9 @@ <click selector="{{StorefrontCategoryProductSection.ProductTitleByName(SimpleProductNameWithDoubleQuote.name)}}" stepKey="clickProductToGoProductPage"/> <waitForPageLoad stepKey="waitForProductDisplayPageLoad"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{SimpleProductNameWithDoubleQuote.name}}" stepKey="seeCorrectName"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{SimpleProductNameWithDoubleQuote.sku}}" stepKey="seeCorrectSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku"> + <argument name="productSku" value="{{SimpleProductNameWithDoubleQuote.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="${{SimpleProductNameWithDoubleQuote.price}}" stepKey="seeCorrectPrice"/> <seeElement selector="{{StorefrontProductInfoMainSection.productImageSrc(ProductImage.fileName)}}" stepKey="seeCorrectImage"/> <see selector="{{StorefrontProductInfoMainSection.stock}}" userInput="In Stock" stepKey="seeInStock"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml index bd2c22c90318a..2156178ea88d0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontProductNameWithDoubleQuoteTest/StorefrontProductNameWithHTMLEntitiesTest.xml @@ -33,7 +33,9 @@ </after> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Check product in category listing--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategoryOne.name$$)}}" stepKey="navigateToCategoryPage"/> @@ -46,7 +48,9 @@ <waitForPageLoad stepKey="waitForProductDisplayPageLoad2"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{productWithHTMLEntityOne.name}}" stepKey="seeCorrectName"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{productWithHTMLEntityOne.sku}}" stepKey="seeCorrectSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku"> + <argument name="productSku" value="{{productWithHTMLEntityOne.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="${{productWithHTMLEntityOne.price}}" stepKey="seeCorrectPrice"/> <!--Veriy the breadcrumbs on Product Display page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml index 767e0c88b7af2..2080aee933aad 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductCustomOptionsDifferentStoreViewsTest.xml @@ -67,7 +67,7 @@ <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogoutStorefront"/> @@ -75,8 +75,7 @@ <!-- Open Product Grid, Filter product and open --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions"> <argument name="product" value="_defaultProduct"/> @@ -103,7 +102,7 @@ <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('Custom Options 1', '1')}}" userInput="option2" stepKey="fillOptionValueTitle2"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValuePrice('Custom Options 1', '1')}}" userInput="50" stepKey="fillOptionValuePrice2"/> <selectOption selector="{{AdminProductCustomizableOptionsSection.clickSelectPriceType('Custom Options 1', '1')}}" userInput="percent" stepKey="clickSelectPriceType"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton1"/> <!-- Switcher to Store FR--> <actionGroup ref="AdminSwitchStoreViewActionGroup" stepKey="switchToStoreFR"> @@ -127,7 +126,7 @@ <click selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="clickHiddenRequireMessage"/> <uncheckOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="uncheckUseDefaultOptionValueTitle2"/> <fillField selector="{{AdminProductCustomizableOptionsSection.fillOptionValueTitle('FR Custom Options 1', '1')}}" userInput="FR option2" stepKey="fillOptionValueTitle4"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton2"/> <!-- Login Customer Storefront --> @@ -181,9 +180,7 @@ <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!--Select payment method--> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> <!-- Place Order --> @@ -258,9 +255,7 @@ <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod2"/> - <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton2"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext2"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext2"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext2"/> <!--Select payment method--> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod2"/> @@ -272,8 +267,7 @@ <!-- Open Product Grid, Filter product and open --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad15"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage1"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="filterGroupedProductOptions1"> <argument name="product" value="_defaultProduct"/> @@ -304,7 +298,7 @@ <waitForPageLoad time="30" stepKey="waitForPageLoad19"/> <checkOption selector="{{AdminProductCustomizableOptionsSection.useDefaultOptionTitleByIndex('1')}}" stepKey="checkUseDefaultOptionValueTitle2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton3"/> <!--Go to Product Page--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml index 09b596f298e0f..e768f7ba5317a 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsTest.xml @@ -115,9 +115,7 @@ <see selector="{{CheckoutPaymentSection.ProductOptionsActiveByProductItemName($$createProduct.name$$)}}" userInput="1:00 AM" stepKey="seeProductOptionTimeInput" /> <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!--Select payment method--> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> <!-- Place Order --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml index 95e48e63419d3..aac76999636b0 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontPurchaseProductWithCustomOptionsWithLongValuesTitleTest.xml @@ -79,9 +79,7 @@ <!--Select shipping method--> <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml index c8872425552be..78fbed1aef3a9 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRememberCategoryPaginationTest.xml @@ -61,7 +61,9 @@ <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="defaultCategory2" stepKey="deleteCategory2"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> </test> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml new file mode 100644 index 0000000000000..914ac3444db22 --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontRemoveProductFromCompareSidebarTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRemoveProductFromCompareSidebarTest"> + <annotations> + <title value="Verify that the product isn't removed on clicking the product name"/> + <stories value="Verify that the product isn't removed on clicking the product name"/> + <description value="Verify that the product isn't removed on clicking the product name, but it's redirected to product page"/> + <features value="Catalog"/> + <severity value="MINOR"/> + <group value="Catalog"/> + <testCaseId value="MC-35068"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="defaultCategory"/> + <createData entity="SimpleProduct" stepKey="simpleProduct"> + <requiredEntity createDataKey="defaultCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="simpleProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="defaultCategory" stepKey="deleteCategory"/> + </after> + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPage"> + <argument name="category" value="$$defaultCategory$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddCategoryProductToCompareActionGroup" stepKey="addProductToCompareList"> + <argument name="productVar" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontClickOnProductFromSidebarCompareListActionGroup" stepKey="clickOnComparingProductLink"> + <argument name="product" value="$$simpleProduct$$"/> + </actionGroup> + <actionGroup ref="StorefrontCheckProductUrlActionGroup" stepKey="checkProductPageUrl"> + <argument name="productUrl" value="$$simpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml new file mode 100644 index 0000000000000..14001ecbb52af --- /dev/null +++ b/app/code/Magento/Catalog/Test/Mftf/Test/StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -0,0 +1,234 @@ +<?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="StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest"> + <annotations> + <features value="Catalog"/> + <stories value="Product Categories Indexer"/> + <title value="Verify Category Product and Product Category partial reindex"/> + <description value="Verify that Merchant Developer can use console commands to perform partial reindex for Category Products, Product Categories, and Catalog Search"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-11386"/> + <useCaseId value="MAGETWO-88184"/> + <group value="catalog"/> + <group value="indexer"/> + </annotations> + <before> + <!-- Change "Category Products", "Product Categories" and "Catalog Search" indexers to "Update by Schedule" mode --> + <magentoCLI command="indexer:set-mode" arguments="schedule catalog_category_product catalog_product_category catalogsearch_fulltext" stepKey="setIndexerMode"/> + + <!-- Create categories K, L, M, N with different nesting in the tree and Anchor = Yes/No--> + <!-- Category K is an anchor category --> + <createData entity="_defaultCategory" stepKey="categoryK"/> + <!-- Category L is a non-anchor subcategory of category K --> + <createData entity="SubCategoryNonAnchor" stepKey="categoryL"> + <requiredEntity createDataKey="categoryK"/> + </createData> + <!-- Category M is a subcategory of category L --> + <createData entity="SubCategoryWithParent" stepKey="categoryM"> + <requiredEntity createDataKey="categoryL"/> + </createData> + <!-- Category N is a subcategory of category K --> + <createData entity="SubCategoryWithParent" stepKey="categoryN"> + <requiredEntity createDataKey="categoryK"/> + </createData> + + <!-- Create different Products with different settings, assign to categories: --> + <!-- Product A in 0 categories, i.e. not assigned to any category --> + <createData entity="SimpleProduct2" stepKey="productA"/> + <!-- Product B in 1 category M --> + <createData entity="SimpleProduct3" stepKey="productB"> + <requiredEntity createDataKey="categoryM"/> + </createData> + <!-- Product C in 2 categories M and N --> + <createData entity="SimpleProduct2" stepKey="productC"/> + <createData entity="AssignProductToCategory" stepKey="assignCategoryMToProductC"> + <requiredEntity createDataKey="categoryM"/> + <requiredEntity createDataKey="productC"/> + </createData> + <createData entity="AssignProductToCategory" stepKey="assignCategoryNToProductC"> + <requiredEntity createDataKey="categoryN"/> + <requiredEntity createDataKey="productC"/> + </createData> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </before> + <after> + <!-- Change indexers to "Update on Save" mode --> + <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setRealtimeMode"/> + + <!-- Delete data --> + <deleteData createDataKey="productA" stepKey="deleteProductA"/> + <deleteData createDataKey="productB" stepKey="deleteProductB"/> + <deleteData createDataKey="productC" stepKey="deleteProductC"/> + <deleteData createDataKey="categoryN" stepKey="deleteCategoryN"/> + <deleteData createDataKey="categoryM" stepKey="deleteCategoryM"/> + <deleteData createDataKey="categoryL" stepKey="deleteCategoryL"/> + <deleteData createDataKey="categoryK" stepKey="deleteCategoryK"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <!-- Open categories K, L, M, N on Storefront --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="onCategoryK"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryK"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="onCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="seeMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProducts"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="onCategoryM"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBOnCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryM"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="onCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCOnCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBOnCategoryN"/> + + <!-- Assign category K to Product A --> + <createData entity="AssignProductToCategory" stepKey="assignCategoryKToProductA"> + <requiredEntity createDataKey="categoryK"/> + <requiredEntity createDataKey="productA"/> + </createData> + + <!-- Unassign category M from Product B --> + <deleteData url="/V1/categories/$categoryM.id$/products/$productB.sku$" stepKey="unassignCategoryMFromProductB"/> + + <!-- Assign category L to Product C --> + <createData entity="AssignProductToCategory" stepKey="assignCategoryLToProductC"> + <requiredEntity createDataKey="categoryL"/> + <requiredEntity createDataKey="productC"/> + </createData> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are not applied yet --> + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="amOnCategoryK"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryK"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductACategoryN"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="amOnCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="seeEmptyMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontseeProduct"/> + + <!-- Category M contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="amOnCategoryM"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryM"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAInCategoryM"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="amOnCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductInCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAInCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBInCategoryN"/> + + <!-- Run cron --> + <magentoCron groups="index" stepKey="runCronIndex"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are applied --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryK"/> + <see userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAOnCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryKWithProductC"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryKWithProductB"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryL"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLWithProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLWithProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLWithProductB"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryM"/> + <waitForPageLoad stepKey="waitForStorefrontCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMAndProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMAndProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMAndProductB"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="storefrontCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCAndCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAAndCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBAndCategoryN"/> + + <!-- Remove Product A assignment for category K --> + <deleteData url="/V1/categories/$categoryK.id$/products/$productA.sku$" stepKey="unassignCategoryKFromProductA"/> + + <!-- Remove Product C assignment for category L --> + <deleteData url="/V1/categories/$categoryL.id$/products/$productC.sku$" stepKey="unassignCategoryLFromProductC"/> + + <!-- Add Product B assignment for category N --> + <createData entity="AssignProductToCategory" stepKey="assignCategoryNToProductB"> + <requiredEntity createDataKey="categoryN"/> + <requiredEntity createDataKey="productB"/> + </createData> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are not applied yet --> + <!-- Category K contains only Products A, C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryK"/> + <see userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductAWithCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductC"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductB"/> + + <!-- Category L contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryL"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryLAndProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLAndProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryLAndProductB"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMWithProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMWithProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMWithProductB"/> + + <!-- Category N contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="onStorefrontCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnTheCategoryN"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductBOnTheCategoryN"/> + + <!-- Run Cron once to reindex product changes --> + <magentoCron groups="index" stepKey="runCronIndex2"/> + + <!-- Open categories K, L, M, N on Storefront in order to make sure that new assignments are applied --> + + <!-- Category K contains only Products B & C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryK"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productBOnCategoryK"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="productCOnCategoryK"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAOnTheCategoryK"/> + + <!-- Category L contains no Products --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryL"/> + <see userInput="We can't find products matching the selection." selector="{{StorefrontCategoryMainSection.emptyProductMessage}}" stepKey="noProductsMessage"/> + <dontSeeElement selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductsOnCategoryL"/> + + <!-- Category M contains only Product C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryL.custom_attributes[url_key]$/$categoryM.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryM"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeCategoryMPageAndProductC"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMPageAndProductA"/> + <dontSee userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeCategoryMPageAndProductB"/> + + <!-- Category N contains only Products B and C --> + <amOnPage url="{{StorefrontCategoryPage.url($categoryK.custom_attributes[url_key]$/$categoryN.custom_attributes[url_key]$)}}" stepKey="onFrontendCategoryN"/> + <see userInput="$productB.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductBAndCategoryN"/> + <see userInput="$productC.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="seeProductCCategoryN"/> + <dontSee userInput="$productA.name$" selector="{{StorefrontCategoryMainSection.productName}}" stepKey="dontSeeProductAWithCategoryN"/> + </test> +</tests> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml index 26cebae318cd9..9c68c08064081 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/TieredPricingAndQuantityIncrementsWorkWithDecimalinventoryTest.xml @@ -37,11 +37,11 @@ stepKey="clickOpenProductForEdit"/> <waitForPageLoad time="30" stepKey="waitForProductEditOpen"/> <!--Step2. Open *Advanced Inventory* pop-up (Click on *Advanced Inventory* link). Set *Qty Uses Decimals* to *Yes*. Click on button *Done* --> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink"/> <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="scrollToQtyUsesDecimalsDropBox"/> <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimals}}" stepKey="clickOnQtyUsesDecimalsDropBox"/> <click selector="{{AdminProductFormAdvancedInventorySection.qtyUsesDecimalsOptions('1')}}" stepKey="chooseYesOnQtyUsesDecimalsDropBox"/> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton"/> <!--Step3. Open *Advanced Pricing* pop-up (Click on *Advanced Pricing* link). Click on *Add* button. Fill *0.5* in *Quantity*--> <scrollTo selector="{{AdminProductFormSection.productName}}" stepKey="scrollToProductName"/> <click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink1"/> @@ -50,7 +50,7 @@ <fillField selector="{{AdminProductFormAdvancedPricingSection.productTierPriceQtyInput('0')}}" userInput="0.5" stepKey="fillProductTierPriceQty"/> <!--Step4. Close *Advanced Pricing* (Click on button *Done*). Save *prod1* (Click on button *Save*)--> <click selector="{{AdminProductFormAdvancedPricingSection.doneButton}}" stepKey="clickOnDoneButton2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <!--The code should be uncommented after fix MAGETWO-96016--> <!--<click selector="{{AdminProductFormSection.advancedPricingLink}}" stepKey="clickOnAdvancedPricingLink2"/>--> @@ -58,8 +58,7 @@ <!--<click selector="{{AdminProductFormAdvancedPricingSection.advancedPricingCloseButton}}" stepKey="clickOnCloseButton"/>--> <!--Step5. Open *Advanced Inventory* pop-up. Set *Enable Qty Increments* to *Yes*. Fill *.5* in *Qty Increments*--> - <click selector="{{AdminProductFormSection.advancedInventoryLink}}" stepKey="clickOnAdvancedInventoryLink2"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminClickOnAdvancedInventoryLinkActionGroup" stepKey="clickOnAdvancedInventoryLink2"/> <scrollTo selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="scrollToEnableQtyIncrements"/> <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrementsUseConfigSettings}}" stepKey="clickOnEnableQtyIncrementsUseConfigSettingsCheckbox"/> <click selector="{{AdminProductFormAdvancedInventorySection.enableQtyIncrements}}" stepKey="clickOnEnableQtyIncrements"/> @@ -69,8 +68,8 @@ <scrollTo selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" stepKey="scrollToQtyIncrements"/> <fillField selector="{{AdminProductFormAdvancedInventorySection.qtyIncrements}}" userInput=".5" stepKey="fillQtyIncrements"/> <!--Step6. Close *Advanced Inventory* (Click on button *Done*). Save *prod1* (Click on button *Save*) --> - <click selector="{{AdminProductFormAdvancedInventorySection.doneButton}}" stepKey="clickOnDoneButton3"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <actionGroup ref="AdminSubmitAdvancedInventoryFormActionGroup" stepKey="clickOnDoneButton3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton2"/> <!--Step7. Open *Customer view* (Go to *Store Front*). Open *prod1* page (Find via search and click on product name) --> <amOnPage url="{{StorefrontHomePage.url}}$$createPreReqSimpleProduct.custom_attributes[url_key]$$.html" stepKey="amOnProductPage"/> <!--Step8. Fill *1.5* in *Qty*. Click on button *Add to Cart*--> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml index b4514c9b53736..e7ba97ad36785 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyCategoryProductAndProductCategoryPartialReindexTest.xml @@ -8,16 +8,19 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="VerifyCategoryProductAndProductCategoryPartialReindexTest"> + <test name="VerifyCategoryProductAndProductCategoryPartialReindexTest" deprecated="Use StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest instead."> <annotations> <features value="Catalog"/> <stories value="Product Categories Indexer"/> - <title value="Verify Category Product and Product Category partial reindex"/> + <title value="DEPRECATED. Verify Category Product and Product Category partial reindex"/> <description value="Verify that Merchant Developer can use console commands to perform partial reindex for Category Products, Product Categories, and Catalog Search"/> <severity value="BLOCKER"/> <testCaseId value="MC-11386"/> <group value="catalog"/> <group value="indexer"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontVerifyCategoryProductAndProductCategoryPartialReindexTest instead.</issueId> + </skip> </annotations> <before> <!-- Change "Category Products" and "Product Categories" indexers to "Update by Schedule" mode --> @@ -62,7 +65,9 @@ <after> <!-- Change "Category Products" and "Product Categories" indexers to "Update on Save" mode --> <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setRealtimeMode"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Delete data --> <deleteData createDataKey="productA" stepKey="deleteProductA"/> @@ -144,7 +149,7 @@ <!-- Run cron --> <wait stepKey="waitBeforeRunMagentoCron" time="30"/> <magentoCLI stepKey="runMagentoCron" command="cron:run --group=index"/> - <wait stepKey="waitAfterRunMagentoCron" time="60"/> + <wait stepKey="waitAfterRunMagentoCron" time="90"/> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> <!-- Category K contains only Products A, C --> @@ -210,7 +215,7 @@ <!-- Run Cron once to reindex product changes --> <wait stepKey="waitBeforeRunCronIndexAfterProductAssignToCategory" time="30"/> <magentoCLI stepKey="runCronIndexAfterProductAssignToCategory" command="cron:run --group=index"/> - <wait stepKey="waitAfterRunCronIndexAfterProductAssignToCategory" time="60"/> + <wait stepKey="waitAfterRunCronIndexAfterProductAssignToCategory" time="90"/> <!-- Open categories K, L, M, N on Storefront in order to make sure that new assigments are applied --> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml index a15081e0cbda3..d7e4f97ed0bc2 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnCatalogTest.xml @@ -37,7 +37,7 @@ <seeElement selector="{{TinyMCESection.InsertImageBtn}}" stepKey="insertImage"/> <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget" /> <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable" /> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCatalog"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCatalog"/> <!-- Go to storefront product page, assert product content --> <amOnPage url="/{{SimpleSubCategory.name_lwr}}.html" stepKey="goToCategoryFrontPage"/> <waitForPageLoad stepKey="waitForPageLoad2"/> diff --git a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml index 1e4adedfc168d..cffc4af6fcbbd 100644 --- a/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml +++ b/app/code/Magento/Catalog/Test/Mftf/Test/VerifyTinyMCEv4IsNativeWYSIWYGOnProductTest.xml @@ -44,13 +44,13 @@ <see selector="{{ProductDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage1"/> <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget1" /> <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable1" /> - <scrollTo selector="{{ProductDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="scrollToDesShowHideBtn2" /> + <scrollTo selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" y="-150" x="0" stepKey="scrollToDesShowHideBtn2" /> <click selector="{{ProductShortDescriptionWYSIWYGToolbarSection.showHideBtn}}" stepKey="clickShowHideBtn2" /> <waitForElementVisible selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" stepKey="waitForInsertImage2" /> <see selector="{{ProductShortDescriptionWYSIWYGToolbarSection.InsertImageBtn}}" userInput="Insert Image..." stepKey="seeInsertImage2"/> <dontSee selector="{{TinyMCESection.InsertWidgetBtn}}" stepKey="insertWidget2" /> <dontSee selector="{{TinyMCESection.InsertVariableBtn}}" stepKey="insertVariable2" /> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Go to storefront product page, assert product content --> <amOnPage url="{{_defaultProduct.name}}.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForPageLoad2"/> diff --git a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php index d1b01db75927c..157f641335497 100644 --- a/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Block/Product/ListProductTest.php @@ -235,7 +235,7 @@ public function testGetAddToCartPostParams() ->willReturn(true); $this->cartHelperMock->expects($this->any()) ->method('getAddUrl') - ->with($this->productMock, []) + ->with($this->productMock, ['_escape' => false]) ->willReturn($url); $this->productMock->expects($this->once()) ->method('getEntityId') diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php index ca35d49113f41..681cef8489796 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/SaveTest.php @@ -7,10 +7,12 @@ namespace Magento\Catalog\Test\Unit\Controller\Adminhtml\Product\Attribute; +use Magento\Backend\Model\Session; use Magento\Backend\Model\View\Result\Redirect as ResultRedirect; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Controller\Adminhtml\Product\Attribute\Save; use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\Catalog\Model\Product\Attribute\Frontend\Inputtype\Presentation; use Magento\Catalog\Model\Product\AttributeSet\Build; use Magento\Catalog\Model\Product\AttributeSet\BuildFactory; use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; @@ -31,63 +33,64 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.TooManyFields) */ class SaveTest extends AttributeTest { /** * @var BuildFactory|MockObject */ - protected $buildFactoryMock; + private $buildFactoryMock; /** * @var FilterManager|MockObject */ - protected $filterManagerMock; + private $filterManagerMock; /** * @var ProductHelper|MockObject */ - protected $productHelperMock; + private $productHelperMock; /** * @var AttributeFactory|MockObject */ - protected $attributeFactoryMock; + private $attributeFactoryMock; /** * @var ValidatorFactory|MockObject */ - protected $validatorFactoryMock; + private $validatorFactoryMock; /** * @var CollectionFactory|MockObject */ - protected $groupCollectionFactoryMock; + private $groupCollectionFactoryMock; /** * @var LayoutFactory|MockObject */ - protected $layoutFactoryMock; + private $layoutFactoryMock; /** * @var ResultRedirect|MockObject */ - protected $redirectMock; + private $redirectMock; /** - * @var AttributeSet|MockObject + * @var AttributeSetInterface|MockObject */ - protected $attributeSetMock; + private $attributeSetMock; /** * @var Build|MockObject */ - protected $builderMock; + private $builderMock; /** * @var InputTypeValidator|MockObject */ - protected $inputTypeValidatorMock; + private $inputTypeValidatorMock; /** * @var FormData|MockObject @@ -104,19 +107,34 @@ class SaveTest extends AttributeTest */ private $attributeCodeValidatorMock; + /** + * @var Presentation|MockObject + */ + private $presentationMock; + + /** + * @var Session|MockObject + */ + + private $sessionMock; + protected function setUp(): void { parent::setUp(); + $this->filterManagerMock = $this->createMock(FilterManager::class); + $this->productHelperMock = $this->createMock(ProductHelper::class); + $this->attributeSetMock = $this->createMock(AttributeSetInterface::class); + $this->builderMock = $this->createMock(Build::class); + $this->inputTypeValidatorMock = $this->createMock(InputTypeValidator::class); + $this->formDataSerializerMock = $this->createMock(FormData::class); + $this->attributeCodeValidatorMock = $this->createMock(AttributeCodeValidator::class); + $this->presentationMock = $this->createMock(Presentation::class); + $this->sessionMock = $this->createMock(Session::class); + $this->layoutFactoryMock = $this->createMock(LayoutFactory::class); $this->buildFactoryMock = $this->getMockBuilder(BuildFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->filterManagerMock = $this->getMockBuilder(FilterManager::class) - ->disableOriginalConstructor() - ->getMock(); - $this->productHelperMock = $this->getMockBuilder(ProductHelper::class) - ->disableOriginalConstructor() - ->getMock(); $this->attributeFactoryMock = $this->getMockBuilder(AttributeFactory::class) ->setMethods(['create']) ->disableOriginalConstructor() @@ -129,32 +147,23 @@ protected function setUp(): void ->setMethods(['create']) ->disableOriginalConstructor() ->getMock(); - $this->layoutFactoryMock = $this->getMockBuilder(LayoutFactory::class) - ->disableOriginalConstructor() - ->getMock(); $this->redirectMock = $this->getMockBuilder(ResultRedirect::class) ->setMethods(['setData', 'setPath']) ->disableOriginalConstructor() ->getMock(); - $this->attributeSetMock = $this->getMockBuilder(AttributeSetInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - $this->builderMock = $this->getMockBuilder(Build::class) - ->disableOriginalConstructor() - ->getMock(); - $this->inputTypeValidatorMock = $this->getMockBuilder(InputTypeValidator::class) - ->disableOriginalConstructor() - ->getMock(); - $this->formDataSerializerMock = $this->getMockBuilder(FormData::class) - ->disableOriginalConstructor() - ->getMock(); - $this->attributeCodeValidatorMock = $this->getMockBuilder(AttributeCodeValidator::class) - ->disableOriginalConstructor() - ->getMock(); $this->productAttributeMock = $this->getMockBuilder(ProductAttributeInterface::class) - ->setMethods(['getId', 'get']) - ->getMockForAbstractClass(); - + ->setMethods( + [ + 'getId', + 'get', + 'getBackendTypeByInput', + 'getDefaultValueByInput', + 'getBackendType', + 'getFrontendClass', + 'addData', + 'save' + ] + )->getMockForAbstractClass(); $this->buildFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->builderMock); @@ -167,7 +176,7 @@ protected function setUp(): void } /** - * {@inheritdoc} + * @inheritdoc */ protected function getModel() { @@ -184,7 +193,9 @@ protected function getModel() 'groupCollectionFactory' => $this->groupCollectionFactoryMock, 'layoutFactory' => $this->layoutFactoryMock, 'formDataSerializer' => $this->formDataSerializerMock, - 'attributeCodeValidator' => $this->attributeCodeValidatorMock + 'attributeCodeValidator' => $this->attributeCodeValidatorMock, + 'presentation' => $this->presentationMock, + '_session' => $this->sessionMock ]); } @@ -214,6 +225,67 @@ public function testExecuteWithEmptyData() $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); } + public function testExecuteSaveFrontendClass() + { + $data = [ + 'frontend_input' => 'test_frontend_input', + ]; + + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap([ + ['isAjax', null, null], + ['serialized_options', '[]', ''], + ['set', null, 1], + ['attribute_code', null, 'test_attribute_code'], + ]); + $this->formDataSerializerMock + ->expects($this->once()) + ->method('unserialize') + ->with('') + ->willReturn([]); + $this->requestMock->expects($this->once()) + ->method('getPostValue') + ->willReturn($data); + $this->inputTypeValidatorMock->expects($this->any()) + ->method('isValid') + ->with($data['frontend_input']) + ->willReturn(true); + $this->presentationMock->expects($this->once()) + ->method('convertPresentationDataToInputType') + ->willReturn($data); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeSourceModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productHelperMock->expects($this->once()) + ->method('getAttributeBackendModelByInputType') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendTypeByInput') + ->with($data['frontend_input']) + ->willReturnSelf('test_backend_type'); + $this->productAttributeMock->expects($this->once()) + ->method('getDefaultValueByInput') + ->with($data['frontend_input']) + ->willReturn(null); + $this->productAttributeMock->expects($this->once()) + ->method('getBackendType') + ->willReturn('static'); + $this->productAttributeMock->expects($this->once()) + ->method('getFrontendClass') + ->willReturn('static'); + $this->resultFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->redirectMock); + $this->redirectMock->expects($this->any()) + ->method('setPath') + ->willReturnSelf(); + + $this->assertInstanceOf(ResultRedirect::class, $this->getModel()->execute()); + } + public function testExecute() { $data = [ diff --git a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php index 122089332f89b..371dac9a64b89 100644 --- a/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Controller/Adminhtml/Product/Attribute/ValidateTest.php @@ -153,7 +153,7 @@ public function testExecute() ->willReturnMap( [ [Attribute::class, [], $this->attributeMock], - [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] + [AttributeSet::class, [], $this->attributeSetMock] ] ); $this->attributeMock->expects($this->once()) @@ -188,6 +188,69 @@ public function testExecute() $this->assertInstanceOf(ResultJson::class, $this->getModel()->execute()); } + /** + * Test that editing existing attribute loads attribute by id + * + * @return void + * @throws NotFoundException + */ + public function testExecuteEditExisting(): void + { + $serializedOptions = '{"key":"value"}'; + $this->requestMock->expects($this->any()) + ->method('getParam') + ->willReturnMap( + [ + ['frontend_label', null, 'test_frontend_label'], + ['attribute_id', null, 10], + ['attribute_code', null, 'test_attribute_code'], + ['new_attribute_set_name', null, 'test_attribute_set_name'], + ['serialized_options', '[]', $serializedOptions], + ] + ); + $this->objectManagerMock->expects($this->exactly(2)) + ->method('create') + ->willReturnMap( + [ + [Attribute::class, [], $this->attributeMock], + [AttributeSet::class, [], $this->attributeSetMock] + ] + ); + $this->attributeMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->attributeMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn('test_attribute_code'); + + $this->attributeCodeValidatorMock->expects($this->once()) + ->method('isValid') + ->with('test_attribute_code') + ->willReturn(true); + + $this->requestMock->expects($this->once()) + ->method('has') + ->with('new_attribute_set_name') + ->willReturn(true); + $this->attributeSetMock->expects($this->once()) + ->method('setEntityTypeId') + ->willReturnSelf(); + $this->attributeSetMock->expects($this->once()) + ->method('load') + ->willReturnSelf(); + $this->attributeSetMock->expects($this->once()) + ->method('getId') + ->willReturn(false); + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($this->resultJson); + $this->resultJson->expects($this->once()) + ->method('setJsonData') + ->willReturnSelf(); + + $this->assertInstanceOf(ResultJson::class, $this->getModel()->execute()); + } + /** * @dataProvider provideUniqueData * @param array $options @@ -605,7 +668,7 @@ public function testExecuteWithOptionsDataError() ->willReturnMap( [ [Attribute::class, [], $this->attributeMock], - [\Magento\Eav\Model\Entity\Attribute\Set::class, [], $this->attributeSetMock] + [AttributeSet::class, [], $this->attributeSetMock] ] ); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php index edbbaebd0576b..05bd3ec2a3d33 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Attribute/OptionManagementTest.php @@ -10,10 +10,14 @@ use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Model\Product\Attribute\OptionManagement; use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeOptionUpdateInterface; use Magento\Eav\Api\Data\AttributeOptionInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Class to test management of attribute options + */ class OptionManagementTest extends TestCase { /** @@ -22,18 +26,28 @@ class OptionManagementTest extends TestCase protected $model; /** - * @var MockObject + * @var AttributeOptionManagementInterface|MockObject */ protected $eavOptionManagementMock; + /** + * @var AttributeOptionUpdateInterface|MockObject + */ + private $eavOptionUpdateMock; + protected function setUp(): void { $this->eavOptionManagementMock = $this->getMockForAbstractClass(AttributeOptionManagementInterface::class); + $this->eavOptionUpdateMock = $this->getMockForAbstractClass(AttributeOptionUpdateInterface::class); $this->model = new OptionManagement( - $this->eavOptionManagementMock + $this->eavOptionManagementMock, + $this->eavOptionUpdateMock ); } + /** + * Test to Retrieve list of attribute options + */ public function testGetItems() { $attributeCode = 10; @@ -44,6 +58,9 @@ public function testGetItems() $this->assertEquals([], $this->model->getItems($attributeCode)); } + /** + * Test to Add option to attribute + */ public function testAdd() { $attributeCode = 42; @@ -56,6 +73,9 @@ public function testAdd() $this->assertTrue($this->model->add($attributeCode, $optionMock)); } + /** + * Test to delete attribute option + */ public function testDelete() { $attributeCode = 'atrCde'; @@ -68,6 +88,9 @@ public function testDelete() $this->assertTrue($this->model->delete($attributeCode, $optionId)); } + /** + * Test to delete attribute option with invalid option id + */ public function testDeleteWithInvalidOption() { $this->expectException('Magento\Framework\Exception\InputException'); @@ -77,4 +100,24 @@ public function testDeleteWithInvalidOption() $this->eavOptionManagementMock->expects($this->never())->method('delete'); $this->model->delete($attributeCode, $optionId); } + + /** + * Test to update attribute option + */ + public function testUpdate() + { + $attributeCode = 'atrCde'; + $optionId = 10; + $optionMock = $this->getMockForAbstractClass(AttributeOptionInterface::class); + + $this->eavOptionUpdateMock->expects($this->once()) + ->method('update') + ->with( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode, + $optionId, + $optionMock + )->willReturn(true); + $this->assertTrue($this->model->update($attributeCode, $optionId, $optionMock)); + } } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php index 539489f18f404..0e6fb8ececf03 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/Type/FileTest.php @@ -24,6 +24,8 @@ use PHPUnit\Framework\TestCase; /** + * Test file option type + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FileTest extends TestCase @@ -142,6 +144,14 @@ protected function getFileObject() ); } + public function testGetFormattedOptionValueWithUnserializedValue() + { + $fileObject = $this->getFileObject(); + + $value = 'some unserialized value, 1, 2.test'; + $this->assertEquals($value, $fileObject->getFormattedOptionValue($value)); + } + public function testGetCustomizedView() { $fileObject = $this->getFileObject(); diff --git a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php index e03ea8c79cc8a..e46884d1637da 100644 --- a/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Model/Product/Option/ValueTest.php @@ -12,13 +12,11 @@ use Magento\Catalog\Model\Product\Option\Value; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\Collection; use Magento\Catalog\Model\ResourceModel\Product\Option\Value\CollectionFactory; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; use Magento\Catalog\Pricing\Price\CustomOptionPriceCalculator; use Magento\Framework\Pricing\Price\PriceInterface; use Magento\Framework\Pricing\PriceInfoInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** @@ -36,11 +34,6 @@ class ValueTest extends TestCase */ private $customOptionPriceCalculatorMock; - /** - * @var CalculateCustomOptionCatalogRule|MockObject - */ - private $CalculateCustomOptionCatalogRule; - protected function setUp(): void { $mockedResource = $this->getMockedResource(); @@ -50,10 +43,6 @@ protected function setUp(): void CustomOptionPriceCalculator::class ); - $this->CalculateCustomOptionCatalogRule = $this->createMock( - CalculateCustomOptionCatalogRule::class - ); - $helper = new ObjectManager($this); $this->model = $helper->getObject( Value::class, @@ -61,7 +50,6 @@ protected function setUp(): void 'resource' => $mockedResource, 'valueCollectionFactory' => $mockedCollectionFactory, 'customOptionPriceCalculator' => $this->customOptionPriceCalculatorMock, - 'CalculateCustomOptionCatalogRule' => $this->CalculateCustomOptionCatalogRule ] ); $this->model->setOption($this->getMockedOption()); @@ -89,8 +77,8 @@ public function testGetPrice() $this->assertEquals($price, $this->model->getPrice(false)); $percentPrice = 100.0; - $this->CalculateCustomOptionCatalogRule->expects($this->atLeastOnce()) - ->method('execute') + $this->customOptionPriceCalculatorMock->expects($this->atLeastOnce()) + ->method('getOptionPriceByPriceCode') ->willReturn($percentPrice); $this->assertEquals($percentPrice, $this->model->getPrice(true)); } diff --git a/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php new file mode 100644 index 0000000000000..c73e02fb7ecbf --- /dev/null +++ b/app/code/Magento/Catalog/Test/Unit/Model/ResourceModel/Category/AggregateCountTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Test\Unit\Model\ResourceModel\Category; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\ResourceModel\Category\AggregateCount; +use Magento\Catalog\Model\ResourceModel\Category as ResourceCategory; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Aggregate count model test + */ +class AggregateCountTest extends TestCase +{ + + /** + * @var AggregateCount + */ + protected $aggregateCount; + + /** + * @var ObjectManagerHelper + */ + protected $objectManagerHelper; + + /** + * @var Category|MockObject + */ + protected $categoryMock; + + /** + * @var ResourceCategory|MockObject + */ + protected $resourceCategoryMock; + + /** + * @var AdapterInterface|MockObject + */ + protected $connectionMock; + + /** + * {@inheritdoc} + */ + public function setUp(): void + { + $this->categoryMock = $this->createMock(Category::class); + $this->resourceCategoryMock = $this->createMock(ResourceCategory::class); + $this->connectionMock = $this->getMockBuilder(AdapterInterface::class) + ->getMockForAbstractClass(); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->aggregateCount = $this->objectManagerHelper->getObject(AggregateCount::class); + } + + /** + * @return void + */ + public function testProcessDelete(): void + { + $parentIds = 3; + $table = 'catalog_category_entity'; + + $this->categoryMock->expects($this->once()) + ->method('getResource') + ->willReturn($this->resourceCategoryMock); + $this->categoryMock->expects($this->once()) + ->method('getParentIds') + ->willReturn($parentIds); + $this->resourceCategoryMock->expects($this->any()) + ->method('getEntityTable') + ->willReturn($table); + $this->resourceCategoryMock->expects($this->once()) + ->method('getConnection') + ->willReturn($this->connectionMock); + $this->connectionMock->expects($this->once()) + ->method('update') + ->with( + $table, + ['children_count' => new \Zend_Db_Expr('children_count - 1')], + ['entity_id IN(?)' => $parentIds] + ); + $this->aggregateCount->processDelete($this->categoryMock); + } +} diff --git a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CalculateCustomOptionCatalogRuleTest.php b/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CalculateCustomOptionCatalogRuleTest.php deleted file mode 100644 index 894408048b536..0000000000000 --- a/app/code/Magento/Catalog/Test/Unit/Pricing/Price/CalculateCustomOptionCatalogRuleTest.php +++ /dev/null @@ -1,266 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Catalog\Test\Unit\Pricing\Price; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\PriceModifier\Composite as PriceModifier; -use Magento\Catalog\Pricing\Price\CalculateCustomOptionCatalogRule; -use Magento\Catalog\Pricing\Price\RegularPrice; -use Magento\Catalog\Pricing\Price\SpecialPrice; -use Magento\CatalogRule\Pricing\Price\CatalogRulePrice; -use Magento\Directory\Model\PriceCurrency; -use Magento\Framework\Pricing\PriceInfo\Base; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Test for CalculateCustomOptionCatalogRule class. - */ -class CalculateCustomOptionCatalogRuleTest extends TestCase -{ - /** - * @var Product|MockObject - */ - private $saleableItemMock; - - /** - * @var RegularPrice|MockObject - */ - private $regularPriceMock; - - /** - * @var SpecialPrice|MockObject - */ - private $specialPriceMock; - - /** - * @var CatalogRulePrice|MockObject - */ - private $catalogRulePriceMock; - - /** - * @var PriceModifier|MockObject - */ - private $priceModifierMock; - - /** - * @var CalculateCustomOptionCatalogRule - */ - private $calculateCustomOptionCatalogRule; - - /** - * @inheritdoc - */ - protected function setUp(): void - { - $objectManager = new ObjectManager($this); - $this->saleableItemMock = $this->createMock(Product::class); - $this->regularPriceMock = $this->createMock(RegularPrice::class); - $this->specialPriceMock = $this->createMock(SpecialPrice::class); - $this->catalogRulePriceMock = $this->createMock(CatalogRulePrice::class); - $priceInfoMock = $this->createMock(Base::class); - $this->saleableItemMock->expects($this->any()) - ->method('getPriceInfo') - ->willReturn($priceInfoMock); - $this->regularPriceMock->expects($this->any()) - ->method('getPriceCode') - ->willReturn(RegularPrice::PRICE_CODE); - $this->specialPriceMock->expects($this->any()) - ->method('getPriceCode') - ->willReturn(SpecialPrice::PRICE_CODE); - $this->catalogRulePriceMock->expects($this->any()) - ->method('getPriceCode') - ->willReturn(CatalogRulePrice::PRICE_CODE); - $priceInfoMock->expects($this->any()) - ->method('getPrices') - ->willReturn( - [ - 'regular_price' => $this->regularPriceMock, - 'special_price' => $this->specialPriceMock, - 'catalog_rule_price' => $this->catalogRulePriceMock - ] - ); - $priceInfoMock->expects($this->any()) - ->method('getPrice') - ->willReturnMap( - [ - ['regular_price', $this->regularPriceMock], - ['special_price', $this->specialPriceMock], - ['catalog_rule_price', $this->catalogRulePriceMock], - ] - ); - $priceCurrencyMock = $this->createMock(PriceCurrency::class); - $priceCurrencyMock->expects($this->any()) - ->method('convertAndRound') - ->willReturnArgument(0); - $this->priceModifierMock = $this->createMock(PriceModifier::class); - - $this->calculateCustomOptionCatalogRule = $objectManager->getObject( - CalculateCustomOptionCatalogRule::class, - [ - 'priceCurrency' => $priceCurrencyMock, - 'priceModifier' => $this->priceModifierMock, - ] - ); - } - - /** - * Tests correct option price calculation with different catalog rules and special prices combination. - * - * @dataProvider executeDataProvider - * @param array $prices - * @param float $catalogRulePriceModifier - * @param float $optionPriceValue - * @param bool $isPercent - * @param float $expectedResult - */ - public function testExecute( - array $prices, - float $catalogRulePriceModifier, - float $optionPriceValue, - bool $isPercent, - float $expectedResult - ) { - $this->regularPriceMock->expects($this->any()) - ->method('getValue') - ->willReturn($prices['regularPriceValue']); - $this->specialPriceMock->expects($this->any()) - ->method('getValue') - ->willReturn($prices['specialPriceValue']); - $this->priceModifierMock->expects($this->any()) - ->method('modifyPrice') - ->willReturnCallback( - function ($price) use ($catalogRulePriceModifier) { - return $price * $catalogRulePriceModifier; - } - ); - - $finalPrice = $this->calculateCustomOptionCatalogRule->execute( - $this->saleableItemMock, - $optionPriceValue, - $isPercent - ); - - $this->assertSame($expectedResult, $finalPrice); - } - - /** - * Data provider for testExecute. - * - * "Active" means this price type has biggest discount, so other prices doesn't count. - * - * @return array - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - public function executeDataProvider(): array - { - return [ - 'No special price, no catalog price rules, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 100.0 - ], - 'No special price, no catalog price rules, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 1000.0 - ], - 'No special price, catalog price rule set, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 90.0 - ], - 'No special price, catalog price rule set, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 1000, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 900.0 - ], - 'Special price set, no catalog price rule, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 900, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 100.0 - ], - 'Special price set, no catalog price rule, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 900, - ], - 'catalogRulePriceModifier' => 1.0, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 900.0 - ], - 'Special price set and active, catalog price rule set, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 800, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 100.0 - ], - 'Special price set and active, catalog price rule set, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 800, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 800.0 - ], - 'Special price set, catalog price rule set and active, fixed option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 950, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => false, - 'expectedResult' => 90.0 - ], - 'Special price set, catalog price rule set and active, percent option price' => [ - 'prices' => [ - 'regularPriceValue' => 1000, - 'specialPriceValue' => 950, - ], - 'catalogRulePriceModifier' => 0.9, - 'optionPriceValue' => 100.0, - 'isPercent' => true, - 'expectedResult' => 900.0 - ], - ]; - } -} diff --git a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php index 254d893d24584..17318d4207841 100644 --- a/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php +++ b/app/code/Magento/Catalog/Test/Unit/Ui/DataProvider/Product/Form/Modifier/CategoriesTest.php @@ -7,13 +7,16 @@ namespace Magento\Catalog\Test\Unit\Ui\DataProvider\Product\Form\Modifier; -use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; -use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; use Magento\Catalog\Ui\DataProvider\Product\Form\Modifier\Categories; +use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory as CategoryCollectionFactory; +use Magento\Catalog\Model\ResourceModel\Category\Collection as CategoryCollection; use Magento\Framework\AuthorizationInterface; use Magento\Framework\DB\Helper as DbHelper; use Magento\Framework\UrlInterface; use Magento\Store\Model\Store; +use Magento\Backend\Model\Auth\Session; +use Magento\Authorization\Model\Role; +use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; /** @@ -51,6 +54,11 @@ class CategoriesTest extends AbstractModifierTest */ private $authorizationMock; + /** + * @var Session|MockObject + */ + private $sessionMock; + protected function setUp(): void { parent::setUp(); @@ -72,7 +80,10 @@ protected function setUp(): void $this->authorizationMock = $this->getMockBuilder(AuthorizationInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); $this->categoryCollectionFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->categoryCollectionMock); @@ -88,6 +99,26 @@ protected function setUp(): void $this->categoryCollectionMock->expects($this->any()) ->method('getIterator') ->willReturn(new \ArrayIterator([])); + + $roleAdmin = $this->getMockBuilder(Role::class) + ->setMethods(['getId']) + ->disableOriginalConstructor() + ->getMock(); + $roleAdmin->expects($this->any()) + ->method('getId') + ->willReturn(0); + + $userAdmin = $this->getMockBuilder(User::class) + ->setMethods(['getRole']) + ->disableOriginalConstructor() + ->getMock(); + $userAdmin->expects($this->any()) + ->method('getRole') + ->willReturn($roleAdmin); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->willReturn($userAdmin); } /** @@ -101,11 +132,28 @@ protected function createModel() 'locator' => $this->locatorMock, 'categoryCollectionFactory' => $this->categoryCollectionFactoryMock, 'arrayManager' => $this->arrayManagerMock, - 'authorization' => $this->authorizationMock + 'authorization' => $this->authorizationMock, + 'session' => $this->sessionMock ] ); } + /** + * @param object $object + * @param string $method + * @param array $args + * @return mixed + * @throws \ReflectionException + */ + private function invokeMethod($object, $method, $args = []) + { + $class = new \ReflectionClass(Categories::class); + $method = $class->getMethod($method); + $method->setAccessible(true); + + return $method->invokeArgs($object, $args); + } + public function testModifyData() { $this->assertSame([], $this->getModel()->modifyData([])); @@ -176,4 +224,44 @@ public function modifyMetaLockedDataProvider() { return [[true], [false]]; } + + /** + * Asserts that a user with an ACL role ID of 0 and a user with an ACL role ID of 1 do not have the same cache IDs + * Assumes a store ID of 0 + * + * @throws \ReflectionException + */ + public function testAclCacheIds() + { + $categoriesAdmin = $this->createModel(); + $cacheIdAdmin = $this->invokeMethod($categoriesAdmin, 'getCategoriesTreeCacheId', [0]); + + $roleAclUser = $this->getMockBuilder(Role::class) + ->disableOriginalConstructor() + ->getMock(); + $roleAclUser->expects($this->any()) + ->method('getId') + ->willReturn(1); + + $userAclUser = $this->getMockBuilder(User::class) + ->disableOriginalConstructor() + ->getMock(); + $userAclUser->expects($this->any()) + ->method('getRole') + ->will($this->returnValue($roleAclUser)); + + $this->sessionMock = $this->getMockBuilder(Session::class) + ->setMethods(['getUser']) + ->disableOriginalConstructor() + ->getMock(); + + $this->sessionMock->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($userAclUser)); + + $categoriesAclUser = $this->createModel(); + $cacheIdAclUser = $this->invokeMethod($categoriesAclUser, 'getCategoriesTreeCacheId', [0]); + + $this->assertNotEquals($cacheIdAdmin, $cacheIdAclUser); + } } diff --git a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php index c80b2663d1f69..6b85ade0995a0 100644 --- a/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php +++ b/app/code/Magento/Catalog/Ui/Component/Listing/Columns/Websites.php @@ -109,6 +109,7 @@ public function prepare() * Apply sorting. * * @return void + * @since 103.0.2 */ protected function applySorting() { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php index 1f154d3204454..9b328e9bcc199 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Alerts.php @@ -54,7 +54,6 @@ public function __construct( /** * {@inheritdoc} - * @since 101.0.0 */ public function modifyData(array $data) { @@ -63,7 +62,6 @@ public function modifyData(array $data) /** * {@inheritdoc} - * @since 101.0.0 */ public function modifyMeta(array $meta) { 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 7608173c8edfc..73d29819c1153 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 @@ -18,12 +18,14 @@ use Magento\Framework\UrlInterface; use Magento\Framework\Stdlib\ArrayManager; use Magento\Framework\AuthorizationInterface; +use Magento\Backend\Model\Auth\Session; /** * Data provider for categories field of product page * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @since 101.0.0 */ class Categories extends AbstractModifier @@ -48,7 +50,7 @@ class Categories extends AbstractModifier /** * @var array - * @deprecated 101.0.3 + * @deprecated 101.0.0 * @since 101.0.0 */ protected $categoriesTrees = []; @@ -86,6 +88,11 @@ class Categories extends AbstractModifier */ private $authorization; + /** + * @var Session + */ + private $session; + /** * @param LocatorInterface $locator * @param CategoryCollectionFactory $categoryCollectionFactory @@ -94,6 +101,7 @@ class Categories extends AbstractModifier * @param ArrayManager $arrayManager * @param SerializerInterface $serializer * @param AuthorizationInterface $authorization + * @param Session $session */ public function __construct( LocatorInterface $locator, @@ -102,7 +110,8 @@ public function __construct( UrlInterface $urlBuilder, ArrayManager $arrayManager, SerializerInterface $serializer = null, - AuthorizationInterface $authorization = null + AuthorizationInterface $authorization = null, + Session $session = null ) { $this->locator = $locator; $this->categoryCollectionFactory = $categoryCollectionFactory; @@ -111,6 +120,7 @@ public function __construct( $this->arrayManager = $arrayManager; $this->serializer = $serializer ?: ObjectManager::getInstance()->get(SerializerInterface::class); $this->authorization = $authorization ?: ObjectManager::getInstance()->get(AuthorizationInterface::class); + $this->session = $session ?: ObjectManager::getInstance()->get(Session::class); } /** @@ -370,10 +380,16 @@ protected function getCategoriesTree($filter = null) * @param string $filter * @return string */ - private function getCategoriesTreeCacheId(int $storeId, string $filter = '') : string + private function getCategoriesTreeCacheId(int $storeId, string $filter = ''): string { + if ($this->session->getUser() !== null) { + return self::CATEGORY_TREE_ID + . '_' . (string)$storeId + . '_' . $this->session->getUser()->getAclRole() + . '_' . $filter; + } return self::CATEGORY_TREE_ID - . '_' . (string) $storeId + . '_' . (string)$storeId . '_' . $filter; } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php index 0295e778f2b9b..dd757841410e2 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav.php @@ -40,7 +40,7 @@ use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory; /** - * Data provider for eav attributes on product page + * Class Eav data provider for product editing form * * @api * @@ -791,7 +791,9 @@ private function getAttributeDefaultValue(ProductAttributeInterface $attribute) \Magento\Store\Model\ScopeInterface::SCOPE_STORE, $this->storeManager->getStore() ); - $attribute->setDefaultValue($defaultValue); + if ($defaultValue !== null) { + $attribute->setDefaultValue($defaultValue); + } } return $attribute->getDefaultValue(); } diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php index 453be0c1a1582..a0935de84627a 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdate.php @@ -50,7 +50,6 @@ private function extractLayoutUpdate(ProductInterface $product) /** * @inheritdoc - * @since 101.1.0 */ public function modifyData(array $data) { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php index c64d3e2e4effb..e9e8229e581ba 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/TierPrice.php @@ -18,7 +18,7 @@ * Tier prices modifier adds price type option to tier prices. * * @api - * @since 101.1.0 + * @since 102.0.0 */ class TierPrice extends AbstractModifier { @@ -46,7 +46,7 @@ public function __construct( /** * @inheritdoc - * @since 101.1.0 + * @since 102.0.0 */ public function modifyData(array $data) { @@ -56,7 +56,7 @@ public function modifyData(array $data) /** * Add tier price info to meta array. * - * @since 101.1.0 + * @since 102.0.0 * @param array $meta * @return array */ diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php index d8f76c40e8fad..2324ca27ffaaf 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/Listing/Collector/Image.php @@ -54,7 +54,7 @@ class Image implements ProductRenderCollectorInterface /** * @var DesignInterface - * @deprecated 2.3.0 DesignLoader is used for design theme loading + * @deprecated 103.0.1 DesignLoader is used for design theme loading */ private $design; diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php index a518afc576d61..b5d2bad1d348e 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductDataProvider.php @@ -130,6 +130,7 @@ public function addFilter(\Magento\Framework\Api\Filter $filter) /** * @inheritdoc + * @since 103.0.0 */ public function getMeta() { diff --git a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php index 3f16e0a6617da..3ea21223816c1 100644 --- a/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php +++ b/app/code/Magento/Catalog/Ui/DataProvider/Product/ProductRenderCollectorInterface.php @@ -12,7 +12,7 @@ * Allows to collect absolutely different product render information from different modules * * @api - * @since 101.1.0 + * @since 102.0.0 */ interface ProductRenderCollectorInterface { @@ -22,7 +22,7 @@ interface ProductRenderCollectorInterface * @param ProductInterface $product * @param ProductRenderInterface $productRender * @return void - * @since 101.1.0 + * @since 102.0.0 */ public function collect(ProductInterface $product, ProductRenderInterface $productRender); } diff --git a/app/code/Magento/Catalog/etc/db_schema.xml b/app/code/Magento/Catalog/etc/db_schema.xml index 1c97c920266df..a0aa48fb76b13 100644 --- a/app/code/Magento/Catalog/etc/db_schema.xml +++ b/app/code/Magento/Catalog/etc/db_schema.xml @@ -138,6 +138,11 @@ <index referenceId="CATALOG_PRODUCT_ENTITY_INT_STORE_ID" indexType="btree"> <column name="store_id"/> </index> + <index referenceId="CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE" indexType="btree"> + <column name="attribute_id"/> + <column name="store_id"/> + <column name="value"/> + </index> </table> <table name="catalog_product_entity_text" resource="default" engine="innodb" comment="Catalog Product Text Attribute Backend Table"> diff --git a/app/code/Magento/Catalog/etc/db_schema_whitelist.json b/app/code/Magento/Catalog/etc/db_schema_whitelist.json index d4bd6927d4345..f4cda73c371d0 100644 --- a/app/code/Magento/Catalog/etc/db_schema_whitelist.json +++ b/app/code/Magento/Catalog/etc/db_schema_whitelist.json @@ -69,7 +69,8 @@ }, "index": { "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID": true, - "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true + "CATALOG_PRODUCT_ENTITY_INT_STORE_ID": true, + "CATALOG_PRODUCT_ENTITY_INT_ATTRIBUTE_ID_STORE_ID_VALUE": true }, "constraint": { "PRIMARY": true, diff --git a/app/code/Magento/Catalog/etc/di.xml b/app/code/Magento/Catalog/etc/di.xml index 5a7a3135b4bfe..97a787c87bfa8 100644 --- a/app/code/Magento/Catalog/etc/di.xml +++ b/app/code/Magento/Catalog/etc/di.xml @@ -35,6 +35,7 @@ <preference for="Magento\Catalog\Api\Data\ProductAttributeTypeInterface" type="Magento\Catalog\Model\Product\Attribute\Type" /> <preference for="Magento\Catalog\Api\ProductAttributeGroupRepositoryInterface" type="Magento\Catalog\Model\ProductAttributeGroupRepository" /> <preference for="Magento\Catalog\Api\ProductAttributeOptionManagementInterface" type="Magento\Catalog\Model\Product\Attribute\OptionManagement" /> + <preference for="Magento\Catalog\Api\ProductAttributeOptionUpdateInterface" type="Magento\Catalog\Model\Product\Attribute\OptionManagement" /> <preference for="Magento\Catalog\Api\ProductLinkRepositoryInterface" type="Magento\Catalog\Model\ProductLink\Repository" /> <preference for="Magento\Catalog\Api\Data\ProductAttributeSearchResultsInterface" type="Magento\Catalog\Model\ProductAttributeSearchResults" /> <preference for="Magento\Catalog\Api\Data\CategoryAttributeSearchResultsInterface" type="Magento\Catalog\Model\CategoryAttributeSearchResults" /> diff --git a/app/code/Magento/Catalog/etc/frontend/di.xml b/app/code/Magento/Catalog/etc/frontend/di.xml index ee9c5b29da894..9c7c3e04b0772 100644 --- a/app/code/Magento/Catalog/etc/frontend/di.xml +++ b/app/code/Magento/Catalog/etc/frontend/di.xml @@ -13,11 +13,6 @@ </argument> </arguments> </virtualType> - <type name="Magento\Catalog\Model\ResourceModel\Category\Collection"> - <arguments> - <argument name="fetchStrategy" xsi:type="object">Magento\Catalog\Model\ResourceModel\Category\Collection\FetchStrategy</argument> - </arguments> - </type> <type name="Magento\Catalog\Model\Indexer\AbstractFlatState"> <arguments> <argument name="isAvailable" xsi:type="boolean">true</argument> @@ -120,4 +115,13 @@ <plugin name="catalog_app_action_dispatch_controller_context_plugin" type="Magento\Catalog\Plugin\Framework\App\Action\ContextPlugin" /> </type> + <type name="\Magento\PageCache\Model\PageCacheTagsPreprocessorComposite"> + <arguments> + <argument name="preprocessors" xsi:type="array"> + <item name="catalog_product_view" xsi:type="array"> + <item name="product_not_found" xsi:type="object">Magento\Catalog\Model\ProductNotFoundPageCacheTags</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/Catalog/etc/webapi.xml b/app/code/Magento/Catalog/etc/webapi.xml index 3f82175ab02eb..5e799cd9f426d 100644 --- a/app/code/Magento/Catalog/etc/webapi.xml +++ b/app/code/Magento/Catalog/etc/webapi.xml @@ -183,6 +183,12 @@ <resource ref="Magento_Catalog::attributes_attributes" /> </resources> </route> + <route url="/V1/products/attributes/:attributeCode/options/:optionId" method="PUT"> + <service class="Magento\Catalog\Api\ProductAttributeOptionUpdateInterface" method="update" /> + <resources> + <resource ref="Magento_Catalog::attributes_attributes" /> + </resources> + </route> <route url="/V1/products/attributes/:attributeCode/options/:optionId" method="DELETE"> <service class="Magento\Catalog\Api\ProductAttributeOptionManagementInterface" method="delete" /> <resources> diff --git a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml index c503196cc8647..89f34a21415d3 100644 --- a/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml +++ b/app/code/Magento/Catalog/view/adminhtml/layout/catalog_product_index.xml @@ -21,6 +21,7 @@ <referenceContainer name="content"> <uiComponent name="product_listing"/> <block class="Magento\Catalog\Block\Adminhtml\Product" name="products_list"/> + <block class="Magento\Backend\Block\Template" template="Magento_Catalog::product/grid/url_filter_applier.phtml" name="product_list_url_filter_applier" /> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml index c77b66733afc4..c19f140687bbc 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit.phtml @@ -6,13 +6,16 @@ /** * @var $block \Magento\Catalog\Block\Adminhtml\Category\Edit + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div data-id="information-dialog-category" class="messages" style="display: none;"> +<div data-id="information-dialog-category" class="messages"> <div class="message message-notice"> <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'div[data-id="information-dialog-category"]') ?> + <script type="text/x-magento-init"> { "*": { diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml index af7aec12a57ed..e52b43b1c3d24 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/edit/assign_products.phtml @@ -5,8 +5,9 @@ */ /** @var \Magento\Catalog\Block\Adminhtml\Category\AssignProducts $block */ - /** @var \Magento\Catalog\Block\Adminhtml\Category\Tab\Product $blockGrid */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $blockGrid = $block->getBlockGrid(); $gridJsObjectName = $blockGrid->getJsObjectName(); ?> @@ -23,6 +24,4 @@ $gridJsObjectName = $blockGrid->getJsObjectName(); } </script> <!-- @todo remove when "UI components" will support such initialization --> -<script> - require('mage/apply/main').apply(); -</script> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "require('mage/apply/main').apply();", false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml index d6340330df8ea..0cd00e88f4350 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/tree.phtml @@ -5,36 +5,48 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Category\Tree */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="categories-side-col"> <div class="sidebar-actions"> - <?php if ($block->getRoot()) :?> + <?php if ($block->getRoot()):?> <?= $block->getAddRootButtonHtml() ?><br/> <?= $block->getAddSubButtonHtml() ?> <?php endif; ?> </div> <div class="tree-actions"> - <?php if ($block->getRoot()) :?> - <?php //echo $block->getCollapseButtonHtml() ?> - <?php //echo $block->getExpandButtonHtml() ?> - <a href="#" - onclick="tree.collapseTree(); return false;"><?= $block->escapeHtml(__('Collapse All')) ?></a> - <span class="separator">|</span> <a href="#" - onclick="tree.expandTree(); return false;"><?= $block->escapeHtml(__('Expand All')) ?></a> + <?php if ($block->getRoot()):?> + <a id="colapseAll" href="#"><?= $block->escapeHtml(__('Collapse All')) ?></a> + <span class="separator">|</span> + <a id="expandAll" href="#"><?= $block->escapeHtml(__('Expand All')) ?></a> <?php endif; ?> </div> - <?php if ($block->getRoot()) :?> + <?php if ($block->getRoot()):?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'tree.collapseTree(); event.preventDefault();', + '#colapseAll' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'tree.expandTree();event.preventDefault();', + '#expandAll' + ) ?> <div class="tree-holder"> <div id="tree-div" class="tree-wrapper"></div> </div> </div> - <div data-id="information-dialog-tree" class="messages" style="display: none;"> + <div data-id="information-dialog-tree" class="messages"> <div class="message message-notice"> <div><?= $block->escapeHtml(__('This operation can take a long time')) ?></div> </div> </div> - <script> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display: none;', + 'div[data-id="information-dialog-tree"]' + ) ?> + <?php $scriptString = <<<script var tree; require([ "jquery", @@ -171,7 +183,7 @@ if (!this.collapsed) { this.collapsed = true; - this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(false))) ?>'; + this.loader.dataUrl = '{$block->escapeJs($block->getLoadTreeUrl(false))}'; this.request(this.loader.dataUrl, false); } }, @@ -180,7 +192,7 @@ this.expandAll(); if (this.collapsed) { this.collapsed = false; - this.loader.dataUrl = '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl(true))) ?>'; + this.loader.dataUrl = '{$block->escapeJs($block->getLoadTreeUrl(true))}'; this.request(this.loader.dataUrl, false); } }, @@ -215,7 +227,9 @@ if (tree && switcherParams) { var url; if (switcherParams.useConfirm) { - if (!confirm("<?= $block->escapeJs(__('Please confirm site switching. All data that hasn\'t been saved will be lost.')) ?>")) { + if (!confirm("{$block->escapeJs(__( + 'Please confirm site switching. All data that hasn\'t been saved will be lost.' +))}")) { return false; } } @@ -258,7 +272,7 @@ } }); } else { - var baseUrl = '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>'; + var baseUrl = '{$block->escapeJs($block->getEditUrl())}'; var urlExt = switcherParams.scopeParams + 'id/' + tree.currentNodeId + '/'; url = parseSidUrl(baseUrl, urlExt); setLocation(url); @@ -295,18 +309,22 @@ if (scopeParams) { url = url + scopeParams; } - <?php if ($block->isClearEdit()) :?> +script; + if ($block->isClearEdit()): + $scriptString .= <<<script if (selectedNode) { url = url + 'id/' + config.parameters.category_id; } - <?php endif;?> +script; +endif; + $scriptString .= <<<script //updateContent(url); //commented since ajax requests replaced with http ones to load a category jQuery('#tree-div').find('.x-tree-node-el').first().remove(); } jQuery(function () { categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' + dataUrl: '{$block->escapeJs($block->getLoadTreeUrl())}' }); categoryLoader.processResponse = function (response, parent, callback) { @@ -388,31 +406,32 @@ enableDD: true, containerScroll: true, selModel: new Ext.tree.CheckNodeMultiSelectionModel(), - rootVisible: '<?= (bool)$block->getRoot()->getIsVisible() ?>', - useAjax: <?= $block->escapeJs($block->getUseAjax()) ?>, - switchTreeUrl: '<?= $block->escapeJs($block->escapeUrl($block->getSwitchTreeUrl())) ?>', - editUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>', - currentNodeId: <?= (int)$block->getCategoryId() ?>, - baseUrl: '<?= $block->escapeJs($block->escapeUrl($block->getEditUrl())) ?>' +script; + $scriptString .= ' + rootVisible: \'' . ($block->getRoot()->getIsVisible() ? 'true' : 'false') . '\', + useAjax: ' . $block->escapeJs($block->getUseAjax()) . ', + switchTreeUrl: \'' . $block->escapeJs($block->escapeUrl($block->getSwitchTreeUrl())) .'\', + editUrl: \'' . $block->escapeJs($block->escapeUrl($block->getEditUrl())) .'\', + currentNodeId: ' . (int)$block->getCategoryId() . ', + baseUrl: \'' . $block->escapeJs($block->escapeUrl($block->getEditUrl())) . '\' }; defaultLoadTreeParams = { parameters: { - text: <?= /* @noEscape */ json_encode(htmlentities($block->getRoot()->getName())) ?>, + text: ' . /* @noEscape */ json_encode(htmlentities($block->getRoot()->getName())) . ', draggable: false, - allowDrop: <?php if ($block->getRoot()->getIsVisible()) :?>true<?php else :?>false<?php endif; ?>, - id: <?= (int)$block->getRoot()->getId() ?>, - expanded: <?= (int)$block->getIsWasExpanded() ?>, - store_id: <?= (int)$block->getStore()->getId() ?>, - category_id: <?= (int)$block->getCategoryId() ?>, - parent: <?= (int)$block->getRequest()->getParam('parent') ?> + allowDrop: ' . ($block->getRoot()->getIsVisible() ? 'true' : 'false') . ', + id: ' . (int)$block->getRoot()->getId() . ', + expanded: ' . (int)$block->getIsWasExpanded() . ', + store_id: ' . (int)$block->getStore()->getId() . ', + category_id: ' . (int)$block->getCategoryId() . ', + parent: ' . (int)$block->getRequest()->getParam('parent') . ' }, - data: <?= /* @noEscape */ $block->getTreeJson() ?> - }; - - reRenderTree(); - }); - + data: ' . /* @noEscape */ $block->getTreeJson() . ' + }; + reRenderTree(); + });' . PHP_EOL; + $scriptString .= <<<script function addNew(url, isRoot) { if (isRoot) { tree.currentNodeId = tree.root.id; @@ -485,7 +504,7 @@ click: function () { (function ($) { $.ajax({ - url: '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', + url: '{$block->escapeJs($block->getMoveUrl())}', method: 'POST', data: registry.get('pd'), showLoader: true @@ -521,5 +540,7 @@ window.addNew = addNew; }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml index e24d676974b01..6c92ddcf36243 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_divId = 'tree' . $block->getId() ?> @@ -10,14 +12,20 @@ <!--[if IE]> <script id="ie-deferred-loader" defer="defer" src="//:"></script> <![endif]--> -<script> -require(['jquery', "prototype", "extjs/ext-tree-checkbox"], function(jQuery){ +<?php +$isUseMassaction = $block->getUseMassaction() ? 1 : 0; +$isAnchorOnly = $block->getIsAnchorOnly() ? 1 : 0; +$intCategoryId = (int)$block->getCategoryId(); +$intRootId = (int) $block->getRoot()->getId(); +$scriptString = <<<script -var tree<?= $block->escapeJs($block->getId()) ?>; +require(['jquery', 'prototype', 'extjs/ext-tree-checkbox'], function(jQuery){ -var useMassaction = <?= $block->getUseMassaction() ? 1 : 0 ?>; +var tree{$block->escapeJs($block->getId())}; -var isAnchorOnly = <?= $block->getIsAnchorOnly() ? 1 : 0 ?>; +var useMassaction = {$isUseMassaction}; + +var isAnchorOnly = {$isAnchorOnly}; Ext.tree.TreePanel.Enhanced = function(el, config) { @@ -41,9 +49,13 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { this.setRootNode(root); if (firstLoad) { - <?php if ($block->getNodeClickListener()) :?> - this.addListener('click', <?= /* @noEscape */ $block->getNodeClickListener() ?>.createDelegate(this)); - <?php endif; ?> + +script; +if ($block->getNodeClickListener()): + $scriptString .= 'this.addListener(\'click\', ' . /* @noEscape */ $block->getNodeClickListener() . + '.createDelegate(this));' . PHP_EOL; +endif; +$scriptString .= <<<script } this.loader.buildCategoryTree(root, data); @@ -55,10 +67,14 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { jQuery(function() { - var emptyNodeAdded = <?= ($block->getWithEmptyNode() ? 'false' : 'true') ?>; + +script; + $scriptString .= 'var emptyNodeAdded = ' . ($block->getWithEmptyNode() ? 'false' : 'true') . ';' . PHP_EOL; + +$scriptString .= <<<script var categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= $block->escapeJs($block->escapeUrl($block->getLoadTreeUrl())) ?>' + dataUrl: '{$block->escapeJs($block->escapeUrl($block->getLoadTreeUrl()))}' }); categoryLoader.buildCategoryTree = function(parent, config) @@ -77,7 +93,7 @@ jQuery(function() // Add empty node to reset category filter if(!emptyNodeAdded) { var empty = Object.clone(_node); - empty.text = '<?= $block->escapeJs(__('None')) ?>'; + empty.text = '{$block->escapeJs(__('None'))}'; empty.children = []; empty.id = 'none'; empty.path = '1/none'; @@ -148,39 +164,41 @@ jQuery(function() }; categoryLoader.on("beforeload", function(treeLoader, node) { - $('<?= $block->escapeJs($_divId) ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); + $('{$block->escapeJs($_divId)}').fire('category:beforeLoad', {treeLoader:treeLoader}); treeLoader.baseParams.id = node.attributes.id; }); - tree<?= $block->escapeJs($block->getId()) ?> = new Ext.tree.TreePanel.Enhanced('<?= $block->escapeJs($_divId) ?>', { + tree{$block->escapeJs($block->getId())} = new Ext.tree.TreePanel.Enhanced('{$block->escapeJs($_divId)}', { animate: false, loader: categoryLoader, enableDD: false, containerScroll: true, rootVisible: false, useAjax: true, - currentNodeId: <?= (int) $block->getCategoryId() ?>, + currentNodeId: {$intCategoryId}, addNodeTo: false }); if (useMassaction) { - tree<?= $block->escapeJs($block->getId()) ?>.on('check', function(node) { - $('<?= $block->escapeJs($_divId) ?>').fire('node:changed', {node:node}); - }, tree<?= $block->escapeJs($block->getId()) ?>); + tree{$block->escapeJs($block->getId())}.on('check', function(node) { + $('{$block->escapeJs($_divId)}').fire('node:changed', {node:node}); + }, tree{$block->escapeJs($block->getId())}); } // set the root node var parameters = { text: 'Psw', draggable: false, - id: <?= (int) $block->getRoot()->getId() ?>, + id: {$intRootId}, expanded: true, - category_id: <?= (int) $block->getCategoryId() ?> + category_id: {$intCategoryId} }; - tree<?= $block->escapeJs($block->getId()) ?>.loadTree({parameters:parameters, data:<?= /* @noEscape */ $block->getTreeJson() ?>},true); + tree{$block->escapeJs($block->getId())}.loadTree({parameters:parameters, data:{$block->getTreeJson()}},true); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml index cbda491a64740..4e70bff5a4884 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/form/renderer/fieldset/element.phtml @@ -5,14 +5,14 @@ */ ?> <?php -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Catalog\Block\Adminhtml\Form\Renderer\Fieldset\Element */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /* @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ $element = $block->getElement(); -$note = $element->getNote() ? '<div class="note admin__field-note">' . $block->escapeHtml($element->getNote()) . '</div>' : ''; +$note = $element->getNote() ? + '<div class="note admin__field-note">' . $block->escapeHtml($element->getNote()) . '</div>' : ''; $elementBeforeLabel = $element->getExtType() == 'checkbox' || $element->getExtType() == 'radio'; $addOn = $element->getBeforeElementHtml() || $element->getAfterElementHtml(); $fieldId = ($element->getHtmlId()) ? ' id="attribute-' . $element->getHtmlId() . '-container"' : ''; @@ -26,6 +26,9 @@ $fieldClass .= ($entity && $entity->getIsUserDefined()) ? ' user-defined type-' $fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . '" ' . $block->getUiId('form-field', $block->escapeHtmlAttr($element->getId())); + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <?php $block->checkFieldDisable() ?> @@ -33,20 +36,19 @@ $fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; ?> -<?php if (!$element->getNoDisplay()) :?> - <?php if ($element->getType() == 'hidden') :?> +<?php if (!$element->getNoDisplay()):?> + <?php if ($element->getType() == 'hidden'):?> <?= $element->getElementHtml() ?> - <?php else :?> + <?php else:?> <div<?= /* @noEscape */ $fieldAttributes ?> data-attribute-code="<?= $element->getHtmlId() ?>" - data-apply-to="<?= $block->escapeHtmlAttr($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] - ))?>" + data-apply-to="<?= /* @noEscape */ $jsonHelper->jsonEncode($element->hasEntityAttribute() ? + $element->getEntityAttribute()->getApplyTo() : []) ?>" > - <?php if ($elementBeforeLabel) :?> + <?php if ($elementBeforeLabel):?> <?= $block->getElementHtml() ?> <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> <?= /* @noEscape */ $note ?> - <?php else :?> + <?php else:?> <?= $element->getLabelHtml('', $block->getScopeLabel()) ?> <div class="admin__field-control control"> <?= ($addOn) ? '<div class="addon">' . $block->getElementHtml() . '</div>' : $block->getElementHtml() ?> @@ -54,16 +56,20 @@ $fieldAttributes = $fieldId . ' class="' . $block->escapeHtmlAttr($fieldClass) . </div> <?php endif; ?> <div class="field-service"> - <?php if ($block->canDisplayUseDefault()) :?> + <?php if ($block->canDisplayUseDefault()):?> <label for="<?= $element->getHtmlId() ?>_default" class="choice use-default"> - <input <?php if ($element->getReadonly()) :?> disabled="disabled"<?php endif; ?> + <input <?php if ($element->getReadonly()):?> disabled="disabled"<?php endif; ?> type="checkbox" name="use_default[]" class="use-default-control" id="<?= $element->getHtmlId() ?>_default" - <?php if ($block->usedDefault()) :?> checked="checked"<?php endif; ?> - onclick="<?= $block->escapeHtmlAttr($elementToggleCode) ?>" + <?php if ($block->usedDefault()):?> checked="checked"<?php endif; ?> value="<?= $block->escapeHtmlAttr($block->getAttributeCode()) ?>"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + $elementToggleCode, + "#" . $element->getHtmlId() . "_default" + ) ?> <span class="use-default-label"><?= $block->escapeHtml(__('Use Default Value')) ?></span> </label> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml index 64384ac391a8d..8dde7013763dc 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/js.phtml @@ -5,9 +5,14 @@ */ use Magento\Catalog\Helper\Data; -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$scriptString = <<<script require([ "jquery", 'Magento_Ui/js/modal/alert', @@ -207,32 +212,39 @@ function switchDefaultValueField() setRowVisibility('is_unique', false); setRowVisibility('frontend_class', false); break; - - <?php // phpcs:ignore Magento2.Templates.ThisInTemplate ?> - <?php foreach ($this->helper(Data::class)->getAttributeHiddenFields() as $type => $fields): ?> - case '<?= $block->escapeJs($type) ?>': +script; +foreach ($jsonHelper->getAttributeHiddenFields() as $type => $fields): + $scriptString .= <<<script + case '{$block->escapeJs($type)}': var isFrontTabHidden = false; - <?php foreach ($fields as $one): ?> - <?php if ($one == '_front_fieldset'): ?> +script; + foreach ($fields as $one): + if ($one == '_front_fieldset'): + $scriptString .= <<<script getFrontTab().hide(); isFrontTabHidden = true; - <?php elseif ($one == '_default_value'): ?> +script; + elseif ($one == '_default_value'): + $scriptString .= <<<script defaultValueTextVisibility = defaultValueTextareaVisibility = defaultValueDateVisibility = defaultValueYesnoVisibility = false; - <?php elseif ($one == '_scope'): ?> - scopeVisibility = false; - <?php else: ?> - setRowVisibility('<?= $block->escapeJs($one) ?>', false); - <?php endif; ?> - <?php endforeach; ?> - +script; + elseif ($one == '_scope'): + $scriptString .= 'scopeVisibility = false;'; + else: + $scriptString .= "setRowVisibility('" . $block->escapeJs($one) . "', false);"; + endif; + endforeach; + $scriptString .= <<<script if (!isFrontTabHidden){ getFrontTab().show(); } break; - <?php endforeach; ?> +script; + endforeach; + $scriptString .= <<<script default: getFrontTab().show(); @@ -278,7 +290,7 @@ function setRowVisibility(id, isVisible) function updateRequriedOptions() { - if ($F('frontend_input')=='select' && $F('is_required')==1) { + if (\$F('frontend_input')=='select' && \$F('is_required')==1) { $('option-count-check').addClassName('required-options-count'); } else { $('option-count-check').removeClassName('required-options-count'); @@ -364,4 +376,6 @@ window.getFrontTab = getFrontTab; window.toggleApplyVisibility = toggleApplyVisibility; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml index dd1009cc5e033..447a9c4149bfa 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main.phtml @@ -5,6 +5,7 @@ */ /** @var $block Magento\Catalog\Block\Adminhtml\Product\Attribute\Set\Main */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="attribute-set"> @@ -17,7 +18,7 @@ </div> <div class="edit-attribute-set attribute-set-col"> <?= $block->getSetFormHtml() ?> - <script> + <?php $scriptString = <<<script require([ "jquery", "mage/mage" @@ -26,14 +27,17 @@ jQuery('#set-prop-form').mage('validation', {errorClass: 'mage-error'}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <div class="attribute-set-col fieldset-wrapper"> <div class="fieldset-wrapper-title"> <span class="title"><?= $block->escapeHtml(__('Groups')) ?></span> </div> - <?php if (!$block->getIsReadOnly()) :?> - <?= /* @noEscape */ $block->getAddGroupButton() ?> <?= /* @noEscape */ $block->getDeleteGroupButton() ?> + <?php if (!$block->getIsReadOnly()):?> + <?= /* @noEscape */ $block->getAddGroupButton() ?>  + <?= /* @noEscape */ $block->getDeleteGroupButton() ?> <p class="note-block"><?= $block->escapeHtml(__('Double click on a group to rename it.')) ?></p> <?php endif; ?> @@ -45,8 +49,19 @@ <span class="title"><?= $block->escapeHtml(__('Unassigned Attributes')) ?></span> </div> <div id="tree-div2" class="attribute-set-tree"></div> - <script id="ie-deferred-loader" defer="defer" src="//:"></script> - <script> + <?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + ['id' => "ie-deferred-loader", 'defer' => "defer", 'src' => "//:"], + ' ', + false + ) ?> + <?php $readOnly = ($block->getIsReadOnly() ? 'false' : 'true'); + $groupTree = /* @noEscape */ $block->getGroupTreeJson(); + $attributeTreeJson = /* @noEscape */ $block->getAttributeTreeJson(); + $systemAttributeWarning = $block->escapeJs( + __('This group contains system attributes. Please move system attributes to another group and try again.') + ); + $scriptString = <<<script define("tree-panel", [ "jquery", @@ -57,8 +72,8 @@ ], function(jQuery, prompt, alert){ //<![CDATA[ - var allowDragAndDrop = <?= ($block->getIsReadOnly() ? 'false' : 'true') ?>; - var canEditGroups = <?= ($block->getIsReadOnly() ? 'false' : 'true') ?>; + var allowDragAndDrop = {$readOnly}; + var canEditGroups = {$readOnly}; var TreePanels = function() { // shorthand @@ -85,7 +100,7 @@ }); tree.setRootNode(this.root); - buildCategoryTree(this.root, <?= /* @noEscape */ $block->getGroupTreeJson() ?>); + buildCategoryTree(this.root, {$groupTree}); // render the tree tree.render(); this.root.expand(false, false); @@ -93,7 +108,7 @@ this.ge = new Ext.tree.TreeEditor(tree, { allowBlank:false, - blankText:'<?= $block->escapeJs(__('A name is required.')) ?>', + blankText:'{$block->escapeJs(__('A name is required.'))}', selectOnFocus:true, cls:'folder' }); @@ -124,7 +139,7 @@ id:'free' }); tree2.setRootNode(this.root2); - buildCategoryTree(this.root2, <?= /* @noEscape */ $block->getAttributeTreeJson() ?>); + buildCategoryTree(this.root2, {$attributeTreeJson}); this.root2.addListener('beforeinsert', editSet.rightBeforeInsert); this.root2.addListener('beforeappend', editSet.rightBeforeAppend); @@ -144,12 +159,15 @@ for( i in rootNode.childNodes ) { if(rootNode.childNodes[i].id) { var group = rootNode.childNodes[i]; - editSet.req.groups[gIterator] = new Array(group.id, group.attributes.text.strip(), (gIterator+1)); + editSet.req.groups[gIterator] = new Array(group.id, group.attributes.text.strip(), + (gIterator+1)); var iterator = 0 for( j in group.childNodes ) { iterator ++; if( group.childNodes[j].id > 0 ) { - editSet.req.attributes[group.childNodes[j].id] = new Array(group.childNodes[j].id, group.id, iterator, group.childNodes[j].attributes.entity_id); + editSet.req.attributes[group.childNodes[j].id] = + new Array(group.childNodes[j].id, group.id, iterator, + group.childNodes[j].attributes.entity_id); } } iterator = 0; @@ -164,7 +182,8 @@ for( i in rootNode.childNodes ) { if(rootNode.childNodes[i].id) { if( rootNode.childNodes[i].id > 0 ) { - editSet.req.not_attributes[iterator] = rootNode.childNodes[i].attributes.entity_id; + editSet.req.not_attributes[iterator] = + rootNode.childNodes[i].attributes.entity_id; } iterator ++; } @@ -231,7 +250,7 @@ if( editSet.SystemNodesExists(editSet.currentNode) ) { alert({ - content: '<?= $block->escapeJs(__('This group contains system attributes. Please move system attributes to another group and try again.')) ?>' + content: '{$systemAttributeWarning}' }); return; } @@ -258,7 +277,7 @@ SystemNodesExists : function(currentNode) { if (!currentNode) { alert({ - content: '<?= $block->escapeJs(__('Please select a node.')) ?>' + content: '{$block->escapeJs(__('Please select a node.'))}' }); return; } @@ -279,8 +298,8 @@ addGroup : function() { prompt({ - title: "<?= $block->escapeJs($block->escapeHtml(__('Add New Group'))) ?>", - content: "<?= $block->escapeJs($block->escapeHtml(__('Please enter a new group name.'))) ?>", + title: "{$block->escapeJs($block->escapeHtml(__('Add New Group')))}", + content: "{$block->escapeJs($block->escapeHtml(__('Please enter a new group name.')))}", value: "", validation: true, validationRules: ['required-entry'], @@ -344,8 +363,11 @@ result = false; } for (var i=0; i < TreePanels.root.childNodes.length; i++) { - if (TreePanels.root.childNodes[i].text.toLowerCase() == name.toLowerCase() && TreePanels.root.childNodes[i].id != exceptNodeId) { - errorText = '<?= $block->escapeJs(__('An attribute group named "/name/" already exists.')) ?>'; + if (TreePanels.root.childNodes[i].text.toLowerCase() == name.toLowerCase() && + TreePanels.root.childNodes[i].id != exceptNodeId) { + errorText = '{$block->escapeJs( + __('An attribute group named "/name/" already exists.') + )}'; alert({ content: errorText.replace("/name/",name) }); @@ -373,7 +395,8 @@ editSet.req.form_key = FORM_KEY; } var req = {data : Ext.util.JSON.encode(editSet.req)}; - var con = new Ext.lib.Ajax.request('POST', '<?= $block->escapeJs($block->escapeUrl($block->getMoveUrl())) ?>', {success:editSet.success,failure:editSet.failure}, req); + var con = new Ext.lib.Ajax.request('POST', '{$block->escapeJs($block->getMoveUrl())}', + {success:editSet.success,failure:editSet.failure}, req); }, success : function(o) { @@ -391,7 +414,7 @@ failure : function(o) { alert({ - content: '<?= $block->escapeJs(__('Sorry, we\'re unable to complete this request.')) ?>' + content: '{$block->escapeJs(__('Sorry, we\'re unable to complete this request.'))}' }); }, @@ -408,7 +431,9 @@ rightBeforeAppend : function(tree, nodeThis, node, newParent) { if (node.attributes.is_user_defined == 0) { alert({ - content: '<?= $block->escapeJs(__('You can\'t remove attributes from this attribute set.')) ?>' + content: '{$block->escapeJs( + __('You can\'t remove attributes from this attribute set.') + )}' }); return false; } else { @@ -424,7 +449,9 @@ if (node.attributes.is_unassignable == 0) { alert({ - content: '<?= $block->escapeJs(__('You can\'t remove attributes from this attribute set.')) ?>' + content: '{$block->escapeJs( + __('You can\'t remove attributes from this attribute set.') + )}' }); return false; } else { @@ -448,7 +475,7 @@ rightRemove : function(tree, nodeThis, node) { if( nodeThis.firstChild == null && node.id != 'empty' ) { var newNode = new Ext.tree.TreeNode({ - text : '<?= $block->escapeJs(__('Empty')) ?>', + text : '{$block->escapeJs(__('Empty'))}', id : 'empty', cls : 'folder', is_user_defined : 1, @@ -485,6 +512,8 @@ }); require(["tree-panel"]); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml index 5717e9f0a0f0b..c3d8d5bc0f44e 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/main/tree/group.phtml @@ -3,5 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="tree-div1" style="height:400px;margin-top:5px;overflow:auto"></div> +<?= /* @noEscape */ $secureRenderer->renderTag('div', ['id' => "tree-div1"], ' ', false) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("height:400px;margin-top:5px;overflow:auto", '#tree-div1') ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml index 227ed4be81fae..8f58d357f83e4 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/attribute/set/toolbar/add.phtml @@ -3,12 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getFormHtml() ?> -<script> + +<?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ - jQuery('#<?= $block->escapeJs($block->getFormId()) ?>').mage('form').mage('validation'); + jQuery('#{$block->escapeJs($block->getFormId())}').mage('form').mage('validation'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml index 32466a1dfa965..5ca88689b9e5f 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/configure.phtml @@ -3,31 +3,85 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +$blockId = $block->getId(); ?> -<div id="product_composite_configure" class="product-configure-popup" style="display:none;"> - <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe" style="width:0; height:0; border:0px solid #fff; position:absolute; top:-1000px; left:-1000px" onload="window.productConfigure && productConfigure.onLoadIFrame()"></iframe> - <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" onsubmit="productConfigure.onConfirmBtn(); return false;" target="product_composite_configure_iframe"> +<div id="product_composite_configure" + class="product-configure-popup product-configure-popup-<?= $block->escapeHtmlAttr($blockId) ?>"> + <iframe name="product_composite_configure_iframe" id="product_composite_configure_iframe"></iframe> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onload', + "window.productConfigure && productConfigure.onLoadIFrame()", + 'iframe[name=\'product_composite_configure_iframe\']:last-of-type' + ) ?> + + <form action="" method="post" id="product_composite_configure_form" enctype="multipart/form-data" + target="product_composite_configure_iframe" class="product_composite_configure_form"> <div class="entry-edit"> - <div id="product_composite_configure_messages" style="display: none;" > + <div id="product_composite_configure_messages" class="product_composite_configure_messages"> <div class="messages"><div class="message message-error error"><div></div></div></div> </div> <div id="product_composite_configure_form_fields" class="content product-composite-configure-inner"></div> - <div id="product_composite_configure_form_additional" style="display:none;"></div> - <div id="product_composite_configure_form_confirmed" style="display:none;"></div> + <div id="product_composite_configure_form_additional" class="product_composite_configure_form_additional"> + </div> + <div id="product_composite_configure_form_confirmed" class="product_composite_configure_form_confirmed"> + </div> </div> <input type="hidden" name="as_js_varname" value="iFrameResponse" /> <input type="hidden" name="form_key" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" /> </form> - <div id="product_composite_configure_confirmed" style="display:none;"></div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onsubmit', + 'productConfigure.onConfirmBtn();event.preventDefault()', + '.product_composite_configure_form:last-of-type' + ) ?> + + <div id="product_composite_configure_confirmed" class="product_composite_configure_confirmed"></div> + + <?php $scriptString = <<<script + prodCompConfIframe = document.querySelector( + ".product-configure-popup-$blockId iframe[name='product_composite_configure_iframe']" + ); + prodCompConfIframe.style.width = 0; + prodCompConfIframe.style.height = 0; + prodCompConfIframe.style.border = "0px solid #fff"; + prodCompConfIframe.style.position = "absolute"; + prodCompConfIframe.style.top = "-1000px"; + prodCompConfIframe.style.left = "-1000px"; + + prodCompConfMessages = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_messages" + ); + prodCompConfMessages.style.display = "none"; + + prodCompConfFormAdd = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_form_additional" + ); + prodCompConfFormAdd.style.display = "none"; + + prodCompConfFormConf = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_form_confirmed" + ); + prodCompConfFormConf.style.display = "none"; + + prodCompConfConf = document.querySelector( + ".product-configure-popup-$blockId .product_composite_configure_confirmed" + ); + prodCompConfConf.style.display = "none"; + + prodConfPopup = document.querySelector(".product-configure-popup-$blockId"); + prodConfPopup.style.display = "none"; - <script> require([ "jquery", "mage/mage" ], function(jQuery){ - - jQuery('#product_composite_configure_form').mage('form').mage('validation'); - + jQuery('.product_composite_configure_form').each(function () { + jQuery(this).mage('form').mage('validation'); + }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml index 722e4ae7ef1f0..f5dfc3ae79fbe 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/js.phtml @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script require([ "prototype", "Magento_Catalog/catalog/product/composite/configure" @@ -89,4 +91,6 @@ var DateOption = Class.create({ productConfigure.opConfig.dateOption = new DateOption(); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml index 68a7a3a69cfd3..fde7a9351756c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/date.phtml @@ -3,10 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\Date */ ?> + <?php $_option = $block->getOption(); ?> <?php $_optionId = (int)$_option->getId(); ?> +<?php $optionId = /* @noEscape */ $_optionId ?> <div class="admin__field field<?= $_option->getIsRequire() ? ' required' : '' ?>"> <label class="label admin__field-label"> <?= $block->escapeHtml($_option->getTitle()) ?> @@ -15,28 +19,30 @@ <div class="admin__field-control control"> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE) :?> + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE):?> <?= $block->getDateHtml() ?> - <?php if (!$block->useCalendar()) :?> - <script> + <?php if (!$block->useCalendar()):?> + <?php $scriptString = <<<script require([ "prototype", "Magento_Catalog/catalog/product/composite/configure" ], function(){ window.dateOption = productConfigure.opConfig.dateOption; - Event.observe('options_<?= /* @noEscape */ $_optionId ?>_month', 'change', dateOption.reloadMonth.bind(dateOption)); - Event.observe('options_<?= /* @noEscape */ $_optionId ?>_year', 'change', dateOption.reloadMonth.bind(dateOption)); + Event.observe('options_{$optionId}_month', 'change', dateOption.reloadMonth.bind(dateOption)); + Event.observe('options_{$optionId}_year', 'change', dateOption.reloadMonth.bind(dateOption)); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> <?php if ($_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_DATE_TIME - || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME) :?> + || $_option->getType() == \Magento\Catalog\Api\Data\ProductCustomOptionInterface::OPTION_TYPE_TIME):?> <span class="time-picker"><?= $block->getTimeHtml() ?></span> <?php endif; ?> @@ -44,24 +50,28 @@ name="validate_datetime_<?= /* @noEscape */ $_optionId ?>" class="validate-datetime-<?= /* @noEscape */ $_optionId ?>" value="" /> - <script> + <?php $scriptString = <<<script require([ "jquery", "mage/backend/validation" ], function(jQuery){ //<![CDATA[ - <?php if ($_option->getIsRequire()) :?> - jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { - var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); +script; + if ($_option->getIsRequire()): + $scriptString .= <<<script + jQuery.validator.addMethod('validate-datetime-{$optionId}', function(v) { + var dateTimeParts = jQuery('.datetime-picker[id^="options_{$optionId}"]'); for (var i=0; i < dateTimeParts.length; i++) { if (dateTimeParts[i].value == "") return false; } return true; - }, '<?= $block->escapeJs(__('This is a required option.')) ?>'); - <?php else :?> - jQuery.validator.addMethod('validate-datetime-<?= /* @noEscape */ $_optionId ?>', function(v) { - var dateTimeParts = jQuery('.datetime-picker[id^="options_<?= /* @noEscape */ $_optionId ?>"]'); + }, '{$block->escapeJs(__('This is a required option.'))}'); +script; + else: + $scriptString .= <<<script + jQuery.validator.addMethod('validate-datetime-{$optionId}', function(v) { + var dateTimeParts = jQuery('.datetime-picker[id^="options_{$optionId}"]'); var hasWithValue = false, hasWithNoValue = false; var pattern = /day_part$/i; for (var i=0; i < dateTimeParts.length; i++) { @@ -74,11 +84,15 @@ } } return hasWithValue ^ hasWithNoValue; - }, '<?= $block->escapeJs(__('The field isn\'t complete.')) ?>'); - <?php endif; ?> + }, '{$block->escapeJs(__('The field isn\'t complete.'))}'); +script; + endif; + $scriptString .= <<<script //]]> }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml index 89d005a178fac..a181ed8d67120 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/composite/fieldset/options/type/file.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData() ? true : false; ?> @@ -14,15 +16,20 @@ <?php $_fileNamed = $_fileName . '_name'; ?> <?php $_rand = rand(); ?> -<script> +<?php +$rand = /* @noEscape */ $_rand; +$fileName = /* @noEscape */ $_fileName; +$fieldNameAction = /* @noEscape */ $_fieldNameAction; +$fileNamed = /* @noEscape */ $_fileNamed; +$scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ - opFile<?= /* @noEscape */ $_rand ?> = { + opFile{$rand} = { initializeFile: function(inputBox) { - this.inputFile = inputBox.select('input[name="<?= /* @noEscape */ $_fileName ?>"]')[0]; - this.inputFileAction = inputBox.select('input[name="<?= /* @noEscape */ $_fieldNameAction ?>"]')[0]; - this.fileNameBox = inputBox.up('div').select('.<?= /* @noEscape */ $_fileNamed ?>')[0]; + this.inputFile = inputBox.select('input[name="{$fileName}"]')[0]; + this.inputFileAction = inputBox.select('input[name="{$fieldNameAction}"]')[0]; + this.fileNameBox = inputBox.up('div').select('.{$fileNamed}')[0]; }, toggleFileChange: function(inputBox) { @@ -57,44 +64,68 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div class="admin__field <?= $_option->getIsRequire() ? ' required _required' : '' ?>"> <label class="admin__field-label label"> <?= $block->escapeHtml($_option->getTitle()) ?> <?= /* @noEscape */ $block->getFormattedPrice() ?> </label> - <div class="admin__field-control control"> - <?php if ($_fileExists) :?> + <div class="admin__field-control control" id="<?= /* @noEscape */ $_fileName ?>"> + <?php if ($_fileExists):?> <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> - <a href="javascript:void(0)" class="label" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileChange($(this).next('.input-box'))"> + <a href="#" class="label"> <?= $block->escapeHtml(__('Change')) ?> </a>  - <?php if (!$_option->getIsRequire()) :?> - <input type="checkbox" onclick="opFile<?= /* @noEscape */ $_rand ?>.toggleFileDelete($(this), $(this).next('.input-box'))" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "event.preventDefault(); opFile" . /* @noEscape */ $_rand . + ".toggleFileChange($(this).next('.input-box'))", + '#' . /* @noEscape */ $_fileName . ' a' + ); ?> + <?php if (!$_option->getIsRequire()):?> + <input type="checkbox" + price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "opFile" . /* @noEscape */ $_rand . ".toggleFileDelete($(this), $(this).next('.input-box'))", + '#' . /* @noEscape */ $_fileName . ' input[type="checkbox"]' + ) ?> <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> <?php endif; ?> <?php endif; ?> <div class="input-box" <?= $_fileExists ? 'style="display:none"' : '' ?>> <!-- ToDo UI: add appropriate file class when z-index issue in ui dialog will be resolved --> - <input type="file" name="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> - <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> + <input type="file" name="<?= /* @noEscape */ $_fileName ?>" + class="product-custom-option<?= $_option->getIsRequire() ? ' required-entry' : '' ?>" + price="<?= $block->escapeHtmlAttr($block->getCurrencyPrice($_option->getPrice(true))) ?>" + <?= $_fileExists ? 'disabled="disabled"' : '' ?>/> + <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" + value="<?= /* @noEscape */ $_fieldValueAction ?>" /> - <?php if ($_option->getFileExtension()) :?> + <?php if ($_option->getFileExtension()):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong></span> + <span><?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: + <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> + </span> </div> <?php endif; ?> - <?php if ($_option->getImageSizeX() > 0) :?> + <?php if ($_option->getImageSizeX() > 0):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> + <span><?= $block->escapeHtml(__('Maximum image width')) ?>: + <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + </span> </div> <?php endif; ?> - <?php if ($_option->getImageSizeY() > 0) :?> + <?php if ($_option->getImageSizeY() > 0):?> <div class="admin__field-note"> - <span><?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong></span> + <span><?= $block->escapeHtml(__('Maximum image height')) ?>: + <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + </span> </div> <?php endif; ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml index 66df098a194ae..eaaee91d7226c 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit.phtml @@ -3,12 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /** * @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="admin__scope-old"> @@ -34,30 +32,40 @@ title="<?= $block->escapeHtmlAttr(__('Product online status')) ?>"></label> </div> - <?php if ($block->getProductId()) :?> + <?php if ($block->getProductId()):?> <?= $block->getDeleteButtonHtml() ?> <?php endif; ?> - <?php if ($block->getProductSetId()) :?> + <?php if ($block->getProductSetId()):?> <?= $block->getChangeAttributeSetButtonHtml() ?> <?= $block->getSaveSplitButtonHtml() ?> <?php endif; ?> <?= $block->getBackButtonHtml() ?> </div> </div> -<?php if ($block->getUseContainer()) :?> +<?php if ($block->getUseContainer()): ?> <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data" data-form="edit-product" data-product-id="<?= $block->escapeHtmlAttr($block->getProduct()->getId()) ?>"> <?php endif; ?> <?= $block->getBlockHtml('formkey') ?> - <div data-role="tabs" id="product-edit-form-tabs"></div> <?php /* @TODO: remove id after elimination of setDestElementId('product-edit-form-tabs') */?> + <div data-role="tabs" id="product-edit-form-tabs"></div> + <?php /* @TODO: remove id after elimination of setDestElementId('product-edit-form-tabs') */?> <?= $block->getChildHtml('product-type-tabs') ?> - <input type="hidden" id="product_type_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getTypeId()) ?>"/> - <input type="hidden" id="attribute_set_id" value="<?= $block->escapeHtmlAttr($block->getProduct()->getAttributeSetId()) ?>"/> + <input type="hidden" id="product_type_id" + value="<?= $block->escapeHtmlAttr($block->getProduct()->getTypeId()) ?>"/> + <input type="hidden" id="attribute_set_id" + value="<?= $block->escapeHtmlAttr($block->getProduct()->getAttributeSetId()) ?>"/> <button type="submit" class="hidden"></button> -<?php if ($block->getUseContainer()) :?> +<?php if ($block->getUseContainer()):?> </form> <?php endif; ?> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$jsonFieldsAutogenerationMasks = /* @noEscape */ $jsonHelper->jsonEncode($block->getFieldsAutogenerationMasks()); +$jsonAttributesAllowedForAutogeneration = /* @noEscape */ $jsonHelper->jsonEncode( + $block->getAttributesAllowedForAutogeneration() +); +$scriptString = <<<scriptStr require([ "jquery", "Magento_Catalog/catalog/type-events", @@ -66,8 +74,8 @@ require([ "mage/backend/tabs", "domReady!" ], function($, TypeSwitcher){ - var $form = $('[data-form=edit-product]'); - $form.data('typeSwitcher', TypeSwitcher.init()); + var \$form = $('[data-form=edit-product]'); + \$form.data('typeSwitcher', TypeSwitcher.init()); var scriptTagManager = (function($) { var hiddenPrefix = 'hidden', @@ -109,7 +117,7 @@ require([ $(this).val($(this).val().substr(0, maxLength)); } }); - $form.mage('form', { + \$form.mage('form', { handlersData: { save: {}, saveAndContinueEdit: { @@ -129,10 +137,10 @@ require([ } } }); - $form.mage('validation', {validationUrl: '<?= $block->escapeJs($block->escapeUrl($block->getValidationUrl())) ?>'}); + \$form.mage('validation', {validationUrl: '{$block->escapeJs($block->getValidationUrl())}'}); - var masks = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getFieldsAutogenerationMasks()) ?>; - var availablePlaceholders = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getAttributesAllowedForAutogeneration()) ?>; + var masks = {$jsonFieldsAutogenerationMasks}; + var availablePlaceholders = {$jsonAttributesAllowedForAutogeneration}; var Autogenerator = function(masks) { this._masks = masks || {}; this._fieldReverseIndex = this._buildReverseIndex(this._masks); @@ -155,13 +163,13 @@ require([ 'change init', elementSelector, $.proxy(function(event) { - var $element = $(event.target); - if (event.type == 'init' && $element.data('disablerInited')) { + var \$element = $(event.target); + if (event.type == 'init' && \$element.data('disablerInited')) { return; } else { - $element.data('disablerInited', true); + \$element.data('disablerInited', true); } - $element.data(this.data.disabled, $element.val().replace(/\s/g, '') != ''); + \$element.data(this.data.disabled, \$element.val().replace(/\s/g, '') != ''); }, this) ).find(elementSelector).trigger('init'); }; @@ -169,19 +177,19 @@ require([ $("#product_info_tabs").on("tabscreate tabsactivate", $.proxy(disabler, this)); $.each(this._masks, function(field, mask) { - var $field = $('#' + field); - if (!$field.val() && mask && mask.length > 0 && !self.varRegexp.test(mask)) { - $field.val(mask); + var \$field = $('#' + field); + if (!\$field.val() && mask && mask.length > 0 && !self.varRegexp.test(mask)) { + \$field.val(mask); } - $field.trigger('change'); + \$field.trigger('change'); }); $.each(self._fieldReverseIndex, function(field) { - var fields = this, $field = $('#' + field); + var fields = this, \$field = $('#' + field); var filler = function(onlyText) { $.each(fields, function() { - var $el = $('#' + this); - if ($el.data(self.data.disabled)) { + var \$el = $('#' + this); + if (\$el.data(self.data.disabled)) { return; } if (onlyText === true && self.varRegexp.test(self._masks[this])) { @@ -190,12 +198,12 @@ require([ var value = self._masks[this].replace(self.varsRegexp, function(maskfieldName) { return $('#' + maskfieldName.slice(2, -2)).val(); }); - $el.val(value); + \$el.val(value); }); }; - if ($field.length) { + if (\$field.length) { self.form.on('keyup change blur click paste', '#' + field, filler); - $field.trigger('change'); + \$field.trigger('change'); } }); }, @@ -217,7 +225,7 @@ require([ } }); - $form.data('autogenerator', new Autogenerator(masks).bindAll()); + \$form.data('autogenerator', new Autogenerator(masks).bindAll()); $('.widget-button-save .item-default').parent().hide(); @@ -229,7 +237,7 @@ require([ $('#status').val($(this).prop('checked') ? '1' : '2'); }); - $form.on('changeAttributeSet', function(event, data) { + \$form.on('changeAttributeSet', function(event, data) { if (data.label) { $('#product-template-suggest-container .action-toggle>span').text(data.label); $('[data-role=affected-attribute-set-selector] [data-role=name-container]').text(data.label); @@ -240,13 +248,13 @@ require([ uri += /\?/.test(uri) ? '&' : '?'; uri += 'set=' + window.encodeURIComponent(data.id); - var $form = $('[data-form=edit-product]'); - $form.attr('action', $form.attr('action').replace(/(\/|&|\?)?\bset(\/|=)\d+/g, '')); - $form.find('#attribute_set_id').attr('name', 'set').val(data.id); + var \$form = $('[data-form=edit-product]'); + \$form.attr('action', \$form.attr('action').replace(/(\/|&|\?)?\bset(\/|=)\d+/g, '')); + \$form.find('#attribute_set_id').attr('name', 'set').val(data.id); $.ajax({ url: uri.replace('/edit/', '/new/') + '&popup=1', type: 'post', - data: $form.serializeArray(), + data: \$form.serializeArray(), dataType: 'html', context: $('body'), showLoader: true @@ -255,67 +263,68 @@ require([ data = scriptTagManager.disableScripts(data); var removedElementClass = 'removed'; - var $page = $('body'); - var $newPage = $(data); + var \$page = $('body'); + var \$newPage = $(data); var nameMapper = function() { return $(this).attr('name'); }; var activeTabId = $('.ui-tabs-active>a').attr('id'); //add new tab tabs or reorder - $page.find('#product_info_tabs .tabs').each(function(i, tabContainer) { - $newPage.find('#product_info_tabs .tabs').each(function(j, newTabContainer) { + \$page.find('#product_info_tabs .tabs').each(function(i, tabContainer) { + \$newPage.find('#product_info_tabs .tabs').each(function(j, newTabContainer) { if (i != j) { return; } - var $tabContainer = $(tabContainer); + var \$tabContainer = $(tabContainer); $(tabContainer).find('li').removeClass(removedElementClass); - var $tabs = $(tabContainer) + var \$tabs = $(tabContainer) .find('li:not(.' + removedElementClass + ') .tab-item-link.user-defined:not(.ajax)'); - var $newTabs = $(newTabContainer).find('.tab-item-link.user-defined:not(.ajax)'), - tabsNames = $tabs.map(nameMapper).toArray(); + var \$newTabs = $(newTabContainer).find('.tab-item-link.user-defined:not(.ajax)'), + tabsNames = \$tabs.map(nameMapper).toArray(); //hide not exists elements $.each( - _.difference(tabsNames, $newTabs.map(nameMapper).toArray()), + _.difference(tabsNames, \$newTabs.map(nameMapper).toArray()), function(index, tabName) { - $tabContainer.find('[name=' + tabName + ']').closest('li') + \$tabContainer.find('[name=' + tabName + ']').closest('li') .addClass(removedElementClass); - $page.find('#' + tabName) + \$page.find('#' + tabName) .addClass(removedElementClass) .addClass('ignore-validate'); } ); $(newTabContainer).find('.tab-item-link.user-defined:not(.ajax)').each(function(index, tab) { - var $tab = $(tab), - tabName = nameMapper.apply($tab), - $tabsContent = $tab.closest('li').clone(); - $tabsContent.find('.fieldset>.field').remove(); - if (nameMapper.apply($tabs.eq(index)) == tabName) { + var \$tab = $(tab), + tabName = nameMapper.apply(\$tab), + \$tabsContent = \$tab.closest('li').clone(); + \$tabsContent.find('.fieldset>.field').remove(); + if (nameMapper.apply(\$tabs.eq(index)) == tabName) { return true; } - var $tabToMove = $.inArray(tabName, tabsNames) !== -1 - ? $tabs.filter(function() { + var \$tabToMove = $.inArray(tabName, tabsNames) !== -1 + ? \$tabs.filter(function() { return nameMapper.apply(this) === tabName; }).closest('li') - : $tabsContent; + : \$tabsContent; if (index === 0) { - $tabToMove.prependTo($tabContainer); + \$tabToMove.prependTo(\$tabContainer); } else { - $tabToMove.insertAfter($tabs.eq(index - 1).closest('li')); + \$tabToMove.insertAfter(\$tabs.eq(index - 1).closest('li')); } - $tabToMove.removeClass(removedElementClass).removeClass('ignore-validate'); - $tabs = $tabContainer.find('li:not(.' + removedElementClass + ') .tab-item-link.user-defined:not(.ajax)'); + \$tabToMove.removeClass(removedElementClass).removeClass('ignore-validate'); + \$tabs = \$tabContainer + .find('li:not(.' + removedElementClass + ') .tab-item-link.user-defined:not(.ajax)'); }); }); }); //add new fieldsets or reorder - $newPage.find('#product_info_tabs .fieldset.user-defined').each(function(index, newFieldset) { + \$newPage.find('#product_info_tabs .fieldset.user-defined').each(function(index, newFieldset) { var fieldsetContainer, newFieldsetContainer, sourceContainer, destinationContainer; newFieldsetContainer = $(newFieldset).parents('[data-ui-id*=-tab-content-]').first(); - if ($page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').length === 0) { + if (\$page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').length === 0) { fieldsetContainer = newFieldsetContainer .clone() .removeClass(removedElementClass) @@ -323,10 +332,10 @@ require([ //Enable hidden js scripts in node. These scripts will be performed after inserting into page fieldsetContainer = scriptTagManager.enableScripts(fieldsetContainer); } else { - fieldsetContainer = $page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').first(); + fieldsetContainer = \$page.find('[data-ui-id=' + newFieldsetContainer.data('uiId') + ']').first(); } sourceContainer = newFieldsetContainer.parents('[data-ui-id*=-tab-content-]').first(); - destinationContainer = $page.find('[data-ui-id=' + sourceContainer.data('uiId') + ']').first(); + destinationContainer = \$page.find('[data-ui-id=' + sourceContainer.data('uiId') + ']').first(); fieldsetContainer.appendTo(destinationContainer); }); @@ -334,7 +343,7 @@ require([ return $(this).data('attributeCode'); }; //add new element elements or reorder - $page.find('[data-form=edit-product] [data-role=tabs] .fieldset, #product_info_tabs .fieldset') + \$page.find('[data-form=edit-product] [data-role=tabs] .fieldset, #product_info_tabs .fieldset') .removeClass('ignore-validate') .removeClass(removedElementClass) .each(function(i, fieldSet) { @@ -342,49 +351,49 @@ require([ if ($(fieldSet).attr('id') != $(newFieldSet).attr('id')) { return } - var $elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); - var $newFieldSet = $(newFieldSet); - var $newElements = $newFieldSet.find('>.field'); + var \$elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); + var \$newFieldSet = $(newFieldSet); + var \$newElements = \$newFieldSet.find('>.field'); - $elements.removeClass(removedElementClass); + \$elements.removeClass(removedElementClass); - var elementNames = $elements.map(nameDataMapper).toArray(); + var elementNames = \$elements.map(nameDataMapper).toArray(); //hide not exists elements $.each( - _.difference(elementNames, $newElements.map(nameDataMapper).toArray()), + _.difference(elementNames, \$newElements.map(nameDataMapper).toArray()), function(index, elementId) { - $page.find('#attribute-' + elementId + '-container') + \$page.find('#attribute-' + elementId + '-container') .addClass(removedElementClass) .addClass('ignore-validate'); } ); - $newElements.each(function(index, element) { - var $element = $(element), - elementId = nameDataMapper.apply($element); - if (nameDataMapper.apply($elements.get(index)) == elementId) { + \$newElements.each(function(index, element) { + var \$element = $(element), + elementId = nameDataMapper.apply(\$element); + if (nameDataMapper.apply(\$elements.get(index)) == elementId) { return true; } - var $elementToMove = $('.fieldset>.field[data-attribute-code="' + elementId + '"]'); - if ($elementToMove.length === 0) { - $elementToMove = $element.clone(); + var \$elementToMove = $('.fieldset>.field[data-attribute-code="' + elementId + '"]'); + if (\$elementToMove.length === 0) { + \$elementToMove = \$element.clone(); } if (index === 0) { - $elementToMove.prependTo(fieldSet); + \$elementToMove.prependTo(fieldSet); } else { - $elementToMove.insertAfter($elements.get(index - 1)) + \$elementToMove.insertAfter(\$elements.get(index - 1)) } - $elementToMove.trigger('contentUpdated'); - $elementToMove.removeClass(removedElementClass).removeClass('.ignore-validate'); - $elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); + \$elementToMove.trigger('contentUpdated'); + \$elementToMove.removeClass(removedElementClass).removeClass('.ignore-validate'); + \$elements = $(fieldSet).find('>.field:not(.' + removedElementClass + ')'); }); }; - $newPage.find('#product_info_tabs .fieldset').each(updateFieldsetElements); + \$newPage.find('#product_info_tabs .fieldset').each(updateFieldsetElements); fieldsetContainer = $(fieldSet).parents('[data-ui-id*=-tab-content-]').first(); - var newFieldsetContainer = $newPage.find('[data-ui-id=' + $(fieldsetContainer).data('uiId') + ']'); + var newFieldsetContainer = \$newPage.find('[data-ui-id=' + $(fieldsetContainer).data('uiId') + ']'); if (newFieldsetContainer.length == 0) { $(fieldsetContainer).find('fieldset .field') .addClass('ignore-validate') @@ -405,7 +414,9 @@ require([ }); }); -</script> +scriptStr; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <script type="text/x-magento-init"> { "*": { diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml index 1d22624751b32..344123cbe5640 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/inventory.phtml @@ -5,8 +5,10 @@ */ /** @var Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-role=toggle-editability-all]').change(function(e) { var toggler = $(this); @@ -27,7 +29,9 @@ someEditable.prop('disabled', useConfigSettings.prop('checked')); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php $defaultMinSaleQty = $block->getDefaultConfigValue('min_sale_qty'); @@ -243,9 +247,10 @@ if (!is_numeric($defaultMinSaleQty)) { class="select" disabled="disabled"> <?php foreach ($block->getBackordersOption() as $option):?> - <?php $_selected = ($option['value'] == $block->getDefaultConfigValue('backorders')) - ? ' selected="selected"' : '' ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?php $_selected = ($option['value'] == $block->getDefaultConfigValue('backorders')) ? + ' selected="selected"' : '' ?> + <option + value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?> </option> <?php endforeach; ?> @@ -397,8 +402,9 @@ if (!is_numeric($defaultMinSaleQty)) { name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[is_in_stock]" class="select" disabled="disabled"> <option value="1"><?= $block->escapeHtml(__('In Stock')) ?></option> - <option value="0"<?php if ($block->getDefaultConfigValue('is_in_stock') == 0):?> - selected<?php endif; ?>><?= $block->escapeHtml(__('Out of Stock')) ?> + <option value="0" + <?php if ($block->getDefaultConfigValue('is_in_stock') == 0):?> selected<?php endif; ?>> + <?= $block->escapeHtml(__('Out of Stock')) ?> </option> </select> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml index 98b06050e0d1d..d5859240875cd 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/action/websites.phtml @@ -5,6 +5,7 @@ */ /** @var $block Magento\Catalog\Block\Adminhtml\Product\Edit\Action\Attribute\Tab\Websites */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="fieldset-wrapper" id="add-products-to-website-wrapper"> @@ -15,24 +16,27 @@ <br> <div class="store-scope"> <div class="store-tree" id="add-products-to-website-content"> - <?php foreach ($block->getWebsiteCollection() as $_website) :?> + <?php foreach ($block->getWebsiteCollection() as $_website):?> <div class="website-name"> <input name="add_website_ids[]" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" - <?php if ($block->getWebsitesReadonly()) :?> + <?php if ($block->getWebsitesReadonly()):?> disabled="disabled" <?php endif;?> class="checkbox website-checkbox" id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" type="checkbox" /> - <label for="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <label for="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"> + <?= $block->escapeHtml($_website->getName()) ?> + </label> </div> - <dl class="webiste-groups" id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group) :?> + <dl class="webiste-groups" + id="add_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> + <?php foreach ($block->getGroupCollection($_website) as $_group):?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd class="group-stores"> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store) :?> + <?php foreach ($block->getStoreCollection($_group) as $_store):?> <li> <?= $block->escapeHtml($_store->getName()) ?> </li> @@ -55,30 +59,35 @@ <br> <div class="messages"> <div class="message message-notice"> - <div><?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?></div> + <div><?= $block->escapeHtml( + __('To hide an item in catalog or search results, set the status to "Disabled".') + ) ?> + </div> </div> </div> <div class="store-scope"> <div class="store-tree" id="remove-products-to-website-content"> - <?php foreach ($block->getWebsiteCollection() as $_website) :?> + <?php foreach ($block->getWebsiteCollection() as $_website):?> <div class="website-name"> <input name="remove_website_ids[]" value="<?= $block->escapeHtmlAttr($_website->getId()) ?>" - <?php if ($block->getWebsitesReadonly()) :?> + <?php if ($block->getWebsitesReadonly()):?> disabled="disabled" <?php endif;?> class="checkbox website-checkbox" id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>" type="checkbox" /> - <label for="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <label for="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>"> + <?= $block->escapeHtml($_website->getName()) ?> + </label> </div> <dl class="webiste-groups" id="remove_product_website_<?= $block->escapeHtmlAttr($_website->getId()) ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group) :?> + <?php foreach ($block->getGroupCollection($_website) as $_group):?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd class="group-stores"> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store) :?> + <?php foreach ($block->getStoreCollection($_group) as $_store):?> <li> <?= $block->escapeHtml($_store->getName()) ?> </li> @@ -93,7 +102,7 @@ </fieldset> </div> -<script> +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -120,4 +129,6 @@ require([ } } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml index d073053e2f854..261de795f7199 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/attribute_set.phtml @@ -7,6 +7,7 @@ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\AttributeSet */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <script id="product-template-selector-template" type="text/x-magento-template"> <% if (!data.term && data.items.length && !data.allShown()) { %> @@ -23,16 +24,19 @@ </button> <% } %> </script> -<script> +<?php /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$selectorOptions = /* @noEscape */ $jsonHelper->jsonEncode($block->getSelectorOptions()); +$scriptString = <<<script require(["jquery","mage/mage","mage/backend/suggest"],function ($) { - var $suggest = $('#product-template-suggest'); - $suggest.closest('.dropdown-menu').siblings('[data-toggle=dropdown]').on('click.toggleDropdown', function () { + var \$suggest = $('#product-template-suggest'); + \$suggest.closest('.dropdown-menu').siblings('[data-toggle=dropdown]').on('click.toggleDropdown', function () { if ($(this).hasClass('active')) { - $suggest.click(); + \$suggest.click(); } }); - $suggest - .mage('suggest',<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) + \$suggest + .mage('suggest', {$selectorOptions}) .on('suggestselect', function (e, ui) { if (ui.item.id) { $('[data-form=edit-product]').trigger('changeAttributeSet', ui.item); @@ -40,4 +44,6 @@ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml index f12a99e6c7843..22dd5de45a073 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/category/new/form.phtml @@ -4,8 +4,13 @@ * See COPYING.txt for license details. */ /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\NewCategory */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none"> +<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>"> <?= $block->getFormHtml() ?> <?= $block->getAfterElementHtml() ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + $block->escapeJs($block->getNameInLayout()) +) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml index ad38d250a3345..7129190d47fb5 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options */ ?> <div class="fieldset-wrapper" id="product-custom-options-wrapper" data-block="product-custom-options"> <div class="fieldset-wrapper-title"> @@ -12,14 +14,19 @@ <span><?= $block->escapeHtml(__('Custom Options')) ?></span> </strong> </div> - <div class="fieldset-wrapper-content" id="product-custom-options-content" data-role="product-custom-options-content"> + <div class="fieldset-wrapper-content" id="product-custom-options-content" + data-role="product-custom-options-content"> <fieldset class="fieldset"> <div class="messages"> - <div class="message message-error" id="dynamic-price-warning" style="display: none;"> + <div class="message message-error" id="dynamic-price-warning"> <div class="message-inner"> - <div class="message-content"><?= $block->escapeHtml(__('We can\'t save custom-defined options for bundles with dynamic pricing.')) ?></div> + <div class="message-content"> + <?= $block->escapeHtml(__( + 'We can\'t save custom-defined options for bundles with dynamic pricing.' + )) ?></div> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", '#dynamic-price-warning') ?> </div> <div id="product_options_container" class="sortable-wrapper"> @@ -35,7 +42,7 @@ </div> </div> -<script> +<?php $scriptString = <<<script require(['jquery'], function($){ var priceType = $('#price_type'); var priceWarning = $('#dynamic-price-warning'); @@ -43,4 +50,6 @@ require(['jquery'], function($){ priceWarning.show(); } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml index 713366e73aba5..ce7dac70010b1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/options/option.phtml @@ -5,8 +5,10 @@ */ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Options\Option */ ?> <?= $block->getTemplatesHtml() ?> <script id="custom-option-base-template" type="text/x-magento-template"> <div class="fieldset-wrapper admin__collapsible-block-wrapper opened" id="option_<%- data.id %>"> @@ -68,7 +70,8 @@ type="text" value="<%- data.title %>" data-store-label="<%- data.title %>" - <% if (typeof data.scopeTitleDisabled != 'undefined' && data.scopeTitleDisabled != null) { %> disabled="disabled" <% } %> + <% if (typeof data.scopeTitleDisabled != 'undefined' && + data.scopeTitleDisabled != null) { %> disabled="disabled" <% } %> > <%- data.checkboxScopeTitle %> </div> @@ -92,20 +95,43 @@ <label for="field-option-req"> <?= $block->escapeHtml(__('Required')) ?> </label> - <span style="display:none"><?= $block->getRequireSelectHtml() ?></span> + <span id="span_<?= /* @noEscape */ $block->getFieldId() ?>"> + <?= $block->getRequireSelectHtml() ?> + </span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + "span_#" . /* @noEscape */ $block->getFieldId() + ) ?> </fieldset> </fieldset> </div> </div> </script> -<div id="import-container" style="display: none;"></div> -<?php if (!$block->isReadonly()) :?> +<div id="import-container"></div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#import-container') ?> +<?php if (!$block->isReadonly()):?> <div><input type="hidden" name="affect_product_custom_options" value="1"/></div> <?php endif; ?> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); + +$customOptions = /* @noEscape */ $jsonHelper->jsonEncode( + [ + 'fieldId' => $block->getFieldId(), + 'productGridUrl' => $block->escapeJs($block->getProductGridUrl()), + 'formKey' => $block->getFormKey(), + 'customOptionsUrl' => $block->escapeJs($block->getCustomOptionsUrl()), + 'isReadonly' => (bool) $block->isReadonly(), + 'itemCount' => (int) $block->getItemCount(), + 'currentProductId' => (int) $block->getCurrentProductId(), + ] +); + +$scriptString = <<<script require([ "jquery", "Magento_Catalog/js/custom-options" @@ -113,23 +139,17 @@ require([ jQuery(function ($) { var fieldSet = $('[data-block=product-custom-options]'); - fieldSet.customOptions(<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - [ - 'fieldId' => $block->getFieldId(), - 'productGridUrl' => $block->escapeUrl($block->getProductGridUrl()), - 'formKey' => $block->getFormKey(), - 'customOptionsUrl' => $block->escapeUrl($block->getCustomOptionsUrl()), - 'isReadonly' => (bool) $block->isReadonly(), - 'itemCount' => (int) $block->getItemCount(), - 'currentProductId' => (int) $block->getCurrentProductId(), - ] - )?>); + fieldSet.customOptions({$customOptions}); //adding data to templates - <?php /** @var $_value \Magento\Framework\DataObject */ ?> - <?php foreach ($block->getOptionValues() as $_value) :?> - fieldSet.customOptions('addOption', <?= /* @noEscape */ $_value->toJson() ?>); - <?php endforeach; ?> +script; +/** @var $_value \Magento\Framework\DataObject */ +foreach ($block->getOptionValues() as $_value): + $scriptString .= " fieldSet.customOptions('addOption', " . /* @noEscape */ $_value->toJson() . ');' . PHP_EOL; +endforeach; +$scriptString .= <<<script }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml index e66a18c677cc3..7e1c48d535dc1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/price/tier.phtml @@ -7,6 +7,7 @@ // phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis /* @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Price\Tier */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $element = $block->getElement(); ?> <?php $_htmlId = $block->getElement()->getHtmlId() ?> @@ -20,33 +21,42 @@ $element = $block->getElement(); <?php $_showWebsite = $block->isShowWebsiteColumn(); ?> <?php $_showWebsite = $block->isMultiWebsites(); ?> -<div class="field" id="attribute-<?= /* @noEscape */ $_htmlId ?>-container" data-attribute-code="<?= /* @noEscape */ $_htmlId ?>" +<?php /** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> +<div class="field" id="attribute-<?= /* @noEscape */ $_htmlId ?>-container" + data-attribute-code="<?= /* @noEscape */ $_htmlId ?>" data-apply-to="<?= $block->escapeHtml( - $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode( - $element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : [] - ) + $jsonHelper->jsonEncode($element->hasEntityAttribute() ? $element->getEntityAttribute()->getApplyTo() : []) )?>"> <label class="label"><span><?= $block->escapeHtml($block->getElement()->getLabel()) ?></span></label> <div class="control"> <table class="admin__control-table tiers_table" id="tiers_table"> <thead> <tr> - <th class="col-websites" <?php if (!$_showWebsite) :?>style="display:none"<?php endif; ?>><?= $block->escapeHtml(__('Web Site')) ?></th> + <th class="col-websites"><?= $block->escapeHtml(__('Web Site')) ?></th> <th class="col-customer-group"><?= $block->escapeHtml(__('Customer Group')) ?></th> <th class="col-qty required"><?= $block->escapeHtml(__('Quantity')) ?></th> - <th class="col-price required"><?= $block->escapeHtml($block->getPriceColumnHeader(__('Item Price'))) ?></th> + <th class="col-price required"> + <?= $block->escapeHtml($block->getPriceColumnHeader(__('Item Price'))) ?> + </th> <th class="col-delete"><?= $block->escapeHtml(__('Action')) ?></th> </tr> + <?php if (!$_showWebsite): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'th.col-websites'); ?> + <?php endif; ?> </thead> <tbody id="<?= /* @noEscape */ $_htmlId ?>_container"></tbody> <tfoot> <tr> - <td colspan="<?php if (!$_showWebsite) :?>4<?php else :?>5<?php endif; ?>" class="col-actions-add"><?= $block->getAddButtonHtml() ?></td> + <td colspan="<?php if (!$_showWebsite):?>4<?php else:?>5<?php endif; ?>" + class="col-actions-add"><?= $block->getAddButtonHtml() ?> + </td> </tr> </tfoot> </table> -<script> +<?php $htmlName = /* @noEscape */ $_htmlName; +$scriptString = <<<script require([ 'mage/template', "prototype", @@ -55,39 +65,81 @@ require([ //<![CDATA[ var tierPriceRowTemplate = '<tr>' - + '<td class="col-websites"<?php if (!$_showWebsite) :?> style="display:none"<?php endif; ?>>' - + '<select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' - <?php foreach ($block->getWebsites() as $_websiteId => $_info) :?> - + '<option value="<?= $block->escapeHtmlAttr($_websiteId) ?>"><?= $block->escapeHtml($_info['name']) ?><?php if (!empty($_info['currency'])) :?> [<?= $block->escapeHtml($_info['currency']) ?>]<?php endif; ?></option>' - <?php endforeach ?> + + '<td class="col-websites">' + + '<select class="{$block->escapeHtmlAttr($_htmlClass)} required-entry" + name="{$htmlName}[<%- data.index %>][website_id]" id="tier_price_row_<%- data.index %>_website">' +script; +foreach ($block->getWebsites() as $_websiteId => $_info): + $scriptString .= <<<script + + '<option value="{$block->escapeHtmlAttr($_websiteId)}">{$block->escapeHtml($_info['name'])} +script; + if (!empty($_info['currency'])): + $scriptString .= <<<script + [{$block->escapeHtml($_info['currency'])}] +script; + endif; + $scriptString .= <<<script + </option>' +script; + endforeach; + $scriptString .= <<<script + '</select></td>' - + '<td class="col-customer-group"><select class="<?= $block->escapeHtmlAttr($_htmlClass) ?> custgroup required-entry" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' - <?php foreach ($block->getCustomerGroups() as $_groupId => $_groupName) :?> - + '<option value="<?= $block->escapeHtmlAttr($_groupId) ?>"><?= $block->escapeHtml($_groupName) ?></option>' - <?php endforeach ?> + + '<td class="col-customer-group"><select class="{$block->escapeJs($_htmlClass)} custgroup required-entry" + name="{$htmlName}[<%- data.index %>][cust_group]" id="tier_price_row_<%- data.index %>_cust_group">' +script; +foreach ($block->getCustomerGroups() as $_groupId => $_groupName): + $scriptString .= <<<script + + '<option value="{$block->escapeJs($_groupId)}">{$block->escapeJs($_groupName)}</option>' +script; + endforeach; + $scriptString .= <<<script + '</select></td>' + '<td class="col-qty">' - + '<input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> qty required-entry validate-greater-than-zero" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price_qty]" value="<%- data.qty %>" id="tier_price_row_<%- data.index %>_qty" />' - + '<span><?= $block->escapeHtml(__("and above")) ?></span>' + + '<input class="{$block->escapeJs($_htmlClass)} qty required-entry validate-greater-than-zero" + type="text" name="{$htmlName}[<%- data.index %>][price_qty]" value="<%- data.qty %>" + id="tier_price_row_<%- data.index %>_qty" />' + + '<span>{$block->escapeHtml(__("and above"))}</span>' + '</td>' - + '<td class="col-price"><input class="<?= $block->escapeHtmlAttr($_htmlClass) ?> required-entry <?= $block->escapeHtmlAttr($_priceValueValidation) ?>" type="text" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][price]" value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' - + '<td class="col-delete"><input type="hidden" name="<?= /* @noEscape */ $_htmlName ?>[<%- data.index %>][delete]" class="delete" value="" id="tier_price_row_<%- data.index %>_delete" />' - + '<button title="<?= $block->escapeHtml(__('Delete Tier')) ?>" type="button" class="action- scalable delete icon-btn delete-product-option" id="tier_price_row_<%- data.index %>_delete_button" onclick="return tierPriceControl.deleteItem(event);">' - + '<span><?= $block->escapeHtml(__("Delete")) ?></span></button></td>' + + '<td class="col-price"><input class="{$block->escapeJs($_htmlClass)} required-entry + {$block->escapeJs($_priceValueValidation)}" type="text" name="{$htmlName}[<%- data.index %>][price]" + value="<%- data.price %>" id="tier_price_row_<%- data.index %>_price" /></td>' + + '<td class="col-delete"><input type="hidden" name="{$htmlName}[<%- data.index %>][delete]" class="delete" + value="" id="tier_price_row_<%- data.index %>_delete" />' + + '<button title="{$block->escapeJs(__('Delete Tier'))}" type="button" + class="action- scalable delete icon-btn delete-product-option" + id="tier_price_row_<%- data.index %>_delete_button">' + + '<span>{$block->escapeJs(__("Delete"))}</span></button></td>' + '</tr>'; - +script; + +if (!$_showWebsite): + $scriptString .= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', 'td.col-websites'); +endif; + $scriptString .= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'return tierPriceControl.deleteItem(event);', + "'td#tier_price_row_<%- data.index %>_delete_button" + ); + + $defaultWesite = (int) $block->getDefaultWebsite(); + $defaultCustomerGroup = (int) $block->getDefaultCustomerGroup(); + $scriptString .= <<<script var tierPriceControl = { template: mageTemplate(tierPriceRowTemplate), itemsCount: 0, addItem : function () { - <?php if ($_readonly) :?> +script; + if ($_readonly): + $scriptString .= <<<script if (arguments.length < 4) { return; } - <?php endif; ?> +script; + endif; + $scriptString .= <<<script var data = { - website_id: '<?= (int) $block->getDefaultWebsite() ?>', - group: '<?= (int) $block->getDefaultCustomerGroup() ?>', + website_id: '{$defaultWesite}', + group: '{$defaultCustomerGroup}', qty: '', price: '', readOnly: false, @@ -104,7 +156,7 @@ var tierPriceControl = { data.readOnly = arguments[4]; } - Element.insert($('<?= $block->escapeJs($_htmlId) ?>_container'), { + Element.insert($('{$block->escapeJs($_htmlId)}_container'), { bottom : this.template({ data: data }) @@ -113,14 +165,17 @@ var tierPriceControl = { $('tier_price_row_' + data.index + '_cust_group').value = data.group; $('tier_price_row_' + data.index + '_website').value = data.website_id; - <?php if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()) :?> +script; + if ($block->isShowWebsiteColumn() && !$block->isAllowChangeWebsite()): + $scriptString .= <<<script var wss = $('tier_price_row_' + data.index + '_website'); var txt = wss.options[wss.selectedIndex].text; wss.insert({after:'<span class="website-name">' + txt + '</span>'}); wss.hide(); - <?php endif;?> - +script; + endif; + $scriptString .= <<<script if (data.readOnly == '1') { ['website', 'cust_group', 'qty', 'price', 'delete'].each(function(idx){ $('tier_price_row_'+data.index+'_'+idx).disabled = true; @@ -128,12 +183,20 @@ var tierPriceControl = { $('tier_price_row_'+data.index+'_delete_button').hide(); } - <?php if ($_readonly) :?> - $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(this.disableElement); - $('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button').each(this.disableElement); - <?php else :?> - $('<?= $block->escapeJs($_htmlId) ?>_container').select('input', 'select').each(function(el){ Event.observe(el, 'change', el.setHasChanges.bind(el)); }); - <?php endif; ?> +script; + if ($_readonly): + $scriptString .= <<<script + $('{$block->escapeJs($_htmlId)}_container').select('input', 'select').each(this.disableElement); + $('{$block->escapeJs($_htmlId)}_container').up('table').select('button').each(this.disableElement); +script; + else: + $scriptString .= <<<script + $('{$block->escapeJs($_htmlId)}_container').select('input', 'select').each(function(el) { + Event.observe(el, 'change', el.setHasChanges.bind(el)); + }); +script; + endif; + $scriptString .= <<<script }, disableElement: function(el) { el.disabled = true; @@ -150,18 +213,30 @@ var tierPriceControl = { return false; } }; -<?php foreach ($block->getValues() as $_item) :?> -tierPriceControl.addItem('<?= $block->escapeJs($_item['website_id']) ?>', '<?= $block->escapeJs($_item['cust_group']) ?>', '<?= $_item['price_qty']*1 ?>', '<?= $block->escapeJs($_item['price']) ?>', <?= (int)!empty($_item['readonly']) ?>); +script; + ?> +<?php foreach ($block->getValues() as $_item):?> + <?php $readonly = (int)!empty($_item['readonly']); + $price_qty = $_item['price_qty']*1; + $scriptString .= <<<script +tierPriceControl.addItem('{$block->escapeJs($_item['website_id'])}', '{$block->escapeJs($_item['cust_group'])}', + '{$price_qty}', '{$block->escapeJs($_item['price'])}', {$readonly}); +script; + ?> <?php endforeach; ?> -<?php if ($_readonly) :?> -$('<?= $block->escapeJs($_htmlId) ?>_container').up('table').select('button') - .each(tierPriceControl.disableElement); +<?php if ($_readonly):?> + <?php $scriptString .= <<<script +$('{$block->escapeJs($_htmlId)}_container').up('table').select('button').each(tierPriceControl.disableElement); +script; + ?> <?php endif; ?> - +<?php $scriptString .= <<<script window.tierPriceControl = tierPriceControl; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml index 0193d7764cbb5..59b5eb7f523c1 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/edit/websites.phtml @@ -5,11 +5,12 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Websites */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset id="grop_fields" class="fieldset"> <legend class="legend"><span><?= $block->escapeHtml(__('Product In Websites')) ?></span></legend> <br> - <?php if ($block->getProductId()) :?> + <?php if ($block->getProductId()):?> <div class="messages"> <div class="message message-notice"> <?= $block->escapeHtml(__('To hide an item in catalog or search results, set the status to "Disabled".')) ?> @@ -20,37 +21,44 @@ <?= $block->getHintHtml() ?> <div class="store-tree"> <?php $_websites = $block->getWebsiteCollection() ?> - <?php foreach ($_websites as $_website) :?> + <?php foreach ($_websites as $_website):?> <div class="website-name"> <input name="product[website_ids][]" value="<?= (int) $_website->getId() ?>" - <?php if ($block->isReadonly()) :?> + <?php if ($block->isReadonly()):?> disabled="disabled" <?php endif;?> class="checkbox website-checkbox" id="product_website_<?= (int) $_website->getId() ?>" type="checkbox" - <?php if ($block->hasWebsite($_website->getId()) || !$block->getProductId() && count($_websites) === 1) :?> + <?php if ($block->hasWebsite($_website->getId()) || + !$block->getProductId() && count($_websites) === 1):?> checked="checked" <?php endif; ?> /> - <label for="product_website_<?= (int) $_website->getId() ?>"><?= $block->escapeHtml($_website->getName()) ?></label> + <label for="product_website_<?= (int) $_website->getId() ?>"> + <?= $block->escapeHtml($_website->getName()) ?> + </label> </div> <dl class="webiste-groups" id="product_website_<?= (int) $_website->getId() ?>_data"> - <?php foreach ($block->getGroupCollection($_website) as $_group) :?> + <?php foreach ($block->getGroupCollection($_website) as $_group):?> <dt><?= $block->escapeHtml($_group->getName()) ?></dt> <dd> <ul> - <?php foreach ($block->getStoreCollection($_group) as $_store) :?> + <?php foreach ($block->getStoreCollection($_group) as $_store):?> <li> <?= $block->escapeHtml($_store->getName()) ?> - <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())) :?> - <span class="website-<?= (int) $_website->getId() ?>-select" style="display:none"> + <?php if ($block->getWebsites() && !$block->hasWebsite($_website->getId())):?> + <span class="website-<?= (int) $_website->getId() ?>-select"> <?= $block->escapeHtml( __('(Copy data from: %1)', $block->getChooseFromStoreHtml($_store)), ['select', 'option', 'optgroup'] ) ?> - </span> + </span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'span.website-' . (int)$_website->getId() . '-select' + ) ?> <?php endif; ?> </li> <?php endforeach; ?> @@ -63,11 +71,11 @@ </div> </fieldset> -<script> +<?php $scriptString = <<<script require(["prototype"], function(){ //<![CDATA[ - var productWebsiteCheckboxes = $$('.website-checkbox'); + var productWebsiteCheckboxes = \$$('.website-checkbox'); for (var i = 0; i < productWebsiteCheckboxes.length; i++) { Event.observe(productWebsiteCheckboxes[i], 'click', toggleStoreFromChoosers); @@ -76,7 +84,8 @@ require(["prototype"], function(){ function toggleStoreFromChoosers(event) { var element = Event.element(event); var selects = $('product_website_' + element.value + '_data').getElementsBySelector('select'); - var selectBlocks = $('product_website_' + element.value + '_data').getElementsByClassName('website-' + element.value + '-select'); + var selectBlocks = $('product_website_' + element.value + '_data') + .getElementsByClassName('website-' + element.value + '-select'); for (var i = 0; i < selects.length; i++) { selects[i].disabled = !element.checked; } @@ -93,4 +102,6 @@ require(["prototype"], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml index d5db46f706ce3..94d71dbb5ab28 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/helper/gallery.phtml @@ -4,20 +4,19 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $elementName = $block->getElement()->getName() . '[images]'; $formName = $block->getFormName(); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div id="<?= $block->getHtmlId() ?>" class="gallery" data-mage-init='{"productGallery":{"template":"#<?= $block->getHtmlId() ?>-template"}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" data-images="<?= $block->escapeHtml($block->getImagesJson()) ?>" - data-types="<?= $block->escapeHtml( - $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes()) - ) ?>" + data-types="<?= $block->escapeHtml($jsonHelper->jsonEncode($block->getImageTypes())) ?>" > <?php if (!$block->getElement()->getReadonly()) {?> <div class="image image-placeholder"> @@ -138,7 +137,9 @@ $formName = $block->getFormName(); <textarea data-role="image-description" rows="3" class="admin__control-textarea" - name="<?= $block->escapeHtmlAttr($elementName) ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> + name="<?= $block->escapeHtmlAttr($elementName) + ?>[<%- data.file_id %>][label]"><%- data.label %> + </textarea> </div> </div> @@ -149,7 +150,7 @@ $formName = $block->getFormName(); <div class="admin__field-control"> <ul class="multiselect-alt"> <?php - foreach ($block->getMediaAttributes() as $attribute) : + foreach ($block->getMediaAttributes() as $attribute): ?> <li class="item"> <label> @@ -182,7 +183,8 @@ $formName = $block->getFormName(); <label class="admin__field-label"> <span><?= $block->escapeHtml(__('Image Resolution')) ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> </div> <div class="admin__field field-image-hide"> @@ -208,6 +210,4 @@ $formName = $block->getFormName(); </script> <?= $block->getChildHtml('new-video') ?> </div> -<script> - jQuery('body').trigger('contentUpdated'); -</script> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "jQuery('body').trigger('contentUpdated');", false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml index 0a13aee5930ad..4a0a37147dd13 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/js.phtml @@ -4,11 +4,16 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var \Magento\Catalog\Block\Adminhtml\Product\Edit\Js $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php +/** @var TaxHelper $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +$priceFormat = /* @noEscape */ $taxHelper->getPriceFormat($block->getStore()); +$allRatesByProductClassJson = /* @noEscape */ $block->getAllRatesByProductClassJson(); +$scriptString = <<<script require([ "jquery", "prototype", @@ -30,8 +35,8 @@ function registerTaxRecalcs() { Event.observe($('tax_class_id'), 'change', recalculateTax); } -var priceFormat = <?= /* @noEscape */ $this->helper(Magento\Tax\Helper\Data::class)->getPriceFormat($block->getStore()) ?>; -var taxRates = <?= /* @noEscape */ $block->getAllRatesByProductClassJson() ?>; +var priceFormat = {$priceFormat}; +var taxRates = {$allRatesByProductClassJson}; function recalculateTax() { if (typeof dynamicTaxes == 'undefined') { @@ -75,16 +80,22 @@ function bindActiveProductTab(event, ui) { jQuery(document).on('tabsactivate', bindActiveProductTab); // bind active tab -<?php if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')) :?> +script; +if ($tabsBlock = $block->getLayout()->getBlock('product_tabs')): + $scriptString .= <<<script jQuery(function () { - if (jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').length && jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').is(':mage-tabs')) { - var activeAnchor = jQuery('#<?= $block->escapeJs($tabsBlock->getId()) ?>').tabs('activeAnchor'); + if (jQuery('#{$block->escapeJs($tabsBlock->getId())}').length && + jQuery('#{$block->escapeJs($tabsBlock->getId())}').is(':mage-tabs')) { + var activeAnchor = jQuery('#{$block->escapeJs($tabsBlock->getId())}').tabs('activeAnchor'); if (activeAnchor && $('store_switcher')) { $('store_switcher').switchParams = 'active_tab/' + activeAnchor.prop('name') + '/'; } } }); -<?php endif; ?> +script; +endif; + +$scriptString .= <<<script window.recalculateTax = recalculateTax; window.bindActiveProductTab = bindActiveProductTab; @@ -92,4 +103,6 @@ window.registerTaxRecalcs = registerTaxRecalcs; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml index 5028d3c1e83d0..2cf5a78a8138a 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/catalog/product/tab/inventory.phtml @@ -5,10 +5,11 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Inventory */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->isReadonly()) :?> +<?php if ($block->isReadonly()):?> <?php $_readonly = ' disabled="disabled" '; ?> -<?php else :?> +<?php else: ?> <?php $_readonly = ''; ?> <?php endif; ?> <fieldset class="fieldset form-inline"> @@ -21,43 +22,56 @@ </label> <div class="control"> <select id="inventory_manage_stock" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][manage_stock]" <?= /* @noEscape */ $_readonly ?>> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][manage_stock]" <?= /* @noEscape */ $_readonly ?>> <option value="1"><?= $block->escapeHtml(__('Yes')) ?></option> - <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> + <option value="0"<?php if ($block->getFieldValue('manage_stock') == 0):?> + selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?> + </option> </select> <input type="hidden" id="inventory_manage_stock_default" value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('manage_stock')) ?>"> - <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_manage_stock') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_manage_stock" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_manage_stock]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_manage_stock"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> - <?php if (!$block->isReadonly()) :?> - <script> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_manage_stock" + ) ?> + <label for="inventory_use_config_manage_stock"><?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> + <?php if (!$block->isReadonly()):?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_manage_stock'), $('inventory_use_config_manage_stock').parentNode); + toggleValueElements($('inventory_use_config_manage_stock'), + $('inventory_use_config_manage_stock').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> - <?php if (!$block->getProduct()->isComposite()) :?> + <?php if (!$block->getProduct()->isComposite()): ?> <div class="field"> <label class="label" for="inventory_qty"> <span><?= $block->escapeHtml(__('Qty')) ?></span> </label> <div class="control"> - <?php if (!$_readonly) :?> + <?php if (!$_readonly): ?> <input type="hidden" id="original_inventory_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][original_inventory_qty]" + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][original_inventory_qty]" value="<?= $block->getFieldValue('qty') * 1 ?>"> <?php endif;?> <input type="text" @@ -66,7 +80,7 @@ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty]" value="<?= $block->getFieldValue('qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -83,24 +97,34 @@ value="<?= $block->getFieldValue('min_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_min_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_min_qty" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_qty]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_min_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_min_qty" + ) ?> + + <label for="inventory_use_config_min_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(["prototype"], function(){ - toggleValueElements($('inventory_use_config_min_qty'), $('inventory_use_config_min_qty').parentNode); + toggleValueElements($('inventory_use_config_min_qty'), + $('inventory_use_config_min_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -114,24 +138,35 @@ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][min_sale_qty]" value="<?= $block->getFieldValue('min_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_min_sale_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_min_sale_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_min_sale_qty]" + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_min_sale_qty]" value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_min_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_min_sale_qty" + ) ?> + <label for="inventory_use_config_min_sale_qty"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()):?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_min_sale_qty'), $('inventory_use_config_min_sale_qty').parentNode); + toggleValueElements($('inventory_use_config_min_sale_qty'), + $('inventory_use_config_min_sale_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -146,60 +181,75 @@ id="inventory_max_sale_qty" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][max_sale_qty]" value="<?= $block->getFieldValue('max_sale_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> - <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_max_sale_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <div class="control-inner-wrap"> <input type="checkbox" id="inventory_use_config_max_sale_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_max_sale_qty]" + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_max_sale_qty]" value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" class="checkbox" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_max_sale_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_max_sale_qty" + ) ?> + <label for="inventory_use_config_max_sale_qty"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()):?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_max_sale_qty'), $('inventory_use_config_max_sale_qty').parentNode); + toggleValueElements($('inventory_use_config_max_sale_qty'), + $('inventory_use_config_max_sale_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> - <?php if ($block->canUseQtyDecimals()) :?> + <?php if ($block->canUseQtyDecimals()): ?> <div class="field"> <label class="label" for="inventory_is_qty_decimal"> <span><?= $block->escapeHtml(__('Qty Uses Decimals')) ?></span> </label> <div class="control"> <select id="inventory_is_qty_decimal" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_qty_decimal]" <?= /* @noEscape */ $_readonly ?>> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][is_qty_decimal]" <?= /* @noEscape */ $_readonly ?>> <option value="0"><?= $block->escapeHtml(__('No')) ?></option> - <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> + <option value="1"<?php if ($block->getFieldValue('is_qty_decimal') == 1):?> + selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?> + </option> </select> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> - <?php if (!$block->isVirtual()) :?> + <?php if (!$block->isVirtual()): ?> <div class="field"> <label class="label" for="inventory_is_decimal_divided"> <span><?= $block->escapeHtml(__('Allow Multiple Boxes for Shipping')) ?></span> </label> <div class="control"> <select id="inventory_is_decimal_divided" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_decimal_divided]" <?= /* @noEscape */ $_readonly ?>> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][is_decimal_divided]" <?= /* @noEscape */ $_readonly ?>> <option value="0"><?= $block->escapeHtml(__('No')) ?></option> - <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1) :?> + <option value="1"<?php if ($block->getFieldValue('is_decimal_divided') == 1): ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> </select> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -212,31 +262,45 @@ </label> <div class="control"> <select id="inventory_backorders" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][backorders]" <?= /* @noEscape */ $_readonly ?>> - <?php foreach ($block->getBackordersOption() as $option) :?> - <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? 'selected="selected"' : '' ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][backorders]" <?= /* @noEscape */ $_readonly ?>> + <?php foreach ($block->getBackordersOption() as $option):?> + <?php $_selected = ($option['value'] == $block->getFieldValue('backorders')) ? + 'selected="selected"' : '' ?> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?> + </option> <?php endforeach; ?> </select> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_backorders') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_backorders" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_backorders]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_backorders"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_backorders" + ) ?> + <label for="inventory_use_config_backorders"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_backorders'), $('inventory_use_config_backorders').parentNode); + toggleValueElements($('inventory_use_config_backorders'), + $('inventory_use_config_backorders').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -250,26 +314,38 @@ class="input-text validate-number" id="inventory_notify_stock_qty" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][notify_stock_qty]" - value="<?= $block->getFieldValue('notify_stock_qty') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> + value="<?= $block->getFieldValue('notify_stock_qty') * 1 ?>" + <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_notify_stock_qty') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_notify_stock_qty" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_notify_stock_qty]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_notify_stock_qty"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_notify_stock_qty]" + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_notify_stock_qty" + ) ?> + <label for="inventory_use_config_notify_stock_qty"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_notify_stock_qty'), $('inventory_use_config_notify_stock_qty').parentNode); + toggleValueElements($('inventory_use_config_notify_stock_qty'), + $('inventory_use_config_notify_stock_qty').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -282,32 +358,48 @@ <div class="control"> <?php $qtyIncrementsEnabled = $block->getFieldValue('enable_qty_increments'); ?> <select id="inventory_enable_qty_increments" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][enable_qty_increments]" <?= /* @noEscape */ $_readonly ?>> - <option value="1"<?php if ($qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('Yes')) ?></option> - <option value="0"<?php if (!$qtyIncrementsEnabled) :?> selected="selected"<?php endif; ?>><?= $block->escapeHtml(__('No')) ?></option> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][enable_qty_increments]" <?= /* @noEscape */ $_readonly ?>> + <option value="1"<?php if ($qtyIncrementsEnabled):?> selected="selected"<?php endif; ?>> + <?= $block->escapeHtml(__('Yes')) ?> + </option> + <option value="0"<?php if (!$qtyIncrementsEnabled):?> selected="selected"<?php endif; ?>> + <?= $block->escapeHtml(__('No')) ?> + </option> </select> <input type="hidden" id="inventory_enable_qty_increments_default" value="<?= $block->escapeHtmlAttr($block->getDefaultConfigValue('enable_qty_increments')) ?>"> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_enable_qty_inc') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_enable_qty_increments" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_enable_qty_increments]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_enable_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][use_config_enable_qty_increments]" + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_enable_qty_increments" + ) ?> + <label for="inventory_use_config_enable_qty_increments"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_enable_qty_increments'), $('inventory_use_config_enable_qty_increments').parentNode); + toggleValueElements($('inventory_use_config_enable_qty_increments'), + $('inventory_use_config_enable_qty_increments').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -323,23 +415,33 @@ name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][qty_increments]" value="<?= $block->getFieldValue('qty_increments') * 1 ?>" <?= /* @noEscape */ $_readonly ?>> <div class="control-inner-wrap"> - <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->isNew()) ? 'checked="checked"' : '' ?> + <?php $_checked = ($block->getFieldValue('use_config_qty_increments') || $block->isNew()) ? + 'checked="checked"' : '' ?> <input type="checkbox" id="inventory_use_config_qty_increments" name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][use_config_qty_increments]" - value="1" <?= /* @noEscape */ $_checked ?> - onclick="toggleValueElements(this, this.parentNode);" <?= /* @noEscape */ $_readonly ?>> - <label for="inventory_use_config_qty_increments"><?= $block->escapeHtml(__('Use Config Settings')) ?></label> + value="1" <?= /* @noEscape */ $_checked ?> <?= /* @noEscape */ $_readonly ?>> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, this.parentNode);", + "#inventory_use_config_qty_increments" + ) ?> + <label for="inventory_use_config_qty_increments"> + <?= $block->escapeHtml(__('Use Config Settings')) ?> + </label> </div> - <?php if (!$block->isReadonly()) :?> - <script> + <?php if (!$block->isReadonly()): ?> + <?php $scriptString = <<<script require(['prototype'], function(){ - toggleValueElements($('inventory_use_config_qty_increments'), $('inventory_use_config_qty_increments').parentNode); + toggleValueElements($('inventory_use_config_qty_increments'), + $('inventory_use_config_qty_increments').parentNode); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()): ?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> @@ -350,21 +452,25 @@ </label> <div class="control"> <select id="inventory_stock_availability" - name="<?= /* @noEscape */ $block->getFieldSuffix() ?>[stock_data][is_in_stock]" <?= /* @noEscape */ $_readonly ?>> - <?php foreach ($block->getStockOption() as $option) :?> - <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?></option> + name="<?= /* @noEscape */ $block->getFieldSuffix() + ?>[stock_data][is_in_stock]" <?= /* @noEscape */ $_readonly ?>> + <?php foreach ($block->getStockOption() as $option):?> + <?php $_selected = ($block->getFieldValue('is_in_stock') !== null && + $option['value'] == $block->getFieldValue('is_in_stock')) ? 'selected="selected"' : '' ?> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?= /* @noEscape */ $_selected ?>><?= $block->escapeHtml($option['label']) ?> + </option> <?php endforeach; ?> </select> </div> - <?php if (!$block->isSingleStoreMode()) :?> + <?php if (!$block->isSingleStoreMode()):?> <div class="field-service"><?= $block->escapeHtml(__('[GLOBAL]')) ?></div> <?php endif; ?> </div> </div> </fieldset> -<script> +<?php $scriptString = <<<script require(["jquery","prototype"], function(jQuery){ //<![CDATA[ @@ -380,7 +486,7 @@ inventory_qty_increments: true }; - $$('#table_cataloginventory > div').each(function(el) { + \$$('#table_cataloginventory > div').each(function(el) { if (el == $('inventory_manage_stock').up(1)) { return; } @@ -406,15 +512,23 @@ } function applyEnableDecimalDivided() { - <?php if (!$block->isVirtual()) :?> +script; +if (!$block->isVirtual()): + $scriptString .= <<<script $('inventory_is_decimal_divided').up('.field').hide(); - <?php endif; ?> +script; + endif; + $scriptString .= <<<script $('inventory_qty_increments').removeClassName('validate-digits').removeClassName('validate-number'); $('inventory_min_sale_qty').removeClassName('validate-digits').removeClassName('validate-number'); if ($('inventory_is_qty_decimal').value == 1) { - <?php if (!$block->isVirtual()) :?> +script; +if (!$block->isVirtual()): + $scriptString .= <<<script $('inventory_is_decimal_divided').up('.field').show(); - <?php endif; ?> +script; + endif; + $scriptString .= <<<script $('inventory_qty_increments').addClassName('validate-number'); $('inventory_min_sale_qty').addClassName('validate-number'); } else { @@ -448,4 +562,6 @@ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml index 17fb517b32547..f58e213b60772 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/edit/attribute/search.phtml @@ -4,13 +4,13 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Edit\Tab\Attributes\Search */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="product-attribute-search-container" class="suggest-expandable attribute-selector"> <div class="action-dropdown"> - <button type="button" class="action-toggle action-choose" data-mage-init='{"dropdown":{}}' data-toggle="dropdown"> + <button type="button" class="action-toggle action-choose" data-mage-init='{"dropdown":{}}' + data-toggle="dropdown"> <span><?= $block->escapeHtml(__('Add Attribute')) ?></span> </button> <div class="dropdown-menu"> @@ -21,7 +21,8 @@ </div> </div> -<script data-template-for="product-attribute-search-<?= $block->escapeHtmlAttr($block->getGroupId()) ?>" type="text/x-magento-template"> +<script data-template-for="product-attribute-search-<?= $block->escapeHtmlAttr($block->getGroupId()) ?>" + type="text/x-magento-template"> <ul data-mage-init='{"menu":[]}'> <% if (data.items.length) { %> <% _.each(data.items, function(value){ %> @@ -32,9 +33,15 @@ <div class="actions"><?= $block->escapeHtml($block->getAttributeCreate()) ?></div> </script> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$selectorOptions = /* @noEscape */ $jsonHelper->jsonEncode($block->getSelectorOptions()); +$scriptString = <<<script require(["jquery","mage/mage","mage/backend/suggest"], function($) { - var $suggest = $('[data-role="product-attribute-search"][data-group="<?= $block->escapeHtml($block->getGroupCode()) ?>"]'); + var $suggest = $('[data-role="product-attribute-search"][data-group="{$block->escapeHtml( + $block->getGroupCode() + )}"]'); $suggest.on('suggestclose', function(e) { $suggest.closest('.dropdown-menu').siblings('[data-toggle=dropdown]').trigger('close.dropdown'); @@ -51,13 +58,13 @@ }); }); - $suggest.mage('suggest', <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSelectorOptions()) ?>) + $suggest.mage('suggest', {$selectorOptions}) .on('suggestselect', function (e, ui) { $(this).val(''); var templateId = $('#attribute_set_id').val(); if (ui.item.id) { $.ajax({ - url: '<?= $block->escapeJs($block->escapeUrl($block->getAddAttributeUrl())) ?>', + url: '{$block->escapeJs($block->getAddAttributeUrl())}', type: 'POST', dataType: 'json', data: {attribute_id: ui.item.id, template_id: templateId, group: $(this).data('group')}, @@ -70,5 +77,7 @@ } }); }); -</script> +script; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml index c814298d1dbc5..3ba507d2b3ebb 100644 --- a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/massaction_extended.phtml @@ -5,9 +5,10 @@ */ /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Grid */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="<?= $block->getHtmlId() ?>" class="admin__grid-massaction"> - <?php if ($block->getHideFormElement() !== true) :?> + <?php if ($block->getHideFormElement() !== true):?> <form action="" id="<?= $block->getHtmlId() ?>-form" method="post"> <?php endif ?> <div class="admin__grid-massaction-form"> @@ -15,20 +16,25 @@ <select id="<?= $block->getHtmlId() ?>-select" class="local-validation admin__control-select"> - <option class="admin__control-select-placeholder" value="" selected><?= $block->escapeHtml(__('Actions')) ?></option> - <?php foreach ($block->getItems() as $_item) :?> - <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>"<?= ($_item->getSelected() ? ' selected="selected"' : '') ?>><?= $block->escapeHtml($_item->getLabel()) ?></option> + <option class="admin__control-select-placeholder" value="" + selected><?= $block->escapeHtml(__('Actions')) ?> + </option> + <?php foreach ($block->getItems() as $_item):?> + <option value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" + <?= ($_item->getSelected() ? ' selected="selected"' : '') ?>> + <?= $block->escapeHtml($_item->getLabel()) ?> + </option> <?php endforeach; ?> </select> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-hiddens"></span> <span class="outer-span" id="<?= $block->getHtmlId() ?>-form-additional"></span> <?= $block->getApplyButtonHtml() ?> </div> - <?php if ($block->getHideFormElement() !== true) :?> + <?php if ($block->getHideFormElement() !== true):?> </form> <?php endif ?> <div class="no-display"> - <?php foreach ($block->getItems() as $_item) :?> + <?php foreach ($block->getItems() as $_item):?> <div id="<?= $block->getHtmlId() ?>-item-<?= $block->escapeHtmlAttr($_item->getId()) ?>-block"> <?= $_item->getAdditionalActionBlockHtml() ?> </div> @@ -39,7 +45,7 @@ <select id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-mass-select" data-menu="grid-mass-select"> <optgroup label="<?= $block->escapeHtmlAttr(__('Mass Actions')) ?>"> <option disabled selected></option> - <?php if ($block->getUseSelectAll()) :?> + <?php if ($block->getUseSelectAll()):?> <option value="selectAll"> <?= $block->escapeHtml(__('Select All')) ?> </option> @@ -58,34 +64,40 @@ <label for="<?= $block->getHtmlId() ?>-mass-select"></label> </div> -<script> + <?php $scriptString = <<<script require(['jquery'], function($){ 'use strict'; - $('#<?= $block->getHtmlId() ?>-mass-select').change(function () { + $('#{$block->getHtmlId()}-mass-select').change(function () { var massAction = $('option:selected', this).val(); switch (massAction) { - <?php if ($block->getUseSelectAll()) :?> +script; + if ($block->getUseSelectAll()): + $scriptString .= <<<script case 'selectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectAll(); + return {$block->escapeJs($block->getJsObjectName())}.selectAll(); break case 'unselectAll': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectAll(); + return {$block->escapeJs($block->getJsObjectName())}.unselectAll(); break - <?php endif; ?> +script; + endif; + $scriptString .= <<<script case 'selectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.selectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.selectVisible(); break case 'unselectVisible': - return <?= $block->escapeJs($block->getJsObjectName()) ?>.unselectVisible(); + return {$block->escapeJs($block->getJsObjectName())}.unselectVisible(); break } this.blur(); }); }); - - <?php if (!$block->getParentBlock()->canDisplayContainer()) :?> - <?= $block->escapeJs($block->getJsObjectName()) ?>.setGridIds('<?= /* @noEscape */ $block->getGridIdsJson() ?>'); - <?php endif; ?> -</script> +script; + if (!$block->getParentBlock()->canDisplayContainer()): + $scriptString .= $block->escapeJs($block->getJsObjectName()) . ".setGridIds('" . + /* @noEscape */ $block->getGridIdsJson() . "');"; + endif; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/url_filter_applier.phtml b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/url_filter_applier.phtml new file mode 100644 index 0000000000000..3e00503a882db --- /dev/null +++ b/app/code/Magento/Catalog/view/adminhtml/templates/product/grid/url_filter_applier.phtml @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $block \Magento\Backend\Block\Template */ +?> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/grid/url-filter-applier": { + "listingNamespace": "product_listing" + } + } + } +</script> 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 d3689a0db1306..6e941821e9505 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 @@ -470,7 +470,7 @@ </imports> </settings> </field> - <field name="page_layout" sortOrder="190" formElement="select" component="Magento_Catalog/js/components/use-parent-settings/select" class="Magento\Catalog\Ui\Component\Form\Field\Category\PageLayout"> + <field name="page_layout" sortOrder="190" formElement="select" component="Magento_Catalog/js/components/use-parent-settings/select"> <settings> <dataType>string</dataType> <label translate="true">Layout</label> diff --git a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml index 78d2883d7401a..950eb16fb5019 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/composite/fieldset/options/view/checkable.phtml @@ -8,9 +8,10 @@ use Magento\Catalog\Model\Product\Option; /** * @var $block \Magento\Catalog\Block\Product\View\Options\Type\Select\Checkable + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $option = $block->getOption(); -if ($option) : ?> +if ($option): ?> <?php $configValue = $block->getPreconfiguredValue($option); $optionType = $option->getType(); @@ -19,17 +20,23 @@ if ($option) : ?> ?> <div class="options-list nested" id="options-<?= $block->escapeHtmlAttr($option->getId()) ?>-list"> - <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()) :?> + <?php if ($optionType === Option::OPTION_TYPE_RADIO && !$option->getIsRequire()):?> <div class="field choice admin__field admin__field-option"> <input type="radio" id="options_<?= $block->escapeHtmlAttr($option->getId()) ?>" class="radio admin__control-radio product-custom-option" name="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" data-selector="options[<?= $block->escapeHtmlAttr($option->getId()) ?>]" - onclick="<?= $block->getSkipJsReloadPrice() ? '' : 'opConfig.reloadPrice()' ?>" value="" checked="checked" /> + <?php if (!$block->getSkipJsReloadPrice()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'opConfig.reloadPrice()', + "options_" . $block->escapeJs($option->getId()) + ) ?> + <?php endif; ?> <label class="label admin__field-label" for="options_<?= $block->escapeHtmlAttr($option->getId()) ?>"> <span> <?= $block->escapeHtml(__('None')) ?> @@ -38,7 +45,7 @@ if ($option) : ?> </div> <?php endif; ?> - <?php foreach ($option->getValues() as $value) : ?> + <?php foreach ($option->getValues() as $value): ?> <?php $checked = ''; $count++; diff --git a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml index af50446c93a95..9e740e693fbf2 100644 --- a/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml +++ b/app/code/Magento/Catalog/view/base/templates/product/price/tier_prices.phtml @@ -10,6 +10,7 @@ // phpcs:disable Generic.WhiteSpace.ScopeIndent /** @var \Magento\Catalog\Pricing\Render\PriceBox $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ /** @var \Magento\Catalog\Pricing\Price\TierPrice $tierPriceModel */ $tierPriceModel = $block->getPrice(); @@ -56,10 +57,16 @@ $product = $block->getSaleableItem(); } ?> <?= $block->escapeHtml(__('Buy %1 for: ', $price['price_qty'])) ?> - <a href="javascript:void(0);" + <a href="#" id="<?= $block->escapeHtmlAttr($popupId) ?>" data-tier-price="<?= $block->escapeHtml($block->jsonEncode($tierPriceData)) ?>"> - <?= $block->escapeHtml(__('Click for price')) ?></a> + <?= $block->escapeHtml(__('Click for price')) ?> + </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . $block->escapeHtmlAttr($popupId) + ) ?> <?php else: $priceAmountBlock = $block->renderAmount( $price['price'], diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml index e0443d5a55d97..aab181a8f7321 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/gallery.phtml @@ -5,28 +5,32 @@ */ /** @var \Magento\Catalog\Block\Product\Gallery $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_width = $block->getImageWidth(); ?> -<div class="product-image-popup" style="width:<?= /* @noEscape */ $_width ?>px;"> - <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> - <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> +<div class="product-image-popup"> + <div class="buttons-set"><a href="#" class="button" role="close-window"> + <span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> + <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()):?> <div class="nav"> - <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> - <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> + <?php if ($_prevUrl = $block->getPreviousImageUrl()):?> + <a href="<?= $block->escapeUrl($_prevUrl) ?>" + class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> <?php endif; ?> - <?php if ($_nextUrl = $block->getNextImageUrl()) :?> - <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> + <?php if ($_nextUrl = $block->getNextImageUrl()):?> + <a href="<?= $block->escapeUrl($_nextUrl) ?>" + class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> <?php endif; ?> </div> <?php endif; ?> - <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())) :?> + <?php if ($_imageTitle = $block->escapeHtml($block->getCurrentImage()->getLabel())):?> <h1 class="image-label"><?= /* @noEscape */ $_imageTitle ?></h1> <?php endif; ?> <?php $imageUrl = $block->getImageUrl(); ?> <img src="<?= $block->escapeUrl($imageUrl) ?>" - <?php if ($_width) :?> + <?php if ($_width):?> width="<?= /* @noEscape */ $_width ?>" <?php endif; ?> alt="<?= $block->escapeHtmlAttr($block->getCurrentImage()->getLabel()) ?>" @@ -34,15 +38,23 @@ id="product-gallery-image" class="image" data-mage-init='{"catalogGallery":{}}'/> - <div class="buttons-set"><a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a></div> - <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()) :?> + <div class="buttons-set">< + a href="#" class="button" role="close-window"><span><?= $block->escapeHtml(__('Close Window')) ?></span></a> + </div> + <?php if ($block->getPreviousImageUrl() || $block->getNextImageUrl()):?> <div class="nav"> - <?php if ($_prevUrl = $block->getPreviousImageUrl()) :?> - <a href="<?= $block->escapeUrl($_prevUrl) ?>" class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> + <?php if ($_prevUrl = $block->getPreviousImageUrl()):?> + <a href="<?= $block->escapeUrl($_prevUrl) ?>" + class="prev">« <?= $block->escapeHtml(__('Prev')) ?></a> <?php endif; ?> - <?php if ($_nextUrl = $block->getNextImageUrl()) :?> - <a href="<?= $block->escapeUrl($_nextUrl) ?>" class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> + <?php if ($_nextUrl = $block->getNextImageUrl()):?> + <a href="<?= $block->escapeUrl($_nextUrl) ?>" + class="next"><?= $block->escapeHtml(__('Next')) ?> »</a> <?php endif; ?> </div> <?php endif; ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'width:' . /* @noEscape */ $_width . 'px;', + 'div.product-image-popup' +) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml index 020eafcff2442..0ac6bc88df8ce 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/image_with_borders.phtml @@ -7,6 +7,7 @@ <?php /** @var $block \Magento\Catalog\Block\Product\Image */ /** @var $escaper \Magento\Framework\Escaper */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ /** * Enable lazy loading for images with borders and if variable enable_lazy_loading_for_images_without_borders * is enabled in view.xml. Otherwise small size images without borders may be distorted. So max-width is used for them @@ -17,12 +18,11 @@ $enableLazyLoadingWithoutBorders = (bool)$block->getVar( 'enable_lazy_loading_for_images_without_borders', 'Magento_Catalog' ); +$width = (int)$block->getWidth(); +$paddingBottom = $block->getRatio() * 100; ?> - -<span class="product-image-container" - style="width:<?= $escaper->escapeHtmlAttr($block->getWidth()) ?>px;"> - <span class="product-image-wrapper" - style="padding-bottom: <?= ($block->getRatio() * 100) ?>%;"> +<span class="product-image-container product-image-container-<?= /* @noEscape */ $block->getProductId() ?>"> + <span class="product-image-wrapper"> <img class="<?= $escaper->escapeHtmlAttr($block->getClass()) ?>" <?php foreach ($block->getCustomAttributes() as $name => $value): ?> <?= $escaper->escapeHtmlAttr($name) ?>="<?= $escaper->escapeHtmlAttr($value) ?>" @@ -38,3 +38,29 @@ $enableLazyLoadingWithoutBorders = (bool)$block->getVar( <?php endif; ?> alt="<?= $escaper->escapeHtmlAttr($block->getLabel()) ?>"/></span> </span> +<?php +$styles = <<<STYLE +.product-image-container-{$block->getProductId()} { + width: {$width}px; +} +.product-image-container-{$block->getProductId()} span.product-image-wrapper { + padding-bottom: {$paddingBottom}%; +} +STYLE; +//In case a script was using "style" attributes of these elements +$script = <<<SCRIPT +prodImageContainers = document.querySelectorAll(".product-image-container-{$block->getProductId()}"); +for (var i = 0; i < prodImageContainers.length; i++) { + prodImageContainers[i].style.width = "{$width}px"; +} +prodImageContainersWrappers = document.querySelectorAll( + ".product-image-container-{$block->getProductId()} span.product-image-wrapper" +); +for (var i = 0; i < prodImageContainersWrappers.length; i++) { + prodImageContainersWrappers[i].style.paddingBottom = "{$paddingBottom}%"; +} +SCRIPT; + +?> +<?= /* @noEscape */ $secureRenderer->renderTag('style', [], $styles, false) ?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', ['type' => 'text/javascript'], $script, false) ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml index 554caf6026001..e0bb6b62f0bf6 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list.phtml @@ -7,23 +7,24 @@ use Magento\Framework\App\Action\Action; ?> <?php -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis - /** * Product list template * * @var $block \Magento\Catalog\Block\Product\ListProduct * @var \Magento\Framework\Escaper $escaper + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_productCollection = $block->getLoadedProductCollection(); /** @var \Magento\Catalog\Helper\Output $_helper */ -$_helper = $this->helper(Magento\Catalog\Helper\Output::class); +$_helper = $block->getData('outputHelper'); ?> -<?php if (!$_productCollection->count()) :?> - <div class="message info empty"><div><?= $escaper->escapeHtml(__('We can\'t find products matching the selection.')) ?></div></div> -<?php else :?> +<?php if (!$_productCollection->count()):?> + <div class="message info empty"> + <div><?= $escaper->escapeHtml(__('We can\'t find products matching the selection.')) ?></div> + </div> +<?php else:?> <?= $block->getToolbarHtml() ?> <?= $block->getAdditionalHtml() ?> <?php @@ -46,14 +47,16 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <div class="products wrapper <?= /* @noEscape */ $viewMode ?> products-<?= /* @noEscape */ $viewMode ?>"> <ol class="products list items product-items"> <?php /** @var $_product \Magento\Catalog\Model\Product */ ?> - <?php foreach ($_productCollection as $_product) :?> + <?php foreach ($_productCollection as $_product):?> <li class="item product product-item"> - <div class="product-item-info" data-container="product-<?= /* @noEscape */ $viewMode ?>"> + <div class="product-item-info" + id="product-item-info_<?= /* @noEscape */ $_product->getId() ?>" + data-container="product-<?= /* @noEscape */ $viewMode ?>"> <?php $productImage = $block->getImage($_product, $imageDisplayArea); if ($pos != null) { - $position = ' style="left:' . $productImage->getWidth() . 'px;' - . 'top:' . $productImage->getHeight() . 'px;"'; + $position = 'left:' . $productImage->getWidth() . 'px;' + . 'top:' . $productImage->getHeight() . 'px;'; } ?> <?php // Product Image ?> @@ -69,19 +72,19 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <strong class="product name product-item-name"> <a class="product-item-link" href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>"> - <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name') ?> + <?=/* @noEscape */ $_helper->productAttribute($_product, $_product->getName(), 'name')?> </a> </strong> <?= $block->getReviewsSummaryHtml($_product, $templateType) ?> <?= /* @noEscape */ $block->getProductPrice($_product) ?> - <?php if ($_product->isAvailable()) :?> + <?php if ($_product->isAvailable()):?> <?= $block->getProductDetailsHtml($_product) ?> <?php endif; ?> <div class="product-item-inner"> - <div class="product actions product-item-actions"<?= strpos($pos, $viewMode . '-actions') ? $escaper->escapeHtmlAttr($position) : '' ?>> - <div class="actions-primary"<?= strpos($pos, $viewMode . '-primary') ? $escaper->escapeHtmlAttr($position) : '' ?>> - <?php if ($_product->isSaleable()) :?> + <div class="product actions product-item-actions"> + <div class="actions-primary"> + <?php if ($_product->isSaleable()):?> <?php $postParams = $block->getAddToCartPostParams($_product); ?> <form data-role="tocart-form" data-product-sku="<?= $escaper->escapeHtml($_product->getSku()) ?>" @@ -90,8 +93,11 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <input type="hidden" name="product" value="<?= /* @noEscape */ $postParams['data']['product'] ?>"> - <input type="hidden" name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" - value="<?= /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] ?>"> + <input type="hidden" + name="<?= /* @noEscape */ Action::PARAM_NAME_URL_ENCODED ?>" + value="<?= + /* @noEscape */ $postParams['data'][Action::PARAM_NAME_URL_ENCODED] + ?>"> <?= $block->getBlockHtml('formkey') ?> <button type="submit" title="<?= $escaper->escapeHtmlAttr(__('Add to Cart')) ?>" @@ -99,23 +105,39 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); <span><?= $escaper->escapeHtml(__('Add to Cart')) ?></span> </button> </form> - <?php else :?> - <?php if ($_product->isAvailable()) :?> - <div class="stock available"><span><?= $escaper->escapeHtml(__('In stock')) ?></span></div> - <?php else :?> - <div class="stock unavailable"><span><?= $escaper->escapeHtml(__('Out of stock')) ?></span></div> + <?php else:?> + <?php if ($_product->isAvailable()):?> + <div class="stock available"> + <span><?= $escaper->escapeHtml(__('In stock')) ?></span></div> + <?php else:?> + <div class="stock unavailable"> + <span><?= $escaper->escapeHtml(__('Out of stock')) ?></span></div> <?php endif; ?> <?php endif; ?> </div> - <div data-role="add-to-links" class="actions-secondary"<?= strpos($pos, $viewMode . '-secondary') ? $escaper->escapeHtmlAttr($position) : '' ?>> - <?php if ($addToBlock = $block->getChildBlock('addto')) :?> + <?= strpos($pos, $viewMode . '-primary') ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + $position, + 'product-item-info_' . $_product->getId() . ' div.actions-primary' + ) : '' ?> + <div data-role="add-to-links" class="actions-secondary"> + <?php if ($addToBlock = $block->getChildBlock('addto')):?> <?= $addToBlock->setProduct($_product)->getChildHtml() ?> <?php endif; ?> </div> + <?= strpos($pos, $viewMode . '-secondary') ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + $position, + 'product-item-info_' . $_product->getId() . ' div.actions-secondary' + ) : '' ?> </div> - <?php if ($showDescription) :?> + <?php if ($showDescription):?> <div class="product description product-item-description"> - <?= /* @noEscape */ $_helper->productAttribute($_product, $_product->getShortDescription(), 'short_description') ?> + <?= /* @noEscape */ $_helper->productAttribute( + $_product, + $_product->getShortDescription(), + 'short_description' + ) ?> <a href="<?= $escaper->escapeUrl($_product->getProductUrl()) ?>" title="<?= /* @noEscape */ $_productNameStripped ?>" class="action more"><?= $escaper->escapeHtml(__('Learn More')) ?></a> @@ -124,12 +146,17 @@ $_helper = $this->helper(Magento\Catalog\Helper\Output::class); </div> </div> </div> + <?= strpos($pos, $viewMode . '-actions') ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + $position, + 'product-item-info_' . $_product->getId() . ' div.product-item-actions' + ) : '' ?> </li> <?php endforeach; ?> </ol> </div> <?= $block->getToolbarHtml() ?> - <?php if (!$block->isRedirectToCartEnabled()) :?> + <?php if (!$block->isRedirectToCartEnabled()):?> <script type="text/x-magento-init"> { "[data-role=tocart-form], .form.map.checkout": { diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml index c8b35e4dc5aa6..e426b940deab7 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/list/items.phtml @@ -7,12 +7,8 @@ use Magento\Catalog\ViewModel\Product\Listing\PreparePostData; use Magento\Framework\App\ActionInterface; -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -// phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect -// phpcs:disable Generic.Files.LineLength -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper - /* @var $block \Magento\Catalog\Block\Product\AbstractProduct */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -164,11 +160,19 @@ $_item = null; <?php if ($exist):?> -<?php if ($type == 'related' || $type == 'upsell'):?> -<?php if ($type == 'related'):?> -<div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>" data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> + <?php if ($type == 'related' || $type == 'upsell'):?> + <?php if ($type == 'related'):?> +<div class="block <?= $block->escapeHtmlAttr($class) ?>" + data-mage-init='{"relatedProducts":{"relatedCheckbox":".related.checkbox"}}' + data-limit="<?= $block->escapeHtmlAttr($limit) ?>" + data-shuffle="<?= /* @noEscape */ $shuffle ?>" + data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> <?php else:?> - <div class="block <?= $block->escapeHtmlAttr($class) ?>" data-mage-init='{"upsellProducts":{}}' data-limit="<?= $block->escapeHtmlAttr($limit) ?>" data-shuffle="<?= /* @noEscape */ $shuffle ?>" data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> + <div class="block <?= $block->escapeHtmlAttr($class) ?>" + data-mage-init='{"upsellProducts":{}}' + data-limit="<?= $block->escapeHtmlAttr($limit) ?>" + data-shuffle="<?= /* @noEscape */ $shuffle ?>" + data-shuffle-weighted="<?= /* @noEscape */ $isWeightedRandom ?>"> <?php endif; ?> <?php else:?> <div class="block <?= $block->escapeHtmlAttr($class) ?>"> @@ -195,7 +199,13 @@ $_item = null; <?php endif; ?> <?php endif; ?> <?php if ($type == 'related' || $type == 'upsell'):?> - <li class="item product product-item" style="display: none;" data-shuffle-group="<?= $block->escapeHtmlAttr($_item->getPriority()) ?>" > + <li class="item product product-item" + id="product-item_<?= /* @noEscape */ $_item->getId() ?>" + data-shuffle-group="<?= $block->escapeHtmlAttr($_item->getPriority()) ?>" > + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none;', + 'li#product-item_' . $_item->getId() + ) ?> <?php else:?> <li class="item product product-item"> <?php endif; ?> @@ -222,17 +232,16 @@ $_item = null; <?php if ($canItemsAddToCart && !$_item->isComposite() && $_item->isSaleable() && $type == 'related'):?> <?php if (!$_item->getRequiredOptions()):?> - <div class="field choice related"><input - type="checkbox" - class="checkbox related" - id="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>" - name="related_products[]" - value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" /> - <label - class="label" - for="related-checkbox<?= $block->escapeHtmlAttr( - $_item->getId() - ) ?>"><span><?= $block->escapeHtml(__('Add to Cart')) ?></span> + <div class="field choice related"> + <input + type="checkbox" + class="checkbox related" + id="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) ?>" + name="related_products[]" + value="<?= $block->escapeHtmlAttr($_item->getId()) ?>" /> + <label class="label" + for="related-checkbox<?= $block->escapeHtmlAttr($_item->getId()) + ?>"><span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </label> </div> <?php endif; ?> diff --git a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml index e83e55ad2a03c..f5fd1c5aa64e1 100644 --- a/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml +++ b/app/code/Magento/Catalog/view/frontend/templates/product/view/options/type/file.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Catalog\Block\Product\View\Options\Type\File */ ?> <?php $_option = $block->getOption(); ?> <?php $_fileInfo = $block->getFileInfo(); ?> <?php $_fileExists = $_fileInfo->hasData(); ?> @@ -19,13 +21,18 @@ <span><?= $block->escapeHtml($_option->getTitle()) ?></span> <?= /* @noEscape */ $block->getFormattedPrice() ?> </label> - <?php if ($_fileExists) :?> + <?php if ($_fileExists):?> <div class="control"> <span class="<?= /* @noEscape */ $_fileNamed ?>"><?= $block->escapeHtml($_fileInfo->getTitle()) ?></span> - <a href="javascript:void(0)" class="label" id="change-<?= /* @noEscape */ $_fileName ?>" > + <a href="#" class="label" id="change-<?= /* @noEscape */ $_fileName ?>" > <?= $block->escapeHtml(__('Change')) ?> </a> - <?php if (!$_option->getIsRequire()) :?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#change-' ./* @noEscape */ $_fileName + ) ?> + <?php if (!$_option->getIsRequire()):?> <input type="checkbox" id="delete-<?= /* @noEscape */ $_fileName ?>" /> <span class="label"><?= $block->escapeHtml(__('Delete')) ?></span> <?php endif; ?> @@ -38,28 +45,36 @@ "fieldNameAction":"<?= /* @noEscape */ $_fieldNameAction ?>", "changeFileSelector":"#change-<?= /* @noEscape */ $_fileName ?>", "deleteFileSelector":"#delete-<?= /* @noEscape */ $_fileName ?>"} - }' - <?= $_fileExists ? 'style="display:none"' : '' ?>> + }'> <input type="file" name="<?= /* @noEscape */ $_fileName ?>" id="<?= /* @noEscape */ $_fileName ?>" class="product-custom-option<?= $_option->getIsRequire() ? ' required' : '' ?>" <?= $_fileExists ? 'disabled="disabled"' : '' ?> /> - <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" value="<?= /* @noEscape */ $_fieldValueAction ?>" /> - <?php if ($_option->getFileExtension()) :?> + <input type="hidden" name="<?= /* @noEscape */ $_fieldNameAction ?>" + value="<?= /* @noEscape */ $_fieldValueAction ?>" /> + <?php if ($_option->getFileExtension()):?> <p class="note"> - <?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> + <?= $block->escapeHtml(__('Compatible file extensions to upload')) ?>: + <strong><?= $block->escapeHtml($_option->getFileExtension()) ?></strong> </p> <?php endif; ?> - <?php if ($_option->getImageSizeX() > 0) :?> + <?php if ($_option->getImageSizeX() > 0):?> <p class="note"> - <?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + <?= $block->escapeHtml(__('Maximum image width')) ?>: <strong><?= (int)$_option->getImageSizeX() + ?> <?= $block->escapeHtml(__('px.')) ?></strong> </p> <?php endif; ?> - <?php if ($_option->getImageSizeY() > 0) :?> + <?php if ($_option->getImageSizeY() > 0):?> <p class="note"> - <?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() ?> <?= $block->escapeHtml(__('px.')) ?></strong> + <?= $block->escapeHtml(__('Maximum image height')) ?>: <strong><?= (int)$_option->getImageSizeY() + ?> <?= $block->escapeHtml(__('px.')) ?></strong> </p> <?php endif; ?> </div> + <?= $_fileExists ? + /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#input-box-' . /* @noEscape */ $_fileName + ) : '' ?> </div> diff --git a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js index dfc0b4291cd6e..013732ca57875 100644 --- a/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js +++ b/app/code/Magento/Catalog/view/frontend/web/js/product/list/toolbar.js @@ -35,10 +35,26 @@ define([ /** @inheritdoc */ _create: function () { - this._bind($(this.options.modeControl), this.options.mode, this.options.modeDefault); - this._bind($(this.options.directionControl), this.options.direction, this.options.directionDefault); - this._bind($(this.options.orderControl), this.options.order, this.options.orderDefault); - this._bind($(this.options.limitControl), this.options.limit, this.options.limitDefault); + this._bind( + $(this.options.modeControl, this.element), + this.options.mode, + this.options.modeDefault + ); + this._bind( + $(this.options.directionControl, this.element), + this.options.direction, + this.options.directionDefault + ); + this._bind( + $(this.options.orderControl, this.element), + this.options.order, + this.options.orderDefault + ); + this._bind( + $(this.options.limitControl, this.element), + this.options.limit, + this.options.limitDefault + ); }, /** @inheritdoc */ diff --git a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php index 4e75139c1a882..e78224ba0af38 100644 --- a/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php +++ b/app/code/Magento/CatalogCustomerGraphQl/Model/Resolver/PriceTiers.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\Tiers; use Magento\CatalogCustomerGraphQl\Model\Resolver\Product\Price\TiersFactory; use Magento\CatalogCustomerGraphQl\Model\Resolver\Customer\GetCustomerGroup; @@ -60,25 +61,33 @@ class PriceTiers implements ResolverInterface */ private $priceProviderPool; + /** + * @var PriceCurrencyInterface + */ + private $priceCurrency; + /** * @param ValueFactory $valueFactory * @param TiersFactory $tiersFactory * @param GetCustomerGroup $getCustomerGroup * @param Discount $discount * @param PriceProviderPool $priceProviderPool + * @param PriceCurrencyInterface $priceCurrency */ public function __construct( ValueFactory $valueFactory, TiersFactory $tiersFactory, GetCustomerGroup $getCustomerGroup, Discount $discount, - PriceProviderPool $priceProviderPool + PriceProviderPool $priceProviderPool, + PriceCurrencyInterface $priceCurrency ) { $this->valueFactory = $valueFactory; $this->tiersFactory = $tiersFactory; $this->getCustomerGroup = $getCustomerGroup; $this->discount = $discount; $this->priceProviderPool = $priceProviderPool; + $this->priceCurrency = $priceCurrency; } /** @@ -130,6 +139,7 @@ private function formatProductTierPrices(array $tierPrices, float $productPrice, $tiers = []; foreach ($tierPrices as $tierPrice) { + $tierPrice->setValue($this->priceCurrency->convertAndRound($tierPrice->getValue())); $percentValue = $tierPrice->getExtensionAttributes()->getPercentageValue(); if ($percentValue && is_numeric($percentValue)) { $discount = $this->discount->getDiscountByPercent($productPrice, (float)$percentValue); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php index 320e0adc29b9f..140659abfbfe6 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/AttributeOptionProvider.php @@ -8,6 +8,7 @@ namespace Magento\CatalogGraphQl\DataProvider\Product\LayeredNavigation; use Magento\Framework\App\ResourceConnection; +use Magento\Store\Model\Store; /** * Fetch product attribute option data including attribute info @@ -41,16 +42,18 @@ public function __construct(ResourceConnection $resourceConnection) * Get option data. Return list of attributes with option data * * @param array $optionIds + * @param int|null $storeId * @param array $attributeCodes * @return array * @throws \Zend_Db_Statement_Exception */ - public function getOptions(array $optionIds, array $attributeCodes = []): array + public function getOptions(array $optionIds, ?int $storeId, array $attributeCodes = []): array { if (!$optionIds) { return []; } + $storeId = $storeId ?: Store::DEFAULT_STORE_ID; $connection = $this->resourceConnection->getConnection(); $select = $connection->select() ->from( @@ -70,9 +73,21 @@ public function getOptions(array $optionIds, array $attributeCodes = []): array ['option_value' => $this->resourceConnection->getTableName('eav_attribute_option_value')], 'options.option_id = option_value.option_id', [ - 'option_label' => 'option_value.value', 'option_id' => 'option_value.option_id', ] + )->joinLeft( + ['option_value_store' => $this->resourceConnection->getTableName('eav_attribute_option_value')], + "options.option_id = option_value_store.option_id AND option_value_store.store_id = {$storeId}", + [ + 'option_label' => $connection->getCheckSql( + 'option_value_store.value_id > 0', + 'option_value_store.value', + 'option_value.value' + ) + ] + )->where( + 'a.attribute_id = options.attribute_id AND option_value.store_id = ?', + Store::DEFAULT_STORE_ID ); $select->where('option_value.option_id IN (?)', $optionIds); diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php index 0ec65c88024f2..105e91320de49 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/LayeredNavigation/Builder/Attribute.php @@ -71,7 +71,7 @@ public function __construct( */ public function build(AggregationInterface $aggregation, ?int $storeId): array { - $attributeOptions = $this->getAttributeOptions($aggregation); + $attributeOptions = $this->getAttributeOptions($aggregation, $storeId); // build layer per attribute $result = []; @@ -133,10 +133,11 @@ private function isBucketEmpty(?BucketInterface $bucket): bool * Get list of attributes with options * * @param AggregationInterface $aggregation + * @param int|null $storeId * @return array * @throws \Zend_Db_Statement_Exception */ - private function getAttributeOptions(AggregationInterface $aggregation): array + private function getAttributeOptions(AggregationInterface $aggregation, ?int $storeId): array { $attributeOptionIds = []; $attributes = []; @@ -154,6 +155,6 @@ function (AggregationValueInterface $value) { return []; } - return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $attributes); + return $this->attributeOptionProvider->getOptions(\array_merge(...$attributeOptionIds), $storeId, $attributes); } } diff --git a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php index 1057d21283ea8..b6837b334fdd8 100644 --- a/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php +++ b/app/code/Magento/CatalogGraphQl/DataProvider/Product/SearchCriteriaBuilder.php @@ -101,9 +101,10 @@ public function build(array $args, bool $includeAggregation): SearchCriteriaInte } if (!$searchCriteria->getSortOrders()) { - $this->addDefaultSortOrder($searchCriteria, $isSearch); + $this->addDefaultSortOrder($searchCriteria, $args, $isSearch); } + $this->addEntityIdSort($searchCriteria, $isSearch); $this->addVisibilityFilter($searchCriteria, $isSearch, !empty($args['filter'])); $searchCriteria->setCurrentPage($args['currentPage']); @@ -132,6 +133,25 @@ private function addVisibilityFilter(SearchCriteriaInterface $searchCriteria, bo $this->addFilter($searchCriteria, 'visibility', $visibilityIds, 'in'); } + /** + * Add sort by Entity ID + * + * @param SearchCriteriaInterface $searchCriteria + * @param bool $isSearch + */ + private function addEntityIdSort(SearchCriteriaInterface $searchCriteria, bool $isSearch): void + { + if ($isSearch) { + return; + } + $sortOrderArray = $searchCriteria->getSortOrders(); + $sortOrderArray[] = $this->sortOrderBuilder + ->setField('_id') + ->setDirection(SortOrder::SORT_DESC) + ->create(); + $searchCriteria->setSortOrders($sortOrderArray); + } + /** * Prepare price aggregation algorithm * @@ -179,18 +199,32 @@ private function addFilter( * Sort by relevance DESC by default * * @param SearchCriteriaInterface $searchCriteria + * @param array $args * @param bool $isSearch */ - private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, $isSearch = false): void + private function addDefaultSortOrder(SearchCriteriaInterface $searchCriteria, array $args, $isSearch = false): void { - $sortField = $isSearch ? 'relevance' : EavAttributeInterface::POSITION; - $sortDirection = $isSearch ? SortOrder::SORT_DESC : SortOrder::SORT_ASC; - $defaultSortOrder = $this->sortOrderBuilder - ->setField($sortField) - ->setDirection($sortDirection) - ->create(); + $defaultSortOrder = []; + if ($isSearch) { + $defaultSortOrder[] = $this->sortOrderBuilder + ->setField('relevance') + ->setDirection(SortOrder::SORT_DESC) + ->create(); + } else { + $categoryIdFilter = isset($args['filter']['category_id']) ? $args['filter']['category_id'] : false; + if ($categoryIdFilter) { + if (!is_array($categoryIdFilter[array_key_first($categoryIdFilter)]) + || count($categoryIdFilter[array_key_first($categoryIdFilter)]) <= 1 + ) { + $defaultSortOrder[] = $this->sortOrderBuilder + ->setField(EavAttributeInterface::POSITION) + ->setDirection(SortOrder::SORT_ASC) + ->create(); + } + } + } - $searchCriteria->setSortOrders([$defaultSortOrder]); + $searchCriteria->setSortOrders($defaultSortOrder); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php index 69592657241a0..0bfd9d58ec969 100644 --- a/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php +++ b/app/code/Magento/CatalogGraphQl/Model/AttributesJoiner.php @@ -8,7 +8,10 @@ namespace Magento\CatalogGraphQl\Model; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; use Magento\Eav\Model\Entity\Collection\AbstractCollection; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Joins attributes for provided field node field names. @@ -43,11 +46,12 @@ public function __construct(array $fieldToAttributeMap = []) * * @param FieldNode $fieldNode * @param AbstractCollection $collection + * @param ResolveInfo $resolveInfo * @return void */ - public function join(FieldNode $fieldNode, AbstractCollection $collection): void + public function join(FieldNode $fieldNode, AbstractCollection $collection, ResolveInfo $resolveInfo): void { - foreach ($this->getQueryFields($fieldNode) as $field) { + foreach ($this->getQueryFields($fieldNode, $resolveInfo) as $field) { $this->addFieldToCollection($collection, $field); } } @@ -56,26 +60,70 @@ public function join(FieldNode $fieldNode, AbstractCollection $collection): void * Get an array of queried fields. * * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return string[] */ - public function getQueryFields(FieldNode $fieldNode): array + public function getQueryFields(FieldNode $fieldNode, ResolveInfo $resolveInfo): array { if (null === $this->getFieldNodeSelections($fieldNode)) { $query = $fieldNode->selectionSet->selections; $selectedFields = []; + $fragmentFields = []; /** @var FieldNode $field */ foreach ($query as $field) { - if ($field->kind === 'InlineFragment') { - continue; + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $fragmentFields[] = $this->addInlineFragmentFields($resolveInfo, $field); + } elseif ($field->kind === NodeKind::FRAGMENT_SPREAD && + ($spreadFragmentNode = $resolveInfo->fragments[$field->name->value])) { + + foreach ($spreadFragmentNode->selectionSet->selections as $spreadNode) { + if (isset($spreadNode->selectionSet->selections)) { + $fragmentFields[] = $this->getQueryFields($spreadNode, $resolveInfo); + } else { + $selectedFields[] = $spreadNode->name->value; + } + } + } else { + $selectedFields[] = $field->name->value; } - $selectedFields[] = $field->name->value; } - $this->setSelectionsForFieldNode($fieldNode, $selectedFields); + if ($fragmentFields) { + $selectedFields = array_merge($selectedFields, array_merge(...$fragmentFields)); + } + $this->setSelectionsForFieldNode($fieldNode, array_unique($selectedFields)); } return $this->getFieldNodeSelections($fieldNode); } + /** + * Add fields from inline fragment nodes + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $inlineFragmentFields + * @return string[] + */ + private function addInlineFragmentFields( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $inlineFragmentFields = [] + ): array { + $query = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($query as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $this->addInlineFragmentFields($resolveInfo, $field, $inlineFragmentFields); + } elseif (isset($field->selectionSet->selections)) { + continue; + } else { + $inlineFragmentFields[] = $field->name->value; + } + } + + return array_unique($inlineFragmentFields); + } + /** * Add field to collection select * diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php index 1fae247c981d2..dc93005983776 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/CategoryFilter.php @@ -13,6 +13,7 @@ use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Filter; +use Magento\Framework\GraphQl\Query\Resolver\Argument\SearchCriteria\ArgumentApplier\Sort; use Magento\Search\Model\Query; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\ScopeInterface; @@ -71,6 +72,7 @@ public function getResult(array $criteria, StoreInterface $store) $categoryIds = []; $criteria[Filter::ARGUMENT_NAME] = $this->formatMatchFilters($criteria['filters'], $store); $criteria[Filter::ARGUMENT_NAME][CategoryInterface::KEY_IS_ACTIVE] = ['eq' => 1]; + $criteria[Sort::ARGUMENT_NAME][CategoryInterface::KEY_POSITION] = ['ASC']; $searchCriteria = $this->searchCriteriaBuilder->build('categoryList', $criteria); $pageSize = $criteria['pageSize'] ?? 20; $currentPage = $criteria['currentPage'] ?? 1; diff --git a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php index b5d02511da4e7..ab100c7272ba0 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php +++ b/app/code/Magento/CatalogGraphQl/Model/Category/DepthCalculator.php @@ -8,6 +8,9 @@ namespace Magento\CatalogGraphQl\Model\Category; use GraphQL\Language\AST\FieldNode; +use GraphQL\Language\AST\InlineFragmentNode; +use GraphQL\Language\AST\NodeKind; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Used for determining the depth information for a requested category tree in a GraphQL request @@ -17,22 +20,57 @@ class DepthCalculator /** * Calculate the total depth of a category tree inside a GraphQL request * + * @param ResolveInfo $resolveInfo * @param FieldNode $fieldNode * @return int */ - public function calculate(FieldNode $fieldNode) : int + public function calculate(ResolveInfo $resolveInfo, FieldNode $fieldNode) : int { $selections = $fieldNode->selectionSet->selections ?? []; $depth = count($selections) ? 1 : 0; $childrenDepth = [0]; foreach ($selections as $node) { - if ($node->kind === 'InlineFragment' || null !== $node->alias) { + if (isset($node->alias) && null !== $node->alias) { continue; } - $childrenDepth[] = $this->calculate($node); + if ($node->kind === NodeKind::INLINE_FRAGMENT) { + $childrenDepth[] = $this->addInlineFragmentDepth($resolveInfo, $node); + } elseif ($node->kind === NodeKind::FRAGMENT_SPREAD && isset($resolveInfo->fragments[$node->name->value])) { + foreach ($resolveInfo->fragments[$node->name->value]->selectionSet->selections as $spreadNode) { + $childrenDepth[] = $this->calculate($resolveInfo, $spreadNode); + } + } else { + $childrenDepth[] = $this->calculate($resolveInfo, $node); + } } return $depth + max($childrenDepth); } + + /** + * Add inline fragment fields into calculating of category depth + * + * @param ResolveInfo $resolveInfo + * @param InlineFragmentNode $inlineFragmentField + * @param array $depth + * @return int + */ + private function addInlineFragmentDepth( + ResolveInfo $resolveInfo, + InlineFragmentNode $inlineFragmentField, + $depth = [] + ): int { + $selections = $inlineFragmentField->selectionSet->selections; + /** @var FieldNode $field */ + foreach ($selections as $field) { + if ($field->kind === NodeKind::INLINE_FRAGMENT) { + $depth[] = $this->addInlineFragmentDepth($resolveInfo, $field, $depth); + } elseif ($field->selectionSet && $field->selectionSet->selections) { + $depth[] = $this->calculate($resolveInfo, $field); + } + } + + return $depth ? max($depth) : 0; + } } diff --git a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php index 5a230ceed0ca4..c6de07bdedd19 100644 --- a/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php +++ b/app/code/Magento/CatalogGraphQl/Model/ProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class ProductLinksTypeResolver implements TypeResolverInterface { @@ -20,9 +20,9 @@ class ProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['related', 'upsell', 'crosssell']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php index 535fe3a80cd25..d7118d71db89b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Categories.php @@ -7,18 +7,18 @@ namespace Magento\CatalogGraphQl\Model\Resolver; -use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; -use Magento\Framework\Exception\LocalizedException; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Catalog\Api\Data\CategoryInterface; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; +use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\CatalogGraphQl\Model\Resolver\Product\ProductCategories; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\CustomAttributesFlattener; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\GraphQl\Config\Element\Field; -use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; -use Magento\CatalogGraphQl\Model\Category\Hydrator as CategoryHydrator; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Store\Model\StoreManagerInterface; /** @@ -121,7 +121,7 @@ function () use ($that, $categoryIds, $info) { } if (!$this->collection->isLoaded()) { - $that->attributesJoiner->join($info->fieldNodes[0], $this->collection); + $that->attributesJoiner->join($info->fieldNodes[0], $this->collection, $info); $this->collection->addIdFilter($this->categoryIds); } /** @var CategoryInterface | \Magento\Catalog\Model\Category $item */ @@ -130,7 +130,7 @@ function () use ($that, $categoryIds, $info) { // Try to extract all requested fields from the loaded collection data $categories[$item->getId()] = $this->categoryHydrator->hydrateCategory($item, true); $categories[$item->getId()]['model'] = $item; - $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0]); + $requestedFields = $that->attributesJoiner->getQueryFields($info->fieldNodes[0], $info); $extractedFields = array_keys($categories[$item->getId()]); $foundFields = array_intersect($requestedFields, $extractedFields); if (count($requestedFields) === count($foundFields)) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php index 85b86f313de4d..b966fce43f56d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Category/Products.php @@ -63,7 +63,7 @@ public function resolve( 'eq' => $value['id'] ] ]; - $searchResult = $this->searchQuery->getResult($args, $info); + $searchResult = $this->searchQuery->getResult($args, $info, $context); //possible division by 0 if ($searchResult->getPageSize()) { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php index 14732ecf37c63..187fd05c1001e 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/BatchProductLinks.php @@ -22,7 +22,15 @@ class BatchProductLinks implements BatchServiceContractResolverInterface /** * @var string[] */ - private static $linkTypes = ['related', 'upsell', 'crosssell']; + private $linkTypes; + + /** + * @param array $linkTypes + */ + public function __construct(array $linkTypes) + { + $this->linkTypes = $linkTypes; + } /** * @inheritDoc @@ -44,7 +52,7 @@ public function convertToServiceArgument(ResolveRequestInterface $request) /** @var \Magento\Catalog\Model\Product $product */ $product = $value['model']; - return new ListCriteria((string)$product->getId(), self::$linkTypes, $product); + return new ListCriteria((string)$product->getId(), $this->linkTypes, $product); } /** diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php new file mode 100644 index 0000000000000..2d6975bb8a4e2 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableEnteredOptionValueUid.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for entered custom options + */ +class CustomizableEnteredOptionValueUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + + /** + * Create a option uid for entered option in "<option-type>/<option-id>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (isset($value['uid'])) { + return $value['uid']; + } + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php new file mode 100644 index 0000000000000..795782d6e3718 --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/CustomizableSelectedOptionValueUid.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for selected custom options + */ +class CustomizableSelectedOptionValueUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'custom-option'; + + /** + * Create a option uid for selected option in "<option-type>/<option-id>/<option-value-id>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (isset($value['uid'])) { + return $value['uid']; + } + if (!isset($value['option_id']) || empty($value['option_id'])) { + throw new GraphQlInputException(__('"option_id" value should be specified.')); + } + + if (!isset($value['option_type_id']) || empty($value['option_type_id'])) { + throw new GraphQlInputException(__('"option_type_id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['option_id'], + $value['option_type_id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php index e1338930afe5d..8843ad02320c6 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/MediaGalleryEntries.php @@ -18,7 +18,7 @@ * @inheritdoc * * Format a product's media gallery information to conform to GraphQL schema representation - * @deprecated + * @deprecated 100.3.3 */ class MediaGalleryEntries implements ResolverInterface { diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php index 9ddad4e6451fa..3139c35774008 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/ProductFieldsSelector.php @@ -7,6 +7,7 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Product; +use GraphQL\Language\AST\NodeKind; use Magento\Framework\GraphQl\Query\FieldTranslator; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; @@ -43,9 +44,9 @@ public function getProductFieldsFromInfo(ResolveInfo $info, string $productNodeN continue; } foreach ($node->selectionSet->selections as $selectionNode) { - if ($selectionNode->kind === 'InlineFragment') { + if ($selectionNode->kind === NodeKind::INLINE_FRAGMENT) { foreach ($selectionNode->selectionSet->selections as $inlineSelection) { - if ($inlineSelection->kind === 'InlineFragment') { + if ($inlineSelection->kind === NodeKind::INLINE_FRAGMENT) { continue; } $fieldNames[] = $this->fieldTranslator->translate($inlineSelection->name->value); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php new file mode 100644 index 0000000000000..1b42b0fde2bcb --- /dev/null +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Product/SpecialPrice.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Pricing\Price\SpecialPrice as PricingSpecialPrice; + +/** + * Resolver for Special Price + */ +class SpecialPrice implements ResolverInterface +{ + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + /** @var ProductInterface $product */ + $product = $value['model']; + /** @var PricingSpecialPrice $specialPrice */ + $specialPrice = $product->getPriceInfo()->getPrice(PricingSpecialPrice::PRICE_CODE); + + if ($specialPrice->getValue()) { + return $specialPrice->getValue(); + } + + return null; + } +} diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php index e3d9ba2a9b3c6..1a244b8a10546 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products.php @@ -69,7 +69,7 @@ public function resolve( ); } - $searchResult = $this->searchQuery->getResult($args, $info); + $searchResult = $this->searchQuery->getResult($args, $info, $context); if ($searchResult->getCurrentPage() > $searchResult->getTotalPages() && $searchResult->getTotalCount() > 0) { throw new GraphQlInputException( diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php index fc5a563c82b4e..c553d4486f9e9 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/CategoryTree.php @@ -8,15 +8,16 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider; use GraphQL\Language\AST\FieldNode; -use Magento\CatalogGraphQl\Model\Category\DepthCalculator; -use Magento\CatalogGraphQl\Model\Category\LevelCalculator; -use Magento\Framework\EntityManager\MetadataPool; -use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use GraphQL\Language\AST\NodeKind; use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category; use Magento\Catalog\Model\ResourceModel\Category\Collection; use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory; use Magento\CatalogGraphQl\Model\AttributesJoiner; -use Magento\Catalog\Model\Category; +use Magento\CatalogGraphQl\Model\Category\DepthCalculator; +use Magento\CatalogGraphQl\Model\Category\LevelCalculator; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; /** * Category tree data provider @@ -85,8 +86,8 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato { $categoryQuery = $resolveInfo->fieldNodes[0]; $collection = $this->collectionFactory->create(); - $this->joinAttributesRecursively($collection, $categoryQuery); - $depth = $this->depthCalculator->calculate($categoryQuery); + $this->joinAttributesRecursively($collection, $categoryQuery, $resolveInfo); + $depth = $this->depthCalculator->calculate($resolveInfo, $categoryQuery); $level = $this->levelCalculator->calculate($rootCategoryId); // If root category is being filter, we've to remove first slash @@ -124,24 +125,27 @@ public function getTree(ResolveInfo $resolveInfo, int $rootCategoryId): \Iterato * * @param Collection $collection * @param FieldNode $fieldNode + * @param ResolveInfo $resolveInfo * @return void */ - private function joinAttributesRecursively(Collection $collection, FieldNode $fieldNode) : void - { + private function joinAttributesRecursively( + Collection $collection, + FieldNode $fieldNode, + ResolveInfo $resolveInfo + ): void { if (!isset($fieldNode->selectionSet->selections)) { return; } $subSelection = $fieldNode->selectionSet->selections; - $this->attributesJoiner->join($fieldNode, $collection); + $this->attributesJoiner->join($fieldNode, $collection, $resolveInfo); /** @var FieldNode $node */ foreach ($subSelection as $node) { - if ($node->kind === 'InlineFragment') { + if ($node->kind === NodeKind::INLINE_FRAGMENT || $node->kind === NodeKind::FRAGMENT_SPREAD) { continue; } - - $this->joinAttributesRecursively($collection, $node); + $this->joinAttributesRecursively($collection, $node, $resolveInfo); } } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php index 86616cc14fe50..22bbc991a78e2 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Deferred/Product.php @@ -10,6 +10,7 @@ use Magento\Catalog\Api\Data\ProductInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductDataProvider; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Deferred resolver for product data. diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php index 2076ec6726988..3e955ae303453 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product.php @@ -14,6 +14,7 @@ use Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory; use Magento\Framework\Api\SearchResultsInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Product field data provider, used for GraphQL resolver processing. @@ -73,18 +74,20 @@ public function __construct( * @param string[] $attributes * @param bool $isSearch * @param bool $isChildSearch + * @param ContextInterface|null $context * @return SearchResultsInterface */ public function getList( SearchCriteriaInterface $searchCriteria, array $attributes = [], bool $isSearch = false, - bool $isChildSearch = false + bool $isChildSearch = false, + ContextInterface $context = null ): SearchResultsInterface { /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ $collection = $this->collectionFactory->create(); - $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes); + $this->collectionPreProcessor->process($collection, $searchCriteria, $attributes, $context); if (!$isChildSearch) { $visibilityIds = $isSearch diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php index fef224b12acfc..abed0ed2a897d 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/AttributeProcessor.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Adds passed in attributes to product collection results @@ -34,12 +35,20 @@ public function __construct($fieldToAttributeMap = []) } /** - * @inheritdoc + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { foreach ($attributeNames as $name) { $this->addAttribute($collection, $name); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php index 5fff991c0d6cd..3c19965c5f7b5 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/ExtensibleEntityProcessor.php @@ -11,6 +11,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add necessary joins for extensible entities. @@ -33,16 +34,20 @@ public function __construct(JoinProcessorInterface $joinProcessor) } /** + * Process collection to add additional joins, attributes, and clauses to a product collection. + * * @param Collection $collection * @param SearchCriteriaInterface $searchCriteria * @param array $attributeNames + * @param ContextInterface|null $context * @return Collection * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $this->joinProcessor->process($collection); 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 index be300e11f12ec..b636bcb001a3b 100644 --- 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 @@ -11,6 +11,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Catalog\Model\Product\Media\Config as MediaConfig; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add attributes required for every GraphQL product resolution process. @@ -35,12 +36,20 @@ public function __construct(MediaConfig $mediaConfig) } /** - * @inheritdoc + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { if (in_array('media_gallery_entries', $attributeNames)) { $mediaAttributes = $this->mediaConfig->getMediaAttributeCodes(); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php index 4c5b657874713..b545047d01541 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/RequiredColumnsProcessor.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add attributes required for every GraphQL product resolution process. @@ -19,12 +20,20 @@ class RequiredColumnsProcessor implements CollectionProcessorInterface { /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $collection->addAttributeToSelect('special_price'); $collection->addAttributeToSelect('special_price_from'); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php index e4c338f599577..45df0d3343c11 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/SearchCriteriaProcessor.php @@ -11,6 +11,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface as SearchCriteriaApplier; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Apply search criteria data to passed in collection. @@ -33,12 +34,20 @@ public function __construct(SearchCriteriaApplier $searchCriteriaApplier) } /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $this->searchCriteriaApplier->process($searchCriteria, $collection); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php index e68136f64e5cf..61085c10a7335 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/StockProcessor.php @@ -12,6 +12,7 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\CatalogInventory\Api\StockConfigurationInterface; use Magento\CatalogInventory\Model\ResourceModel\Stock\Status as StockStatusResource; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add stock filtering if configuration requires it. @@ -41,12 +42,20 @@ public function __construct(StockConfigurationInterface $stockConfig, StockStatu } /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { if (!$this->stockConfig->isShowOutOfStock()) { $this->stockStatusResource->addIsInStockFilterToCollection($collection); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php index 30174a94aaba0..964edc9d5a0ad 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessor/VisibilityStatusProcessor.php @@ -10,6 +10,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Join visibility and status tables to product collection @@ -19,12 +20,20 @@ class VisibilityStatusProcessor implements CollectionProcessorInterface { /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { $collection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner'); $collection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php index 62501a1a2382b..18e249ff23ac7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CollectionProcessorInterface.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Add additional joins, attributes, and clauses to a product collection. @@ -21,11 +22,13 @@ interface CollectionProcessorInterface * @param Collection $collection * @param SearchCriteriaInterface $searchCriteria * @param array $attributeNames + * @param ContextInterface|null $context * @return Collection */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php index 687899c1e60ac..415dbf565a0b7 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/Product/CompositeCollectionProcessor.php @@ -9,6 +9,7 @@ use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * {@inheritdoc} @@ -29,15 +30,22 @@ public function __construct(array $collectionProcessors = []) } /** - * {@inheritdoc} + * Process collection to add additional joins, attributes, and clauses to a product collection. + * + * @param Collection $collection + * @param SearchCriteriaInterface $searchCriteria + * @param array $attributeNames + * @param ContextInterface|null $context + * @return Collection */ public function process( Collection $collection, SearchCriteriaInterface $searchCriteria, - array $attributeNames + array $attributeNames, + ContextInterface $context = null ): Collection { foreach ($this->collectionProcessors as $collectionProcessor) { - $collection = $collectionProcessor->process($collection, $searchCriteria, $attributeNames); + $collection = $collectionProcessor->process($collection, $searchCriteria, $attributeNames, $context); } return $collection; diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php index 4c83afb89cc46..4807cad54bd50 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/DataProvider/ProductSearch.php @@ -18,6 +18,7 @@ use Magento\Framework\Api\Search\SearchResultInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SearchResultsInterface; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Product field data provider for product search, used for GraphQL resolver processing. @@ -84,12 +85,14 @@ public function __construct( * @param SearchCriteriaInterface $searchCriteria * @param SearchResultInterface $searchResult * @param array $attributes + * @param ContextInterface|null $context * @return SearchResultsInterface */ public function getList( SearchCriteriaInterface $searchCriteria, SearchResultInterface $searchResult, - array $attributes = [] + array $attributes = [], + ContextInterface $context = null ): SearchResultsInterface { /** @var Collection $collection */ $collection = $this->collectionFactory->create(); @@ -103,7 +106,7 @@ public function getList( $this->getSortOrderArray($searchCriteriaForCollection) )->apply(); - $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes); + $this->collectionPreProcessor->process($collection, $searchCriteriaForCollection, $attributes, $context); $collection->load(); $this->collectionPostProcessor->process($collection, $attributes); @@ -150,6 +153,12 @@ private function getSortOrderArray(SearchCriteriaInterface $searchCriteria) $sortOrders = $searchCriteria->getSortOrders(); if (is_array($sortOrders)) { foreach ($sortOrders as $sortOrder) { + // I am replacing _id with entity_id because in ElasticSearch _id is required for sorting by ID. + // Where as entity_id is required when using ID as the sort in $collection->load();. + // @see \Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search::getResult + if ($sortOrder->getField() === '_id') { + $sortOrder->setField('entity_id'); + } $ordersArray[$sortOrder->getField()] = $sortOrder->getDirection(); } } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php index 670eee9c4583e..d70a3aa7e63c3 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Filter.php @@ -7,7 +7,6 @@ namespace Magento\CatalogGraphQl\Model\Resolver\Products\Query; -use Magento\Catalog\Model\Layer\Resolver as LayerResolver; use Magento\Catalog\Model\Product; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Exception\InputException; @@ -16,6 +15,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product as ProductProvider; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Model\Query; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Store\Model\ScopeInterface; @@ -35,11 +35,6 @@ class Filter implements ProductQueryInterface */ private $productDataProvider; - /** - * @var LayerResolver - */ - private $layerResolver; - /** * FieldSelection */ @@ -58,7 +53,6 @@ class Filter implements ProductQueryInterface /** * @param SearchResultFactory $searchResultFactory * @param ProductProvider $productDataProvider - * @param LayerResolver $layerResolver * @param FieldSelection $fieldSelection * @param SearchCriteriaBuilder $searchCriteriaBuilder * @param ScopeConfigInterface $scopeConfig @@ -66,14 +60,12 @@ class Filter implements ProductQueryInterface public function __construct( SearchResultFactory $searchResultFactory, ProductProvider $productDataProvider, - LayerResolver $layerResolver, FieldSelection $fieldSelection, SearchCriteriaBuilder $searchCriteriaBuilder, ScopeConfigInterface $scopeConfig ) { $this->searchResultFactory = $searchResultFactory; $this->productDataProvider = $productDataProvider; - $this->layerResolver = $layerResolver; $this->fieldSelection = $fieldSelection; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->scopeConfig = $scopeConfig; @@ -84,16 +76,18 @@ public function __construct( * * @param array $args * @param ResolveInfo $info + * @param ContextInterface $context * @return SearchResult */ public function getResult( array $args, - ResolveInfo $info + ResolveInfo $info, + ContextInterface $context ): SearchResult { $fields = $this->fieldSelection->getProductsFieldSelection($info); try { $searchCriteria = $this->buildSearchCriteria($args, $info); - $searchResults = $this->productDataProvider->getList($searchCriteria, $fields); + $searchResults = $this->productDataProvider->getList($searchCriteria, $fields, false, false, $context); } catch (InputException $e) { return $this->createEmptyResult($args); } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php index 580af5d87be26..fca6f3d4f7770 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/ProductQueryInterface.php @@ -8,6 +8,7 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; /** * Search for products by criteria @@ -19,7 +20,8 @@ interface ProductQueryInterface * * @param array $args * @param ResolveInfo $info + * @param ContextInterface $context * @return SearchResult */ - public function getResult(array $args, ResolveInfo $info): SearchResult; + public function getResult(array $args, ResolveInfo $info, ContextInterface $context): SearchResult; } diff --git a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php index fbb0e42f2afeb..4eb76fb5c2d5b 100644 --- a/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php +++ b/app/code/Magento/CatalogGraphQl/Model/Resolver/Products/Query/Search.php @@ -12,7 +12,9 @@ use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResult; use Magento\CatalogGraphQl\Model\Resolver\Products\SearchResultFactory; use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\Exception\InputException; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Search\Api\SearchInterface; use Magento\Search\Model\Search\PageSizeProvider; @@ -80,12 +82,14 @@ public function __construct( * * @param array $args * @param ResolveInfo $info + * @param ContextInterface $context * @return SearchResult - * @throws \Exception + * @throws InputException */ public function getResult( array $args, - ResolveInfo $info + ResolveInfo $info, + ContextInterface $context ): SearchResult { $queryFields = $this->fieldSelection->getProductsFieldSelection($info); $searchCriteria = $this->buildSearchCriteria($args, $info); @@ -101,7 +105,12 @@ public function getResult( //Address limitations of sort and pagination on search API apply original pagination from GQL query $searchCriteria->setPageSize($realPageSize); $searchCriteria->setCurrentPage($realCurrentPage); - $searchResults = $this->productsProvider->getList($searchCriteria, $itemsResults, $queryFields); + $searchResults = $this->productsProvider->getList( + $searchCriteria, + $itemsResults, + $queryFields, + $context + ); $totalPages = $realPageSize ? ((int)ceil($searchResults->getTotalCount() / $realPageSize)) : 0; diff --git a/app/code/Magento/CatalogGraphQl/composer.json b/app/code/Magento/CatalogGraphQl/composer.json index d6e9bfa3c0505..46d7454a6d7e2 100644 --- a/app/code/Magento/CatalogGraphQl/composer.json +++ b/app/code/Magento/CatalogGraphQl/composer.json @@ -11,10 +11,10 @@ "magento/module-store": "*", "magento/module-eav-graph-ql": "*", "magento/module-catalog-search": "*", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-graph-ql": "*" }, "suggest": { - "magento/module-graph-ql": "*", "magento/module-graph-ql-cache": "*", "magento/module-store-graph-ql": "*" }, diff --git a/app/code/Magento/CatalogGraphQl/etc/di.xml b/app/code/Magento/CatalogGraphQl/etc/di.xml index 5fec7bfd4fda7..03f9d7ad03f04 100644 --- a/app/code/Magento/CatalogGraphQl/etc/di.xml +++ b/app/code/Magento/CatalogGraphQl/etc/di.xml @@ -74,4 +74,14 @@ <preference type="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\Provider" for="\Magento\CatalogGraphQl\Model\Resolver\Product\Price\ProviderInterface"/> <preference type="Magento\CatalogGraphQl\Model\Resolver\Products\Query\Search" for="Magento\CatalogGraphQl\Model\Resolver\Products\Query\ProductQueryInterface"/> + + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="related" xsi:type="string">related</item> + <item name="upsell" xsi:type="string">upsell</item> + <item name="crosssell" xsi:type="string">crosssell</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls index a9720bf17445b..35f2c767b3e1e 100644 --- a/app/code/Magento/CatalogGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CatalogGraphQl/etc/schema.graphqls @@ -88,7 +88,7 @@ interface ProductInterface @typeResolver(class: "Magento\\CatalogGraphQl\\Model\ sku: String @doc(description: "A number or code assigned to a product to identify the product, options, price, and manufacturer.") description: ComplexTextValue @doc(description: "Detailed information about the product. The value can include simple HTML tags.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") short_description: ComplexTextValue @doc(description: "A short description of the product. Its use depends on the theme.") @resolver(class: "\\Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\ProductComplexTextAttribute") - special_price: Float @doc(description: "The discounted price of the product.") + special_price: Float @doc(description: "The discounted price of the product.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\SpecialPrice") special_from_date: String @doc(description: "The beginning date that a product has a special price.") special_to_date: String @doc(description: "The end date that a product has a special price.") attribute_set_id: Int @doc(description: "The attribute set assigned to the product.") @@ -132,6 +132,7 @@ type CustomizableAreaValue @doc(description: "CustomizableAreaValue defines the price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CategoryTree implements CategoryInterface @doc(description: "Category Tree implementation.") { @@ -153,6 +154,7 @@ type CustomizableDateValue @doc(description: "CustomizableDateValue defines the price: Float @doc(description: "The price assigned to this option.") price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CustomizableDropDownOption implements CustomizableOptionInterface @doc(description: "CustomizableDropDownOption contains information about a drop down menu that is defined as part of a customizable option.") { @@ -166,6 +168,7 @@ type CustomizableDropDownValue @doc(description: "CustomizableDropDownValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type CustomizableMultipleOption implements CustomizableOptionInterface @doc(description: "CustomizableMultipleOption contains information about a multiselect that is defined as part of a customizable option.") { @@ -179,6 +182,7 @@ type CustomizableMultipleValue @doc(description: "CustomizableMultipleValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the option is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") } type CustomizableFieldOption implements CustomizableOptionInterface @doc(description: "CustomizableFieldOption contains information about a text field that is defined as part of a customizable option.") { @@ -191,6 +195,7 @@ type CustomizableFieldValue @doc(description: "CustomizableFieldValue defines th price_type: PriceTypeEnum @doc(description: "FIXED, PERCENT, or DYNAMIC.") sku: String @doc(description: "The Stock Keeping Unit for this option.") max_characters: Int @doc(description: "The maximum number of characters that can be entered for this customizable option.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } type CustomizableFileOption implements CustomizableOptionInterface @doc(description: "CustomizableFileOption contains information about a file picker that is defined as part of a customizable option.") { @@ -205,6 +210,7 @@ type CustomizableFileValue @doc(description: "CustomizableFileValue defines the file_extension: String @doc(description: "The file extension to accept.") image_size_x: Int @doc(description: "The maximum width of an image.") image_size_y: Int @doc(description: "The maximum height of an image.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableEnteredOptionValueUid") # A Base64 string that encodes option details. } interface MediaGalleryInterface @doc(description: "Contains basic information about a product image or video.") @typeResolver(class: "Magento\\CatalogGraphQl\\Model\\MediaGalleryTypeResolver") { @@ -274,6 +280,7 @@ type CustomizableRadioValue @doc(description: "CustomizableRadioValue defines th sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the radio button is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type CustomizableCheckboxOption implements CustomizableOptionInterface @doc(description: "CustomizableCheckbbixOption contains information about a set of checkbox values that are defined as part of a customizable option.") { @@ -287,6 +294,7 @@ type CustomizableCheckboxValue @doc(description: "CustomizableCheckboxValue defi sku: String @doc(description: "The Stock Keeping Unit for this option.") title: String @doc(description: "The display name for this option.") sort_order: Int @doc(description: "The order in which the checkbox value is displayed.") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product\\CustomizableSelectedOptionValueUid") # A Base64 string that encodes option details. } type VirtualProduct implements ProductInterface, CustomizableProductInterface @doc(description: "A virtual product is non-tangible product that does not require shipping and is not kept in inventory.") { diff --git a/app/code/Magento/CatalogImportExport/Model/Export/Product.php b/app/code/Magento/CatalogImportExport/Model/Export/Product.php index 530bf6b1a0057..bcd103c6d62ba 100644 --- a/app/code/Magento/CatalogImportExport/Model/Export/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Export/Product.php @@ -979,6 +979,7 @@ protected function getExportData() * * @return array Keys are product IDs, values arrays with keys as store IDs * and values as store-specific versions of Product entity. + * @since 100.2.1 */ protected function loadCollection(): array { @@ -1168,7 +1169,7 @@ protected function collectMultirawData() * @param \Magento\Catalog\Model\Product $item * @param int $storeId * @return bool - * @deprecated + * @deprecated 100.2.3 */ protected function hasMultiselectData($item, $storeId) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product.php b/app/code/Magento/CatalogImportExport/Model/Import/Product.php index c5fcac99767bd..74c6576e6bcdf 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product.php @@ -225,7 +225,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** * Links attribute name-to-link type ID. * - * @deprecated use DI for LinkProcessor class if you want to add additional types + * @deprecated 101.1.0 use DI for LinkProcessor class if you want to add additional types * * @var array */ @@ -554,7 +554,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** * @var \Magento\CatalogInventory\Model\ResourceModel\Stock\ItemFactory - * @deprecated this variable isn't used anymore. + * @deprecated 101.0.0 this variable isn't used anymore. */ protected $_stockResItemFac; @@ -618,7 +618,7 @@ class Product extends \Magento\ImportExport\Model\Import\Entity\AbstractEntity /** * @var array - * @deprecated 100.1.5 + * @deprecated 100.0.3 * @since 100.0.3 */ protected $productUrlKeys = []; @@ -969,6 +969,7 @@ public function getMultipleValueSeparator() * Return empty attribute value constant * * @return string + * @since 101.0.0 */ public function getEmptyAttributeValueConstant() { @@ -1289,7 +1290,7 @@ protected function _prepareRowForDb(array $rowData) * * Must be called after ALL products saving done. * - * @deprecated use linkProcessor Directly + * @deprecated 101.1.0 use linkProcessor Directly * * @return $this */ @@ -1473,7 +1474,7 @@ private function getNewSkuFieldsForSelect() * * @return void * @since 100.0.4 - * @deprecated + * @deprecated 100.2.3 */ protected function initMediaGalleryResources() { @@ -1595,6 +1596,7 @@ protected function _saveProducts() } $rowSku = $rowData[self::COL_SKU]; + $rowSkuNormalized = mb_strtolower($rowSku); if (null === $rowSku) { $this->getErrorAggregator()->addRowToSkip($rowNum); @@ -1604,9 +1606,9 @@ protected function _saveProducts() $storeId = !empty($rowData[self::COL_STORE]) ? $this->getStoreIdByCode($rowData[self::COL_STORE]) : Store::DEFAULT_STORE_ID; - $rowExistingImages = $existingImages[$storeId][$rowSku] ?? []; + $rowExistingImages = $existingImages[$storeId][$rowSkuNormalized] ?? []; $rowStoreMediaGalleryValues = $rowExistingImages; - $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSku] ?? []; + $rowExistingImages += $existingImages[Store::DEFAULT_STORE_ID][$rowSkuNormalized] ?? []; if (self::SCOPE_STORE == $rowScope) { // set necessary data from SCOPE_DEFAULT row @@ -1762,10 +1764,11 @@ protected function _saveProducts() continue; } - if (isset($rowExistingImages[$uploadedFile])) { - $currentFileData = $rowExistingImages[$uploadedFile]; + $uploadedFileNormalized = ltrim($uploadedFile, '/\\'); + if (isset($rowExistingImages[$uploadedFileNormalized])) { + $currentFileData = $rowExistingImages[$uploadedFileNormalized]; $currentFileData['store_id'] = $storeId; - $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFile]); + $storeMediaGalleryValueExists = isset($rowStoreMediaGalleryValues[$uploadedFileNormalized]); if (array_key_exists($uploadedFile, $imageHiddenStates) && $currentFileData['disabled'] != $imageHiddenStates[$uploadedFile] ) { @@ -3077,6 +3080,9 @@ private function formatStockDataForRow(array $rowData): array ); if ($this->stockConfiguration->isQty($this->skuProcessor->getNewSku($sku)['type_id'])) { + if (isset($rowData['qty']) && $rowData['qty'] == 0) { + $row['is_in_stock'] = 0; + } $stockItemDo->setData($row); $row['is_in_stock'] = $row['is_in_stock'] ?? $this->stockStateProvider->verifyStock($stockItemDo); if ($this->stockStateProvider->verifyNotification($stockItemDo)) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php index a45338c391a58..22a83671f630a 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/LinkProcessor.php @@ -89,7 +89,6 @@ public function saveLinks( $resource = $this->linkFactory->create(); $mainTable = $resource->getMainTable(); $positionAttrId = []; - $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable); // pre-load 'position' attributes ID for each link type once foreach ($this->linkNameToId as $linkId) { @@ -103,6 +102,7 @@ public function saveLinks( $positionAttrId[$linkId] = $importEntity->getConnection()->fetchOne($select, $bind); } while ($bunch = $dataSourceModel->getNextBunch()) { + $nextLinkId = $this->resourceHelper->getNextAutoincrement($mainTable); $this->processLinkBunches($importEntity, $linkField, $bunch, $resource, $nextLinkId, $positionAttrId); } } @@ -110,7 +110,7 @@ public function saveLinks( /** * Add link types (exists for backwards compatibility) * - * @deprecated Use DI to inject to the constructor + * @deprecated 101.1.0 Use DI to inject to the constructor * @param array $nameToIds */ public function addNameToIds(array $nameToIds): void diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php index a94a87a44b32a..d4694b72ba64f 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/MediaGalleryProcessor.php @@ -384,7 +384,9 @@ public function getExistingImages(array $bunch) foreach ($this->connection->fetchAll($select) as $image) { $storeId = $image['store_id']; unset($image['store_id']); - $result[$storeId][$image['sku']][$image['value']] = $image; + $sku = mb_strtolower($image['sku']); + $value = ltrim($image['value'], '/\\'); + $result[$storeId][$sku][$value] = $image; } return $result; diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php index 1c6d679848216..27869cf1d771b 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/StatusProcessor.php @@ -101,7 +101,8 @@ public function loadOldStatus(array $linkIdBySku): void $select = $connection->select() ->from($this->getAttribute()->getBackend()->getTable()) ->columns([$linkId, 'store_id', 'value']) - ->where(sprintf('%s IN (?)', $linkId), array_values($linkIdBySku)); + ->where(sprintf('%s IN (?)', $linkId), array_values($linkIdBySku)) + ->where('attribute_id = ?', $this->getAttribute()->getId()); $skuByLinkId = array_flip($linkIdBySku); foreach ($connection->fetchAll($select) as $item) { diff --git a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php index d87c3d8477556..6571b16c87565 100644 --- a/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php +++ b/app/code/Magento/CatalogImportExport/Model/Import/Product/Type/AbstractType.php @@ -33,6 +33,7 @@ abstract class AbstractType * Maintain a list of invisible attributes * * @var array + * @since 100.2.5 */ public static $invAttributesCache = []; diff --git a/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php b/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php index bc314d825ba3e..6ee0e536c0ae8 100644 --- a/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php +++ b/app/code/Magento/CatalogImportExport/Model/StockItemImporterInterface.php @@ -11,6 +11,7 @@ * Interface StockItemImporterInterface * * @api + * @since 101.0.0 */ interface StockItemImporterInterface { @@ -22,6 +23,7 @@ interface StockItemImporterInterface * @throws \Magento\Framework\Exception\CouldNotSaveException * @throws \Magento\Framework\Exception\InputException * @throws \Magento\Framework\Validation\ValidationException + * @since 101.0.0 */ public function import(array $stockData); } diff --git a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml index d2517fa28cdd1..f0e6e12204a9e 100644 --- a/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/CatalogImportExport/Test/Mftf/Test/AdminExportImportConfigurableProductWithImagesTest.xml @@ -148,7 +148,7 @@ <deleteData createDataKey="createConfigChildProduct" stepKey="deleteConfigChildProduct"/> <deleteData createDataKey="createConfigProductAttr" stepKey="deleteConfigProductAttr"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Admin logout--> diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php index 9ae22e5e1a364..07b8429ddf188 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockCollectionInterface.php @@ -16,7 +16,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php index 087fae6e6568a..581081f2924ea 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php index 59a1f58b74c2c..2d5f980a57039 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemCollectionInterface.php @@ -16,7 +16,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php index e2e8a744c4bcd..759cc9883be9f 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockItemInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php index 1cc045745a0c1..a7d70a943d405 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusCollectionInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php index 66b639fb088d1..35e56b0e3e7bb 100644 --- a/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php +++ b/app/code/Magento/CatalogInventory/Api/Data/StockStatusInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php index 6fd1e7466970d..ddb3fce22a853 100644 --- a/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RegisterProductSaleInterface.php @@ -13,9 +13,10 @@ /** * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @since 100.3.0 */ interface RegisterProductSaleInterface { @@ -29,6 +30,7 @@ interface RegisterProductSaleInterface * @param int $websiteId * @return StockItemInterface[] * @throws LocalizedException + * @since 100.3.0 */ public function registerProductsSale($items, $websiteId = null); } diff --git a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php index 552e30da89235..83f7d73deaed9 100644 --- a/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php +++ b/app/code/Magento/CatalogInventory/Api/RevertProductSaleInterface.php @@ -10,9 +10,10 @@ /** * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @since 100.3.0 */ interface RevertProductSaleInterface { @@ -24,6 +25,7 @@ interface RevertProductSaleInterface * @param string[] $items * @param int $websiteId * @return bool + * @since 100.3.0 */ public function revertProductsSale($items, $websiteId = null); } diff --git a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php index 5019e86b7af40..ab52580988c5e 100644 --- a/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockConfigurationInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php index eb6fb2e812f2e..92f2290ec08ad 100644 --- a/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockCriteriaInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php index 18bab6571c209..24dbaf5bb6d5f 100644 --- a/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockIndexInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php index 1d2cabbb48a11..b72289ee09278 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemCriteriaInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php index eecf6cbe07632..4269569f9da1a 100644 --- a/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockItemRepositoryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php index 8796953e32fd0..3c1c7ea137c89 100644 --- a/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockManagementInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php index 5478f90fb7d9f..bab5f9b457c45 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRegistryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php index 3cfdf45506340..a7d64ec9eedb3 100644 --- a/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockRepositoryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php index 8be7f5be79f27..d404e885d78df 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStateInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStateInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php index 99ad7005d9da4..be1c9642826a7 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusCriteriaInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php index d29171f557f05..91efd55761335 100644 --- a/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php +++ b/app/code/Magento/CatalogInventory/Api/StockStatusRepositoryInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php index ffcb758dcbd66..bc63114d99801 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Minsaleqty.php @@ -13,7 +13,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php index 5378801b6c24b..3c1a6e7982708 100644 --- a/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php +++ b/app/code/Magento/CatalogInventory/Block/Adminhtml/Form/Field/Stock.php @@ -15,7 +15,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php index a12b72cd0a971..dd8c987fe5da4 100644 --- a/app/code/Magento/CatalogInventory/Block/Qtyincrements.php +++ b/app/code/Magento/CatalogInventory/Block/Qtyincrements.php @@ -15,7 +15,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php index 5a3a3ca6ee983..c19dc5fb34bf6 100644 --- a/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php +++ b/app/code/Magento/CatalogInventory/Block/Stockqty/DefaultStockqty.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Helper/Stock.php b/app/code/Magento/CatalogInventory/Helper/Stock.php index 798ac4074c188..87a0e3c32ad09 100644 --- a/app/code/Magento/CatalogInventory/Helper/Stock.php +++ b/app/code/Magento/CatalogInventory/Helper/Stock.php @@ -19,9 +19,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @since 100.0.2 */ class Stock { diff --git a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php index 145b0d1454ae2..04e54acad5c0e 100644 --- a/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php +++ b/app/code/Magento/CatalogInventory/Model/Adminhtml/Stock/Item.php @@ -21,7 +21,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php index e67568b80898e..2ccb726f2c625 100644 --- a/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php +++ b/app/code/Magento/CatalogInventory/Model/Quote/Item/QuantityValidator.php @@ -26,7 +26,7 @@ * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php index c5644060c689f..c151e5897abd5 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/DefaultStock.php @@ -19,7 +19,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php index 115002b237645..665ebf2db2f30 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/QueryProcessorInterface.php @@ -12,7 +12,7 @@ * @api * @since 100.1.0 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php index 24ed496372817..9a1945d5aefac 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/Stock/StockInterface.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php index 0ee162e429f40..49e4889c8edee 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Indexer/StockFactory.php @@ -13,7 +13,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php index 31b2ada809823..1f6f3a16ac617 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock.php @@ -223,7 +223,7 @@ protected function _initConfig() /** * Set items out of stock basing on their quantities and config settings * - * @deprecated + * @deprecated 100.2.5 * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateSetOutOfStock * @param string|int $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -260,7 +260,7 @@ public function updateSetOutOfStock($website = null) /** * Set items in stock basing on their quantities and config settings * - * @deprecated + * @deprecated 100.2.5 * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateSetInStock * @param int|string $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -295,7 +295,7 @@ public function updateSetInStock($website) /** * Update items low stock date basing on their quantities and config settings * - * @deprecated + * @deprecated 100.2.5 * @see \Magento\CatalogInventory\Model\ResourceModel\Stock\Item::updateLowStockDate * @param int|string $website * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php index 402ce5f2f611e..25bc0a0ce899e 100644 --- a/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php +++ b/app/code/Magento/CatalogInventory/Model/ResourceModel/Stock/Status.php @@ -14,9 +14,10 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html + * @since 100.0.2 */ class Status extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { @@ -227,6 +228,7 @@ public function addStockStatusToSelect(\Magento\Framework\DB\Select $select, \Ma * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $collection * @param bool $isFilterInStock * @return \Magento\Catalog\Model\ResourceModel\Product\Collection $collection + * @since 100.0.6 */ public function addStockDataToCollection($collection, $isFilterInStock) { diff --git a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php index 0bffb9a9888cd..d28da4e5b3497 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Backorders.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Backorders.php @@ -10,7 +10,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/Source/Stock.php b/app/code/Magento/CatalogInventory/Model/Source/Stock.php index 9661fc83ce275..69e80658ecd74 100644 --- a/app/code/Magento/CatalogInventory/Model/Source/Stock.php +++ b/app/code/Magento/CatalogInventory/Model/Source/Stock.php @@ -12,7 +12,7 @@ * @api * @since 100.0.2 * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.0 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ @@ -38,6 +38,7 @@ public function getAllOptions() * @param string $dir * * @return $this + * @since 100.2.4 */ public function addValueSortToCollection($collection, $dir = \Magento\Framework\Data\Collection::SORT_ORDER_DESC) { diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php index 0fa4b919c40fa..b2dfe532ffbe0 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockRegistryProviderInterface.php @@ -8,7 +8,7 @@ /** * Interface StockRegistryProviderInterface * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.2 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php index 30f703b5b928f..5bb78e1489b39 100644 --- a/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php +++ b/app/code/Magento/CatalogInventory/Model/Spi/StockStateProviderInterface.php @@ -10,7 +10,7 @@ /** * Interface StockStateProviderInterface * - * @deprecated 2.3.0 Replaced with Multi Source Inventory + * @deprecated 100.3.2 Replaced with Multi Source Inventory * @link https://devdocs.magento.com/guides/v2.3/inventory/index.html * @link https://devdocs.magento.com/guides/v2.3/inventory/catalog-inventory-replacements.html */ diff --git a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml index 2cdb2413122bd..f1c01919d52f1 100644 --- a/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml +++ b/app/code/Magento/CatalogInventory/Test/Mftf/Test/AssociatedProductToConfigurableOutOfStockTest.xml @@ -106,8 +106,7 @@ <waitForElementVisible selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="waitForProductAdded"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToShoppingCartPage"/> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> - <waitForElement selector="{{CheckoutShippingMethodsSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrder"/> @@ -128,8 +127,7 @@ <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoice"/> <waitForPageLoad stepKey="waitForNewInvoicePageToLoad"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForNewInvoiceToBeCreated"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShip"/> <waitForLoadingMaskToDisappear stepKey="waitForShipLoadingMask"/> diff --git a/app/code/Magento/CatalogInventory/etc/di.xml b/app/code/Magento/CatalogInventory/etc/di.xml index b050c6ae3b6ca..751fa465bdb17 100644 --- a/app/code/Magento/CatalogInventory/etc/di.xml +++ b/app/code/Magento/CatalogInventory/etc/di.xml @@ -37,7 +37,7 @@ </type> <type name="Magento\Catalog\Model\FilterProductCustomAttribute"> <arguments> - <argument name="blackList" xsi:type="array"> + <argument name="excludedList" xsi:type="array"> <item name="quantity_and_stock_status" xsi:type="string">quantity_and_stock_status</item> </argument> </arguments> diff --git a/app/code/Magento/CatalogInventory/etc/webapi.xml b/app/code/Magento/CatalogInventory/etc/webapi.xml index c172b9c971500..af9c70bf59c36 100644 --- a/app/code/Magento/CatalogInventory/etc/webapi.xml +++ b/app/code/Magento/CatalogInventory/etc/webapi.xml @@ -10,25 +10,25 @@ <route url="/V1/stockItems/:productSku" method="GET"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="getStockItemBySku"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> <route url="/V1/products/:productSku/stockItems/:itemId" method="PUT"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="updateStockItemBySku"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> <route url="/V1/stockItems/lowStock/" method="GET"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="getLowStockItems"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> <route url="/V1/stockStatuses/:productSku" method="GET"> <service class="Magento\CatalogInventory\Api\StockRegistryInterface" method="getStockStatusBySku"/> <resources> - <resource ref="Magento_CatalogInventory::cataloginventory"/> + <resource ref="Magento_Catalog::catalog_inventory"/> </resources> </route> </routes> diff --git a/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php b/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php index 7f584fb1154e0..116a4529a8e60 100644 --- a/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php +++ b/app/code/Magento/CatalogRule/Cron/DailyCatalogUpdate.php @@ -6,19 +6,34 @@ namespace Magento\CatalogRule\Cron; +use Magento\CatalogRule\Model\Indexer\PartialIndex; +use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; + +/** + * Daily update catalog price rule by cron + */ class DailyCatalogUpdate { /** - * @var \Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor + * @var RuleProductProcessor */ protected $ruleProductProcessor; /** - * @param \Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor $ruleProductProcessor + * @var PartialIndex */ - public function __construct(\Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor $ruleProductProcessor) - { + private $partialIndex; + + /** + * @param RuleProductProcessor $ruleProductProcessor + * @param PartialIndex $partialIndex + */ + public function __construct( + RuleProductProcessor $ruleProductProcessor, + PartialIndex $partialIndex + ) { $this->ruleProductProcessor = $ruleProductProcessor; + $this->partialIndex = $partialIndex; } /** @@ -31,6 +46,8 @@ public function __construct(\Magento\CatalogRule\Model\Indexer\Rule\RuleProductP */ public function execute() { - $this->ruleProductProcessor->markIndexerAsInvalid(); + $this->ruleProductProcessor->isIndexerScheduled() + ? $this->partialIndex->partialUpdateCatalogRuleProductPrice() + : $this->ruleProductProcessor->markIndexerAsInvalid(); } } diff --git a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php index 1fc53c78985fb..df167d171e001 100644 --- a/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php +++ b/app/code/Magento/CatalogRule/Model/Indexer/IndexBuilder.php @@ -30,7 +30,7 @@ class IndexBuilder /** * @var \Magento\Framework\EntityManager\MetadataPool - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @since 100.1.0 */ protected $metadataPool; @@ -41,7 +41,7 @@ class IndexBuilder * This array contain list of CatalogRuleGroupWebsite table columns * * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_catalogRuleGroupWebsiteColumnsList = ['rule_id', 'customer_group_id', 'website_id']; @@ -446,7 +446,7 @@ private function assignProductToRule(Rule $rule, int $productEntityId, array $we * @param Product $product * @return $this * @throws \Exception - * @deprecated + * @deprecated 101.1.5 * @see ReindexRuleProduct::execute * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -500,7 +500,7 @@ protected function getTable($tableName) * * @param Rule $rule * @return $this - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ReindexRuleProduct::execute */ protected function updateRuleProductData(Rule $rule) @@ -528,7 +528,7 @@ protected function updateRuleProductData(Rule $rule) * @param Product|null $product * @throws \Exception * @return $this - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ReindexRuleProductPrice::execute * @see ReindexRuleGroupWebsite::execute */ @@ -543,7 +543,7 @@ protected function applyAllRules(Product $product = null) * Update CatalogRuleGroupWebsite data * * @return $this - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ReindexRuleGroupWebsite::execute */ protected function updateCatalogRuleGroupWebsiteData() @@ -569,7 +569,7 @@ protected function deleteOldData() * @param array $ruleData * @param array $productData * @return float - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see ProductPriceCalculator::calculate */ protected function calcRuleProductPrice($ruleData, $productData = null) @@ -584,7 +584,7 @@ protected function calcRuleProductPrice($ruleData, $productData = null) * @param Product|null $product * @return \Zend_Db_Statement_Interface * @throws \Magento\Framework\Exception\LocalizedException - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see RuleProductsSelectBuilder::build */ protected function getRuleProductsStmt($websiteId, Product $product = null) @@ -598,7 +598,7 @@ protected function getRuleProductsStmt($websiteId, Product $product = null) * @param array $arrData * @return $this * @throws \Exception - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see RuleProductPricesPersistor::execute */ protected function saveRuleProductPrices($arrData) diff --git a/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php b/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php new file mode 100644 index 0000000000000..12a77f81826d6 --- /dev/null +++ b/app/code/Magento/CatalogRule/Model/Indexer/PartialIndex.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CatalogRule\Model\Indexer; + +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\App\ResourceConnection; + +/** + * Catalog rule partial index + * + * This class triggers the dependent index "catalog_product_price", + * and the cache is cleared only for the matched products for partial indexing. + */ +class PartialIndex +{ + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var AdapterInterface + */ + private $connection; + + /** + * @var IndexBuilder + */ + private $indexBuilder; + + /** + * @param ResourceConnection $resource + * @param IndexBuilder $indexBuilder + */ + public function __construct( + ResourceConnection $resource, + IndexBuilder $indexBuilder + ) { + $this->resource = $resource; + $this->connection = $resource->getConnection(); + $this->indexBuilder = $indexBuilder; + } + + /** + * Synchronization replica table with original table "catalogrule_product_price" + * + * Used replica table for correctly working MySQL trigger + * + * @return void + */ + public function partialUpdateCatalogRuleProductPrice(): void + { + $this->indexBuilder->reindexFull(); + $indexTableName = $this->resource->getTableName('catalogrule_product_price'); + $select = $this->connection->select()->from( + ['crp' => $indexTableName], + 'product_id' + ); + $selectFields = $this->connection->select()->from( + ['crp' => $indexTableName], + [ + 'rule_date', + 'customer_group_id', + 'product_id', + 'rule_price', + 'website_id', + 'latest_start_date', + 'earliest_end_date', + ] + ); + $where = ['product_id' .' NOT IN (?)' => $select]; + //remove products that are no longer used in indexing + $this->connection->delete($this->resource->getTableName('catalogrule_product_price_replica'), $where); + //add updated products to indexing + $this->connection->query( + $this->connection->insertFromSelect( + $selectFields, + $this->resource->getTableName('catalogrule_product_price_replica'), + [ + 'rule_date', + 'customer_group_id', + 'product_id', + 'rule_price', + 'website_id', + 'latest_start_date', + 'earliest_end_date', + ], + AdapterInterface::INSERT_ON_DUPLICATE + ) + ); + } +} diff --git a/app/code/Magento/CatalogRule/Model/Rule.php b/app/code/Magento/CatalogRule/Model/Rule.php index cd24201963f25..f2e8e54d34665 100644 --- a/app/code/Magento/CatalogRule/Model/Rule.php +++ b/app/code/Magento/CatalogRule/Model/Rule.php @@ -3,8 +3,6 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - namespace Magento\CatalogRule\Model; use Magento\Catalog\Model\Product; @@ -15,7 +13,6 @@ use Magento\CatalogRule\Helper\Data; use Magento\CatalogRule\Model\Data\Condition\Converter; use Magento\CatalogRule\Model\Indexer\Rule\RuleProductProcessor; -use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; use Magento\CatalogRule\Model\ResourceModel\Rule as RuleResourceModel; use Magento\CatalogRule\Model\Rule\Action\CollectionFactory as RuleCollectionFactory; use Magento\CatalogRule\Model\Rule\Condition\CombineFactory; @@ -36,6 +33,7 @@ use Magento\Framework\Stdlib\DateTime; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Store\Model\StoreManagerInterface; +use Magento\CatalogRule\Model\ResourceModel\Product\ConditionsToCollectionApplier; /** * Catalog Rule data model @@ -501,8 +499,7 @@ public function calcProductPriceRule(Product $product, $price) } else { $customerGroupId = $this->_customerSession->getCustomerGroupId(); } - $currentDateTime = new \DateTime(); - $dateTs = $currentDateTime->getTimestamp(); + $dateTs = $this->_localeDate->scopeTimeStamp($storeId); $cacheKey = date('Y-m-d', $dateTs) . "|{$websiteId}|{$customerGroupId}|{$productId}|{$price}"; if (!array_key_exists($cacheKey, self::$_priceRulesData)) { @@ -898,12 +895,4 @@ public function getIdentities() { return ['price']; } - - /** - * Clear price rules cache. - */ - public function clearPriceRulesData(): void - { - self::$_priceRulesData = []; - } } diff --git a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml index 1170b08b1add9..27edab962033e 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/ActionGroup/AdminCatalogPriceRuleDeleteAllActionGroup.xml @@ -16,7 +16,7 @@ <!-- It sometimes is loading too long for default 10s --> <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> - <helper class="\Magento\CatalogRule\Test\Mftf\Helper\CatalogPriceRuleHelper" method="deleteAllCatalogPriceRules" stepKey="deleteAllCatalogPriceRulesOneByOne"> + <helper class="\Magento\Rule\Test\Mftf\Helper\RuleHelper" method="deleteAllRulesOneByOne" stepKey="deleteAllRulesOneByOne"> <argument name="firstNotEmptyRow">{{AdminDataGridTableSection.firstNotEmptyRow}}</argument> <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml index d1f9ebd4c99a4..e6b825ff3cf70 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleByCategoryTest.xml @@ -36,18 +36,17 @@ <deleteData createDataKey="createSimpleProductTwo" stepKey="deleteSimpleProductTwo"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- 1. Begin creating a new catalog price rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <click selector="{{AdminGridMainControls.add}}" stepKey="addNewRule"/> <waitForPageLoad stepKey="waitForIndividualRulePage"/> <fillField selector="{{AdminNewCatalogPriceRule.ruleName}}" userInput="{{_defaultCatalogRule.name}}" stepKey="fillName"/> @@ -78,8 +77,12 @@ <!-- 3. Save and apply the new catalog price rule --> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- 4. Verify the storefront --> <amOnPage url="$$createCategoryOne.name$$.html" stepKey="goToCategoryOne"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml index 882a92a2ee433..1de036d1026dd 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminApplyCatalogRuleForConfigurableProductWithSpecialPricesTest.xml @@ -127,8 +127,12 @@ <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> <!-- Reindex and flash cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Open Storefront product page and assert created configurable product --> <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml index fcae0065f1b53..d45c3af1c2da0 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleByPercentTest.xml @@ -25,8 +25,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- log in and create the price rule --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -42,7 +46,7 @@ <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> @@ -61,8 +65,7 @@ <see stepKey="seeNewPrice2" selector="{{StorefrontProductInfoMainSection.updatedPrice}}" userInput="$110.70"/> <!-- Add the product to cart and check that the price is correct there --> - <click stepKey="addToCart" selector="{{StorefrontProductActionSection.addToCart}}"/> - <waitForPageLoad stepKey="waitForAddedToCart"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCheckout"/> <see stepKey="seeNewPriceInCart" selector="{{CheckoutCartSummarySection.subtotal}}" userInput="$110.70"/> </test> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml index c3132e5c46cc9..fb218297b646d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleForCustomerGroupTest.xml @@ -41,12 +41,21 @@ </after> <!-- Create a catalog rule for the NOT LOGGED IN customer group --> - <actionGroup ref="AdminOpenNewCatalogPriceRuleFormPageActionGroup" stepKey="createNewPriceRule"/> - <actionGroup ref="AdminCatalogPriceRuleFillMainInfoActionGroup" stepKey="fillMainInfoForPriceRule"> - <argument name="groups" value="'NOT LOGGED IN'"/> + <actionGroup ref="NewCatalogPriceRuleByUIActionGroup" stepKey="createNewPriceRule"/> + <actionGroup ref="SelectNotLoggedInCustomerGroupActionGroup" stepKey="selectNotLoggedInCustomerGroup"/> + <conditionalClick selector="{{AdminNewCatalogPriceRule.active}}" dependentSelector="{{AdminNewCatalogPriceRule.activeIsEnabled}}" visible="false" stepKey="enableActiveBtn"/> + <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/> + + <!--<click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="saveAndApply"/>--> + <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the rule." stepKey="assertSuccess"/> + + <!-- Perform reindex and flush cache --> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> </actionGroup> - <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsPriceRule"/> - <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyFPriceRule"/> <!-- As a NOT LOGGED IN user, go to the storefront category page and should see the discount --> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToCategory1"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml index 90a0835508b06..77228dde8797f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminCreateCatalogPriceRuleTest/AdminCreateCatalogPriceRuleWithInvalidDataTest.xml @@ -20,7 +20,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <actionGroup ref="NewCatalogPriceRuleWithInvalidDataActionGroup" stepKey="createNewPriceRule"> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml index 6b34fd1e67e9b..7247d61bea87c 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromConfigurableProductTest.xml @@ -72,8 +72,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> @@ -105,8 +109,7 @@ </after> <!-- Delete the simple product and catalog price rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage1"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule1"> <argument name="name" value="{{DeleteActiveCatalogPriceRuleWithConditions.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -115,8 +118,12 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> <!-- Reindex --> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert that the rule isn't present on the Category page --> <amOnPage url="$$createCategory1.name$$.html" stepKey="goToStorefrontCategoryPage1"/> @@ -131,9 +138,7 @@ <!-- Assert that the rule isn't present in the Shopping Cart --> <selectOption selector="{{StorefrontProductInfoMainSection.productAttributeOptionsSelectButton}}" userInput="option1" stepKey="selectOption1"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addToCart1"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad4"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <see selector="{{StorefrontMessagesSection.success}}" userInput="You added $$createConfigProduct1.name$ to your shopping cart." stepKey="seeAddToCartSuccessMessage"/> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="openMiniShoppingCart1"/> <see selector="{{StorefrontMinicartSection.productPriceByName($$createConfigProduct1.name$$)}}" userInput="$$createConfigProduct1.price$$" stepKey="seeCorrectProductPrice1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml index 59fa4fde1c88a..a3b1729102390 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleEntityTest/AdminDeleteCatalogPriceRuleEntityFromSimpleProductTest.xml @@ -49,8 +49,7 @@ </after> <!-- Delete the simple product and catalog price rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage1"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule1"> <argument name="name" value="{{DeleteActiveCatalogPriceRuleWithConditions.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -61,8 +60,12 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You deleted the rule." stepKey="seeDeletedRuleMessage1"/> <!-- Reindex --> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex1"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert that the rule isn't present on the Category page --> <amOnPage url="$$createCategory1.name$$.html" stepKey="goToStorefrontCategoryPage1"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml index 69508490774dd..64fe4d8a130a7 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminDeleteCatalogPriceRuleTest.xml @@ -61,17 +61,21 @@ <!-- Verify that the simple product page shows the discount --> <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="goToSimpleProductPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeCorrectName1"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createSimpleProduct.sku$$" stepKey="seeCorrectSku1"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku1"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$110.70" stepKey="seeCorrectPrice1"/> <!-- Verify that the configurable product page the catalog price rule discount --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToConfigurableProductPage1"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="seeCorrectName2"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="seeCorrectSku2"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku2"> + <argument name="productSku" value="{{_defaultProduct.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$0.90" stepKey="seeCorrectPrice2"/> <!-- Delete the rule --> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -79,8 +83,12 @@ <!-- Apply and flush the cache --> <click selector="{{AdminCatalogPriceRuleGrid.applyRules}}" stepKey="clickApplyRules"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify that category page shows the original prices --> <amOnPage url="$$createCategory.name$$.html" stepKey="goToCategoryPage2"/> @@ -92,13 +100,17 @@ <!-- Verify that the simple product page shows the original price --> <amOnPage url="$$createSimpleProduct.custom_attributes[url_key]$$.html" stepKey="goToSimpleProductPage2"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createSimpleProduct.name$$" stepKey="seeCorrectName3"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createSimpleProduct.sku$$" stepKey="seeCorrectSku3"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku3"> + <argument name="productSku" value="$$createSimpleProduct.sku$$"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$123.00" stepKey="seeCorrectPrice3"/> <!-- Verify that the configurable product page shows the original price --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToConfigurableProductPage2"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="seeCorrectName4"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="{{_defaultProduct.sku}}" stepKey="seeCorrectSku4"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="seeCorrectSku4"> + <argument name="productSku" value="{{_defaultProduct.sku}}"/> + </actionGroup> <see selector="{{StorefrontProductInfoMainSection.productPrice}}" userInput="$1.00" stepKey="seeCorrectPrice4"/> </test> </tests> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml index 9d7607d7521c9..745025073dceb 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/AdminEnableAttributeIsUndefinedCatalogPriceRuleTest.xml @@ -49,7 +49,7 @@ <after> <!--Delete created data--> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToCatalogPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -68,8 +68,7 @@ </after> <!--Create catalog price rule--> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage"/> - <waitForPageLoad stepKey="waitForPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup ref="CreateCatalogPriceRuleActionGroup" stepKey="createCatalogPriceRule"> <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> </actionGroup> @@ -80,9 +79,12 @@ <argument name="targetSelectValue" value="is undefined"/> </actionGroup> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> - <magentoCLI command="cache:flush" stepKey="flushCache3"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Check Catalog Price Rule for first product--> <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToFirstProductPage"/> @@ -104,7 +106,7 @@ <!--Delete previous attribute and Catalog Price Rule--> <deleteData createDataKey="createProductAttribute" stepKey="deleteProductAttribute"/> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToCatalogPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{CatalogRuleWithAllCustomerGroups.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -116,8 +118,7 @@ </createData> <!--Create new Catalog Price Rule--> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToPriceRulePage1"/> - <waitForPageLoad stepKey="waitForPriceRulePage1"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage1"/> <actionGroup ref="CreateCatalogPriceRuleActionGroup" stepKey="createCatalogPriceRule1"> <argument name="catalogRule" value="CatalogRuleWithAllCustomerGroups"/> </actionGroup> @@ -128,9 +129,12 @@ <argument name="targetSelectValue" value="is undefined"/> </actionGroup> <click selector="{{AdminNewCatalogPriceRule.saveAndApply}}" stepKey="clickSaveAndApplyRules1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex1"/> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexSecondTime"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheSecondTime"> + <argument name="tags" value=""/> + </actionGroup> <!--Check Catalog Price Rule for third product--> <amOnPage url="{{StorefrontProductPage.url($$createThirdProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToThirdProductPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml index 1919f7d5cc544..6817dd4dafc5f 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogPriceRuleByProductAttributeTest.xml @@ -97,7 +97,7 @@ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> - <amOnPage url="{{CatalogRulePage.url}}" stepKey="goToCatalogPriceRulePage"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToCatalogPriceRulePage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deletePriceRule"> <argument name="name" value="{{SimpleCatalogPriceRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -232,10 +232,11 @@ <see userInput="You saved the rule." selector="{{ContentManagementSection.StoreConfigurationPageSuccessMessage}}" stepKey="seeMessage"/> <see userInput="Updated rules applied." selector="{{ContentManagementSection.StoreConfigurationPageSuccessMessage}}" stepKey="seeSuccessMessage"/> - <!-- Run cron twice --> - <magentoCLI command="cron:run" stepKey="runCron1"/> - <magentoCLI command="cron:run" stepKey="runCron2"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <!-- Run cron --> + <magentoCron stepKey="runAllCronJobs"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Go to Frontend and open the simple product --> <amOnPage url="{{StorefrontProductPage.url($$createFirstProduct.sku$$)}}" stepKey="amOnSimpleProductPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml index 23fc7e1a9ffba..1651f8425ec1c 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleAndConfigurableProductTest.xml @@ -81,7 +81,7 @@ </before> <after> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -112,8 +112,12 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml index dfd34181108b8..8103e6b115950 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductAndFixedMethodTest.xml @@ -41,7 +41,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{CatalogRuleByFixed.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -61,8 +61,12 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml index 25351ca650db9..b90cc66a10d68 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductForNewCustomerGroupTest.xml @@ -46,7 +46,7 @@ <deleteData createDataKey="customerGroup" stepKey="deleteCustomerGroup"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{CatalogRuleByFixed.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -69,7 +69,9 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml index 59976fbac1724..d9b62ef8fc913 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/ApplyCatalogRuleForSimpleProductWithCustomOptionsTest.xml @@ -49,7 +49,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <!-- Delete the catalog price rule --> - <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup ref="AdminOpenCatalogPriceRulePageActionGroup" stepKey="goToPriceRulePage"/> <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> <argument name="name" value="{{_defaultCatalogRule.name}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> @@ -68,8 +68,12 @@ <!-- Save and apply the new catalog price rule --> <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Navigate to category on store front --> <amOnPage url="{{StorefrontProductPage.url($createCategory.name$)}}" stepKey="goToCategoryPage"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml index 6ac9f713e2844..b678e379a603d 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/CatalogPriceRuleAndCustomerGroupMembershipArePersistedUnderLongTermCookieTest.xml @@ -41,7 +41,9 @@ <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="clickSaveAndApplyRule"/> <!-- Perform reindex --> - <magentoCLI command="indexer:reindex" arguments="catalogrule_rule" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogrule_rule"/> + </actionGroup> </before> <after> <createData entity="PersistentConfigDefault" stepKey="setDefaultPersistentState"/> diff --git a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml index 2df891b24223b..264c55ba43390 100644 --- a/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml +++ b/app/code/Magento/CatalogRule/Test/Mftf/Test/StorefrontInactiveCatalogRuleTest.xml @@ -34,7 +34,9 @@ <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForThirdPriceRule"/> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyFirstPriceRule"/> <!-- Perform reindex --> - <magentoCLI command="indexer:reindex" arguments="catalogrule_rule" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogrule_rule"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/CatalogRule/etc/mview.xml b/app/code/Magento/CatalogRule/etc/mview.xml index 9e5a1c866a842..9f793d5c8c393 100644 --- a/app/code/Magento/CatalogRule/etc/mview.xml +++ b/app/code/Magento/CatalogRule/etc/mview.xml @@ -26,6 +26,7 @@ <view id="catalog_product_price" class="Magento\Catalog\Model\Indexer\Product\Price" group="indexer"> <subscriptions> <table name="catalogrule_product_price" entity_column="product_id" /> + <table name="catalogrule_product_price_replica" entity_column="product_id" /> </subscriptions> </view> </config> diff --git a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml index 1c3eedf43b264..e1229dc56cfbf 100644 --- a/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml +++ b/app/code/Magento/CatalogRule/view/adminhtml/templates/promo/fieldset.phtml @@ -5,14 +5,16 @@ */ /**@var \Magento\Backend\Block\Widget\Form\Renderer\Fieldset $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_element = $block->getElement() ?> <?php $_jsObjectName = $block->getFieldSetId() != null ? $block->getFieldSetId() : $_element->getHtmlId() ?> <div class="rule-tree"> - <fieldset id="<?= $block->escapeHtmlAttr($_jsObjectName) ?>" <?= /* @noEscape */ $_element->serialize(['class']) ?> class="fieldset"> +<fieldset id="<?= $block->escapeHtmlAttr($_jsObjectName) ?>" <?= /* @noEscape */ $_element->serialize(['class']) ?> + class="fieldset"> <legend class="legend"><span><?= $block->escapeHtml($_element->getLegend()) ?></span></legend> <br> - <?php if ($_element->getComment()) : ?> + <?php if ($_element->getComment()): ?> <div class="messages"> <div class="message message-notice"><?= $block->escapeHtml($_element->getComment()) ?></div> </div> @@ -22,16 +24,21 @@ </div> </fieldset> </div> -<script> + +<?php $scriptString = <<<script + require([ - "Magento_Rule/rules", - "prototype" + 'Magento_Rule/rules', + 'prototype' ], function(VarienRulesForm){ -window.<?= /* @noEscape */ $_jsObjectName ?> = new VarienRulesForm('<?= /* @noEscape */ $_jsObjectName ?>', '<?= /* @noEscape */ $block->getNewChildUrl() ?>'); -<?php if ($_element->getReadonly()) : ?> - <?= /* @noEscape */ $_element->getHtmlId() ?>.setReadonly(true); -<?php endif; ?> +script; +$scriptString .= 'window.' . /* @noEscape */ $_jsObjectName . ' = new VarienRulesForm(\'' . + /* @noEscape */ $_jsObjectName . '\', \'' . /* @noEscape */ $block->getNewChildUrl() . '\');'; +if ($_element->getReadonly()): + $scriptString .= /* @noEscape */ $_element->getHtmlId() . '.setReadonly(true);' . PHP_EOL; +endif; -}); -</script> +$scriptString .= '});' . PHP_EOL; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php index 681b7ecfb02dc..f8c159f5d6d73 100644 --- a/app/code/Magento/CatalogSearch/Block/Advanced/Form.php +++ b/app/code/Magento/CatalogSearch/Block/Advanced/Form.php @@ -9,11 +9,13 @@ use Magento\CatalogSearch\Model\Advanced; use Magento\Directory\Model\CurrencyFactory; use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Collection\AbstractDb as DbCollection; use Magento\Framework\View\Element\AbstractBlock; use Magento\Framework\View\Element\BlockInterface; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\Template\Context; +use Magento\CatalogSearch\Helper\Data as CatalogSearchHelper; /** * Advanced search form @@ -42,15 +44,19 @@ class Form extends Template * @param Advanced $catalogSearchAdvanced * @param CurrencyFactory $currencyFactory * @param array $data + * @param CatalogSearchHelper|null $catalogSearchHelper */ public function __construct( Context $context, Advanced $catalogSearchAdvanced, CurrencyFactory $currencyFactory, - array $data = [] + array $data = [], + ?CatalogSearchHelper $catalogSearchHelper = null ) { $this->_catalogSearchAdvanced = $catalogSearchAdvanced; $this->_currencyFactory = $currencyFactory; + $data['catalogSearchHelper'] = $catalogSearchHelper ?? + ObjectManager::getInstance()->get(CatalogSearchHelper::class); parent::__construct($context, $data); } @@ -185,7 +191,7 @@ public function getCurrency($attribute) * Retrieve attribute input type * * @param AbstractAttribute $attribute - * @return string + * @return string */ public function getAttributeInputType($attribute) { diff --git a/app/code/Magento/CatalogSearch/Model/Advanced.php b/app/code/Magento/CatalogSearch/Model/Advanced.php index 8ce2e0140f528..5143762a07e08 100644 --- a/app/code/Magento/CatalogSearch/Model/Advanced.php +++ b/app/code/Magento/CatalogSearch/Model/Advanced.php @@ -67,7 +67,7 @@ class Advanced extends \Magento\Framework\Model\AbstractModel /** * Initialize dependencies * - * @deprecated + * @deprecated 101.0.2 * @var Config */ protected $_catalogConfig; diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php index ca6ff0720023f..e226bdc6900e6 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext.php @@ -126,6 +126,7 @@ public function execute($entityIds) * @inheritdoc * * @throws \InvalidArgumentException + * @since 101.0.0 */ public function executeByDimensions(array $dimensions, \Traversable $entityIds = null) { 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 360df8f4edc66..ffa7dfd80df0c 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/DataProvider.php @@ -572,7 +572,7 @@ public function prepareProductIndex($indexData, $productData, $storeId) foreach ($indexData as $entityId => $attributeData) { foreach ($attributeData as $attributeId => $attributeValues) { $value = $this->getAttributeValue($attributeId, $attributeValues, $storeId); - if ($value !== null && $value !== false && $value != '') { + if ($value !== null && $value !== false && $value !== '') { if (!isset($index[$attributeId])) { $index[$attributeId] = []; } diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php index bd63e1e79989c..fa4d9fee415cf 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Fulltext/Action/Full.php @@ -41,7 +41,7 @@ class Full * Index values separator * * @var string - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$separator */ protected $separator = ' | '; @@ -50,7 +50,7 @@ class Full * Array of \DateTime objects per store * * @var \DateTime[] - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $dates = []; @@ -58,7 +58,7 @@ class Full * Product Type Instances cache * * @var array - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productTypes */ protected $productTypes = []; @@ -67,7 +67,7 @@ class Full * Product Emulators cache * * @var array - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$productEmulators */ protected $productEmulators = []; @@ -95,7 +95,7 @@ class Full * Catalog product type * * @var \Magento\Catalog\Model\Product\Type - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$catalogProductType */ protected $catalogProductType; @@ -111,7 +111,7 @@ class Full * Core store config * * @var \Magento\Framework\App\Config\ScopeConfigInterface - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $scopeConfig; @@ -119,7 +119,7 @@ class Full * Store manager * * @var \Magento\Store\Model\StoreManagerInterface - * @deprecated 100.1.6 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider + * @deprecated 100.1.0 Moved to \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider * @see \Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider::$storeManager */ protected $storeManager; @@ -131,25 +131,25 @@ class Full /** * @var \Magento\Framework\Indexer\SaveHandler\IndexerInterface - * @deprecated 100.1.6 As part of self::cleanIndex() + * @deprecated 100.1.0 As part of self::cleanIndex() */ protected $indexHandler; /** * @var \Magento\Framework\Stdlib\DateTime - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $dateTime; /** * @var \Magento\Framework\Locale\ResolverInterface - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $localeResolver; /** * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $localeDate; @@ -160,19 +160,19 @@ class Full /** * @var \Magento\CatalogSearch\Model\ResourceModel\Fulltext - * @deprecated 100.1.6 Not used anymore + * @deprecated 100.1.0 Not used anymore */ protected $fulltextResource; /** * @var \Magento\Framework\Search\Request\Config - * @deprecated 100.1.6 As part of self::reindexAll() + * @deprecated 100.1.0 As part of self::reindexAll() */ protected $searchRequestConfig; /** * @var \Magento\Framework\Search\Request\DimensionFactory - * @deprecated 100.1.6 As part of self::cleanIndex() + * @deprecated 100.1.0 As part of self::cleanIndex() */ private $dimensionFactory; @@ -301,7 +301,7 @@ protected function getTable($table) /** * Get parents IDs of product IDs to be re-indexed * - * @deprecated as it not used in the class anymore and duplicates another API method + * @deprecated 100.2.3 as it not used in the class anymore and duplicates another API method * @see \Magento\CatalogSearch\Model\ResourceModel\Fulltext::getRelationsByChild() * * @param int[] $entityIds diff --git a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php index 8722cd52b618a..c79d876c1127d 100644 --- a/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php +++ b/app/code/Magento/CatalogSearch/Model/Indexer/Scope/UnknownStateException.php @@ -13,7 +13,7 @@ * * @api * @since 100.2.0 - * @deprecated + * @deprecated 101.0.0 * @see \Magento\ElasticSearch */ class UnknownStateException extends LocalizedException diff --git a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php index 332bb991bf29f..b2aaa054ebc34 100644 --- a/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php +++ b/app/code/Magento/CatalogSearch/Model/Layer/Filter/Price.php @@ -176,15 +176,16 @@ public function getCurrencyRate() * * @param float|string $fromPrice * @param float|string $toPrice + * @param boolean $isLast * @return float|\Magento\Framework\Phrase */ - protected function _renderRangeLabel($fromPrice, $toPrice) + protected function _renderRangeLabel($fromPrice, $toPrice, $isLast = false) { $fromPrice = empty($fromPrice) ? 0 : $fromPrice * $this->getCurrencyRate(); $toPrice = empty($toPrice) ? $toPrice : $toPrice * $this->getCurrencyRate(); $formattedFromPrice = $this->priceCurrency->format($fromPrice); - if ($toPrice === '') { + if ($isLast) { return __('%1 and above', $formattedFromPrice); } elseif ($fromPrice == $toPrice && $this->dataProvider->getOnePriceIntervalValue()) { return $formattedFromPrice; @@ -215,12 +216,15 @@ protected function _getItemsData() $data = []; if (count($facets) > 1) { // two range minimum + $lastFacet = array_key_last($facets); foreach ($facets as $key => $aggregation) { $count = $aggregation['count']; if (strpos($key, '_') === false) { continue; } - $data[] = $this->prepareData($key, $count, $data); + + $isLast = $lastFacet === $key; + $data[] = $this->prepareData($key, $count, $isLast); } } @@ -264,18 +268,13 @@ protected function getFrom($from) * * @param string $key * @param int $count + * @param boolean $isLast * @return array */ - private function prepareData($key, $count) + private function prepareData($key, $count, $isLast = false) { - list($from, $to) = explode('_', $key); - if ($from == '*') { - $from = $this->getFrom($to); - } - if ($to == '*') { - $to = $this->getTo($to); - } - $label = $this->_renderRangeLabel($from, $to); + [$from, $to] = explode('_', $key); + $label = $this->_renderRangeLabel($from, $to, $isLast); $value = $from . '-' . $to . $this->dataProvider->getAdditionalRequestData(); $data = [ diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php index 291fad1e16ebf..47160bff1d571 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Advanced/Collection.php @@ -236,6 +236,7 @@ public function addFieldsToFilter($fields) /** * @inheritdoc + * @since 101.0.2 */ public function setOrder($attribute, $dir = Select::SQL_DESC) { @@ -253,6 +254,7 @@ public function setOrder($attribute, $dir = Select::SQL_DESC) /** * @inheritdoc + * @since 101.0.2 */ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) { @@ -272,6 +274,7 @@ public function addCategoryFilter(\Magento\Catalog\Model\Category $category) /** * @inheritdoc + * @since 101.0.2 */ public function setVisibility($visibility) { @@ -338,6 +341,7 @@ protected function _renderFiltersBefore() /** * @inheritDoc + * @since 101.0.4 */ public function clear() { @@ -347,6 +351,7 @@ public function clear() /** * @inheritDoc + * @since 101.0.4 */ protected function _reset() { @@ -356,6 +361,7 @@ protected function _reset() /** * @inheritdoc + * @since 101.0.4 */ public function _loadEntities($printQuery = false, $logQuery = false) { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php index d1259159606d3..d0456ff011027 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/EngineProvider.php @@ -29,7 +29,7 @@ class EngineProvider /** * @var \Magento\Framework\App\Config\ScopeConfigInterface - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $scopeConfig; diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php index 0835fb66f876a..3614cd9dbf3a9 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext.php @@ -62,7 +62,7 @@ protected function _construct() * Reset search results * * @return $this - * @deprecated Not used anymore + * @deprecated 101.0.0 Not used anymore * @see Fulltext::resetSearchResultsByStore */ public function resetSearchResults() @@ -78,6 +78,7 @@ public function resetSearchResults() * * @param int $storeId * @return $this + * @since 101.0.0 */ public function resetSearchResultsByStore($storeId) { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php index e63ed1bd3d72f..06dcc69ef60f5 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection.php @@ -337,6 +337,7 @@ public function addFieldToFilter($field, $condition = null) /** * @inheritDoc + * @since 101.0.4 */ public function clear() { @@ -348,6 +349,7 @@ public function clear() /** * @inheritDoc + * @since 101.0.4 */ protected function _reset() { @@ -359,6 +361,7 @@ protected function _reset() /** * @inheritdoc + * @since 101.0.4 */ public function _loadEntities($printQuery = false, $logQuery = false) { @@ -429,6 +432,7 @@ public function setOrder($attribute, $dir = Select::SQL_DESC) * @param string $attribute * @param string $dir * @return $this + * @since 101.0.2 */ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) { @@ -555,6 +559,7 @@ private function getSearchResultApplier(SearchResultInterface $searchResult): Se /** * @inheritdoc + * @since 100.2.3 */ protected function _beforeLoad() { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php index b396437fc66c7..a7e9c237f58c3 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php @@ -8,7 +8,7 @@ /** * This class add in backward compatibility purposes to check if need to apply old strategy for filter prepare process. - * @deprecated + * @deprecated 101.0.2 */ class DefaultFilterStrategyApplyChecker implements DefaultFilterStrategyApplyCheckerInterface { diff --git a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php index a067767775393..d9e41af658089 100644 --- a/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php +++ b/app/code/Magento/CatalogSearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyCheckerInterface.php @@ -8,7 +8,7 @@ /** * Added in backward compatibility purposes to check if need to apply old strategy for filter prepare process. - * @deprecated + * @deprecated 101.0.2 */ interface DefaultFilterStrategyApplyCheckerInterface { diff --git a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php index 916e03f471493..2f6a402b20406 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php +++ b/app/code/Magento/CatalogSearch/Model/Search/ReaderPlugin.php @@ -6,7 +6,7 @@ namespace Magento\CatalogSearch\Model\Search; /** - * @deprecated + * @deprecated 101.0.0 * @see \Magento\ElasticSearch */ class ReaderPlugin diff --git a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php index 68ca546b81919..aa3bd1f149c16 100644 --- a/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php +++ b/app/code/Magento/CatalogSearch/Model/Search/RequestGenerator/GeneratorResolver.php @@ -9,7 +9,7 @@ /** * @api * @since 100.1.6 - * @deprecated + * @deprecated 101.0.0 * @see \Magento\ElasticSearch */ class GeneratorResolver diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml index a6e3dfd7eaad4..1dc57cd083435 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/ActionGroup/AdminSetMinimalQueryLengthActionGroup.xml @@ -25,7 +25,6 @@ <see userInput="{{MinMaxQueryLength.Hint}}" selector="{{AdminCatalogSearchConfigurationSection.maxQueryLengthHint}}" stepKey="seeHint2"/> <uncheckOption selector="{{AdminCatalogSearchConfigurationSection.minQueryLengthInherit}}" stepKey="uncheckSystemValue"/> <fillField selector="{{AdminCatalogSearchConfigurationSection.minQueryLength}}" userInput="{{minLength}}" stepKey="setMinQueryLength"/> - <click selector="{{AdminCatalogSearchConfigurationSection.catalogSearchTab}}" stepKey="collapseTab"/> <click selector="{{ContentManagementSection.Save}}" stepKey="saveConfig"/> <waitForPageLoad stepKey="waitForConfigSaved"/> <see userInput="You saved the configuration." stepKey="seeSuccessMessage"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml index d23663d43dcd0..c02ef4957ad3d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByDescriptionTest.xml @@ -13,8 +13,12 @@ <group value="CatalogSearch"/> </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByDescriptionActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml index 0b3fb2fa42532..0c8e192f9366e 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByNameTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml index 517e200f8ce11..99c09b5ba93a5 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByPriceTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductNameAndPriceActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml index 0bd08d31e8ffa..1e18c5ea4d0a9 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductByShortDescriptionTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByShortDescriptionActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml index d273f9828dc95..34e0a73e91fe0 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/AdvanceCatalogSearchSimpleProductTest/AdvanceCatalogSearchSimpleProductBySkuTest.xml @@ -14,8 +14,12 @@ </annotations> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="GoToStoreViewAdvancedCatalogSearchActionGroup" stepKey="GoToStoreViewAdvancedCatalogSearchActionGroup"/> <actionGroup ref="StorefrontAdvancedCatalogSearchByProductSkuActionGroup" stepKey="search"> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml index 44bfc66a466a8..d6a5aa8b93572 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/MinimalQueryLengthForCatalogSearchTest.xml @@ -36,7 +36,7 @@ </after> <actionGroup ref="SetMinimalQueryLengthActionGroup" stepKey="setMinQueryLength"/> <comment userInput="Go to Storefront and search for product" stepKey="searchProdUsingMinQueryLength"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> <comment userInput="Quick search by single character and avoid using ES stopwords" stepKey="commentQuickSearch"/> <fillField selector="{{StorefrontQuickSearchResultsSection.searchTextBox}}" userInput="B" stepKey="fillAttribute"/> <waitForPageLoad stepKey="waitForSearchTextBox"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml index 49fce41fddf05..09f7ee455ebb5 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleDynamicTest.xml @@ -44,8 +44,12 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> @@ -53,7 +57,7 @@ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createBundleProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml index 4b0a5c84ac360..98d1b3412360c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartBundleFixedTest.xml @@ -55,8 +55,12 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteBundleProduct" createDataKey="createBundleProduct"/> @@ -67,7 +71,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> <comment userInput="$simpleProduct1.name$" stepKey="asdf"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createBundleProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml index 35db90363b1ae..3298eff34759b 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartConfigurableTest.xml @@ -26,8 +26,12 @@ </actionGroup> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> @@ -37,7 +41,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="{{_defaultProduct.name}}"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml index 79a2fc8646c04..85e3c46654502 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartDownloadableTest.xml @@ -28,15 +28,19 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> <deleteData stepKey="deleteProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml index cf30e4d06e8e7..3488a63140809 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartGroupedTest.xml @@ -28,15 +28,19 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteGroupedProduct" createDataKey="createProduct"/> <deleteData stepKey="deleteSimpleProduct" createDataKey="simple1"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value=""$createProduct.name$""/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml index ba6fa813367c3..26f4cd77b60bc 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartTest.xml @@ -24,14 +24,18 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createSimpleProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml index b71388f5f409b..9277baba94aa8 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchAndAddToCartVirtualTest.xml @@ -24,14 +24,18 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createVirtualProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createVirtualProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml index 566b4d204751d..a6654db91effb 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchEmptyResultsTest.xml @@ -25,15 +25,19 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="ThisShouldn'tReturnAnything"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml index 814e27182799f..83436ebb44c6d 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchProductBySkuTest.xml @@ -24,14 +24,18 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData stepKey="deleteProduct" createDataKey="createSimpleProduct"/> <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createSimpleProduct.sku$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml index e1488f4d000eb..b28a810344e5c 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/SearchEntityResultsTest/QuickSearchTwoProductsWithSameWeightTest.xml @@ -77,7 +77,7 @@ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="{{_defaultProduct.name}}"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml index 968435747bdbb..14ae988e6ce79 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontAdvancedSearchEntitySimpleProductTest.xml @@ -32,8 +32,12 @@ </after> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- 1. Navigate to Frontend --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml index 6f510fa315d7d..66d695cbb2025 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontQuickSearchConfigurableChildrenTest.xml @@ -73,7 +73,9 @@ </createData> <!-- Perform reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createConfigurableProduct" stepKey="deleteConfigurableProduct"/> @@ -83,7 +85,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> </after> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefront"> <argument name="phrase" value="$createConfigurableProduct.name$"/> </actionGroup> @@ -98,7 +100,7 @@ <actionGroup ref="ToggleProductEnabledActionGroup" stepKey="disableProduct"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAgain"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePageAgain"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="searchStorefrontAgain"> <argument name="phrase" value="$createConfigurableProduct.name$"/> </actionGroup> diff --git a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml index 8a0d91ae05b34..b42313fc14773 100644 --- a/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml +++ b/app/code/Magento/CatalogSearch/Test/Mftf/Test/StorefrontUpdateSearchTermEntityTest.xml @@ -26,8 +26,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStorefrontPage1"/> </before> <after> diff --git a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml index f158cebf41aae..bec3e57b44798 100644 --- a/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml +++ b/app/code/Magento/CatalogSearch/view/frontend/templates/advanced/form.phtml @@ -4,21 +4,23 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -// @codingStandardsIgnoreFile -?> -<?php /** * Catalog advanced search form * * @var $block \Magento\CatalogSearch\Block\Advanced\Form + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php $maxQueryLength = $this->helper(\Magento\CatalogSearch\Helper\Data::class)->getMaxQueryLength();?> -<form class="form search advanced" action="<?= $block->escapeUrl($block->getSearchPostUrl()) ?>" method="get" id="form-validate"> + +<?php +/** @var \Magento\CatalogSearch\Helper\Data $catalogSearchHelper */ +$catalogSearchHelper = $block->getData('catalogSearchHelper'); ?> +<?php $maxQueryLength = $catalogSearchHelper->getMaxQueryLength();?> +<form class="form search advanced" action="<?= $block->escapeUrl($block->getSearchPostUrl()) ?>" method="get" + id="form-validate"> <fieldset class="fieldset"> <legend class="legend"><span><?= $block->escapeHtml(__('Search Settings')) ?></span></legend><br /> - <?php foreach ($block->getSearchableAttributes() as $_attribute) : ?> + <?php foreach ($block->getSearchableAttributes() as $_attribute): ?> <?php $_code = $_attribute->getAttributeCode() ?> <div class="field <?= $block->escapeHtmlAttr($_code) ?>"> <label class="label" for="<?= $block->escapeHtmlAttr($_code) ?>"> @@ -26,7 +28,7 @@ </label> <div class="control"> <?php - switch ($block->getAttributeInputType($_attribute)) : + switch ($block->getAttributeInputType($_attribute)): case 'number': ?> <div class="range fields group group-2"> @@ -39,7 +41,8 @@ title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" class="input-text" maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" - data-validate="{number:true, 'less-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>_to'}" /> + data-validate="{number:true, 'less-than-equals-to':'#<?= + $block->escapeHtmlAttr($_code) ?>_to'}" /> </div> </div> <div class="field no-label"> @@ -51,7 +54,8 @@ title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" class="input-text" maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" - data-validate="{number:true, 'greater-than-equals-to':'#<?= $block->escapeHtmlAttr($_code) ?>'}" /> + data-validate="{number:true, 'greater-than-equals-to':'#<?= + $block->escapeHtmlAttr($_code) ?>'}" /> </div> </div> </div> @@ -126,7 +130,7 @@ id="<?= $block->escapeHtmlAttr($_code) ?>" value="<?= $block->escapeHtml($block->getAttributeValue($_attribute)) ?>" title="<?= $block->escapeHtml($block->getAttributeLabel($_attribute)) ?>" - class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass($_attribute)) ?>" + class="input-text <?= $block->escapeHtmlAttr($block->getAttributeValidationClass($_attribute))?>" maxlength="<?= $block->escapeHtmlAttr($maxQueryLength) ?>" /> <?php endswitch; ?> </div> @@ -143,7 +147,7 @@ </div> </div> </form> -<script> +<?php $scriptString = <<<script require([ "jquery", "mage/mage", @@ -159,9 +163,11 @@ require([ } }, messages: { - 'price[to]': {'greater-than-equals-to': '<?= $block->escapeJs(__('Please enter a valid price range.')) ?>'}, - 'price[from]': {'less-than-equals-to': '<?= $block->escapeJs(__('Please enter a valid price range.')) ?>'} + 'price[to]': {'greater-than-equals-to': '{$block->escapeJs(__('Please enter a valid price range.'))}'}, + 'price[from]': {'less-than-equals-to': '{$block->escapeJs(__('Please enter a valid price range.'))}'} } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php index f8d9ddf0c4ad9..5a339670bbb81 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Category/CurrentUrlRewritesRegenerator.php @@ -27,13 +27,13 @@ class CurrentUrlRewritesRegenerator /** * @var \Magento\Catalog\Model\Category - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $category; /** * @var \Magento\UrlRewrite\Model\UrlFinderInterface - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $urlFinder; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php index a86604672e2b4..d48bcd446fcfd 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteGenerator.php @@ -30,7 +30,7 @@ class CategoryUrlRewriteGenerator /** * @var \Magento\Catalog\Model\Category - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $category; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php index 42d3fd9cb40e1..628615803f6e8 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/Product/CurrentUrlRewritesRegenerator.php @@ -26,19 +26,19 @@ class CurrentUrlRewritesRegenerator { /** * @var Product - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $product; /** * @var ObjectRegistry - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $productCategories; /** * @var UrlFinderInterface - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $urlFinder; diff --git a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php index 868c417b5ff52..f5e6ae9a6d615 100644 --- a/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php +++ b/app/code/Magento/CatalogUrlRewrite/Model/ProductUrlRewriteGenerator.php @@ -26,49 +26,49 @@ class ProductUrlRewriteGenerator const ENTITY_TYPE = 'product'; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Service\V1\StoreViewService */ protected $storeViewService; /** * @var \Magento\Catalog\Model\Product - * @deprecated 100.1.4 + * @deprecated 100.1.0 */ protected $product; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\Product\CurrentUrlRewritesRegenerator */ protected $currentUrlRewritesRegenerator; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\Product\CategoriesUrlRewriteGenerator */ protected $categoriesUrlRewriteGenerator; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\Product\CanonicalUrlRewriteGenerator */ protected $canonicalUrlRewriteGenerator; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistryFactory */ protected $objectRegistryFactory; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\CatalogUrlRewrite\Model\ObjectRegistry */ protected $productCategories; /** - * @deprecated 100.1.4 + * @deprecated 100.1.0 * @var \Magento\Store\Model\StoreManagerInterface */ protected $storeManager; diff --git a/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php index ca514be51d99b..8816b2816b797 100644 --- a/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php +++ b/app/code/Magento/CatalogUrlRewrite/Plugin/Catalog/Block/Adminhtml/Category/Tab/Attributes.php @@ -3,38 +3,90 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\CatalogUrlRewrite\Plugin\Catalog\Block\Adminhtml\Category\Tab; +use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Model\Category\DataProvider; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\CatalogUrlRewrite\Block\UrlKeyRenderer; +use Magento\Store\Model\ScopeInterface; + /** - * Class Attributes + * Category tab attributes */ class Attributes { /** - * @param \Magento\Catalog\Model\Category\DataProvider $subject - * @param array $result + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Adds attributes meta if url_key exist * + * @param DataProvider $subject + * @param array $result * @return array */ - public function afterGetAttributesMeta( - \Magento\Catalog\Model\Category\DataProvider $subject, - $result - ) { - /** @var \Magento\Catalog\Model\Category $category */ + public function afterGetAttributesMeta(DataProvider $subject, $result) + { + if (!isset($result['url_key'])) { + return $result; + } + $category = $subject->getCurrentCategory(); - if (isset($result['url_key'])) { - if ($category && $category->getId()) { - if ($category->getLevel() == 1) { - $result['url_key_group']['componentDisabled'] = true; - } else { - $result['url_key_create_redirect']['valueMap']['true'] = $category->getUrlKey(); - $result['url_key_create_redirect']['value'] = $category->getUrlKey(); - $result['url_key_create_redirect']['disabled'] = true; - } + if ($category && $category->getId()) { + if ((int) $category->getLevel() === 1) { + $result['url_key_group']['componentDisabled'] = true; } else { - $result['url_key_create_redirect']['visible'] = false; + $result['url_key_create_redirect'] = $this->getUrlRewriteMeta($category); } + } else { + $result['url_key_create_redirect']['visible'] = false; } + return $result; } + + /** + * Returns url rewrite meta + * + * @param CategoryInterface $category + * @return array + */ + private function getUrlRewriteMeta(CategoryInterface $category): array + { + return [ + 'value' => $this->isSaveRewriteHistory($category->getStoreId()) ? $category->getUrlKey() : '', + 'valueMap' => [ + 'false' => '', + 'true' => $category->getUrlKey() + ], + 'disabled' => true, + ]; + } + + /** + * Returns Create Permanent Redirect for URLs if changed config enabled + * + * @param int $storeId + * @return bool + */ + private function isSaveRewriteHistory(int $storeId): bool + { + return $this->scopeConfig->isSetFlag( + UrlKeyRenderer::XML_PATH_SEO_SAVE_HISTORY, + ScopeInterface::SCOPE_STORE, + $storeId + ); + } } diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml index 9ce6d397a551b..b4b391326a36f 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Data/GenerateCategoryProductUrlRewriteConfigData.xml @@ -27,4 +27,12 @@ <data key="path">catalog/seo/product_use_categories</data> <data key="value">0</data> </entity> + <entity name="EnableCreatePermanentRedirect"> + <data key="path">catalog/seo/save_rewrites_history</data> + <data key="value">1</data> + </entity> + <entity name="DisableCreatePermanentRedirect"> + <data key="path">catalog/seo/save_rewrites_history</data> + <data key="value">0</data> + </entity> </entities> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml index 9e4689bd8aa4f..0e4ee26a462e6 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminRewriteProductWithTwoStoreTest.xml @@ -17,7 +17,9 @@ <before> <magentoCLI command="config:set {{EnableCategoriesPathProductUrls.path}} {{EnableCategoriesPathProductUrls.value}}" stepKey="enableUseCategoriesPath"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView" /> @@ -36,7 +38,9 @@ <deleteData createDataKey="simpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="defaultCategory" stepKey="deleteNewRootCategory"/> <magentoCLI command="config:set {{DisableCategoriesPathProductUrls.path}} {{DisableCategoriesPathProductUrls.value}}" stepKey="disableUseCategoriesPath"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="navigateToCreatedDefaultCategory"> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml index 329f5e8cae3f6..ad426c4bc6c4c 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminUrlForProductRewrittenCorrectlyTest.xml @@ -56,8 +56,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Select product and go toUpdate Attribute page--> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="GoToCatalogPageChangingView"/> - <waitForPageLoad stepKey="WaitForPageToLoadFullyChangingView"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToCatalogPageChangingView"/> <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="filterBundleProductOptionsDownToName"> <argument name="product" value="ApiSimpleProduct"/> </actionGroup> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml new file mode 100644 index 0000000000000..d529c6dd3ecc3 --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminVerifyCheckboxIsDisabledCreatePermanentRedirectSetNoTest"> + <annotations> + <features value="CatalogUrlRewrite"/> + <stories value="Url rewrites"/> + <title value="Verify checkbox is disabled 'Create Permanent Redirect' set 'No'"/> + <description value="Verify checkbox is disabled 'Create Permanent Redirect' set 'No' on category and product edit page."/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35589"/> + </annotations> + <before> + <magentoCLI command="config:set {{DisableCreatePermanentRedirect.path}} {{DisableCreatePermanentRedirect.value}}" stepKey="enableCreatePermanentRedirect"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <createData entity="_defaultCategory" stepKey="createDefaultCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createDefaultCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createDefaultCategory" stepKey="deleteDefaultCategory"/> + <magentoCLI command="config:set {{EnableCreatePermanentRedirect.path}} {{EnableCreatePermanentRedirect.value}}" stepKey="disableCreatePermanentRedirect"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> + <argument name="productId" value="$$createSimpleProduct.id$$"/> + </actionGroup> + <scrollTo selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="scrollToSeoSection" x="0" y="-120" /> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <grabValueFrom selector="{{AdminProductSEOSection.urlKeyRedirectCheckbox}}" stepKey="grabValue"/> + <assertEmpty stepKey="checkUrlKeyRedirectCheckbox"> + <actualResult type="string">$grabValue</actualResult> + </assertEmpty> + + <actionGroup ref="NavigateToCreatedCategoryActionGroup" stepKey="openCreatedSubCategory"> + <argument name="Category" value="$$createDefaultCategory$$"/> + </actionGroup> + <scrollTo selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="scrollToSeoSection1" x="0" y="-120" /> + <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="openSeoSection1"/> + <grabValueFrom selector="{{AdminCategorySEOSection.UrlKeyRedirectCheckbox}}" stepKey="grabValue1"/> + <assertEmpty stepKey="checkUrlKeyRedirectCheckbox1"> + <actualResult type="string">$grabValue1</actualResult> + </assertEmpty> + </test> +</tests> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml index 99037a5c89af1..4880d438373f4 100644 --- a/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml +++ b/app/code/Magento/CatalogUrlRewrite/Test/Mftf/Test/StorefrontCategoryAccessibleWhenSuffixIsNullTest.xml @@ -21,7 +21,9 @@ <magentoCLI command="config:set catalog/seo/category_url_suffix ''" stepKey="setCategoryUrlSuffix"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="setCategoryProductRewrites"/> - <magentoCLI command="cache:flush" stepKey="flushCacheBefore"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheBefore"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="_defaultCategory" stepKey="createCategory"/> </before> <after> @@ -30,7 +32,9 @@ stepKey="restoreCategoryUrlSuffix"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="restoreCategoryProductRewrites"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfter"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfter"> + <argument name="tags" value=""/> + </actionGroup> </after> <amOnPage url="/$$createCategory.name$$" stepKey="onCategoryPage"/> diff --git a/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php new file mode 100644 index 0000000000000..8134ecef3db6d --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/Test/Unit/Plugin/Catalog/Block/Adminhtml/Category/Tab/AttributesTest.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CatalogUrlRewrite\Test\Unit\Plugin\Catalog\Block\Adminhtml\Category\Tab; + +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category\DataProvider as CategoryDataProvider; +use Magento\CatalogUrlRewrite\Plugin\Catalog\Block\Adminhtml\Category\Tab\Attributes; +use Magento\Framework\App\Config\ScopeConfigInterface; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\CatalogUrlRewrite\Plugin\Catalog\Block\Adminhtml\Category\Tab\Attributes. + */ +class AttributesTest extends TestCase +{ + private const STUB_CATEGORY_META = ['url_key' => 'url_key_test']; + private const STUB_URL_KEY = 'url_key_777'; + + /** + * @var Attributes + */ + private $model; + + /** + * @var Category|MockObject + */ + private $categoryMock; + + /** + * @var CategoryDataProvider|MockObject + */ + private $dataProviderMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->categoryMock = $this->createMock(Category::class); + $this->dataProviderMock = $this->createMock(CategoryDataProvider::class); + $this->dataProviderMock->expects($this->any()) + ->method('getCurrentCategory') + ->willReturn($this->categoryMock); + + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->model = $objectManager->getObject(Attributes::class, ['scopeConfig' => $this->scopeConfigMock]); + } + + /** + * Test get attributes meta + * + * @dataProvider attributesMetaDataProvider + * + * @param bool $configEnabled + * @param string $expectedValue + * @param string $expectedValueMap + * @return void + */ + public function testGetAttributesMeta(bool $configEnabled, string $expectedValue, string $expectedValueMap): void + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->categoryMock->expects($this->once()) + ->method('getLevel') + ->willReturn(2); + $this->categoryMock->expects($this->atMost(2)) + ->method('getUrlKey') + ->willReturn(self::STUB_URL_KEY); + $this->scopeConfigMock->expects($this->once()) + ->method('isSetFlag') + ->willReturn($configEnabled); + $this->categoryMock->expects($this->once()) + ->method('getStoreId') + ->willReturn(1); + + $result = $this->model->afterGetAttributesMeta($this->dataProviderMock, self::STUB_CATEGORY_META); + + $this->assertArrayHasKey('url_key_create_redirect', $result); + + $this->assertArrayHasKey('value', $result['url_key_create_redirect']); + $this->assertEquals($expectedValue, $result['url_key_create_redirect']['value']); + + $this->assertArrayHasKey('valueMap', $result['url_key_create_redirect']); + $this->assertArrayHasKey('true', $result['url_key_create_redirect']['valueMap']); + $this->assertEquals($expectedValueMap, $result['url_key_create_redirect']['valueMap']['true']); + + $this->assertArrayHasKey('disabled', $result['url_key_create_redirect']); + $this->assertTrue($result['url_key_create_redirect']['disabled']); + } + + /** + * DataProvider for testGetAttributesMeta + * + * @return array + */ + public function attributesMetaDataProvider(): array + { + return [ + 'save rewrite history config enabled' => [true, self::STUB_URL_KEY, self::STUB_URL_KEY], + 'save rewrite history config disabled' => [false, '', 'url_key_777'] + ]; + } + + /** + * Test get category without id attributes meta + * + * @return void + */ + public function testGetAttributesMetaWithoutCategoryId(): void + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(null); + + $result = $this->model->afterGetAttributesMeta($this->dataProviderMock, self::STUB_CATEGORY_META); + + $this->assertArrayHasKey('url_key_create_redirect', $result); + $this->assertArrayHasKey('visible', $result['url_key_create_redirect']); + $this->assertFalse($result['url_key_create_redirect']['visible']); + } + + /** + * Test get root category attributes meta + * + * @return void + */ + public function testGetAttributesMetaRootCategory(): void + { + $this->categoryMock->expects($this->once()) + ->method('getId') + ->willReturn(1); + $this->categoryMock->expects($this->once()) + ->method('getLevel') + ->willReturn(1); + + $result = $this->model->afterGetAttributesMeta($this->dataProviderMock, self::STUB_CATEGORY_META); + + $this->assertArrayHasKey('url_key_group', $result); + $this->assertArrayHasKey('componentDisabled', $result['url_key_group']); + $this->assertTrue($result['url_key_group']['componentDisabled']); + } +} diff --git a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml b/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml index 75d395473f969..ccd077e615221 100644 --- a/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml +++ b/app/code/Magento/CatalogUrlRewrite/etc/adminhtml/system.xml @@ -32,7 +32,7 @@ <label>Generate "category/product" URL Rewrites</label> <backend_model>Magento\CatalogUrlRewrite\Model\TableCleaner</backend_model> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> - <comment><![CDATA[<strong style="color:red">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.]]></comment> + <comment><![CDATA[<strong class="colorRed">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.]]></comment> <frontend_class>generate_category_product_rewrites</frontend_class> </field> </group> diff --git a/app/code/Magento/CatalogUrlRewrite/etc/csp_whitelist.xml b/app/code/Magento/CatalogUrlRewrite/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..0af163606fcaf --- /dev/null +++ b/app/code/Magento/CatalogUrlRewrite/etc/csp_whitelist.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="authorize_net_direct" type="host">secure.authorize.net</value> + <value id="authorize_net_direct_test" type="host">test.authorize.net</value> + </values> + </policy> + <policy id="frame-src"> + <values> + <value id="authorize_net_direct" type="host">secure.authorize.net</value> + <value id="authorize_net_direct_test" type="host">test.authorize.net</value> + </values> + </policy> + <policy id="form-action"> + <values> + <value id="authorize_net_direct" type="host">secure.authorize.net</value> + <value id="authorize_net_direct_test" type="host">test.authorize.net</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv index 7f1e1cd086408..0def4f6de32eb 100644 --- a/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv +++ b/app/code/Magento/CatalogUrlRewrite/i18n/en_US.csv @@ -9,4 +9,4 @@ "URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key.","URL key ""%1"" matches a reserved endpoint name (%2). Use another URL key." "Invalid URL key. The ""%1"" URL key can not be used to generate Latin URL key. Please use Latin letters and numbers to avoid generating URL key issues.","Invalid URL key. The ""%1"" URL key can not be used to generate Latin URL key. Please use Latin letters and numbers to avoid generating URL key issues." "Invalid URL key. The ""%1"" category name can not be used to generate Latin URL key. Please add URL key or change category name using Latin letters and numbers to avoid generating URL key issues.","Invalid URL key. The ""%1"" category name can not be used to generate Latin URL key. Please add URL key or change category name using Latin letters and numbers to avoid generating URL key issues." -"<strong style=""color:red"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.","<strong style=""color:red"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them." +"<strong class=""colorRed"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them.","<strong style=""color:red"">Warning!</strong> Turning this option off will result in permanent removal of category/product URL rewrites without an ability to restore them." diff --git a/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml index 800ecfd8a6e2f..9708f89f6377f 100644 --- a/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml +++ b/app/code/Magento/CatalogUrlRewrite/view/adminhtml/templates/confirm.phtml @@ -3,20 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require([ "jquery", "Magento_Ui/js/modal/confirm", "mage/translate", - ], function(jQuery, confirmation, $t) { + ], function(jQuery, confirmation, _t) { //confirmation for removing category/product URL rewrites jQuery('select.generate_category_product_rewrites').on('change', function () { if (this.value == 0) { confirmation({ - title: $t('Turn off "category/products" URL rewrites?'), - content: $t('Turning off automatic generation of "category/products" URL rewrites will result in permanent removal of all the currently existing “category/product” type URL rewrites without an ability to restore them back. ' + - 'This may potentially cause unresolved “category/product” type URL conflicts which you have to resolve by updating URL key manually.'), + title: _t('Turn off "category/products" URL rewrites?'), + content: _t('Turning off automatic generation of "category/products" URL rewrites will result in ' + + 'permanent removal of all the currently existing “category/product” type URL rewrites without ' + + 'an ability to restore them back. ' + + 'This may potentially cause unresolved “category/product” type URL conflicts which you have ' + + 'to resolve by updating URL key manually.'), actions: { cancel: function () { jQuery('select.generate_category_product_rewrites').val(1); @@ -27,4 +35,6 @@ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml index 59f0cd7437f44..c40071f4ef263 100644 --- a/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml +++ b/app/code/Magento/CatalogWidget/Test/Mftf/Test/CatalogProductListWidgetOperatorsTest.xml @@ -71,8 +71,7 @@ <conditionalClick selector="{{AdminCategoryDisplaySettingsSection.settingsHeader}}" dependentSelector="{{AdminCategoryDisplaySettingsSection.displayMode}}" visible="false" stepKey="openDisplaySettingsSection"/> <waitForPageLoad stepKey="waitForDisplaySettingsLoad"/> <selectOption stepKey="selectStaticBlockOnlyOption" userInput="Static block only" selector="{{AdminCategoryDisplaySettingsSection.displayMode}}"/> - <click selector="{{AdminCategoryMainActionsSection.SaveButton}}" stepKey="saveCategoryWithProducts"/> - <waitForPageLoad stepKey="waitForCategorySaved"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategoryWithProducts"/> <actionGroup ref="AssertAdminCategorySaveSuccessMessageActionGroup" stepKey="seeSuccessMessage"/> <!--Go to Storefront > category--> diff --git a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml index 0e21f9e42c995..4b1750eba9f19 100644 --- a/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml +++ b/app/code/Magento/CatalogWidget/view/adminhtml/templates/product/widget/conditions.phtml @@ -5,6 +5,7 @@ */ /** @var \Magento\CatalogWidget\Block\Product\Widget\Conditions $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $element = $block->getElement(); $fieldId = $element->getHtmlContainerId() ? ' id="' . $block->escapeHtmlAttr($element->getHtmlContainerId()) . '"' : ''; @@ -25,12 +26,14 @@ $fieldAttributes = $fieldId . ' class="' . $fieldClass . '" ' </div> </div> - -<script> +<?php $scriptString = <<<script require([ "Magento_Rule/rules", "prototype" ], function(VarienRulesForm){ - window.<?= $block->escapeJs($block->getHtmlId()) ?> = new VarienRulesForm('<?= $block->escapeJs($block->getHtmlId()) ?>', '<?= $block->escapeUrl($block->getNewChildUrl()) ?>'); + window.{$block->escapeJs($block->getHtmlId())} = new VarienRulesForm('{$block->escapeJs($block->getHtmlId())}', + '{$block->escapeUrl($block->getNewChildUrl())}'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php b/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php index d97492f31a79d..22d4fdf502f7c 100644 --- a/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php +++ b/app/code/Magento/Checkout/Api/AgreementsValidatorInterface.php @@ -8,6 +8,7 @@ /** * Interface AgreementsValidatorInterface * @api + * @since 100.0.2 */ interface AgreementsValidatorInterface { diff --git a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php index cad1c100c7e5b..361c50cdcfe86 100644 --- a/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php +++ b/app/code/Magento/Checkout/Api/Data/PaymentDetailsInterface.php @@ -8,6 +8,7 @@ /** * Interface PaymentDetailsInterface * @api + * @since 100.0.2 */ interface PaymentDetailsInterface extends \Magento\Framework\Api\ExtensibleDataInterface { diff --git a/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php b/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php index e4032066a6f10..188d2987e5daf 100644 --- a/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php +++ b/app/code/Magento/Checkout/Api/Data/ShippingInformationInterface.php @@ -8,6 +8,7 @@ /** * Interface ShippingInformationInterface * @api + * @since 100.0.2 */ interface ShippingInformationInterface extends \Magento\Framework\Api\CustomAttributesDataInterface { diff --git a/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php b/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php index a9dd05856b72f..c8234bb560cba 100644 --- a/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php +++ b/app/code/Magento/Checkout/Api/Data/TotalsInformationInterface.php @@ -8,6 +8,7 @@ /** * Interface TotalsInformationInterface * @api + * @since 100.0.2 */ interface TotalsInformationInterface extends \Magento\Framework\Api\CustomAttributesDataInterface { diff --git a/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php b/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php index 63296081ab97c..80c2bb7752b73 100644 --- a/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/GuestPaymentInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing guest payment information * @api + * @since 100.0.2 */ interface GuestPaymentInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php b/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php index def7442ad4672..6ac5ec9442b6f 100644 --- a/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/GuestShippingInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing guest shipping address information * @api + * @since 100.0.2 */ interface GuestShippingInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php b/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php index d2d7dfad609cb..c98d193534d36 100644 --- a/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/GuestTotalsInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for guest quote totals calculation * @api + * @since 100.0.2 */ interface GuestTotalsInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php b/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php index f80deca1acc5a..b025dc4c7c4a4 100644 --- a/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/PaymentInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing quote payment information * @api + * @since 100.0.2 */ interface PaymentInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php b/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php index 0d22e1485c099..ee8fb42a581c0 100644 --- a/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/ShippingInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for managing customer shipping address information * @api + * @since 100.0.2 */ interface ShippingInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php b/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php index 60fd254eb199e..f3ecf957f3e06 100644 --- a/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php +++ b/app/code/Magento/Checkout/Api/TotalsInformationManagementInterface.php @@ -8,6 +8,7 @@ /** * Interface for quote totals calculation * @api + * @since 100.0.2 */ interface TotalsInformationManagementInterface { diff --git a/app/code/Magento/Checkout/Block/Cart.php b/app/code/Magento/Checkout/Block/Cart.php index 7940c37917624..76bf917f02d8b 100644 --- a/app/code/Magento/Checkout/Block/Cart.php +++ b/app/code/Magento/Checkout/Block/Cart.php @@ -11,6 +11,7 @@ * Shopping cart block * * @api + * @since 100.0.2 */ class Cart extends \Magento\Checkout\Block\Cart\AbstractCart { @@ -239,7 +240,7 @@ public function getItemsCount() * Render pagination HTML * * @return string - * @since 100.2.0 + * @since 100.1.7 */ public function getPagerHtml() { diff --git a/app/code/Magento/Checkout/Block/Cart/Additional/Info.php b/app/code/Magento/Checkout/Block/Cart/Additional/Info.php index 196992cbaf9c8..9bf8c8c8e9b51 100644 --- a/app/code/Magento/Checkout/Block/Cart/Additional/Info.php +++ b/app/code/Magento/Checkout/Block/Cart/Additional/Info.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class Info extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Cart/Coupon.php b/app/code/Magento/Checkout/Block/Cart/Coupon.php index acf3c0922f3c9..98707e7e7c694 100644 --- a/app/code/Magento/Checkout/Block/Cart/Coupon.php +++ b/app/code/Magento/Checkout/Block/Cart/Coupon.php @@ -11,6 +11,7 @@ * Block with apply-coupon form. * * @api + * @since 100.0.2 */ class Coupon extends \Magento\Checkout\Block\Cart\AbstractCart { @@ -44,6 +45,7 @@ public function getCouponCode() /** * @inheritDoc + * @since 100.3.2 */ protected function _prepareLayout() { diff --git a/app/code/Magento/Checkout/Block/Cart/Crosssell.php b/app/code/Magento/Checkout/Block/Cart/Crosssell.php index 99408003b981b..07b95c0769f3a 100644 --- a/app/code/Magento/Checkout/Block/Cart/Crosssell.php +++ b/app/code/Magento/Checkout/Block/Cart/Crosssell.php @@ -25,6 +25,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Crosssell extends AbstractProduct diff --git a/app/code/Magento/Checkout/Block/Cart/Grid.php b/app/code/Magento/Checkout/Block/Cart/Grid.php index bfe4b6ceed9d0..db5d90ecddc16 100644 --- a/app/code/Magento/Checkout/Block/Cart/Grid.php +++ b/app/code/Magento/Checkout/Block/Cart/Grid.php @@ -13,7 +13,7 @@ * custom_items weren't set to cart block * * @api - * @since 100.2.0 + * @since 100.1.7 */ class Grid extends \Magento\Checkout\Block\Cart { @@ -56,7 +56,6 @@ class Grid extends \Magento\Checkout\Block\Cart * @param \Magento\Quote\Model\ResourceModel\Quote\Item\CollectionFactory $itemCollectionFactory * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $joinProcessor * @param array $data - * @since 100.2.0 */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -89,7 +88,7 @@ public function __construct( * Configuration path is Store->Configuration->Sales->Checkout->Shopping Cart->Number of items to display pager * * @return void - * @since 100.2.0 + * @since 100.1.7 */ protected function _construct() { @@ -103,7 +102,7 @@ protected function _construct() /** * {@inheritdoc} - * @since 100.2.0 + * @since 100.1.7 */ protected function _prepareLayout() { @@ -128,7 +127,7 @@ protected function _prepareLayout() * Prepare quote items collection for pager * * @return \Magento\Quote\Model\ResourceModel\Quote\Item\Collection - * @since 100.2.0 + * @since 100.1.7 */ public function getItemsForGrid() { @@ -147,7 +146,7 @@ public function getItemsForGrid() /** * {@inheritdoc} - * @since 100.2.0 + * @since 100.1.7 */ public function getItems() { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Configure.php b/app/code/Magento/Checkout/Block/Cart/Item/Configure.php index 086518a312f71..c5c3af1d3c8c9 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Configure.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Configure.php @@ -11,6 +11,7 @@ * * @api * @module Checkout + * @since 100.0.2 */ class Configure extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php index c99c9041941b1..830191bd13c40 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer.php @@ -24,6 +24,7 @@ * @method \Magento\Checkout\Block\Cart\Item\Renderer setDeleteUrl(string) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @since 100.0.2 */ class Renderer extends \Magento\Framework\View\Element\Template implements \Magento\Framework\DataObject\IdentityInterface diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php index 3be4f76d8d67e..b2d4ef28347a5 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Actions extends Text { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php index 4542f19c4670a..fd34cdc4314f5 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Edit.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Edit extends Generic { diff --git a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php index d50eeb1b0a263..b52c7dc4c2131 100644 --- a/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php +++ b/app/code/Magento/Checkout/Block/Cart/Item/Renderer/Actions/Remove.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class Remove extends Generic { diff --git a/app/code/Magento/Checkout/Block/Cart/Shipping.php b/app/code/Magento/Checkout/Block/Cart/Shipping.php index 712ee84afd232..749f64ed83a65 100644 --- a/app/code/Magento/Checkout/Block/Cart/Shipping.php +++ b/app/code/Magento/Checkout/Block/Cart/Shipping.php @@ -20,6 +20,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Shipping extends \Magento\Checkout\Block\Cart\AbstractCart { diff --git a/app/code/Magento/Checkout/Block/Cart/Sidebar.php b/app/code/Magento/Checkout/Block/Cart/Sidebar.php index 147782e501ae4..51f25a41971b1 100644 --- a/app/code/Magento/Checkout/Block/Cart/Sidebar.php +++ b/app/code/Magento/Checkout/Block/Cart/Sidebar.php @@ -11,6 +11,7 @@ * Cart sidebar block * * @api + * @since 100.0.2 */ class Sidebar extends AbstractCart { diff --git a/app/code/Magento/Checkout/Block/Cart/Totals.php b/app/code/Magento/Checkout/Block/Cart/Totals.php index 131e5b157c77a..a0ca67f52d73f 100644 --- a/app/code/Magento/Checkout/Block/Cart/Totals.php +++ b/app/code/Magento/Checkout/Block/Cart/Totals.php @@ -14,6 +14,7 @@ * Totals cart block. * * @api + * @since 100.0.2 */ class Totals extends \Magento\Checkout\Block\Cart\AbstractCart { diff --git a/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php b/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php index 0ec2982b83c01..1429eeb04995d 100644 --- a/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php +++ b/app/code/Magento/Checkout/Block/Cart/ValidationMessages.php @@ -12,6 +12,7 @@ * Shopping cart validation messages block * * @api + * @since 100.0.2 */ class ValidationMessages extends \Magento\Framework\View\Element\Messages { diff --git a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php index 0e7931146b4c4..a566d1f606ba0 100644 --- a/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php +++ b/app/code/Magento/Checkout/Block/Checkout/AttributeMerger.php @@ -412,7 +412,7 @@ protected function getFieldOptions($attributeCode, array $attributeConfig) * * @param array $countryOptions * @return array - * @deprecated 100.2.0 + * @deprecated 100.1.7 */ protected function orderCountryOptions(array $countryOptions) { diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php index 16450ec6ff2c2..4c84ea5ae764f 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessor.php @@ -351,6 +351,9 @@ private function getBillingAddressComponent($paymentCode, $elements) ], ], 'telephone' => [ + 'validation' => [ + 'validate-phoneStrict' => 0, + ], 'config' => [ 'tooltip' => [ 'description' => __('For delivery questions.'), diff --git a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php index ad14f2a45426d..31a744c7d4d48 100644 --- a/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php +++ b/app/code/Magento/Checkout/Block/Checkout/LayoutProcessorInterface.php @@ -13,6 +13,7 @@ * @see \Magento\Checkout\Block\Onepage * * @api + * @since 100.0.2 */ interface LayoutProcessorInterface { diff --git a/app/code/Magento/Checkout/Block/Item/Price/Renderer.php b/app/code/Magento/Checkout/Block/Item/Price/Renderer.php index 2210b1cd9243e..b0f5a6b51a158 100644 --- a/app/code/Magento/Checkout/Block/Item/Price/Renderer.php +++ b/app/code/Magento/Checkout/Block/Item/Price/Renderer.php @@ -12,6 +12,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Renderer extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage.php b/app/code/Magento/Checkout/Block/Onepage.php index e01d5835b4cf0..c335b3909d5fd 100644 --- a/app/code/Magento/Checkout/Block/Onepage.php +++ b/app/code/Magento/Checkout/Block/Onepage.php @@ -9,6 +9,7 @@ * Onepage checkout block * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Onepage extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage/Failure.php b/app/code/Magento/Checkout/Block/Onepage/Failure.php index 46e56d24a7fa0..70f445173567a 100644 --- a/app/code/Magento/Checkout/Block/Onepage/Failure.php +++ b/app/code/Magento/Checkout/Block/Onepage/Failure.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Failure extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage/Link.php b/app/code/Magento/Checkout/Block/Onepage/Link.php index b8f3926baa58a..de26fe68287de 100644 --- a/app/code/Magento/Checkout/Block/Onepage/Link.php +++ b/app/code/Magento/Checkout/Block/Onepage/Link.php @@ -10,6 +10,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Link extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Onepage/Success.php b/app/code/Magento/Checkout/Block/Onepage/Success.php index e7cfaf68cc789..f8e286ca14bc8 100644 --- a/app/code/Magento/Checkout/Block/Onepage/Success.php +++ b/app/code/Magento/Checkout/Block/Onepage/Success.php @@ -12,6 +12,7 @@ * One page checkout success page * * @api + * @since 100.0.2 */ class Success extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php index 3b2f1604fae44..27910277617dd 100644 --- a/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php +++ b/app/code/Magento/Checkout/Block/QuoteShortcutButtons.php @@ -11,6 +11,7 @@ * Displays buttons on shopping cart page * * @api + * @since 100.0.2 */ class QuoteShortcutButtons extends \Magento\Catalog\Block\ShortcutButtons { diff --git a/app/code/Magento/Checkout/Block/Registration.php b/app/code/Magento/Checkout/Block/Registration.php index e880230f50a74..75bc3fa467ad6 100644 --- a/app/code/Magento/Checkout/Block/Registration.php +++ b/app/code/Magento/Checkout/Block/Registration.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class Registration extends \Magento\Framework\View\Element\Template { diff --git a/app/code/Magento/Checkout/Block/Total/DefaultTotal.php b/app/code/Magento/Checkout/Block/Total/DefaultTotal.php index ef113ad73fcc1..a351d73005fe7 100644 --- a/app/code/Magento/Checkout/Block/Total/DefaultTotal.php +++ b/app/code/Magento/Checkout/Block/Total/DefaultTotal.php @@ -5,6 +5,10 @@ */ namespace Magento\Checkout\Block\Total; +use Magento\Framework\App\ObjectManager; +use Magento\Sales\Model\ConfigInterface; +use Magento\Checkout\Helper\Data as CheckoutHelper; + /** * Default Total Row Renderer */ @@ -21,11 +25,32 @@ class DefaultTotal extends \Magento\Checkout\Block\Cart\Totals protected $_store; /** - * @return void + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param ConfigInterface $salesConfig + * @param array $layoutProcessors + * @param array $data + * @param CheckoutHelper $checkoutHelper */ - protected function _construct() - { - parent::_construct(); + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Model\Session $customerSession, + \Magento\Checkout\Model\Session $checkoutSession, + ConfigInterface $salesConfig, + array $layoutProcessors = [], + array $data = [], + ?CheckoutHelper $checkoutHelper = null + ) { + $data['checkoutHelper'] = $checkoutHelper ?? ObjectManager::getInstance()->get(CheckoutHelper::class); + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $salesConfig, + $layoutProcessors, + $data + ); $this->_store = $this->_storeManager->getStore(); } @@ -40,6 +65,8 @@ public function getStyle() } /** + * Set Total value. + * * @param float $total * @return $this */ @@ -53,6 +80,8 @@ public function setTotal($total) } /** + * Return store. + * * @return \Magento\Store\Model\Store */ public function getStore() diff --git a/app/code/Magento/Checkout/Controller/Account/Create.php b/app/code/Magento/Checkout/Controller/Account/Create.php index dae0bb98be453..21706186d803d 100644 --- a/app/code/Magento/Checkout/Controller/Account/Create.php +++ b/app/code/Magento/Checkout/Controller/Account/Create.php @@ -10,7 +10,7 @@ use Magento\Framework\Exception\NoSuchEntityException; /** - * @deprecated + * @deprecated 100.2.5 * @see DelegateCreate */ class Create extends \Magento\Framework\App\Action\Action diff --git a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php index 7eb9362031258..66be0c483ed72 100644 --- a/app/code/Magento/Checkout/Controller/Cart/Addgroup.php +++ b/app/code/Magento/Checkout/Controller/Cart/Addgroup.php @@ -104,13 +104,25 @@ private function addOrderItem(Item $item) if ($orderCustomerId == $currentCustomerId) { $this->cart->addOrderItem($item, 1); if (!$this->cart->getQuote()->getHasError()) { - $message = __( - 'You added %1 to your shopping cart.', - $this->escaper->escapeHtml($item->getName()) + $this->messageManager->addComplexSuccessMessage( + 'addCartSuccessMessage', + [ + 'product_name' => $item->getName(), + 'cart_url' => $this->getCartUrl() + ] ); - $this->messageManager->addSuccessMessage($message); } } } } + + /** + * Returns cart url + * + * @return string + */ + private function getCartUrl() + { + return $this->_url->getUrl('checkout/cart', ['_secure' => true]); + } } diff --git a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php index f50d8843a5f9d..239fdce499ffe 100644 --- a/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php +++ b/app/code/Magento/Checkout/Controller/Sidebar/RemoveItem.php @@ -19,6 +19,9 @@ use Magento\Framework\Exception\LocalizedException; use Psr\Log\LoggerInterface; +/** + * Controller for removing quote item from shopping cart. + */ class RemoveItem extends Action implements HttpPostActionInterface { /** @@ -96,6 +99,9 @@ public function execute() $this->sidebar->removeQuoteItem($itemId); } catch (LocalizedException $e) { $error = $e->getMessage(); + } catch (\Zend_Db_Exception $e) { + $this->logger->critical($e); + $error = __('An unspecified error occurred. Please contact us for assistance.'); } catch (Exception $e) { $this->logger->critical($e); $error = $e->getMessage(); diff --git a/app/code/Magento/Checkout/CustomerData/AbstractItem.php b/app/code/Magento/Checkout/CustomerData/AbstractItem.php index 9c2e3a32ef901..e5ed511924a7b 100644 --- a/app/code/Magento/Checkout/CustomerData/AbstractItem.php +++ b/app/code/Magento/Checkout/CustomerData/AbstractItem.php @@ -12,6 +12,7 @@ * Abstract item * * @api + * @since 100.0.2 */ abstract class AbstractItem implements ItemInterface { diff --git a/app/code/Magento/Checkout/CustomerData/ItemInterface.php b/app/code/Magento/Checkout/CustomerData/ItemInterface.php index fb8bd831f1ccd..fc8d954387b89 100644 --- a/app/code/Magento/Checkout/CustomerData/ItemInterface.php +++ b/app/code/Magento/Checkout/CustomerData/ItemInterface.php @@ -12,6 +12,7 @@ * Item interface * * @api + * @since 100.0.2 */ interface ItemInterface { diff --git a/app/code/Magento/Checkout/Exception.php b/app/code/Magento/Checkout/Exception.php index 4957e6be9b5da..6297041e065aa 100644 --- a/app/code/Magento/Checkout/Exception.php +++ b/app/code/Magento/Checkout/Exception.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Exception extends \Magento\Framework\Exception\LocalizedException { diff --git a/app/code/Magento/Checkout/Model/Cart.php b/app/code/Magento/Checkout/Model/Cart.php index cec99909dc999..3c1a70ef7a3d6 100644 --- a/app/code/Magento/Checkout/Model/Cart.php +++ b/app/code/Magento/Checkout/Model/Cart.php @@ -18,8 +18,10 @@ * @api * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @deprecated 100.1.0 Use \Magento\Quote\Model\Quote instead * @see \Magento\Quote\Api\Data\CartInterface + * @since 100.0.2 */ class Cart extends DataObject implements CartInterface { @@ -272,6 +274,10 @@ public function addOrderItem($orderItem, $qtyFlag = null) * with the same id may have different sets of order attributes. */ $product = $this->productRepository->getById($orderItem->getProductId(), false, $storeId, true); + if ($orderItem->getOrderId() !== null) { + //reorder existing order + $product->setSkipCheckRequiredOption(true); + } } catch (NoSuchEntityException $e) { return $this; } @@ -282,7 +288,14 @@ public function addOrderItem($orderItem, $qtyFlag = null) } else { $info->setQty(1); } - + $productOptions = $orderItem->getProductOptions(); + if ($productOptions !== null && !empty($productOptions['options'])) { + $formattedOptions = []; + foreach ($productOptions['options'] as $option) { + $formattedOptions[$option['option_id']] = $option['option_value']; + } + $info->setData('options', $formattedOptions); + } $this->addProduct($product, $info); } return $this; @@ -291,8 +304,8 @@ public function addOrderItem($orderItem, $qtyFlag = null) /** * Get product object based on requested product information * - * @param Product|int|string $productInfo - * @return Product + * @param Product|int|string $productInfo + * @return Product * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getProduct($productInfo) @@ -332,8 +345,8 @@ protected function _getProduct($productInfo) /** * Get request for product add to cart procedure * - * @param \Magento\Framework\DataObject|int|array $requestInfo - * @return \Magento\Framework\DataObject + * @param \Magento\Framework\DataObject|int|array $requestInfo + * @return \Magento\Framework\DataObject * @throws \Magento\Framework\Exception\LocalizedException */ protected function _getProductRequest($requestInfo) diff --git a/app/code/Magento/Checkout/Model/Cart/CartInterface.php b/app/code/Magento/Checkout/Model/Cart/CartInterface.php index 40aff1980e787..d8264e5535497 100644 --- a/app/code/Magento/Checkout/Model/Cart/CartInterface.php +++ b/app/code/Magento/Checkout/Model/Cart/CartInterface.php @@ -14,6 +14,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @deprecated 100.1.0 Use \Magento\Quote\Api\Data\CartInterface instead * @see \Magento\Quote\Api\Data\CartInterface + * @since 100.0.2 */ interface CartInterface { diff --git a/app/code/Magento/Checkout/Model/Cart/ImageProvider.php b/app/code/Magento/Checkout/Model/Cart/ImageProvider.php index cdadf3573c8ec..bc409357bf409 100644 --- a/app/code/Magento/Checkout/Model/Cart/ImageProvider.php +++ b/app/code/Magento/Checkout/Model/Cart/ImageProvider.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class ImageProvider { @@ -20,12 +21,15 @@ class ImageProvider /** * @var \Magento\Checkout\CustomerData\ItemPoolInterface - * @deprecated No need for the pool as images are resolved in the default item implementation + * @deprecated 100.2.7 No need for the pool as images are resolved in the default item implementation * @see \Magento\Checkout\CustomerData\DefaultItem::getProductForThumbnail */ protected $itemPool; - /** @var \Magento\Checkout\CustomerData\DefaultItem */ + /** + * @var \Magento\Checkout\CustomerData\DefaultItem + * @since 100.2.7 + */ protected $customerDataItem; /** diff --git a/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php index f38e15dd628fd..ee68ef9d275b1 100644 --- a/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php +++ b/app/code/Magento/Checkout/Model/Cart/RequestInfoFilterComposite.php @@ -20,7 +20,6 @@ class RequestInfoFilterComposite implements RequestInfoFilterInterface /** * @param RequestInfoFilter[] $filters - * @since 100.1.2 */ public function __construct( $filters = [] diff --git a/app/code/Magento/Checkout/Model/CompositeConfigProvider.php b/app/code/Magento/Checkout/Model/CompositeConfigProvider.php index 3577b1a145403..7c6d04f2947a0 100644 --- a/app/code/Magento/Checkout/Model/CompositeConfigProvider.php +++ b/app/code/Magento/Checkout/Model/CompositeConfigProvider.php @@ -10,6 +10,7 @@ * * @see \Magento\Checkout\Model\ConfigProviderInterface * @api + * @since 100.0.2 */ class CompositeConfigProvider implements ConfigProviderInterface { diff --git a/app/code/Magento/Checkout/Model/ConfigProviderInterface.php b/app/code/Magento/Checkout/Model/ConfigProviderInterface.php index 9e15027e26927..58bbc02485642 100644 --- a/app/code/Magento/Checkout/Model/ConfigProviderInterface.php +++ b/app/code/Magento/Checkout/Model/ConfigProviderInterface.php @@ -8,6 +8,7 @@ /** * Interface ConfigProviderInterface * @api + * @since 100.0.2 */ interface ConfigProviderInterface { diff --git a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php index 1d15a5dd7f176..8b8d2602fbfc7 100644 --- a/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/GuestPaymentInformationManagement.php @@ -152,7 +152,7 @@ public function getPaymentInformation($cartId) * Get logger instance * * @return \Psr\Log\LoggerInterface - * @deprecated 100.2.0 + * @deprecated 100.1.8 */ private function getLogger() { diff --git a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php index ca577ed714a6e..a670482cb98d6 100644 --- a/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php +++ b/app/code/Magento/Checkout/Model/Layout/AbstractTotalsProcessor.php @@ -17,6 +17,7 @@ * * phpcs:disable Magento2.Classes.AbstractApi * @api + * @since 100.0.2 */ abstract class AbstractTotalsProcessor { diff --git a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php index 1f7931d7d3e6a..2f68aba5ec6ae 100644 --- a/app/code/Magento/Checkout/Model/PaymentInformationManagement.php +++ b/app/code/Magento/Checkout/Model/PaymentInformationManagement.php @@ -17,7 +17,7 @@ class PaymentInformationManagement implements \Magento\Checkout\Api\PaymentInfor { /** * @var \Magento\Quote\Api\BillingAddressManagementInterface - * @deprecated 100.2.0 This call was substituted to eliminate extra quote::save call + * @deprecated 100.1.0 This call was substituted to eliminate extra quote::save call */ protected $billingAddressManagement; @@ -152,7 +152,7 @@ public function getPaymentInformation($cartId) * Get logger instance * * @return \Psr\Log\LoggerInterface - * @deprecated 100.2.0 + * @deprecated 100.1.8 */ private function getLogger() { diff --git a/app/code/Magento/Checkout/Model/Session.php b/app/code/Magento/Checkout/Model/Session.php index 7af00f1df8e95..618f745e77105 100644 --- a/app/code/Magento/Checkout/Model/Session.php +++ b/app/code/Magento/Checkout/Model/Session.php @@ -20,6 +20,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) * @SuppressWarnings(PHPMD.TooManyFields) + * @since 100.0.2 */ class Session extends \Magento\Framework\Session\SessionManager { diff --git a/app/code/Magento/Checkout/Model/Session/SuccessValidator.php b/app/code/Magento/Checkout/Model/Session/SuccessValidator.php index 5858dcba8b902..6bfab606445fb 100644 --- a/app/code/Magento/Checkout/Model/Session/SuccessValidator.php +++ b/app/code/Magento/Checkout/Model/Session/SuccessValidator.php @@ -9,6 +9,7 @@ * Test if checkout session valid for success action * * @api + * @since 100.0.2 */ class SuccessValidator { diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml new file mode 100644 index 0000000000000..4e76e3113bdb8 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminOpenSalesCheckoutConfigPageActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenSalesCheckoutConfigPageActionGroup"> + <annotations> + <description>Goes to the Store Configuration > Sales > Checkout configuration page in admin.</description> + </annotations> + <arguments> + <argument name="tabGroupAnchor" type="string" defaultValue=""/> + </arguments> + <amOnPage url="{{AdminCheckoutConfigPage.url(tabGroupAnchor)}}" stepKey="openCheckoutConfigPage"/> + <waitForPageLoad stepKey="waitForCheckoutConfigPageLoad"/> + </actionGroup> +</actionGroups> + diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml new file mode 100644 index 0000000000000..7d9cc0ca90d4e --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AdminSelectClearShoppingCartConfigurationActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectClearShoppingCartConfigurationActionGroup"> + <annotations> + <description>Enable/Disable clear shopping cart store configuration using UI.</description> + </annotations> + <arguments> + <argument name="value" type="string" defaultValue="{{EnableClearShoppingCart.textValue}}"/> + </arguments> + <waitForElementVisible selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabledInherit}}" stepKey="waitForClearShoppingCartEnabledInherit" /> + <uncheckOption selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabledInherit}}" stepKey="uncheckUseSystem" /> + <waitForElementVisible selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabled}}" stepKey="waitForClearShoppingCartEnabled" /> + <selectOption selector="{{AdminCheckoutConfigSection.clearShoppingCartEnabled}}" userInput="{{value}}" stepKey="fillClearShoppingCartEnabled" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml index c3f3865ef4549..c81540382c86f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefronElementVisibleActionGroup.xml @@ -16,7 +16,8 @@ <argument name="selector" type="string"/> <argument name="userInput" type="string"/> </arguments> - + + <waitForElementVisible selector="{{selector}}" time="60" stepKey="waitForElementVisible"/> <see selector="{{selector}}" userInput="{{userInput}}" stepKey="assertElement"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml index e2d4fd2e89c2f..daa27b9918e47 100644 --- a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/AssertStorefrontCheckoutCartItemsActionGroup.xml @@ -19,6 +19,7 @@ <argument name="qty" type="string"/> </arguments> + <waitForElementVisible selector="{{CheckoutCartProductSection.productName}}" time="60" stepKey="waitForProductNameVisible"/> <see selector="{{CheckoutCartProductSection.productName}}" userInput="{{productName}}" stepKey="seeProductNameInCheckoutSummary"/> <see selector="{{CheckoutCartProductSection.ProductPriceByName(productName)}}" userInput="{{productPrice}}" stepKey="seeProductPriceInCart"/> <see selector="{{CheckoutCartProductSection.productSubtotalByName(productName)}}" userInput="{{subtotal}}" stepKey="seeSubtotalPrice"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml new file mode 100644 index 0000000000000..2582cba5a6871 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/ActionGroup/StorefrontClearShoppingCartActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontClearShoppingCartActionGroup"> + <annotations> + <description>Clicks the Clear Shopping Cart button on the storefront on the shopping cart page and verifies shopping cart gets emptied.</description> + </annotations> + + <waitForElementVisible selector="{{CheckoutCartProductSection.emptyCartButton}}" stepKey="waitForEmptyCartButton"/> + <click selector="{{CheckoutCartProductSection.emptyCartButton}}" stepKey="clickEmptyCartButton"/> + <waitForElementVisible selector="{{CheckoutCartProductSection.modalMessage}}" stepKey="waitForModalMessage"/> + <waitForText selector="{{CheckoutCartProductSection.modalMessage}}" userInput="Are you sure you want to remove all items from your shopping cart?" stepKey="waitForTextModalMessage"/> + <waitForElementVisible selector="{{CheckoutCartProductSection.modalConfirmButton}}" stepKey="waitForModalConfirmButton"/> + <click selector="{{CheckoutCartProductSection.modalConfirmButton}}" stepKey="clickModalConfirmButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeCurrentUrlEquals url="{{_ENV.MAGENTO_BASE_URL}}checkout/cart" stepKey="seeCurrentUrlEqualsCartPage"/> + <waitForText selector="{{CheckoutCartMessageSection.emptyCartMessage}}" userInput="You have no items in your shopping cart." stepKey="waitForEmptyCartMessage"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml index bb47a2fcc3070..9ab8a64c9ab88 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Data/ConfigData.xml @@ -100,4 +100,17 @@ <data key="label">Display number of items in cart</data> <data key="value">0</data> </entity> + + <entity name="EnableClearShoppingCart"> + <data key="path">checkout/cart/enable_clear_shopping_cart</data> + <data key="label">Display clear shopping cart button on the cart page</data> + <data key="value">1</data> + <data key="textValue">Yes</data> + </entity> + <entity name="DisableClearShoppingCart"> + <data key="path">checkout/cart/enable_clear_shopping_cart</data> + <data key="label">Do not display clear shopping cart button on the cart page</data> + <data key="value">0</data> + <data key="textValue">No</data> + </entity> </entities> diff --git a/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml b/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml new file mode 100644 index 0000000000000..21d69a1ad93c7 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Page/AdminCheckoutConfigPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminCheckoutConfigPage" url="admin/system_config/edit/section/checkout/{{tabLink}}" area="admin" parameterized="true" module="Magento_Checkout"> + <section name="AdminCheckoutConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml new file mode 100644 index 0000000000000..72cba8349ec0b --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Section/AdminCheckoutConfigSection.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCheckoutConfigSection"> + <element name="clearShoppingCartEnabled" type="select" selector="#checkout_cart_enable_clear_shopping_cart" timeout="30"/> + <element name="clearShoppingCartEnabledInherit" type="select" selector="#checkout_cart_enable_clear_shopping_cart_inherit" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml index af9d81249e8ac..84f9a7930d40b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/CheckoutCartProductSection.xml @@ -48,6 +48,9 @@ <element name="checkoutCartProductPrice" type="text" selector="//td[@class='col price']//span[@class='price']"/> <element name="checkoutCartSubtotal" type="text" selector="//td[@class='col subtotal']//span[@class='price']"/> <element name="emptyCart" selector=".cart-empty" type="text"/> + <element name="emptyCartButton" type="button" selector="#empty_cart_button" timeout="30"/> + <element name="modalMessage" type="text" selector=".modal-popup.confirm._show .modal-content" timeout="30"/> + <element name="modalConfirmButton" type="button" selector=".modal-popup.confirm._show .action-accept" timeout="30"/> <!-- Required attention section --> <element name="removeProductBySku" type="button" selector="//div[contains(., '{{sku}}')]/ancestor::tbody//button" parameterized="true" timeout="30"/> <element name="failedItemBySku" type="block" selector="//div[contains(.,'{{sku}}')]/ancestor::tbody" parameterized="true" timeout="30"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index 29a1b72947c06..3f3d9faf3f17d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Section/StorefrontProductInfoMainSection.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="StorefrontProductInfoMainSection"> - <element name="AddToCart" type="button" selector="#product-addtocart-button"/> - <element name="updateCart" type="button" selector="#product-updatecart-button" timeout="30"/> + <element name="AddToCart" type="button" selector="button#product-addtocart-button"/> + <element name="updateCart" type="button" selector="button#product-updatecart-button" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml index 52a69307550c5..7eae5d0d292d1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldForUKCustomerRemainOptionAfterRefreshTest.xml @@ -55,8 +55,7 @@ <fillField selector="{{CheckoutShippingSection.telephone}}" userInput="{{UK_Address.telephone}}" stepKey="enterTelephone"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask"/> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml index dd454d7aca10b..12e1a6e9872d3 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AddressStateFieldShouldNotAcceptJustIntegerValuesTest.xml @@ -23,8 +23,12 @@ <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml index ab0453e1faa18..a1065daedd4f8 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/AdminCheckConfigsChangesIsNotAffectedStartedCheckoutProcessTest.xml @@ -84,7 +84,9 @@ <actionGroup ref="AdminChangeFlatRateShippingMethodStatusActionGroup" stepKey="enableFlatRateShippingStatus"/> <!-- Flush cache --> - <magentoCLI command="cache:flush" stepKey="cacheFlush"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Back to the Checkout and refresh the page --> <switchToPreviousTab stepKey="switchToPreviousTab"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml new file mode 100644 index 0000000000000..92a4b9563ab3d --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ClearShoppingCartEnableDisableConfigurationTest.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="ClearShoppingCartEnableDisableConfigurationTest"> + <annotations> + <features value="Checkout"/> + <stories value="Shopping Cart"/> + <title value="Enable and Disable Clear Shopping Cart Configuration"/> + <description value="Verify that disabling the clear shopping cart store configuration will remove the clear shopping cart configuration button from the storefront's shopping cart page. Verify that enabling the configuration will add the button to the page and that the button functions as expected"/> + <group value="shoppingCart"/> + <severity value="MAJOR"/> + </annotations> + <before> + <!-- Create simple products and category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + + <!-- Disable clear shopping cart --> + <magentoCLI command="config:set {{DisableClearShoppingCart.path}} {{DisableClearShoppingCart.value}}" stepKey="disableClearShoppingCart"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Navigate to sales checkout cart configuration --> + <actionGroup ref="AdminOpenSalesCheckoutConfigPageActionGroup" stepKey="openSalesCheckoutCartConfig1"> + <argument name="tabGroupAnchor" value="#checkout_cart-link"/> + </actionGroup> + + <!-- Enable clear shopping cart button --> + <actionGroup ref="AdminSelectClearShoppingCartConfigurationActionGroup" stepKey="enableClearShoppingCartButton"/> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration1"/> + + <!-- Open product 1 and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct1Page1"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product1AddToCart"/> + + <!-- Open product 2 and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct2Page"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product2AddToCart"/> + + <!-- Go to shopping cart page --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage1"/> + + <!-- Clear shopping cart --> + <actionGroup ref="StorefrontClearShoppingCartActionGroup" stepKey="clearShoppingCart"/> + <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="assertMiniCartEmpty"/> + + <!-- Return to Admin to disable clear shopping cart --> + <actionGroup ref="AdminOpenSalesCheckoutConfigPageActionGroup" stepKey="openSalesCheckoutCartConfig2"/> + <actionGroup ref="AdminSelectClearShoppingCartConfigurationActionGroup" stepKey="disableClearShoppingCartButton"> + <argument name="value" value="{{DisableClearShoppingCart.textValue}}"/> + </actionGroup> + <actionGroup ref="SaveStoreConfigurationActionGroup" stepKey="saveStoreConfiguration2"/> + + <!-- Open product 1 page and add to cart --> + <actionGroup ref="StorefrontOpenProductEntityPageActionGroup" stepKey="openProduct1Page2"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddToTheCartActionGroup" stepKey="product1AddToCart2"/> + + <!-- Go to shopping cart and assert clear shopping cart button is not rendered in UI --> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage2"/> + <dontSeeElementInDOM selector="{{CheckoutCartProductSection.emptyCartButton}}" stepKey="dontSeeElementEmptyCartButton"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml index 5fd201290655a..96a236336993f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleDynamicProductFromShoppingCartTest.xml @@ -38,7 +38,9 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete category --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml index 603ee1ecea4df..b64b59ef6109c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/DeleteBundleFixedProductFromShoppingCartTest.xml @@ -33,7 +33,9 @@ <requiredEntity createDataKey="createBundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Delete bundle product data --> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml index c61545e51d535..f34becdd35af1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/EditShippingAddressOnePageCheckoutTest.xml @@ -26,8 +26,12 @@ </createData> <createData entity="Simple_US_Customer_NY" stepKey="createCustomer"/> <!--Clear cache and reindex--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml index ceaf72fff83bb..313f5997e0af0 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/NoErrorCartCheckoutForProductsDeletedFromMiniCartTest.xml @@ -26,8 +26,12 @@ <field key="price">100.00</field> <requiredEntity createDataKey="createCategory"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml index 3215503b3205a..e90e1bf5a2e82 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingDefaultAddressTest.xml @@ -85,8 +85,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml index 76a998fec8adc..d867b00310761 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNewAddressTest.xml @@ -73,8 +73,7 @@ <waitForPageLoad stepKey="waitForAddressSaving"/> <!-- Click next button to open payment section --> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> - <waitForPageLoad stepKey="waitForShipmentPageLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Change the address --> <uncheckOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution"/> @@ -98,8 +97,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml index 340ff4159900a..6a211c3908059 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonDefaultAddressTest.xml @@ -86,8 +86,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest.xml new file mode 100644 index 0000000000000..92dad56e81135 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest.xml @@ -0,0 +1,116 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="OnePageCheckoutAsCustomerUsingNonExistentCustomerGroupTest"> + <annotations> + <features value="OnePageCheckout"/> + <stories value="OnePageCheckout within Offline Payment Methods"/> + <title value="OnePageCheckout as a customer with non-existent customer group assigned to the quote"/> + <description value="Checkout as a customer with non-existent customer group assigned to the quote"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-36385"/> + <group value="checkout"/> + </annotations> + <before> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createSimpleProduct"> + <field key="price">560</field> + </createData> + + <!-- Create customer group --> + <createData entity="CustomCustomerGroup" stepKey="createCustomerGroup"/> + + <!-- Create customer and assign it to the customer group created on the previous step --> + <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"> + <field key="group_id">$$createCustomerGroup.id$$</field> + </createData> + </before> + <after> + <!-- Admin log out --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Customer log out --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + + <!-- Delete created product --> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + + <!-- Delete customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Login as customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Add Simple Product to cart --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToSimpleProductPage"/> + <waitForPageLoad stepKey="waitForSimpleProductPageLoad"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + </actionGroup> + + <!-- Delete customer group --> + <deleteData createDataKey="createCustomerGroup" stepKey="deleteCustomerGroup"/> + + <!-- Go to shopping cart --> + <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="goToShoppingCartFromMinicart"/> + <actionGroup ref="FillShippingZipForm" stepKey="fillShippingZipForm"> + <argument name="address" value="US_Address_CA"/> + </actionGroup> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + + <!-- Check that error does not appear and shipping methods are available to select --> + <dontSee selector="{{CheckoutCartMessageSection.errorMessage}}" userInput="No such entity with id = $$createCustomerGroup.id$$" stepKey="assertErrorMessage"/> + <dontSee selector="{{CheckoutShippingMethodsSection.noQuotesMsg}}" userInput="Sorry, no quotes are available for this order at this time" stepKey="assertNoQuotesMessage"/> + + <!-- Fill customer address data --> + <waitForElementVisible selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="waitForShipHereVisible"/> + <!-- Change address --> + <click selector="{{CheckoutShippingSection.shipHereButton(UK_Not_Default_Address.street[0])}}" stepKey="clickShipHere"/> + + <!-- Click next button to open payment section --> + <click selector="{{CheckoutShippingGuestInfoSection.next}}" stepKey="clickNext"/> + <waitForPageLoad stepKey="waitForShipmentPageLoad"/> + + <!-- Select payment solution --> + <checkOption selector="{{CheckoutPaymentSection.billingAddressNotSameCheckbox}}" stepKey="selectPaymentSolution" /> + + <!-- Check order summary in checkout --> + <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" stepKey="waitForPaymentSectionLoaded"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="clickPlaceOrderButton"/> + <seeElement selector="{{CheckoutSuccessMainSection.success}}" stepKey="orderIsSuccessfullyPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> + + <!-- Login as admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + + <!-- Open created order in backend --> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> + <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderNumber"/> + </actionGroup> + + <!-- Assert order total --> + <scrollTo selector="{{AdminOrderTotalSection.grandTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.grandTotal}}" userInput="$565.00" stepKey="checkOrderTotalInBackend"/> + + <!-- Assert order addresses --> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeBillingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeBillingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.billingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeBillingAddressPostcode"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.street[0]}}" stepKey="seeShippingAddressStreet"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.city}}" stepKey="seeShippingAddressCity"/> + <see selector="{{AdminShipmentAddressInformationSection.shippingAddress}}" userInput="{{UK_Not_Default_Address.postcode}}" stepKey="seeShippingAddressPostcode"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml index 1c03808ac71cf..42d61abca845b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutUsingSignInLinkTest.xml @@ -79,8 +79,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order in backend --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml index e678bb0d2a87b..f9e1326e474af 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/OnePageCheckoutWithAllProductTypesTest.xml @@ -88,7 +88,9 @@ <!-- Create customer --> <createData entity="Simple_Customer_Without_Address" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> @@ -198,8 +200,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml index 571aa24209389..70faa3721efe9 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/ShoppingCartAndMiniShoppingCartPerCustomerTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <!-- Flush cache --> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Create two customers --> <createData entity="Simple_US_Customer" stepKey="createFirstCustomer"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml index a5c8eb0da6530..b65cfe0eb574f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartWithoutAnySelectedOptionTest.xml @@ -23,7 +23,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithCustomOptions"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml index bd81a1cfab604..026f33b04f69a 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontCheckCustomerInfoCreatedByGuestTest.xml @@ -12,12 +12,15 @@ <annotations> <features value="Checkout"/> <stories value="Check customer information created by guest"/> - <title value="Check Customer Information Created By Guest"/> + <title value="Deprecated. Check Customer Information Created By Guest"/> <description value="Check customer information after placing the order as the guest who created an account"/> <severity value="MAJOR"/> <testCaseId value="MAGETWO-95932"/> <useCaseId value="MAGETWO-95820"/> <group value="checkout"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest instead.</issueId> + </skip> </annotations> <before> @@ -25,7 +28,9 @@ <createData entity="_defaultProduct" stepKey="product"> <requiredEntity createDataKey="category"/> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml index af3a2e6870cd7..8d508f381c765 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StoreFrontFreeShippingRecalculationAfterCouponCodeAddedTest.xml @@ -33,7 +33,9 @@ <createData entity="FlatRateShippingMethodConfig" stepKey="enableFlatRate"/> <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> <createData entity="MinimumOrderAmount90" stepKey="minimumOrderAmount90"/> - <magentoCLI command="cache:flush" stepKey="flushCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminCreateCartPriceRuleWithCouponCodeActionGroup" stepKey="createCartPriceRule"> <argument name="ruleName" value="CatPriceRule"/> <argument name="couponCode" value="CatPriceRule.coupon_code"/> @@ -50,7 +52,10 @@ <createData entity="DefaultShippingMethodsConfig" stepKey="defaultShippingMethodsConfig"/> <createData entity="DefaultMinimumOrderAmount" stepKey="defaultMinimumOrderAmount"/> <deleteData createDataKey="createSimpleUsCustomer" stepKey="deleteCustomer"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="DeleteCartPriceRuleByName" stepKey="deleteCartPriceRule"> <argument name="ruleName" value="{{CatPriceRule.name}}"/> </actionGroup> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml index e82f3c0588835..3c090900563a5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartTest.xml @@ -9,6 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> <test name="StorefrontAddBundleDynamicProductToShoppingCartTest"> <annotations> + <features value="Checkout"/> <stories value="Shopping Cart"/> <title value="Add bundle dynamic product to the cart"/> <description value="Add bundle dynamic product to the cart"/> @@ -18,6 +19,7 @@ </annotations> <before> + <magentoCLI command="config:set {{DisableFreeShippingConfigData.path}} {{DisableFreeShippingConfigData.value}}" stepKey="disableFreeShipping"/> <magentoCLI command="config:set {{EnableFlatRateConfigData.path}} {{EnableFlatRateConfigData.value}}" stepKey="enableFlatRate"/> <magentoCLI command="config:set {{EnableFlatRateDefaultPriceConfigData.path}} {{EnableFlatRateDefaultPriceConfigData.value}}" stepKey="enableFlatRateDefaultPrice"/> <createData entity="SimpleSubCategory" stepKey="createSubCategory"/> @@ -46,19 +48,26 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCartPriceRuleDeleteAllActionGroup" stepKey="deleteAllRules"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="cataloginventory_stock"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteProduct2"/> <deleteData createDataKey="createBundleProduct" stepKey="deleteBundleProduct"/> <deleteData createDataKey="createSubCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsAdmin"/> </after> <!--Open Product page in StoreFront --> <actionGroup ref="AssertProductNameAndSkuInStorefrontProductPageByCustomAttributeUrlKeyActionGroup" stepKey="openProductPageAndVerifyProduct"> - <argument name="product" value="$$createBundleProduct$$"/> + <argument name="product" value="$createBundleProduct$"/> </actionGroup> <!--Assert Product Price Range --> @@ -93,8 +102,8 @@ <!--Assert Product items in cart --> <actionGroup ref="AssertStorefrontCheckoutCartItemsActionGroup" stepKey="assertSimpleProduct1ItemsInCheckOutCart"> - <argument name="productName" value="$$createBundleProduct.name$$"/> - <argument name="productSku" value="$$createBundleProduct.sku$$"/> + <argument name="productName" value="$createBundleProduct.name$"/> + <argument name="productSku" value="$createBundleProduct.sku$"/> <argument name="productPrice" value="$50.00"/> <argument name="subtotal" value="$100.00" /> <argument name="qty" value="2"/> @@ -107,13 +116,13 @@ </actionGroup> <actionGroup ref="AssertStorefrontElementVisibleActionGroup" stepKey="seeProductOptionInCart"> <argument name="selector" value="{{CheckoutCartProductSection.productOptionLabel}}"/> - <argument name="userInput" value="1 x $$simpleProduct2.name$$ $50.00"/> + <argument name="userInput" value="1 x $simpleProduct2.name$ $50.00"/> </actionGroup> <!-- Assert Product in Mini Cart --> <actionGroup ref="StorefrontClickOnMiniCartActionGroup" stepKey="clickOnMiniCart"/> <actionGroup ref="AssertStorefrontMiniCartItemsActionGroup" stepKey="assertSimpleProduct3MiniCart"> - <argument name="productName" value="$$createBundleProduct.name$$"/> + <argument name="productName" value="$createBundleProduct.name$"/> <argument name="productPrice" value="$50.00"/> <argument name="cartSubtotal" value="$100.00" /> <argument name="qty" value="2"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml index 5d5e2b3a91f49..edb6f8ba97b27 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddBundleDynamicProductToShoppingCartWithDisableMiniCartSidebarTest.xml @@ -49,8 +49,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> @@ -111,7 +115,9 @@ <!--Enabled Mini Cart --> <magentoCLI stepKey="enableShoppingCartSidebar" command="config:set checkout/sidebar/display 1"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <reloadPage stepKey="reloadThePage"/> <!--Click on mini cart--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml index 21e785de6cab3..146ecde047016 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddConfigurableProductToShoppingCartTest.xml @@ -110,8 +110,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct3"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml index bbc0a29000a77..4f54363bd8dc4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddDownloadableProductToShoppingCartTest.xml @@ -28,8 +28,12 @@ <createData entity="downloadableLink2" stepKey="addDownloadableLink2"> <requiredEntity createDataKey="createDownloadableProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml index 3e2f32a4ab055..13a179fe52444 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddGroupedProductToShoppingCartTest.xml @@ -43,7 +43,9 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple3"/> </updateData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml index af12aecb6345a..eff18f9081b67 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddOneBundleMultiSelectOptionToTheShoppingCartTest.xml @@ -46,8 +46,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml index e8a72b6e88109..6cf5a390a964d 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddProductWithAllTypesOfCustomOptionToTheShoppingCartTest.xml @@ -23,7 +23,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <updateData createDataKey="createProduct" entity="productWithOptions" stepKey="updateProductWithCustomOptions"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml index 265f9a7cbbc98..cd1c0542c5c5b 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontAddTwoBundleMultiSelectOptionsToTheShoppingCartTest.xml @@ -46,8 +46,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml index 0b52caa7165af..4c0484f88d549 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartAndSummaryBlockItemDisplayWithDefaultDisplayLimitationTest.xml @@ -51,7 +51,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> <field key="price">100.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml index a496ff68c0cd0..b399d76e86e2f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWhenMoreItemsAddedToTheCartThanDefaultDisplayLimitTest.xml @@ -54,8 +54,12 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct11"> <field key="price">110.00</field> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml index 8e84deafea9f2..e0aeb2f93d30c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCartItemDisplayWithDefaultDisplayLimitAndDefaultTotalQuantityTest.xml @@ -49,7 +49,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct10"> <field key="price">100.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml new file mode 100644 index 0000000000000..fa75a280e69f1 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontCheckCustomerInfoOnOrderPageCreatedByGuestTest"> + <annotations> + <features value="Checkout"/> + <stories value="Check customer information created by guest"/> + <title value="Check Customer Information Created By Guest"/> + <description value="Check customer information after placing the order as the guest who created an account"/> + <severity value="MAJOR"/> + <testCaseId value="MC-28550"/> + <useCaseId value="MAGETWO-95820"/> + <group value="checkout"/> + </annotations> + + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="_defaultProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrdersGridFilter"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> + </after> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="goToCheckoutFromMinicart"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillShippingSectionAsGuest"> + <argument name="customerVar" value="CustomerEntityOne"/> + <argument name="customerAddressVar" value="CustomerAddressSimple"/> + </actionGroup> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="placeOrder"> + <argument name="orderNumberMessage" value="CONST.successGuestCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + <grabTextFrom selector="{{CheckoutSuccessRegisterSection.orderNumber}}" stepKey="grabOrderNumber"/> + <actionGroup ref="StorefrontRegisterCustomerFromOrderSuccessPage" stepKey="createCustomerAfterPlaceOrder"> + <argument name="customer" value="CustomerEntityOne"/> + </actionGroup> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="filterOrdersGridById"> + <argument name="orderId" value="{$grabOrderNumber}"/> + </actionGroup> + <see userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminShipmentOrderInformationSection.customerName}}" stepKey="seeCustomerName"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml index 79e46d093c2f6..ce6d465408382 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckSimpleProductCartItemDisplayWithDefaultLimitationTest.xml @@ -54,7 +54,9 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct11"> <field key="price">110.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml index 9f3eacbf5f455..c1dc0b7e62ba7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckVirtualProductCountDisplayWithCustomDisplayConfigurationTest.xml @@ -34,7 +34,9 @@ <createData entity="VirtualProduct" stepKey="virtualProduct4"> <field key="price">40.00</field> </createData> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="virtualProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml index 27d4e4c207ae7..f16f577a4088c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutDisabledBundleProductTest.xml @@ -36,8 +36,12 @@ <requiredEntity createDataKey="bundleOption"/> <requiredEntity createDataKey="createSimpleProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="cacheFlush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete category --> @@ -63,7 +67,7 @@ <openNewTab stepKey="openNewTab"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <!-- Disabled bundle product from grid --> <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> <argument name="product" value="$$createBundleDynamicProduct$$"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml index 38efc9d7eca24..ca1d21c34c956 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndProductWithTierPricesTest.xml @@ -33,7 +33,9 @@ <argument name="price" value="Fixed"/> <argument name="amount" value="24.00"/> </actionGroup> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{DisablePaymentBankTransferConfigData.path}} {{DisablePaymentBankTransferConfigData.value}}" stepKey="enableGuestCheckout"/> @@ -108,8 +110,7 @@ <see selector="{{StorefrontCustomerAddressesSection.shippingAddress}}" userInput="T: {{updateCustomerUKAddress.telephone}}" stepKey="seeTelephoneInShippingAddress"/> <!--Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrderIndexPageToLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml index eda6d5f867540..23a8fd5a2c88c 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithDifferentShippingAndBillingAddressAndRegisterCustomerAfterCheckoutTest.xml @@ -77,8 +77,7 @@ <actionGroup ref="StorefrontRegisterCustomerAfterCheckoutActionGroup" stepKey="registerCustomer"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml index 0042c73b13826..566457b1f18a5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCheckoutWithSpecialPriceProductsTest.xml @@ -90,8 +90,12 @@ <click selector="{{AdminProductFormAdvancedPricingSection.save}}" stepKey="clickSaveProduct1"/> <waitForPageLoad time='60' stepKey="waitForSpecialPriceProductSaved"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSaveSuccessMessage1"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -150,8 +154,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml index a2ff149af1a87..05d24696197d2 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutDisabledProductAndCouponTest.xml @@ -35,7 +35,7 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <deleteData createDataKey="createUSCustomer" stepKey="deleteCustomer"/> <deleteData createDataKey="createSalesRule" stepKey="deleteSalesRule"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> </after> @@ -70,7 +70,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <!-- Disabled simple product from grid --> <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml index 5fbfdb5a07678..5112be119df80 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutOnLoginWhenGuestCheckoutIsDisabledTest.xml @@ -77,8 +77,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml index b5f573aba7561..e97f7f0d3e8e4 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTest.xml @@ -23,7 +23,9 @@ <requiredEntity createDataKey="createCategory"/> </createData> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml index 4c3c1561a2445..c124090e6e865 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithMultipleAddressesAndTaxRatesTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -44,21 +43,23 @@ <click stepKey="clickSave" selector="{{AdminStoresMainActionsSection.saveButton}}"/> <!--TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> <argument name="name" value="{{SimpleTaxNY.state}}-{{SimpleTaxNY.rate}}"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml index d042a15e3c958..5e5c37cc9e486 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutTest/StorefrontCustomerCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -28,8 +28,12 @@ <createData entity="Simple_US_Customer" stepKey="simpleuscustomer"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml index 0f82302260995..4672815fb1b10 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithNewCustomerRegistrationAndDisableGuestCheckoutTest.xml @@ -115,8 +115,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml index 0c762519e9083..24ca488ea25e5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontCustomerCheckoutWithoutRegionTest.xml @@ -49,7 +49,7 @@ <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckoutPage"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButton"/> <see selector="{{StorefrontMessagesSection.error}}" userInput='Please specify a regionId in shipping address.' stepKey="seeErrorMessages"/> </test> </tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml index d116d0049c9df..6e304ff9cfb50 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteBundleProductFromMiniShoppingCartTest.xml @@ -40,7 +40,9 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml index eb8b047b57288..d012d44d84052 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromMiniShoppingCartTest.xml @@ -63,8 +63,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteSimpleProduct1"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml index 8a52fa7740b95..d2bcaedb74fd1 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontDeleteDownloadableProductFromMiniShoppingCartTest.xml @@ -27,8 +27,12 @@ <createData entity="downloadableLink1" stepKey="addDownloadableLink1"> <requiredEntity createDataKey="createDownloadableProduct"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml index 0d69306a4b1ba..be5cf143f13dc 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTest.xml @@ -24,8 +24,12 @@ </createData> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml index 0520accdd4b84..7660df18407d5 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutTest/StorefrontGuestCheckoutTestWithRestrictedCountriesForPaymentTest.xml @@ -24,8 +24,12 @@ </createData> <magentoCLI stepKey="allowSpecificValue" command="config:set payment/checkmo/allowspecific 1"/> <magentoCLI stepKey="specificCountryValue" command="config:set payment/checkmo/specificcountry GB"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml index dbb695fb4fb00..1ce48bd8bf408 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutUsingFreeShippingAndTaxesTest.xml @@ -90,8 +90,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <actionGroup ref="AdminDeleteTaxRule" stepKey="deleteTaxRule"> @@ -115,8 +119,7 @@ </after> <!-- Create a Tax Rule --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> @@ -195,8 +198,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml index e9d056417330d..f910a9d47244f 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontGuestCheckoutWithCouponAndZeroSubtotalTest.xml @@ -73,8 +73,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml index 5df8338030efc..82324525bad24 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutDataWhenChangeQtyTest.xml @@ -58,9 +58,7 @@ <!--Select shipping method and finalize checkout--> <click selector="{{CheckoutShippingSection.firstShippingMethod}}" stepKey="selectFirstShippingMethod"/> - <waitForElement selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> - <waitForPageLoad stepKey="waitForShippingMethodLoad"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" time="30" stepKey="waitForPaymentSectionLoaded"/> <seeInCurrentUrl url="{{CheckoutPage.url}}/#payment" stepKey="assertCheckoutPaymentUrl"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutPhoneValidationTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutPhoneValidationTest.xml new file mode 100644 index 0000000000000..b001128fee906 --- /dev/null +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontOnePageCheckoutPhoneValidationTest.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="StorefrontOnePageCheckoutPhoneValidationTest"> + <annotations> + <features value="Checkout"/> + <stories value="Checkout validation phone field"/> + <title value="Validate phone field on checkout page"/> + <description value="Validate phone field on checkout page, field must not contain alphabetical symbols"/> + <severity value="MAJOR" /> + <testCaseId value="MC-35292"/> + </annotations> + <before> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="ApiSimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategoryPageOnFrontend"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + + <actionGroup ref="StorefrontAddSimpleProductToCartActionGroup" stepKey="addToCartFromStorefrontProductPage"> + <argument name="product" value="$$createProduct$$"/> + </actionGroup> + + <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="guestGoToCheckout"/> + + <fillField userInput="Sample text" selector="{{CheckoutShippingSection.telephone}}" stepKey="enterAlphabeticalSymbols"/> + <see userInput="Please enter a valid phone number. For example (123) 456-7890 or 123-456-7890." selector="{{CheckoutShippingSection.addressFieldValidationError}}" stepKey="checkPhoneFieldValidationIsPassed"/> + </test> +</tests> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml index 9aea4ac79312a..e42d5e1bae956 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontPersistentDataForGuestCustomerWithPhysicalQuoteTest.xml @@ -23,7 +23,7 @@ <field key="price">10</field> </createData> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToStorefront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <executeJS function="window.localStorage.clear();" stepKey="clearLocalStorage"/> </before> <after> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml index 37a7bfff60eb1..f20d0b790edfa 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductNameMinicartOnCheckoutPageDifferentStoreViewsTest.xml @@ -53,7 +53,7 @@ <waitForPageLoad stepKey="waitForPageLoad"/> <click selector="{{AdminProductFormSection.productNameUseDefault}}" stepKey="uncheckUseDefault"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="$$createProduct.name$$-new" stepKey="fillProductName"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!--Add product to cart--> <amOnPage url="{{StorefrontProductPage.url($$createProduct.name$$)}}" stepKey="amOnSimpleProductPage"/> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml index ffdbab03ca337..492bcec7bcc37 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontProductQuantityChangesInBackendAfterCustomerCheckoutTest.xml @@ -67,8 +67,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml index 792025acf1708..d037718a1ec94 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKCustomerCheckoutWithCouponTest.xml @@ -112,8 +112,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdminPanel"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml index 76a3adfb67057..f11144aa454af 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUKGuestCheckoutWithConditionProductQuantityEqualsToOrderedQuantityTest.xml @@ -62,8 +62,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml index 8410dd15fa04e..655865a62cdba 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUSCustomerCheckoutWithCouponAndBankTransferPaymentMethodTest.xml @@ -74,8 +74,7 @@ <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumberWithoutLink}}" stepKey="orderId"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId and assert order--> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml index 12e2820821c87..a7a0917532dcb 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontUpdatePriceInShoppingCartAfterProductSaveTest.xml @@ -30,7 +30,9 @@ <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> <actionGroup ref="SetCustomerDataLifetimeActionGroup" stepKey="setDefaultCustomerDataLifetime"/> - <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCustomerGrid"> + <argument name="indices" value="customer_grid"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Go to product page--> diff --git a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml index 778967c187f65..b7c1d7b83e9b7 100644 --- a/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml +++ b/app/code/Magento/Checkout/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCheckoutTest.xml @@ -36,11 +36,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> </after> diff --git a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php index 09bc9e36c0abc..9fa77145ab8fa 100644 --- a/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php +++ b/app/code/Magento/Checkout/Test/Unit/Controller/Sidebar/RemoveItemTest.php @@ -221,6 +221,57 @@ public function testExecuteWithException() $this->assertEquals($resultJson, $this->action->execute()); } + /** + * Test controller when DB exception is thrown. + * + * @return void + */ + public function testExecuteWithDbException(): void + { + $itemId = 1; + $dbError = 'Error'; + $message = __('An unspecified error occurred. Please contact us for assistance.'); + $responseData = [ + 'success' => false, + 'error_message' => $message, + ]; + + $this->formKeyValidatorMock + ->expects($this->once()) + ->method('validate') + ->with($this->requestMock) + ->willReturn(true); + $this->requestMock->expects($this->once()) + ->method('getParam') + ->with('item_id') + ->willReturn($itemId); + + $exception = new \Zend_Db_Exception($dbError); + + $this->sidebarMock->expects($this->once()) + ->method('checkQuoteItem') + ->with($itemId) + ->willThrowException($exception); + + $this->loggerMock->expects($this->once())->method('critical')->with($exception); + + $this->sidebarMock->expects($this->once()) + ->method('getResponseData') + ->with($message) + ->willReturn($responseData); + + $resultJson = $this->createMock(ResultJson::class); + $resultJson->expects($this->once()) + ->method('setData') + ->with($responseData) + ->willReturnSelf(); + $this->resultJsonFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($resultJson); + + $this->action->execute(); + } + public function testExecuteWhenFormKeyValidationFailed() { $resultRedirect = $this->createMock(ResultRedirect::class); diff --git a/app/code/Magento/Checkout/ViewModel/Cart.php b/app/code/Magento/Checkout/ViewModel/Cart.php new file mode 100644 index 0000000000000..f5415079d396e --- /dev/null +++ b/app/code/Magento/Checkout/ViewModel/Cart.php @@ -0,0 +1,52 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Checkout\ViewModel; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Framework\View\Element\Context; +use Magento\Store\Model\ScopeInterface; + +/** + * Cart form view model. + */ +class Cart implements ArgumentInterface +{ + /** + * Config settings path to enable clear shopping cart button + */ + private const XPATH_CONFIG_ENABLE_CLEAR_SHOPPING_CART = 'checkout/cart/enable_clear_shopping_cart'; + + /** + * @var ScopeConfigInterface + */ + private $_scopeConfig; + + /** + * Constructor + * + * @param Context $context + */ + public function __construct( + Context $context + ) { + $this->_scopeConfig = $context->getScopeConfig(); + } + + /** + * Check if clear shopping cart button is enabled + * + * @return bool + */ + public function isClearShoppingCartEnabled() + { + return (bool)$this->_scopeConfig->getValue( + self::XPATH_CONFIG_ENABLE_CLEAR_SHOPPING_CART, + ScopeInterface::SCOPE_WEBSITE + ); + } +} diff --git a/app/code/Magento/Checkout/etc/adminhtml/system.xml b/app/code/Magento/Checkout/etc/adminhtml/system.xml index 7454c2b6524f3..b56566a043c3e 100644 --- a/app/code/Magento/Checkout/etc/adminhtml/system.xml +++ b/app/code/Magento/Checkout/etc/adminhtml/system.xml @@ -48,6 +48,10 @@ <label>Show Cross-sell Items in the Shopping Cart</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> + <field id="enable_clear_shopping_cart" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Enable Clear Shopping Cart</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> </group> <group id="cart_link" translate="label" sortOrder="3" showInDefault="1" showInWebsite="1"> <label>My Cart Link</label> diff --git a/app/code/Magento/Checkout/etc/config.xml b/app/code/Magento/Checkout/etc/config.xml index c8408f6d902fa..4db5f5bdc01c9 100644 --- a/app/code/Magento/Checkout/etc/config.xml +++ b/app/code/Magento/Checkout/etc/config.xml @@ -19,6 +19,7 @@ <redirect_to_cart>0</redirect_to_cart> <number_items_to_display_pager>20</number_items_to_display_pager> <crosssell_enabled>1</crosssell_enabled> + <enable_clear_shopping_cart>0</enable_clear_shopping_cart> </cart> <cart_link> <use_qty>1</use_qty> diff --git a/app/code/Magento/Checkout/i18n/en_US.csv b/app/code/Magento/Checkout/i18n/en_US.csv index 251985faf6cc4..4a78f8deae841 100644 --- a/app/code/Magento/Checkout/i18n/en_US.csv +++ b/app/code/Magento/Checkout/i18n/en_US.csv @@ -153,6 +153,8 @@ Shipping,Shipping "Maximum Number of Items to Display in Order Summary","Maximum Number of Items to Display in Order Summary" "Quote Lifetime (days)","Quote Lifetime (days)" "After Adding a Product Redirect to Shopping Cart","After Adding a Product Redirect to Shopping Cart" +"Enable Clear Shopping Cart","Enable Clear Shopping Cart" +"Are you sure you want to remove all items from your shopping cart?","Are you sure you want to remove all items from your shopping cart?" "Number of Items to Display Pager","Number of Items to Display Pager" "My Cart Link","My Cart Link" "Display Cart Summary","Display Cart Summary" diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml index 81ee1a5e6db4c..b465c68078641 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_cart_index.xml @@ -181,6 +181,9 @@ </block> </container> <block class="Magento\Checkout\Block\Cart\Grid" name="checkout.cart.form" as="cart-items" template="Magento_Checkout::cart/form.phtml" after="cart.summary"> + <arguments> + <argument name="view_model" xsi:type="object">Magento\Checkout\ViewModel\Cart</argument> + </arguments> <block class="Magento\Framework\View\Element\RendererList" name="checkout.cart.item.renderers" as="renderer.list"/> <block class="Magento\Framework\View\Element\Text\ListText" name="checkout.cart.order.actions"/> </block> diff --git a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml index c33b784fcd20c..ab058110fe66f 100644 --- a/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml +++ b/app/code/Magento/Checkout/view/frontend/layout/checkout_index_index.xml @@ -105,7 +105,7 @@ <item name="trigger" xsi:type="string">opc-new-shipping-address</item> <item name="buttons" xsi:type="array"> <item name="save" xsi:type="array"> - <item name="text" xsi:type="string" translate="true">Ship here</item> + <item name="text" xsi:type="string" translate="true">Ship Here</item> <item name="class" xsi:type="string">action primary action-save-address</item> </item> <item name="cancel" xsi:type="array"> @@ -223,6 +223,9 @@ </item> </item> <item name="telephone" xsi:type="array"> + <item name="validation" xsi:type="array"> + <item name="validate-phoneStrict" xsi:type="number">0</item> + </item> <item name="config" xsi:type="array"> <item name="tooltip" xsi:type="array"> <item name="description" xsi:type="string" translate="true">For delivery questions.</item> diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml index 370d70c44d886..59e33a7c855ce 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/form.phtml @@ -20,7 +20,7 @@ class="form form-cart"> <?= $block->getBlockHtml('formkey') ?> <div class="cart table-wrapper<?= $mergedCells == 2 ? ' detailed' : '' ?>"> - <?php if ($block->getPagerHtml()) :?> + <?php if ($block->getPagerHtml()): ?> <div class="cart-products-toolbar cart-products-toolbar-top toolbar" data-attribute="cart-products-toolbar-top"><?= $block->getPagerHtml() ?> </div> @@ -38,32 +38,34 @@ <th class="col subtotal" scope="col"><span><?= $block->escapeHtml(__('Subtotal')) ?></span></th> </tr> </thead> - <?php foreach ($block->getItems() as $_item) :?> + <?php foreach ($block->getItems() as $_item): ?> <?= $block->getItemHtml($_item) ?> <?php endforeach ?> </table> - <?php if ($block->getPagerHtml()) :?> + <?php if ($block->getPagerHtml()): ?> <div class="cart-products-toolbar cart-products-toolbar-bottom toolbar" data-attribute="cart-products-toolbar-bottom"><?= $block->getPagerHtml() ?> </div> <?php endif ?> </div> <div class="cart main actions"> - <?php if ($block->getContinueShoppingUrl()) :?> + <?php if ($block->getContinueShoppingUrl()): ?> <a class="action continue" href="<?= $block->escapeUrl($block->getContinueShoppingUrl()) ?>" title="<?= $block->escapeHtml(__('Continue Shopping')) ?>"> <span><?= $block->escapeHtml(__('Continue Shopping')) ?></span> </a> <?php endif; ?> - <button type="button" - name="update_cart_action" - data-cart-empty="" - value="empty_cart" - title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" - class="action clear" id="empty_cart_button"> - <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> - </button> + <?php if ($block->getViewModel()->isClearShoppingCartEnabled()): ?> + <button type="button" + name="update_cart_action" + data-cart-empty="" + value="empty_cart" + title="<?= $block->escapeHtml(__('Clear Shopping Cart')) ?>" + class="action clear" id="empty_cart_button"> + <span><?= $block->escapeHtml(__('Clear Shopping Cart')) ?></span> + </button> + <?php endif ?> <button type="submit" name="update_cart_action" data-cart-item-update="" @@ -77,4 +79,3 @@ </form> <?= $block->getChildHtml('checkout.cart.order.actions') ?> <?= $block->getChildHtml('shopping.cart.table.after') ?> - diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml index 28275e0223936..56cd8cd7a1fab 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Checkout\Block\Cart\Sidebar */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-block="minicart" class="minicart-wrapper"> @@ -12,7 +13,8 @@ data-bind="scope: 'minicart_content'"> <span class="text"><?= $block->escapeHtml(__('My Cart')) ?></span> <span class="counter qty empty" - data-bind="css: { empty: !!getCartParam('summary_count') == false && !isLoading() }, blockLoader: isLoading"> + data-bind="css: { empty: !!getCartParam('summary_count') == false && !isLoading() }, + blockLoader: isLoading"> <span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span> <span class="counter-label"> <!-- ko if: getCartParam('summary_count') --> @@ -22,7 +24,7 @@ </span> </span> </a> - <?php if ($block->getIsNeedToDisplaySideBar()) :?> + <?php if ($block->getIsNeedToDisplaySideBar()):?> <div class="block block-minicart" data-role="dropdownDialog" data-mage-init='{"dropdownDialog":{ @@ -39,18 +41,19 @@ </div> <?= $block->getChildHtml('minicart.addons') ?> </div> - <?php else :?> - <script> + <?php else: ?> + <?php $scriptString = <<<script require(['jquery'], function ($) { $('a.action.showcart').click(function() { $(document.body).trigger('processStart'); }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <?php endif ?> - <script> - window.checkout = <?= /* @noEscape */ $block->getSerializedConfig() ?>; - </script> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], 'window.checkout = ' . + /* @noEscape */ $block->getSerializedConfig(), false); ?> <script type="text/x-magento-init"> { "[data-block='minicart']": { @@ -64,5 +67,3 @@ } </script> </div> - - diff --git a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml index a44d37dccfdc5..78625521403a4 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/cart/shipping.phtml @@ -6,7 +6,7 @@ ?> <?php /** @var $block \Magento\Checkout\Block\Cart\Shipping */ ?> - +<?php /** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="block-shipping" class="block shipping" data-mage-init='{"collapsible":{"openedState": "active", "saveState": true}}' @@ -33,8 +33,11 @@ } } </script> - <script> - window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; +<?php $serializedCheckoutConfig = /* @noEscape */ $block->getSerializedCheckoutConfig(); + +$scriptString = <<<script + + window.checkoutConfig = {$serializedCheckoutConfig}; window.customerData = window.checkoutConfig.customerData; window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; require([ @@ -42,10 +45,12 @@ 'Magento_Ui/js/block-loader' ], function(url, blockLoader) { blockLoader( - "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>" + "{$block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')))}" ); - return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); + return url.setBaseUrl('{$block->escapeJs($block->escapeUrl($block->getBaseUrl()))}'); }) - </script> +script; +?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml index 55f7039f33344..f4cc667e4ce8a 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/onepage.phtml @@ -5,15 +5,17 @@ */ /** @var $block \Magento\Checkout\Block\Onepage */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> + <div id="checkout" data-bind="scope:'checkout'" class="checkout-container"> <div id="checkout-loader" data-role="checkout-loader" class="loading-mask" data-mage-init='{"checkoutLoader": {}}'> <div class="loader"> <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')) ?>" - alt="<?= $block->escapeHtmlAttr(__('Loading...')) ?>" - style="position: absolute;"> + alt="<?= $block->escapeHtmlAttr(__('Loading...')) ?>"> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("position: absolute;", "#checkout-loader img") ?> <!-- ko template: getTemplate() --><!-- /ko --> <script type="text/x-magento-init"> { @@ -22,19 +24,24 @@ } } </script> - <script> - window.checkoutConfig = <?= /* @noEscape */ $block->getSerializedCheckoutConfig() ?>; + <?php $serializedCheckoutConfig = /* @noEscape */ $block->getSerializedCheckoutConfig(); + $scriptString = <<<script + window.checkoutConfig = {$serializedCheckoutConfig}; // Create aliases for customer.js model from customer module window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; window.customerData = window.checkoutConfig.customerData; - </script> - <script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <?php $scriptString = <<<script require([ 'mage/url', 'Magento_Ui/js/block-loader' ], function(url, blockLoader) { - blockLoader("<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>"); - return url.setBaseUrl('<?= $block->escapeJs($block->escapeUrl($block->getBaseUrl())) ?>'); + blockLoader("{$block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')))}"); + return url.setBaseUrl('{$block->escapeJs($block->escapeUrl($block->getBaseUrl()))}'); }) - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml index 0d9da171c11a8..dbe8a2142e3f1 100644 --- a/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml +++ b/app/code/Magento/Checkout/view/frontend/templates/total/default.phtml @@ -4,40 +4,42 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** @var $block \Magento\Checkout\Block\Total\DefaultTotal */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + +<?php +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> <tr class="totals"> - <th - colspan="<?= $block->escapeHtmlAttr($block->getColspan()) ?>" - style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" - class="mark" scope="row" - > - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <th colspan="<?= $block->escapeHtmlAttr($block->getColspan()) ?>" + class="mark" scope="row"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> <strong> <?php endif; ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> </strong> <?php endif; ?> </th> - <td - style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" - class="amount" - data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>" - > - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <td class="amount" + data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> <strong> <?php endif; ?> <span> <?= $block->escapeHtml( - $this->helper(Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()), + $checkoutHelper->formatPrice($block->getTotal()->getValue()), ['span'] ) ?> </span> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) :?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()):?> </strong> <?php endif; ?> </td> </tr> +<?php if ($block->getTotal()->getStyle()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($block->getTotal()->getStyle(), 'tr.totals th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($block->getTotal()->getStyle(), 'tr.totals td.amount') ?> +<?php endif; ?> diff --git a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js index 4b30ad8075274..2e9bdf1f31086 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/empty-cart.js @@ -3,14 +3,16 @@ * See COPYING.txt for license details. */ -define([ - 'Magento_Customer/js/customer-data' -], function (customerData) { +define(['Magento_Customer/js/customer-data'], function (customerData) { 'use strict'; - var cartData = customerData.get('cart'); + return function () { + var cartData = customerData.get('cart'); - if (cartData().items && cartData().items.length !== 0) { - customerData.reload(['cart'], false); - } + customerData.getInitCustomerData().done(function () { + if (cartData().items && cartData().items.length !== 0) { + customerData.reload(['cart'], false); + } + }); + }; }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js index 6e1b031ab48ce..a59ea7101f16c 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/model/address-converter.js @@ -84,7 +84,8 @@ define([ quoteAddressToFormAddressData: function (addrs) { var self = this, output = {}, - streetObject; + streetObject, + customAttributesObject; $.each(addrs, function (key) { if (addrs.hasOwnProperty(key) && !$.isFunction(addrs[key])) { @@ -100,6 +101,16 @@ define([ output.street = streetObject; } + //jscs:disable requireCamelCaseOrUpperCaseIdentifiers + if ($.isArray(addrs.customAttributes)) { + customAttributesObject = {}; + addrs.customAttributes.forEach(function (value) { + customAttributesObject[value.attribute_code] = value.value; + }); + output.custom_attributes = customAttributesObject; + } + //jscs:enable requireCamelCaseOrUpperCaseIdentifiers + return output; }, diff --git a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js index 6d54f607484b4..68f6b1b2753c0 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/region-updater.js @@ -56,6 +56,9 @@ define([ if (this.options.isMultipleCountriesAllowed) { this.element.parents('div.field').show(); this.element.on('change', $.proxy(function (e) { + // clear region inputs on country change + $(this.options.regionListId).val(''); + $(this.options.regionInputId).val(''); this._updateRegion($(e.target).val()); }, this)); @@ -157,19 +160,25 @@ define([ regionInput = $(this.options.regionInputId), postcode = $(this.options.postcodeId), label = regionList.parent().siblings('label'), - container = regionList.parents('div.field'); + container = regionList.parents('div.field'), + regionsEntries, + regionId, + regionData; this._clearError(); this._checkRegionRequired(country); - $(regionList).find('option:selected').removeAttr('selected'); - regionInput.val(''); - // Populate state/province dropdown list if available or use input box if (this.options.regionJson[country]) { this._removeSelectOptions(regionList); - $.each(this.options.regionJson[country], $.proxy(function (key, value) { - this._renderSelectOption(regionList, key, value); + regionsEntries = _.pairs(this.options.regionJson[country]); + regionsEntries.sort(function (a, b) { + return a[1].name > b[1].name ? 1 : -1; + }); + $.each(regionsEntries, $.proxy(function (key, value) { + regionId = value[0]; + regionData = value[1]; + this._renderSelectOption(regionList, regionId, regionData); }, this)); if (this.currentRegionOption) { @@ -193,7 +202,7 @@ define([ regionList.hide(); container.hide(); } else { - regionList.show(); + regionList.removeAttr('disabled').show(); } } diff --git a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js index b15599673095f..97dff2f6fd47a 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/shopping-cart.js @@ -5,8 +5,10 @@ define([ 'jquery', - 'jquery-ui-modules/widget' -], function ($) { + 'Magento_Ui/js/modal/confirm', + 'jquery-ui-modules/widget', + 'mage/translate' +], function ($, confirm) { 'use strict'; $.widget('mage.shoppingCart', { @@ -15,13 +17,7 @@ define([ var items, i, reload; $(this.options.emptyCartButton).on('click', $.proxy(function () { - $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); - $(this.options.updateCartActionContainer) - .attr('name', 'update_cart_action').attr('value', 'empty_cart'); - - if ($(this.options.emptyCartButton).parents('form').length > 0) { - $(this.options.emptyCartButton).parents('form').submit(); - } + this._confirmClearCart(); }, this)); items = $.find('[data-role="cart-item-qty"]'); @@ -61,6 +57,40 @@ define([ $('div.block.block-minicart').off('dropdowndialogclose'); })); }, this)); + }, + + /** + * Display confirmation modal for clearing the cart + * @private + */ + _confirmClearCart: function () { + var self = this; + + confirm({ + content: $.mage.__('Are you sure you want to remove all items from your shopping cart?'), + actions: { + /** + * Confirmation modal handler to execute clear cart action + */ + confirm: function () { + self.clearCart(); + } + } + }); + }, + + /** + * Prepares the form and submit to clear the cart + * @public + */ + clearCart: function () { + $(this.options.emptyCartButton).attr('name', 'update_cart_action_temp'); + $(this.options.updateCartActionContainer) + .attr('name', 'update_cart_action').attr('value', 'empty_cart'); + + if ($(this.options.emptyCartButton).parents('form').length > 0) { + $(this.options.emptyCartButton).parents('form').submit(); + } } }); diff --git a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js index a7ccb217fa102..2e501c0c42b77 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/sidebar.js @@ -42,7 +42,6 @@ define([ update: function () { $(this.options.targetElement).trigger('contentUpdated'); this._calcHeight(); - this._isOverflowed(); }, /** @@ -135,23 +134,6 @@ define([ this._on(this.element, events); this._calcHeight(); - this._isOverflowed(); - }, - - /** - * Add 'overflowed' class to minicart items wrapper element - * - * @private - */ - _isOverflowed: function () { - var list = $(this.options.minicart.list), - cssOverflowClass = 'overflowed'; - - if (this.scrollHeight > list.innerHeight()) { - list.parent().addClass(cssOverflowClass); - } else { - list.parent().removeClass(cssOverflowClass); - } }, /** diff --git a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js index c77e72e38107a..d3890556f3ccd 100644 --- a/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js +++ b/app/code/Magento/Checkout/view/frontend/web/js/view/minicart.js @@ -123,20 +123,6 @@ define([ $('[data-block="minicart"]').find('[data-role="dropdownDialog"]').dropdownDialog('close'); }, - /** - * @return {Boolean} - */ - closeSidebar: function () { - var minicart = $('[data-block="minicart"]'); - - minicart.on('click', '[data-action="close"]', function (event) { - event.stopPropagation(); - minicart.find('[data-role="dropdownDialog"]').dropdownDialog('close'); - }); - - return true; - }, - /** * @param {String} productType * @return {*|String} diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html index c5fd6d545702b..a47f11e5787c3 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/content.html @@ -21,7 +21,12 @@ id="btn-minicart-close" class="action close" data-action="close" - data-bind="attr: { title: $t('Close') }"> + data-bind=" + attr: { + title: $t('Close') + }, + click: closeMinicart() + "> <span translate="'Close'"/> </button> @@ -74,7 +79,6 @@ <ifnot args="getCartParam('summary_count')"> <strong class="subtitle empty" - data-bind="visible: closeSidebar()" translate="'You have no items in your shopping cart.'" /> <if args="getCartParam('cart_empty_message')"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html index 5489089452d85..053b15b4ad343 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/minicart/item/default.html @@ -44,11 +44,11 @@ <!-- ko if: Array.isArray(option.value) --> <span data-bind="html: option.value.join('<br>')"></span> <!-- /ko --> - <!-- ko if: (!Array.isArray(option.value) && option.option_type == 'file') --> + <!-- ko if: (!Array.isArray(option.value) && ['file', 'html'].includes(option.option_type)) --> <span data-bind="html: option.value"></span> <!-- /ko --> - <!-- ko if: (!Array.isArray(option.value) && option.option_type != 'file') --> - <span data-bind="text: option.value"></span> + <!-- ko if: (!Array.isArray(option.value) && !['file', 'html'].includes(option.option_type)) --> + <span data-bind="text: option.value"></span> <!-- /ko --> </dd> <!-- /ko --> @@ -112,7 +112,7 @@ </div> </div> </div> - <div class="message notice" if="message"> - <div data-bind="text: message"></div> + <div class="message notice" if="$data.message"> + <div data-bind="text: $data.message"></div> </div> </li> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html index 4e49d4502d8a8..8fc514990d567 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/cart-items.html @@ -15,7 +15,7 @@ </strong> </div> <div class="content minicart-items" data-role="content"> - <div class="minicart-items-wrapper overflowed"> + <div class="minicart-items-wrapper"> <ol class="minicart-items"> <each args="items()"> <li class="product-item"> diff --git a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html index eb218bbee9941..fa32ea1b212ae 100644 --- a/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html +++ b/app/code/Magento/Checkout/view/frontend/web/template/summary/item/details/thumbnail.html @@ -5,7 +5,7 @@ */ --> <span class="product-image-container" - data-bind="attr: {'style': 'height: ' + getHeight($parents[1]) + 'px; width: ' + getWidth($parents[1]) + 'px;' }"> + data-bind="attr: {'style': 'height: ' + getHeight($parents[1])/2 + 'px; width: ' + getWidth($parents[1])/2 + 'px;' }"> <span class="product-image-wrapper"> <img data-bind="attr: {'src': getSrc($parents[1]), 'width': getWidth($parents[1]), 'height': getHeight($parents[1]), 'alt': getAlt($parents[1]), 'title': getAlt($parents[1]) }"/> diff --git a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php index b91701acef04d..a15191244a030 100644 --- a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php +++ b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsListInterface.php @@ -13,6 +13,7 @@ * search filters without predefined limitations. * * @api + * @since 100.3.0 */ interface CheckoutAgreementsListInterface { @@ -21,6 +22,7 @@ interface CheckoutAgreementsListInterface * * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return \Magento\CheckoutAgreements\Api\Data\AgreementInterface[] + * @since 100.3.0 */ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCriteria) : array; } diff --git a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php index 5822b8f082fef..7dc757395a478 100644 --- a/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php +++ b/app/code/Magento/CheckoutAgreements/Api/CheckoutAgreementsRepositoryInterface.php @@ -25,7 +25,7 @@ public function get($id, $storeId = null); * Lists active checkout agreements. * * @return \Magento\CheckoutAgreements\Api\Data\AgreementInterface[] - * @deprecated + * @deprecated 100.3.0 * @see \Magento\CheckoutAgreements\Api\CheckoutAgreementsListInterface::getList */ public function getList(); diff --git a/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php b/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php index 4a35a58a41ff9..21b318cd00f09 100644 --- a/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php +++ b/app/code/Magento/CheckoutAgreements/Block/Adminhtml/Agreement/Grid.php @@ -12,7 +12,7 @@ class Grid extends \Magento\Backend\Block\Widget\Grid\Extended { /** * @var \Magento\CheckoutAgreements\Model\ResourceModel\Agreement\CollectionFactory - * @deprecated + * @deprecated 100.2.2 */ protected $_collectionFactory; diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml index 5cb256090c196..d7b1565c95400 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/agreements.phtml @@ -4,30 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Files.LineLength - +use Magento\CheckoutAgreements\Model\AgreementModeOptions; ?> <?php /** * @var $block \Magento\CheckoutAgreements\Block\Agreements */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php if (!$block->getAgreements()) { return; } ?> <ol id="checkout-agreements" class="agreements checkout items"> <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> - <?php foreach ($block->getAgreements() as $agreement) :?> + <?php foreach ($block->getAgreements() as $agreement):?> <li class="item"> - <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> - <?php if ($agreement->getIsHtml()) :?> + <div class="checkout-agreement-item-content" id="<?= /* @noEscape */ $agreement->getAgreementId() ?>" + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getContent() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> <?php endif; ?> </div> - <form id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> - <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> + <?php if ($agreement->getContentHeight()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'height:' . $agreement->getContentHeight(), + '#' . $agreement->getAgreementId() + ) ?> + <?php endif; ?> + <form id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" + class="field choice agree required"> + <?php if ($agreement->getMode() == AgreementModeOptions::MODE_MANUAL):?> <input type="checkbox" id="agreement-<?= (int) $agreement->getAgreementId() ?>" name="agreement[<?= (int) $agreement->getAgreementId() ?>]" @@ -37,19 +44,19 @@ data-validate="{required:true}"/> <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> </label> - <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> + <?php elseif ($agreement->getMode() == AgreementModeOptions::MODE_AUTO):?> <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> diff --git a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml index fb2d5168d21de..bc714f21e4dfa 100644 --- a/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml +++ b/app/code/Magento/CheckoutAgreements/view/frontend/templates/multishipping_agreements.phtml @@ -4,13 +4,12 @@ * See COPYING.txt for license details. */ -// @deprecated -// phpcs:disable Magento2.Files.LineLength - +use Magento\CheckoutAgreements\Model\AgreementModeOptions; ?> <?php /** * @var $block \Magento\CheckoutAgreements\Block\Agreements + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php if (!$block->getAgreements()) { @@ -18,17 +17,24 @@ } ?> <ol id="checkout-agreements" class="agreements checkout items"> <?php /** @var \Magento\CheckoutAgreements\Api\Data\AgreementInterface $agreement */ ?> - <?php foreach ($block->getAgreements() as $agreement) :?> + <?php foreach ($block->getAgreements() as $agreement):?> <li class="item"> - <div class="checkout-agreement-item-content"<?= $block->escapeHtmlAttr($agreement->getContentHeight() ? ' style="height:' . $agreement->getContentHeight() . '"' : '') ?>> - <?php if ($agreement->getIsHtml()) :?> + <div class="checkout-agreement-item-content" id="<?= /* @noEscape */ $agreement->getAgreementId() ?>" + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getContent() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml(nl2br($agreement->getContent())) ?> <?php endif; ?> </div> - <?php if ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_MANUAL) :?> - <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree required"> + <?php if ($agreement->getContentHeight()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'height:' . $agreement->getContentHeight(), + '#' . $agreement->getAgreementId() + ) ?> + <?php endif; ?> + <?php if ($agreement->getMode() == AgreementModeOptions::MODE_MANUAL):?> + <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" + class="field choice agree required"> <input type="checkbox" id="agreement-<?= (int) $agreement->getAgreementId() ?>" name="agreement[<?= (int) $agreement->getAgreementId() ?>]" @@ -38,20 +44,20 @@ data-validate="{required:true}"/> <label class="label" for="agreement-<?= (int) $agreement->getAgreementId() ?>"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> </label> </div> - <?php elseif ($agreement->getMode() == \Magento\CheckoutAgreements\Model\AgreementModeOptions::MODE_AUTO) :?> + <?php elseif ($agreement->getMode() == AgreementModeOptions::MODE_AUTO):?> <div id="checkout-agreements-form-<?= (int) $agreement->getAgreementId() ?>" class="field choice agree"> <span> - <?php if ($agreement->getIsHtml()) :?> + <?php if ($agreement->getIsHtml()):?> <?= /* @noEscape */ $agreement->getCheckboxText() ?> - <?php else :?> + <?php else:?> <?= $block->escapeHtml($agreement->getCheckboxText()) ?> <?php endif; ?> </span> diff --git a/app/code/Magento/Cms/Api/Data/PageInterface.php b/app/code/Magento/Cms/Api/Data/PageInterface.php index 7a31ab1b9a94f..402c2ccd289e0 100644 --- a/app/code/Magento/Cms/Api/Data/PageInterface.php +++ b/app/code/Magento/Cms/Api/Data/PageInterface.php @@ -125,7 +125,7 @@ public function getSortOrder(); * Get layout update xml * * @return string|null - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. */ public function getLayoutUpdateXml(); @@ -146,7 +146,7 @@ public function getCustomRootTemplate(); /** * Get custom layout update xml * - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface * @return string|null */ @@ -275,7 +275,7 @@ public function setSortOrder($sortOrder); * * @param string $layoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. */ public function setLayoutUpdateXml($layoutUpdateXml); @@ -300,7 +300,7 @@ public function setCustomRootTemplate($customRootTemplate); * * @param string $customLayoutUpdateXml * @return \Magento\Cms\Api\Data\PageInterface - * @deprecated Existing updates are applied, new are not accepted. + * @deprecated 103.0.4 Existing updates are applied, new are not accepted. * @see \Magento\Cms\Model\Page\CustomLayout\Data\CustomLayoutSelectedInterface */ public function setCustomLayoutUpdateXml($customLayoutUpdateXml); diff --git a/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php b/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php index 7e8bbdc3e8d2f..70fadae42f327 100644 --- a/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php +++ b/app/code/Magento/Cms/Api/GetBlockByIdentifierInterface.php @@ -8,6 +8,7 @@ /** * Command to load the block data by specified identifier * @api + * @since 103.0.0 */ interface GetBlockByIdentifierInterface { @@ -18,6 +19,7 @@ interface GetBlockByIdentifierInterface * @param int $storeId * @throws \Magento\Framework\Exception\NoSuchEntityException * @return \Magento\Cms\Api\Data\BlockInterface + * @since 103.0.0 */ public function execute(string $identifier, int $storeId) : \Magento\Cms\Api\Data\BlockInterface; } diff --git a/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php b/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php index f432f678d3a12..8f47de5266321 100644 --- a/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php +++ b/app/code/Magento/Cms/Api/GetPageByIdentifierInterface.php @@ -8,6 +8,7 @@ /** * Command to load the page data by specified identifier * @api + * @since 103.0.0 */ interface GetPageByIdentifierInterface { @@ -18,6 +19,7 @@ interface GetPageByIdentifierInterface * @param int $storeId * @throws \Magento\Framework\Exception\NoSuchEntityException * @return \Magento\Cms\Api\Data\PageInterface + * @since 103.0.0 */ public function execute(string $identifier, int $storeId) : \Magento\Cms\Api\Data\PageInterface; } diff --git a/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php b/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php index c6bf4c8404701..07c5f5c8a9e07 100644 --- a/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php +++ b/app/code/Magento/Cms/Api/GetUtilityPageIdentifiersInterface.php @@ -9,12 +9,14 @@ * Utility Cms Pages * * @api + * @since 102.0.4 */ interface GetUtilityPageIdentifiersInterface { /** * Get List Page Identifiers * @return array + * @since 102.0.4 */ public function execute(); } diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php index f1862026f0e35..71b620a1632ca 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Page/PostDataProcessor.php @@ -79,7 +79,7 @@ public function filter($data) * * @param array $data * @return bool Return FALSE if some item is invalid - * @deprecated + * @deprecated 103.0.2 */ public function validate($data) { diff --git a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php index 5344472a79a9d..29f84e0b2e534 100644 --- a/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php +++ b/app/code/Magento/Cms/Controller/Adminhtml/Wysiwyg/Images/DeleteFolder.php @@ -4,6 +4,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Cms\Controller\Adminhtml\Wysiwyg\Images; use Magento\Framework\App\Action\HttpPostActionInterface; @@ -60,13 +63,8 @@ public function execute() { try { $path = $this->getStorage()->getCmsWysiwygImages()->getCurrentPath(); - if (!$this->directoryResolver->validatePath($path, DirectoryList::MEDIA)) { - throw new \Magento\Framework\Exception\LocalizedException( - __('Directory %1 is not under storage root path.', $path) - ); - } $this->getStorage()->deleteDirectory($path); - + return $this->resultRawFactory->create(); } catch (\Exception $e) { $result = ['error' => true, 'message' => $e->getMessage()]; diff --git a/app/code/Magento/Cms/Model/BlockRepository.php b/app/code/Magento/Cms/Model/BlockRepository.php index fa29cc9ff7631..d0df0d2b31caa 100644 --- a/app/code/Magento/Cms/Model/BlockRepository.php +++ b/app/code/Magento/Cms/Model/BlockRepository.php @@ -196,7 +196,7 @@ public function deleteById($blockId) /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php index 23a452c0fe58c..3413aa7b0dd6c 100644 --- a/app/code/Magento/Cms/Model/Page/Source/PageLayout.php +++ b/app/code/Magento/Cms/Model/Page/Source/PageLayout.php @@ -20,7 +20,7 @@ class PageLayout implements OptionSourceInterface /** * @var array - * @deprecated since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles + * @deprecated 103.0.1 since the cache is now handled by \Magento\Theme\Model\PageLayout\Config\Builder::$configFiles */ protected $options; diff --git a/app/code/Magento/Cms/Model/PageRepository.php b/app/code/Magento/Cms/Model/PageRepository.php index 2de44b6691274..0439fbcd2f799 100644 --- a/app/code/Magento/Cms/Model/PageRepository.php +++ b/app/code/Magento/Cms/Model/PageRepository.php @@ -256,7 +256,7 @@ public function deleteById($pageId) /** * Retrieve collection processor * - * @deprecated 101.1.0 + * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Cms/Model/Template/Filter.php b/app/code/Magento/Cms/Model/Template/Filter.php index 7e71a06de1f31..66bca1052b797 100644 --- a/app/code/Magento/Cms/Model/Template/Filter.php +++ b/app/code/Magento/Cms/Model/Template/Filter.php @@ -5,32 +5,11 @@ */ namespace Magento\Cms\Model\Template; -use Magento\Framework\Exception\LocalizedException; - /** * Cms Template Filter Model */ class Filter extends \Magento\Email\Model\Template\Filter { - /** - * Whether to allow SID in store directive: AUTO - * - * @var bool - */ - protected $_useSessionInUrl; - - /** - * Setter whether SID is allowed in store directive - * - * @param bool $flag - * @return $this - */ - public function setUseSessionInUrl($flag) - { - $this->_useSessionInUrl = (bool)$flag; - return $this; - } - /** * Retrieve media file URL directive * @@ -41,8 +20,8 @@ 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'); + if (preg_match('/(^.*:\/\/.*|\.\.\/.*)/', $params['url'])) { + throw new \InvalidArgumentException('Image path must be absolute and not include URLs'); } return $this->_storeManager->getStore()->getBaseMediaDir() . '/' . $params['url']; diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Config.php b/app/code/Magento/Cms/Model/Wysiwyg/Config.php index 1da7b99c6d886..95f5971251f1c 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Config.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Config.php @@ -61,14 +61,14 @@ class Config extends \Magento\Framework\DataObject implements ConfigInterface /** * @var \Magento\Variable\Model\Variable\Config - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Cms\Model\ConfigProvider::processVariableConfig */ protected $_variableConfig; /** * @var \Magento\Widget\Model\Widget\Config - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Cms\Model\ConfigProvider::processWidgetConfig */ protected $_widgetConfig; diff --git a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php index f0a232bdccccc..ae88b24bd2682 100644 --- a/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php +++ b/app/code/Magento/Cms/Model/Wysiwyg/Images/Storage.php @@ -225,6 +225,8 @@ public function __construct( * * @param string $path * @return void + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ protected function createSubDirectories($path) { @@ -295,6 +297,7 @@ protected function removeItemFromCollection($collection, $conditions) * * @param string $path Parent directory path * @return \Magento\Framework\Data\Collection\Filesystem + * @throws \Exception */ public function getDirsCollection($path) { @@ -393,6 +396,7 @@ public function getFilesCollection($path, $type = null) * * @param string $path Path to the directory * @return \Magento\Cms\Model\Wysiwyg\Images\Storage\Collection + * @throws \Exception */ public function getCollection($path = null) { @@ -485,6 +489,9 @@ public function deleteDirectory($path) * * @param string $path * @return void + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\ValidatorException */ protected function _deleteByPath($path) { @@ -500,6 +507,8 @@ protected function _deleteByPath($path) * * @param string $target File path to be deleted * @return $this + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function deleteFile($target) { @@ -561,9 +570,11 @@ public function uploadFile($targetPath, $type = null) /** * Thumbnail path getter * - * @param string $filePath original file path - * @param bool $checkFile OPTIONAL is it necessary to check file availability + * @param string $filePath original file path + * @param bool $checkFile OPTIONAL is it necessary to check file availability * @return string|false + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function getThumbnailPath($filePath, $checkFile = false) { @@ -587,9 +598,11 @@ public function getThumbnailPath($filePath, $checkFile = false) /** * Thumbnail URL getter * - * @param string $filePath original file path - * @param bool $checkFile OPTIONAL is it necessary to check file availability + * @param string $filePath original file path + * @param bool $checkFile OPTIONAL is it necessary to check file availability * @return string|false + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function getThumbnailUrl($filePath, $checkFile = false) { @@ -610,6 +623,8 @@ public function getThumbnailUrl($filePath, $checkFile = false) * @param string $source Image path to be resized * @param bool $keepRatio Keep aspect ratio or not * @return bool|string Resized filepath or false if errors were occurred + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function resizeFile($source, $keepRatio = true) { @@ -628,8 +643,12 @@ public function resizeFile($source, $keepRatio = true) } $image = $this->_imageFactory->create(); $image->open($source); + $image->keepAspectRatio($keepRatio); - $image->resize($this->_resizeParameters['width'], $this->_resizeParameters['height']); + + list($imageWidth, $imageHeight) = $this->getResizedParams($source); + + $image->resize($imageWidth, $imageHeight); $dest = $targetDir . '/' . $this->ioFile->getPathInfo($source)['basename']; $image->save($dest); if ($this->_directory->isFile($this->_directory->getRelativePath($dest))) { @@ -638,11 +657,37 @@ public function resizeFile($source, $keepRatio = true) return false; } + /** + * Return width height for the image resizing. + * + * @param string $source + * @return array + */ + private function getResizedParams(string $source): array + { + $configWidth = $this->_resizeParameters['width']; + $configHeight = $this->_resizeParameters['height']; + + //phpcs:ignore Generic.PHP.NoSilencedErrors + list($imageWidth, $imageHeight) = @getimagesize($source); + + if ($imageWidth && $imageHeight) { + $imageWidth = $configWidth > $imageWidth ? $imageWidth : $configWidth; + $imageHeight = $configHeight > $imageHeight ? $imageHeight : $configHeight; + + return [$imageWidth, $imageHeight]; + } + return [$configWidth, $configHeight]; + } + /** * Resize images on the fly in controller action * * @param string $filename File basename * @return bool|string Thumbnail path or false for errors + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\LocalizedException + * @throws \Magento\Framework\Exception\ValidatorException */ public function resizeOnTheFly($filename) { @@ -658,6 +703,8 @@ public function resizeOnTheFly($filename) * * @param bool|string $filePath Path to the file * @return string + * @throws \Magento\Framework\Exception\FileSystemException + * @throws \Magento\Framework\Exception\ValidatorException */ public function getThumbsPath($filePath = false) { @@ -782,10 +829,20 @@ protected function _validatePath($path) * * @param string $path * @return string + * @throws \Magento\Framework\Exception\ValidatorException */ protected function _sanitizePath($path) { - return rtrim(preg_replace('~[/\\\]+~', '/', $this->_directory->getDriver()->getRealPathSafety($path)), '/'); + return rtrim( + preg_replace( + '~[/\\\]+~', + '/', + $this->_directory->getDriver()->getRealPathSafety( + $this->_directory->getAbsolutePath($path) + ) + ), + '/' + ); } /** @@ -793,6 +850,7 @@ protected function _sanitizePath($path) * * @param string $path * @return string|bool + * @throws \Magento\Framework\Exception\ValidatorException */ protected function _getRelativePathToRoot($path) { diff --git a/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSPageByUrlKeyActionGroup.xml b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSPageByUrlKeyActionGroup.xml new file mode 100644 index 0000000000000..01e430807d7bd --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/ActionGroup/AdminDeleteCMSPageByUrlKeyActionGroup.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminDeleteCMSPageByUrlKeyActionGroup"> + <annotations> + <description>Goes to the Admin CMS Pages page. Filters the grid based on the provided Page url key. Deletes the Page via the grid.</description> + </annotations> + <arguments> + <argument name="pageUrlKey" type="string" defaultValue="cms_page"/> + </arguments> + + <amOnPage url="{{CmsPagesPage.url}}" stepKey="navigateToCMSPagesGrid"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" visible="true" stepKey="clickToResetFilter"/> + <waitForPageLoad stepKey="waitForPageLoadAfterClearFilters"/> + <click selector="{{CmsPagesPageActionsSection.filterButton}}" stepKey="clickFilterButton"/> + <fillField selector="{{CmsPagesPageActionsSection.URLKey}}" userInput="{{pageUrlKey}}" stepKey="fillPageUrlKeyFilter"/> + <click selector="{{CmsPagesPageActionsSection.ApplyFiltersBtn}}" stepKey="applyFilter"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.select(pageUrlKey)}}" stepKey="waitItemAppears"/> + <click selector="{{CmsPagesPageActionsSection.select(pageUrlKey)}}" stepKey="clickSelect"/> + <click selector="{{CmsPagesPageActionsSection.delete(pageUrlKey)}}" stepKey="clickDelete"/> + <waitForElementVisible selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="waitForOkButtonToBeVisible"/> + <click selector="{{CmsPagesPageActionsSection.deleteConfirm}}" stepKey="clickOkButton"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitSuccessMessageAppeared"/> + <see selector="{{AdminMessagesSection.success}}" userInput="The page has been deleted." stepKey="seeSuccessMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml index 46a968959407f..ea5e90383511c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml +++ b/app/code/Magento/Cms/Test/Mftf/Data/WysiwygConfigData.xml @@ -18,7 +18,7 @@ <data key="scope_id">0</data> <data key="value">hidden</data> </entity> - <entity name="WysiwygTinyMCE3Enable"> + <entity name="WysiwygTinyMCE3Enable" deprecated="Use WysiwygTinyMCE4Enable instead"> <data key="path">cms/wysiwyg/editor</data> <data key="scope_id">0</data> <data key="value">Magento_Tinymce3/tinymce3Adapter</data> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml index d487517269c01..ac9c66fe82c74 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/BlockPageActionsSection.xml @@ -15,8 +15,10 @@ <element name="idColumn" type="button" selector="//div[contains(@data-role, 'grid-wrapper')]/table/thead/tr/th/span[contains(text(), 'ID')]"/> <element name="clearAll" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]"/> <element name="activeFilters" type="button" selector="//div[@class='admin__data-grid-header']//span[contains(text(), 'Active filters:')]" /> + <element name="activeFilterDiv" type="button" selector="(//div[contains(@class, 'admin__data-grid-filters-current') and contains(@class, '_show')])[1]"/> <element name="FilterBtn" type="input" selector="//button[text()='Filters']"/> <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> + <element name="blockGridRowByTitle" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml index 6f16fa54a6ebf..a287685dbdefb 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/CmsPagesPageActionsSection.xml @@ -11,14 +11,15 @@ <section name="CmsPagesPageActionsSection"> <element name="filterButton" type="input" selector="//button[text()='Filters']"/> <element name="URLKey" type="input" selector="//div[@class='admin__form-field-control']/input[@name='identifier']"/> - <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']"/> - <element name="searchInput" type="input" selector="//*[@id='fulltext']"/> + <element name="ApplyFiltersBtn" type="button" selector="//span[text()='Apply Filters']" timeout="60"/> + <element name="searchInput" type="input" selector="#fulltext"/> <element name="searchButton" type="button" selector="//*[@id='fulltext']/parent::*/button"/> <element name="addNewPageButton" type="button" selector="#add" timeout="30"/> <element name="select" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//button[text()='Select']" parameterized="true"/> <element name="edit" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='Edit']" parameterized="true"/> <element name="preview" type="button" selector="//div[text()='{{var1}}']/parent::td//following-sibling::td[@class='data-grid-actions-cell']//a[text()='View']" parameterized="true"/> - <element name="clearAllButton" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]"/> + <element name="clearAllButton" type="button" selector="//div[@class='admin__data-grid-header']//button[contains(text(), 'Clear all')]" timeout="60"/> + <element name="clearFilters" type="button" selector=".admin__data-grid-header button[data-action='grid-filter-reset']" timeout="30"/> <element name="activeFilters" type="button" selector="//div[@class='admin__data-grid-header']//span[contains(text(), 'Active filters:')]" /> <element name="spinner" type="input" selector='//div[@data-component="cms_page_listing.cms_page_listing.cms_page_columns"]'/> <element name="firstItemSelectButton" type="button" selector=".data-grid .action-select-wrap button.action-select"/> @@ -31,5 +32,6 @@ <element name="massActionsButton" type="button" selector="//div[@class='admin__data-grid-header'][(not(ancestor::*[@class='sticky-header']) and not(contains(@style,'visibility: hidden'))) or (ancestor::*[@class='sticky-header' and not(contains(@style,'display: none'))])]//button[contains(@class, 'action-select')]" /> <element name="massActionsOption" type="button" selector="//div[@class='admin__data-grid-header'][(not(ancestor::*[@class='sticky-header']) and not(contains(@style,'visibility: hidden'))) or (ancestor::*[@class='sticky-header' and not(contains(@style,'display: none'))])]//span[contains(@class, 'action-menu-item') and .= '{{action}}']" parameterized="true"/> <element name="gridDataRow" type="input" selector=".data-row .data-grid-cell-content"/> + <element name="pagesGridRowByTitle" type="input" selector="//tbody//tr//td//div[contains(., '{{var1}}')]" parameterized="true" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml index 112335e726270..725d050554f2d 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/MediaGallerySection.xml @@ -9,7 +9,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="MediaGallerySection"> <element name="Browse" type="button" selector=".mce-i-browse"/> - <element name="browseForImage" type="button" selector="//*[@id='srcbrowser']"/> + <element name="browseForImage" type="button" selector="#srcbrowser"/> <element name="BrowseUploadImage" type="file" selector=".fileupload"/> <element name="image" type="text" selector="//small[text()='{{var1}}']" parameterized="true"/> <element name="imageOrImageCopy" type="text" selector="//div[contains(@class,'media-gallery-modal')]//img[contains(@alt, '{{arg1}}.{{arg2}}')]|//img[contains(@alt,'{{arg1}}_') and contains(@alt,'.{{arg2}}')]" parameterized="true"/> @@ -17,7 +17,8 @@ <element name="imageSelected" type="text" selector="//small[text()='{{var1}}']/parent::*[@class='filecnt selected']" parameterized="true"/> <element name="ImageSource" type="input" selector=".mce-combobox.mce-abs-layout-item.mce-last.mce-has-open"/> <element name="ImageDescription" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-last"/> - <element name="ImageDescriptionTinyMCE3" type="input" selector="#alt"/> + <element name="ImageDescriptionTinyMCE3" type="input" selector="#alt" deprecated="Deprecated New element was introduced. Please use 'ImageDescriptionTinyMCE4'"/> + <element name="ImageDescriptionTinyMCE4" type="input" selector="#alt" /> <element name="Height" type="input" selector=".mce-textbox.mce-abs-layout-item.mce-first"/> <element name="UploadImage" type="file" selector=".fileupload"/> <element name="OkBtn" type="button" selector="//span[text()='Ok']"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml index 1869a6544c3d3..5be91f61e1e1e 100644 --- a/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml +++ b/app/code/Magento/Cms/Test/Mftf/Section/TinyMCESection/WidgetSection.xml @@ -38,6 +38,8 @@ <element name="PageSize" type="input" selector="input[name='parameters[page_size]']"/> <element name="ProductAttribute" type="multiselect" selector="select[name='parameters[show_attributes][]']"/> <element name="ButtonToShow" type="multiselect" selector="select[name='parameters[show_buttons][]']"/> + <element name="InputAnchorCustomText" type="input" selector="input[name='parameters[anchor_text]']"/> + <element name="InputAnchorCustomTitle" type="input" selector="input[name='parameters[title]']"/> <!--Compare on Storefront--> <element name="ProductName" type="text" selector=".product.name.product-item-name"/> <element name="CompareBtn" type="button" selector=".action.tocompare"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml index 7c2aedceb9b7e..9163ec4d9f5f8 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToCMSPageTinyMCE3Test.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminAddImageToCMSPageTinyMCE3Test"> + <test name="AdminAddImageToCMSPageTinyMCE3Test" deprecated="TinyMCE3 is no longer supported"> <annotations> <features value="Cms"/> <stories value="Admin should be able to upload images with TinyMCE3 WYSIWYG"/> @@ -17,6 +17,9 @@ <description value="Verify that admin is able to upload image to CMS Page with TinyMCE3 enabled"/> <severity value="BLOCKER"/> <testCaseId value="MAGETWO-95725"/> + <skip> + <issueId value="DEPRECATED">TinyMCE3 is no longer supported</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> @@ -25,6 +28,10 @@ <magentoCLI command="config:set cms/wysiwyg/editor Magento_Tinymce3/tinymce3Adapter" stepKey="enableTinyMCE3"/> </before> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <!-- Switch WYSIWYG editor to TinyMCE4--> <comment userInput="Reset editor as TinyMCE4" stepKey="chooseTinyMCE4AsEditor"/> <magentoCLI command="config:set cms/wysiwyg/editor mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter" stepKey="enableTinyMCE4"/> @@ -34,11 +41,11 @@ <waitForPageLoad stepKey="wait5"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle2"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab2" /> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE3}}" stepKey="waitForTinyMCE3"/> - <seeElement selector="{{TinyMCESection.TinyMCE3}}" stepKey="seeTinyMCE3" /> + <comment userInput="removing deprecated element" stepKey="waitForTinyMCE3"/> + <comment userInput="removing deprecated element" stepKey="seeTinyMCE3" /> <wait time="3" stepKey="waiting"/> <comment userInput="Click Insert image button" stepKey="clickImageButton"/> - <click selector="{{TinyMCESection.InsertImageBtnTinyMCE3}}" stepKey="clickInsertImage" /> + <comment userInput="removing deprecated element" stepKey="clickInsertImage" /> <waitForPageLoad stepKey="waitForiFrameToLoad" /> <!-- Switch to the Edit/Insert Image iFrame --> <comment userInput="Switching to iFrame" stepKey="insertImageiFrame"/> @@ -47,6 +54,9 @@ <click selector="{{MediaGallerySection.browseForImage}}" stepKey="clickBrowse"/> <switchToIFrame stepKey="switchOutOfIFrame"/> <waitForPageLoad stepKey="waitForPageToLoad" /> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -59,7 +69,7 @@ <executeJS function="document.querySelector('.clearlooks2 iframe').setAttribute('name', 'insert-image');" stepKey="makeIFrameInteractable2"/> <switchToIFrame selector="insert-image" stepKey="switchToIFrame2"/> <waitForElementVisible selector="{{MediaGallerySection.insertBtn}}" stepKey="waitForInsertBtnOnIFrame" /> - <fillField selector="{{MediaGallerySection.ImageDescriptionTinyMCE3}}" userInput="{{ImageUpload.content}}" stepKey="fillImageDescription" /> + <comment userInput="removing deprecated element" stepKey="fillImageDescription" /> <click selector="{{MediaGallerySection.insertBtn}}" stepKey="clickInsertBtn" /> <waitForPageLoad stepKey="wait3"/> <click selector="{{CmsNewPagePageActionsSection.expandSplitButton}}" stepKey="expandButtonMenu"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml index 162c9a60fd6b1..c0424e09f8f76 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGBlockTest.xml @@ -37,6 +37,9 @@ <waitForPageLoad stepKey="waitForPageLoad2" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -56,6 +59,10 @@ <seeElement selector="{{StorefrontBlockSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontBlockSection.imageSource(ImageUpload.fileName)}}" stepKey="assertMediaSource"/> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <amOnPage url="{{CmsPagesPage.url}}" stepKey="amOnEditPage"/> <waitForPageLoad stepKey="waitForPageLoad"/> <conditionalClick selector="{{CmsPagesPageActionsSection.clearAllButton}}" dependentSelector="{{CmsPagesPageActionsSection.activeFilters}}" stepKey="clickToResetFilter" visible="true"/> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml index 0476ecf99ad36..4f67b81446ae7 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminAddImageToWYSIWYGCMSTest.xml @@ -23,6 +23,17 @@ <actionGroup ref="EnabledWYSIWYGActionGroup" stepKey="enableWYSIWYG"/> <actionGroup ref="SwitchToVersion4ActionGroup" stepKey="switchToTinyMCE4" /> </before> + <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYGFirst"/> + <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> + <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> <argument name="CMSPage" value="$$createCMSPage$$"/> </actionGroup> @@ -32,6 +43,9 @@ <waitForPageLoad stepKey="waitForPageLoad" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -54,11 +68,5 @@ <waitForPageLoad stepKey="wait4"/> <seeElement selector="{{StorefrontCMSPageSection.mediaDescription}}" stepKey="assertMediaDescription"/> <seeElementInDOM selector="{{StorefrontCMSPageSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> - <after> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYGFirst"/> - <deleteData createDataKey="createCMSPage" stepKey="deletePreReqCMSPage" /> - <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - </after> </test> </tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml new file mode 100644 index 0000000000000..e2cf9c20627f8 --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsBlockGridUrlFilterApplierTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCmsBlockGridUrlFilterApplierTest"> + <annotations> + <features value="Cms"/> + <stories value="Filter CMS block using GET URL parameter"/> + <title value="Verify that filter is applied on block grid when filters parameter is set on url"/> + <description value="Accessing block grid url with filters parameter"/> + <severity value="MAJOR"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> + <group value="Cms"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="Sales25offBlock" stepKey="createBlock"/> + </before> + <after> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <deleteData createDataKey="createBlock" stepKey="deletePage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + <amOnPage url="{{CmsBlocksPage.url}}?filters[title]=$$createBlock.title$$" stepKey="navigateToBlockGridWithFilters"/> + <waitForPageLoad stepKey="waitForBlockGrid"/> + <see selector="{{BlockPageActionsSection.blockGridRowByTitle($$createBlock.title$$)}}" userInput="$$createBlock.title$$" stepKey="seeBlock"/> + <seeElement selector="{{BlockPageActionsSection.activeFilterDiv}}" stepKey="seeEnabledFilters"/> + <see selector="{{BlockPageActionsSection.activeFilterDiv}}" userInput="Title: $$createBlock.title$$" stepKey="seeBlockTitleFilter"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml new file mode 100644 index 0000000000000..1f1f1c98d507b --- /dev/null +++ b/app/code/Magento/Cms/Test/Mftf/Test/AdminCmsPageGridUrlFilterApplierTest.xml @@ -0,0 +1,36 @@ +<?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="AdminCmsPageGridUrlFilterApplierTest"> + <annotations> + <features value="CmsPage"/> + <stories value="Filter CMS page using GET URL parameter"/> + <title value="Verify that filter is applied on page grid when filters parameter is set on url"/> + <description value="Accessing page grid url with filters parameter"/> + <severity value="MAJOR"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4931106"/> + <group value="Cms"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <createData entity="_defaultCmsPage" stepKey="createPage"/> + </before> + <after> + <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearGridFilter"/> + <deleteData createDataKey="createPage" stepKey="deletePage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> + </after> + <amOnPage url="{{CmsPagesPage.url}}?filters[title]=$$createPage.title$$" stepKey="navigateToPageGridWithFilters"/> + <waitForPageLoad stepKey="waitForPageGrid"/> + <see selector="{{CmsPagesPageActionsSection.pagesGridRowByTitle($$createPage.title$$)}}" userInput="$$createPage.title$$" stepKey="seePage"/> + <seeElement selector="{{CmsPagesPageActionsSection.activeFilter}}" stepKey="seeEnabledFilters"/> + <see selector="{{CmsPagesPageActionsSection.activeFilter}}" userInput="Title: $$createPage.title$$" stepKey="seePageTitleFilter"/> + </test> +</tests> diff --git a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml index fe3e69880fc5c..eba7812e29a0c 100644 --- a/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml +++ b/app/code/Magento/Cms/Test/Mftf/Test/CheckStaticBlocksTest.xml @@ -15,27 +15,37 @@ <title value="Check static blocks: ID should be unique per Store View"/> <description value="Check static blocks: ID should be unique per Store View"/> <severity value="BLOCKER"/> - <testCaseId value="MAGETWO-94229"/> + <testCaseId value="MC-25828"/> <group value="Cms"/> + <group value="WYSIWYGDisabled"/> </annotations> + <before> - <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> - <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="AdminCreateWebsite"> - <argument name="newWebsiteName" value="secondWebsite"/> - <argument name="websiteCode" value="second_website"/> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="AdminCreateStore"> - <argument name="website" value="secondWebsite"/> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="AdminCreateStoreView"> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> <argument name="StoreGroup" value="customStoreGroup"/> <argument name="customStore" value="customStore"/> </actionGroup> </before> + <after> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + </after> + <!--Go to Cms blocks page--> <amOnPage url="{{CmsBlocksPage.url}}" stepKey="navigateToCMSPagesGrid"/> <waitForPageLoad stepKey="waitForPageLoad1"/> @@ -73,13 +83,5 @@ <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="ClickToSaveBlock2"/> <waitForPageLoad stepKey="waitForPageLoad9"/> <see userInput="You saved the block." stepKey="VerifyBlockIsSaved2"/> - - <after> - <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="DeleteWebsite"> - <argument name="websiteName" value="secondWebsite"/> - </actionGroup> - <actionGroup ref="DeleteCMSBlockActionGroup" stepKey="DeleteCMSBlockActionGroup"/> - <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> - </after> </test> </tests> diff --git a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php index 9b02050156cc7..f942e62588f0e 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/PageRepository/ValidationCompositeTest.php @@ -43,7 +43,7 @@ public function testConstructorValidation($validators) new ValidationComposite($this->subject, $validators); } - public function testSaveInvokesValidatorsWithSucess() + public function testSaveInvokesValidatorsWithSuccess() { $validator1 = $this->getMockForAbstractClass(ValidatorInterface::class); $validator2 = $this->getMockForAbstractClass(ValidatorInterface::class); 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 e92554094224e..502b7aa63a1a2 100644 --- a/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Cms/Test/Unit/Model/Template/FilterTest.php @@ -114,4 +114,25 @@ public function testMediaDirectiveRelativePath() ->willReturn($baseMediaDir); $this->filter->mediaDirective($construction); } + + /** + * Test using media directive with a URL path including schema. + * + * @covers \Magento\Cms\Model\Template\Filter::mediaDirective + */ + public function testMediaDirectiveURL() + { + $this->expectException(\InvalidArgumentException::class); + + $baseMediaDir = 'pub/media'; + $construction = [ + '{{media url="http://wysiwyg/images/image.jpg"}}', + 'media', + ' url="http://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 12f0791290b49..c2c748dcc7633 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 @@ -162,7 +162,7 @@ protected function setUp(): void $this->directoryMock = $this->createPartialMock( Write::class, - ['delete', 'getDriver', 'create', 'getRelativePath', 'isExist', 'isFile'] + ['delete', 'getDriver', 'create', 'getRelativePath', 'getAbsolutePath', 'isExist', 'isFile'] ); $this->directoryMock->expects( $this->any() @@ -304,6 +304,7 @@ public function testDeleteDirectoryOverRoot() $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage('Directory /storage/some/another/dir is not under storage root path.'); $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->willReturnArgument(0); + $this->directoryMock->expects($this->atLeastOnce())->method('getAbsolutePath')->willReturnArgument(0); $this->imagesStorage->deleteDirectory(self::INVALID_DIRECTORY_OVER_ROOT); } @@ -315,6 +316,7 @@ public function testDeleteRootDirectory() $this->expectException('Magento\Framework\Exception\LocalizedException'); $this->expectExceptionMessage('We can\'t delete root directory /storage/root/dir right now.'); $this->driverMock->expects($this->atLeastOnce())->method('getRealPathSafety')->willReturnArgument(0); + $this->directoryMock->expects($this->atLeastOnce())->method('getAbsolutePath')->willReturnArgument(0); $this->imagesStorage->deleteDirectory(self::STORAGE_ROOT_DIR); } diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml index d22eaf504e703..4a8002d89726d 100644 --- a/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml +++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_block_index.xml @@ -9,6 +9,11 @@ <body> <referenceContainer name="content"> <uiComponent name="cms_block_listing"/> + <block class="Magento\Backend\Block\Template" template="Magento_Cms::url_filter_applier.phtml" name="block_list_url_filter_applier"> + <arguments> + <argument name="listing_namespace" xsi:type="string">cms_block_listing</argument> + </arguments> + </block> </referenceContainer> <referenceContainer name="admin.scope.col.wrap" htmlClass="admin__old" /> <!-- ToDo UI: remove this wrapper with old styles removal. The class name "admin__old" is for tests only, we shouldn't use it in any way --> </body> diff --git a/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml b/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml index bf78b1cd49448..1256751e65742 100644 --- a/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml +++ b/app/code/Magento/Cms/view/adminhtml/layout/cms_page_index.xml @@ -10,6 +10,11 @@ <body> <referenceContainer name="content"> <uiComponent name="cms_page_listing"/> + <block class="Magento\Backend\Block\Template" template="Magento_Cms::url_filter_applier.phtml" name="page_list_url_filter_applier"> + <arguments> + <argument name="listing_namespace" xsi:type="string">cms_page_listing</argument> + </arguments> + </block> </referenceContainer> </body> </page> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml index 099efa0abdb88..d1c204c01ad1c 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/content/uploader.phtml @@ -5,17 +5,21 @@ */ /** @var $block \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Content\Uploader */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $filters = $block->getConfig()->getFilters() ?? []; $allowedExtensions = []; $blockHtmlId = $block->getHtmlId(); +$listExtensions = [[]]; foreach ($filters as $media_type) { - $allowedExtensions = array_merge($allowedExtensions, array_map(function ($fileExt) { + $listExtensions[] = array_map(function ($fileExt) { return ltrim($fileExt, '.*'); - }, $media_type['files'])); + }, $media_type['files']); } +$allowedExtensions = array_merge(...$listExtensions); + $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() ? "{action: 'resize', maxWidth: " . $block->escapeHtml($block->getImageUploadMaxWidth()) @@ -28,7 +32,9 @@ $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() <div id="<?= /* @noEscape */ $blockHtmlId ?>" class="uploader"> <span class="fileinput-button form-buttons"> <span><?= $block->escapeHtml(__('Upload Images')) ?></span> - <input class="fileupload" type="file" name="<?= $block->escapeHtmlAttr($block->getConfig()->getFileField()) ?>" data-url="<?= $block->escapeUrl($block->getConfig()->getUrl()) ?>" multiple> + <input class="fileupload" type="file" + name="<?= $block->escapeHtmlAttr($block->getConfig()->getFileField()) ?>" + data-url="<?= $block->escapeUrl($block->getConfig()->getUrl()) ?>" multiple> </span> <div class="clear"></div> <script type="text/x-magento-template" id="<?= /* @noEscape */ $blockHtmlId ?>-template"> @@ -40,7 +46,11 @@ $resizeConfig = $block->getImageUploadConfigData()->getIsResizeEnabled() <div class="clear"></div> </div> </script> - <script> + <?php $intMaxSize = $block->getFileSizeService()->getMaxFileSize(); + $resizeConfig = /* @noEscape */ $resizeConfig; + $blockHtmlId = /* @noEscape */ $blockHtmlId; + $scriptString = <<<script + require([ 'jquery', 'mage/template', @@ -50,10 +60,10 @@ require([ 'domReady!', 'mage/translate' ], function ($, mageTemplate, validator, uiAlert) { - var maxFileSize = <?= $block->escapeJs($block->getFileSizeService()->getMaxFileSize()) ?>, - allowedExtensions = '<?= $block->escapeHtml(implode(' ', $allowedExtensions)) ?>'; + var maxFileSize = {$block->escapeJs($block->getFileSizeService()->getMaxFileSize())}, + allowedExtensions = '{$block->escapeJs(implode(' ', $allowedExtensions))}'; - $('#<?= /* @noEscape */ $blockHtmlId ?> .fileupload').fileupload({ + $('#{$blockHtmlId} .fileupload').fileupload({ dataType: 'json', formData: { isAjax: 'true', @@ -63,9 +73,9 @@ require([ acceptFileTypes: /(\.|\/)(gif|jpe?g|png)$/i, allowedExtensions: allowedExtensions, maxFileSize: maxFileSize, - dropZone: $('#<?= /* @noEscape */ $blockHtmlId ?>').closest('[role="dialog"]'), + dropZone: $('#{$blockHtmlId}').closest('[role="dialog"]'), add: function (e, data) { - var progressTmpl = mageTemplate('#<?= /* @noEscape */ $blockHtmlId ?>-template'), + var progressTmpl = mageTemplate('#{$blockHtmlId}-template'), fileSize, tmpl, validationResult; @@ -109,7 +119,7 @@ require([ } }); - $(tmpl).data('image', data).appendTo('#<?= /* @noEscape */ $blockHtmlId ?>'); + $(tmpl).data('image', data).appendTo('#{$blockHtmlId}'); return true; }); @@ -146,17 +156,20 @@ require([ } }); - $('#<?= /* @noEscape */ $blockHtmlId ?> .fileupload').fileupload('option', { + $('#{$blockHtmlId} .fileupload').fileupload('option', { process: [{ action: 'load', fileTypes: /^image\/(gif|jpeg|png)$/, - maxFileSize: <?= (int) $block->getFileSizeService()->getMaxFileSize() ?> * 10 + maxFileSize: {$intMaxSize} * 10 }, - <?= /* @noEscape */ $resizeConfig ?>, + {$resizeConfig}, { action: 'save' }] }); }); -</script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml index 9603bb4f1a412..4c8d1ca7157e9 100644 --- a/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml +++ b/app/code/Magento/Cms/view/adminhtml/templates/browser/tree.phtml @@ -4,16 +4,34 @@ * See COPYING.txt for license details. */ - /** @var \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree $block */ +/** @var \Magento\Cms\Block\Adminhtml\Wysiwyg\Images\Tree $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + +<?php +/** Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div class="tree-panel" > <div class="categories-side-col"> <div class="tree-actions"> - <a onclick="jQuery('[data-role=tree]').jstree('close_all');"><?= $block->escapeHtml(__('Collapse All')) ?></a> + <a id="collapseAll"><?= $block->escapeHtml(__('Collapse All')) ?></a> <span class="separator">|</span> - <a onclick="jQuery('[data-role=tree]').jstree('open_all');"><?= $block->escapeHtml(__('Expand All')) ?></a> + <a id="expandAll"><?= $block->escapeHtml(__('Expand All')) ?></a> </div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "jQuery('[data-role=tree]').jstree('close_all');", + '#div.tree-actions a#collapseAll' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "jQuery('[data-role=tree]').jstree('open_all');", + '#div.tree-actions a#expandAll' + ) ?> </div> - <div data-role="tree" data-mage-init='<?= $block->escapeHtml($this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getTreeWidgetOptions())) ?>'> + <div data-role="tree" data-mage-init='<?= $block->escapeHtml( + $jsonHelper->jsonEncode($block->getTreeWidgetOptions()) + ) ?>'> </div> </div> diff --git a/app/code/Magento/Cms/view/adminhtml/templates/url_filter_applier.phtml b/app/code/Magento/Cms/view/adminhtml/templates/url_filter_applier.phtml new file mode 100644 index 0000000000000..a4918e86715a8 --- /dev/null +++ b/app/code/Magento/Cms/view/adminhtml/templates/url_filter_applier.phtml @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $block \Magento\Backend\Block\Template */ +/** @var \Magento\Framework\Escaper $escaper */ +?> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/grid/url-filter-applier": { + "listingNamespace": "<?= $escaper->escapeJs($block->getListingNamespace()) ?>" + } + } + } +</script> diff --git a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php index 257f8ba9bfe53..15e62d00cd9f9 100644 --- a/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php +++ b/app/code/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/View.php @@ -7,6 +7,7 @@ namespace Magento\CmsUrlRewrite\Plugin\Cms\Model\Store; +use Magento\Cms\Api\Data\PageInterface; use Magento\Cms\Api\PageRepositoryInterface; use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -21,6 +22,8 @@ */ class View { + private const ALL_STORE_VIEWS = '0'; + /** * @var UrlPersistInterface */ @@ -67,16 +70,18 @@ public function __construct( * @param ResourceStore $object * @param ResourceStore $result * @param ResourceStore $store - * @return void + * @return ResourceStore * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function afterSave(ResourceStore $object, ResourceStore $result, AbstractModel $store): void + public function afterSave(ResourceStore $object, ResourceStore $result, AbstractModel $store): ResourceStore { if ($store->isObjectNew()) { $this->urlPersist->replace( $this->generateCmsPagesUrls((int)$store->getId()) ); } + + return $result; } /** @@ -89,9 +94,8 @@ private function generateCmsPagesUrls(int $storeId): array { $rewrites = []; $urls = []; - $searchCriteria = $this->searchCriteriaBuilder->create(); - $cmsPagesCollection = $this->pageRepository->getList($searchCriteria)->getItems(); - foreach ($cmsPagesCollection as $page) { + + foreach ($this->getCmsPageItems() as $page) { $page->setStoreId($storeId); $rewrites[] = $this->cmsPageUrlRewriteGenerator->generate($page); } @@ -99,4 +103,18 @@ private function generateCmsPagesUrls(int $storeId): array return $urls; } + + /** + * Return cms page items for all store view + * + * @return PageInterface[] + */ + private function getCmsPageItems(): array + { + $searchCriteria = $this->searchCriteriaBuilder->addFilter('store_id', self::ALL_STORE_VIEWS) + ->create(); + $list = $this->pageRepository->getList($searchCriteria); + + return $list->getItems(); + } } diff --git a/app/code/Magento/CmsUrlRewrite/Test/Unit/Plugin/Cms/Model/Store/ViewTest.php b/app/code/Magento/CmsUrlRewrite/Test/Unit/Plugin/Cms/Model/Store/ViewTest.php new file mode 100644 index 0000000000000..7a0e17015dc3c --- /dev/null +++ b/app/code/Magento/CmsUrlRewrite/Test/Unit/Plugin/Cms/Model/Store/ViewTest.php @@ -0,0 +1,179 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\CmsUrlRewrite\Test\Unit\Plugin\Cms\Model\Store; + +use Magento\Cms\Api\Data\PageSearchResultsInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Cms\Model\Page; +use Magento\CmsUrlRewrite\Model\CmsPageUrlRewriteGenerator; +use Magento\CmsUrlRewrite\Plugin\Cms\Model\Store\View; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Store\Model\ResourceModel\Store; +use Magento\UrlRewrite\Model\UrlPersistInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\CmsUrlRewrite\Plugin\Cms\Model\Store\View. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ViewTest extends TestCase +{ + private const STUB_STORE_ID = 777; + private const STUB_URL_REWRITE = ['cms/page/view']; + + /** + * @var View + */ + private $model; + + /** + * @var SearchCriteria|MockObject + */ + private $searchCriteriaMock; + + /** + * @var PageSearchResultsInterface|MockObject + */ + private $pageSearchResultMock; + + /** + * @var Page|MockObject + */ + private $pageMock; + + /** + * @var Store|MockObject + */ + private $storeObjectMock; + + /** + * @var AbstractModel|MockObject + */ + private $abstractModelMock; + + /** + * @var UrlPersistInterface|MockObject + */ + private $urlPersistMock; + + /** + * @var PageRepositoryInterface|MockObject + */ + private $pageRepositoryMock; + + /** + * @var CmsPageUrlRewriteGenerator|MockObject + */ + private $cmsPageUrlGeneratorMock; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = new ObjectManager($this); + + $this->storeObjectMock = $this->createMock(Store::class); + $this->searchCriteriaMock = $this->createMock(SearchCriteria::class); + $this->pageSearchResultMock = $this->createMock(PageSearchResultsInterface::class); + + $this->pageMock = $this->getMockBuilder(Page::class) + ->disableOriginalConstructor() + ->addMethods(['setStoreId']) + ->getMock(); + + $this->abstractModelMock = $this->getMockBuilder(AbstractModel::class) + ->onlyMethods(['isObjectNew', 'getId']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->urlPersistMock = $this->createMock(UrlPersistInterface::class); + $this->pageRepositoryMock = $this->createMock(PageRepositoryInterface::class); + $this->cmsPageUrlGeneratorMock = $this->createMock(CmsPageUrlRewriteGenerator::class); + + $searchCriteriaBuilderMock = $this->createMock(SearchCriteriaBuilder::class); + $searchCriteriaBuilderMock->expects($this->any()) + ->method('addFilter') + ->willReturnSelf(); + $searchCriteriaBuilderMock->expects($this->any()) + ->method('create') + ->willReturn($this->searchCriteriaMock); + + $this->model = $objectManager->getObject( + View::class, + [ + 'urlPersist' => $this->urlPersistMock, + 'searchCriteriaBuilder' => $searchCriteriaBuilderMock, + 'pageRepository' => $this->pageRepositoryMock, + 'cmsPageUrlRewriteGenerator' => $this->cmsPageUrlGeneratorMock, + ] + ); + } + + /** + * After save when object is not new + * + * @return void + */ + public function testAfterSaveObjectIsNotNew(): void + { + $storeResult = clone $this->storeObjectMock; + + $this->abstractModelMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn(false); + + $this->urlPersistMock->expects($this->never()) + ->method('replace'); + + $result = $this->model->afterSave($this->storeObjectMock, $storeResult, $this->abstractModelMock); + $this->assertEquals($storeResult, $result); + } + + /** + * After save when object is new + * + * @return void + */ + public function testAfterSaveObjectIsNew(): void + { + $storeResult = clone $this->storeObjectMock; + + $this->abstractModelMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn(true); + $this->abstractModelMock->expects($this->once()) + ->method('getId') + ->willReturn(self::STUB_STORE_ID); + $this->pageRepositoryMock->expects($this->once()) + ->method('getList') + ->with($this->searchCriteriaMock) + ->willReturn($this->pageSearchResultMock); + $this->pageSearchResultMock->expects($this->once()) + ->method('getItems') + ->willReturn([$this->pageMock]); + $this->pageMock->expects($this->once()) + ->method('setStoreId') + ->with(self::STUB_STORE_ID); + $this->cmsPageUrlGeneratorMock->expects($this->once()) + ->method('generate') + ->with($this->pageMock) + ->willReturn(self::STUB_URL_REWRITE); + $this->urlPersistMock->expects($this->once()) + ->method('replace') + ->with(self::STUB_URL_REWRITE); + + $result = $this->model->afterSave($this->storeObjectMock, $storeResult, $this->abstractModelMock); + $this->assertEquals($storeResult, $result); + } +} diff --git a/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php b/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php index d2b87b1ae2841..10f9af9268ae6 100644 --- a/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/EnvironmentConfigSource.php @@ -15,7 +15,7 @@ * Class for retrieving configurations from environment variables. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class EnvironmentConfigSource implements ConfigSourceInterface { @@ -47,7 +47,7 @@ public function __construct( /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function get($path = '') { diff --git a/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php b/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php index 40978320797d4..a22639ec51641 100644 --- a/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/InitialSnapshotConfigSource.php @@ -12,7 +12,7 @@ /** * The source with previously imported configuration. * @api - * @since 100.2.0 + * @since 101.0.0 */ class InitialSnapshotConfigSource implements ConfigSourceInterface { @@ -45,7 +45,7 @@ public function __construct(FlagManager $flagManager, DataObjectFactory $dataObj * Snapshots are stored in flags. * * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function get($path = '') { diff --git a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php index 7926708772a9f..641db6d035ca5 100644 --- a/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php +++ b/app/code/Magento/Config/App/Config/Source/RuntimeConfigSource.php @@ -70,7 +70,8 @@ public function __construct( public function get($path = '') { $data = new DataObject($this->deploymentConfig->isDbAvailable() ? $this->loadConfig() : []); - return $data->getData($path) ?: []; + + return $data->getData($path) !== null ? $data->getData($path) : null; } /** diff --git a/app/code/Magento/Config/Block/System/Config/Edit.php b/app/code/Magento/Config/Block/System/Config/Edit.php index ba27cb33b20f0..7955f28f59f4e 100644 --- a/app/code/Magento/Config/Block/System/Config/Edit.php +++ b/app/code/Magento/Config/Block/System/Config/Edit.php @@ -121,6 +121,7 @@ public function getSaveUrl() /** * @return string + * @since 101.1.0 */ public function getConfigSearchParamsJson() { diff --git a/app/code/Magento/Config/Block/System/Config/Form.php b/app/code/Magento/Config/Block/System/Config/Form.php index 8378c058c1955..8e07ef8b45203 100644 --- a/app/code/Magento/Config/Block/System/Config/Form.php +++ b/app/code/Magento/Config/Block/System/Config/Form.php @@ -838,10 +838,10 @@ private function getAppConfigDataValue($path) * Gets instance of ElementVisibilityInterface. * * @return ElementVisibilityInterface - * @deprecated 100.2.0 Added to not break backward compatibility of the constructor signature + * @deprecated 101.0.0 Added to not break backward compatibility of the constructor signature * by injecting the new dependency directly. * The method can be removed in a future major release, when constructor signature can be changed. - * @since 100.2.0 + * @since 101.0.0 */ public function getElementVisibility() { diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field.php b/app/code/Magento/Config/Block/System/Config/Form/Field.php index ac4a85b7d3bc6..9801467a4cf61 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field.php @@ -6,6 +6,10 @@ namespace Magento\Config\Block\System\Config\Form; +use Magento\Framework\App\ObjectManager; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Render field html element in Stores Configuration * @@ -17,6 +21,25 @@ class Field extends \Magento\Backend\Block\Template implements \Magento\Framework\Data\Form\Element\Renderer\RendererInterface { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Retrieve element HTML markup * @@ -108,7 +131,12 @@ protected function _renderInheritCheckbox(\Magento\Framework\Data\Form\Element\A '[inherit]" type="checkbox" value="1"' . ' class="checkbox config-inherit" ' . $checkedHtml . $disabled . - ' onclick="toggleValueElements(this, Element.previous(this.parentNode))" /> '; + ' />'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleValueElements(this, Element.previous(this.parentNode))", + 'input#' . $htmlId . '_inherit' + ); $html .= '<label for="' . $htmlId . '_inherit" class="inherit">' . $this->_getInheritCheckboxLabel( $element ) . '</label>'; @@ -174,7 +202,12 @@ protected function _renderHint(\Magento\Framework\Data\Form\Element\AbstractElem { $html = '<td class="">'; if ($element->getHint()) { - $html .= '<div class="hint"><div style="display: none;">' . $element->getHint() . '</div></div>'; + $html .= '<div class="hint"><div id="hint_' . $element->getHtmlId() . '">' . + $element->getHint() . '</div></div>'; + $html .= /* @noEscape */ $this->secureRenderer->renderStyleAsTag( + "display: none;", + 'div#hint_' . $element->getHtmlId() + ); } $html .= '</td>'; return $html; diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php b/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php index 3bb4632ee4517..cc6b7e4b441dc 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/FieldArray/AbstractFieldArray.php @@ -282,7 +282,7 @@ public function getColumns() /** * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getAddButtonLabel() { diff --git a/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php b/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php index b62584537e2b3..9a0bc416d4d46 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Field/Select/Allowspecific.php @@ -11,8 +11,40 @@ */ namespace Magento\Config\Block\System\Config\Form\Field\Select; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Data\Form\Element\Factory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Allowspecific extends \Magento\Framework\Data\Form\Element\Select { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * Allowspecific constructor. + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection + * @param Escaper $escaper + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct( + Factory $factoryElement, + CollectionFactory $factoryCollection, + Escaper $escaper, + $data = [], + ?SecureHtmlRenderer $secureRenderer = null + ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + /** * Add additional Javascript code * @@ -25,7 +57,6 @@ public function getAfterElementHtml() $useDefaultElementId = $countryListId . '_inherit'; $elementJavaScript = <<<HTML -<script type="text/javascript"> //<![CDATA[ document.getElementById('{$elementId}').addEventListener('change', function(event) { var isCountrySpecific = event.target.value == 1, @@ -42,13 +73,15 @@ public function getAfterElementHtml() } }); //]]> -</script> HTML; - return $elementJavaScript . parent::getAfterElementHtml(); + return $this->secureRenderer->renderTag('script', [], $elementJavaScript, false) . + parent::getAfterElementHtml(); } /** + * Return generated html. + * * @return string */ public function getHtml() @@ -61,6 +94,8 @@ public function getHtml() } /** + * Return country specific element id. + * * @return string */ protected function _getSpecificCountryElementId() diff --git a/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php b/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php index 05c98a3eba99d..0e918c23857ac 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Fieldset.php @@ -9,7 +9,9 @@ */ namespace Magento\Config\Block\System\Config\Form; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @api @@ -35,21 +37,29 @@ class Fieldset extends \Magento\Backend\Block\AbstractBlock implements */ protected $isCollapsedDefault = false; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Backend\Model\Auth\Session $authSession * @param \Magento\Framework\View\Helper\Js $jsHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Backend\Model\Auth\Session $authSession, \Magento\Framework\View\Helper\Js $jsHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsHelper = $jsHelper; $this->_authSession = $authSession; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -71,6 +81,8 @@ public function render(AbstractElement $element) } /** + * Return children elements html. + * * @param AbstractElement $element * @return string * @since 100.1.0 @@ -84,6 +96,8 @@ protected function _getChildrenElementsHtml(AbstractElement $element) . '<td colspan="4">' . $field->toHtml() . '</td></tr>'; } else { $elements .= $field->toHtml(); + $styleTag = $this->addVisibilityTag($field); + $elements .= $styleTag; } } @@ -156,16 +170,20 @@ protected function _getFrontendClass($element) */ protected function _getHeaderTitleHtml($element) { + $styleTag = $this->addVisibilityTag($element); return '<a id="' . $element->getHtmlId() . '-head" href="#' . $element->getHtmlId() . - '-link" onclick="Fieldset.toggleCollapse(\'' . - $element->getHtmlId() . - '\', \'' . - $this->getUrl( - '*/*/state' - ) . '\'); return false;">' . $element->getLegend() . '</a>'; + '-link">' . $element->getLegend() . '</a>' . + $styleTag . + /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault();' . + "Fieldset.toggleCollapse('" . $element->getHtmlId() . "', '" . + $this->_urlBuilder->getUrl('*/*/state') . "'); return false;", + 'a#' . $element->getHtmlId() . '-head' + ); } /** @@ -194,6 +212,7 @@ protected function _getFieldsetCss() /** * Return footer html for fieldset + * * Add extra tooltip comments to elements * * @param AbstractElement $element @@ -205,10 +224,14 @@ protected function _getFooterHtml($element) foreach ($element->getElements() as $field) { if ($field->getTooltip()) { $html .= sprintf( - '<div id="row_%s_comment" class="system-tooltip-box" style="display:none;">%s</div>', + '<div id="row_%s_comment" class="system-tooltip-box">%s</div>', $field->getId(), $field->getTooltip() ); + $html .= $this->secureRenderer->renderStyleAsTag( + 'display:none;', + '#row_' . $field->getId() . '_comment' + ); } } $html .= '</fieldset>' . $this->_getExtraJs($element); @@ -233,6 +256,7 @@ protected function _getExtraJs($element) { $htmlId = $element->getHtmlId(); $output = "require(['prototype'], function(){Fieldset.applyCollapse('{$htmlId}');});"; + return $this->_jsHelper->getScript($output); } @@ -250,10 +274,70 @@ protected function _isCollapseState($element) return true; } + if ($this->isCollapseStateByDependentField($element)) { + return false; + } + $extra = $this->_authSession->getUser()->getExtra(); + if (isset($extra['configState'][$element->getId()])) { return $extra['configState'][$element->getId()]; } return $this->isCollapsedDefault; } + + /** + * Check if element should be collapsed by dependent field value. + * + * @param AbstractElement $element + * @return bool + */ + private function isCollapseStateByDependentField(AbstractElement $element): bool + { + if (!empty($element->getGroup()['depends']['fields'])) { + foreach ($element->getGroup()['depends']['fields'] as $dependFieldData) { + if (is_array($dependFieldData) && isset($dependFieldData['value'], $dependFieldData['id'])) { + $fieldSetForm = $this->getForm(); + $dependentFieldConfigValue = $this->_scopeConfig->getValue( + $dependFieldData['id'], + $fieldSetForm->getScope(), + $fieldSetForm->getScopeCode() + ); + + if ($dependFieldData['value'] !== $dependentFieldConfigValue) { + return true; + } + } + } + } + + return false; + } + + /** + * If element or it's parent depends on other element we hide it during page load. + * + * @param AbstractElement $field + * @return string + */ + private function addVisibilityTag(AbstractElement $field): string + { + $elementId = ''; + $styleTag = ''; + + if (!empty($field->getFieldConfig()['depends']['fields'])) { + $elementId = '#row_' . $field->getHtmlId(); + } elseif (!empty($field->getGroup()['depends']['fields'])) { + $elementId = '#' . $field->getHtmlId() . '-head'; + } + + if (!empty($elementId)) { + $styleTag .= $this->secureRenderer->renderStyleAsTag( + 'display: none;', + $elementId + ); + } + + return $styleTag; + } } diff --git a/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php b/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php index 99fa7b5addee8..4c2e6873d9593 100644 --- a/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php +++ b/app/code/Magento/Config/Block/System/Config/Form/Fieldset/Modules/DisableOutput.php @@ -10,7 +10,7 @@ * on the store settings page. * * @method \Magento\Config\Block\System\Config\Form getForm() - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -22,25 +22,25 @@ class DisableOutput extends \Magento\Config\Block\System\Config\Form\Fieldset { /** * @var \Magento\Framework\DataObject - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_dummyElement; /** * @var \Magento\Config\Block\System\Config\Form\Field - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_fieldRenderer; /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_values; /** * @var \Magento\Framework\Module\ModuleListInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_moduleList; @@ -64,7 +64,7 @@ public function __construct( /** * {@inheritdoc} - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -97,7 +97,7 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Framework\DataObject */ protected function _getDummyElement() @@ -109,7 +109,7 @@ protected function _getDummyElement() } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Config\Block\System\Config\Form\Field */ protected function _getFieldRenderer() @@ -123,7 +123,7 @@ protected function _getFieldRenderer() } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return array */ protected function _getValues() @@ -140,7 +140,7 @@ protected function _getValues() /** * @param \Magento\Framework\Data\Form\Element\Fieldset $fieldset * @param string $moduleName - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return mixed */ protected function _getFieldHtml($fieldset, $moduleName) diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php index 1b287573a9285..e95797658bb33 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorFactory.php @@ -17,7 +17,7 @@ * @see ConfigSetCommand * * @api - * @since 100.2.0 + * @since 101.0.0 */ class ConfigSetProcessorFactory { @@ -68,7 +68,7 @@ public function __construct( * @return ConfigSetProcessorInterface New processor instance * @throws ConfigurationMismatchException If processor type is not exists in processors array * or declared class has wrong implementation - * @since 100.2.0 + * @since 101.0.0 */ public function create($processorName) { diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php index 01aa03b188e62..0abebb604d36f 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ConfigSetProcessorInterface.php @@ -14,7 +14,7 @@ * @see ConfigSetCommand * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface ConfigSetProcessorInterface { @@ -27,7 +27,7 @@ interface ConfigSetProcessorInterface * @param string $scopeCode The scope code * @return void * @throws CouldNotSaveException An exception on processing error - * @since 100.2.0 + * @since 101.0.0 */ public function process($path, $value, $scope, $scopeCode); } diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php index c622a48b7f2c8..d49d65774d0d2 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/DefaultProcessor.php @@ -21,7 +21,7 @@ * * @inheritdoc * @api - * @since 100.2.0 + * @since 101.0.0 */ class DefaultProcessor implements ConfigSetProcessorInterface { @@ -76,7 +76,7 @@ public function __construct( * Requires installed application. * * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function process($path, $value, $scope, $scopeCode) { diff --git a/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php b/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php index fcd7c0d5335b1..aa33c96c7e0e2 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php +++ b/app/code/Magento/Config/Console/Command/ConfigSet/ProcessorFacade.php @@ -24,7 +24,7 @@ * @see ConfigSetCommand * * @api - * @since 100.2.0 + * @since 101.0.0 */ class ProcessorFacade { @@ -99,8 +99,8 @@ public function __construct( * @param boolean $lock The lock flag * @return string Processor response message * @throws ValidatorException If some validation is wrong - * @since 100.2.0 - * @deprecated + * @since 101.0.0 + * @deprecated 101.0.4 * @see processWithLockTarget() */ public function process($path, $value, $scope, $scopeCode, $lock) @@ -119,6 +119,7 @@ public function process($path, $value, $scope, $scopeCode, $lock) * @param string $lockTarget * @return string Processor response message * @throws ValidatorException If some validation is wrong + * @since 101.0.4 */ public function processWithLockTarget( $path, diff --git a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php index 999d8e41af5bc..f278a07cc6806 100644 --- a/app/code/Magento/Config/Console/Command/ConfigSetCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigSetCommand.php @@ -24,7 +24,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @since 100.2.0 + * @since 101.0.0 */ class ConfigSetCommand extends Command { @@ -86,7 +86,7 @@ public function __construct( /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ protected function configure() { @@ -141,7 +141,7 @@ protected function configure() * * @param InputInterface $input * @param OutputInterface $output - * @since 100.2.0 + * @since 101.0.0 * @return int|null */ protected function execute(InputInterface $input, OutputInterface $output) diff --git a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php index aeb57010e4969..2465eecec71dc 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php +++ b/app/code/Magento/Config/Console/Command/ConfigShow/ValueProcessor.php @@ -19,7 +19,7 @@ * Class processes values using backend model which declared in system.xml. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class ValueProcessor { @@ -83,7 +83,7 @@ public function __construct( * @param string $value The value to process * @param string $path The configuration path for getting backend model. E.g. scope_id/group_id/field_id * @return string processed value result - * @since 100.2.0 + * @since 101.0.0 */ public function process($scope, $scopeCode, $value, $path) { diff --git a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php index 2d3dabdb24e67..53c7445f9508a 100644 --- a/app/code/Magento/Config/Console/Command/ConfigShowCommand.php +++ b/app/code/Magento/Config/Console/Command/ConfigShowCommand.php @@ -16,12 +16,15 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Config\Model\Config\PathValidatorFactory; /** * Command provides possibility to show saved system configuration. * * @api - * @since 100.2.0 + * @since 101.0.0 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigShowCommand extends Command { @@ -78,29 +81,47 @@ class ConfigShowCommand extends Command */ private $inputPath; + /** + * @var PathValidatorFactory + */ + private $pathValidatorFactory; + + /** + * @var EmulatedAdminhtmlAreaProcessor + */ + private $emulatedAreaProcessor; + /** * @param ValidatorInterface $scopeValidator * @param ConfigSourceInterface $configSource * @param ConfigPathResolver $pathResolver * @param ValueProcessor $valueProcessor + * @param PathValidatorFactory|null $pathValidatorFactory + * @param EmulatedAdminhtmlAreaProcessor|null $emulatedAreaProcessor * @internal param ScopeConfigInterface $appConfig */ public function __construct( ValidatorInterface $scopeValidator, ConfigSourceInterface $configSource, ConfigPathResolver $pathResolver, - ValueProcessor $valueProcessor + ValueProcessor $valueProcessor, + ?PathValidatorFactory $pathValidatorFactory = null, + ?EmulatedAdminhtmlAreaProcessor $emulatedAreaProcessor = null ) { parent::__construct(); $this->scopeValidator = $scopeValidator; $this->configSource = $configSource; $this->pathResolver = $pathResolver; $this->valueProcessor = $valueProcessor; + $this->pathValidatorFactory = $pathValidatorFactory + ?: ObjectManager::getInstance()->get(PathValidatorFactory::class); + $this->emulatedAreaProcessor = $emulatedAreaProcessor + ?: ObjectManager::getInstance()->get(EmulatedAdminhtmlAreaProcessor::class); } /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ protected function configure() { @@ -136,8 +157,8 @@ protected function configure() * Shows error message if configuration for given path doesn't exist * or scope/scope-code doesn't pass validation. * - * {@inheritdoc} - * @since 100.2.0 + * @inheritdoc + * @since 101.0.0 */ protected function execute(InputInterface $input, OutputInterface $output) { @@ -146,17 +167,17 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->scopeCode = $input->getOption(self::INPUT_OPTION_SCOPE_CODE); $this->inputPath = trim($input->getArgument(self::INPUT_ARGUMENT_PATH), '/'); - $this->scopeValidator->isValid($this->scope, $this->scopeCode); - $configPath = $this->pathResolver->resolve($this->inputPath, $this->scope, $this->scopeCode); - $configValue = $this->configSource->get($configPath); + $configValue = $this->emulatedAreaProcessor->process(function () { + $this->scopeValidator->isValid($this->scope, $this->scopeCode); + if ($this->inputPath) { + $pathValidator = $this->pathValidatorFactory->create(); + $pathValidator->validate($this->inputPath); + } - if (empty($configValue)) { - $output->writeln(sprintf( - '<error>%s</error>', - __('Configuration for path: "%1" doesn\'t exist', $this->inputPath)->render() - )); - return Cli::RETURN_FAILURE; - } + $configPath = $this->pathResolver->resolve($this->inputPath, $this->scope, $this->scopeCode); + + return $this->configSource->get($configPath); + }); $this->outputResult($output, $configValue, $this->inputPath); return Cli::RETURN_SUCCESS; diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php b/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php index 274ee3b8d2a6e..c644ec8eb15cc 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/AbstractConfig.php @@ -31,7 +31,7 @@ abstract class AbstractConfig extends \Magento\Backend\App\AbstractAction protected $_configStructure; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_sectionChecker; diff --git a/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php b/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php index 0af19b83a5a3f..b656498e97dba 100644 --- a/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php +++ b/app/code/Magento/Config/Controller/Adminhtml/System/ConfigSectionChecker.php @@ -9,7 +9,7 @@ use Magento\Framework\Exception\NotFoundException; /** - * @deprecated 100.2.0 - unused class. + * @deprecated 101.0.0 - unused class. * @see \Magento\Config\Model\Config\Structure\Element\Section::isAllowed() */ class ConfigSectionChecker diff --git a/app/code/Magento/Config/Model/Config.php b/app/code/Magento/Config/Model/Config.php index 356c6ca17da18..f61e99529c3cc 100644 --- a/app/code/Magento/Config/Model/Config.php +++ b/app/code/Magento/Config/Model/Config.php @@ -208,6 +208,7 @@ public function save() ); $groupChangedPaths = $this->getChangedPaths($sectionId, $groupId, $groupData, $oldConfig, $extraOldGroups); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $changedPaths = \array_merge($changedPaths, $groupChangedPaths); } @@ -370,6 +371,7 @@ private function getChangedPaths( $oldConfig, $extraOldGroups ); + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $changedPaths = \array_merge($changedPaths, $subGroupChangedPaths); } } @@ -435,11 +437,11 @@ protected function _processGroup( if (!isset($fieldData['value'])) { $fieldData['value'] = null; } - + if ($field->getType() == 'multiline' && is_array($fieldData['value'])) { $fieldData['value'] = trim(implode(PHP_EOL, $fieldData['value'])); } - + $data = [ 'field' => $fieldId, 'groups' => $groups, @@ -453,7 +455,7 @@ protected function _processGroup( $backendModel->addData($data); $this->_checkSingleStoreMode($field, $backendModel); - $path = $this->getFieldPath($field, $fieldId, $extraOldGroups, $oldConfig); + $path = $this->getFieldPath($field, $fieldId, $oldConfig, $extraOldGroups); $backendModel->setPath($path)->setValue($fieldData['value']); $inherit = !empty($fieldData['inherit']); diff --git a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php index 7bbbafe826422..e6acd431be3d5 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php +++ b/app/code/Magento/Config/Model/Config/Backend/Admin/Robots.php @@ -13,7 +13,7 @@ use Magento\Framework\App\ObjectManager; /** - * @deprecated 100.2.0 robots.txt file is no longer stored in filesystem. It generates as response on request. + * @deprecated 100.1.7 robots.txt file is no longer stored in filesystem. It generates as response on request. */ class Robots extends \Magento\Framework\App\Config\Value { diff --git a/app/code/Magento/Config/Model/Config/Backend/Baseurl.php b/app/code/Magento/Config/Model/Config/Backend/Baseurl.php index a218d2b0d07e6..5e43e53f1b64f 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Baseurl.php +++ b/app/code/Magento/Config/Model/Config/Backend/Baseurl.php @@ -231,7 +231,7 @@ public function afterSave() /** * Get URL Validator * - * @deprecated 100.2.0 + * @deprecated 100.1.12 * @return UrlValidator */ private function getUrlValidator() diff --git a/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php b/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php index 7ff1d367c5e58..b0db20e2fb25a 100644 --- a/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php +++ b/app/code/Magento/Config/Model/Config/Backend/Currency/Allow.php @@ -81,7 +81,7 @@ public function afterSave() /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ protected function _getAllowedCurrencies() { diff --git a/app/code/Magento/Config/Model/Config/Export/ExcludeList.php b/app/code/Magento/Config/Model/Config/Export/ExcludeList.php index e556c42f66a99..e7efb5ac50b79 100644 --- a/app/code/Magento/Config/Model/Config/Export/ExcludeList.php +++ b/app/code/Magento/Config/Model/Config/Export/ExcludeList.php @@ -8,7 +8,7 @@ /** * Class ExcludeList contains list of config fields which should be excluded from config export file. * - * @deprecated 100.2.0 because in Magento since version 2.2.0 there are several + * @deprecated 101.0.0 because in Magento since version 2.2.0 there are several * types for configuration fields that require special processing. * @see \Magento\Config\Model\Config\TypePool */ @@ -32,7 +32,7 @@ public function __construct(array $configs = []) * * @param string $path * @return bool - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function isPresent($path) { @@ -43,7 +43,7 @@ public function isPresent($path) * Retrieves all excluded field paths for export * * @return array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function get() { diff --git a/app/code/Magento/Config/Model/Config/Importer.php b/app/code/Magento/Config/Model/Config/Importer.php index a54af2ead5048..a870b5f2403c3 100644 --- a/app/code/Magento/Config/Model/Config/Importer.php +++ b/app/code/Magento/Config/Model/Config/Importer.php @@ -23,7 +23,7 @@ * {@inheritdoc} * @see \Magento\Deploy\Console\Command\App\ConfigImport\Importer * @api - * @since 100.2.0 + * @since 101.0.0 */ class Importer implements ImporterInterface { @@ -103,7 +103,7 @@ public function __construct( * or current value is different from previously imported. * * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function import(array $data) { @@ -145,7 +145,7 @@ public function import(array $data) /** * @inheritdoc * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @since 100.2.0 + * @since 101.0.0 */ public function getWarningMessages(array $data) { diff --git a/app/code/Magento/Config/Model/Config/Parser/Comment.php b/app/code/Magento/Config/Model/Config/Parser/Comment.php index b46b2308f8df5..4a749ba030d80 100644 --- a/app/code/Magento/Config/Model/Config/Parser/Comment.php +++ b/app/code/Magento/Config/Model/Config/Parser/Comment.php @@ -19,7 +19,7 @@ * It is used to parse config paths from * comment section in provided configuration file. * @api - * @since 100.2.0 + * @since 101.0.0 */ class Comment implements CommentParserInterface { @@ -84,7 +84,7 @@ public function __construct( * @param string $fileName the basename of file * @return array * @throws FileSystemException - * @since 100.2.0 + * @since 101.0.0 */ public function execute($fileName) { diff --git a/app/code/Magento/Config/Model/Config/PathValidator.php b/app/code/Magento/Config/Model/Config/PathValidator.php index 68363bef69d91..bc4f863b7b05f 100644 --- a/app/code/Magento/Config/Model/Config/PathValidator.php +++ b/app/code/Magento/Config/Model/Config/PathValidator.php @@ -11,7 +11,7 @@ /** * Validates the config path by config structure schema. * @api - * @since 100.2.0 + * @since 101.0.0 */ class PathValidator { @@ -36,7 +36,7 @@ public function __construct(Structure $structure) * @param string $path The config path * @return true The result of validation * @throws ValidatorException If provided path is not valid - * @since 100.2.0 + * @since 101.0.0 */ public function validate($path) { diff --git a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php index 5f8dc3f7ab4a7..bf59c729790a7 100644 --- a/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php +++ b/app/code/Magento/Config/Model/Config/Reader/Source/Deployed/DocumentRoot.php @@ -14,7 +14,7 @@ * Class DocumentRoot * @package Magento\Config\Model\Config\Reader\Source\Deployed * @api - * @since 100.2.0 + * @since 101.0.0 */ class DocumentRoot { @@ -37,7 +37,7 @@ public function __construct(DeploymentConfig $config) * deployment configuration. * * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getPath() { @@ -50,7 +50,7 @@ public function getPath() * likely be extended to control other areas). * * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isPub() { diff --git a/app/code/Magento/Config/Model/Config/Structure.php b/app/code/Magento/Config/Model/Config/Structure.php index a16920f0dc527..437aca04ec577 100644 --- a/app/code/Magento/Config/Model/Config/Structure.php +++ b/app/code/Magento/Config/Model/Config/Structure.php @@ -185,7 +185,7 @@ public function getElement($path) * * @param string $path The configuration path * @return \Magento\Config\Model\Config\Structure\ElementInterface|null - * @since 100.2.0 + * @since 101.0.0 */ public function getElementByConfigPath($path) { @@ -369,7 +369,7 @@ protected function _getGroupFieldPathsByAttribute(array $fields, $parentPath, $a * ``` * * @return array An array of config path to config structure path map - * @since 100.2.0 + * @since 100.1.12 */ public function getFieldPaths() { diff --git a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php index c4a0cb5e886d9..8ce5c7f5f13d2 100644 --- a/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php +++ b/app/code/Magento/Config/Model/Config/Structure/AbstractElement.php @@ -225,10 +225,10 @@ public function getPath($fieldPrefix = '') * Get instance of ElementVisibilityInterface. * * @return ElementVisibilityInterface - * @deprecated 100.2.0 Added to not break backward compatibility of the constructor signature + * @deprecated 101.0.0 Added to not break backward compatibility of the constructor signature * by injecting the new dependency directly. * The method can be removed in a future major release, when constructor signature can be changed. - * @since 100.2.0 + * @since 101.0.0 */ public function getElementVisibility() { diff --git a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php index 252042a41cc1d..568adceda9430 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php +++ b/app/code/Magento/Config/Model/Config/Structure/ConcealInProductionConfigList.php @@ -11,8 +11,9 @@ * Defines status of visibility of form elements on Stores > Settings > Configuration page * in Admin Panel in Production mode. * @api - * @deprecated class location was changed + * @deprecated 101.0.6 class location was changed * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction + * @since 101.0.0 */ class ConcealInProductionConfigList implements ElementVisibilityInterface { @@ -55,7 +56,8 @@ public function __construct(State $state, array $configs = []) /** * @inheritdoc - * @deprecated + * @deprecated 101.0.6 + * @since 101.0.0 */ public function isHidden($path) { @@ -68,7 +70,8 @@ public function isHidden($path) /** * @inheritdoc - * @deprecated + * @deprecated 101.0.6 + * @since 101.0.0 */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php b/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php index 5ba6221601725..b29887219a258 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementInterface.php @@ -10,7 +10,7 @@ /** * @api * @since 100.0.2 - * @deprecated + * @deprecated 101.1.0 * @see StructureElementInterface */ interface ElementInterface diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php index ec57d629e61da..c5a0b6127f122 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProduction.php @@ -16,6 +16,7 @@ * Defines status of visibility of form elements on Stores > Settings > Configuration page * in Admin Panel in Production mode. * @api + * @since 101.0.6 */ class ConcealInProduction implements ElementVisibilityInterface { @@ -80,7 +81,7 @@ public function __construct(State $state, array $configs = [], array $exemptions /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.6 */ public function isHidden($path) { @@ -105,7 +106,7 @@ public function isHidden($path) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.6 */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php index 29148a244dcc6..ee6be789232e8 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibility/ConcealInProductionWithoutScdOnDemand.php @@ -18,6 +18,7 @@ * @see \Magento\Config\Model\Config\Structure\ElementVisibility\ConcealInProduction * * @api + * @since 101.0.6 */ class ConcealInProductionWithoutScdOnDemand implements ElementVisibilityInterface { @@ -50,6 +51,7 @@ public function __construct( /** * @inheritdoc + * @since 101.0.6 */ public function isHidden($path): bool { @@ -61,6 +63,7 @@ public function isHidden($path): bool /** * @inheritdoc + * @since 101.0.6 */ public function isDisabled($path): bool { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php index 23074297e6323..e5b60c596d713 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityComposite.php @@ -11,7 +11,7 @@ * Contains list of classes which implement ElementVisibilityInterface for * checking of visibility of form elements on Stores > Settings > Configuration page in Admin Panel. * @api - * @since 100.2.0 + * @since 101.0.0 */ class ElementVisibilityComposite implements ElementVisibilityInterface { @@ -49,7 +49,7 @@ public function __construct(array $visibility = []) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function isHidden($path) { @@ -64,7 +64,7 @@ public function isHidden($path) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function isDisabled($path) { diff --git a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php index 21dff52843765..a11a549eebc35 100644 --- a/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php +++ b/app/code/Magento/Config/Model/Config/Structure/ElementVisibilityInterface.php @@ -9,7 +9,7 @@ * Checks visibility status of form elements on Stores > Settings > Configuration page in Admin Panel * by their paths in the system.xml structure. * @api - * @since 100.2.0 + * @since 101.0.0 */ interface ElementVisibilityInterface { @@ -25,7 +25,7 @@ interface ElementVisibilityInterface * * @param string $path The path of form element in the system.xml structure * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isDisabled($path); @@ -34,7 +34,7 @@ public function isDisabled($path); * * @param string $path The path of form element in the system.xml structure * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isHidden($path); } diff --git a/app/code/Magento/Config/Model/Config/StructureElementInterface.php b/app/code/Magento/Config/Model/Config/StructureElementInterface.php index 946d6e3c766a4..e1e855d37325b 100644 --- a/app/code/Magento/Config/Model/Config/StructureElementInterface.php +++ b/app/code/Magento/Config/Model/Config/StructureElementInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 101.1.0 */ interface StructureElementInterface extends Structure\ElementInterface { @@ -15,6 +16,7 @@ interface StructureElementInterface extends Structure\ElementInterface * * @param string $fieldPrefix * @return string + * @since 101.1.0 */ public function getPath($fieldPrefix = ''); } diff --git a/app/code/Magento/Config/Model/Config/TypePool.php b/app/code/Magento/Config/Model/Config/TypePool.php index e41ff8e88a595..9080db81d1f55 100644 --- a/app/code/Magento/Config/Model/Config/TypePool.php +++ b/app/code/Magento/Config/Model/Config/TypePool.php @@ -13,7 +13,7 @@ * Used when you need to know if the configuration path belongs to a certain type. * Participates in the mechanism for creating the configuration dump file. * @api - * @since 100.2.0 + * @since 101.0.0 */ class TypePool { @@ -52,7 +52,7 @@ class TypePool * Checks if the configuration path is contained in exclude list. * * @var ExcludeList - * @deprecated 100.2.0 We use it only to support backward compatibility. If some configurations + * @deprecated 101.0.0 We use it only to support backward compatibility. If some configurations * were set to this list before, we need to read them. * It will be supported for next 2 minor releases or until a major release. * TypePool should be used to mark configurations with types. @@ -83,7 +83,7 @@ public function __construct(array $sensitive = [], array $environment = [], Excl * @param string $path Configuration field path. For example, 'contact/email/recipient_email' * @param string $type Type of configuration fields * @return bool True when the path belongs to requested type, false otherwise - * @since 100.2.0 + * @since 101.0.0 */ public function isPresent($path, $type) { diff --git a/app/code/Magento/Config/Model/PreparedValueFactory.php b/app/code/Magento/Config/Model/PreparedValueFactory.php index 19d607ad3dc1a..c86c0a820e86c 100644 --- a/app/code/Magento/Config/Model/PreparedValueFactory.php +++ b/app/code/Magento/Config/Model/PreparedValueFactory.php @@ -21,7 +21,7 @@ * * @see ValueInterface * @api - * @since 100.2.0 + * @since 101.0.0 */ class PreparedValueFactory { @@ -92,7 +92,7 @@ public function __construct( * @return ValueInterface * @throws RuntimeException If Value can not be created * @see ValueInterface - * @since 100.2.0 + * @since 101.0.0 */ public function create($path, $value, $scope, $scopeCode = null) { diff --git a/app/code/Magento/Config/Setup/Patch/Data/UnsetTinymce3.php b/app/code/Magento/Config/Setup/Patch/Data/UnsetTinymce3.php new file mode 100644 index 0000000000000..115b3baeded8d --- /dev/null +++ b/app/code/Magento/Config/Setup/Patch/Data/UnsetTinymce3.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Config\Setup\Patch\Data; + +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Setup\Patch\PatchVersionInterface; +use Magento\Framework\Setup\ModuleDataSetupInterface; + +/** + * Update config to Tinymce4 if Tinymce3 adapter is used. + */ +class UnsetTinymce3 implements DataPatchInterface, PatchVersionInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * CreateDefaultPages constructor. + * @param ModuleDataSetupInterface $moduleDataSetup + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup + ) { + $this->moduleDataSetup = $moduleDataSetup; + } + + /** + * @inheritdoc + */ + public function apply() + { + try { + $connection = $this->moduleDataSetup->getConnection(); + $table = $this->moduleDataSetup->getTable('core_config_data'); + $select = $connection + ->select() + ->from( + $table, + ['value'] + ) + ->where('path = ?', 'cms/wysiwyg/editor'); + + if (strpos($connection->fetchOne($select), 'Tinymce3/tinymce3Adapter') !== false) { + $row = [ + 'value' => 'mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter' + ]; + $where = $connection->quoteInto( + 'path = ?', + 'cms/wysiwyg/editor' + ); + $connection->update( + $table, + $row, + $where + ); + } + return $this; + } catch (\Exception $e) { + return $this; + } + } + + /** + * @inheritdoc + */ + public static function getDependencies() + { + return []; + } + + /** + * @inheritdoc + */ + public static function getVersion() + { + return '2.3.6'; + } + + /** + * @inheritdoc + */ + public function getAliases() + { + return []; + } +} diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCollapseStorefrontTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCollapseStorefrontTabActionGroup.xml new file mode 100644 index 0000000000000..1b6148f64ce4c --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigCollapseStorefrontTabActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminConfigCollapseStorefrontTabActionGroup"> + <conditionalClick selector="{{CatalogSection.storefront}}" dependentSelector="{{CatalogSection.CheckIfTabIsExpanded}}" visible="true" stepKey="collapseStorefrontTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigExpandStorefrontTabActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigExpandStorefrontTabActionGroup.xml new file mode 100644 index 0000000000000..e2afb3b56cd8d --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigExpandStorefrontTabActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminConfigExpandStorefrontTabActionGroup"> + <conditionalClick selector="{{CatalogSection.storefront}}" dependentSelector="{{CatalogSection.CheckIfTabExpand}}" visible="true" stepKey="expandStorefrontTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigFillInputFieldActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigFillInputFieldActionGroup.xml new file mode 100644 index 0000000000000..cce439185089a --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminConfigFillInputFieldActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminConfigFillInputFilterFieldActionGroup"> + <arguments> + <argument name="selector"/> + <argument name="value" type="string"/> + </arguments> + <fillField selector="{{selector}}" userInput="{{value}}" stepKey="fillInputField"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigCatalogPageActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigCatalogPageActionGroup.xml new file mode 100644 index 0000000000000..fe1eb1baff4c8 --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/AdminOpenStoreConfigCatalogPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenStoreConfigCatalogPageActionGroup"> + <annotations> + <description>Go to admin store configuration catalog page.</description> + </annotations> + + <amOnPage url="{{CatalogConfigPage.url}}" stepKey="openAdminStoreConfigPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml index b725610b7b2ee..837402005fecf 100644 --- a/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml +++ b/app/code/Magento/Config/Test/Mftf/ActionGroup/SwitchToTinyMCE3ActionGroup.xml @@ -8,7 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="SwitchToTinyMCE3ActionGroup"> + <actionGroup name="SwitchToTinyMCE3ActionGroup" deprecated="This version of TinyMCE is no longer supported"> <annotations> <description>Goes to the 'Configuration' page for 'Content Management'. Sets 'WYSIWYG Editor' to 'TinyMCE 3'. Clicks on the Save button. PLEASE NOTE: The value is Hardcoded.</description> </annotations> diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml index e82ad4670f9b3..9b54697e2d6ba 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSearchAdminConfigSection.xml @@ -7,9 +7,9 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminCatalogSearchConfigurationSection"> - <element name="catalogSearchTab" type="button" selector="#catalog_search-head"/> + <element name="catalogSearchTab" type="button" selector="a#catalog_search-head"/> <element name="checkIfCatalogSearchTabExpand" type="button" selector="#catalog_search-head:not(.open)"/> <element name="searchEngineDefaultSystemValue" type="checkbox" selector="#catalog_search_engine_inherit"/> <element name="searchEngine" type="select" selector="#catalog_search_engine"/> </section> -</sections> \ No newline at end of file +</sections> diff --git a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml index 851157c5d03c0..72675414576cf 100644 --- a/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml +++ b/app/code/Magento/Config/Test/Mftf/Section/CatalogSection/CatalogSection.xml @@ -10,6 +10,7 @@ <section name="CatalogSection"> <element name="storefront" type="select" selector="#catalog_frontend-head"/> <element name="CheckIfTabExpand" type="button" selector="#catalog_frontend-head:not(.open)"/> + <element name="CheckIfTabIsExpanded" type="button" selector="#catalog_frontend-head.open"/> <element name="price" type="button" selector="#catalog_price-head"/> <element name="checkIfPriceExpand" type="button" selector="//a[@id='catalog_price-head' and @class='open']"/> <element name="catalogPriceScope" type="select" selector="#catalog_price_scope"/> @@ -23,5 +24,6 @@ <element name="CheckIfSeoTabExpand" type="button" selector="#catalog_seo-head:not(.open)"/> <element name="GenerateUrlRewrites" type="select" selector="#catalog_seo_generate_category_product_rewrites"/> <element name="successMessage" type="text" selector="#messages"/> + <element name="productsPerPageOnGridAllowedValues" type="input" selector="//input[@id='catalog_frontend_grid_per_page_values']"/> </section> </sections> diff --git a/app/code/Magento/Config/Test/Mftf/Test/AdminConfigCollapsedFieldsetValidationTest.xml b/app/code/Magento/Config/Test/Mftf/Test/AdminConfigCollapsedFieldsetValidationTest.xml new file mode 100644 index 0000000000000..877676e2a7cda --- /dev/null +++ b/app/code/Magento/Config/Test/Mftf/Test/AdminConfigCollapsedFieldsetValidationTest.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="AdminConfigCollapsedFieldsetValidationTest"> + <annotations> + <features value="Backend"/> + <stories value="Configuration Form Validation"/> + <title value="Verify that form validation triggered on element inside hidden fieldset opens the fieldset in case of error"/> + <description value="Verify that form validation triggered on element inside hidden fieldset opens the fieldset in case of error"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35785"/> + <group value="configuration"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminOpenStoreConfigCatalogPageActionGroup" stepKey="navigateToConfigurationPage"/> + <actionGroup ref="AdminConfigExpandStorefrontTabActionGroup" stepKey="expandStorefrontTab"/> + <actionGroup ref="AdminUncheckUseSystemValueActionGroup" stepKey="uncheckUseSystemValue"> + <argument name="rowId" value="row_catalog_frontend_grid_per_page_values"/> + </actionGroup> + <actionGroup ref="AdminConfigFillInputFilterFieldActionGroup" stepKey="fillInputField"> + <argument name="selector" value="CatalogSection.productsPerPageOnGridAllowedValues"/> + <argument name="value" value=""/> + </actionGroup> + <actionGroup ref="AdminConfigCollapseStorefrontTabActionGroup" stepKey="collapseStorefrontTab"/> + <click selector="{{CatalogSection.save}}" stepKey="clickSaveConfigBtn"/> + + <actionGroup ref="AssertAdminValidationErrorActionGroup" stepKey="assertValidationError"> + <argument name="inputId" value="catalog_frontend_grid_per_page_values"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml index 5327979154389..d0edd4cf1cb64 100644 --- a/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml +++ b/app/code/Magento/Config/Test/Mftf/Test/CheckingCountryDropDownWithOneAllowedCountryTest.xml @@ -22,7 +22,9 @@ <createData entity="EnableAdminAccountAllowCountry" stepKey="setAllowedCountries"/> </before> <after> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="DisableAdminAccountAllowCountry" stepKey="setDefaultValueForAllowCountries"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> @@ -33,7 +35,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Flush Magento Cache--> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Create a customer account from Storefront--> <actionGroup ref="StorefrontOpenCustomerAccountCreatePageActionGroup" stepKey="openCreateAccountPage"/> <actionGroup ref="StorefrontFillCustomerAccountCreationFormActionGroup" stepKey="fillCreateAccountForm"> diff --git a/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php b/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php index 4ab882a33f9af..a16208c0e61b0 100644 --- a/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php +++ b/app/code/Magento/Config/Test/Unit/App/Config/Source/RuntimeConfigSourceTest.php @@ -17,118 +17,140 @@ use Magento\Framework\App\Config\Value; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\DB\Adapter\TableNotFoundException; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** * Test Class for retrieving runtime configuration from database. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RuntimeConfigSourceTest extends TestCase { + /** + * @var RuntimeConfigSource + */ + private $model; + /** * @var CollectionFactory|MockObject */ - private $collectionFactory; + private $collectionFactoryMock; /** * @var ScopeCodeResolver|MockObject */ - private $scopeCodeResolver; + private $scopeCodeResolverMock; /** * @var Converter|MockObject */ - private $converter; + private $converterMock; /** * @var Value|MockObject */ - private $configItem; + private $configItemMock; /** * @var Value|MockObject */ - private $configItemTwo; + private $configItemMockTwo; - /** - * @var RuntimeConfigSource - */ - private $configSource; /** * @var DeploymentConfig|MockObject */ - private $deploymentConfig; + private $deploymentConfigMock; + /** + * @inheritdoc + */ protected function setUp(): void { - $this->collectionFactory = $this->getMockBuilder(CollectionFactory::class) + $objectManager = new ObjectManager($this); + + $this->collectionFactoryMock = $this->getMockBuilder(CollectionFactory::class) ->disableOriginalConstructor() - ->setMethods(['create']) ->getMock(); - $this->scopeCodeResolver = $this->getMockBuilder(ScopeCodeResolver::class) + $this->scopeCodeResolverMock = $this->getMockBuilder(ScopeCodeResolver::class) ->disableOriginalConstructor() ->getMock(); - $this->converter = $this->getMockBuilder(Converter::class) + $this->converterMock = $this->getMockBuilder(Converter::class) ->disableOriginalConstructor() ->getMock(); - $this->configItem = $this->getMockBuilder(Value::class) + $this->configItemMock = $this->getMockBuilder(Value::class) ->disableOriginalConstructor() - ->setMethods(['getScope', 'getPath', 'getValue']) + ->addMethods(['getScope', 'getPath', 'getValue']) ->getMock(); - $this->configItemTwo = $this->getMockBuilder(Value::class) + $this->configItemMockTwo = $this->getMockBuilder(Value::class) ->disableOriginalConstructor() - ->setMethods(['getScope', 'getPath', 'getValue', 'getScopeId']) + ->addMethods(['getScope', 'getPath', 'getValue', 'getScopeId']) ->getMock(); - $this->deploymentConfig = $this->createPartialMock(DeploymentConfig::class, ['isDbAvailable']); - $this->configSource = new RuntimeConfigSource( - $this->collectionFactory, - $this->scopeCodeResolver, - $this->converter, - $this->deploymentConfig + $this->deploymentConfigMock = $this->createPartialMock( + DeploymentConfig::class, + ['isDbAvailable'] + ); + $this->model = $objectManager->getObject( + RuntimeConfigSource::class, + [ + 'collectionFactory' => $this->collectionFactoryMock, + 'scopeCodeResolver' => $this->scopeCodeResolverMock, + 'converter' => $this->converterMock, + 'deploymentConfig' => $this->deploymentConfigMock, + ] ); } - public function testGet() + /** + * Test get initial data. + * + * @return void + */ + public function testGet(): void { - $this->deploymentConfig->method('isDbAvailable') + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); $collection = $this->createPartialMock(Collection::class, ['load', 'getIterator']); - $collection->method('load') + $collection->expects($this->once()) + ->method('load') ->willReturn($collection); - $collection->method('getIterator') - ->willReturn(new ArrayIterator([$this->configItem, $this->configItemTwo])); + $collection->expects($this->once()) + ->method('getIterator') + ->willReturn(new ArrayIterator([$this->configItemMock, $this->configItemMockTwo])); $scope = 'websites'; $scopeCode = 'myWebsites'; - $this->collectionFactory->expects($this->once()) + $this->collectionFactoryMock->expects($this->once()) ->method('create') ->willReturn($collection); - $this->configItem->expects($this->exactly(2)) + $this->configItemMock->expects($this->exactly(2)) ->method('getScope') ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); - $this->configItem->expects($this->once()) + $this->configItemMock->expects($this->once()) ->method('getPath') ->willReturn('dev/test/setting'); - $this->configItem->expects($this->once()) + $this->configItemMock->expects($this->once()) ->method('getValue') ->willReturn(true); - $this->configItemTwo->expects($this->exactly(3)) + $this->configItemMockTwo->expects($this->exactly(3)) ->method('getScope') ->willReturn($scope); - $this->configItemTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->once()) ->method('getScopeId') ->willReturn($scopeCode); - $this->configItemTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->once()) ->method('getPath') ->willReturn('dev/test/setting2'); - $this->configItemTwo->expects($this->once()) + $this->configItemMockTwo->expects($this->once()) ->method('getValue') ->willReturn(false); - $this->scopeCodeResolver->expects($this->once()) + $this->scopeCodeResolverMock->expects($this->once()) ->method('resolve') ->with($scope, $scopeCode) ->willReturnArgument(1); - $this->converter->expects($this->exactly(2)) + $this->converterMock->expects($this->exactly(2)) ->method('convert') ->withConsecutive( [['dev/test/setting' => true]], @@ -150,25 +172,97 @@ public function testGet() ] ] ], - $this->configSource->get() + $this->model->get() ); } - public function testGetWhenDbIsNotAvailable() + /** + * Test get with not available db + * + * @return void + */ + public function testGetWhenDbIsNotAvailable(): void { - $this->deploymentConfig->method('isDbAvailable')->willReturn(false); - $this->assertEquals([], $this->configSource->get()); + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') + ->willReturn(false); + $this->assertEquals([], $this->model->get()); } - public function testGetWhenDbIsEmpty() + /** + * Test get with empty db + * + * @return void + */ + public function testGetWhenDbIsEmpty(): void { - $this->deploymentConfig->method('isDbAvailable') + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') ->willReturn(true); $collection = $this->createPartialMock(Collection::class, ['load']); - $collection->method('load') + $collection->expects($this->once()) + ->method('load') ->willThrowException($this->createMock(TableNotFoundException::class)); - $this->collectionFactory->method('create') + $this->collectionFactoryMock->expects($this->once()) + ->method('create') ->willReturn($collection); - $this->assertEquals([], $this->configSource->get()); + + $this->assertEquals([], $this->model->get()); + } + + /** + * Test get value for specified config + * + * @dataProvider configDataProvider + * + * @param string $path + * @param array $configData + * @param string $expectedResult + * @return void + */ + public function testGetConfigValue(string $path, array $configData, string $expectedResult): void + { + $this->deploymentConfigMock->expects($this->once()) + ->method('isDbAvailable') + ->willReturn(true); + + $collection = $this->createPartialMock(Collection::class, ['load', 'getIterator']); + $collection->expects($this->once()) + ->method('load') + ->willReturn($collection); + $collection->expects($this->once()) + ->method('getIterator') + ->willReturn(new ArrayIterator([$this->configItemMock])); + + $this->collectionFactoryMock->expects($this->once()) + ->method('create') + ->willReturn($collection); + + $this->configItemMock->expects($this->exactly(2)) + ->method('getScope') + ->willReturn(ScopeConfigInterface::SCOPE_TYPE_DEFAULT); + $this->configItemMock->expects($this->once()) + ->method('getPath') + ->willReturn($path); + + $this->converterMock->expects($this->once()) + ->method('convert') + ->willReturn($configData); + + $this->assertEquals($expectedResult, $this->model->get($path)); + } + + /** + * DataProvider for testGetConfigValue + * + * @return array + */ + public function configDataProvider(): array + { + return [ + 'config value 0' => ['default/test/option', ['test' => ['option' => 0]], '0'], + 'config value blank' => ['default/test/option', ['test' => ['option' => '']], ''], + 'config value null' => ['default/test/option', ['test' => ['option' => null]], ''], + ]; } } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php index 822779a5736a8..120e83a70ffc4 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/ImageTest.php @@ -17,6 +17,8 @@ use Magento\Framework\Url; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class ImageTest extends TestCase { @@ -39,11 +41,22 @@ protected function setUp(): void { $objectManager = new ObjectManager($this); $this->urlBuilderMock = $this->createMock(Url::class); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); $this->image = $objectManager->getObject( Image::class, [ 'urlBuilder' => $this->urlBuilderMock, - '_escaper' => $objectManager->getObject(Escaper::class) + '_escaper' => $objectManager->getObject(Escaper::class), + 'random' => $randomMock, + 'secureRenderer' => $secureRendererMock ] ); @@ -108,14 +121,15 @@ public function testGetElementHtmlWithValue() $this->assertStringContainsString('type="file"', $html); $this->assertStringContainsString('value="test_value"', $html); $this->assertStringContainsString( - '<a href="' + '<a previewlinkid="linkIdsome-rando-string" href="' . $url . $this->testData['path'] . '/' . $this->testData['value'] - . '" onclick="imagePreview(\'' . $expectedHtmlId . '_image\'); return false;"', + . '"', $html ); + $this->assertStringContainsString("imagePreview('{$expectedHtmlId}_image');\nreturn false;", $html); $this->assertStringContainsString('<input type="checkbox"', $html); } } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php index 04b0bad314b11..129dfc902963f 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Field/Select/AllowspecificTest.php @@ -14,6 +14,9 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\DataObject; class AllowspecificTest extends TestCase { @@ -30,10 +33,30 @@ class AllowspecificTest extends TestCase protected function setUp(): void { $testHelper = new ObjectManager($this); + + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); $this->_object = $testHelper->getObject( Allowspecific::class, [ - '_escaper' => $testHelper->getObject(Escaper::class) + '_escaper' => $testHelper->getObject(Escaper::class), + 'random' => $randomMock, + 'secureRenderer' => $secureRendererMock ] ); $this->_object->setData('html_id', 'spec_element'); @@ -68,7 +91,7 @@ public function testGetAfterElementHtml() $actual = $this->_object->getAfterElementHtml(); $this->assertStringEndsWith('</script>' . $afterHtmlCode, $actual); - $this->assertStringStartsWith('<script type="text/javascript">', trim($actual)); + $this->assertStringStartsWith('<script >', trim($actual)); $this->assertStringContainsString('test_prefix_spec_element_test_suffix', $actual); } diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php index 679b240cf13ab..3193d4f737984 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldTest.php @@ -14,6 +14,7 @@ use Magento\Store\Model\StoreManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Test how class render field html element in Stores Configuration @@ -48,10 +49,24 @@ class FieldTest extends TestCase protected function setUp(): void { $this->_storeManagerMock = $this->createMock(StoreManager::class); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $data = [ 'storeManager' => $this->_storeManagerMock, 'urlBuilder' => $this->createMock(Url::class), + 'secureRenderer' => $secureRendererMock ]; $helper = new ObjectManager($this); $this->_object = $helper->getObject(Field::class, $data); @@ -157,7 +172,7 @@ public function testRenderHint() { $testHint = 'test_hint'; $this->_elementMock->expects($this->any())->method('getHint')->willReturn($testHint); - $expected = '<td class=""><div class="hint"><div style="display: none;">' . $testHint . '</div></div>'; + $expected = '<td class=""><div class="hint"><div id="hint_test_field_id">' . $testHint . '</div></div>'; $actual = $this->_object->render($this->_elementMock); $this->assertStringContainsString($expected, $actual); } @@ -194,8 +209,9 @@ public function testRenderInheritCheckbox() '_inherit" name="' . $this->_testData['name'] . '[inherit]" type="checkbox" value="1"' . - ' class="checkbox config-inherit" checked="checked"' . ' disabled="disabled"' . ' readonly="1"' . - ' onclick="toggleValueElements(this, Element.previous(this.parentNode))" /> '; + ' class="checkbox config-inherit" checked="checked"' . ' disabled="disabled"' . ' readonly="1" />' . + '<script>document.querySelector(\'input#test_field_id_inherit\').onclick = function () '. + '{ toggleValueElements(this, Element.previous(this.parentNode)) };</script>'; $expected .= '<label for="' . $this->_testData['htmlId'] . '_inherit" class="inherit">Use Website</label>'; $actual = $this->_object->render($this->_elementMock); diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php index 87e42953ddd49..df028ea27f01c 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/Fieldset/Modules/DisableOutputTest.php @@ -23,6 +23,7 @@ use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -160,6 +161,14 @@ protected function setUp(): void ] ); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $data = [ 'context' => $context, 'authSession' => $this->authSessionMock, @@ -169,6 +178,7 @@ protected function setUp(): void 'group' => $groupMock, 'form' => $formMock, ], + 'secureRenderer' => $secureRendererMock ]; $this->object = $this->objectManager->getObject( diff --git a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php index 07db12a282cc9..fd5fbe7c4cdc6 100644 --- a/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php +++ b/app/code/Magento/Config/Test/Unit/Block/System/Config/Form/FieldsetTest.php @@ -24,6 +24,7 @@ use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -120,6 +121,13 @@ protected function setUp(): void $groupMock->expects($this->any())->method('getFieldsetCss')->willReturn('test_fieldset_css'); $this->_helperMock = $this->createMock(Js::class); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $data = [ 'request' => $this->_requestMock, @@ -128,6 +136,7 @@ protected function setUp(): void 'layout' => $this->_layoutMock, 'jsHelper' => $this->_helperMock, 'data' => ['group' => $groupMock], + 'secureRenderer' => $secureRendererMock ]; $this->_testHelper = new ObjectManager($this); $this->_object = $this->_testHelper->getObject(Fieldset::class, $data); @@ -233,8 +242,8 @@ public function testRenderWithStoredElements($expanded, $nested, $extra) $this->assertStringContainsString('test_field_toHTML', $actual); - $expected = '<div id="row_test_field_id_comment" class="system-tooltip-box"' . - ' style="display:none;">test_field_tootip</div>'; + $expected = '<div id="row_test_field_id_comment" class="system-tooltip-box">test_field_tootip</div>' . + '<style>#row_test_field_id_comment { display:none; }</style>'; $this->assertStringContainsString($expected, $actual); if ($nested) { $this->assertStringContainsString('nested', $actual); diff --git a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php index 59511d9a947ab..dc3db6ab926f7 100644 --- a/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php +++ b/app/code/Magento/Config/Test/Unit/Console/Command/ConfigShowCommandTest.php @@ -3,23 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Config\Test\Unit\Console\Command; use Magento\Config\Console\Command\ConfigShow\ValueProcessor; use Magento\Config\Console\Command\ConfigShowCommand; +use Magento\Config\Console\Command\EmulatedAdminhtmlAreaProcessor; use Magento\Framework\App\Config\ConfigPathResolver; use Magento\Framework\App\Config\ConfigSourceInterface; use Magento\Framework\App\Scope\ValidatorInterface; use Magento\Framework\Console\Cli; use Magento\Framework\Exception\LocalizedException; +use Magento\Config\Model\Config\PathValidatorFactory; +use Magento\Config\Model\Config\PathValidator; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; +/** + * Test for \Magento\Config\Console\Command\ConfigShowCommand. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ConfigShowCommandTest extends TestCase { + private const CONFIG_PATH = 'some/config/path'; + private const SCOPE = 'some/config/path'; + private const SCOPE_CODE = 'someScopeCode'; + + /** + * @var ConfigShowCommand + */ + private $model; + /** * @var ValidatorInterface|MockObject */ @@ -41,12 +59,22 @@ class ConfigShowCommandTest extends TestCase private $pathResolverMock; /** - * @var ConfigShowCommand + * @var EmulatedAdminhtmlAreaProcessor|MockObject + */ + private $emulatedAreProcessorMock; + + /** + * @var PathValidator|MockObject */ - private $command; + private $pathValidatorMock; + /** + * @inheritdoc + */ protected function setUp(): void { + $objectManager = new ObjectManager($this); + $this->valueProcessorMock = $this->getMockBuilder(ValueProcessor::class) ->disableOriginalConstructor() ->getMock(); @@ -57,29 +85,49 @@ protected function setUp(): void ->getMockForAbstractClass(); $this->configSourceMock = $this->getMockBuilder(ConfigSourceInterface::class) ->getMockForAbstractClass(); + $this->pathValidatorMock = $this->getMockBuilder(PathValidator::class) + ->disableOriginalConstructor() + ->getMock(); + $pathValidatorFactoryMock = $this->getMockBuilder(PathValidatorFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $pathValidatorFactoryMock->expects($this->atMost(1)) + ->method('create') + ->willReturn($this->pathValidatorMock); - $this->command = new ConfigShowCommand( - $this->scopeValidatorMock, - $this->configSourceMock, - $this->pathResolverMock, - $this->valueProcessorMock + $this->emulatedAreProcessorMock = $this->getMockBuilder(EmulatedAdminhtmlAreaProcessor::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->model = $objectManager->getObject( + ConfigShowCommand::class, + [ + 'scopeValidator' => $this->scopeValidatorMock, + 'configSource' => $this->configSourceMock, + 'pathResolver' => $this->pathResolverMock, + 'valueProcessor' => $this->valueProcessorMock, + 'pathValidatorFactory' => $pathValidatorFactoryMock, + 'emulatedAreaProcessor' => $this->emulatedAreProcessorMock, + ] ); } - public function testExecute() + /** + * Test get config value + * + * @return void + */ + public function testExecute(): void { - $configPath = 'some/config/path'; $resolvedConfigPath = 'someScope/someScopeCode/some/config/path'; - $scope = 'someScope'; - $scopeCode = 'someScopeCode'; $this->scopeValidatorMock->expects($this->once()) ->method('isValid') - ->with($scope, $scopeCode) + ->with(self::SCOPE, self::SCOPE_CODE) ->willReturn(true); $this->pathResolverMock->expects($this->once()) ->method('resolve') - ->with($configPath, $scope, $scopeCode) + ->with(self::CONFIG_PATH, self::SCOPE, self::SCOPE_CODE) ->willReturn($resolvedConfigPath); $this->configSourceMock->expects($this->once()) ->method('get') @@ -87,10 +135,19 @@ public function testExecute() ->willReturn('someValue'); $this->valueProcessorMock->expects($this->once()) ->method('process') - ->with($scope, $scopeCode, 'someValue', $configPath) + ->with(self::SCOPE, self::SCOPE_CODE, 'someValue', self::CONFIG_PATH) ->willReturn('someProcessedValue'); - - $tester = $this->getConfigShowCommandTester($configPath, $scope, $scopeCode); + $this->emulatedAreProcessorMock->expects($this->once()) + ->method('process') + ->willReturnCallback(function ($function) { + return $function(); + }); + + $tester = $this->getConfigShowCommandTester( + self::CONFIG_PATH, + self::SCOPE, + self::SCOPE_CODE + ); $this->assertEquals( Cli::RETURN_SUCCESS, @@ -102,18 +159,28 @@ public function testExecute() ); } - public function testNotValidScopeOrScopeCode() + /** + * Test not valid scope or scope code + * + * @return void + */ + public function testNotValidScopeOrScopeCode(): void { - $configPath = 'some/config/path'; - $scope = 'someScope'; - $scopeCode = 'someScopeCode'; - $this->scopeValidatorMock->expects($this->once()) ->method('isValid') - ->with($scope, $scopeCode) + ->with(self::SCOPE, self::SCOPE_CODE) ->willThrowException(new LocalizedException(__('error message'))); - - $tester = $this->getConfigShowCommandTester($configPath, $scope, $scopeCode); + $this->emulatedAreProcessorMock->expects($this->once()) + ->method('process') + ->willReturnCallback(function ($function) { + return $function(); + }); + + $tester = $this->getConfigShowCommandTester( + self::CONFIG_PATH, + self::SCOPE, + self::SCOPE_CODE + ); $this->assertEquals( Cli::RETURN_FAILURE, @@ -125,17 +192,35 @@ public function testNotValidScopeOrScopeCode() ); } - public function testConfigPathNotExist() + /** + * Test get config value for not existed path. + * + * @return void + */ + public function testConfigPathNotExist(): void { - $configPath = 'some/path'; - $tester = $this->getConfigShowCommandTester($configPath); + $exception = new LocalizedException( + __('The "%1" path doesn\'t exist. Verify and try again.', self::CONFIG_PATH) + ); + + $this->pathValidatorMock->expects($this->once()) + ->method('validate') + ->with(self::CONFIG_PATH) + ->willThrowException($exception); + $this->emulatedAreProcessorMock->expects($this->once()) + ->method('process') + ->willReturnCallback(function ($function) { + return $function(); + }); + + $tester = $this->getConfigShowCommandTester(self::CONFIG_PATH); $this->assertEquals( Cli::RETURN_FAILURE, $tester->getStatusCode() ); $this->assertStringContainsString( - __('Configuration for path: "%1" doesn\'t exist', $configPath)->render(), + __('The "%1" path doesn\'t exist. Verify and try again.', self::CONFIG_PATH)->render(), $tester->getDisplay() ); } @@ -159,7 +244,7 @@ private function getConfigShowCommandTester($configPath, $scope = null, $scopeCo $arguments['--' . ConfigShowCommand::INPUT_OPTION_SCOPE_CODE] = $scopeCode; } - $tester = new CommandTester($this->command); + $tester = new CommandTester($this->model); $tester->execute($arguments); return $tester; diff --git a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml index 49a75d36fd8a5..d8fb7cd412a7a 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/page/system/config/robots/reset.phtml @@ -7,21 +7,24 @@ /** * @deprecated * @var $block \Magento\Backend\Block\Page\System\Config\Robots\Reset - * @var $jsonHelper \Magento\Framework\Json\Helper\Data + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ -$jsonHelper = $this->helper(\Magento\Framework\Json\Helper\Data::class); -?> -<script> +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +?> +<?php +$robotsDefault = /* @noEscape */ $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()); +$scriptString = <<<script require([ 'jquery' ], function ($) { window.resetRobotsToDefault = function(){ - $('#design_search_engine_robots_custom_instructions').val(<?= - /* @noEscape */ $jsonHelper->jsonEncode($block->getRobotsDefaultCustomInstructions()) - ?>); + $('#design_search_engine_robots_custom_instructions').val({$robotsDefault}); } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= $block->getButtonHtml() ?> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml index 7e7a540e88b2e..4dbc70efd25e3 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/edit.phtml @@ -3,6 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?php /** @@ -14,11 +18,13 @@ * getConfigSearchParamsJson() - string */ ?> -<style> - .highlighted { - background-color: #DFF7FF!important; - } -</style> + +<?= /* @noEscape */ $secureRenderer->renderTag( + 'style', + [], + '.highlighted { background-color: #DFF7FF!important; }' +) ?> + <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="config-edit-form" enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> @@ -26,7 +32,8 @@ <?= $block->getChildHtml('form') ?> </div> </form> -<script> + +<?php $scriptString = <<<script require([ "jquery", "uiRegistry", @@ -73,14 +80,14 @@ require([ } }, getUp: function (element, tag) { - var $element = Element.extend(element); - if (typeof $element.upTag == 'undefined') { - $element.upTag = {}; + var _element = Element.extend(element); + if (typeof _element.upTag == 'undefined') { + _element.upTag = {}; } - if (typeof $element.upTag[tag] == 'undefined') { - $element.upTag[tag] = Element.extend($element.up(tag)); + if (typeof _element.upTag[tag] == 'undefined') { + _element.upTag[tag] = Element.extend(_element.up(tag)); } - return $element.upTag[tag]; + return _element.upTag[tag]; }, getUpTd: function (element) { return this.getUp(element, 'td'); @@ -89,26 +96,26 @@ require([ return this.getUp(element, 'tr'); }, getScopeElement: function(element) { - var $element = Element.extend(element); - if (typeof $element.scopeElement == 'undefined') { + var _element = Element.extend(element); + if (typeof _element.scopeElement == 'undefined') { var scopeElementName = element.getAttribute('name').replace(/\[value\]$/, '[inherit]'); - $element.scopeElement = this.getUpTr(element).select('input[name="' + scopeElementName + '"]')[0]; - if (typeof $element.scopeElement == 'undefined') { - $element.scopeElement = false; + _element.scopeElement = this.getUpTr(element).select('input[name="' + scopeElementName + '"]')[0]; + if (typeof _element.scopeElement == 'undefined') { + _element.scopeElement = false; } } - return $element.scopeElement; + return _element.scopeElement; }, getDeleteElement: function(element) { - var $element = Element.extend(element); - if (typeof $element.deleteElement == 'undefined') { - $element.deleteElement = this.getUpTd(element) + var _element = Element.extend(element); + if (typeof _element.deleteElement == 'undefined') { + _element.deleteElement = this.getUpTd(element) .select('input[name="'+ element.getAttribute('name') + '[delete]"]')[0]; - if (typeof $element.deleteElement == 'undefined') { - $element.deleteElement = false; + if (typeof _element.deleteElement == 'undefined') { + _element.deleteElement = false; } } - return $element.deleteElement; + return _element.deleteElement; }, mapClasses: function(element, full, callback, classPrefix) { if (typeof classPrefix == 'undefined') { @@ -159,11 +166,11 @@ require([ var tagName = el.tagName.toLowerCase(); if (tagName == 'input' && el.getAttribute('type') == 'file') { - var $el = Element.extend(el); + var _el = Element.extend(el); var events = adminSystemConfig.getRegisteredEvents(el); - $el.stopObserving('change'); - var elId = $el.id; - $el.replace($el.outerHTML); + _el.stopObserving('change'); + var elId = _el.id; + _el.replace(_el.outerHTML); events.each(function(event) { Event.observe( Element.extend(document.getElementById(elId)), event.eventName, event.handler @@ -176,9 +183,9 @@ require([ Element.extend(el).click(); } } else if (tagName == 'select') { - var $el = Element.extend(el); + var _el = Element.extend(el); Element.extend(element).select('option').each(function(option) { - var relatedOption = $el.select('option[value="' + option.value + '"]')[0]; + var relatedOption = _el.select('option[value="' + option.value + '"]')[0]; if (typeof relatedOption != 'undefined') { relatedOption.selected = option.selected; } @@ -255,6 +262,24 @@ require([ } }); + window.configForm.on('invalid-form.validate', function (event, validation) { + if (validation.errorList.length === 0) { + return; + } + + jQuery.each(validation.errorList, function () { + var element = jQuery(this.element || []); + + if (element.length) { + jQuery(element.parents('.section-config')).each(function () { + if (!jQuery(this).hasClass('active')) { + Fieldset.toggleCollapse(jQuery(this).children('.config.admin__collapsible-block').attr('id')); + } + }); + } + }); + }) + $$('.shared').each(function(element){ Event.observe(element, 'change', adminSystemConfig.onchangeSharedElement); @@ -392,7 +417,8 @@ require([ handleHash(); registry.set('adminSystemConfig', adminSystemConfig); +script; +$scriptString .= 'adminSystemConfig.navigateToElement(' . /* @noEscape */ $block->getConfigSearchParamsJson() . '); +});'; - adminSystemConfig.navigateToElement(<?= /* @noEscape */ $block->getConfigSearchParamsJson(); ?>); -}); -</script> +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml index cf188bfeb6868..f08cc77249582 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/form/field/array.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -15,7 +17,7 @@ $_colspan = $block->isAddAfter() ? 2 : 1; <table class="admin__control-table" id="<?= $block->escapeHtmlAttr($block->getElement()->getId()) ?>"> <thead> <tr> - <?php foreach ($block->getColumns() as $columnName => $column) : ?> + <?php foreach ($block->getColumns() as $columnName => $column): ?> <th><?= $block->escapeHtml($column['label']) ?></th> <?php endforeach; ?> <th class="col-actions" colspan="<?= (int)$_colspan ?>"><?= $block->escapeHtml(__('Action')) ?></th> @@ -24,7 +26,10 @@ $_colspan = $block->isAddAfter() ? 2 : 1; <tfoot> <tr> <td colspan="<?= count($block->getColumns())+$_colspan ?>" class="col-actions-add"> - <button id="addToEndBtn<?= $block->escapeHtmlAttr($_htmlId) ?>" class="action-add" title="<?= $block->escapeHtmlAttr(__('Add')) ?>" type="button"> + <button id="addToEndBtn<?= $block->escapeHtmlAttr($_htmlId) ?>" + class="action-add" + title="<?= $block->escapeHtmlAttr(__('Add')) ?>" + type="button"> <span><?= $block->escapeHtml($block->getAddButtonLabel()) ?></span> </button> </td> @@ -35,34 +40,52 @@ $_colspan = $block->isAddAfter() ? 2 : 1; </div> <input type="hidden" name="<?= $block->escapeHtmlAttr($block->getElement()->getName()) ?>[__empty]" value="" /> - <script> + <?php $scriptString = <<<script require([ 'mage/template', 'prototype' ], function (mageTemplate) { // create row creator - window.arrayRow<?= $block->escapeJs($_htmlId) ?> = { + window.arrayRow{$block->escapeJs($_htmlId)} = { // define row prototypeJS template template: mageTemplate( '<tr id="<%- _id %>">' - <?php foreach ($block->getColumns() as $columnName => $column) : ?> +script; + foreach ($block->getColumns() as $columnName => $column): + $scriptString .= <<<script + + '<td>' - + '<?= $block->escapeJs($block->renderCellTemplate($columnName)) ?>' + + '{$block->escapeJs($block->renderCellTemplate($columnName))}' + '<\/td>' - <?php endforeach; ?> +script; + endforeach; + + if ($block->isAddAfter()): + $scriptString .= <<<script - <?php if ($block->isAddAfter()) : ?> + '<td><button class="action-add" type="button" id="addAfterBtn<%- _id %>"><span>' - + '<?= $block->escapeJs($block->escapeHtml(__('Add after'))) ?>' + + '{$block->escapeJs(__('Add after'))}' + '<\/span><\/button><\/td>' - <?php endif; ?> +script; + endif; + $scriptString .= <<<script + '<td class="col-actions"><button ' - + 'onclick="arrayRow<?= $block->escapeJs($_htmlId) ?>.del(\'<%- _id %>\')" ' + 'class="action-delete" type="button">' - + '<span><?= $block->escapeJs($block->escapeHtml(__('Delete'))) ?><\/span><\/button><\/td>' + + '<span>{$block->escapeJs(__('Delete'))}<\/span><\/button><\/td>' + '<\/tr>' + +script; + $scriptString1 = /* $noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "arrayRow" . $block->escapeJs($_htmlId) . ".del('<%- _id %>')", + "tr#<%- _id %> button.action-delete" + ); + + $scriptString .= " + '" . $block->escapeJs($scriptString1) . "'" . PHP_EOL; + + $scriptString .= <<<script ), add: function(rowData, insertAfterId) { @@ -75,10 +98,16 @@ $_colspan = $block->isAddAfter() ? 2 : 1; } else { var d = new Date(); templateValues = { - <?php foreach ($block->getColumns() as $columnName => $column) : ?> - <?= $block->escapeJs($columnName) ?>: '', +script; + foreach ($block->getColumns() as $columnName => $column): + $scriptString .= <<<script + + {$block->escapeJs($columnName)}: '', 'option_extra_attrs': {}, - <?php endforeach; ?> +script; + endforeach; + $scriptString .= <<<script + _id: '_' + d.getTime() + '_' + d.getMilliseconds() }; } @@ -87,7 +116,7 @@ $_colspan = $block->isAddAfter() ? 2 : 1; if (insertAfterId) { Element.insert($(insertAfterId), {after: this.template(templateValues)}); } else { - Element.insert($('addRow<?= $block->escapeJs($_htmlId) ?>'), {bottom: this.template(templateValues)}); + Element.insert($('addRow{$block->escapeJs($_htmlId)}'), {bottom: this.template(templateValues)}); } // Fill controls with data @@ -101,9 +130,17 @@ $_colspan = $block->isAddAfter() ? 2 : 1; } // Add event for {addAfterBtn} button - <?php if ($block->isAddAfter()) : ?> + +script; + if ($block->isAddAfter()): + $scriptString .= <<<script + Event.observe('addAfterBtn' + templateValues._id, 'click', this.add.bind(this, false, templateValues._id)); - <?php endif; ?> + +script; + endif; + $scriptString .= <<<script + }, del: function(rowId) { @@ -112,24 +149,35 @@ $_colspan = $block->isAddAfter() ? 2 : 1; } // bind add action to "Add" button in last row - Event.observe('addToEndBtn<?= $block->escapeJs($_htmlId) ?>', + Event.observe('addToEndBtn{$block->escapeJs($_htmlId)}', 'click', - arrayRow<?= $block->escapeJs($_htmlId) ?>.add.bind( - arrayRow<?= $block->escapeJs($_htmlId) ?>, false, false + arrayRow{$block->escapeJs($_htmlId)}.add.bind( + arrayRow{$block->escapeJs($_htmlId)}, false, false ) ); // add existing rows - <?php - foreach ($block->getArrayRows() as $_rowId => $_row) { - echo /** @noEscape */ "arrayRow{$block->escapeJs($_htmlId)}.add(" . /** @noEscape */ $_row->toJson() . ");\n"; - } - ?> + +script; + + foreach ($block->getArrayRows() as $_rowId => $_row) { + $scriptString .= /** @noEscape */ " arrayRow" .$block->escapeJs($_htmlId) . + ".add(" . /** @noEscape */ $_row->toJson() . ");\n"; + } + $scriptString .= <<<script // Toggle the grid availability, if element is disabled (depending on scope) - <?php if ($block->getElement()->getDisabled()) : ?> - toggleValueElements({checked: true}, $('grid<?= $block->escapeJs($_htmlId) ?>').parentNode); - <?php endif; ?> +script; + if ($block->getElement()->getDisabled()): + $scriptString .= <<<script + + toggleValueElements({checked: true}, $('grid{$block->escapeJs($_htmlId)}').parentNode); +script; + endif; + $scriptString .= <<<script + }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml index 297687786833d..a0ada7814cee8 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/js.phtml @@ -3,8 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require([ 'prototype' ], function () { @@ -68,7 +73,10 @@ originModel.prototype = { { this.reload = false; this.loader = new varienLoader(true); - this.regionsUrl = "<?= $block->escapeJs($block->escapeUrl($block->getUrl('directory/json/countryRegion'))) ?>"; +script; + +$scriptString .= 'this.regionsUrl = "' . $block->escapeJs($block->getUrl('directory/json/countryRegion')) . '";'; +$scriptString .= <<<script this.bindCountryRegionRelation(); }, @@ -259,4 +267,7 @@ function showHint() { Event.observe(window, 'load', showHint); }); -</script> +script; +?> + +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml index 0d07051e6667d..19f7a73739400 100644 --- a/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml +++ b/app/code/Magento/Config/view/adminhtml/templates/system/config/switcher.phtml @@ -3,34 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Backend\Block\Template */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Backend\Block\Template */ ?> <div class="field field-store-switcher"> <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Current Configuration Scope:')) ?></label> <div class="control"> - <select id="store_switcher" class="system-config-store-switcher" - onchange="location.href=this.options[this.selectedIndex].getAttribute('url')"> - <?php foreach ($block->getStoreSelectOptions() as $_value => $_option) : ?> - <?php if (isset($_option['is_group'])) : ?> - <?php if ($_option['is_close']) : ?> + <select id="store_switcher" class="system-config-store-switcher"> + <?php foreach ($block->getStoreSelectOptions() as $_value => $_option): ?> + <?php if (isset($_option['is_group'])): ?> + <?php if ($_option['is_close']): ?> </optgroup> - <?php else : ?> - <optgroup label="<?= $block->escapeHtmlAttr($_option['label']) ?>" - style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> + <?php else: ?> + <optgroup label="<?= $block->escapeHtmlAttr($_option['label']) ?>"> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $_option['style'], + "optgroup[label='" . $block->escapeJs($_option['label']) . "']" + ) ?> <?php endif; ?> <?php continue ?> <?php endif; ?> <option value="<?= $block->escapeHtmlAttr($_value) ?>" url="<?= $block->escapeUrl($_option['url']) ?>" - <?= $_option['selected'] ? 'selected="selected"' : '' ?> - style="<?= $block->escapeHtmlAttr($_option['style']) ?>"> + <?= $_option['selected'] ? 'selected="selected"' : '' ?>> <?= $block->escapeHtml($_option['label']) ?> </option> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $_option['style'], + "optgroup[url='" . $block->escapeJs($_option['url']) . "']" + ) ?> <?php endforeach ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "location.href=this.options[this.selectedIndex].getAttribute('url')", + '#store_switcher' + ) ?> </div> <?= $block->getHintHtml() ?> - <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')) : ?> + <?php if ($block->getAuthorization()->isAllowed('Magento_Backend::store')): ?> <div class="actions"> <a href="<?= $block->escapeUrl($block->getUrl('*/system_store')) ?>"> <?= $block->escapeHtml(__('Stores')) ?> diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php index 11e75839ec33c..1718a460d7544 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Composite/Fieldset/Configurable.php @@ -7,12 +7,68 @@ */ namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset; +use Magento\Catalog\Helper\Product as ProductHelper; +use Magento\ConfigurableProduct\Model\ConfigurableAttributeData; +use Magento\Customer\Helper\Session\CurrentCustomer; +use Magento\Customer\Model\Session; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data; +use Magento\Framework\Locale\Format; +use Magento\Framework\Pricing\PriceCurrencyInterface; + /** * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Configurable extends \Magento\ConfigurableProduct\Block\Product\View\Type\Configurable { + /** + * @param \Magento\Catalog\Block\Product\Context $context + * @param \Magento\Framework\Stdlib\ArrayUtils $arrayUtils + * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder + * @param \Magento\ConfigurableProduct\Helper\Data $helper + * @param \Magento\Catalog\Helper\Product $catalogProduct + * @param CurrentCustomer $currentCustomer + * @param PriceCurrencyInterface $priceCurrency + * @param ConfigurableAttributeData $configurableAttributeData + * @param array $data + * @param Format|null $localeFormat + * @param Session|null $customerSession + * @param \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices|null $variationPrices + */ + public function __construct( + \Magento\Catalog\Block\Product\Context $context, + \Magento\Framework\Stdlib\ArrayUtils $arrayUtils, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + \Magento\ConfigurableProduct\Helper\Data $helper, + \Magento\Catalog\Helper\Product $catalogProduct, + CurrentCustomer $currentCustomer, + PriceCurrencyInterface $priceCurrency, + ConfigurableAttributeData $configurableAttributeData, + array $data = [], + Format $localeFormat = null, + Session $customerSession = null, + \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Variations\Prices $variationPrices = null + ) { + $data['productHelper'] = $catalogProduct; + parent::__construct( + $context, + $arrayUtils, + $jsonEncoder, + $helper, + $catalogProduct, + $currentCustomer, + $priceCurrency, + $configurableAttributeData, + $data, + $localeFormat, + $customerSession, + $variationPrices + ); + } + /** * Retrieve product * diff --git a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php index bb5c8d8b49ca2..b400ef5f97efb 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php +++ b/app/code/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/Bulk.php @@ -5,13 +5,16 @@ */ namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps; +use Magento\Backend\Helper\Js; use Magento\Catalog\Helper\Image; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Media\Config; use Magento\Catalog\Model\Product\Type; use Magento\Catalog\Model\ProductFactory; use Magento\Eav\Model\Entity\Attribute; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** * Adminhtml block for fieldset of configurable product @@ -41,21 +44,26 @@ class Bulk extends \Magento\Ui\Block\Component\StepsWizard\StepAbstract * @param Image $image * @param Config $catalogProductMediaConfig * @param ProductFactory $productFactory + * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( Context $context, Image $image, Config $catalogProductMediaConfig, - ProductFactory $productFactory + ProductFactory $productFactory, + array $data = [], + JsonHelper $jsonHelper = null ) { - parent::__construct($context); + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); + parent::__construct($context, $data); $this->image = $image; $this->productFactory = $productFactory; $this->catalogProductMediaConfig = $catalogProductMediaConfig; } /** - * {@inheritdoc} + * @inheritdoc */ public function getCaption() { @@ -63,6 +71,8 @@ public function getCaption() } /** + * Return no image url. + * * @return string */ public function getNoImageUrl() @@ -92,6 +102,8 @@ public function getImageTypes() } /** + * Return media attributes. + * * @return array */ public function getMediaAttributes() diff --git a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php index 77110975401ff..a73e7e7277d34 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Cart/Item/Renderer/Configurable.php @@ -75,6 +75,7 @@ public function getIdentities() * Get price for exact simple product added to cart * * @inheritdoc + * @since 100.3.1 */ public function getProductPriceHtml(\Magento\Catalog\Model\Product $product) { diff --git a/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php new file mode 100644 index 0000000000000..fbc45a9cfc791 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Block/DataProviders/PermissionsData.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\DataProviders; + +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; + +/** + * Provides permissions data into template. + */ +class PermissionsData implements ArgumentInterface +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * Constructor + * + * @param AuthorizationInterface $authorization + */ + public function __construct(AuthorizationInterface $authorization) + { + $this->authorization = $authorization; + } + + /** + * Check that user is allowed to manage attributes + * + * @return bool + */ + public function isAllowedToManageAttributes(): bool + { + return $this->authorization->isAllowed('Magento_Catalog::attributes_attributes'); + } +} diff --git a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php index 55c0c8f6ca4ce..636ff85d12e24 100644 --- a/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Block/Product/View/Type/Configurable.php @@ -35,7 +35,7 @@ class Configurable extends \Magento\Catalog\Block\Product\View\AbstractView /** * Current customer * - * @deprecated 100.2.0, as unused property + * @deprecated 100.2.0 as unused property * @var CurrentCustomer */ protected $currentCustomer; @@ -134,7 +134,7 @@ public function __construct( * Get cache key informative items. * * @return array - * @since 100.2.0 + * @since 100.1.10 */ public function getCacheKeyInfo() { @@ -253,7 +253,7 @@ public function getJsonConfig() * Get product images for configurable variations * * @return array - * @since 100.2.0 + * @since 100.1.10 */ protected function getOptionImages() { @@ -332,7 +332,7 @@ protected function getOptionPrices() /** * Replace ',' on '.' for js * - * @deprecated 100.2.0 Will be removed in major release + * @deprecated 100.1.10 Will be removed in major release * @param float $price * @return string */ @@ -345,7 +345,7 @@ protected function _registerJsPrice($price) * Should we generate "As low as" block or not * * @return bool - * @since 100.2.0 + * @since 100.1.10 */ public function showMinimalPrice() { diff --git a/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php b/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php index ca289122a2126..e9e91485ea7a5 100644 --- a/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php +++ b/app/code/Magento/ConfigurableProduct/Model/AttributeOptionProviderInterface.php @@ -8,7 +8,7 @@ /** * Interface to retrieve options for attribute * @api - * @since 100.2.0 + * @since 100.1.11 */ interface AttributeOptionProviderInterface { @@ -18,7 +18,7 @@ interface AttributeOptionProviderInterface * @param \Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute * @param int $productId * @return array - * @since 100.2.0 + * @since 100.1.11 */ public function getAttributeOptions(\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute, $productId); } diff --git a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php index 890564fdb303c..c7217dc9df80a 100644 --- a/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php +++ b/app/code/Magento/ConfigurableProduct/Model/LinkManagement.php @@ -199,7 +199,7 @@ public function removeChild($sku, $childSku) * * @return \Magento\ConfigurableProduct\Helper\Product\Options\Factory * - * @deprecated 100.1.2 + * @deprecated 100.2.0 */ private function getOptionsFactory() { diff --git a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php index 8bc7f05b49e30..dc4ad39752e4f 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php +++ b/app/code/Magento/ConfigurableProduct/Model/Plugin/ProductRepositorySave.php @@ -3,6 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\ConfigurableProduct\Model\Plugin; use Magento\Catalog\Api\Data\ProductInterface; @@ -56,7 +59,7 @@ public function beforeSave( ProductRepositoryInterface $subject, ProductInterface $product, $saveOptions = false - ) { + ): array { $result[] = $product; if ($product->getTypeId() !== Configurable::TYPE_CODE) { return $result; @@ -102,7 +105,7 @@ public function afterSave( ProductInterface $result, ProductInterface $product, $saveOptions = false - ) { + ): ProductInterface { if ($product->getTypeId() !== Configurable::TYPE_CODE) { return $result; } @@ -120,19 +123,23 @@ public function afterSave( * @throws InputException * @throws NoSuchEntityException */ - private function validateProductLinks(array $attributeCodes, array $linkIds) + private function validateProductLinks(array $attributeCodes, array $linkIds): void { $valueMap = []; foreach ($linkIds as $productId) { $variation = $this->productRepository->getById($productId); $valueKey = ''; foreach ($attributeCodes as $attributeCode) { - if (!$variation->getData($attributeCode)) { + if ($variation->getData($attributeCode) === null) { throw new InputException( - __('Product with id "%1" does not contain required attribute "%2".', $productId, $attributeCode) + __( + 'Product with id "%1" does not contain required attribute "%2".', + $productId, + $attributeCode + ) ); } - $valueKey = $valueKey . $attributeCode . ':' . $variation->getData($attributeCode) . ';'; + $valueKey .= $attributeCode . ':' . $variation->getData($attributeCode) . ';'; } if (isset($valueMap[$valueKey])) { throw new InputException( diff --git a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php index 1b3ecfb1d222a..c2ae381b345c6 100644 --- a/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php +++ b/app/code/Magento/ConfigurableProduct/Model/Product/Type/Configurable.php @@ -106,6 +106,7 @@ class Configurable extends \Magento\Catalog\Model\Product\Type\AbstractType * Local cache * * @var array + * @since 100.4.0 */ protected $isSaleableBySku = []; @@ -591,6 +592,7 @@ protected function getGalleryReadHandler() * * @param \Magento\Catalog\Model\Product $product * @return \Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\Collection + * @since 100.4.0 */ protected function getLinkedProductCollection($product) { @@ -1266,7 +1268,7 @@ private function getCatalogConfig() /** * @inheritdoc - * @since 100.2.0 + * @since 100.1.11 */ public function isPossibleBuyFromList($product) { 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 57f701721a6f3..0ced38c4a6923 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 @@ -41,7 +41,7 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * Product instance * * @var \Magento\Catalog\Model\Product - * @deprecated Now collection supports fetching options for multiple products. This field will be set to first + * @deprecated 100.3.0 Now collection supports fetching options for multiple products. This field will be set to first * element of products array. */ protected $_product; @@ -174,6 +174,7 @@ public function getStoreId() * * @return $this * @throws \Exception + * @since 100.3.0 */ protected function _beforeLoad() { diff --git a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php index b76954075bcde..ae591474cd13e 100644 --- a/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php +++ b/app/code/Magento/ConfigurableProduct/Model/ResourceModel/Product/Type/Configurable/Product/Collection.php @@ -74,6 +74,7 @@ public function setProductFilter($product) * Add parent ids to `in` filter before load. * * @return $this + * @since 100.3.0 */ protected function _renderFilters() { diff --git a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php index 1555e88700a45..2f333e7ca6f6e 100644 --- a/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php +++ b/app/code/Magento/ConfigurableProduct/Plugin/Model/ResourceModel/Product.php @@ -4,11 +4,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\ConfigurableProduct\Plugin\Model\ResourceModel; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\ConfigurableProduct\Api\Data\OptionInterface; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Indexer\ActionInterface; +/** + * Plugin product resource model + */ class Product { /** @@ -21,18 +31,45 @@ class Product */ private $productIndexer; + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + /** * Initialize Product dependencies. * * @param Configurable $configurable * @param ActionInterface $productIndexer + * @param ProductAttributeRepositoryInterface $productAttributeRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterBuilder $filterBuilder */ public function __construct( Configurable $configurable, - ActionInterface $productIndexer + ActionInterface $productIndexer, + ProductAttributeRepositoryInterface $productAttributeRepository = null, + SearchCriteriaBuilder $searchCriteriaBuilder = null, + FilterBuilder $filterBuilder = null ) { $this->configurable = $configurable; $this->productIndexer = $productIndexer; + $this->productAttributeRepository = $productAttributeRepository ?: ObjectManager::getInstance() + ->get(ProductAttributeRepositoryInterface::class); + $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: ObjectManager::getInstance() + ->get(SearchCriteriaBuilder::class); + $this->filterBuilder = $filterBuilder ?: ObjectManager::getInstance() + ->get(FilterBuilder::class); } /** @@ -41,6 +78,7 @@ public function __construct( * @param \Magento\Catalog\Model\ResourceModel\Product $subject * @param \Magento\Framework\DataObject $object * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException * * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -51,6 +89,39 @@ public function beforeSave( /** @var \Magento\Catalog\Model\Product $object */ if ($object->getTypeId() == Configurable::TYPE_CODE) { $object->getTypeInstance()->getSetAttributes($object); + $this->resetConfigurableOptionsData($object); + } + } + + /** + * Set null for configurable options attribute of configurable product + * + * @param \Magento\Catalog\Model\Product $object + * @return void + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function resetConfigurableOptionsData($object) + { + $extensionAttribute = $object->getExtensionAttributes(); + if ($extensionAttribute && $extensionAttribute->getConfigurableProductOptions()) { + $attributeIds = []; + /** @var OptionInterface $option */ + foreach ($extensionAttribute->getConfigurableProductOptions() as $option) { + $attributeIds[] = $option->getAttributeId(); + } + + $filter = $this->filterBuilder + ->setField(ProductAttributeInterface::ATTRIBUTE_ID) + ->setConditionType('in') + ->setValue($attributeIds) + ->create(); + $this->searchCriteriaBuilder->addFilters([$filter]); + $searchCriteria = $this->searchCriteriaBuilder->create(); + $optionAttributes = $this->productAttributeRepository->getList($searchCriteria)->getItems(); + + foreach ($optionAttributes as $optionAttribute) { + $object->setData($optionAttribute->getAttributeCode(), null); + } } } diff --git a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php index 5581fcc07b861..af9e6e7bdebcd 100644 --- a/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php +++ b/app/code/Magento/ConfigurableProduct/Pricing/Price/ConfigurablePriceResolver.php @@ -19,13 +19,13 @@ class ConfigurablePriceResolver implements PriceResolverInterface /** * @var PriceCurrencyInterface - * @deprecated 100.1.1 + * @deprecated 100.0.2 */ protected $priceCurrency; /** * @var Configurable - * @deprecated 100.1.1 + * @deprecated 100.0.2 */ protected $configurable; diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml new file mode 100644 index 0000000000000..c48f22a3656d5 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddOptionsToAttributeWithDefaultLayeredNavigationActionGroup"> + <annotations> + <description>Adds 3 provided Options to a new Attribute on the Configurable Product creation/edit page. Selected default first option. Set "Use in Layered Navigation" to "Yes".</description> + </annotations> + <arguments> + <argument name="label" defaultValue="colorProductAttribute" /> + <argument name="option1" defaultValue="colorProductAttribute1"/> + <argument name="option2" defaultValue="colorProductAttribute2"/> + <argument name="option3" defaultValue="colorProductAttribute3"/> + </arguments> + + <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickOnCreateConfigurations"/> + <click selector="{{AdminCreateProductConfigurationsPanel.createNewAttribute}}" stepKey="clickOnNewAttribute"/> + <waitForPageLoad stepKey="waitForIFrame"/> + <switchToIFrame selector="{{AdminNewAttributePanel.newAttributeIFrame}}" stepKey="switchToNewAttributeIFrame"/> + <fillField selector="{{AdminNewAttributePanel.defaultLabel}}" userInput="{{label.default_label}}" stepKey="fillDefaultLabel"/> + + <!--Add option 1 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption1"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('1')}}" time="30" stepKey="waitForOptionRow1" after="clickAddOption1"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('0')}}" userInput="{{option1.name}}" stepKey="fillAdminLabel1" after="waitForOptionRow1"/> + <click selector="{{AdminNewAttributePanel.isDefault('1')}}" stepKey="selectDefault" after="fillAdminLabel1"/> + + <!--Add option 2 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption2" after="selectDefault"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('2')}}" time="30" stepKey="waitForOptionRow2" after="clickAddOption2"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('1')}}" userInput="{{option2.name}}" stepKey="fillAdminLabel2" after="waitForOptionRow2"/> + + <!--Add option 3 to attribute--> + <click selector="{{AdminNewAttributePanel.addOption}}" stepKey="clickAddOption3" after="fillAdminLabel2"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.isDefault('3')}}" time="30" stepKey="waitForOptionRow3" after="clickAddOption3"/> + <fillField selector="{{AdminNewAttributePanel.optionAdminValue('2')}}" userInput="{{option3.name}}" stepKey="fillAdminLabel3" after="waitForOptionRow3"/> + + <!-- Set Use In Layered Navigation --> + <click selector="{{AdminNewAttributePanel.storefrontPropertiesTab}}" stepKey="goToStorefrontPropertiesTab" after="fillAdminLabel3"/> + <waitForElementVisible selector="{{AdminNewAttributePanel.storefrontPropertiesTitle}}" stepKey="waitTabLoad" after="goToStorefrontPropertiesTab"/> + <selectOption selector="{{AdminNewAttributePanel.useInLayeredNavigation}}" stepKey="selectUseInLayer" userInput="Filterable (with results)" after="waitTabLoad"/> + + <!--Save attribute--> + <click selector="{{AdminNewAttributePanel.saveAttribute}}" stepKey="clickSaveAttribute"/> + <waitForPageLoad stepKey="waitForSavingAttribute"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..cc709b80efebb --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminFillBasicValueConfigurableProductActionGroup.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillBasicValueConfigurableProductActionGroup"> + <annotations> + <description>Goes to the Admin Product grid page. Fill basic value for Configurable Product using the default Product Options.</description> + </annotations> + <arguments> + <argument name="product" defaultValue="_defaultProduct"/> + <argument name="category" defaultValue="_defaultCategory"/> + </arguments> + <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <waitForPageLoad time="30" stepKey="wait1"/> + <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> + <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> + <fillField userInput="{{product.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> + <fillField userInput="{{product.sku}}" selector="{{AdminProductFormSection.productSku}}" stepKey="fillSKU"/> + <fillField userInput="{{product.price}}" selector="{{AdminProductFormSection.productPrice}}" stepKey="fillPrice"/> + <fillField userInput="{{product.quantity}}" selector="{{AdminProductFormSection.productQuantity}}" stepKey="fillQuantity"/> + <searchAndMultiSelectOption selector="{{AdminProductFormSection.categoriesDropdown}}" parameterArray="[{{category.name}}]" stepKey="fillCategory"/> + <selectOption userInput="{{product.visibility}}" selector="{{AdminProductFormSection.visibility}}" stepKey="fillVisibility"/> + <click selector="{{AdminProductSEOSection.sectionHeader}}" stepKey="openSeoSection"/> + <fillField userInput="{{product.urlKey}}" selector="{{AdminProductSEOSection.urlKeyInput}}" stepKey="fillUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml new file mode 100644 index 0000000000000..969a41e27d459 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminGotoSelectValueAttributePageActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminGotoSelectValueAttributePageActionGroup"> + <annotations> + <description>Goes to the select values page from each attribute to include in the product.</description> + </annotations> + + <arguments> + <argument name="defaultLabelAttribute" type="string" defaultValue="{{colorProductAttribute.default_label}}"/> + </arguments> + + <click selector="{{AdminCreateProductConfigurationsPanel.filters}}" stepKey="clickOnFilters"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.attributeCode}}" userInput="{{defaultLabelAttribute}}" stepKey="fillFilterAttributeCodeField"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applyFilters}}" stepKey="clickApplyFiltersButton"/> + <click selector="{{AdminCreateProductConfigurationsPanel.firstCheckbox}}" stepKey="clickOnFirstCheckbox"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml new file mode 100644 index 0000000000000..cc2ff9a63ae40 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSelectValueFromAttributeActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSelectValueFromAttributeActionGroup"> + <annotations> + <description>Click to check option.</description> + </annotations> + + <arguments> + <argument name="option" defaultValue="colorProductAttribute1"/> + </arguments> + <click selector="{{AdminCreateProductConfigurationsPanel.attributeOption(option.name)}}" stepKey="clickOnCreateNewValue2"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml new file mode 100644 index 0000000000000..3cca319d9569c --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/AdminSetQuantityToEachSkusConfigurableProductActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSetQuantityToEachSkusConfigurableProductActionGroup"> + <annotations> + <description>Set quantity 1 to all child skus for configurable product. Save a configurable product and confirm.</description> + </annotations> + + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> + <click selector="{{AdminCreateProductConfigurationsPanel.applySingleQuantityToEachSkus}}" stepKey="clickOnApplySingleQuantityToEachSku"/> + <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="1" stepKey="enterAttributeQuantity"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> + <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> + <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton2"/> + <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> + <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontUpdateCartItemEditParametersProductActionGroup.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontUpdateCartItemEditParametersProductActionGroup.xml new file mode 100644 index 0000000000000..595cfa7c77409 --- /dev/null +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/ActionGroup/StorefrontUpdateCartItemEditParametersProductActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontUpdateCartItemEditParametersProductActionGroup"> + <arguments> + <argument name="rowNumber" type="string" defaultValue="1"/> + </arguments> + <click selector="{{CheckoutCartProductSection.nthEditButton(rowNumber)}}" stepKey="clickEditConfigurableProductButton"/> + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml index a1a499f33eda0..c827b9998450a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Data/ConfigurableProductOptionData.xml @@ -26,4 +26,23 @@ <requiredEntity type="ValueIndex">ValueIndex2</requiredEntity> <requiredEntity type="ValueIndex">ValueIndex3</requiredEntity> </entity> + <entity name="ConfigurableProduct15Options" type="ConfigurableProductOption"> + <var key="attribute_id" entityKey="attribute_id" entityType="ProductAttribute" /> + <data key="label">option</data> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + <requiredEntity type="ValueIndex">ValueIndex1</requiredEntity> + </entity> </entities> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml index 0d83cc6610194..4190dafb927e1 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddDefaultImageConfigurableTest.xml @@ -81,7 +81,7 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2"/> <deleteData createDataKey="childProductHandle1" stepKey="deleteChild1"/> @@ -94,8 +94,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGrid"> <argument name="product" value="$$baseConfigProductHandle$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml index 72ebd7962f420..c318b3e37cd0f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAddingNewOptionsWithImagesAndPricesToConfigurableProductTest.xml @@ -29,7 +29,7 @@ <deleteData createDataKey="createConfigProductAttributeCreateConfigurableProduct" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createConfigChildProduct1CreateConfigurableProduct" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2CreateConfigurableProduct" stepKey="deleteConfigChildProduct2"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> <argument name="productName" value="$$createConfigProductCreateConfigurableProduct.name$$"/> @@ -108,6 +108,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <magentoCron stepKey="runCronIndex" groups="index"/> <!--Go to frontend and check image and price--> <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct.custom_attributes[url_key]$$)}}" stepKey="goToProductPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml index 6e26d73f3a36f..d83b994b5d6b3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminAssertNoticeThatExistingSkuAutomaticallyChangedWhenSavingProductWithSameSkuTest.xml @@ -36,8 +36,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -64,7 +63,7 @@ <!-- Save product --> <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductAgain"/> <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmPopup"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml index 8962efbb8dd26..804de69e38280 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckConfigurableProductAttributeValueUniquenessTest.xml @@ -46,13 +46,11 @@ </createData> <!--Go to created product page--> <comment userInput="Go to created product page" stepKey="goToProdPage"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductGrid"/> - <waitForPageLoad stepKey="waitForProductPage1"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductGrid"/> <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterByName"> <argument name="name" value="$$createConfigProduct.name$$"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductName"/> - <waitForPageLoad stepKey="waitForProductEditPageToLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductName"/> <!--Create configurations for the product--> <comment userInput="Create configurations for the product" stepKey="createConfigurations"/> <conditionalClick selector="{{AdminProductFormConfigurationsSection.sectionHeader}}" dependentSelector="{{AdminProductFormConfigurationsSection.createConfigurations}}" visible="false" stepKey="expandConfigurationsTab1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml index 597e95117349f..f4cad6590e1f6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckResultsOfColorAndOtherFiltersTest.xml @@ -115,8 +115,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> <!-- Create three configurable products with options --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <!-- Edit created first product as configurable product with options --> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterGridByFirstProduct"> <argument name="product" value="$$createFirstConfigurableProduct$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml index dc8c09864d0ab..b9e331bbfe5b4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCheckValidatorConfigurableProductTest.xml @@ -50,16 +50,14 @@ </after> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <waitForPageLoad stepKey="waitForProductFilterLoad"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Create configurations based off the Text Swatch we created earlier --> <click selector="{{AdminProductFormConfigurationsSection.createConfigurations}}" stepKey="clickCreateConfigurations"/> @@ -101,7 +99,7 @@ <waitForElementVisible selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="waitForNextPageOpened3"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateProducts"/> <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveButtonVisible"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitForPopUpVisible"/> <click selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="clickOnConfirmInPopup"/> <dontSeeElement selector="{{AdminMessagesSection.success}}" stepKey="dontSeeSaveProductMessage"/> @@ -119,7 +117,7 @@ <!--Click on "Save"--> <waitForElementVisible selector="{{AdminProductFormActionSection.saveButton}}" stepKey="waitForSaveBtnVisible"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductAgain"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductAgain"/> <!--Click on "Confirm". Product is saved, success message appears --> <waitForElementVisible selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" stepKey="waitPopUpVisible"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml index 274a75aedbc5f..43d1ed40e92ad 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductCreateTest/AdminCreateConfigurableProductAfterGettingIncorrectSKUMessageTest.xml @@ -53,15 +53,14 @@ <waitForPageLoad stepKey="waitForGenerateConfigure"/> <grabValueFrom selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" stepKey="grabTextFromContent"/> <fillField stepKey="fillMoreThan64Symbols" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="01234567890123456789012345678901234567890123456789012345678901234"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct1"/> <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.closePopUp}}" visible="true" stepKey="clickOnCloseInPopup"/> <see stepKey="seeErrorMessage" userInput="Please enter less or equal than 64 symbols."/> <fillField stepKey="fillCorrectSKU" selector="{{AdminProductFormConfigurationsSection.firstSKUInConfigurableProductsGrid}}" userInput="$grabTextFromContent"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct2"/> <conditionalClick selector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" dependentSelector="{{AdminChooseAffectedAttributeSetPopup.confirm}}" visible="true" stepKey="clickOnConfirmInPopup"/> <see userInput="You saved the product." stepKey="seeSaveConfirmation"/> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid1"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> @@ -72,8 +71,7 @@ <actionGroup stepKey="deleteProduct1" ref="DeleteProductBySkuActionGroup"> <argument name="sku" value="$grabTextFromContent"/> </actionGroup> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad time="60" stepKey="waitForPageLoadInitial"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml index a7615d5565828..4f6407ca4150c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductBulkDeleteTest.xml @@ -139,8 +139,7 @@ </after> <!-- Search for prefix of the 3 products we created via api --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml index 807ea69bb3958..186752fd52684 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductDeleteTest/AdminConfigurableProductDeleteTest.xml @@ -81,8 +81,7 @@ <!-- go to admin and delete --> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml index 10cdcea2855d6..f8cd1760788a8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductLongSkuTest.xml @@ -71,7 +71,7 @@ </actionGroup> <!--See SKU length errors in Current Variations grid--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductFail"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductFail"/> <seeInCurrentUrl url="{{AdminProductCreatePage.url(AddToDefaultSet.attributeSetId, 'configurable')}}" stepKey="seeRemainOnCreateProductPage"/> <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('1')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError1"/> <see selector="{{AdminProductFormConfigurationsSection.variationsSkuInputErrorByRow('2')}}" userInput="Please enter less or equal than 64 symbols." stepKey="seeSkuTooLongError2"/> @@ -79,7 +79,7 @@ <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('1')}}" userInput="LongSku-$$getConfigAttributeOption1.label$$" stepKey="fixConfigurationSku1"/> <fillField selector="{{AdminProductFormConfigurationsSection.variationsSkuInputByRow('2')}}" userInput="LongSku-$$getConfigAttributeOption2.label$$" stepKey="fixConfigurationSku2"/> <!--Save product successfully--> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProductSuccess"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProductSuccess"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> <!--Assert configurations on the product edit pag--> @@ -91,7 +91,9 @@ <see selector="{{AdminProductFormConfigurationsSection.currentVariationsPriceCells}}" userInput="{{ProductWithLongNameSku.price}}" stepKey="seeConfigurationsPrice"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Assert storefront category list page--> <amOnPage url="/" stepKey="amOnStorefront"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml index 8d2f80ef262fd..3f21007a76282 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductChildrenOutOfStockTest.xml @@ -93,19 +93,17 @@ <see stepKey="checkForOutOfStock" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="ApiSimpleOne"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Edit the quantity of the simple first product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- Check to make sure that the configurable product shows up as in stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage2"/> @@ -113,19 +111,17 @@ <see stepKey="checkForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> <!-- Find the second simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage2"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct2"> <argument name="product" value="ApiSimpleTwo"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> - <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage2"/> <!-- Edit the quantity of the second simple product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct2"/> <!-- Check to make sure that the configurable product shows up as out of stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml index 3121725c23fe9..5a97fb14abda9 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductOutOfStockTest/AdminConfigurableProductOutOfStockAndDeleteCombinationTest.xml @@ -102,19 +102,17 @@ <see stepKey="checkForOutOfStock2" selector="{{StorefrontProductInfoMainSection.stockIndication}}" userInput="IN STOCK"/> <!-- Find the second simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage2"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct2"> <argument name="product" value="ApiSimpleTwo"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied2"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> - <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage2"/> <!-- Edit the quantity of the second simple product as 0 --> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="0" stepKey="fillProductQuantity2"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct2"/> <!-- Check to make sure that the configurable product shows up as out of stock --> <amOnPage url="/{{ApiConfigurableProduct.urlKey}}2.html" stepKey="goToConfigProductPage3"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml index a35ef058dfd80..6311eaa9f2f99 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductFilterByTypeTest.xml @@ -77,8 +77,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <click selector="{{AdminProductGridFilterSection.filters}}" stepKey="clickShowFilters"/> <selectOption selector="{{AdminProductGridFilterSection.typeFilter}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="selectConfigurableType"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml index 6d9015b5d1cbf..421fcb0c03263 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductSearchTest/AdminConfigurableProductSearchTest.xml @@ -77,15 +77,18 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" stepKey="clearAll" visible="true"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> </actionGroup> <waitForPageLoad stepKey="wait2"/> <seeNumberOfElements selector="{{AdminProductGridSection.productGridRows}}" userInput="1" stepKey="seeOneResult"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="seeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="{{ApiConfigurableProduct.name}}"/> + </actionGroup> <see selector="{{AdminProductGridFilterSection.enabledFilters}}" userInput="{{ApiConfigurableProduct.name}}" stepKey="seeInActiveFilters"/> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml index 4b6baf8c58493..ba120d75f8e62 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateAttributeTest.xml @@ -116,16 +116,13 @@ <grabTextFrom stepKey="getBeforeOption" selector="{{StorefrontProductInfoMainSection.nthAttributeOnPage('1')}}"/> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <waitForPageLoad stepKey="waitForProductFilterLoad"/> - - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- change the option on the first attribute --> <selectOption stepKey="clickFirstAttribute" selector="{{ModifyAttributes.nthExistingAttribute($$createModifiableProductAttribute.default_frontend_label$$)}}" userInput="option1"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml index 56f53519e69af..e0150f08d8360 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateAttributeTest/AdminConfigurableProductUpdateChildAttributeTest.xml @@ -94,8 +94,7 @@ </after> <!-- Find the product that we just created using the product grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="$$createConfigProduct$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml index 589f20d0d544c..854cfb98607a7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductAddConfigurationTest.xml @@ -51,7 +51,7 @@ <fillField selector="{{AdminCreateProductConfigurationsPanel.quantity}}" userInput="42" stepKey="enterAttributeQuantity"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Verify that the added option is present in the storefront --> <amOnPage url="{{StorefrontProductPage.url(_defaultProduct.urlKey)}}" stepKey="amOnStorefrontPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml index 186799bf4626b..556ede0bdc06f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductBulkUpdateTest.xml @@ -37,15 +37,13 @@ <deleteData createDataKey="createProduct1" stepKey="deleteFirstProduct"/> <deleteData createDataKey="createProduct2" stepKey="deleteSecondProduct"/> <deleteData createDataKey="createProduct3" stepKey="deleteThirdProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- Search for prefix of the 3 products we created via api --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearAll}}" dependentSelector="{{AdminProductGridFilterSection.clearAll}}" visible="true" stepKey="clearAll"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchForProduct"> <argument name="keyword" value="ApiConfigurableProduct.name"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml index 1eb3df993dd1c..345b21246f30f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductDisableAnOptionTest.xml @@ -88,8 +88,7 @@ <waitForPageLoad stepKey="wait2"/> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActions"/> <click selector="{{AdminProductFormConfigurationsSection.disableProductBtn}}" stepKey="clickDisable"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <!--check storefront for one option--> <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="amOnStorefront2"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml index 00b17fda944f1..ec0ed623d2b0c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveAnOptionTest.xml @@ -92,8 +92,7 @@ <!--remove an option--> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActions"/> <click selector="{{AdminProductFormConfigurationsSection.removeProductBtn}}" stepKey="clickRemove"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> - <waitForPageLoad stepKey="wait3"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSave"/> <!--check admin for one option--> <dontSee selector="{{AdminProductFormConfigurationsSection.currentVariationsNameCells}}" userInput="$$createConfigChildProduct1.name$$" stepKey="dontSeeOption1Admin"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml index a4051adfe5d28..75fbaa7f43887 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminConfigurableProductUpdateTest/AdminConfigurableProductRemoveConfigurationTest.xml @@ -52,7 +52,7 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton2"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton3"/> <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="clickOnNextButton4"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!-- Verify that the removed option is not present in the storefront --> <amOnPage url="{{StorefrontProductPage.url(_defaultProduct.urlKey)}}" stepKey="amOnStorefrontPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml index 98bd5a0fed4ed..ebefab1f6650a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToSimpleTest.xml @@ -24,6 +24,10 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillProductForm"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Simple Product"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml index 756cdfd5d5d6f..a0ad8475b7d41 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateConfigurableProductSwitchToVirtualTest.xml @@ -21,6 +21,10 @@ <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="openProductFillForm"> <argument name="productType" value="configurable"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Virtual Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Virtual Product"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml index db5c824341c57..dc3608ec827df 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateDownloadableProductSwitchToConfigurableTest.xml @@ -67,7 +67,11 @@ <actionGroup ref="FilterProductGridByNameActionGroup" stepKey="searchForProduct"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="2"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> <actionGroup ref="AssertProductInStorefrontProductPageActionGroup" stepKey="assertProductInStorefrontProductPage"> <argument name="product" value="_defaultProduct"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml index cbfa1cc2b8bd6..bf7d97df75bb0 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateSimpleProductSwitchToConfigurableTest.xml @@ -40,7 +40,11 @@ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> </actionGroup> <actionGroup ref="SaveConfiguredProductActionGroup" stepKey="saveProductForm"/> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="2"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> <!-- Verify product on store front --> <comment userInput="Verify product on store front" stepKey="commentVerifyProductGrid"/> <actionGroup ref="VerifyOptionInProductStorefrontActionGroup" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml index cfeb95afc4924..b1df023e7deec 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateAndSwitchProductType/AdminCreateVirtualProductSwitchToConfigurableTest.xml @@ -38,7 +38,11 @@ <argument name="attributeCode" value="$$createConfigProductAttribute.attribute_code$$"/> </actionGroup> <actionGroup ref="SaveConfiguredProductActionGroup" stepKey="saveProductForm"/> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Type')}}" userInput="Configurable Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="2"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> <actionGroup ref="VerifyOptionInProductStorefrontActionGroup" stepKey="verifyConfigurableOption" after="AssertProductInStorefrontProductPage"> <argument name="attributeCode" value="$createConfigProductAttribute.default_frontend_label$"/> <argument name="optionName" value="$createConfigProductAttributeOption1.option[store_labels][1][label]$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml index e5456429373e1..044346041d30c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductBasedOnParentSkuTest.xml @@ -36,8 +36,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -68,15 +67,22 @@ <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveProduct"/> <!-- Assert child products generated sku in grid --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="openProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPageLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterFirstProductByNameInGrid"> <argument name="name" value="{{colorConfigurableProductAttribute1.name}}"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute1.name}}" stepKey="seeFirstProductSkuInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeFirstProductSkuInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="SKU"/> + <argument name="value" value="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute1.name}}"/> + </actionGroup> <actionGroup ref="FilterProductGridByName2ActionGroup" stepKey="filterSecondProductByNameInGrid"> <argument name="name" value="{{colorConfigurableProductAttribute2.name}}"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'SKU')}}" userInput="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute2.name}}" stepKey="seeSecondProductSkuInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeSecondProductSkuInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="SKU"/> + <argument name="value" value="{{ApiConfigurableProduct.sku}}-{{colorConfigurableProductAttribute2.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml index 32117fdfe4366..c285287130aca 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithCreatingCategoryAndAttributeTest.xml @@ -53,8 +53,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -88,21 +87,21 @@ <actionGroup ref="SaveConfigurableProductWithNewAttributeSetActionGroup" stepKey="saveConfigurableProduct"/> <!-- Find configurable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <!-- Assert configurable product on admin product page --> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <actionGroup ref="AssertConfigurableProductOnAdminProductPageActionGroup" stepKey="assertConfigurableProductOnAdminProductPage"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml index 3bf5666d5a997..d210f90779d99 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithDisabledChildrenProductsTest.xml @@ -60,8 +60,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -89,15 +88,13 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveConfigurableProduct"/> <!-- Find configurable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> <!-- Assert configurable product on admin product page --> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <actionGroup ref="AssertConfigurableProductOnAdminProductPageActionGroup" stepKey="assertConfigurableProductOnAdminProductPage"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -111,7 +108,9 @@ <actionGroup ref="DisplayOutOfStockProductActionGroup" stepKey="displayOutOfStockProduct"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert configurable product is not present in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml index fa8866fa7d91c..120734d679d09 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithImagesTest.xml @@ -63,8 +63,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -133,7 +132,9 @@ <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveProduct"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml index e76d14f3a6aae..4afc95f9a6355 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDisplayOutOfStockProductsTest.xml @@ -83,8 +83,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -125,7 +124,9 @@ <actionGroup ref="DisplayOutOfStockProductActionGroup" stepKey="displayOutOfStockProduct"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml index 9516216d4a62e..ad634ed0144ae 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithThreeProductDontDisplayOutOfStockProductsTest.xml @@ -82,8 +82,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -121,7 +120,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveConfigurableProduct"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml index 660eb82a9eacb..1491081a82ee4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTierPriceForOneItemTest.xml @@ -71,8 +71,7 @@ </after> <!--Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml index f2a8e78523758..2fcf9a622a97f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsAssignedToCategoryTest.xml @@ -56,8 +56,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -102,11 +101,17 @@ <actionGroup ref="FilterProductGridBySkuAndNameActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="{{ApiConfigurableProduct.type_id}}"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml index 273e37089973b..b562c8ab6fb1a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminCreateConfigurableProductWithTwoOptionsWithoutAssignedToCategoryTest.xml @@ -49,8 +49,7 @@ </after> <!-- Create configurable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="createConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -92,11 +91,17 @@ <actionGroup ref="FilterProductGridBySkuAndNameActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="{{ApiConfigurableProduct.type_id}}" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="{{ApiConfigurableProduct.type_id}}"/> + </actionGroup> <click selector="{{AdminProductGridFilterSection.clearFilters}}" stepKey="clickClearFiltersAfter"/> <!-- Flash cache --> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Assert configurable product on product page --> <amOnPage url="{{ApiConfigurableProduct.urlKey}}.html" stepKey="amOnProductPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml index e625a1cf6f2be..1a6d802987cd3 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminDeleteConfigurableProductTest.xml @@ -36,10 +36,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createConfigurableProduct.name$$)}}" stepKey="amOnConfigurableProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createConfigurableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchBarByProductSku"> + <argument name="query" value="$$createConfigurableProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createConfigurableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml index 90a396b970c3a..e986ea38f0fe1 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminVirtualProductTypeSwitchingToConfigurableProductTest.xml @@ -64,10 +64,26 @@ <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySkuForConfigurable"> <argument name="sku" value="$$createProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeConfigurableProductNameInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Configurable Product" stepKey="seeConfigurableProductTypeInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('2', 'Name')}}" userInput="$$createProduct.name$$-option1" stepKey="seeConfigurableProductNameInGrid1"/> - <see selector="{{AdminProductGridSection.productGridCell('3', 'Name')}}" userInput="$$createProduct.name$$-option2" stepKey="seeConfigurableProductNameInGrid2"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductNameInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Configurable Product"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductNameInGrid1"> + <argument name="row" value="2"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$-option1"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeConfigurableProductNameInGrid2"> + <argument name="row" value="3"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$-option2"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearConfigurableProductFilters"/> <!--Assert configurable product on storefront--> <comment userInput="Assert configurable product on storefront" stepKey="commentAssertConfigurableProductOnStorefront"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml index b6b3d21c8a626..fa25277554b74 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRelatedProductsTest.xml @@ -81,7 +81,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2"/> <deleteData createDataKey="childProductHandle1" stepKey="deleteChild1"/> @@ -95,8 +95,7 @@ </after> <comment userInput="Filter and edit simple product 1" stepKey="filterAndEditComment1"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridSimple"> <argument name="product" value="$$simple1Handle$$"/> @@ -140,8 +139,7 @@ </actionGroup> <comment userInput="Filter and edit config product" stepKey="filterAndEditComment2"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage2"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage2"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridConfig"> <argument name="product" value="$$baseConfigProductHandle$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml index 86d4070a9a2c8..076d55025aca5 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/AdminRemoveDefaultImageConfigurableTest.xml @@ -81,7 +81,7 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simple1Handle" stepKey="deleteSimple1"/> <deleteData createDataKey="simple2Handle" stepKey="deleteSimple2"/> <deleteData createDataKey="childProductHandle1" stepKey="deleteChild1"/> @@ -93,8 +93,7 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="productIndexPage"/> - <waitForPageLoad time="30" stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="productIndexPage"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGrid"> <argument name="product" value="$$baseConfigProductHandle$$"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml index a34dfd06ce844..5ecc0c33ad7a2 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ConfigurableProductPriceAdditionalStoreViewTest.xml @@ -15,11 +15,10 @@ <title value="Configurable product prices should not disappear on storefront for additional store"/> <description value="Configurable product price should not disappear for additional stores on frontEnd if disabled for default store"/> <severity value="CRITICAL"/> - <testCaseId value="MAGETWO-92247"/> + <testCaseId value="MC-25761"/> <group value="ConfigurableProduct"/> </annotations> <before> - <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> <requiredEntity createDataKey="createCategory"/> @@ -65,6 +64,22 @@ <requiredEntity createDataKey="createConfigChildProduct2"/> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> + + <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> + + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createAdditionalWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createNewStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> + </actionGroup> </before> <after> @@ -75,46 +90,21 @@ <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteSecondWebsite"> - <argument name="websiteName" value="Second Website"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="addNewWebsite"> - <argument name="newWebsiteName" value="Second Website"/> - <argument name="websiteCode" value="second_website"/> - </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="addNewStoreGroup"> - <argument name="website" value="Second Website"/> - <argument name="storeGroupName" value="Second Store"/> - <argument name="storeGroupCode" value="second_store"/> - </actionGroup> - - <!--Create Store view --> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="amOnAdminSystemStorePage"/> - <click selector="{{AdminStoresMainActionsSection.createStoreViewButton}}" stepKey="createStoreViewButton"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> - <selectOption userInput="Second Store" selector="{{AdminNewStoreSection.storeGrpDropdown}}" stepKey="selectStoreGroup"/> - <fillField userInput="Second Store View" selector="{{AdminNewStoreSection.storeNameTextField}}" stepKey="fillStoreViewName"/> - <fillField userInput="second_store_view" selector="{{AdminNewStoreSection.storeCodeTextField}}" stepKey="fillStoreViewCode"/> - <selectOption userInput="1" selector="{{AdminNewStoreSection.statusDropdown}}" stepKey="enableStoreViewStatus"/> - <click selector="{{AdminStoresMainActionsSection.saveButton}}" stepKey="clickStoreViewSaveButton"/> - <waitForElementVisible selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" stepKey="waitForAcceptNewStoreViewCreationModal" /> - <conditionalClick selector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" dependentSelector="{{AdminNewStoreSection.acceptNewStoreViewCreation}}" visible="true" stepKey="AcceptNewStoreViewCreation"/> - <waitForElementVisible selector="{{AdminStoresGridSection.storeFilterTextField}}" stepKey="waitForPageReolad"/> - <see userInput="You saved the store view." stepKey="seeSaveMessage" /> - <!--go to admin and open product edit page to disable product all store view --> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductEditPage"> - <argument name="productId" value="$$createConfigProduct.id$$"/> + <argument name="productId" value="$createConfigProduct.id$"/> </actionGroup> <waitForPageLoad stepKey="waitEditPage"/> <click selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="disableProductForAllStoreView"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton2"/> - <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave1" /> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveWithThreeOptions"/> <dontSeeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="dontSeeCheckboxEnableProductIsChecked"/> <!-- Disable each of the child products for All Store views --> @@ -126,17 +116,17 @@ <!-- Add product to second website --> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProductInWebsitesSection1"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite"/> <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSave"/> <waitForLoadingMaskToDisappear stepKey="waitForProductPageSave"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="seeSaveProductMessage"/> <!-- switch to the second store view --> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage"/> <waitForPageLoad time="30" stepKey="waitForPageLoad9"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName"/> <!-- enable the config product for the second store --> <waitForElementVisible selector="{{AdminProductFormSection.productStatusUseDefault}}" stepKey="waitForDefaultValueCheckBox"/> @@ -152,10 +142,10 @@ </actionGroup> <waitForPageLoad stepKey="waitEditPage2"/> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcher1"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView1"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView1"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessage1"/> <waitForPageLoad time="30" stepKey="waitForPageLoad8"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName1"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewName1"/> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('1')}}" stepKey="clickToExpandActionsForFirstVariation2"/> <click selector="{{AdminProductFormConfigurationsSection.enableProductBtn}}" stepKey="clickEnableChildProduct1"/> <click selector="{{AdminProductFormConfigurationsSection.actionsBtn('2')}}" stepKey="clickToExpandActionsForSecondVariation2"/> @@ -163,7 +153,7 @@ <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveAll"/> <!-- assert second store view storefront category list page --> - <amOnPage url="/second_store_view/" stepKey="amOnsecondStoreFront1"/> + <amOnPage url="/{{customStore.code}}/" stepKey="amOnsecondStoreFront1"/> <waitForPageLoad stepKey="waitForPageLoad31"/> <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName1"/> <waitForPageLoad stepKey="waitForPageLoad41"/> @@ -175,7 +165,7 @@ </actionGroup> <waitForPageLoad stepKey="waitChild1EditPageToLoad"/> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProduct1InWebsitesSection"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite1"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite1"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveUpdatedChild1Again"/> <!--go to admin again and open child product1 and enable for second store view--> @@ -184,10 +174,10 @@ </actionGroup> <waitForPageLoad stepKey="waitChild1EditPageToLoad1"/> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcherP1"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView2P1"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView2P1"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessageP1"/> <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP1"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP1"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP1"/> <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP1"/> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct1IsEnabled"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="save2UpdatedChild1"/> @@ -198,7 +188,7 @@ </actionGroup> <waitForPageLoad stepKey="waitChild2EditPageToLoad"/> <click selector="{{ProductInWebsitesSection.sectionHeader}}" stepKey="openProduct2InWebsitesSection"/> - <click selector="{{ProductInWebsitesSection.website('Second Website')}}" stepKey="selectSecondWebsite2"/> + <click selector="{{ProductInWebsitesSection.website(customWebsite.name)}}" stepKey="selectSecondWebsite2"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="saveUpdatedChild2"/> <!--go to admin again and open child product2 and enable for second store view--> @@ -207,16 +197,16 @@ </actionGroup> <waitForPageLoad stepKey="waitChild2EditPageToLoad1"/> <click selector="{{AdminProductFormActionSection.changeStoreButton}}" stepKey="clickStoreviewSwitcherP2"/> - <click selector="{{AdminProductFormActionSection.selectStoreView('Second Store View')}}" stepKey="chooseStoreView2P2"/> + <click selector="{{AdminProductFormActionSection.selectStoreView(customStore.name)}}" stepKey="chooseStoreView2P2"/> <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="acceptStoreSwitchingMessageP2"/> <waitForPageLoad time="30" stepKey="waitForStoreViewSwitchedP2"/> - <see userInput="Second Store View" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP2"/> + <see userInput="{{customStore.name}}" selector="{{AdminMainActionsSection.storeSwitcher}}" stepKey="seeNewStoreViewNameP2"/> <waitForElementVisible selector="{{AdminProductFormSection.enableProductLabel}}" stepKey="waitForProductEnableSliderP2"/> <seeCheckboxIsChecked selector="{{AdminProductFormSection.productStatus}}" stepKey="seeThatProduct2IsEnabled"/> <actionGroup ref="AdminFormSaveAndCloseActionGroup" stepKey="save2UpdatedChild2"/> <!-- assert storefront category list page --> - <amOnPage url="/second_store_view/" stepKey="amOnsecondStoreFront"/> + <amOnPage url="/{{customStore.code}}/" stepKey="amOnsecondStoreFront"/> <waitForPageLoad stepKey="waitForPageLoad3"/> <click userInput="$$createCategory.name$$" stepKey="clickOnCategoryName"/> <waitForPageLoad stepKey="waitForPageLoad4"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml index 13c4cad312188..e34bf7c22f06b 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/EndToEndB2CLoggedInUserTest.xml @@ -78,8 +78,12 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Verify Configurable Product in checkout cart items --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml index 372aa03e4e152..7f1034db062df 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/NewProductsListWidgetConfigurableProductTest.xml @@ -84,7 +84,7 @@ <waitForPageLoad stepKey="waitForEditPage"/> <fillField selector="{{AdminProductFormSection.setProductAsNewFrom}}" userInput="01/1/2000" stepKey="fillProductNewFrom"/> <fillField selector="{{AdminProductFormSection.setProductAsNewTo}}" userInput="01/1/2099" stepKey="fillProductNewTo"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml index a8856288b422a..898e277cff55c 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/ProductsQtyReturnAfterOrderCancelTest.xml @@ -42,8 +42,7 @@ <argument name="product" value="$$createConfigProduct$$"/> </actionGroup> <fillField selector="{{AdminProductFormSection.productQuantity}}" userInput="100" stepKey="changeProductQuantity"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveChanges"/> - <waitForPageLoad stepKey="waitProductGridToBeLoaded"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveChanges"/> <amOnPage url="$$createConfigProduct.sku$$.html" stepKey="navigateToProductPage"/> <waitForPageLoad stepKey="waitForProductPage"/> @@ -76,7 +75,7 @@ <fillField selector="{{AdminInvoiceItemsSection.qtyToInvoiceColumn}}" userInput="1" stepKey="ChangeQtyToInvoice"/> <click selector="{{AdminInvoiceItemsSection.updateQty}}" stepKey="updateQuantity"/> <waitForPageLoad stepKey="waitPageToBeLoaded"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> <waitForPageLoad stepKey="waitOrderDetailToLoad"/> <fillField selector="{{AdminShipmentItemsSection.itemQtyToShip('1')}}" userInput="1" stepKey="changeItemQtyToShip"/> @@ -93,7 +92,11 @@ <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> <argument name="sku" value="$$createConfigProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Quantity')}}" userInput="99" stepKey="seeProductSkuInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductSkuInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Quantity"/> + <argument name="value" value="99"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearProductFilters"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> </test> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml index fbf23597a3927..4ad2d0dc936eb 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductChildSearchTest.xml @@ -117,8 +117,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <!-- Go to the product page for the first product --> - <amOnPage stepKey="goToProductGrid" url="{{ProductCatalogPage.url}}"/> - <waitForPageLoad stepKey="waitForProductGridLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductGrid"/> <actionGroup stepKey="searchForSimpleProduct" ref="FilterProductGridBySku2ActionGroup"> <argument name="sku" value="$$createConfigChildProduct1.sku$$"/> </actionGroup> @@ -127,7 +126,7 @@ <!-- Edit the attribute for the first simple product --> <selectOption stepKey="editSelectAttribute" selector="{{ModifyAttributes.nthExistingAttribute($$createConfigProductAttributeSelect.default_frontend_label$$)}}" userInput="$$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$"/> <scrollToTopOfPage stepKey="scrollToTop"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <seeElement selector="{{AdminProductMessagesSection.successMessage}}" stepKey="assertSaveMessageSuccess"/> </before> @@ -145,12 +144,15 @@ <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> </after> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Quick search the storefront for the first attribute option --> - <amOnPage stepKey="goToStoreFront" url="{{StorefrontHomePage.url}}"/> - <waitForPageLoad stepKey="waitForStorefront"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStoreFront"/> <submitForm selector="#search_mini_form" parameterArray="['q' => $$createConfigProductAttributeSelectOption1.option[store_labels][0][label]$$]" stepKey="searchStorefront1" /> <seeElement stepKey="seeProduct1" selector="{{StorefrontCategoryProductSection.ProductTitleByName('$$createConfigProduct.name$$')}}"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml index ca0426f1b97d5..e20a6dcfa09b8 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductViewTest/StorefrontConfigurableProductGridViewTest.xml @@ -27,8 +27,12 @@ <argument name="category" value="$$createCategory$$"/> </actionGroup> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush eav" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="eav"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml index bbd5dbd8068f7..d9ad32df872f7 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontConfigurableProductWithFileCustomOptionTest.xml @@ -37,7 +37,7 @@ </actionGroup> <!--Add custom option to configurable product--> <actionGroup ref="AddProductCustomOptionFileActionGroup" stepKey="addCustomOptionToProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!--Go to storefront--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml index 7662779a6955f..3519503c1e287 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontShouldSeeOnlyConfigurableProductChildAssignedToSeparateCategoryTest.xml @@ -112,7 +112,9 @@ <argument name="categoryName" value="$$secondCategory.name$$"/> </actionGroup> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext" stepKey="reindexSearchIndex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexSearchIndex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> <!-- Go to storefront to view child product --> <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="goToSecondCategoryStorefront"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml index ef9f71da0ebca..363a8ea4d4fd6 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontSortingByPriceForConfigurableWithCatalogRuleAppliedTest.xml @@ -153,8 +153,12 @@ <argument name="discountAmount" value="{{CatalogRuleByPercentWith96Amount.discount_amount}}"/> </actionGroup> <actionGroup ref="AdminCatalogPriceRuleSaveAndApplyActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> - <magentoCLI command="indexer:reindex" arguments="catalogsearch_fulltext catalog_category_product catalog_product_price catalogrule_rule" stepKey="reindexIndices"/> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexIndices"> + <argument name="indices" value="catalogsearch_fulltext catalog_category_product catalog_product_price catalogrule_rule"/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="fullCache"> + <argument name="tags" value="full_page"/> + </actionGroup> <!--Reopen category with products and Sort by price desc--> <actionGroup ref="GoToStorefrontCategoryPageByParametersActionGroup" stepKey="goToStorefrontCategoryPage2"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml index 7acece767760d..9b046d5c71cfc 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVerifyConfigurableProductLayeredNavigationTest.xml @@ -123,8 +123,7 @@ </after> <!-- Open Product Index Page and Filter First Child product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageToLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="ApiSimpleOne"/> </actionGroup> @@ -134,12 +133,13 @@ <waitForPageLoad stepKey="waitForProductPageToLoad"/> <scrollTo selector="{{AdminProductFormSection.productQuantity}}" stepKey="scrollToProductQuantity"/> <selectOption selector="{{AdminProductFormSection.productStockStatus}}" userInput="Out of Stock" stepKey="disableProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickOnSaveButton"/> - <waitForPageLoad stepKey="waitForPageLoad1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickOnSaveButton"/> <see selector="{{AdminCategoryMessagesSection.SuccessMessage}}" userInput="You saved the product." stepKey="messageYouSavedTheProductIsShown"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Open Category in Store Front and select product attribute option from sidebar --> <actionGroup ref="SelectStorefrontSideBarAttributeOption" stepKey="selectStorefrontProductAttributeOption"> diff --git a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml index 801dfdb8540e8..976be77122547 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml +++ b/app/code/Magento/ConfigurableProduct/Test/Mftf/Test/StorefrontVisibilityOfDuplicateProductTest.xml @@ -190,8 +190,12 @@ <click selector="{{AdminCreateProductConfigurationsPanel.next}}" stepKey="generateConfigsForDuplicatedProduct"/> <waitForPageLoad stepKey="waitForDuplicatedProductPageLoad"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveDuplicatedProduct"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Assert configurable product in category--> <comment userInput="Assert configurable product in category" stepKey="commentAssertProductInCategoryPage"/> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="onStorefrontCategoryPage"/> diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php index a79c2ebbceca9..07b4a1faf3db4 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Model/Plugin/ProductRepositorySaveTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\ConfigurableProduct\Test\Unit\Model\Plugin; @@ -18,6 +19,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Exception\InputException; /** * Test for ProductRepositorySave plugin @@ -71,7 +73,8 @@ class ProductRepositorySaveTest extends TestCase */ protected function setUp(): void { - $this->productAttributeRepository = $this->getMockForAbstractClass(ProductAttributeRepositoryInterface::class); + $this->productAttributeRepository = + $this->getMockForAbstractClass(ProductAttributeRepositoryInterface::class); $this->product = $this->getMockBuilder(Product::class) ->disableOriginalConstructor() @@ -105,8 +108,10 @@ protected function setUp(): void /** * Validating the result after saving a configurable product + * + * @return void */ - public function testBeforeSaveWhenProductIsSimple() + public function testBeforeSaveWhenProductIsSimple(): void { $this->product->expects(static::once()) ->method('getTypeId') @@ -122,8 +127,10 @@ public function testBeforeSaveWhenProductIsSimple() /** * Test saving a configurable product without attribute options + * + * @return void */ - public function testBeforeSaveWithoutOptions() + public function testBeforeSaveWithoutOptions(): void { $this->product->expects(static::once()) ->method('getTypeId') @@ -151,10 +158,12 @@ public function testBeforeSaveWithoutOptions() /** * Test saving a configurable product with same set of attribute values + * + * @return void */ - public function testBeforeSaveWithLinks() + public function testBeforeSaveWithLinks(): void { - $this->expectException('Magento\Framework\Exception\InputException'); + $this->expectException(InputException::class); $this->expectExceptionMessage('Products "5" and "4" have the same set of attribute values.'); $links = [4, 5]; $this->product->expects(static::once()) @@ -191,10 +200,12 @@ public function testBeforeSaveWithLinks() /** * Test saving a configurable product with missing attribute + * + * @return void */ - public function testBeforeSaveWithLinksWithMissingAttribute() + public function testBeforeSaveWithLinksWithMissingAttribute(): void { - $this->expectException('Magento\Framework\Exception\InputException'); + $this->expectException(InputException::class); $this->expectExceptionMessage('Product with id "4" does not contain required attribute "color".'); $simpleProductId = 4; $links = [$simpleProductId, 5]; @@ -239,17 +250,19 @@ public function testBeforeSaveWithLinksWithMissingAttribute() $product->expects(static::once()) ->method('getData') ->with($attributeCode) - ->willReturn(false); + ->willReturn(null); $this->plugin->beforeSave($this->productRepository, $this->product); } /** * Test saving a configurable product with duplicate attributes + * + * @return void */ - public function testBeforeSaveWithLinksWithDuplicateAttributes() + public function testBeforeSaveWithLinksWithDuplicateAttributes(): void { - $this->expectException('Magento\Framework\Exception\InputException'); + $this->expectException(InputException::class); $this->expectExceptionMessage('Products "5" and "4" have the same set of attribute values.'); $links = [4, 5]; $attributeCode = 'color'; diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php index abab103fa6d37..3d5a0d1cc6a3f 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Model/ResourceModel/ProductTest.php @@ -7,20 +7,38 @@ namespace Magento\ConfigurableProduct\Test\Unit\Plugin\Model\ResourceModel; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\Product as ModelProduct; use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\ProductAttributeSearchResults; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; +use Magento\Catalog\Model\ResourceModel\Product as ResourceModelProduct; use Magento\ConfigurableProduct\Model\Product\Type\Configurable; -use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute as ConfigurableAttribute; +use Magento\ConfigurableProduct\Plugin\Model\ResourceModel\Product as PluginResourceModelProduct; +use Magento\Framework\Api\ExtensionAttributesInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\Indexer\ActionInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class ProductTest extends TestCase { /** - * @var ObjectManager + * @var PluginResourceModelProduct */ - private $objectManager; + private $model; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; /** * @var Configurable|MockObject @@ -33,39 +51,128 @@ class ProductTest extends TestCase private $actionMock; /** - * @var Product + * @var ProductAttributeRepositoryInterface|MockObject */ - private $model; + private $productAttributeRepositoryMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var FilterBuilder|MockObject + */ + private $filterBuilderMock; protected function setUp(): void { - $this->objectManager = new ObjectManager($this); $this->configurableMock = $this->createMock(Configurable::class); $this->actionMock = $this->getMockForAbstractClass(ActionInterface::class); - - $this->model = $this->objectManager->getObject( - Product::class, + $this->productAttributeRepositoryMock = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->setMethods(['getList']) + ->getMockForAbstractClass(); + $this->searchCriteriaBuilderMock = $this->createPartialMock( + SearchCriteriaBuilder::class, + ['addFilters', 'create'] + ); + $this->filterBuilderMock = $this->createPartialMock( + FilterBuilder::class, + ['setField', 'setConditionType', 'setValue', 'create'] + ); + $this->objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $this->objectManagerHelper->getObject( + PluginResourceModelProduct::class, [ 'configurable' => $this->configurableMock, 'productIndexer' => $this->actionMock, + 'productAttributeRepository' => $this->productAttributeRepositoryMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + 'filterBuilder' => $this->filterBuilderMock ] ); } - public function testBeforeSaveConfigurable() + public function testBeforeSaveConfigurable(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $object */ - $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']); + /** @var ResourceModelProduct|MockObject $subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $object */ + $object = $this->createPartialMock( + ModelProduct::class, + [ + 'getTypeId', + 'getTypeInstance', + 'getExtensionAttributes', + 'setData' + ] + ); $type = $this->createPartialMock( Configurable::class, ['getSetAttributes'] ); - $type->expects($this->once())->method('getSetAttributes')->with($object); - - $object->expects($this->once())->method('getTypeId')->willReturn(Configurable::TYPE_CODE); - $object->expects($this->once())->method('getTypeInstance')->willReturn($type); + $extensionAttributes = $this->getMockBuilder(ExtensionAttributesInterface::class) + ->disableOriginalConstructor() + ->addMethods(['getConfigurableProductOptions']) + ->getMock(); + $option = $this->createPartialMock( + ConfigurableAttribute::class, + ['getAttributeId'] + ); + $extensionAttributes->expects($this->exactly(2)) + ->method('getConfigurableProductOptions') + ->willReturn([$option]); + $object->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($extensionAttributes); + + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setField') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setValue') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('setConditionType') + ->willReturnSelf(); + $this->filterBuilderMock->expects($this->atLeastOnce()) + ->method('create') + ->willReturnSelf(); + $searchCriteria = $this->createMock(SearchCriteria::class); + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($searchCriteria); + $searchResultMockClass = $this->createPartialMock( + ProductAttributeSearchResults::class, + ['getItems'] + ); + $this->productAttributeRepositoryMock->expects($this->once()) + ->method('getList') + ->with($searchCriteria) + ->willReturn($searchResultMockClass); + $optionAttribute = $this->createPartialMock( + EavAttribute::class, + ['getAttributeCode'] + ); + $searchResultMockClass->expects($this->once()) + ->method('getItems') + ->willReturn([$optionAttribute]); + $type->expects($this->once()) + ->method('getSetAttributes') + ->with($object); + $object->expects($this->once()) + ->method('getTypeId') + ->will($this->returnValue(Configurable::TYPE_CODE)); + $object->expects($this->once()) + ->method('getTypeInstance') + ->will($this->returnValue($type)); + $object->expects($this->once()) + ->method('setData'); + $option->expects($this->once()) + ->method('getAttributeId'); + $optionAttribute->expects($this->once()) + ->method('getAttributeCode'); $this->model->beforeSave( $subject, @@ -73,14 +180,23 @@ public function testBeforeSaveConfigurable() ); } - public function testBeforeSaveSimple() + public function testBeforeSaveSimple(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $object */ - $object = $this->createPartialMock(\Magento\Catalog\Model\Product::class, ['getTypeId', 'getTypeInstance']); - $object->expects($this->once())->method('getTypeId')->willReturn(Type::TYPE_SIMPLE); - $object->expects($this->never())->method('getTypeInstance'); + /** @var ResourceModelProduct|MockObject$subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $object */ + $object = $this->createPartialMock( + ModelProduct::class, + [ + 'getTypeId', + 'getTypeInstance' + ] + ); + $object->expects($this->once()) + ->method('getTypeId') + ->will($this->returnValue(Type::TYPE_SIMPLE)); + $object->expects($this->never()) + ->method('getTypeInstance'); $this->model->beforeSave( $subject, @@ -88,29 +204,35 @@ public function testBeforeSaveSimple() ); } - public function testAroundDelete() + public function testAroundDelete(): void { $productId = '1'; $parentConfigId = ['2']; - /** @var \Magento\Catalog\Model\ResourceModel\Product|MockObject $subject */ - $subject = $this->createMock(\Magento\Catalog\Model\ResourceModel\Product::class); - /** @var \Magento\Catalog\Model\Product|MockObject $product */ + /** @var ResourceModelProduct|MockObject $subject */ + $subject = $this->createMock(ResourceModelProduct::class); + /** @var ModelProduct|MockObject $product */ $product = $this->createPartialMock( - \Magento\Catalog\Model\Product::class, + ModelProduct::class, ['getId', 'delete'] ); - $product->expects($this->once())->method('getId')->willReturn($productId); - $product->expects($this->once())->method('delete')->willReturn(true); + $product->expects($this->once()) + ->method('getId') + ->willReturn($productId); + $product->expects($this->once()) + ->method('delete') + ->willReturn(true); $this->configurableMock->expects($this->once()) ->method('getParentIdsByChild') ->with($productId) ->willReturn($parentConfigId); - $this->actionMock->expects($this->once())->method('executeList')->with($parentConfigId); + $this->actionMock->expects($this->once()) + ->method('executeList') + ->with($parentConfigId); $return = $this->model->aroundDelete( $subject, - /** @var \Magento\Catalog\Model\Product|MockObject $prod */ - function (\Magento\Catalog\Model\Product $prod) use ($subject) { + /** @var ModelProduct|MockObject $prod */ + function (ModelProduct $prod) use ($subject) { $prod->delete(); return $subject; }, diff --git a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php index 0a014b9aeef99..bb79c13bba82a 100644 --- a/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php +++ b/app/code/Magento/ConfigurableProduct/Test/Unit/Plugin/Product/Initialization/CleanConfigurationTmpImagesTest.php @@ -64,7 +64,7 @@ class CleanConfigurationTmpImagesTest extends TestCase /** * @var Json|MockObject */ - private $seralizer; + private $serializer; /** * @var ProductInitializationHelper|MockObject @@ -87,7 +87,7 @@ protected function setUp(): void $this->writeFolder = $this->getMockBuilder(Write::class) ->disableOriginalConstructor() ->getMock(); - $this->seralizer = $this->getMockBuilder(Json::class) + $this->serializer = $this->getMockBuilder(Json::class) ->disableOriginalConstructor() ->getMock(); $this->subjectMock = $this->getMockBuilder(ProductInitializationHelper::class) @@ -106,7 +106,7 @@ protected function setUp(): void 'fileStorageDb' => $this->fileStorageDb, 'mediaConfig' => $this->mediaConfig, 'filesystem' => $this->filesystem, - 'seralizer' => $this->seralizer + 'serializer' => $this->serializer ] ); } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml index a084abfc31eaa..ffd17a8bf4734 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/layout/catalog_product_wizard.xml @@ -48,6 +48,7 @@ <item name="modal" xsi:type="string">configurableModal</item> <item name="dataScope" xsi:type="string">productFormConfigurable</item> </argument> + <argument name="permissions" xsi:type="object">Magento\ConfigurableProduct\Block\DataProviders\PermissionsData</argument> </arguments> </block> <block class="Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Bulk" name="step3" template="Magento_ConfigurableProduct::catalog/product/edit/attribute/steps/bulk.phtml"> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml index 9307da21e6659..4ad7a6419ca63 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/new/created.phtml @@ -4,11 +4,13 @@ * See COPYING.txt for license details. */ /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Attribute\NewAttribute\Product\Created */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $attributes = /* @noEscape */ $block->getAttributesBlockJson(); +$scriptString = <<<script (function ($) { - var data = <?= /* @noEscape */ $block->getAttributesBlockJson() ?>; + var data = {$attributes}; var set = data.set || {id: $('#attribute_set_id').val()}; if (data.tab == 'variations') { $('[data-role=product-variations-matrix]').trigger('add', data.attribute); @@ -18,4 +20,6 @@ $('#create_new_attribute').modal('closeModal'); })(window.parent.jQuery); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml index 5f49d5eb47442..272234a0ee074 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/attribute/set/js.phtml @@ -5,8 +5,11 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require([ "Magento_Ui/js/modal/alert", "tree-panel" @@ -27,7 +30,10 @@ editSet.submit = editSet.submit.wrap(function(original) { if (editSet.currentNode){ if (ConfigurableNodeExists(editSet.currentNode)) { alert({ - content: '<?= $block->escapeJs(__('This group contains attributes used in configurable products. Please move these attributes to another group and try again.')) ?>' + content: '{$block->escapeJs( + __('This group contains attributes used in configurable products. ' . + 'Please move these attributes to another group and try again.') + )}' }); return; } @@ -38,7 +44,9 @@ editSet.submit = editSet.submit.wrap(function(original) { editSet.rightBeforeAppend = editSet.rightBeforeAppend.wrap(function(original, tree, nodeThis, node, newParent) { if (node.attributes.is_configurable == 1) { alert({ - content: '<?= $block->escapeJs(__('This attribute is used in configurable products. You cannot remove it from the attribute set.')) ?>' + content: '{$block->escapeJs( + __('This attribute is used in configurable products. You cannot remove it from the attribute set.') + )}' }); return false; } @@ -48,7 +56,9 @@ editSet.rightBeforeAppend = editSet.rightBeforeAppend.wrap(function(original, tr editSet.rightBeforeInsert = editSet.rightBeforeInsert.wrap(function(original, tree, nodeThis, node, newParent) { if (node.attributes.is_configurable == 1) { alert({ - content: '<?= $block->escapeJs(__('This attribute is used in configurable products. You cannot remove it from the attribute set.')) ?>' + content: '{$block->escapeJs( + __('This attribute is used in configurable products. You cannot remove it from the attribute set.') + )}' }); return false; } @@ -56,4 +66,6 @@ editSet.rightBeforeInsert = editSet.rightBeforeInsert.wrap(function(original, tr }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml index 844422b2a2d7a..a46d50176369a 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/composite/fieldset/configurable.phtml @@ -3,26 +3,30 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -?> -<?php /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable */ ?> +/* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Composite\Fieldset\Configurable */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> <?php $_product = $block->getProduct(); ?> <?php $_attributes = $block->decorateArray($block->getAllowAttributes()); ?> -<?php $_skipSaleableCheck = $this->helper(Magento\Catalog\Helper\Product::class)->getSkipSaleableCheck(); ?> -<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)) :?> +<?php +/** @var \Magento\Catalog\Helper\Product $productHelper */ +$productHelper = $block->getData('productHelper'); +?> +<?php $_skipSaleableCheck = $productHelper->getSkipSaleableCheck(); ?> +<?php if (($_product->isSaleable() || $_skipSaleableCheck) && count($_attributes)):?> <fieldset id="catalog_product_composite_configure_fields_configurable" class="fieldset admin__fieldset"> <legend class="legend admin__legend"> <span><?= $block->escapeHtml(__('Associated Products')) ?></span> </legend> <div class="product-options fieldset admin__fieldset"> - <?php foreach ($_attributes as $_attribute) : ?> + <?php foreach ($_attributes as $_attribute): ?> <div class="field admin__field required"> <label class="label admin__field-label"><?= $block->escapeHtml($_attribute->getProductAttribute()->getStoreLabel($_product->getStoreId())); ?></label> <div class="control admin__field-control <?php - if ($_attribute->getDecoratedIsLast()) : + if ($_attribute->getDecoratedIsLast()): ?> last<?php endif; ?>"> <select name="super_attribute[<?= $block->escapeHtmlAttr($_attribute->getAttributeId()) ?>]" @@ -35,13 +39,14 @@ <?php endforeach; ?> </div> </fieldset> -<script> + <?php $config = /* @noEscape */ $block->getJsonConfig(); + $scriptString = <<<script require([ "Magento_ConfigurableProduct/js/configurable", "Magento_Catalog/catalog/product/composite/configure" ], function(){ - var config = <?= /* @noEscape */ $block->getJsonConfig() ?>; + var config = {$config}; if (window.productConfigure) { config.containerId = window.productConfigure.blockFormFields.id; if (window.productConfigure.restorePhase) { @@ -52,5 +57,7 @@ require([ ProductConfigure.spConfig = new Product.Config(config); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml index e996df8260719..e94d94e0ded55 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/attributes_values.phtml @@ -5,6 +5,9 @@ */ /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ +$isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes(); +$attributesUrl = $block->getUrl('catalog/product_attribute/getAttributes'); +$optionsUrl = $block->getUrl('catalog/product_attribute/createOptions'); ?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'"> <h2 class="steps-wizard-title"><?= $block->escapeHtml( @@ -12,7 +15,8 @@ ); ?></h2> <div class="steps-wizard-info"> <span><?= $block->escapeHtml( - __('Select values from each attribute to include in this product. Each unique combination of values creates a unique product SKU.') + __('Select values from each attribute to include in this product. ' . + 'Each unique combination of values creates a unique product SKU.') );?></span> </div> <div data-bind="foreach: attributes, sortableList: attributes"> @@ -72,7 +76,8 @@ <label data-bind="text: label, visible: label, attr:{for:id}" class="admin__field-label"></label> </div> - <div class="admin__field admin__field-create-new" data-bind="attr:{'data-role':id}, visible: !label"> + <div class="admin__field admin__field-create-new" + data-bind="attr:{'data-role':id}, visible: !label"> <div class="admin__field-control"> <input class="admin__control-text" name="label" @@ -101,14 +106,14 @@ </li> </ul> </fieldset> - <button class="action-create-new action-tertiary" - type="button" - data-action="addOption" - data-bind="click: $parent.createOption, visible: canCreateOption"> - <span><?= $block->escapeHtml( - __('Create New Value') - ); ?></span> - </button> + <?php if ($isAllowedToManageAttributes): ?> + <button class="action-create-new action-tertiary" + type="button" + data-action="addOption" + data-bind="click: $parent.createOption, visible: canCreateOption"> + <span><?= $block->escapeHtml(__('Create New Value')); ?></span> + </button> + <?php endif; ?> </div> </div> </div> @@ -120,8 +125,8 @@ "<?= /* @noEscape */ $block->getComponentName() ?>": { "component": "Magento_ConfigurableProduct/js/variations/steps/attributes_values", "appendTo": "<?= /* @noEscape */ $block->getParentComponentName() ?>", - "optionsUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product_attribute/getAttributes') ?>", - "createOptionsUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product_attribute/createOptions') ?>" + "optionsUrl": "<?= /* @noEscape */ $attributesUrl ?>", + "createOptionsUrl": "<?= /* @noEscape */ $optionsUrl ?>" } } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml index a792a35da8051..6cd930978c85f 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/bulk.phtml @@ -3,20 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\Bulk */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +?> <div data-bind="scope: '<?= /* @noEscape */ $block->getComponentName() ?>'" data-role="bulk-step"> <h2 class="steps-wizard-title"><?= $block->escapeHtml(__('Step 3: Bulk Images, Price and Quantity')) ?></h2> <div class="steps-wizard-info"> - <?= /* @noEscape */ __('Based on your selections %1 new products will be created. Use this step to customize images and price for your new products.', '<span class="new-products-count" data-bind="text:countVariations"></span>') ?> + <?= /* @noEscape */ __( + 'Based on your selections %1 new products will be created. ' . + 'Use this step to customize images and price for your new products.', + '<span class="new-products-count" data-bind="text:countVariations"></span>' + ) ?> </div> <div data-bind="with: sections().images" class="steps-wizard-section"> <div data-role="section"> <div class="steps-wizard-section-title"> - <span><?= $block->escapeHtml(__('Images')); ?></span> + <span><?= $block->escapeHtml(__('Images')); ?></span> </div> <ul class="steps-wizard-section-list"> @@ -65,7 +74,9 @@ <div data-role="gallery" class="gallery" data-images="[]" - data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>"> + data-types="<?= $block->escapeHtmlAttr($jsonHelper->jsonEncode( + $block->getImageTypes() + )) ?>"> <div class="image image-placeholder"> <div data-role="uploader" class="uploader"> <div class="image-browse"> @@ -75,32 +86,39 @@ name="image" class="admin__control-file" multiple="multiple" - data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> + data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') + ?>" /> </div> </div> <div class="product-image-wrapper"> - <p class="image-placeholder-text"><?= $block->escapeHtml(__('Browse to find or drag image here')) ?></p> + <p class="image-placeholder-text"><?= $block->escapeHtml(__( + 'Browse to find or drag image here' + )) ?></p> </div> </div> - <?php foreach ($block->getImageTypes() as $typeData) : ?> + <?php foreach ($block->getImageTypes() as $typeData): ?> <input name="<?= $block->escapeHtml($typeData['name']) ?>" class="image-<?= $block->escapeHtml($typeData['code']) ?>" type="hidden" value="<?= $block->escapeHtml($typeData['value']) ?>"/> - <?php endforeach; ?> + <?php endforeach; ?> <script data-template="uploader" type="text/x-magento-template"> <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress"></div> </div> <div class="spinner"> <span></span><span></span><span></span><span></span> <span></span><span></span><span></span><span></span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> </script> <script data-template="gallery-content" type="text/x-magento-template"> @@ -124,7 +142,8 @@ <input type="hidden" name="product[media_gallery][images][<%- data.file_id %>][removed]"/> <div class="product-image-wrapper"> - <img class="product-image" data-role="image-element" src="<%- data.url %>" alt="<%- data.label %>"/> + <img class="product-image" data-role="image-element" src="<%- data.url %>" + alt="<%- data.label %>"/> <div class="actions"> <button type="button" class="action-remove" @@ -139,16 +158,18 @@ <div class="item-description"> <div class="item-title" data-role="img-title"><%- data.label %></div> <div class="item-size"> - <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> + <span data-role="image-dimens"></span>, + <span data-role="image-size"><%- data.sizeLabel %></span> </div> </div> <ul class="item-roles" data-role="roles-labels"> - <?php foreach ($block->getMediaAttributes() as $attribute) :?> + <?php foreach ($block->getMediaAttributes() as $attribute):?> <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" - class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> + class="item-role item-role-<?= + $block->escapeHtml($attribute->getAttributeCode()) ?>"> <?= /* @noEscape */ $attribute->getFrontendLabel() ?> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </script> @@ -210,40 +231,44 @@ <div class="admin__field field-image-role"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Role')); ?></span> + <span><?= $block->escapeHtml(__('Role')); ?></span> </label> <div class="admin__field-control"> <ul class="multiselect-alt"> <?php - foreach ($block->getMediaAttributes() as $attribute) : + foreach ($block->getMediaAttributes() as $attribute): ?> <li class="item"> <label> <input class="image-type" data-role="type-selector" type="checkbox" - value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" + value="<?= + $block->escapeHtml($attribute->getAttributeCode()) ?>" /> <?= $block->escapeHtml($attribute->getFrontendLabel()); ?> </label> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </div> <div class="admin__field admin__field-inline field-image-size" data-role="size"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Size')); ?></span> + <span><?= $block->escapeHtml(__('Image Size')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}'));?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{size}'));?>"></div> </div> - <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> + <div class="admin__field admin__field-inline field-image-resolution" + data-role="resolution"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> + <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px'));?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{width}^{height} px'));?>"></div> </div> <div class="admin__field field-image-hide"> @@ -273,7 +298,7 @@ <fieldset class="admin__fieldset bulk-attribute-values"> <div class="admin__field _required"> <label class="admin__field-label" for="apply-images-attributes"> - <span><?= $block->escapeHtml(__('Select attribute')); ?></span> + <span><?= $block->escapeHtml(__('Select attribute')); ?></span> </label> <div class="admin__field-control"> <select @@ -300,17 +325,22 @@ <div data-role="gallery" class="gallery" data-images="[]" - data-types="<?= $block->escapeHtml($this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes())) ?>"> + data-types="<?= $block->escapeHtmlAttr( + $jsonHelper->jsonEncode($block->getImageTypes()) + ) ?>"> <div class="image image-placeholder"> <div data-role="uploader" class="uploader"> <div class="image-browse"> - <span><?= $block->escapeHtml(__('Browse Files...')); ?></span> + <span><?= $block->escapeHtml(__('Browse Files...')); ?></span> <input type="file" name="image" multiple="multiple" - data-url="<?= /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> + data-url="<?= + /* @noEscape */ $block->getUrl('catalog/product_gallery/upload') ?>" /> </div> </div> <div class="product-image-wrapper"> - <p class="image-placeholder-text"><?= $block->escapeHtml(__('Browse to find or drag image here')); ?></p> + <p class="image-placeholder-text"> + <?= $block->escapeHtml(__('Browse to find or drag image here')); ?> + </p> </div> <div class="spinner"> <span></span><span></span><span></span><span></span> @@ -318,24 +348,28 @@ </div> </div> - <?php foreach ($block->getImageTypes() as $typeData) :?> + <?php foreach ($block->getImageTypes() as $typeData): ?> <input name="<?= $block->escapeHtml($typeData['name']) ?>" class="image-<?= $block->escapeHtml($typeData['code']) ?>" type="hidden" value="<?= $block->escapeHtml($typeData['value']) ?>"/> - <?php endforeach; ?> + <?php endforeach; ?> <script data-template="uploader" type="text/x-magento-template"> <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress"></div> </div> <div class="spinner"> <span></span><span></span><span></span><span></span> <span></span><span></span><span></span><span></span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> </script> <script data-template="gallery-content" type="text/x-magento-template"> @@ -361,31 +395,36 @@ value="" class="is-removed"/> <div class="product-image-wrapper"> - <img class="product-image" data-role="image-element" src="<%- data.url %>" alt="<%- data.label %>"/> + <img class="product-image" data-role="image-element" src="<%- data.url %>" + alt="<%- data.label %>"/> <div class="actions"> <button type="button" class="action-remove" data-role="delete-button" title="<?= $block->escapeHtml(__('Remove image')) ?>"> - <span><?= $block->escapeHtml(__('Remove image')); ?></span> + <span><?= $block->escapeHtml(__('Remove image')); ?></span> </button> <div class="draggable-handle"></div> </div> - <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')); ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')); ?></span> + </div> </div> <div class="item-description"> <div class="item-title" data-role="img-title"><%- data.label %></div> <div class="item-size"> - <span data-role="image-dimens"></span>, <span data-role="image-size"><%- data.sizeLabel %></span> + <span data-role="image-dimens"></span>, + <span data-role="image-size"><%- data.sizeLabel %></span> </div> </div> <ul class="item-roles" data-role="roles-labels"> - <?php foreach ($block->getMediaAttributes() as $attribute) :?> - <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" - class="item-role item-role-<?= $block->escapeHtml($attribute->getAttributeCode()) ?>"> + <?php foreach ($block->getMediaAttributes() as $attribute):?> + <li data-role-code="<?= $block->escapeHtml($attribute->getAttributeCode()) + ?>" + class="item-role item-role-<?= + $block->escapeHtml($attribute->getAttributeCode()) ?>"> <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </script> @@ -407,7 +446,8 @@ </button> <div class="draggable-handle"></div> </div> - <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')) ?></span></div> + <div class="image-fade"><span><?= $block->escapeHtml(__('Hidden')) ?></span> + </div> </div> <!--<ul class="item-roles"> <li class="item-role item-role-base">Base</li> @@ -416,7 +456,8 @@ </script> <script data-role="img-dialog-container-tmpl" type="text/x-magento-template"> - <div class="image-panel ui-tabs-panel ui-widget-content ui-corner-bottom" data-role="dialog"> + <div class="image-panel ui-tabs-panel ui-widget-content ui-corner-bottom" + data-role="dialog"> </div> </script> @@ -430,7 +471,7 @@ <fieldset class="admin__fieldset fieldset-image-panel"> <div class="admin__field field-image-description"> <label class="admin__field-label" for="image-description"> - <span><?= $block->escapeHtml(__('Alt Text'));?></span> + <span><?= $block->escapeHtml(__('Alt Text'));?></span> </label> <div class="admin__field-control"> @@ -444,38 +485,43 @@ <div class="admin__field field-image-role"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Role'));?></span> + <span><?= $block->escapeHtml(__('Role'));?></span> </label> <div class="admin__field-control"> <ul class="multiselect-alt"> - <?php foreach ($block->getMediaAttributes() as $attribute) :?> + <?php foreach ($block->getMediaAttributes() as $attribute):?> <li class="item"> <label> <input class="image-type" data-role="type-selector" type="checkbox" - value="<?= $block->escapeHtml($attribute->getAttributeCode()) ?>" + // @codingStandardsIgnoreLine + value="<?= $block->escapeHtmlAttr($attribute->getAttributeCode()) ?>" /> - <?= $block->escapeHtml($attribute->getFrontendLabel()) ?> + <?= $block->escapeHtml($attribute->getFrontendLabel())?> </label> </li> - <?php endforeach; ?> + <?php endforeach; ?> </ul> </div> </div> <div class="admin__field admin__field-inline field-image-size" data-role="size"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Size')); ?></span> + <span><?= $block->escapeHtml(__('Image Size')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{size}')); ?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{size}')); ?>"></div> </div> - <div class="admin__field admin__field-inline field-image-resolution" data-role="resolution"> + <div class="admin__field admin__field-inline field-image-resolution" + data-role="resolution"> <label class="admin__field-label"> - <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> + <span><?= $block->escapeHtml(__('Image Resolution')); ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtml(__('{width}^{height} px')); ?>"></div> + <div class="admin__field-value" + data-message="<?= $block->escapeHtml(__('{width}^{height} px')); ?>"> + </div> </div> <div class="admin__field field-image-hide"> @@ -486,7 +532,8 @@ data-role="visibility-trigger" value="1" class="admin__control-checkbox" - name="product[media_gallery][images][<%- data.file_id %>][disabled]" + // @codingStandardsIgnoreLine + name="product[media_gallery][images][<%- data.file_id %>][disabled]" <% if (data.disabled == 1) { %>checked="checked"<% } %> /> <label for="hide-from-product-page" class="admin__field-label"> @@ -556,7 +603,7 @@ <fieldset class="admin__fieldset bulk-attribute-values" data-bind="visible: type() == 'single'"> <div class="admin__field _required"> <label for="apply-single-price-input" class="admin__field-label"> - <span><?= $block->escapeHtml(__('Price')); ?></span> + <span><?= $block->escapeHtml(__('Price')); ?></span> </label> <div class="admin__field-control"> <div class="currency-addon"> @@ -589,7 +636,8 @@ <fieldset class="admin__fieldset bulk-attribute-values" data-bind="if:attribute"> <!-- ko foreach: attribute().chosen --> <div class="admin__field _required"> - <label data-bind="attr: {for: 'apply-single-price-input-' + $index()}" class="admin__field-label"> + <label data-bind="attr: {for: 'apply-single-price-input-' + $index()}" + class="admin__field-label"> <span data-bind="text:label"></span> </label> <div class="admin__field-control"> @@ -717,7 +765,7 @@ "component": "Magento_ConfigurableProduct/js/variations/steps/bulk", "appendTo": "<?= /* @noEscape */ $block->getParentComponentName() ?>", "noImage": "<?= /* @noEscape */ $block->getNoImageUrl() ?>", - "variationsComponent": "<?= /* @noEscape */ $block->getData('config/form') ?>.configurableVariations" + "variationsComponent": "<?= /* @noEscape */ $block->getData('config/form')?>.configurableVariations" } } } diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml index c3dc614232201..92fae99f6ec24 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/attribute/steps/select_attributes.phtml @@ -5,8 +5,10 @@ */ /* @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\SelectAttributes */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div class="select-attributes-block <?= /* @noEscape */ $block->getData('config/dataScope') ?>" data-role="select-attributes-step"> +<div class="select-attributes-block <?= /* @noEscape */ $block->getData('config/dataScope') ?>" + data-role="select-attributes-step"> <div class="select-attributes-actions" data-type="skipKO"> <?= /* @noEscape */ $block->getAddNewAttributeButton() ?> </div> @@ -37,9 +39,12 @@ } } </script> -<script> +<?php $dataScope = /* @noEscape */ $block->getData('config/dataScope'); +$scriptString = <<<script require(['jquery'], function ($) { - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=select-attributes-step]').applyBindings(); + $('.{$dataScope}[data-role=select-attributes-step]').applyBindings(); $('body').trigger('contentUpdated'); }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml index 22ff1992c94a7..73067fdee3b84 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/matrix.phtml @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $productMatrix = $block->getProductMatrix(); $attributes = $block->getProductAttributes(); $currencySymbol = $block->getCurrencySymbol(); + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div id="product-variations-matrix" data-role="product-variations-matrix"> @@ -26,15 +30,25 @@ $currencySymbol = $block->getCurrencySymbol(); <div data-role="configurable-attributes-container"> <!-- ko foreach: {data: attributes, as: 'attribute'} --> <div data-role="attribute-info"> - <input name="attributes[]" data-bind="value: attribute.id, attr:{id: 'configurable_attribute_' + attribute.id}" type="hidden"/> - <input data-bind="value: attribute.id, attr: {name: $parent.getAttributeRowName(attribute, 'attribute_id')}" type="hidden"/> - <input data-bind="value: attribute.code, attr: {name: $parent.getAttributeRowName(attribute, 'code')}" type="hidden"/> - <input data-bind="value: attribute.label, attr: {name: $parent.getAttributeRowName(attribute, 'label')}" type="hidden"/> - <input data-bind="value: $index(), attr: {name: $parent.getAttributeRowName(attribute, 'position')}" type="hidden"/> + <input name="attributes[]" + data-bind="value: attribute.id, attr:{id: 'configurable_attribute_' + attribute.id}" + type="hidden"/> + <input data-bind="value: attribute.id, + attr: {name: $parent.getAttributeRowName(attribute, 'attribute_id')}" type="hidden"/> + <input data-bind="value: attribute.code, + attr: {name: $parent.getAttributeRowName(attribute, 'code')}" type="hidden"/> + <input data-bind="value: attribute.label, + attr: {name: $parent.getAttributeRowName(attribute, 'label')}" type="hidden"/> + <input data-bind="value: $index(), + attr: {name: $parent.getAttributeRowName(attribute, 'position')}" type="hidden"/> <!-- ko foreach: {data: attribute.chosen, as: 'option'} --> <div data-role="option-info"> - <input value="1" data-bind="attr: {name: $parents[1].getOptionRowName(attribute, option, 'include')}" type="hidden"/> - <input data-bind="value: option.value, attr: {name: $parents[1].getOptionRowName(attribute, option, 'value_index')}" type="hidden"/> + <input value="1" + data-bind="attr: {name: $parents[1].getOptionRowName(attribute, option, + 'include')}" type="hidden"/> + <input data-bind="value: option.value, + attr: {name: $parents[1].getOptionRowName(attribute, option, 'value_index')}" + type="hidden"/> </div> <!-- /ko --> </div> @@ -104,7 +118,9 @@ $currencySymbol = $block->getCurrencySymbol(); </button> <ul class="dropdown"> <li> - <a class="item" data-action="no-image"><?= $block->escapeHtml(__('No Image')) ?></a> + <a class="item" data-action="no-image"> + <?= $block->escapeHtml(__('No Image')) ?> + </a> </li> </ul> </div> @@ -167,7 +183,8 @@ $currencySymbol = $block->getCurrencySymbol(); <input type="text" class="validate-zero-or-greater" data-bind="attr: {id: $parent.getRowId(variation, 'qty'), - name: $parent.getVariationRowName(variation, 'quantity_and_stock_status/qty'), + name: $parent.getVariationRowName(variation, + 'quantity_and_stock_status/qty'), value: variation.quantity}"/> <!-- /ko --> </td> @@ -187,17 +204,20 @@ $currencySymbol = $block->getCurrencySymbol(); <td data-bind="text: label"></td> <!-- /ko --> <td class="data-grid-actions-cell"> - <input type="hidden" name="associated_product_ids[]" data-bind="value: variation.productId" data-column="entity_id"/> + <input type="hidden" name="associated_product_ids[]" + data-bind="value: variation.productId" data-column="entity_id"/> <div class="action-select-wrap" data-bind=" css : { '_active' : $parent.opened() === $index() }, outerClick: $parent.closeList.bind($parent, $index)" > - <button class="action-select" data-bind="click: $parent.toggleList.bind($parent, $index())"> + <button class="action-select" + data-bind="click: $parent.toggleList.bind($parent, $index())"> <span data-bind="i18n: 'Select'"></span> </button> - <ul class="action-menu _active" data-bind="css: {'_active': $parent.opened() === $index()}"> + <ul class="action-menu _active" + data-bind="css: {'_active': $parent.opened() === $index()}"> <li> <a class="action-menu-item" data-bind=" @@ -211,12 +231,14 @@ $currencySymbol = $block->getCurrencySymbol(); </li> <li> <a class="action-menu-item" data-bind=" - text: variation.status == 1 ? $t('Disable Product') : $t('Enable Product'), + text: variation.status == 1 ? $t('Disable Product') : + $t('Enable Product'), click: $parent.toggleProduct.bind($parent, $index())"> </a> </li> <li> - <a class="action-menu-item" data-bind="click: $parent.removeProduct.bind($parent, $index())"> + <a class="action-menu-item" + data-bind="click: $parent.removeProduct.bind($parent, $index())"> <?= $block->escapeHtml(__('Remove Product')) ?> </a> </li> @@ -231,7 +253,8 @@ $currencySymbol = $block->getCurrencySymbol(); <!-- /ko --> </div> <div data-role="step-wizard-dialog" - data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= $block->escapeJs(__('Create Product Configurations')) ?>", + data-mage-init='{"Magento_Ui/js/modal/modal":{"type":"slide","title":"<?= + $block->escapeJs(__('Create Product Configurations')) ?>", "buttons":[]}}' class="no-display"> <?= /* @noEscape */ $block->getVariationWizard([ @@ -249,8 +272,8 @@ $currencySymbol = $block->getCurrencySymbol(); "components": { "configurableVariations": { "component": "Magento_ConfigurableProduct/js/variations/variations", - "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, - "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, + "variations": <?= /* @noEscape */ $jsonHelper->jsonEncode($productMatrix) ?>, + "productAttributes":<?=/* @noEscape */ $jsonHelper->jsonEncode($attributes)?>, "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", "configurableProductGrid": "configurableProductGrid" @@ -260,9 +283,11 @@ $currencySymbol = $block->getCurrencySymbol(); } } </script> -<script> +<?php $scriptString = <<<script require(['jquery'], function ($) { $('body').trigger('contentUpdated'); $('[data-panel=product-variations]').applyBindings(); }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml index 7b85efdbb73aa..9d0e78d6ad14c 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard-ajax.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $productMatrix = $block->getProductMatrix(); $attributes = $block->getProductAttributes(); @@ -15,13 +16,18 @@ $attributes = $block->getProductAttributes(); 'configurableModal' => $block->getForm() . '.' . $block->getModal() ]); ?> -<script> + +<?php $dataScope = /* @noEscape */ $block->getData('config/dataScope'); +$nameStep = /* @noEscape */ $block->getData('config/nameStepWizard'); +$scriptString = <<<script require(['jquery', 'uiRegistry', 'underscore'], function ($, registry, _) { $('body').trigger('contentUpdated'); - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=steps-wizard-main]').applyBindings(); + $('.{$dataScope}[data-role=steps-wizard-main]').applyBindings(); - registry.async('<?= /* @noEscape */ $block->getData('config/nameStepWizard') ?>')(function (component) { + registry.async('{$nameStep}')(function (component) { _.delay(component.open.bind(component), 500); // TODO: MAGETWO-50246 }) }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml index f009962bb97ff..2cd5a32ce5449 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/catalog/product/edit/super/wizard.phtml @@ -3,18 +3,24 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** @var $block \Magento\ConfigurableProduct\Block\Adminhtml\Product\Edit\Tab\Variations\Config\Matrix */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); $productMatrix = $block->getProductMatrix(); $attributes = $block->getProductAttributes(); $currencySymbol = $block->getCurrencySymbol(); ?> -<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" data-role="step-wizard-dialog" data-bind="scope: '<?= /* @noEscape */ $block->getForm() ?>.<?= /* @noEscape */ $block->getModal() ?>'"> +<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" + data-role="step-wizard-dialog" + data-bind="scope: '<?= /* @noEscape */ $block->getForm() ?>.<?= /* @noEscape */ $block->getModal() ?>'"> <!-- ko template: getTemplate() --><!-- /ko --> </div> -<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" id="product-variations-matrix" data-role="product-variations-matrix"> +<div class="<?= /* @noEscape */ $block->getData('config/dataScope') ?>" + id="product-variations-matrix" data-role="product-variations-matrix"> <div data-bind="scope: 'configurableVariations'"></div> </div> <script type="text/x-magento-init"> @@ -24,13 +30,17 @@ $currencySymbol = $block->getCurrencySymbol(); "components": { "<?= /* @noEscape */ $block->getData('config/form') ?>.<?= /* @noEscape */ $block->getModal() ?>": { "component": "Magento_ConfigurableProduct/js/components/modal-configurable", - "options": {"type": "slide", "title": "<?= $block->escapeHtml(__('Create Product Configurations')) ?>"}, + "options": {"type": "slide", + "title": "<?= $block->escapeHtml(__('Create Product Configurations')) ?>"}, "formName": "<?= /* @noEscape */ $block->getForm() ?>", "isTemplate": false, "stepWizard": "<?= /* @noEscape */ $block->getData('config/nameStepWizard') ?>", "children": { "wizard": { - "url": "<?= /* @noEscape */ $block->getUrl($block->getData('config/urlWizard'), ['id' => $block->getProduct()->getId()]) ?>", + "url": "<?= /* @noEscape */ $block->getUrl( + $block->getData('config/urlWizard'), + ['id' => $block->getProduct()->getId()] + ) ?>", "component": "Magento_Ui/js/form/components/html" } } @@ -43,12 +53,14 @@ $currencySymbol = $block->getCurrencySymbol(); "dataScopeAttributeCodes": "data.attribute_codes", "dataScopeAttributesData": "data.product.configurable_attributes_data", "formName": "<?= /* @noEscape */ $block->getForm() ?>", - "attributeSetHandler": "<?= /* @noEscape */ $block->getForm() ?>.configurable_attribute_set_handler_modal", - "wizardModalButtonName": "<?= /* @noEscape */ $block->getForm() ?>.configurable.configurable_products_button_set.create_configurable_products_button", + "attributeSetHandler": "<?= /* @noEscape */ $block->getForm() + ?>.configurable_attribute_set_handler_modal", + "wizardModalButtonName": "<?= /* @noEscape */ $block->getForm() + ?>.configurable.configurable_products_button_set.create_configurable_products_button", "wizardModalButtonTitle": "<?= $block->escapeHtml(__('Edit Configurations')) ?>", - "productAttributes": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($attributes) ?>, + "productAttributes":<?=/* @noEscape */ $jsonHelper->jsonEncode($attributes)?>, "productUrl": "<?= /* @noEscape */ $block->getUrl('catalog/product/edit', ['id' => '%id%']) ?>", - "variations": <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($productMatrix) ?>, + "variations": <?= /* @noEscape */ $jsonHelper->jsonEncode($productMatrix) ?>, "currencySymbol": "<?= /* @noEscape */ $currencySymbol ?>", "attributeSetCreationUrl": "<?= /* @noEscape */ $block->getUrl('*/product_set/save') ?>" } @@ -57,10 +69,13 @@ $currencySymbol = $block->getCurrencySymbol(); } } </script> -<script> +<?php $dataScope = /* @noEscape */ $block->getData('config/dataScope'); +$scriptString = <<<script require(['jquery', 'mage/apply/main'], function ($, main) { main.apply(); - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=step-wizard-dialog]').applyBindings(); - $('.<?= /* @noEscape */ $block->getData('config/dataScope') ?>[data-role=product-variations-matrix]').applyBindings(); + $('.{$dataScope}[data-role=step-wizard-dialog]').applyBindings(); + $('.{$dataScope}[data-role=product-variations-matrix]').applyBindings(); }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml index cdb12b54e5e67..d5c946621e90d 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/affected-attribute-set-selector/js.phtml @@ -5,22 +5,24 @@ */ /* @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script (function(){ 'use strict'; - - var $form; + + var _form; require([ 'jquery', 'jquery/ui', 'Magento_Ui/js/modal/modal' ], function($){ - $form = $('[data-role=affected-attribute-set-selector]'); + _form = $('[data-role=affected-attribute-set-selector]'); var resetValidation = function() { - $form.find('.messages .message.error').hide(); - $form.find('form').validation().data('validator').resetForm(); + _form.find('.messages .message.error').hide(); + _form.find('form').validation().data('validator').resetForm(); }, setAttributeSetId = function (id) { $('[data-role=new-variations-attribute-set-id]').val(id); @@ -31,10 +33,10 @@ newAttributeSetContainer = $('[data-role=affected-attribute-set-new-name-container]'), existingAttributeSetContainer = $('[data-role=affected-attribute-set-existing-name-container]'); - $form.find('input[type=text]').on('keypress',function(e){ + _form.find('input[type=text]').on('keypress',function(e){ if(e.keyCode === 13){ e.preventDefault(); - $form.closest('[data-role=modal]').find('button[data-action=confirm]').click(); + _form.closest('[data-role=modal]').find('button[data-action=confirm]').click(); } }); @@ -44,66 +46,67 @@ 'data-role': 'new-variations-attribute-set-id' })); - $form + _form .modal({ - title: '<?= $block->escapeJs(__('Choose Affected Attribute Set')) ?>', + title: '{$block->escapeJs(__('Choose Affected Attribute Set'))}', closed: function () { resetValidation(); }, buttons: [{ - text: '<?= $block->escapeJs(__('Confirm')) ?>', + text: '{$block->escapeJs(__('Confirm'))}', attr: { 'data-action': 'confirm' }, 'class': 'action-primary', click: function() { - var affectedAttributeSetId = $form.find('input[name=affected-attribute-set]:checked').val(); + var affectedAttributeSetId = _form.find('input[name=affected-attribute-set]:checked').val(); if (affectedAttributeSetId == 'current') { setAttributeSetId($('#attribute_set_id').val()); - closeDialogAndProcessForm($form); + closeDialogAndProcessForm(_form); return; } else if (affectedAttributeSetId == 'existing') { setAttributeSetId($('select', existingAttributeSetContainer).val()); - closeDialogAndProcessForm($form); + closeDialogAndProcessForm(form); } - $form.find('.messages .message.error').hide(); - if (!$form.find('form').validation().valid()) { - $form.find('input[name=new-attribute-set-name]').focus(); + _form.find('.messages .message.error').hide(); + if (!_form.find('form').validation().valid()) { + _form.find('input[name=new-attribute-set-name]').focus(); return false; } $.ajax({ type: 'POST', - url: '<?= $block->escapeUrl($block->getAttributeSetCreationUrl()) ?>', + url: '{$block->escapeUrl($block->getAttributeSetCreationUrl())}', data: { gotoEdit: 1, - attribute_set_name: $form.find('input[name=new-attribute-set-name]').val(), + attribute_set_name: _form.find('input[name=new-attribute-set-name]').val(), skeleton_set: $('#attribute_set_id').val(), - form_key: '<?= $block->escapeJs($block->getFormKey()) ?>', + form_key: '{$block->escapeJs($block->getFormKey())}', return_session_messages_only: 1 }, dataType: 'json', showLoader: true, - context: $form + context: _form }) .done(function (data) { if (!data.error) { setAttributeSetId(data.id); - closeDialogAndProcessForm($form); + closeDialogAndProcessForm(_form); } else { - $form.find('.messages .message.error').replaceWith($(data.messages).find('.message.error')); + _form.find('.messages .message.error').replaceWith($(data.messages) + .find('.message.error')); } }); return false; } },{ - text: '<?= $block->escapeJs(__('Cancel')) ?>', - id: '<?= $block->escapeJs($block->getJsId('close-button')) ?>', + text: '{$block->escapeJs(__('Cancel'))}', + id: '{$block->escapeJs($block->getJsId('close-button'))}', 'class': 'action-close', click: function() { - $form.modal('closeModal'); + _form.modal('closeModal'); } }] }) @@ -117,7 +120,7 @@ } }); }); - + require([ 'jquery' ], function ($) { @@ -127,17 +130,17 @@ * * @return {Array} */ - var getAttributes = function ($node) { + var getAttributes = function (_node) { return $.map( - $node.find('[data-role=configurable-attributes-container] [data-role=attribute-info]') || [], + _node.find('[data-role=configurable-attributes-container] [data-role=attribute-info]') || [], function (attribute) { - var $attribute = $(attribute); + var _attribute = $(attribute); return { - id: $attribute.find('[name$="[attribute_id]"]').val(), - code: $attribute.find('[name$="[code]"]').val(), - label: $attribute.find('[name$="[label]"]').val(), - position: $attribute.find('[name$="[position]"]').val() + id: _attribute.find('[name$="[attribute_id]"]').val(), + code: _attribute.find('[name$="[code]"]').val(), + label: _attribute.find('[name$="[label]"]').val(), + position: _attribute.find('[name$="[position]"]').val() }; } ); @@ -170,10 +173,13 @@ event.stopImmediatePropagation(); - $form.data('target', event.target).modal('openModal'); + _form.data('target', event.target).modal('openModal'); }); }); })(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml index e6cf1e9c6870d..59bfabe736c02 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/attribute-selector/js.phtml @@ -3,12 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + /** @var $block \Magento\ConfigurableProduct\Block\Product\Configurable\AttributeSelector */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$suggestWidgetOptions = /* @noEscape */ $jsonHelper->jsonEncode($block->getSuggestWidgetOptions()); +$scriptString = <<<script require(["jquery","mage/mage","mage/backend/suggest"], function($){ - var options = <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getSuggestWidgetOptions()) ?>; + var options = {$suggestWidgetOptions}; $('#configurable-attribute-selector') .mage('suggest', options) .on('suggestselect', function (event, ui) { @@ -29,4 +35,6 @@ require(["jquery","mage/mage","mage/backend/suggest"], function($){ return false; }) }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml index fe41f07a4434d..c0f3f8617bd16 100644 --- a/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml +++ b/app/code/Magento/ConfigurableProduct/view/adminhtml/templates/product/configurable/stock/disabler.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-tab-panel=product-details]').on('stockbeforedisable', function(e) { var variations = $('[data-panel=product-variations]'); @@ -14,4 +17,6 @@ require(['jquery'], function($){ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php index f28bf97adf930..0cb0eddf8a246 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/ConfigurableVariant.php @@ -97,8 +97,8 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value $this->variantCollection->addEavAttributes($fields); $this->optionCollection->addProductId((int)$value[$linkField]); - $result = function () use ($value, $linkField) { - $children = $this->variantCollection->getChildProductsByParentId((int)$value[$linkField]); + $result = function () use ($value, $linkField, $context) { + $children = $this->variantCollection->getChildProductsByParentId((int)$value[$linkField], $context); $options = $this->optionCollection->getAttributesByProductId((int)$value[$linkField]); $variants = []; /** @var Product $child */ diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php index faf666144422c..3a064f3399255 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes.php @@ -60,7 +60,8 @@ public function resolve( $option['options_map'] ?? [], $code, (int) $optionId, - (int) $model->getData($code) + (int) $model->getData($code), + (int) $option['attribute_id'] ); if (!empty($optionsFromMap)) { $data[] = $optionsFromMap; @@ -77,14 +78,20 @@ public function resolve( * @param string $code * @param int $optionId * @param int $attributeCodeId + * @param int $attributeId * @return array */ - private function getOptionsFromMap(array $optionMap, string $code, int $optionId, int $attributeCodeId): array - { + private function getOptionsFromMap( + array $optionMap, + string $code, + int $optionId, + int $attributeCodeId, + int $attributeId + ): array { $data = []; if (isset($optionMap[$optionId . ':' . $attributeCodeId])) { $optionValue = $optionMap[$optionId . ':' . $attributeCodeId]; - $data = $this->getOptionsArray($optionValue, $code); + $data = $this->getOptionsArray($optionValue, $code, $attributeId); } return $data; } @@ -94,15 +101,17 @@ private function getOptionsFromMap(array $optionMap, string $code, int $optionId * * @param array $optionValue * @param string $code + * @param int $attributeId * @return array */ - private function getOptionsArray(array $optionValue, string $code): array + private function getOptionsArray(array $optionValue, string $code, int $attributeId): array { return [ 'label' => $optionValue['label'] ?? null, 'code' => $code, 'use_default_value' => $optionValue['use_default_value'] ?? null, 'value_index' => $optionValue['value_index'] ?? null, + 'attribute_id' => $attributeId, ]; } } diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php new file mode 100644 index 0000000000000..13f31e7e2ce10 --- /dev/null +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Resolver/Variant/Attributes/ConfigurableAttributeUid.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProductGraphQl\Model\Resolver\Variant\Attributes; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Format new option uid in base64 encode for super attribute options + */ +class ConfigurableAttributeUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'configurable'; + + /** + * Create a option uid for super attribute in "<option-type>/<attribute-id>/<value-index>" format + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return string + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['attribute_id']) || empty($value['attribute_id'])) { + throw new GraphQlInputException(__('"attribute_id" value should be specified.')); + } + + if (!isset($value['value_index']) || empty($value['value_index'])) { + throw new GraphQlInputException(__('"value_index" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['attribute_id'], + $value['value_index'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php index 6c4371b23927e..b60a660251f4d 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php +++ b/app/code/Magento/ConfigurableProductGraphQl/Model/Variant/Collection.php @@ -13,6 +13,7 @@ use Magento\ConfigurableProduct\Model\ResourceModel\Product\Type\Configurable\Product\CollectionFactory; use Magento\Framework\EntityManager\MetadataPool; use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\GraphQl\Model\Query\ContextInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionProcessorInterface; use Magento\CatalogGraphQl\Model\Resolver\Products\DataProvider\Product\CollectionPostProcessor; @@ -118,11 +119,12 @@ public function addEavAttributes(array $attributeCodes) : void * Retrieve child products from for passed in parent id. * * @param int $id + * @param ContextInterface|null $context * @return array */ - public function getChildProductsByParentId(int $id) : array + public function getChildProductsByParentId(int $id, ContextInterface $context = null) : array { - $childrenMap = $this->fetch(); + $childrenMap = $this->fetch($context); if (!isset($childrenMap[$id])) { return []; @@ -134,9 +136,10 @@ public function getChildProductsByParentId(int $id) : array /** * Fetch all children products from parent id's. * + * @param ContextInterface|null $context * @return array */ - private function fetch() : array + private function fetch(ContextInterface $context = null) : array { if (empty($this->parentProducts) || !empty($this->childrenMap)) { return $this->childrenMap; @@ -150,7 +153,8 @@ private function fetch() : array $this->collectionProcessor->process( $childCollection, $this->searchCriteriaBuilder->create(), - $attributeData + $attributeData, + $context ); $childCollection->load(); $this->collectionPostProcessor->process($childCollection, $attributeData); diff --git a/app/code/Magento/ConfigurableProductGraphQl/composer.json b/app/code/Magento/ConfigurableProductGraphQl/composer.json index 76ec4ad3153e2..295efb65b1978 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/composer.json +++ b/app/code/Magento/ConfigurableProductGraphQl/composer.json @@ -6,6 +6,7 @@ "php": "~7.3.0||~7.4.0", "magento/module-catalog": "*", "magento/module-configurable-product": "*", + "magento/module-graph-ql": "*", "magento/module-catalog-graph-ql": "*", "magento/module-quote": "*", "magento/module-quote-graph-ql": "*", diff --git a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls index 2e9576b35e6e8..6e85653380acc 100644 --- a/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls +++ b/app/code/Magento/ConfigurableProductGraphQl/etc/schema.graphqls @@ -18,6 +18,7 @@ type ConfigurableAttributeOption @doc(description: "ConfigurableAttributeOption label: String @doc(description: "A string that describes the configurable attribute option") code: String @doc(description: "The ID assigned to the attribute") value_index: Int @doc(description: "A unique index number assigned to the configurable product option") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\ConfigurableProductGraphQl\\Model\\Resolver\\Variant\\Attributes\\ConfigurableAttributeUid") # A Base64 string that encodes option details. } type ConfigurableProductOptions @doc(description: "ConfigurableProductOptions defines configurable attributes for the specified product") { diff --git a/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml index 0c46ed4729d66..2300740f23c7d 100644 --- a/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml +++ b/app/code/Magento/Contact/Test/Mftf/Test/StorefrontVerifySecureURLRedirectContactTest.xml @@ -25,11 +25,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <executeJS function="return window.location.host" stepKey="hostname"/> <amOnUrl url="http://{$hostname}/contact" stepKey="goToUnsecureContactURL"/> diff --git a/app/code/Magento/Contact/view/frontend/templates/form.phtml b/app/code/Magento/Contact/view/frontend/templates/form.phtml index d218e650657ac..eee9f742a59a4 100644 --- a/app/code/Magento/Contact/view/frontend/templates/form.phtml +++ b/app/code/Magento/Contact/view/frontend/templates/form.phtml @@ -4,6 +4,9 @@ * See COPYING.txt for license details. */ +// phpcs:disable Magento2.Templates.ThisInTemplate +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Contact\Block\ContactForm $block */ /** @var \Magento\Contact\ViewModel\UserDataProvider $viewModel */ @@ -23,35 +26,35 @@ $viewModel = $block->getViewModel(); <div class="field name required"> <label class="label" for="name"><span><?= $block->escapeHtml(__('Name')) ?></span></label> <div class="control"> - <input name="name" - id="name" - title="<?= $block->escapeHtmlAttr(__('Name')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserName()) ?>" - class="input-text" - type="text" + <input name="name" + id="name" + title="<?= $block->escapeHtmlAttr(__('Name')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserName()) ?>" + class="input-text" + type="text" data-validate="{required:true}"/> </div> </div> <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input name="email" - id="email" - title="<?= $block->escapeHtmlAttr(__('Email')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserEmail()) ?>" - class="input-text" - type="email" + <input name="email" + id="email" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserEmail()) ?>" + class="input-text" + type="email" data-validate="{required:true, 'validate-email':true}"/> </div> </div> <div class="field telephone"> <label class="label" for="telephone"><span><?= $block->escapeHtml(__('Phone Number')) ?></span></label> <div class="control"> - <input name="telephone" - id="telephone" - title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" - value="<?= $block->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" - class="input-text" + <input name="telephone" + id="telephone" + title="<?= $block->escapeHtmlAttr(__('Phone Number')) ?>" + value="<?= $block->escapeHtmlAttr($viewModel->getUserTelephone()) ?>" + class="input-text" type="tel" /> </div> </div> @@ -60,12 +63,12 @@ $viewModel = $block->getViewModel(); <span><?= $block->escapeHtml(__('What’s on your mind?')) ?></span> </label> <div class="control"> - <textarea name="comment" - id="comment" - title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" - class="input-text" - cols="5" - rows="3" + <textarea name="comment" + id="comment" + title="<?= $block->escapeHtmlAttr(__('What’s on your mind?')) ?>" + class="input-text" + cols="5" + rows="3" data-validate="{required:true}"><?= $block->escapeHtml($viewModel->getUserComment()) ?> </textarea> </div> @@ -81,3 +84,12 @@ $viewModel = $block->getViewModel(); </div> </div> </form> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "contact-form" + } + } + } +</script> diff --git a/app/code/Magento/Cookie/Block/Html/Notices.php b/app/code/Magento/Cookie/Block/Html/Notices.php index b4dda788a0292..4bc7ffd7e7e16 100644 --- a/app/code/Magento/Cookie/Block/Html/Notices.php +++ b/app/code/Magento/Cookie/Block/Html/Notices.php @@ -9,12 +9,30 @@ */ namespace Magento\Cookie\Block\Html; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\Cookie\Helper\Cookie as CookieHelper; + /** * @api * @since 100.0.2 */ class Notices extends \Magento\Framework\View\Element\Template { + /** + * @param Template\Context $context + * @param array $data + * @param CookieHelper|null $cookieHelper + */ + public function __construct( + Template\Context $context, + array $data = [], + ?CookieHelper $cookieHelper = null + ) { + $data['cookieHelper'] = $cookieHelper ?? ObjectManager::getInstance()->get(CookieHelper::class); + parent::__construct($context, $data); + } + /** * Get Link to cookie restriction privacy policy page * diff --git a/app/code/Magento/Cookie/Helper/Cookie.php b/app/code/Magento/Cookie/Helper/Cookie.php index 8bab596ab4c13..0e04e7ace2cea 100644 --- a/app/code/Magento/Cookie/Helper/Cookie.php +++ b/app/code/Magento/Cookie/Helper/Cookie.php @@ -80,7 +80,7 @@ public function isUserNotAllowSaveCookie() * Check if cookie restriction mode is enabled for this store * * @return bool - * @since 100.2.0 + * @since 100.1.3 */ public function isCookieRestrictionModeEnabled() { diff --git a/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php b/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php index 9e370a186d272..6522c3ad1dcaa 100644 --- a/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php +++ b/app/code/Magento/Cookie/Test/Unit/Helper/CookieTest.php @@ -162,6 +162,6 @@ public function getConfigMethodStub($hashName) return $defaultConfig[$hashName]; } - throw new \InvalidArgumentException('Unknow id = ' . $hashName); + throw new \InvalidArgumentException('Unknown id = ' . $hashName); } } diff --git a/app/code/Magento/Cookie/etc/adminhtml/system.xml b/app/code/Magento/Cookie/etc/adminhtml/system.xml index 3bf9d11e0a462..e43fc5c5c1e2d 100644 --- a/app/code/Magento/Cookie/etc/adminhtml/system.xml +++ b/app/code/Magento/Cookie/etc/adminhtml/system.xml @@ -29,7 +29,7 @@ <field id="cookie_httponly" translate="label comment" type="select" sortOrder="40" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Use HTTP Only</label> <comment> - <![CDATA[<strong style="color:red">Warning</strong>: Do not set to "No". User security could be compromised.]]> + <![CDATA[<strong class="colorRed">Warning</strong>: Do not set to "No". User security could be compromised.]]> </comment> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> </field> diff --git a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml index b05c53db02abf..f9d9c9071d69d 100644 --- a/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml +++ b/app/code/Magento/Cookie/view/base/templates/html/cookie.phtml @@ -8,10 +8,11 @@ * Cookie settings initialization script * * @var $block \Magento\Framework\View\Element\Js\Cookie + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ -?> -<script> +$scriptString = ' window.cookiesConfig = window.cookiesConfig || {}; - window.cookiesConfig.secure = <?= /* @noEscape */ $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false' ?>; -</script> + window.cookiesConfig.secure = ' . /* @noEscape */ $block->getSessionConfig()->getCookieSecure() ? 'true' : 'false'; + +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml index 8712f31e71b36..38f0d8655f2d6 100644 --- a/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/Cookie/view/frontend/templates/html/notices.phtml @@ -5,17 +5,23 @@ */ /** @var \Magento\Cookie\Block\Html\Notices $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($this->helper(\Magento\Cookie\Helper\Cookie::class)->isCookieRestrictionModeEnabled()) : ?> +<?php +/** @var \Magento\Cookie\Helper\Cookie $cookieHelper */ +$cookieHelper = $block->getData('cookieHelper'); +if ($cookieHelper->isCookieRestrictionModeEnabled()): ?> <div role="alertdialog" tabindex="-1" class="message global cookie" - id="notice-cookie-block" - style="display: none;"> + id="notice-cookie-block"> <div role="document" class="content" tabindex="0"> <p> <strong><?= $block->escapeHtml(__('We use cookies to make your experience better.')) ?></strong> - <span><?= $block->escapeHtml(__('To comply with the new e-Privacy directive, we need to ask for your consent to set the cookies.')) ?></span> + <span><?= $block->escapeHtml(__( + 'To comply with the new e-Privacy directive, we need to ask for your consent to set the cookies.' + )) ?> + </span> <?= $block->escapeHtml(__('<a href="%1">Learn more</a>.', $block->getPrivacyPolicyLink()), ['a']) ?> </p> <div class="actions"> @@ -25,15 +31,16 @@ </div> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#notice-cookie-block') ?> <script type="text/x-magento-init"> { "#notice-cookie-block": { "cookieNotices": { "cookieAllowButtonSelector": "#btn-cookie-allow", "cookieName": "<?= /* @noEscape */ \Magento\Cookie\Helper\Cookie::IS_USER_ALLOWED_SAVE_COOKIE ?>", - "cookieValue": <?= /* @noEscape */ $this->helper(\Magento\Cookie\Helper\Cookie::class)->getAcceptedSaveCookiesWebsiteIds() ?>, - "cookieLifetime": <?= /* @noEscape */ $this->helper(\Magento\Cookie\Helper\Cookie::class)->getCookieRestrictionLifetime() ?>, - "noCookiesUrl": "<?= $block->escapeJs($block->escapeUrl($block->getUrl('cookie/index/noCookies'))) ?>" + "cookieValue": <?= /* @noEscape */ $cookieHelper->getAcceptedSaveCookiesWebsiteIds() ?>, + "cookieLifetime": <?= /* @noEscape */ $cookieHelper->getCookieRestrictionLifetime() ?>, + "noCookiesUrl": "<?= $block->escapeJs($block->getUrl('cookie/index/noCookies')) ?>" } } } diff --git a/app/code/Magento/Cron/Model/Schedule.php b/app/code/Magento/Cron/Model/Schedule.php index 3769b8f12cad2..136e2ef191084 100644 --- a/app/code/Magento/Cron/Model/Schedule.php +++ b/app/code/Magento/Cron/Model/Schedule.php @@ -189,6 +189,7 @@ public function matchCronExpression($expr, $num) } // handle all match by modulus + $offset = 0; if ($expr === '*') { $from = 0; $to = 60; @@ -201,6 +202,13 @@ public function matchCronExpression($expr, $num) $from = $this->getNumeric($e[0]); $to = $this->getNumeric($e[1]); + if ($mod !== 1) { + $offset = $from; + } + } elseif ($mod !== 1) { + $offset = $this->getNumeric($expr); + $from = 0; + $to = 60; } else { // handle regular token $from = $this->getNumeric($expr); @@ -211,7 +219,7 @@ public function matchCronExpression($expr, $num) throw new CronException(__('Invalid cron expression: %1', $expr)); } - return $num >= $from && $num <= $to && $num % $mod === 0; + return $num >= $from && $num <= $to && ($num - $offset) % $mod === 0; } /** diff --git a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php index 25e9a8347b2cd..81e96c6ea75b3 100644 --- a/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php +++ b/app/code/Magento/Cron/Test/Unit/Model/ScheduleTest.php @@ -128,24 +128,28 @@ public function setCronExprDataProvider(): array [', * * * *', [',', '*', '*', '*', '*']], ['1-2 * * * *', ['1-2', '*', '*', '*', '*']], ['0/5 * * * *', ['0/5', '*', '*', '*', '*']], + ['3/5 * * * *', ['3/5', '*', '*', '*', '*']], ['* 0 * * *', ['*', '0', '*', '*', '*']], ['* 59 * * *', ['*', '59', '*', '*', '*']], ['* , * * *', ['*', ',', '*', '*', '*']], ['* 1-2 * * *', ['*', '1-2', '*', '*', '*']], ['* 0/5 * * *', ['*', '0/5', '*', '*', '*']], + ['* 3/5 * * *', ['*', '3/5', '*', '*', '*']], ['* * 0 * *', ['*', '*', '0', '*', '*']], ['* * 23 * *', ['*', '*', '23', '*', '*']], ['* * , * *', ['*', '*', ',', '*', '*']], ['* * 1-2 * *', ['*', '*', '1-2', '*', '*']], ['* * 0/5 * *', ['*', '*', '0/5', '*', '*']], + ['* * 3/5 * *', ['*', '*', '3/5', '*', '*']], ['* * * 1 *', ['*', '*', '*', '1', '*']], ['* * * 31 *', ['*', '*', '*', '31', '*']], ['* * * , *', ['*', '*', '*', ',', '*']], ['* * * 1-2 *', ['*', '*', '*', '1-2', '*']], ['* * * 0/5 *', ['*', '*', '*', '0/5', '*']], + ['* * * 3/5 *', ['*', '*', '*', '3/5', '*']], ['* * * ? *', ['*', '*', '*', '?', '*']], ['* * * L *', ['*', '*', '*', 'L', '*']], ['* * * W *', ['*', '*', '*', 'W', '*']], @@ -156,6 +160,7 @@ public function setCronExprDataProvider(): array ['* * * * ,', ['*', '*', '*', '*', ',']], ['* * * * 1-2', ['*', '*', '*', '*', '1-2']], ['* * * * 0/5', ['*', '*', '*', '*', '0/5']], + ['* * * * 3/5', ['*', '*', '*', '*', '3/5']], ['* * * * JAN', ['*', '*', '*', '*', 'JAN']], ['* * * * DEC', ['*', '*', '*', '*', 'DEC']], ['* * * * JAN-DEC', ['*', '*', '*', '*', 'JAN-DEC']], @@ -165,6 +170,7 @@ public function setCronExprDataProvider(): array ['* * * * * ,', ['*', '*', '*', '*', '*', ',']], ['* * * * * 1-2', ['*', '*', '*', '*', '*', '1-2']], ['* * * * * 0/5', ['*', '*', '*', '*', '*', '0/5']], + ['* * * * * 3/5', ['*', '*', '*', '*', '*', '3/5']], ['* * * * * ?', ['*', '*', '*', '*', '*', '?']], ['* * * * * L', ['*', '*', '*', '*', '*', 'L']], ['* * * * * 6#3', ['*', '*', '*', '*', '*', '6#3']], @@ -372,9 +378,19 @@ public function matchCronExpressionDataProvider(): array ['0-20/5', 21, false], ['0-20/5', 25, false], + ['3-20/5', 3, true], + ['3-20/5', 8, true], + ['3-20/5', 13, true], + ['3-20/5', 24, false], + ['3-20/5', 28, false], + ['1/5', 5, false], ['5/5', 5, true], ['10/5', 10, true], + + ['4/5', 8, false], + ['8/5', 8, true], + ['13/5', 13, true], ]; } diff --git a/app/code/Magento/Csp/Api/CspAwareActionInterface.php b/app/code/Magento/Csp/Api/CspAwareActionInterface.php new file mode 100644 index 0000000000000..f4d58dd2bf55a --- /dev/null +++ b/app/code/Magento/Csp/Api/CspAwareActionInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Framework\App\ActionInterface; + +/** + * Interface for controllers that can provide route-specific CSPs. + */ +interface CspAwareActionInterface extends ActionInterface +{ + /** + * Return CSPs that will be applied to current route (page). + * + * The array returned will be used as is so if you need to keep policies that have been already applied they need + * to be included in the resulting array. + * + * @param \Magento\Csp\Api\Data\PolicyInterface[] $appliedPolicies + * @return \Magento\Csp\Api\Data\PolicyInterface[] + */ + public function modifyCsp(array $appliedPolicies): array; +} diff --git a/app/code/Magento/Csp/Api/InlineUtilInterface.php b/app/code/Magento/Csp/Api/InlineUtilInterface.php new file mode 100644 index 0000000000000..dac2adbfd2270 --- /dev/null +++ b/app/code/Magento/Csp/Api/InlineUtilInterface.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Api; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Utility for classes responsible for rendering and templates that allows whitelist inline sources. + */ +interface InlineUtilInterface +{ + /** + * Render HTML tag and whitelist it as trusted source. + * + * Use this method to whitelist remote static resources and inline styles/scripts. + * Do not use user-provided as any of the parameters. + * + * @param string $tagName + * @param string[] $attributes + * @param string|null $content + * @return string + */ + public function renderTag(string $tagName, array $attributes, ?string $content = null): string; + + /** + * Render event listener as an HTML attribute and whitelist it as trusted source. + * + * Do not use user-provided values as any of the parameters. + * + * @param string $eventName Full attribute name like "onclick". + * @param string $javascript + * @return string + */ + public function renderEventListener(string $eventName, string $javascript): string; +} diff --git a/app/code/Magento/Csp/Helper/InlineUtil.php b/app/code/Magento/Csp/Helper/InlineUtil.php new file mode 100644 index 0000000000000..f9dd9aafa459e --- /dev/null +++ b/app/code/Magento/Csp/Helper/InlineUtil.php @@ -0,0 +1,236 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Helper; + +use Magento\Csp\Api\InlineUtilInterface; +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRender\EventHandlerData; +use Magento\Framework\View\Helper\SecureHtmlRender\HtmlRenderer; +use Magento\Framework\View\Helper\SecureHtmlRender\SecurityProcessorInterface; +use Magento\Framework\View\Helper\SecureHtmlRender\TagData; + +/** + * Helper for classes responsible for rendering and templates. + * + * Allows to whitelist dynamic sources specific to a certain page. + */ +class InlineUtil implements InlineUtilInterface, SecurityProcessorInterface +{ + /** + * @var DynamicCollector + */ + private $dynamicCollector; + + /** + * @var bool + */ + private $useUnsafeHashes; + + /** + * @var HtmlRenderer + */ + private $htmlRenderer; + + private static $tagMeta = [ + 'script' => ['id' => 'script-src', 'remote' => ['src'], 'hash' => true], + 'style' => ['id' => 'style-src', 'remote' => [], 'hash' => true], + 'img' => ['id' => 'img-src', 'remote' => ['src']], + 'audio' => ['id' => 'media-src', 'remote' => ['src']], + 'video' => ['id' => 'media-src', 'remote' => ['src']], + 'track' => ['id' => 'media-src', 'remote' => ['src']], + 'source' => ['id' => 'media-src', 'remote' => ['src']], + 'object' => ['id' => 'object-src', 'remote' => ['data', 'archive']], + 'embed' => ['id' => 'object-src', 'remote' => ['src']], + 'applet' => ['id' => 'object-src', 'remote' => ['code', 'archive']], + 'link' => ['id' => 'style-src', 'remote' => ['href']], + 'form' => ['id' => 'form-action', 'remote' => ['action']], + 'iframe' => ['id' => 'frame-src', 'remote' => ['src']], + 'frame' => ['id' => 'frame-src', 'remote' => ['src']] + ]; + + /** + * @param DynamicCollector $dynamicCollector + * @param bool $useUnsafeHashes Use 'unsafe-hashes' policy (not supported by CSP v2). + * @param HtmlRenderer|null $htmlRenderer + */ + public function __construct( + DynamicCollector $dynamicCollector, + bool $useUnsafeHashes = false, + ?HtmlRenderer $htmlRenderer = null + ) { + $this->dynamicCollector = $dynamicCollector; + $this->useUnsafeHashes = $useUnsafeHashes; + $this->htmlRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(HtmlRenderer::class); + } + + /** + * Generate fetch policy hash for some content. + * + * @param string $content + * @return array Hash data to insert into a FetchPolicy. + */ + private function generateHashValue(string $content): array + { + return [base64_encode(hash('sha256', $content, true)) => 'sha256']; + } + + /** + * Extract host for a fetch policy from a URL. + * + * @param string $url + * @return string|null Null is returned when URL does not point to a remote host. + */ + private function extractHost(string $url): ?string + { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $urlData = parse_url($url); + if (!$urlData + || empty($urlData['scheme']) + || ($urlData['scheme'] !== 'http' && $urlData['scheme'] !== 'https') + ) { + return null; + } + + return $urlData['scheme'] .'://' .$urlData['host']; + } + + /** + * Extract remote hosts used to get fonts. + * + * @param string $styleContent + * @return string[] + */ + private function extractRemoteFonts(string $styleContent): array + { + $urlsFound = [[]]; + preg_match_all('/\@font\-face\s*?\{([^\}]*)[^\}]*?\}/im', $styleContent, $fontFaces); + foreach ($fontFaces[1] as $fontFaceContent) { + preg_match_all('/url\([\'\"]?(http(s)?\:[^\)]+)[\'\"]?\)/i', $fontFaceContent, $urls); + $urlsFound[] = $urls[1]; + } + + return array_map([$this, 'extractHost'], array_merge(...$urlsFound)); + } + + /** + * Extract remote hosts utilized. + * + * @param string $tag + * @param string[] $attributes + * @param string|null $content + * @return string[] + */ + private function extractRemoteHosts(string $tag, array $attributes, ?string $content): array + { + /** @var string[] $remotes */ + $remotes = []; + foreach (self::$tagMeta[$tag]['remote'] as $remoteAttr) { + if (!empty($attributes[$remoteAttr]) && $host = $this->extractHost($attributes[$remoteAttr])) { + $remotes[] = $host; + break; + } + } + if ($tag === 'style' && $content) { + $remotes += $this->extractRemoteFonts($content); + } + + return $remotes; + } + + /** + * @inheritDoc + */ + public function renderTag(string $tagName, array $attributes, ?string $content = null): string + { + if (!array_key_exists($tagName, self::$tagMeta)) { + throw new \InvalidArgumentException('Unknown source type - ' .$tagName); + } + + return $this->htmlRenderer->renderTag($this->processTag(new TagData($tagName, $attributes, $content, false))); + } + + /** + * @inheritDoc + */ + public function renderEventListener(string $eventName, string $javascript): string + { + return $this->htmlRenderer->renderEventHandler( + $this->processEventHandler(new EventHandlerData($eventName, $javascript)) + ); + } + + /** + * @inheritDoc + */ + public function processTag(TagData $tagData): TagData + { + //Processing tag data + if (array_key_exists($tagData->getTag(), self::$tagMeta)) { + /** @var string $policyId */ + $policyId = self::$tagMeta[$tagData->getTag()]['id']; + $remotes = $this->extractRemoteHosts($tagData->getTag(), $tagData->getAttributes(), $tagData->getContent()); + if (empty($remotes) && !$tagData->getContent()) { + throw new \InvalidArgumentException('Either remote URL or hashable content is required to whitelist'); + } + + //Adding required policies. + if ($remotes) { + $this->dynamicCollector->add( + new FetchPolicy($policyId, false, $remotes) + ); + } + if ($tagData->getContent() && !empty(self::$tagMeta[$tagData->getTag()]['hash'])) { + $this->dynamicCollector->add( + new FetchPolicy( + $policyId, + false, + [], + [], + false, + false, + false, + [], + $this->generateHashValue($tagData->getContent()) + ) + ); + } + } + + return $tagData; + } + + /** + * @inheritDoc + */ + public function processEventHandler(EventHandlerData $eventHandlerData): EventHandlerData + { + if ($this->useUnsafeHashes) { + $policy = new FetchPolicy( + 'script-src', + false, + [], + [], + false, + false, + false, + [], + $this->generateHashValue($eventHandlerData->getCode()), + false, + true + ); + } else { + $policy = new FetchPolicy('script-src', false, [], [], false, true); + } + $this->dynamicCollector->add($policy); + + return $eventHandlerData; + } +} diff --git a/app/code/Magento/Csp/Model/BlockCache.php b/app/code/Magento/Csp/Model/BlockCache.php new file mode 100644 index 0000000000000..f0469c3251379 --- /dev/null +++ b/app/code/Magento/Csp/Model/BlockCache.php @@ -0,0 +1,135 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model; + +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\App\CacheInterface; +use Magento\Framework\Serialize\SerializerInterface; + +/** + * CSP aware block cache. + */ +class BlockCache implements CacheInterface +{ + /** + * @var CacheInterface + */ + private $cache; + + /** + * @var DynamicCollector + */ + private $dynamicCollector; + + /** + * @var SerializerInterface + */ + private $serializer; + + /** + * @param CacheInterface $cache + * @param DynamicCollector $dynamicCollector + * @param SerializerInterface $serializer + */ + public function __construct( + CacheInterface $cache, + DynamicCollector $dynamicCollector, + SerializerInterface $serializer + ) { + $this->cache = $cache; + $this->dynamicCollector = $dynamicCollector; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function getFrontend() + { + return $this->cache->getFrontend(); + } + + /** + * @inheritDoc + */ + public function load($identifier) + { + /** @var array|null $data */ + $data = null; + $loaded = $this->cache->load($identifier); + try { + $data = $this->serializer->unserialize($loaded); + if (!is_array($data) || !array_key_exists('policies', $data) || !array_key_exists('html', $data)) { + $data = null; + } + } catch (\Throwable $exception) { + //Most likely block HTML was cached without policy data. + $data = null; + } + if ($data) { + foreach ($data['policies'] as $policyData) { + $this->dynamicCollector->add( + new FetchPolicy( + $policyData['id'], + false, + $policyData['hosts'], + [], + false, + false, + false, + [], + $policyData['hashes'] + ) + ); + } + $loaded = $data['html']; + } + + return $loaded; + } + + /** + * @inheritDoc + */ + public function save($data, $identifier, $tags = [], $lifeTime = null) + { + $collected = $this->dynamicCollector->collect(); + if ($collected) { + $policiesData = []; + foreach ($collected as $policy) { + if ($policy instanceof FetchPolicy) { + $policiesData[] = [ + 'id' => $policy->getId(), + 'hosts' => $policy->getHostSources(), + 'hashes' => $policy->getHashes() + ]; + } + } + $data = $this->serializer->serialize(['policies' => $policiesData, 'html' => $data]); + } + + return $this->cache->save($data, $identifier, $tags, $lifeTime); + } + + /** + * @inheritDoc + */ + public function remove($identifier) + { + return $this->cache->remove($identifier); + } + + /** + * @inheritDoc + */ + public function clean($tags = []) + { + return $this->cache->clean($tags); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/ControllerCollector.php b/app/code/Magento/Csp/Model/Collector/ControllerCollector.php new file mode 100644 index 0000000000000..0239601951744 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/ControllerCollector.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Api\PolicyCollectorInterface; + +/** + * Asks for route-specific policies from a compatible controller. + */ +class ControllerCollector implements PolicyCollectorInterface +{ + /** + * @var CspAwareActionInterface|null + */ + private $controller; + + /** + * Set the action interface that is responsible for processing current HTTP request. + * + * @param CspAwareActionInterface $cspAwareAction + * @return void + */ + public function setCurrentActionInstance(CspAwareActionInterface $cspAwareAction): void + { + $this->controller = $cspAwareAction; + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + if ($this->controller) { + return $this->controller->modifyCsp($defaultPolicies); + } + + return $defaultPolicies; + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php index ab1ee8bb0befe..e0b3af9f9ed81 100644 --- a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Converter.php @@ -43,7 +43,6 @@ public function convert($source) } } $policyConfig[$id]['hosts'] = array_unique($policyConfig[$id]['hosts']); - $policyConfig[$id]['hashes'] = array_unique($policyConfig[$id]['hashes']); } return $policyConfig; diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Data.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Data.php new file mode 100644 index 0000000000000..015327df90efb --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXml/Data.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector\CspWhitelistXml; + +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\Config\Data\Scoped; +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Config\CacheInterface; + +/** + * Provides CSP whitelist configuration + */ +class Data extends Scoped +{ + /** + * Scope priority loading scheme + * + * @var array + */ + protected $_scopePriorityScheme = ['global']; + + /** + * Constructor + * + * @param Reader $reader + * @param ScopeInterface $configScope + * @param CacheInterface $cache + * @param SerializerInterface $serializer + */ + public function __construct( + Reader $reader, + ScopeInterface $configScope, + CacheInterface $cache, + SerializerInterface $serializer + ) { + parent::__construct($reader, $configScope, $cache, 'csp_whitelist_config', $serializer); + } +} diff --git a/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php b/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php index 9f19a5299c063..7fa16fda52ab9 100644 --- a/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php +++ b/app/code/Magento/Csp/Model/Collector/CspWhitelistXmlCollector.php @@ -9,7 +9,7 @@ namespace Magento\Csp\Model\Collector; use Magento\Csp\Api\PolicyCollectorInterface; -use Magento\Csp\Model\Collector\CspWhitelistXml\Reader as ConfigReader; +use Magento\Framework\Config\DataInterface as ConfigReader; use Magento\Csp\Model\Policy\FetchPolicy; /** @@ -36,7 +36,7 @@ public function __construct(ConfigReader $configReader) public function collect(array $defaultPolicies = []): array { $policies = $defaultPolicies; - $config = $this->configReader->read(); + $config = $this->configReader->get(null); foreach ($config as $policyId => $values) { $policies[] = new FetchPolicy( $policyId, diff --git a/app/code/Magento/Csp/Model/Collector/DynamicCollector.php b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php new file mode 100644 index 0000000000000..6478e9622f910 --- /dev/null +++ b/app/code/Magento/Csp/Model/Collector/DynamicCollector.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Api\PolicyCollectorInterface; + +/** + * CSPs dynamically added during the rendering of current page (from .phtml templates for instance). + */ +class DynamicCollector implements PolicyCollectorInterface +{ + /** + * @var PolicyInterface[] + */ + private $added = []; + + /** + * Add a policy for current page. + * + * @param PolicyInterface $policy + * @return void + */ + public function add(PolicyInterface $policy): void + { + $this->added[] = $policy; + } + + /** + * @inheritDoc + */ + public function collect(array $defaultPolicies = []): array + { + return array_merge($defaultPolicies, $this->added); + } +} diff --git a/app/code/Magento/Csp/Model/CompositePolicyCollector.php b/app/code/Magento/Csp/Model/CompositePolicyCollector.php index b775c91b4e1ef..d2c35fe993ed5 100644 --- a/app/code/Magento/Csp/Model/CompositePolicyCollector.php +++ b/app/code/Magento/Csp/Model/CompositePolicyCollector.php @@ -37,22 +37,33 @@ public function __construct(array $collectors, array $mergers) } /** - * Merge 2 policies with the same ID. + * Merge policies with same IDs and return a list of policies with 1 DTO per policy ID. * - * @param PolicyInterface $policy1 - * @param PolicyInterface $policy2 - * @return PolicyInterface + * @param PolicyInterface[] $collected + * @return PolicyInterface[] * @throws \RuntimeException When failed to merge. */ - private function merge(PolicyInterface $policy1, PolicyInterface $policy2): PolicyInterface + private function merge(array $collected): array { - foreach ($this->mergers as $merger) { - if ($merger->canMerge($policy1, $policy2)) { - return $merger->merge($policy1, $policy2); + /** @var PolicyInterface[] $merged */ + $merged = []; + + foreach ($collected as $policy) { + if (array_key_exists($policy->getId(), $merged)) { + foreach ($this->mergers as $merger) { + if ($merger->canMerge($merged[$policy->getId()], $policy)) { + $merged[$policy->getId()] = $merger->merge($merged[$policy->getId()], $policy); + continue 2; + } + } + + throw new \RuntimeException(sprintf('Merge for policies #%s was not found', $policy->getId())); + } else { + $merged[$policy->getId()] = $policy; } } - throw new \RuntimeException(sprintf('Merge for policies #%s was not found', $policy1->getId())); + return $merged; } /** @@ -62,19 +73,9 @@ public function collect(array $defaultPolicies = []): array { $collected = $defaultPolicies; foreach ($this->collectors as $collector) { - $collected = $collector->collect($collected); - } - //Merging policies. - /** @var PolicyInterface[] $result */ - $result = []; - foreach ($collected as $policy) { - if (array_key_exists($policy->getId(), $result)) { - $result[$policy->getId()] = $this->merge($result[$policy->getId()], $policy); - } else { - $result[$policy->getId()] = $policy; - } + $collected = $this->merge($collector->collect($collected)); } - return array_values($result); + return array_values($collected); } } diff --git a/app/code/Magento/Csp/Model/Policy/FetchPolicy.php b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php index 7350cbe80aecb..d045ee48b0ba2 100644 --- a/app/code/Magento/Csp/Model/Policy/FetchPolicy.php +++ b/app/code/Magento/Csp/Model/Policy/FetchPolicy.php @@ -226,11 +226,13 @@ public function getValue(): string if ($this->areEventHandlersAllowed()) { $sources[] = '\'unsafe-hashes\''; } - foreach ($this->getNonceValues() as $nonce) { - $sources[] = '\'nonce-' .base64_encode($nonce) .'\''; - } - foreach ($this->getHashes() as $hash => $algorithm) { - $sources[]= "'$algorithm-$hash'"; + if (!$this->isInlineAllowed()) { + foreach ($this->getNonceValues() as $nonce) { + $sources[] = '\'nonce-' . base64_encode($nonce) . '\''; + } + foreach ($this->getHashes() as $hash => $algorithm) { + $sources[] = "'$algorithm-$hash'"; + } } return implode(' ', $sources); diff --git a/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php b/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php index 14ae23eb3fe37..d419c25acc4ce 100644 --- a/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php +++ b/app/code/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRenderer.php @@ -45,7 +45,7 @@ public function render(PolicyInterface $policy, HttpResponse $response): void $header = 'Content-Security-Policy'; } $value = $policy->getId() .' ' .$policy->getValue() .';'; - if ($config->getReportUri()) { + if ($config->getReportUri() && !$response->getHeader('Report-To')) { $reportToData = [ 'group' => 'report-endpoint', 'max_age' => 10886400, @@ -57,7 +57,10 @@ public function render(PolicyInterface $policy, HttpResponse $response): void $value .= ' report-to '. $reportToData['group'] .';'; $response->setHeader('Report-To', json_encode($reportToData), true); } - $response->setHeader($header, $value, false); + if ($existing = $response->getHeader($header)) { + $value = $value .' ' .$existing->getFieldValue(); + } + $response->setHeader($header, $value, true); } /** diff --git a/app/code/Magento/Csp/Plugin/CspAwareControllerPlugin.php b/app/code/Magento/Csp/Plugin/CspAwareControllerPlugin.php new file mode 100644 index 0000000000000..09dc7256568bc --- /dev/null +++ b/app/code/Magento/Csp/Plugin/CspAwareControllerPlugin.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Plugin; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Model\Collector\ControllerCollector; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\RouterInterface; + +/** + * Plugin that registers CSP aware action instance processing current request. + */ +class CspAwareControllerPlugin +{ + /** + * @var ControllerCollector + */ + private $collector; + + /** + * @param ControllerCollector $collector + */ + public function __construct(ControllerCollector $collector) + { + $this->collector = $collector; + } + + /** + * Register matched action instance. + * + * @param RouterInterface $router + * @param ActionInterface|null $matched + * @return ActionInterface|null + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterMatch(RouterInterface $router, $matched) + { + if ($matched && $matched instanceof CspAwareActionInterface) { + $this->collector->setCurrentActionInstance($matched); + } + + return $matched; + } +} diff --git a/app/code/Magento/Csp/etc/config.xml b/app/code/Magento/Csp/etc/config.xml index e45f6b223ed22..6e2235479da93 100644 --- a/app/code/Magento/Csp/etc/config.xml +++ b/app/code/Magento/Csp/etc/config.xml @@ -16,6 +16,218 @@ <report_only>1</report_only> </admin> </mode> + <policies> + <storefront> + <base> + <policy_id>base-uri</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </base> + <default> + <policy_id>default-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </default> + <children> + <policy_id>child-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + <schemes> + <http>http</http> + <https>https</https> + <blob>blob</blob> + </schemes> + </children> + <connections> + <policy_id>connect-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </connections> + <manifests> + <policy_id>manifest-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </manifests> + <media> + <policy_id>media-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </media> + <objects> + <policy_id>object-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </objects> + <styles> + <policy_id>style-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </styles> + <scripts> + <policy_id>script-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </scripts> + <images> + <policy_id>img-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </images> + <frames> + <policy_id>frame-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frames> + <frame-ancestors> + <policy_id>frame-ancestors</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frame-ancestors> + <forms> + <policy_id>form-action</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </forms> + <fonts> + <policy_id>font-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </fonts> + </storefront> + <admin> + <base> + <policy_id>base-uri</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </base> + <default> + <policy_id>default-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </default> + <children> + <policy_id>child-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + <schemes> + <http>http</http> + <https>https</https> + <blob>blob</blob> + </schemes> + </children> + <connections> + <policy_id>connect-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </connections> + <manifests> + <policy_id>manifest-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </manifests> + <media> + <policy_id>media-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </media> + <objects> + <policy_id>object-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </objects> + <styles> + <policy_id>style-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </styles> + <scripts> + <policy_id>script-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>1</eval> + <dynamic>0</dynamic> + </scripts> + <images> + <policy_id>img-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </images> + <frames> + <policy_id>frame-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frames> + <frame-ancestors> + <policy_id>frame-ancestors</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </frame-ancestors> + <forms> + <policy_id>form-action</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </forms> + <fonts> + <policy_id>font-src</policy_id> + <self>1</self> + <inline>1</inline> + <eval>0</eval> + <dynamic>0</dynamic> + </fonts> + </admin> + </policies> </csp> </default> </config> diff --git a/app/code/Magento/Csp/etc/di.xml b/app/code/Magento/Csp/etc/di.xml index 0804f6d579137..7b1129a0e1a41 100644 --- a/app/code/Magento/Csp/etc/di.xml +++ b/app/code/Magento/Csp/etc/di.xml @@ -18,8 +18,10 @@ <type name="Magento\Csp\Model\CompositePolicyCollector"> <arguments> <argument name="collectors" xsi:type="array"> - <item name="config" xsi:type="object">Magento\Csp\Model\Collector\ConfigCollector</item> - <item name="csp_whitelist" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXmlCollector</item> + <item name="config" xsi:type="object" sortOrder="1">Magento\Csp\Model\Collector\ConfigCollector\Proxy</item> + <item name="whitelist" xsi:type="object" sortOrder="2">Magento\Csp\Model\Collector\CspWhitelistXmlCollector\Proxy</item> + <item name="controller" xsi:type="object" sortOrder="100">Magento\Csp\Model\Collector\ControllerCollector\Proxy</item> + <item name="dynamic" xsi:type="object" sortOrder="3">Magento\Csp\Model\Collector\DynamicCollector\Proxy</item> </argument> <argument name="mergers" xsi:type="array"> <item name="fetch" xsi:type="object">Magento\Csp\Model\Collector\FetchPolicyMerger</item> @@ -47,4 +49,55 @@ <argument name="fileName" xsi:type="string">csp_whitelist.xml</argument> </arguments> </type> + <type name="Magento\Csp\Model\Collector\CspWhitelistXmlCollector"> + <arguments> + <argument name="configReader" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\Data</argument> + </arguments> + </type> + <type name="Magento\Csp\Model\Collector\CspWhitelistXml\Data"> + <arguments> + <argument name="reader" xsi:type="object">Magento\Csp\Model\Collector\CspWhitelistXml\Reader\Proxy</argument> + </arguments> + </type> + <preference for="Magento\Csp\Api\InlineUtilInterface" type="Magento\Csp\Helper\InlineUtil" /> + <type name="Magento\Csp\Plugin\TemplateRenderingPlugin"> + <arguments> + <argument name="util" xsi:type="object">Magento\Csp\Api\InlineUtilInterface\Proxy</argument> + </arguments> + </type> + <type name="Magento\Framework\View\TemplateEngine\Php"> + <arguments> + <argument name="blockVariables" xsi:type="array"> + <item name="csp" xsi:type="object">Magento\Csp\Api\InlineUtilInterface\Proxy</item> + <item name="secureRenderer" xsi:type="object">Magento\Framework\View\Helper\SecureHtmlRenderer\Proxy</item> + <item name="escaper" xsi:type="object">Magento\Framework\Escaper</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\RouterInterface"> + <plugin name="csp_aware_plugin" type="Magento\Csp\Plugin\CspAwareControllerPlugin" /> + </type> + <type name="Magento\Framework\View\Helper\SecureHtmlRenderer"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="csp" xsi:type="object">Magento\Csp\Helper\InlineUtil\Proxy</item> + </argument> + </arguments> + </type> + <type name="Magento\Csp\Observer\Render"> + <arguments> + <argument name="cspRenderer" xsi:type="object">Magento\Csp\Api\CspRendererInterface</argument> + </arguments> + </type> + + <type name="Magento\Csp\Model\BlockCache"> + <arguments> + <argument name="cache" xsi:type="object">configured_block_cache</argument> + </arguments> + </type> + <type name="Magento\Framework\View\Element\Context"> + <arguments> + <argument name="cache" xsi:type="object">Magento\Csp\Model\BlockCache</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml index f7e6e05347345..5cfb4b8f0bb2e 100644 --- a/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml +++ b/app/code/Magento/CurrencySymbol/Test/Mftf/Test/AdminCurrencyConverterAPIConfigurationTest.xml @@ -26,7 +26,9 @@ <!--Set currency allow config--> <magentoCLI command="config:set currency/options/allow RHD,CHW,CHE,AMD,EUR,USD" stepKey="setCurrencyAllow"/> <!--TODO: Add Api key--> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Create product--> <createData entity="SimpleSubCategory" stepKey="createCategory"/> <createData entity="SimpleProduct" stepKey="createProduct"> @@ -68,7 +70,9 @@ <see selector="{{StorefrontCategoryMainSection.productPrice}}" userInput="€" stepKey="seeEURInPrice"/> <!--Set allowed currencies greater then 10--> <magentoCLI command="config:set currency/options/allow RHD,CHW,YER,ZMK,CHE,EUR,USD,AMD,RUB,DZD,ARS,AWG" stepKey="setCurrencyAllow"/> - <magentoCLI command="cache:flush" stepKey="clearCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Import rates from Currency Converter API with currencies greater then 10--> <amOnPage url="{{AdminCurrencyRatesPage.url}}" stepKey="onCurrencyRatePageSecondTime"/> <actionGroup ref="AdminImportCurrencyRatesActionGroup" stepKey="importCurrencyRatesGreaterThen10"> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml index 55195698c5dc8..5cce76a791e2d 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/grid.phtml @@ -6,6 +6,7 @@ /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currencysymbol + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> @@ -31,9 +32,6 @@ ?> <input id="custom_currency_symbol_inherit<?= $block->escapeHtmlAttr($code) ?>" class="admin__control-checkbox" type="checkbox" - <?php //@codingStandardsIgnoreStart ?> - onclick="toggleUseDefault(<?= '\'' . $escapedCode . '\',\'' . $escapedSymbol . '\'' ?>)" - <?php //@codingStandardsIgnoreEnd ?> <?= $data['inherited'] ? ' checked="checked"' : '' ?> value="1" name="inherit_custom_currency_symbol[<?= $block->escapeHtmlAttr($code) ?>]"> @@ -43,6 +41,11 @@ <?= $block->escapeHtml($block->getInheritText()) ?> </span> </label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "toggleUseDefault('" . $escapedCode . "','" . $escapedSymbol . "')", + '#custom_currency_symbol_inherit' . $block->escapeJs($code) + ) ?> </div> </div> </div> diff --git a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml index 18b3c7eef746d..bbc2f95825127 100644 --- a/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml +++ b/app/code/Magento/CurrencySymbol/view/adminhtml/templates/system/currency/rate/matrix.phtml @@ -6,15 +6,20 @@ /** * @var $block \Magento\CurrencySymbol\Block\Adminhtml\System\Currency\Rate\Matrix + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $_oldRates = $block->getOldRates(); $_newRates = $block->getNewRates(); $_rates = ($_newRates) ? $_newRates : $_oldRates; ?> -<?php if (empty($_rates)) : ?> - <div class="message message-warning warning"><p><?= $block->escapeHtml(__('You must first configure currency options before being able to see currency rates.')) ?></p></div> -<?php else : ?> +<?php if (empty($_rates)): ?> + <div class="message message-warning warning"><p> + <?= $block->escapeHtml( + __('You must first configure currency options before being able to see currency rates.') + ) ?></p> + </div> +<?php else: ?> <form name="rateForm" id="rate-form" method="post" action="<?= $block->escapeUrl($block->getRatesFormAction()) ?>"> <?= $block->getBlockHtml('formkey') ?> <div class="admin__control-table-wrapper"> @@ -22,36 +27,53 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; <thead> <tr> <th> </th> - <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode) : ?> + <?php $_i = 0; foreach ($block->getAllowedCurrencies() as $_currencyCode): ?> <th><span><?= $block->escapeHtml($_currencyCode) ?></span></th> <?php endforeach; ?> </tr> </thead> - <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode) : ?> + <?php $_j = 0; foreach ($block->getDefaultCurrencies() as $_currencyCode): ?> <tr> - <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])) : ?> - <?php foreach ($_rates[$_currencyCode] as $_rate => $_value) : ?> - <?php if (++$_j == 1) : ?> - <td><span class="admin__control-support-text"><?= $block->escapeHtml($_currencyCode) ?></span></td> + <?php if (isset($_rates[$_currencyCode]) && is_array($_rates[$_currencyCode])): ?> + <?php foreach ($_rates[$_currencyCode] as $_rate => $_value): ?> + <?php if (++$_j == 1): ?> + <td><span class="admin__control-support-text"><?= $block->escapeHtml($_currencyCode) ?> + </span></td> <td> <input type="text" - name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" - value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" + name="rate[<?= $block->escapeHtmlAttr($_currencyCode) + ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" + value="<?= ($_currencyCode == $_rate) ? '1.0000' : + ($_value>0 ? $block->escapeHtmlAttr($_value) : + (isset($_oldRates[$_currencyCode][$_rate]) ? + $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) + ?>" class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> - <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> - <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> + <?php if (isset($_newRates) && $_currencyCode != $_rate && + isset($_oldRates[$_currencyCode][$_rate])): ?> + <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> + <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong> + </div> <?php endif; ?> </td> - <?php else : ?> + <?php else: ?> <td> <input type="text" - name="rate[<?= $block->escapeHtmlAttr($_currencyCode) ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" - value="<?= ($_currencyCode == $_rate) ? '1.0000' : ($_value>0 ? $block->escapeHtmlAttr($_value) : (isset($_oldRates[$_currencyCode][$_rate]) ? $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) ?>" + name="rate[<?= $block->escapeHtmlAttr($_currencyCode) + ?>][<?= $block->escapeHtmlAttr($_rate) ?>]" + value="<?= ($_currencyCode == $_rate) ? '1.0000' : + ($_value>0 ? $block->escapeHtmlAttr($_value) : + (isset($_oldRates[$_currencyCode][$_rate]) ? + $block->escapeHtmlAttr($_oldRates[$_currencyCode][$_rate]) : '')) + ?>" class="admin__control-text" <?= ($_currencyCode == $_rate) ? ' disabled' : '' ?> /> - <?php if (isset($_newRates) && $_currencyCode != $_rate && isset($_oldRates[$_currencyCode][$_rate])) : ?> - <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong></div> + <?php if (isset($_newRates) && $_currencyCode != $_rate && + isset($_oldRates[$_currencyCode][$_rate])): ?> + <div class="admin__field-note"><?= $block->escapeHtml(__('Old rate:')) ?> + <strong><?= $block->escapeHtml($_oldRates[$_currencyCode][$_rate]) ?></strong> + </div> <?php endif; ?> </td> <?php endif; ?> @@ -64,10 +86,12 @@ $_rates = ($_newRates) ? $_newRates : $_oldRates; </div> </form> <?php endif; ?> -<script> +<?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ jQuery('#rate-form').mage('form').mage('validation'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php b/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php index 6e118b2b40e76..ccbf06206aed7 100644 --- a/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php +++ b/app/code/Magento/Customer/Api/CustomerGroupConfigInterface.php @@ -9,7 +9,7 @@ * Interface for system configuration operations for customer groups. * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface CustomerGroupConfigInterface { @@ -22,7 +22,7 @@ interface CustomerGroupConfigInterface * @throws \Exception * @throws \Magento\Framework\Exception\NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ public function setDefaultCustomerGroup($id); } diff --git a/app/code/Magento/Customer/Api/SessionCleanerInterface.php b/app/code/Magento/Customer/Api/SessionCleanerInterface.php new file mode 100644 index 0000000000000..eb24712105f96 --- /dev/null +++ b/app/code/Magento/Customer/Api/SessionCleanerInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Api; + +/** + * Interface for cleaning customer session data. + */ +interface SessionCleanerInterface +{ + /** + * Destroy all active customer sessions related to given customer id, including current session. + * + * @param int $customerId + * @return void + */ + public function clearFor(int $customerId): void; +} diff --git a/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php b/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php index 07e0704ee6e43..6695d7fd6d3e8 100644 --- a/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php +++ b/app/code/Magento/Customer/Block/Account/AuthenticationPopup.php @@ -70,7 +70,7 @@ public function getConfig() * Added in scope of https://github.com/magento/magento2/pull/8617 * * @return bool|string - * @since 100.2.0 + * @since 101.0.0 */ public function getSerializedConfig() { diff --git a/app/code/Magento/Customer/Block/Account/AuthorizationLink.php b/app/code/Magento/Customer/Block/Account/AuthorizationLink.php index ff9d56c8fc4cb..16ab9d26450b1 100644 --- a/app/code/Magento/Customer/Block/Account/AuthorizationLink.php +++ b/app/code/Magento/Customer/Block/Account/AuthorizationLink.php @@ -94,7 +94,7 @@ public function isLoggedIn() /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Customer/Block/Account/Dashboard.php b/app/code/Magento/Customer/Block/Account/Dashboard.php index 92537009175fd..7281c9fc7b78d 100644 --- a/app/code/Magento/Customer/Block/Account/Dashboard.php +++ b/app/code/Magento/Customer/Block/Account/Dashboard.php @@ -120,7 +120,7 @@ public function getAddressEditUrl($address) * Retrieve the Url for customer orders. * * @return string - * @deprecated Action does not exist + * @deprecated 102.0.3 Action does not exist */ public function getOrdersUrl() { diff --git a/app/code/Magento/Customer/Block/Account/Delimiter.php b/app/code/Magento/Customer/Block/Account/Delimiter.php index 056a53e259c49..2bd93668cc438 100644 --- a/app/code/Magento/Customer/Block/Account/Delimiter.php +++ b/app/code/Magento/Customer/Block/Account/Delimiter.php @@ -10,13 +10,13 @@ * Class for delimiter. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class Delimiter extends \Magento\Framework\View\Element\Template implements SortLinkInterface { /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Customer/Block/Account/Link.php b/app/code/Magento/Customer/Block/Account/Link.php index ed29a10abc8b7..60ade7fe9207c 100644 --- a/app/code/Magento/Customer/Block/Account/Link.php +++ b/app/code/Magento/Customer/Block/Account/Link.php @@ -45,7 +45,7 @@ public function getHref() /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Customer/Block/Account/Navigation.php b/app/code/Magento/Customer/Block/Account/Navigation.php index 705acbcda4c6a..9a8aa698eaa47 100644 --- a/app/code/Magento/Customer/Block/Account/Navigation.php +++ b/app/code/Magento/Customer/Block/Account/Navigation.php @@ -7,20 +7,20 @@ namespace Magento\Customer\Block\Account; -use \Magento\Framework\View\Element\Html\Links; -use \Magento\Customer\Block\Account\SortLinkInterface; +use Magento\Framework\View\Element\Html\Links; +use Magento\Customer\Block\Account\SortLinkInterface; /** * Class for sorting links in navigation panels. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class Navigation extends Links { /** - * {@inheritdoc} - * @since 100.2.0 + * @inheritdoc + * @since 101.0.0 */ public function getLinks() { @@ -47,6 +47,6 @@ public function getLinks() */ private function compare(SortLinkInterface $firstLink, SortLinkInterface $secondLink): int { - return $secondLink->getSortOrder() <=> $firstLink->getSortOrder(); + return $secondLink->getSortOrder() <=> $firstLink->getSortOrder(); } } diff --git a/app/code/Magento/Customer/Block/Account/SortLinkInterface.php b/app/code/Magento/Customer/Block/Account/SortLinkInterface.php index 114bb02e1444c..5dc59aaf95854 100644 --- a/app/code/Magento/Customer/Block/Account/SortLinkInterface.php +++ b/app/code/Magento/Customer/Block/Account/SortLinkInterface.php @@ -9,7 +9,7 @@ /** * Interface for sortable links. * @api - * @since 100.2.0 + * @since 101.0.0 */ interface SortLinkInterface { @@ -23,7 +23,7 @@ interface SortLinkInterface * Get sort order for block. * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder(); } diff --git a/app/code/Magento/Customer/Block/Address/Book.php b/app/code/Magento/Customer/Block/Address/Book.php index 04669446ffee9..f37ae21a9b83c 100644 --- a/app/code/Magento/Customer/Block/Address/Book.php +++ b/app/code/Magento/Customer/Block/Address/Book.php @@ -93,7 +93,7 @@ protected function _prepareLayout() * Generate and return "New Address" URL * * @return string - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getAddAddressUrl */ public function getAddAddressUrl() @@ -118,7 +118,7 @@ public function getBackUrl() * Generate and return "Delete" URL * * @return string - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getDeleteUrl */ public function getDeleteUrl() @@ -133,7 +133,7 @@ public function getDeleteUrl() * * @param int $addressId * @return string - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getAddressEditUrl */ public function getAddressEditUrl($addressId) @@ -159,7 +159,7 @@ public function hasPrimaryAddress() * * @return \Magento\Customer\Api\Data\AddressInterface[]|bool * @throws \Magento\Framework\Exception\LocalizedException - * @deprecated not used in this block + * @deprecated 102.0.1 not used in this block * @see \Magento\Customer\Block\Address\Grid::getAdditionalAddresses */ public function getAdditionalAddresses() diff --git a/app/code/Magento/Customer/Block/Address/Edit.php b/app/code/Magento/Customer/Block/Address/Edit.php index ef9937a0cde8b..9ac1870cd17d9 100644 --- a/app/code/Magento/Customer/Block/Address/Edit.php +++ b/app/code/Magento/Customer/Block/Address/Edit.php @@ -6,6 +6,7 @@ namespace Magento\Customer\Block\Address; use Magento\Customer\Api\AddressMetadataInterface; +use Magento\Customer\Helper\Address; use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; @@ -69,6 +70,7 @@ class Edit extends \Magento\Directory\Block\Data * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper * @param array $data * @param AddressMetadataInterface|null $addressMetadata + * @param Address|null $addressHelper * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -85,7 +87,8 @@ public function __construct( \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, \Magento\Framework\Api\DataObjectHelper $dataObjectHelper, array $data = [], - AddressMetadataInterface $addressMetadata = null + AddressMetadataInterface $addressMetadata = null, + Address $addressHelper = null ) { $this->_customerSession = $customerSession; $this->_addressRepository = $addressRepository; @@ -93,6 +96,8 @@ public function __construct( $this->currentCustomer = $currentCustomer; $this->dataObjectHelper = $dataObjectHelper; $this->addressMetadata = $addressMetadata ?: ObjectManager::getInstance()->get(AddressMetadataInterface::class); + $data['addressHelper'] = $addressHelper ?: ObjectManager::getInstance()->get(Address::class); + $data['directoryHelper'] = $directoryHelper; parent::__construct( $context, $directoryHelper, diff --git a/app/code/Magento/Customer/Block/Address/Grid.php b/app/code/Magento/Customer/Block/Address/Grid.php index 963efc648d94b..9053fd57154bb 100644 --- a/app/code/Magento/Customer/Block/Address/Grid.php +++ b/app/code/Magento/Customer/Block/Address/Grid.php @@ -15,6 +15,7 @@ * Customer address grid * * @api + * @since 102.0.1 */ class Grid extends \Magento\Framework\View\Element\Template { @@ -64,6 +65,7 @@ public function __construct( * * @return void * @throws \Magento\Framework\Exception\LocalizedException + * @since 102.0.1 */ protected function _prepareLayout(): void { @@ -75,6 +77,7 @@ protected function _prepareLayout(): void * Generate and return "New Address" URL * * @return string + * @since 102.0.1 */ public function getAddAddressUrl(): string { @@ -85,6 +88,7 @@ public function getAddAddressUrl(): string * Generate and return "Delete" URL * * @return string + * @since 102.0.1 */ public function getDeleteUrl(): string { @@ -98,6 +102,7 @@ public function getDeleteUrl(): string * * @param int $addressId * @return string + * @since 102.0.1 */ public function getAddressEditUrl($addressId): string { @@ -112,6 +117,7 @@ public function getAddressEditUrl($addressId): string * @return \Magento\Customer\Api\Data\AddressInterface[] * @throws \Magento\Framework\Exception\LocalizedException * @throws NoSuchEntityException + * @since 102.0.1 */ public function getAdditionalAddresses(): array { @@ -132,6 +138,7 @@ public function getAdditionalAddresses(): array * Return stored customer or get it from session * * @return \Magento\Customer\Api\Data\CustomerInterface + * @since 102.0.1 */ public function getCustomer(): \Magento\Customer\Api\Data\CustomerInterface { @@ -148,6 +155,7 @@ public function getCustomer(): \Magento\Customer\Api\Data\CustomerInterface * * @param \Magento\Customer\Api\Data\AddressInterface $address * @return string + * @since 102.0.1 */ public function getStreetAddress(\Magento\Customer\Api\Data\AddressInterface $address): string { @@ -165,6 +173,7 @@ public function getStreetAddress(\Magento\Customer\Api\Data\AddressInterface $ad * * @param string $countryCode * @return string + * @since 102.0.1 */ public function getCountryByCode(string $countryCode): string { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php index 0aeed1562c51e..ad1e7989239f3 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Renderer/Region.php @@ -5,6 +5,9 @@ */ namespace Magento\Customer\Block\Adminhtml\Edit\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Customer address region field renderer */ @@ -16,18 +19,26 @@ class Region extends \Magento\Backend\Block\AbstractBlock implements */ protected $_directoryHelper; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Directory\Helper\Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Directory\Helper\Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_directoryHelper = $directoryHelper; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -60,14 +71,14 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele $selectId . '" name="' . $selectName . - '" class="select required-entry admin__control-select" style="display:none">'; + '" class="select required-entry admin__control-select">'; $html .= '<option value="">' . __('Please select') . '</option>'; $html .= '</select>'; - $html .= '<script>' . "\n"; - $html .= 'require(["prototype", "mage/adminhtml/form"], function(){'; - $html .= '$("' . $selectId . '").setAttribute("defaultValue", "' . $regionId . '");' . "\n"; - $html .= 'new regionUpdater("' . + $scriptString = "\ndocument.querySelector('#$selectId').style.display = 'none';\n"; + $scriptString .= 'require(["prototype", "mage/adminhtml/form"], function(){'; + $scriptString .= '$("' . $selectId . '").setAttribute("defaultValue", "' . $regionId . '");' . "\n"; + $scriptString .= 'new regionUpdater("' . $country->getHtmlId() . '", "' . $element->getHtmlId() . @@ -78,8 +89,9 @@ public function render(\Magento\Framework\Data\Form\Element\AbstractElement $ele ');' . "\n"; - $html .= '});'; - $html .= '</script>' . "\n"; + $scriptString .= '});'; + $scriptString .= "\n"; + $html .= $this->secureRenderer->renderTag('script', [], $scriptString, false); $html .= '</div></div>' . "\n"; diff --git a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php index 656a78d1165e3..799d6e3bc1263 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart.php @@ -316,6 +316,7 @@ private function prepareWebsiteFilter(): void /** * @inheritDoc + * @since 103.0.0 */ public function getMainButtonsHtml() { diff --git a/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php b/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php index bd6e8b69a29ea..726daf69dc587 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Grid/Renderer/Multiaction.php @@ -5,6 +5,10 @@ */ namespace Magento\Customer\Block\Adminhtml\Grid\Renderer; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml customers wishlist grid item action renderer for few action controls in one cell * @@ -12,6 +16,32 @@ */ class Multiaction extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Action { + + /** + * @var SecureHtmlRenderer + */ + private $secureHtmlRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @inheritDoc + */ + public function __construct( + \Magento\Backend\Block\Context $context, + \Magento\Framework\Json\EncoderInterface $jsonEncoder, + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null, + ?Random $random = null + ) { + parent::__construct($context, $jsonEncoder, $data, $secureHtmlRenderer, $random); + $this->secureHtmlRenderer = $secureHtmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + /** * Renders column * @@ -55,9 +85,15 @@ protected function _toLinkHtml($action, \Magento\Framework\DataObject $row) if (isset($action['process']) && $action['process'] == 'configurable') { if ($product->canConfigure()) { - $style = ''; - $onClick = sprintf('onclick="return %s.configureItem(%s)"', $action['control_object'], $row->getId()); - return sprintf('<a href="%s" %s %s>%s</a>', $action['url'], $style, $onClick, $action['caption']); + $id = 'id' .$this->random->getRandomString(10); + $onClick = sprintf('return %s.configureItem(%s)', $action['control_object'], $row->getId()); + return sprintf( + '<a href="%s" id="%s" class="configure-item-link">%s</a>%s', + $action['url'], + $id, + $action['caption'], + $this->secureHtmlRenderer->renderEventListenerAsTag('onclick', $onClick, "#$id") + ); } else { return false; } diff --git a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php index 9ee856f6e0af9..ebdf0090fe1c8 100644 --- a/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php +++ b/app/code/Magento/Customer/Block/Adminhtml/Sales/Order/Address/Form/Renderer/Vat.php @@ -5,7 +5,9 @@ */ namespace Magento\Customer\Block\Adminhtml\Sales\Order\Address\Form\Renderer; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * VAT ID element renderer @@ -31,18 +33,26 @@ class Vat extends \Magento\Backend\Block\Widget\Form\Renderer\Fieldset\Element */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -95,11 +105,8 @@ public function getValidateButton() ); $optionsVarName = $this->getJsVariablePrefix() . 'VatParameters'; - $beforeHtml = '<script>var ' . - $optionsVarName . - ' = ' . - $vatValidateOptions . - ';</script>'; + $scriptString = 'var ' . $optionsVarName . ' = ' . $vatValidateOptions . ';'; + $beforeHtml = $this->secureRenderer->renderTag('script', [], $scriptString, false); $this->_validateButton = $this->getLayout()->createBlock( \Magento\Backend\Block\Widget\Button::class )->setData( @@ -110,6 +117,7 @@ public function getValidateButton() ] ); } + return $this->_validateButton; } } diff --git a/app/code/Magento/Customer/Block/CustomerData.php b/app/code/Magento/Customer/Block/CustomerData.php index 98eb2d9f9ea40..bbc54cfa71c09 100644 --- a/app/code/Magento/Customer/Block/CustomerData.php +++ b/app/code/Magento/Customer/Block/CustomerData.php @@ -59,7 +59,7 @@ public function getCustomerDataUrl($route) * Once this period has expired the corresponding section must be invalidated and reloaded. * * @return int section lifetime in minutes - * @since 100.2.0 + * @since 101.0.0 */ public function getExpirableSectionLifetime() { @@ -70,7 +70,7 @@ public function getExpirableSectionLifetime() * Retrieve the list of sections that can expire. * * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function getExpirableSectionNames() { diff --git a/app/code/Magento/Customer/Block/CustomerScopeData.php b/app/code/Magento/Customer/Block/CustomerScopeData.php index f875386695b4b..63ff863f89ce3 100644 --- a/app/code/Magento/Customer/Block/CustomerScopeData.php +++ b/app/code/Magento/Customer/Block/CustomerScopeData.php @@ -15,7 +15,7 @@ * with appropriate value in store front private cache. * * @api - * @since 100.2.0 + * @since 100.1.9 */ class CustomerScopeData extends \Magento\Framework\View\Element\Template { @@ -54,7 +54,7 @@ public function __construct( * Can be used when necessary to obtain website id of the current customer. * * @return integer - * @since 100.2.0 + * @since 100.1.9 */ public function getWebsiteId() { @@ -67,6 +67,7 @@ public function getWebsiteId() * @param array $configuration * @return bool|string * @throws \InvalidArgumentException + * @since 102.0.0 */ public function encodeConfiguration(array $configuration) { diff --git a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php index 2be340c8ccca4..b4c737f6600bf 100644 --- a/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php +++ b/app/code/Magento/Customer/Block/DataProviders/AddressAttributeData.php @@ -52,7 +52,7 @@ public function getFrontendLabel(string $attributeCode): string { try { $attribute = $this->addressMetadata->getAttributeMetadata($attributeCode); - $frontendLabel = $attribute->getFrontendLabel(); + $frontendLabel = $attribute->getStoreLabel() ?: $attribute->getFrontendLabel(); } catch (NoSuchEntityException $e) { $frontendLabel = ''; } diff --git a/app/code/Magento/Customer/Block/Form/Register.php b/app/code/Magento/Customer/Block/Form/Register.php index 46d1088e37d0f..d6d0d9c494c11 100644 --- a/app/code/Magento/Customer/Block/Form/Register.php +++ b/app/code/Magento/Customer/Block/Form/Register.php @@ -5,6 +5,7 @@ */ namespace Magento\Customer\Block\Form; +use Magento\Customer\Helper\Address; use Magento\Customer\Model\AccountManagement; use Magento\Framework\App\ObjectManager; use Magento\Newsletter\Model\Config; @@ -52,6 +53,7 @@ class Register extends \Magento\Directory\Block\Data * @param \Magento\Customer\Model\Url $customerUrl * @param array $data * @param Config $newsLetterConfig + * @param Address|null $addressHelper * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ @@ -66,8 +68,11 @@ public function __construct( \Magento\Customer\Model\Session $customerSession, \Magento\Customer\Model\Url $customerUrl, array $data = [], - Config $newsLetterConfig = null + Config $newsLetterConfig = null, + Address $addressHelper = null ) { + $data['addressHelper'] = $addressHelper ?: ObjectManager::getInstance()->get(Address::class); + $data['directoryHelper'] = $directoryHelper; $this->_customerUrl = $customerUrl; $this->_moduleManager = $moduleManager; $this->_customerSession = $customerSession; diff --git a/app/code/Magento/Customer/Controller/AbstractAccount.php b/app/code/Magento/Customer/Controller/AbstractAccount.php index 4f2c80711d292..21357f0505f7d 100644 --- a/app/code/Magento/Customer/Controller/AbstractAccount.php +++ b/app/code/Magento/Customer/Controller/AbstractAccount.php @@ -12,7 +12,7 @@ * AbstractAccount class is deprecated, in favour of Composition approach to build Controllers * * @SuppressWarnings(PHPMD.NumberOfChildren) - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Customer\Controller\AccountInterface */ abstract class AbstractAccount extends Action implements AccountInterface diff --git a/app/code/Magento/Customer/Controller/Account/Confirm.php b/app/code/Magento/Customer/Controller/Account/Confirm.php index a1ec3164a3c31..2fc6ed4d422fb 100644 --- a/app/code/Magento/Customer/Controller/Account/Confirm.php +++ b/app/code/Magento/Customer/Controller/Account/Confirm.php @@ -108,7 +108,7 @@ public function __construct( /** * Retrieve cookie manager * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Framework\Stdlib\Cookie\PhpCookieManager */ private function getCookieManager() @@ -124,7 +124,7 @@ private function getCookieManager() /** * Retrieve cookie metadata factory * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return \Magento\Framework\Stdlib\Cookie\CookieMetadataFactory */ private function getCookieMetadataFactory() diff --git a/app/code/Magento/Customer/Controller/Account/CreatePost.php b/app/code/Magento/Customer/Controller/Account/CreatePost.php index 4c1cfa94c5565..14c2ed43171f6 100644 --- a/app/code/Magento/Customer/Controller/Account/CreatePost.php +++ b/app/code/Magento/Customer/Controller/Account/CreatePost.php @@ -452,7 +452,7 @@ protected function checkPasswordConfirmation($password, $confirmation) /** * Retrieve success message * - * @deprecated + * @deprecated 102.0.4 * @see getMessageManagerSuccessMessage() * @return string */ diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index.php b/app/code/Magento/Customer/Controller/Adminhtml/Index.php index ffae1e9f8bf1e..51dc39a2fc658 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index.php @@ -35,7 +35,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Validator - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_validator; @@ -53,13 +53,13 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Customer\Model\CustomerFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_customerFactory = null; /** * @var \Magento\Customer\Model\AddressFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_addressFactory = null; @@ -85,7 +85,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Math\Random - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_random; @@ -96,7 +96,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Api\ExtensibleDataObjectConverter - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_extensibleDataObjectConverter; @@ -132,7 +132,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\Reflection\DataObjectProcessor - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $dataObjectProcessor; @@ -143,7 +143,7 @@ abstract class Index extends \Magento\Backend\App\Action /** * @var \Magento\Framework\View\LayoutFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $layoutFactory; @@ -306,7 +306,7 @@ protected function _addSessionErrorMessages($messages) * @param callable $singleAction A single action callable that takes a customer ID as input * @param int[] $customerIds Array of customer Ids to perform the action upon * @return int Number of customers successfully acted upon - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function actUponMultipleCustomers(callable $singleAction, $customerIds) { diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php index 6528ac4c1f211..910f4e94b90b7 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Cart.php @@ -42,7 +42,7 @@ * Admin customer shopping cart controller * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class Cart extends BaseAction implements HttpGetActionInterface, HttpPostActionInterface { diff --git a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php index 977d3753ded65..192a0f1362ecd 100644 --- a/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php +++ b/app/code/Magento/Customer/Controller/Adminhtml/Index/Save.php @@ -243,7 +243,7 @@ protected function _extractData( /** * Saves default_billing and default_shipping flags for customer address * - * @deprecated must be removed because addresses are save separately for now + * @deprecated 102.0.1 must be removed because addresses are save separately for now * @param array $addressIdList * @param array $extractedCustomerData * @return array @@ -286,7 +286,7 @@ protected function saveDefaultFlags(array $addressIdList, array &$extractedCusto /** * Reformat customer addresses data to be compatible with customer service interface * - * @deprecated addresses are saved separately for now + * @deprecated 102.0.1 addresses are saved separately for now * @param array $extractedCustomerData * @return array */ diff --git a/app/code/Magento/Customer/Controller/Section/Load.php b/app/code/Magento/Customer/Controller/Section/Load.php index 6c3aa06b9f022..e735366d0b8b8 100644 --- a/app/code/Magento/Customer/Controller/Section/Load.php +++ b/app/code/Magento/Customer/Controller/Section/Load.php @@ -23,7 +23,7 @@ class Load extends \Magento\Framework\App\Action\Action implements HttpGetAction /** * @var Identifier - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $sectionIdentifier; diff --git a/app/code/Magento/Customer/CustomerData/SectionPool.php b/app/code/Magento/Customer/CustomerData/SectionPool.php index eef2854cf363e..28d79bf42ecd7 100644 --- a/app/code/Magento/Customer/CustomerData/SectionPool.php +++ b/app/code/Magento/Customer/CustomerData/SectionPool.php @@ -66,6 +66,7 @@ public function getSectionsData(array $sectionNames = null, $forceNewTimestamp = * Return array of section names. * * @return array + * @since 102.0.4 */ public function getSectionNames() { diff --git a/app/code/Magento/Customer/Helper/Address.php b/app/code/Magento/Customer/Helper/Address.php index 765c13b287704..74eee759b4abd 100644 --- a/app/code/Magento/Customer/Helper/Address.php +++ b/app/code/Magento/Customer/Helper/Address.php @@ -81,7 +81,7 @@ class Address extends \Magento\Framework\App\Helper\AbstractHelper /** * @var CustomerMetadataInterface * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_customerMetadataService; @@ -407,7 +407,7 @@ public function isVatAttributeVisible() * @return bool * @throws NoSuchEntityException * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ public function isAttributeVisible($code) { diff --git a/app/code/Magento/Customer/Model/Account/Redirect.php b/app/code/Magento/Customer/Model/Account/Redirect.php index 0389a380b36dc..9824be73f36b5 100644 --- a/app/code/Magento/Customer/Model/Account/Redirect.php +++ b/app/code/Magento/Customer/Model/Account/Redirect.php @@ -58,7 +58,7 @@ class Redirect protected $customerUrl; /** - * @deprecated 100.1.8 + * @deprecated 100.0.2 * @var UrlInterface */ protected $url; diff --git a/app/code/Magento/Customer/Model/AccountManagement.php b/app/code/Magento/Customer/Model/AccountManagement.php index 122a062beeff8..d22a10145c7be 100644 --- a/app/code/Magento/Customer/Model/AccountManagement.php +++ b/app/code/Magento/Customer/Model/AccountManagement.php @@ -13,6 +13,7 @@ use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; +use Magento\Customer\Api\SessionCleanerInterface; use Magento\Customer\Helper\View as CustomerViewHelper; use Magento\Customer\Model\Config\Share as ConfigShare; use Magento\Customer\Model\Customer as CustomerModel; @@ -68,55 +69,55 @@ class AccountManagement implements AccountManagementInterface /** * Configuration paths for create account email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE */ const XML_PATH_REGISTER_EMAIL_TEMPLATE = 'customer/create_account/email_template'; /** * Configuration paths for register no password email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE */ const XML_PATH_REGISTER_NO_PASSWORD_EMAIL_TEMPLATE = 'customer/create_account/email_no_password_template'; /** * Configuration paths for remind email identity * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REGISTER_EMAIL_IDENTITY */ const XML_PATH_REGISTER_EMAIL_IDENTITY = 'customer/create_account/email_identity'; /** * Configuration paths for remind email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_REMIND_EMAIL_TEMPLATE */ const XML_PATH_REMIND_EMAIL_TEMPLATE = 'customer/password/remind_email_template'; /** * Configuration paths for forgot email email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_FORGOT_EMAIL_TEMPLATE */ const XML_PATH_FORGOT_EMAIL_TEMPLATE = 'customer/password/forgot_email_template'; /** * Configuration paths for forgot email identity * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_FORGOT_EMAIL_IDENTITY */ const XML_PATH_FORGOT_EMAIL_IDENTITY = 'customer/password/forgot_email_identity'; /** * Configuration paths for account confirmation required * - * @deprecated Get rid of Helpers in Password Security Management + * @deprecated get rid of Helpers in Password Security Management. * @see AccountConfirmation::XML_PATH_IS_CONFIRM */ const XML_PATH_IS_CONFIRM = 'customer/create_account/confirm'; @@ -124,48 +125,48 @@ class AccountManagement implements AccountManagementInterface /** * Configuration paths for account confirmation email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_CONFIRM_EMAIL_TEMPLATE */ const XML_PATH_CONFIRM_EMAIL_TEMPLATE = 'customer/create_account/email_confirmation_template'; /** * Configuration paths for confirmation confirmed email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_CONFIRMED_EMAIL_TEMPLATE */ const XML_PATH_CONFIRMED_EMAIL_TEMPLATE = 'customer/create_account/email_confirmed_template'; /** * Constants for the type of new account email to be sent * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED */ const NEW_ACCOUNT_EMAIL_REGISTERED = 'registered'; /** * Welcome email, when password setting is required * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD */ const NEW_ACCOUNT_EMAIL_REGISTERED_NO_PASSWORD = 'registered_no_password'; /** * Welcome email, when confirmation is enabled * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_CONFIRMATION */ const NEW_ACCOUNT_EMAIL_CONFIRMATION = 'confirmation'; /** * Confirmation email, when account is confirmed * - * @deprecated Get rid of Helpers in Password Security Management - * @see EmailNotificationInterface::NEW_ACCOUNT_EMAIL_REGISTERED + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotificationInterface::NEW_ACCOUNT_EMAIL_CONFIRMED */ const NEW_ACCOUNT_EMAIL_CONFIRMED = 'confirmed'; @@ -191,15 +192,16 @@ class AccountManagement implements AccountManagementInterface /** * Configuration path to customer reset password email template * - * @deprecated Get rid of Helpers in Password Security Management - * @see Magento/Customer/Model/EmailNotification::XML_PATH_REGISTER_EMAIL_TEMPLATE + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\EmailNotification::XML_PATH_RESET_PASSWORD_TEMPLATE */ const XML_PATH_RESET_PASSWORD_TEMPLATE = 'customer/password/reset_password_template'; /** * Minimum password length * - * @deprecated Get rid of Helpers in Password Security Management + * @deprecated get rid of Helpers in Password Security Management. + * @see \Magento\Customer\Model\AccountManagement::XML_PATH_MINIMUM_PASSWORD_LENGTH */ const MIN_PASSWORD_LENGTH = 6; @@ -283,21 +285,6 @@ class AccountManagement implements AccountManagementInterface */ private $transportBuilder; - /** - * @var SessionManagerInterface - */ - private $sessionManager; - - /** - * @var SaveHandlerInterface - */ - private $saveHandler; - - /** - * @var CollectionFactory - */ - private $visitorCollectionFactory; - /** * @var DataObjectProcessor */ @@ -383,6 +370,11 @@ class AccountManagement implements AccountManagementInterface */ private $getByToken; + /** + * @var SessionCleanerInterface + */ + private $sessionCleaner; + /** * @param CustomerFactory $customerFactory * @param ManagerInterface $eventManager @@ -417,10 +409,12 @@ class AccountManagement implements AccountManagementInterface * @param AddressRegistry|null $addressRegistry * @param GetCustomerByToken|null $getByToken * @param AllowedCountries|null $allowedCountriesReader + * @param SessionCleanerInterface|null $sessionCleaner * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.LongVariable) + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( CustomerFactory $customerFactory, @@ -455,7 +449,8 @@ public function __construct( SearchCriteriaBuilder $searchCriteriaBuilder = null, AddressRegistry $addressRegistry = null, GetCustomerByToken $getByToken = null, - AllowedCountries $allowedCountriesReader = null + AllowedCountries $allowedCountriesReader = null, + SessionCleanerInterface $sessionCleaner = null ) { $this->customerFactory = $customerFactory; $this->eventManager = $eventManager; @@ -486,12 +481,6 @@ public function __construct( $this->dateTimeFactory = $dateTimeFactory ?: $objectManager->get(DateTimeFactory::class); $this->accountConfirmation = $accountConfirmation ?: $objectManager ->get(AccountConfirmation::class); - $this->sessionManager = $sessionManager - ?: $objectManager->get(SessionManagerInterface::class); - $this->saveHandler = $saveHandler - ?: $objectManager->get(SaveHandlerInterface::class); - $this->visitorCollectionFactory = $visitorCollectionFactory - ?: $objectManager->get(CollectionFactory::class); $this->searchCriteriaBuilder = $searchCriteriaBuilder ?: $objectManager->get(SearchCriteriaBuilder::class); $this->addressRegistry = $addressRegistry @@ -500,6 +489,7 @@ public function __construct( ?: $objectManager->get(GetCustomerByToken::class); $this->allowedCountriesReader = $allowedCountriesReader ?: $objectManager->get(AllowedCountries::class); + $this->sessionCleaner = $sessionCleaner ?? $objectManager->get(SessionCleanerInterface::class); } /** @@ -538,7 +528,10 @@ public function resendConfirmation($email, $websiteId = null, $redirectUrl = '') } catch (MailException $e) { // If we are not able to send a new account email, this should be ignored $this->logger->critical($e); + + return false; } + return true; } @@ -685,16 +678,18 @@ public function initiatePasswordReset($email, $template, $websiteId = null) */ private function handleUnknownTemplate($template) { - $phrase = __( - 'Invalid value of "%value" provided for the %fieldName field. Possible values: %template1 or %template2.', - [ - 'value' => $template, - 'fieldName' => 'template', - 'template1' => AccountManagement::EMAIL_REMINDER, - 'template2' => AccountManagement::EMAIL_RESET - ] + throw new InputException( + __( + 'Invalid value of "%value" provided for the %fieldName field. ' + . 'Possible values: %template1 or %template2.', + [ + 'value' => $template, + 'fieldName' => 'template', + 'template1' => AccountManagement::EMAIL_REMINDER, + 'template2' => AccountManagement::EMAIL_RESET + ] + ) ); - throw new InputException($phrase); } /** @@ -725,7 +720,7 @@ public function resetPassword($email, $resetToken, $newPassword) $customerSecure->setRpToken(null); $customerSecure->setRpTokenCreatedAt(null); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); - $this->destroyCustomerSessions($customer->getId()); + $this->sessionCleaner->clearFor((int)$customer->getId()); $this->customerRepository->save($customer); return true; @@ -872,6 +867,7 @@ public function createAccountWithPasswordHash(CustomerInterface $customer, $hash if ($customer->getId()) { $customer = $this->customerRepository->get($customer->getEmail()); $websiteId = $customer->getWebsiteId(); + if ($this->isCustomerInStore($websiteId, $customer->getStoreId())) { throw new InputException(__('This customer already exists in this store.')); } @@ -1050,7 +1046,7 @@ private function changePasswordForCustomer($customer, $currentPassword, $newPass $customerSecure->setRpToken(null); $customerSecure->setRpTokenCreatedAt(null); $customerSecure->setPasswordHash($this->createPasswordHash($newPassword)); - $this->destroyCustomerSessions($customer->getId()); + $this->sessionCleaner->clearFor((int)$customer->getId()); $this->disableAddressValidation($customer); $this->customerRepository->save($customer); @@ -1379,7 +1375,7 @@ protected function sendEmailTemplate( * * @param CustomerInterface $customer * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ protected function isConfirmationRequired($customer) @@ -1396,7 +1392,7 @@ protected function isConfirmationRequired($customer) * * @param CustomerInterface $customer * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ protected function canSkipConfirmation($customer) diff --git a/app/code/Magento/Customer/Model/Address.php b/app/code/Magento/Customer/Model/Address.php index ea9b103f42273..241abbb06f8a1 100644 --- a/app/code/Magento/Customer/Model/Address.php +++ b/app/code/Magento/Customer/Model/Address.php @@ -395,7 +395,7 @@ private function getAttributeList() * Retrieve attribute set id for customer address. * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getAttributeSetId() { diff --git a/app/code/Magento/Customer/Model/Address/AbstractAddress.php b/app/code/Magento/Customer/Model/Address/AbstractAddress.php index fb067decd0b37..8421fc92f8c4a 100644 --- a/app/code/Magento/Customer/Model/Address/AbstractAddress.php +++ b/app/code/Magento/Customer/Model/Address/AbstractAddress.php @@ -632,7 +632,7 @@ protected function _createCountryInstance() * Unset Region from address * * @return $this - * @since 100.2.0 + * @since 101.0.0 */ public function unsRegion() { @@ -644,7 +644,7 @@ public function unsRegion() * * @return bool * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ protected function isCompanyRequired() { @@ -656,7 +656,7 @@ protected function isCompanyRequired() * * @return bool * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ protected function isTelephoneRequired() { @@ -668,7 +668,7 @@ protected function isTelephoneRequired() * * @return bool * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 101.0.0 */ protected function isFaxRequired() { diff --git a/app/code/Magento/Customer/Model/Address/ValidatorInterface.php b/app/code/Magento/Customer/Model/Address/ValidatorInterface.php index 8468f28e70e70..1bdfd77a19311 100644 --- a/app/code/Magento/Customer/Model/Address/ValidatorInterface.php +++ b/app/code/Magento/Customer/Model/Address/ValidatorInterface.php @@ -10,6 +10,7 @@ * Interface for address validator. * * @api + * @since 102.0.0 */ interface ValidatorInterface { @@ -19,6 +20,7 @@ interface ValidatorInterface * * @param AbstractAddress $address * @return array + * @since 102.0.0 */ public function validate(AbstractAddress $address); } diff --git a/app/code/Magento/Customer/Model/AuthenticationInterface.php b/app/code/Magento/Customer/Model/AuthenticationInterface.php index f2d213be2ccfe..3c4cae3089218 100644 --- a/app/code/Magento/Customer/Model/AuthenticationInterface.php +++ b/app/code/Magento/Customer/Model/AuthenticationInterface.php @@ -11,6 +11,7 @@ /** * Interface \Magento\Customer\Model\AuthenticationInterface * @api + * @since 100.1.0 */ interface AuthenticationInterface { @@ -19,6 +20,7 @@ interface AuthenticationInterface * * @param int $customerId * @return void + * @since 100.1.0 */ public function processAuthenticationFailure($customerId); @@ -27,6 +29,7 @@ public function processAuthenticationFailure($customerId); * * @param int $customerId * @return void + * @since 100.1.0 */ public function unlock($customerId); @@ -35,6 +38,7 @@ public function unlock($customerId); * * @param int $customerId * @return boolean + * @since 100.1.0 */ public function isLocked($customerId); @@ -46,6 +50,7 @@ public function isLocked($customerId); * @return boolean * @throws InvalidEmailOrPasswordException * @throws UserLockedException + * @since 100.1.0 */ public function authenticate($customerId, $password); } diff --git a/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php b/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php index ef0b8b7163ad8..53c1b470a5175 100644 --- a/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php +++ b/app/code/Magento/Customer/Model/Checkout/ConfigProvider.php @@ -26,7 +26,7 @@ class ConfigProvider implements ConfigProviderInterface /** * @var UrlInterface - * @deprecated + * @deprecated 101.0.4 */ protected $urlBuilder; diff --git a/app/code/Magento/Customer/Model/Config/Source/Group.php b/app/code/Magento/Customer/Model/Config/Source/Group.php index 7132b8ad4cedf..65b2e14019c40 100644 --- a/app/code/Magento/Customer/Model/Config/Source/Group.php +++ b/app/code/Magento/Customer/Model/Config/Source/Group.php @@ -17,13 +17,13 @@ class Group implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var GroupManagementInterface */ protected $_groupManagement; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var \Magento\Framework\Convert\DataObject */ protected $_converter; diff --git a/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php b/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php index bf1fae8d34bed..38f717b82ea35 100644 --- a/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php +++ b/app/code/Magento/Customer/Model/Config/Source/Group/Multiselect.php @@ -19,13 +19,13 @@ class Multiselect implements \Magento\Framework\Option\ArrayInterface protected $_options; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var GroupManagementInterface */ protected $_groupManagement; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var \Magento\Framework\Convert\DataObject */ protected $_converter; diff --git a/app/code/Magento/Customer/Model/Customer.php b/app/code/Magento/Customer/Model/Customer.php index ea52994735c63..f90b67216254d 100644 --- a/app/code/Magento/Customer/Model/Customer.php +++ b/app/code/Magento/Customer/Model/Customer.php @@ -820,7 +820,7 @@ public function sendNewAccountEmail($type = 'registered', $backUrl = '', $storeI * Check if accounts confirmation is required in config * * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ public function isConfirmationRequired() @@ -1000,6 +1000,7 @@ public function getSharedWebsiteIds() * Retrieve attribute set id for customer. * * @return int + * @since 102.0.1 */ public function getAttributeSetId() { @@ -1197,7 +1198,7 @@ public function setIsReadonly($value) * Check whether confirmation may be skipped when registering using certain email address * * @return bool - * @deprecated + * @deprecated 101.0.4 * @see AccountConfirmation::isConfirmationRequired */ protected function canSkipConfirmation() diff --git a/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php b/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php index a74838a1a7812..184a9ea8ed7bc 100644 --- a/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php +++ b/app/code/Magento/Customer/Model/Customer/Attribute/Backend/Password.php @@ -9,7 +9,7 @@ use Magento\Framework\Exception\LocalizedException; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * Customer password attribute backend */ class Password extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend @@ -69,7 +69,7 @@ public function beforeSave($object) } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param \Magento\Framework\DataObject $object * @return bool */ diff --git a/app/code/Magento/Customer/Model/Customer/DataProvider.php b/app/code/Magento/Customer/Model/Customer/DataProvider.php index 38e597e4e0fe7..ef32ad57886fe 100644 --- a/app/code/Magento/Customer/Model/Customer/DataProvider.php +++ b/app/code/Magento/Customer/Model/Customer/DataProvider.php @@ -33,7 +33,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * - * @deprecated \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses is used instead + * @deprecated 102.0.1 \Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses is used instead * @api * @since 100.0.2 */ @@ -324,7 +324,7 @@ private function canShowAttribute(AbstractAttribute $customerAttribute): bool * Retrieve Country With Websites Source * * @return CountryWithWebsites - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getCountryWithWebsiteSource() { diff --git a/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php b/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php index 26be387a02f9c..e7addc942811d 100644 --- a/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php +++ b/app/code/Magento/Customer/Model/Customer/Source/GroupSourceInterface.php @@ -9,7 +9,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ interface GroupSourceInterface extends OptionSourceInterface { diff --git a/app/code/Magento/Customer/Model/CustomerRegistry.php b/app/code/Magento/Customer/Model/CustomerRegistry.php index d68904f6d1645..f2868132790cf 100644 --- a/app/code/Magento/Customer/Model/CustomerRegistry.php +++ b/app/code/Magento/Customer/Model/CustomerRegistry.php @@ -101,8 +101,10 @@ public function retrieve($customerId) public function retrieveByEmail($customerEmail, $websiteId = null) { if ($websiteId === null) { - $websiteId = $this->storeManager->getStore()->getWebsiteId(); + $websiteId = $this->storeManager->getStore()->getWebsiteId() + ?: $this->storeManager->getDefaultStoreView()->getWebsiteId(); } + $emailKey = $this->getEmailKey($customerEmail, $websiteId); if (isset($this->customerRegistryByEmail[$emailKey])) { return $this->customerRegistryByEmail[$emailKey]; diff --git a/app/code/Magento/Customer/Model/Group/RetrieverInterface.php b/app/code/Magento/Customer/Model/Group/RetrieverInterface.php index 7d2d47cae2f09..1b1e4efdb7c50 100644 --- a/app/code/Magento/Customer/Model/Group/RetrieverInterface.php +++ b/app/code/Magento/Customer/Model/Group/RetrieverInterface.php @@ -9,7 +9,7 @@ * Interface for getting current customer group from session. * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface RetrieverInterface { @@ -17,7 +17,7 @@ interface RetrieverInterface * Retrieve customer group id. * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getCustomerGroupId(); } diff --git a/app/code/Magento/Customer/Model/Metadata/Form/File.php b/app/code/Magento/Customer/Model/Metadata/Form/File.php index 227e85ed98f91..1a1c48075fce5 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/File.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/File.php @@ -57,7 +57,7 @@ class File extends AbstractData /** * @var FileProcessorFactory - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $fileProcessorFactory; diff --git a/app/code/Magento/Customer/Model/Metadata/Form/Image.php b/app/code/Magento/Customer/Model/Metadata/Form/Image.php index 33bdf827f80fa..d023db1454906 100644 --- a/app/code/Magento/Customer/Model/Metadata/Form/Image.php +++ b/app/code/Magento/Customer/Model/Metadata/Form/Image.php @@ -3,17 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Customer\Model\Metadata\Form; use Magento\Customer\Api\AddressMetadataInterface; use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Api\Data\AttributeMetadataInterface; use Magento\Customer\Model\FileProcessor; +use Magento\Customer\Model\FileProcessorFactory; use Magento\Framework\Api\ArrayObjectSearch; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\UploaderFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Io\File as IoFileSystem; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\Framework\Url\EncoderInterface; +use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; +use Psr\Log\LoggerInterface; /** * Metadata for form image field @@ -27,38 +43,55 @@ class Image extends File */ private $imageContentFactory; + /** + * @var IoFileSystem + */ + private $ioFileSystem; + + /** + * @var WriteInterface + */ + private $mediaEntityTmpDirectory; + /** * Constructor * - * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate - * @param \Psr\Log\LoggerInterface $logger - * @param \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param TimezoneInterface $localeDate + * @param LoggerInterface $logger + * @param AttributeMetadataInterface $attribute + * @param ResolverInterface $localeResolver * @param null|string $value * @param string $entityTypeCode * @param bool $isAjax - * @param \Magento\Framework\Url\EncoderInterface $urlEncoder - * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator + * @param EncoderInterface $urlEncoder + * @param NotProtectedExtension $fileValidator * @param Filesystem $fileSystem * @param UploaderFactory $uploaderFactory - * @param \Magento\Customer\Model\FileProcessorFactory|null $fileProcessorFactory - * @param \Magento\Framework\Api\Data\ImageContentInterfaceFactory|null $imageContentInterfaceFactory + * @param FileProcessorFactory|null $fileProcessorFactory + * @param ImageContentInterfaceFactory|null $imageContentInterfaceFactory + * @param IoFileSystem|null $ioFileSystem + * @param DirectoryList|null $directoryList + * @param WriteFactory|null $writeFactory * @SuppressWarnings(PHPMD.ExcessiveParameterList) + * @throws FileSystemException */ public function __construct( - \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate, - \Psr\Log\LoggerInterface $logger, - \Magento\Customer\Api\Data\AttributeMetadataInterface $attribute, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + TimezoneInterface $localeDate, + LoggerInterface $logger, + AttributeMetadataInterface $attribute, + ResolverInterface $localeResolver, $value, $entityTypeCode, $isAjax, - \Magento\Framework\Url\EncoderInterface $urlEncoder, - \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator, + EncoderInterface $urlEncoder, + NotProtectedExtension $fileValidator, Filesystem $fileSystem, UploaderFactory $uploaderFactory, - \Magento\Customer\Model\FileProcessorFactory $fileProcessorFactory = null, - \Magento\Framework\Api\Data\ImageContentInterfaceFactory $imageContentInterfaceFactory = null + FileProcessorFactory $fileProcessorFactory = null, + ImageContentInterfaceFactory $imageContentInterfaceFactory = null, + IoFileSystem $ioFileSystem = null, + ?DirectoryList $directoryList = null, + ?WriteFactory $writeFactory = null ) { parent::__construct( $localeDate, @@ -75,7 +108,16 @@ public function __construct( $fileProcessorFactory ); $this->imageContentFactory = $imageContentInterfaceFactory ?: ObjectManager::getInstance() - ->get(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class); + ->get(ImageContentInterfaceFactory::class); + $this->ioFileSystem = $ioFileSystem ?: ObjectManager::getInstance() + ->get(IoFileSystem::class); + $writeFactory = $writeFactory ?? ObjectManager::getInstance()->get(WriteFactory::class); + $directoryList = $directoryList ?? ObjectManager::getInstance()->get(DirectoryList::class); + $this->mediaEntityTmpDirectory = $writeFactory->create( + $directoryList->getPath($directoryList::MEDIA) + . '/' . $this->_entityTypeCode + . '/' . FileProcessor::TMP_DIR + ); } /** @@ -85,6 +127,7 @@ public function __construct( * * @param array $value * @return string[] + * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -93,7 +136,11 @@ protected function _validateByRules($value) $label = $value['name']; $rules = $this->getAttribute()->getValidationRules(); - $imageProp = @getimagesize($value['tmp_name']); + try { + $imageProp = getimagesize($value['tmp_name']); + } catch (\Throwable $e) { + $imageProp = false; + } if (!$this->_isUploadedFile($value['tmp_name']) || !$imageProp) { return [__('"%1" is not a valid file.', $label)]; @@ -106,9 +153,11 @@ protected function _validateByRules($value) } // modify image name - $extension = pathinfo($value['name'], PATHINFO_EXTENSION); + $extension = $this->ioFileSystem->getPathInfo($value['name'])['extension']; if ($extension != $allowImageTypes[$imageProp[2]]) { - $value['name'] = pathinfo($value['name'], PATHINFO_FILENAME) . '.' . $allowImageTypes[$imageProp[2]]; + $value['name'] = $this->ioFileSystem->getPathInfo($value['name'])['filename'] + . '.' + . $allowImageTypes[$imageProp[2]]; } $maxFileSize = ArrayObjectSearch::getArrayElementByName( @@ -153,6 +202,7 @@ protected function _validateByRules($value) * * @param array $value * @return bool|int|ImageContentInterface|string + * @throws LocalizedException */ protected function processUiComponentValue(array $value) { @@ -174,11 +224,23 @@ protected function processUiComponentValue(array $value) * * @param array $value * @return string + * @throws LocalizedException */ protected function processCustomerAddressValue(array $value) { - $result = $this->getFileProcessor()->moveTemporaryFile($value['file']); - return $result; + $fileName = $this->mediaEntityTmpDirectory + ->getDriver() + ->getRealPathSafety( + $this->mediaEntityTmpDirectory->getAbsolutePath( + ltrim( + $value['file'], + '/' + ) + ) + ); + return $this->getFileProcessor()->moveTemporaryFile( + $this->mediaEntityTmpDirectory->getRelativePath($fileName) + ); } /** @@ -186,20 +248,19 @@ protected function processCustomerAddressValue(array $value) * * @param array $value * @return bool|int|ImageContentInterface|string + * @throws LocalizedException */ protected function processCustomerValue(array $value) { - $temporaryFile = FileProcessor::TMP_DIR . '/' . ltrim($value['file'], '/'); - - if ($this->getFileProcessor()->isExist($temporaryFile)) { + $file = ltrim($value['file'], '/'); + if ($this->mediaEntityTmpDirectory->isExist($file)) { + $temporaryFile = FileProcessor::TMP_DIR . '/' . $file; $base64EncodedData = $this->getFileProcessor()->getBase64EncodedData($temporaryFile); - /** @var ImageContentInterface $imageContentDataObject */ $imageContentDataObject = $this->imageContentFactory->create() ->setName($value['name']) ->setBase64EncodedData($base64EncodedData) ->setType($value['type']); - // Remove temporary file $this->getFileProcessor()->removeUploadedFile($temporaryFile); diff --git a/app/code/Magento/Customer/Model/Options.php b/app/code/Magento/Customer/Model/Options.php index 71e70f8e14208..c230353f2a284 100644 --- a/app/code/Magento/Customer/Model/Options.php +++ b/app/code/Magento/Customer/Model/Options.php @@ -74,7 +74,7 @@ public function getNameSuffixOptions($store = null) * @param bool $isOptional * @return array|bool * - * @deprecated + * @deprecated 101.0.4 * @see prepareNamePrefixSuffixOptions() */ protected function _prepareNamePrefixSuffixOptions($options, $isOptional = false) diff --git a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php index 9eb9ffb806c9f..b877b2cca67a5 100644 --- a/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php +++ b/app/code/Magento/Customer/Model/Plugin/CustomerAuthorization.php @@ -7,7 +7,10 @@ namespace Magento\Customer\Model\Plugin; use Magento\Authorization\Model\UserContextInterface; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; use Magento\Integration\Api\AuthorizationServiceInterface as AuthorizationService; +use Magento\Store\Model\StoreManagerInterface; /** * Plugin around \Magento\Framework\Authorization::isAllowed @@ -19,16 +22,41 @@ class CustomerAuthorization /** * @var UserContextInterface */ - protected $userContext; + private $userContext; + + /** + * @var CustomerFactory + */ + private $customerFactory; + + /** + * @var CustomerResource + */ + private $customerResource; + + /** + * @var StoreManagerInterface + */ + private $storeManager; /** * Inject dependencies. * * @param UserContextInterface $userContext + * @param CustomerFactory $customerFactory + * @param CustomerResource $customerResource + * @param StoreManagerInterface $storeManager */ - public function __construct(UserContextInterface $userContext) - { + public function __construct( + UserContextInterface $userContext, + CustomerFactory $customerFactory, + CustomerResource $customerResource, + StoreManagerInterface $storeManager + ) { $this->userContext = $userContext; + $this->customerFactory = $customerFactory; + $this->customerResource = $customerResource; + $this->storeManager = $storeManager; } /** @@ -53,9 +81,15 @@ public function aroundIsAllowed( && $this->userContext->getUserId() && $this->userContext->getUserType() === UserContextInterface::USER_TYPE_CUSTOMER ) { - return true; - } else { - return $proceed($resource, $privilege); + $customer = $this->customerFactory->create(); + $this->customerResource->load($customer, $this->userContext->getUserId()); + $currentStoreId = $this->storeManager->getStore()->getId(); + $sharedStoreIds = $customer->getSharedStoreIds(); + if (in_array($currentStoreId, $sharedStoreIds)) { + return true; + } } + + return $proceed($resource, $privilege); } } diff --git a/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php new file mode 100644 index 0000000000000..fdde31e05fb2e --- /dev/null +++ b/app/code/Magento/Customer/Model/Plugin/UpdateCustomer.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model\Plugin; + +use Magento\Framework\Webapi\Rest\Request as RestRequest; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; + +/** + * Update customer by id from request param + */ +class UpdateCustomer +{ + /** + * @var RestRequest + */ + private $request; + + /** + * @param RestRequest $request + */ + public function __construct(RestRequest $request) + { + $this->request = $request; + } + + /** + * Update customer by id from request if exist + * + * @param CustomerRepositoryInterface $customerRepository + * @param CustomerInterface $customer + * @param string|null $passwordHash + * @return array + */ + public function beforeSave( + CustomerRepositoryInterface $customerRepository, + CustomerInterface $customer, + ?string $passwordHash = null + ): array { + $customerId = $this->request->getParam('customerId'); + + if ($customerId) { + $customer = $this->getUpdatedCustomer($customerRepository->getById($customerId), $customer); + } + + return [$customer, $passwordHash]; + } + + /** + * Return updated customer + * + * @param CustomerInterface $originCustomer + * @param CustomerInterface $customer + * @return CustomerInterface + */ + private function getUpdatedCustomer( + CustomerInterface $originCustomer, + CustomerInterface $customer + ): CustomerInterface { + $newCustomer = clone $originCustomer; + foreach ($customer->__toArray() as $name => $value) { + if ($name === CustomerInterface::CUSTOM_ATTRIBUTES) { + $value = $customer->getCustomAttributes(); + } elseif ($name === CustomerInterface::EXTENSION_ATTRIBUTES_KEY) { + $value = $customer->getExtensionAttributes(); + } elseif ($name === CustomerInterface::KEY_ADDRESSES) { + $value = $customer->getAddresses(); + } + + $newCustomer->setData($name, $value); + } + + return $newCustomer; + } +} diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address.php b/app/code/Magento/Customer/Model/ResourceModel/Address.php index 200eaabe6517d..8e44638e7aee8 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address.php @@ -126,7 +126,7 @@ public function delete($object) /** * Get instance of DeleteRelation class * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return DeleteRelation */ private function getDeleteRelation() @@ -137,7 +137,7 @@ private function getDeleteRelation() /** * Get instance of CustomerRegistry class * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CustomerRegistry */ private function getCustomerRegistry() diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php index 37b3b1af0a42d..62339cc7b974f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Attribute/Source/Country.php @@ -67,7 +67,7 @@ protected function _createCountriesCollection() /** * Retrieve Store Manager - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return StoreManagerInterface */ private function getStoreManager() diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php index 0e2eb3e1d8e65..c7b44288bc85f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Grid/Collection.php @@ -185,7 +185,7 @@ public function addFieldToFilter($field, $condition = null) { if ($field === 'region') { $conditionSql = $this->_getConditionSql( - $this->getRegionNameExpresion(), + $this->getRegionNameExpression(), $condition ); $this->getSelect()->where($conditionSql); @@ -211,7 +211,7 @@ public function addFullTextFilter(string $value) $whereCondition = ''; foreach ($fields as $key => $field) { $field = $field === 'region' - ? $this->getRegionNameExpresion() + ? $this->getRegionNameExpression() : 'main_table.' . $field; $condition = $this->_getConditionSql( $this->getConnection()->quoteIdentifier($field), @@ -246,18 +246,18 @@ private function joinRegionNameTable() )->joinLeft( ['rnt' => $this->getTable('directory_country_region_name')], "rnt.region_id={$regionIdField} AND {$localeCondition}", - ['region' => $this->getRegionNameExpresion()] + ['region' => $this->getRegionNameExpression()] ); return $this; } /** - * Get SQL Expresion to define Region Name field by locale + * Get SQL Expression to define Region Name field by locale * * @return \Zend_Db_Expr */ - private function getRegionNameExpresion(): \Zend_Db_Expr + private function getRegionNameExpression(): \Zend_Db_Expr { $connection = $this->getConnection(); $defaultNameExpr = $connection->getIfNullSql( diff --git a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php index cf837e2924161..dd502abcafca9 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Address/Relation.php @@ -142,7 +142,7 @@ private function updateCustomerRegistry(Customer $customer, array $changedAddres /** * Checks if address has chosen as default and has had an id * - * @deprecated Is not used anymore due to changes in logic of save of address. + * @deprecated 102.0.1 Is not used anymore due to changes in logic of save of address. * If address was default and becomes not default than default address id for customer must be * set to null * @param AbstractModel $object diff --git a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php index 3fe61785de897..48828d58fca04 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/AddressRepository.php @@ -209,7 +209,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param FilterGroup $filterGroup * @param Collection $collection * @return void @@ -268,7 +268,7 @@ public function deleteById($addressId) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php index 0611a2df641e7..9a513493ce62f 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/CustomerRepository.php @@ -10,6 +10,7 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerSearchResultsInterfaceFactory; +use Magento\Customer\Api\GroupRepositoryInterface; use Magento\Customer\Model\Customer as CustomerModel; use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Customer\Model\CustomerFactory; @@ -27,6 +28,8 @@ use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Model\StoreManagerInterface; /** @@ -119,6 +122,11 @@ class CustomerRepository implements CustomerRepositoryInterface */ private $delegatedStorage; + /** + * @var GroupRepositoryInterface + */ + private $groupRepository; + /** * @param CustomerFactory $customerFactory * @param CustomerSecureFactory $customerSecureFactory @@ -136,6 +144,7 @@ class CustomerRepository implements CustomerRepositoryInterface * @param CollectionProcessorInterface $collectionProcessor * @param NotificationStorage $notificationStorage * @param DelegatedStorage|null $delegatedStorage + * @param GroupRepositoryInterface|null $groupRepository * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -154,7 +163,8 @@ public function __construct( JoinProcessorInterface $extensionAttributesJoinProcessor, CollectionProcessorInterface $collectionProcessor, NotificationStorage $notificationStorage, - DelegatedStorage $delegatedStorage = null + DelegatedStorage $delegatedStorage = null, + ?GroupRepositoryInterface $groupRepository = null ) { $this->customerFactory = $customerFactory; $this->customerSecureFactory = $customerSecureFactory; @@ -172,6 +182,7 @@ public function __construct( $this->collectionProcessor = $collectionProcessor; $this->notificationStorage = $notificationStorage; $this->delegatedStorage = $delegatedStorage ?? ObjectManager::getInstance()->get(DelegatedStorage::class); + $this->groupRepository = $groupRepository ?: ObjectManager::getInstance()->get(GroupRepositoryInterface::class); } /** @@ -216,6 +227,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) $prevCustomerData ? $prevCustomerData->getStoreId() : $this->storeManager->getStore()->getId() ); } + $this->validateGroupId($customer->getGroupId()); $this->setCustomerGroupId($customerModel, $customerArr, $prevCustomerDataArr); // Need to use attribute set or future updates can cause data loss if (!$customerModel->getAttributeSetId()) { @@ -268,10 +280,7 @@ public function save(CustomerInterface $customer, $passwordHash = null) $savedAddressIds[] = $address->getId(); } } - $addressIdsToDelete = array_diff($existingAddressIds, $savedAddressIds); - foreach ($addressIdsToDelete as $addressId) { - $this->addressRepository->deleteById($addressId); - } + $this->deleteAddressesByIds(array_diff($existingAddressIds, $savedAddressIds)); } $this->customerRegistry->remove($customerId); $savedCustomer = $this->get($customer->getEmail(), $customer->getWebsiteId()); @@ -286,6 +295,39 @@ public function save(CustomerInterface $customer, $passwordHash = null) return $savedCustomer; } + /** + * Delete addresses by ids + * + * @param array $addressIds + * @return void + */ + private function deleteAddressesByIds(array $addressIds): void + { + foreach ($addressIds as $id) { + $this->addressRepository->deleteById($id); + } + } + + /** + * Validate customer group id if exist + * + * @param int|null $groupId + * @return bool + * @throws LocalizedException + */ + private function validateGroupId(?int $groupId): bool + { + if ($groupId) { + try { + $this->groupRepository->getById($groupId); + } catch (NoSuchEntityException $e) { + throw new LocalizedException(__('The specified customer group id does not exist.')); + } + } + + return true; + } + /** * Set secure data to customer model * @@ -425,7 +467,7 @@ public function deleteById($customerId) /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param FilterGroup $filterGroup * @param Collection $collection * @return void diff --git a/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php b/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php index bf8ef767063bd..0fab27161ce25 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php +++ b/app/code/Magento/Customer/Model/ResourceModel/Grid/Collection.php @@ -74,7 +74,7 @@ public function addFieldToFilter($field, $condition = null) { if ($field === 'billing_region') { $conditionSql = $this->_getConditionSql( - $this->getRegionNameExpresion(), + $this->getRegionNameExpression(), $condition ); $this->getSelect()->where($conditionSql); @@ -100,7 +100,7 @@ public function addFullTextFilter(string $value) $whereCondition = ''; foreach ($fields as $key => $field) { $field = $field === 'billing_region' - ? $this->getRegionNameExpresion() + ? $this->getRegionNameExpression() : 'main_table.' . $field; $condition = $this->_getConditionSql( $this->getConnection()->quoteIdentifier($field), @@ -152,18 +152,18 @@ private function joinRegionNameTable() )->joinLeft( ['rnt' => $this->getTable('directory_country_region_name')], "rnt.region_id={$regionIdField} AND {$localeCondition}", - ['billing_region' => $this->getRegionNameExpresion()] + ['billing_region' => $this->getRegionNameExpression()] ); return $this; } /** - * Get SQL Expresion to define Region Name field by locale + * Get SQL Expression to define Region Name field by locale * * @return \Zend_Db_Expr */ - private function getRegionNameExpresion(): \Zend_Db_Expr + private function getRegionNameExpression(): \Zend_Db_Expr { $connection = $this->getConnection(); $defaultNameExpr = $connection->getIfNullSql( diff --git a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php index 664f85f841e3f..10979cd9cc22a 100644 --- a/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php +++ b/app/code/Magento/Customer/Model/ResourceModel/GroupRepository.php @@ -220,7 +220,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) /** * Helper function that adds a FilterGroup to the collection. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param FilterGroup $filterGroup * @param Collection $collection * @return void @@ -243,7 +243,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti /** * Translates a field name to a DB column name for use in collection queries. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param string $field a field name that should be translated to a DB column name. * @return string */ @@ -343,7 +343,7 @@ protected function _verifyTaxClassModel($taxClassId, $group) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Customer/Model/Session/SessionCleaner.php b/app/code/Magento/Customer/Model/Session/SessionCleaner.php new file mode 100644 index 0000000000000..1423c94782535 --- /dev/null +++ b/app/code/Magento/Customer/Model/Session/SessionCleaner.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model\Session; + +use Magento\Customer\Api\SessionCleanerInterface; +use Magento\Customer\Model\ResourceModel\Visitor\CollectionFactory as VisitorCollectionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Intl\DateTimeFactory; +use Magento\Framework\Session\Config; +use Magento\Framework\Session\SaveHandlerInterface; +use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\Stdlib\DateTime; +use Magento\Store\Model\ScopeInterface; + +/** + * Deletes all session data which relates to customer, including current session data. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class SessionCleaner implements SessionCleanerInterface +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var DateTimeFactory + */ + private $dateTimeFactory; + + /** + * @var VisitorCollectionFactory + */ + private $visitorCollectionFactory; + + /** + * @var SessionManagerInterface + */ + private $sessionManager; + + /** + * @var SaveHandlerInterface + */ + private $saveHandler; + + /** + * @inheritdoc + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + DateTimeFactory $dateTimeFactory, + VisitorCollectionFactory $visitorCollectionFactory, + SessionManagerInterface $sessionManager, + SaveHandlerInterface $saveHandler + ) { + $this->scopeConfig = $scopeConfig; + $this->dateTimeFactory = $dateTimeFactory; + $this->visitorCollectionFactory = $visitorCollectionFactory; + $this->sessionManager = $sessionManager; + $this->saveHandler = $saveHandler; + } + + /** + * @inheritdoc + */ + public function clearFor(int $customerId): void + { + if ($this->sessionManager->isSessionExists()) { + //delete old session and move data to the new session + //use this instead of $this->sessionManager->regenerateId because last one doesn't delete old session + // phpcs:ignore Magento2.Functions.DiscouragedFunction + session_regenerate_id(true); + } + + $sessionLifetime = $this->scopeConfig->getValue( + Config::XML_PATH_COOKIE_LIFETIME, + ScopeInterface::SCOPE_STORE + ); + $dateTime = $this->dateTimeFactory->create(); + $activeSessionsTime = $dateTime->setTimestamp($dateTime->getTimestamp() - $sessionLifetime) + ->format(DateTime::DATETIME_PHP_FORMAT); + /** @var \Magento\Customer\Model\ResourceModel\Visitor\Collection $visitorCollection */ + $visitorCollection = $this->visitorCollectionFactory->create(); + $visitorCollection->addFieldToFilter('customer_id', $customerId); + $visitorCollection->addFieldToFilter('last_visit_at', ['from' => $activeSessionsTime]); + /** @var \Magento\Customer\Model\Visitor $visitor */ + foreach ($visitorCollection->getItems() as $visitor) { + $sessionId = $visitor->getSessionId(); + $this->sessionManager->start(); + $this->saveHandler->destroy($sessionId); + $this->sessionManager->writeClose(); + } + } +} diff --git a/app/code/Magento/Customer/Model/Visitor.php b/app/code/Magento/Customer/Model/Visitor.php index 53745aa7a30c6..99dec57b89d15 100644 --- a/app/code/Magento/Customer/Model/Visitor.php +++ b/app/code/Magento/Customer/Model/Visitor.php @@ -25,7 +25,7 @@ use Magento\Store\Model\ScopeInterface; /** - * Class Visitor + * Class Visitor responsible for initializing visitor's. * * Used to track sessions of the logged in customers * @@ -206,7 +206,9 @@ public function initByRequest($observer) public function saveByRequest($observer) { // prevent saving Visitor for safe methods, e.g. GET request - if ($this->skipRequestLogging || $this->requestSafety->isSafeMethod() || $this->isModuleIgnored($observer)) { + if (($this->skipRequestLogging || $this->requestSafety->isSafeMethod() || $this->isModuleIgnored($observer)) + && !$this->sessionIdHasChanged() + ) { return $this; } @@ -223,6 +225,23 @@ public function saveByRequest($observer) return $this; } + /** + * Check if visitor session id was changed. + * + * @return bool + */ + private function sessionIdHasChanged(): bool + { + $visitorData = $this->session->getVisitorData(); + $hasChanged = false; + + if (isset($visitorData['session_id'])) { + $hasChanged = $this->session->getSessionId() !== $visitorData['session_id']; + } + + return $hasChanged; + } + /** * Returns true if the module is required * diff --git a/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php new file mode 100644 index 0000000000000..c2b7189b808a3 --- /dev/null +++ b/app/code/Magento/Customer/Observer/UpgradeOrderCustomerEmailObserver.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Observer; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\Customer\Model\Data\Customer; + +/** + * Class observer UpgradeOrderCustomerEmailObserver + * Update orders customer email after corresponding customer email changed + */ +class UpgradeOrderCustomerEmailObserver implements ObserverInterface +{ + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param OrderRepositoryInterface $orderRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + OrderRepositoryInterface $orderRepository, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->orderRepository = $orderRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * Upgrade order customer email when customer has changed email + * + * @param Observer $observer + * @return void + */ + public function execute(Observer $observer): void + { + /** @var Customer $originalCustomer */ + $originalCustomer = $observer->getEvent()->getOrigCustomerDataObject(); + if (!$originalCustomer) { + return; + } + + /** @var Customer $customer */ + $customer = $observer->getEvent()->getCustomerDataObject(); + $customerEmail = $customer->getEmail(); + + if ($customerEmail === $originalCustomer->getEmail()) { + return; + } + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + /** + * @var Collection $orders + */ + $orders = $this->orderRepository->getList($searchCriteria); + $orders->setDataToAll(OrderInterface::CUSTOMER_EMAIL, $customerEmail); + $orders->save(); + } +} diff --git a/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php b/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php index bad5735bc3e3a..1a8cdb8987db7 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/AddCustomerUpdatedAtAttribute.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddCustomerUpdatedAtAttribute - * @package Magento\Customer\Setup\Patch + * Class add customer updated attribute to customer */ class AddCustomerUpdatedAtAttribute implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class AddCustomerUpdatedAtAttribute implements DataPatchInterface, PatchVersionI private $customerSetupFactory; /** - * AddCustomerUpdatedAtAttribute constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -61,10 +59,12 @@ public function apply() 'system' => false, ] ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -74,7 +74,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -82,7 +82,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php b/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php index ba50f6e17dd87..36611afc6a2aa 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/AddNonSpecifiedGenderAttributeOption.php @@ -3,30 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Directory\Model\AllowedCountries; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\Setup\SetupInterface; -use Magento\Framework\Setup\UpgradeDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\DB\FieldDataConverterFactory; -use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddNonSpecifiedGenderAttributeOption - * @package Magento\Customer\Setup\Patch + * Class add non specified gender attribute option to customer */ class AddNonSpecifiedGenderAttributeOption implements DataPatchInterface, PatchVersionInterface { @@ -41,7 +29,6 @@ class AddNonSpecifiedGenderAttributeOption implements DataPatchInterface, PatchV private $customerSetupFactory; /** - * AddNonSpecifiedGenderAttributeOption constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -54,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -64,10 +51,12 @@ public function apply() $option = ['attribute_id' => $attributeId, 'values' => [3 => 'Not Specified']]; $customerSetup->addAttributeOption($option); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -77,7 +66,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -85,7 +74,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php b/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php index b066d14a3c63e..09611ac1ccca3 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/AddSecurityTrackingAttributes.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class AddSecurityTrackingAttributes - * @package Magento\Customer\Setup\Patch + * Class add security tracking attributes to customer */ class AddSecurityTrackingAttributes implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class AddSecurityTrackingAttributes implements DataPatchInterface, PatchVersionI private $customerSetupFactory; /** - * AddSecurityTrackingAttributes constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -94,12 +92,14 @@ public function apply() $this->moduleDataSetup->getConnection()->update( $configTable, ['value' => new \Zend_Db_Expr('value*24')], - ['path = ?' => \Magento\Customer\Model\Customer::XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD] + ['path = ?' => Customer::XML_PATH_CUSTOMER_RESET_PASSWORD_LINK_EXPIRATION_PERIOD] ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -109,7 +109,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -117,7 +117,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php b/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php index 83c5fe7ae6d1e..e25fe5803e46c 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/ConvertValidationRulesFromSerializedToJson.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Framework\DB\FieldDataConverterFactory; use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class ConvertValidationRulesFromSerializedToJson - * @package Magento\Customer\Setup\Patch + * Class convert validation rules from serialized to json for customer */ class ConvertValidationRulesFromSerializedToJson implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class ConvertValidationRulesFromSerializedToJson implements DataPatchInterface, private $fieldDataConverterFactory; /** - * ConvertValidationRulesFromSerializedToJson constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param FieldDataConverterFactory $fieldDataConverterFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -54,10 +52,12 @@ public function apply() 'attribute_id', 'validate_rules' ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -67,7 +67,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -75,7 +75,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php b/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php index 6e61b66f3c9db..fccda2ee9dac1 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/DefaultCustomerGroupsAndAttributes.php @@ -4,19 +4,20 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetup; use Magento\Customer\Setup\CustomerSetupFactory; +use Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend; use Magento\Framework\Module\Setup\Migration; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class DefaultCustomerGroupsAndAttributes - * @package Magento\Customer\Setup\Patch + * Class default groups and attributes for customer */ class DefaultCustomerGroupsAndAttributes implements DataPatchInterface, PatchVersionInterface { @@ -31,20 +32,20 @@ class DefaultCustomerGroupsAndAttributes implements DataPatchInterface, PatchVer private $moduleDataSetup; /** - * DefaultCustomerGroupsAndAttributes constructor. * @param CustomerSetupFactory $customerSetupFactory * @param ModuleDataSetupInterface $moduleDataSetup */ public function __construct( CustomerSetupFactory $customerSetupFactory, - \Magento\Framework\Setup\ModuleDataSetupInterface $moduleDataSetup + ModuleDataSetupInterface $moduleDataSetup ) { $this->customerSetupFactory = $customerSetupFactory; $this->moduleDataSetup = $moduleDataSetup; } /** - * {@inheritdoc} + * @inheritdoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function apply() @@ -133,7 +134,7 @@ public function apply() 'customer_address', 'street', 'backend_model', - \Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend::class + DefaultBackend::class ); $migrationSetup = $this->moduleDataSetup->createMigrationSetup(); @@ -146,10 +147,12 @@ public function apply() ['attribute_id'] ); $migrationSetup->doUpdateClassAliases(); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -157,7 +160,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -165,7 +168,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php index e4978070f53ad..1f21c7d4e83ba 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/MigrateStoresAllowedCountriesToWebsite.php @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; +use Exception; use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Store\Model\ScopeInterface; @@ -34,15 +36,14 @@ class MigrateStoresAllowedCountriesToWebsite implements DataPatchInterface, Patc private $allowedCountries; /** - * MigrateStoresAllowedCountriesToWebsite constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param StoreManagerInterface $storeManager * @param AllowedCountries $allowedCountries */ public function __construct( ModuleDataSetupInterface $moduleDataSetup, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Directory\Model\AllowedCountries $allowedCountries + StoreManagerInterface $storeManager, + AllowedCountries $allowedCountries ) { $this->moduleDataSetup = $moduleDataSetup; $this->storeManager = $storeManager; @@ -51,6 +52,8 @@ public function __construct( /** * @inheritdoc + * + * @throws Exception */ public function apply() { @@ -60,10 +63,12 @@ public function apply() try { $this->migrateStoresAllowedCountriesToWebsite(); $this->moduleDataSetup->getConnection()->commit(); - } catch (\Exception $e) { + } catch (Exception $e) { $this->moduleDataSetup->getConnection()->rollBack(); throw $e; } + + return $this; } /** diff --git a/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php b/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php index 51f54dc4a432c..5dfcf2bf9bf0d 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/RemoveCheckoutRegisterAndUpdateAttributes.php @@ -3,30 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Address; +use Magento\Customer\Model\ResourceModel\Address\Attribute\Backend\Region; +use Magento\Customer\Model\ResourceModel\Address\Attribute\Source\Country; +use Magento\Customer\Model\ResourceModel\Attribute\Collection; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Directory\Model\AllowedCountries; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\Setup\SetupInterface; -use Magento\Framework\Setup\UpgradeDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; +use Magento\Eav\Model\Entity\Increment\NumericValue; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\DB\FieldDataConverterFactory; -use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class RemoveCheckoutRegisterAndUpdateAttributes - * @package Magento\Customer\Setup\Patch + * Remove register and update attributes for checkout */ class RemoveCheckoutRegisterAndUpdateAttributes implements DataPatchInterface, PatchVersionInterface { @@ -41,7 +34,6 @@ class RemoveCheckoutRegisterAndUpdateAttributes implements DataPatchInterface, P private $customerSetupFactory; /** - * RemoveCheckoutRegisterAndUpdateAttributes constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -54,7 +46,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -64,52 +56,54 @@ public function apply() ); $customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]); $customerSetup->updateEntityType( - \Magento\Customer\Model\Customer::ENTITY, + Customer::ENTITY, 'entity_model', \Magento\Customer\Model\ResourceModel\Customer::class ); $customerSetup->updateEntityType( - \Magento\Customer\Model\Customer::ENTITY, + Customer::ENTITY, 'increment_model', - \Magento\Eav\Model\Entity\Increment\NumericValue::class + NumericValue::class ); $customerSetup->updateEntityType( - \Magento\Customer\Model\Customer::ENTITY, + Customer::ENTITY, 'entity_attribute_collection', - \Magento\Customer\Model\ResourceModel\Attribute\Collection::class + Collection::class ); $customerSetup->updateEntityType( 'customer_address', 'entity_model', - \Magento\Customer\Model\ResourceModel\Address::class + Address::class ); $customerSetup->updateEntityType( 'customer_address', 'entity_attribute_collection', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Collection::class + Address\Attribute\Collection::class ); $customerSetup->updateAttribute( 'customer_address', 'country_id', 'source_model', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Source\Country::class + Country::class ); $customerSetup->updateAttribute( 'customer_address', 'region', 'backend_model', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Backend\Region::class + Region::class ); $customerSetup->updateAttribute( 'customer_address', 'region_id', 'source_model', - \Magento\Customer\Model\ResourceModel\Address\Attribute\Source\Region::class + Address\Attribute\Source\Region::class ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -119,7 +113,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -127,7 +121,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php index 30435ace54d46..8b8092cbb22c6 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateAutocompleteOnStorefrontConfigPath.php @@ -3,17 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; -use Magento\Framework\App\ResourceConnection; +use Magento\Customer\Model\Form; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpdateAutocompleteOnStorefrontCOnfigPath - * @package Magento\Customer\Setup\Patch + * Update storefront's autocomplete of config path */ class UpdateAutocompleteOnStorefrontConfigPath implements DataPatchInterface, PatchVersionInterface { @@ -23,7 +23,6 @@ class UpdateAutocompleteOnStorefrontConfigPath implements DataPatchInterface, Pa private $moduleDataSetup; /** - * UpdateAutocompleteOnStorefrontCOnfigPath constructor. * @param ModuleDataSetupInterface $moduleDataSetup */ public function __construct( @@ -33,19 +32,21 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { $this->moduleDataSetup->getConnection()->update( $this->moduleDataSetup->getTable('core_config_data'), - ['path' => \Magento\Customer\Model\Form::XML_PATH_ENABLE_AUTOCOMPLETE], + ['path' => Form::XML_PATH_ENABLE_AUTOCOMPLETE], ['path = ?' => 'general/restriction/autocomplete_on_storefront'] ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -55,7 +56,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -63,7 +64,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php index 938cd3cd52e73..ff6decb1d2123 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateCustomerAttributeInputFilters.php @@ -3,18 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpdateCustomerAttributeInputFilters - * @package Magento\Customer\Setup\Patch + * Update attribute input filters for customer */ class UpdateCustomerAttributeInputFilters implements DataPatchInterface, PatchVersionInterface { @@ -29,7 +28,6 @@ class UpdateCustomerAttributeInputFilters implements DataPatchInterface, PatchVe private $customerSetupFactory; /** - * UpdateCustomerAttributeInputFilters constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -42,7 +40,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -72,10 +70,12 @@ public function apply() ], ]; $customerSetup->upgradeAttributes($entityAttributes); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -85,7 +85,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -93,7 +93,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateDefaultCustomerGroupInConfig.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateDefaultCustomerGroupInConfig.php new file mode 100644 index 0000000000000..c8159adc2ccff --- /dev/null +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateDefaultCustomerGroupInConfig.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Setup\Patch\Data; + +use Magento\Customer\Model\GroupManagement; +use Magento\Customer\Model\Vat; +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; + +/** + * Update default customer group id in customer configuration if it's value is NULL + */ +class UpdateDefaultCustomerGroupInConfig implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var GroupManagement + */ + private $groupManagement; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param GroupManagement $groupManagement + */ + public function __construct( + ModuleDataSetupInterface $moduleDataSetup, + GroupManagement $groupManagement + ) { + $this->moduleDataSetup = $moduleDataSetup; + $this->groupManagement = $groupManagement; + } + + /** + * @inheritDoc + */ + public function apply() + { + $customerGroups = $this->groupManagement->getLoggedInGroups(); + $commonGroup = array_shift($customerGroups); + + $this->moduleDataSetup->getConnection()->update( + $this->moduleDataSetup->getTable('core_config_data'), + ['value' => $commonGroup->getId()], + [ + 'value is ?' => new \Zend_Db_Expr('NULL'), + 'path = ?' => GroupManagement::XML_PATH_DEFAULT_ID, + ] + ); + + return $this; + } + + /** + * @inheritDoc + */ + public function getAliases() + { + return []; + } + + /** + * @inheritDoc + */ + public static function getDependencies() + { + return [ + DefaultCustomerGroupsAndAttributes::class, + ]; + } +} diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php index 7d0cad768d6b0..8519fab81efc5 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateIdentifierCustomerAttributesVisibility.php @@ -3,18 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpdateIdentifierCustomerAttributesVisibility - * @package Magento\Customer\Setup\Patch + * Update identifier attributes visibility for customer */ class UpdateIdentifierCustomerAttributesVisibility implements DataPatchInterface, PatchVersionInterface { @@ -29,7 +28,6 @@ class UpdateIdentifierCustomerAttributesVisibility implements DataPatchInterface private $customerSetupFactory; /** - * UpdateIdentifierCustomerAttributesVisibility constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -42,7 +40,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -70,10 +68,12 @@ public function apply() ], ]; $customerSetup->upgradeAttributes($entityAttributes); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -83,7 +83,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -91,7 +91,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php b/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php index d31301eedf4b1..ea3207c7ccb85 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpdateVATNumber.php @@ -3,27 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; -use Magento\Customer\Model\Customer; use Magento\Customer\Setup\CustomerSetupFactory; -use Magento\Directory\Model\AllowedCountries; -use Magento\Framework\App\ObjectManager; -use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\Indexer\IndexerRegistry; -use Magento\Framework\Setup\SetupInterface; -use Magento\Framework\Setup\UpgradeDataInterface; -use Magento\Framework\Setup\ModuleContextInterface; use Magento\Framework\Setup\ModuleDataSetupInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Store\Model\StoreManagerInterface; -use Magento\Framework\DB\FieldDataConverterFactory; -use Magento\Framework\DB\DataConverter\SerializedToJson; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; +/** + * Upgrade vat number + */ class UpdateVATNumber implements DataPatchInterface, PatchVersionInterface { /** @@ -37,7 +28,6 @@ class UpdateVATNumber implements DataPatchInterface, PatchVersionInterface private $customerSetupFactory; /** - * UpdateVATNumber constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -50,16 +40,23 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { $customerSetup = $this->customerSetupFactory->create(['resourceConnection' => $this->moduleDataSetup]); - $customerSetup->updateAttribute('customer_address', 'vat_id', 'frontend_label', 'VAT Number'); + $customerSetup->updateAttribute( + 'customer_address', + 'vat_id', + 'frontend_label', + 'VAT Number' + ); + + return $this; } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -69,7 +66,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -77,7 +74,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php b/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php index 3b8f96a037343..5d6e490bead22 100644 --- a/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php +++ b/app/code/Magento/Customer/Setup/Patch/Data/UpgradePasswordHashAndAddress.php @@ -3,19 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Customer\Setup\Patch\Data; use Magento\Customer\Setup\CustomerSetupFactory; use Magento\Framework\Encryption\Encryptor; -use Magento\Framework\App\ResourceConnection; use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\DataPatchInterface; use Magento\Framework\Setup\Patch\PatchVersionInterface; /** - * Class UpgradePasswordHashAndAddress - * @package Magento\Customer\Setup\Patch + * Update passwordHash and address */ class UpgradePasswordHashAndAddress implements DataPatchInterface, PatchVersionInterface { @@ -30,7 +29,6 @@ class UpgradePasswordHashAndAddress implements DataPatchInterface, PatchVersionI private $customerSetupFactory; /** - * UpgradePasswordHashAndAddress constructor. * @param ModuleDataSetupInterface $moduleDataSetup * @param CustomerSetupFactory $customerSetupFactory */ @@ -43,7 +41,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function apply() { @@ -58,9 +56,13 @@ public function apply() ]; $customerSetup = $this->customerSetupFactory->create(['setup' => $this->moduleDataSetup]); $customerSetup->upgradeAttributes($entityAttributes); + + return $this; } /** + * Password hash upgrade + * * @return void */ private function upgradeHash() @@ -93,7 +95,7 @@ private function upgradeHash() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { @@ -103,7 +105,7 @@ public static function getDependencies() } /** - * {@inheritdoc} + * @inheritdoc */ public static function getVersion() { @@ -111,7 +113,7 @@ public static function getVersion() } /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersNowOnlineGridActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersNowOnlineGridActionGroup.xml new file mode 100644 index 0000000000000..05ccc6e67c697 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminAssertCustomerInCustomersNowOnlineGridActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!--Assert company in customers now online grid row --> + <actionGroup name="AdminAssertCustomerInCustomersNowOnlineGridActionGroup"> + <annotations> + <description>Validates that the provided Customer is present and correct in the Customer now online grid page.</description> + </annotations> + <arguments> + <argument name="text" type="string"/> + <argument name="columnName" type="string"/> + </arguments> + <waitForText selector="{{AdminCustomersNowOnlineGridSection.gridCell('1', columnName)}}" userInput="{{text}}" stepKey="waitForCustomesrNowOnline"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml new file mode 100644 index 0000000000000..b827cba8490b8 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerDeleteWishlistItemActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCustomerDeleteWishlistItemActionGroup"> + <click selector="{{AdminCustomerWishlistSection.deleteButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForResultsLoading"/> + <click selector="{{AdminCustomerWishlistSection.deleteConfirm}}" stepKey="confirmDeleting"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml new file mode 100644 index 0000000000000..bbdc4de330840 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminCustomerFindWishlistItemActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCustomerFindWishlistItemActionGroup"> + <arguments> + <argument name="productName" type="string"/> + </arguments> + <fillField userInput="{{productName}}" selector="{{AdminCustomerWishlistSection.productName}}" stepKey="fillProductNameField"/> + <click selector="{{AdminCustomerWishlistSection.searchButton}}" stepKey="clickSearchButton"/> + <waitForPageLoad stepKey="waitForGridLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml new file mode 100644 index 0000000000000..66b464006aa0f --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateCustomerWishlistTabActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminNavigateCustomerWishlistTabActionGroup"> + <click selector="{{AdminCustomerInformationSection.wishList}}" stepKey="clickWishlistButton"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateToCustomerOnlinePageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateToCustomerOnlinePageActionGroup.xml new file mode 100644 index 0000000000000..dd048c2636cd4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AdminNavigateToCustomerOnlinePageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminNavigateToCustomerOnlinePageActionGroup"> + <annotations> + <description>Goes to the Admin Customer Online page.</description> + </annotations> + + <amOnPage url="{{AdminCustomersNowOnlinePage.url}}" stepKey="openCustomersOnlineGridPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Backup/Test/Mftf/ActionGroup/deleteBackupActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml similarity index 60% rename from app/code/Magento/Backup/Test/Mftf/ActionGroup/deleteBackupActionGroup.xml rename to app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml index b879a2aa9647a..16688be61171e 100644 --- a/app/code/Magento/Backup/Test/Mftf/ActionGroup/deleteBackupActionGroup.xml +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/AssertAdminCustomerNoItemsInWishlistActionGroup.xml @@ -8,5 +8,7 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="deleteBackup" extends="AdminBackupDeleteActionGroup" deprecated="Use DeleteBackupActionGroup"/> + <actionGroup name="AssertAdminCustomerNoItemsInWishlistActionGroup"> + <see userInput="No Items Found" selector="{{AdminCustomerWishlistSection.gridTable}}" stepKey="assertNoItems"/> + </actionGroup> </actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml new file mode 100644 index 0000000000000..f287c728bb28d --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/CreateCustomerOrderActionGroup.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="CreateCustomerOrderActionGroup"> + <annotations> + <description>Create Order via API assigned to Customer.</description> + </annotations> + + <createData entity="CustomerCart" stepKey="CustomerCart"> + <requiredEntity createDataKey="Customer"/> + </createData> + + <createData entity="CustomerCartItem" stepKey="addCartItem"> + <requiredEntity createDataKey="CustomerCart"/> + <requiredEntity createDataKey="Product"/> + </createData> + + <createData entity="CustomerAddressInformation" stepKey="addCustomerOrderAddress"> + <requiredEntity createDataKey="CustomerCart"/> + </createData> + + <updateData createDataKey="CustomerCart" entity="CustomerOrderPaymentMethod" stepKey="sendCustomerPaymentInformation"> + <requiredEntity createDataKey="CustomerCart"/> + </updateData> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml new file mode 100644 index 0000000000000..5dafe59bf3c48 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup"> + <arguments> + <argument name="itemName" type="string"/> + </arguments> + <dontSee userInput="{{itemName}}" selector="{{StorefrontCustomerSidebarSection.sidebarTab(itemName)}}" stepKey="dontSeeElement"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertOnCustomerLoginPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertOnCustomerLoginPageActionGroup.xml new file mode 100644 index 0000000000000..830d021380b16 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontAssertOnCustomerLoginPageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertOnCustomerLoginPageActionGroup"> + <annotations> + <description>Assert on the Storefront Customer Sign-In page.</description> + </annotations> + + <seeInCurrentUrl url="{{StorefrontCustomerSignInPage.url}}" stepKey="seeOnSignInPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontGoToCustomerOrderDetailsPageActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontGoToCustomerOrderDetailsPageActionGroup.xml new file mode 100644 index 0000000000000..47d6fe1ed204e --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontGoToCustomerOrderDetailsPageActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontGoToCustomerOrderDetailsPageActionGroup"> + <annotations> + <description>Navigate to storefront order details page</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + <argument name="orderNumber" type="string"/> + </arguments> + <amOnPage url="{{StorefrontCustomerOrderViewPage.url(orderId)}}" stepKey="goToOrdersPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForText selector="{{StorefrontCustomerAccountMainSection.pageTitle}}" userInput="{{orderNumber}}" stepKey="verifyOrderNo"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerOrderDataActionGroup.xml b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerOrderDataActionGroup.xml new file mode 100644 index 0000000000000..81ce5dbe69980 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/ActionGroup/StorefrontVerifyCustomerOrderDataActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontVerifyCustomerOrderDataActionGroup"> + <annotations> + <description>Verify a customer's order details on the view order page on the storefront</description> + </annotations> + <arguments> + <argument name="createdDate" type="string"/> + <argument name="productName" type="string"/> + <argument name="grandTotal" type="string"/> + <argument name="orderPlacedBy" type="string"/> + <argument name="paymentMethod" type="string"/> + </arguments> + <waitForText selector="{{StorefrontCustomerOrderViewSection.paymentMethod}}" userInput="{{paymentMethod}}" stepKey="storefrontVerifyPaymentMethod"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.createdDate}}" userInput="{{createdDate}}" stepKey="storefrontVerifyOrderCreatedDate"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.orderPlacedBy}}" userInput="{{orderPlacedBy}}" stepKey="storefrontVerifyOrderPlacedBy"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.productName}}" userInput="{{productName}}" stepKey="storefrontVerifyProductName"/> + <waitForText selector="{{StorefrontCustomerOrderViewSection.grandTotal}}" userInput="{{grandTotal}}" stepKey="storefrontVerifyGrandTotal"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml index e31be78185aaf..695fc138e592b 100644 --- a/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml +++ b/app/code/Magento/Customer/Test/Mftf/Data/AddressData.xml @@ -19,7 +19,7 @@ <item>Bld D</item> </array> <data key="company">Magento</data> - <data key="telephone">1234568910</data> + <data key="telephone">123-456-7890</data> <data key="fax">1234568910</data> <data key="postcode">78729</data> <data key="city">Austin</data> @@ -172,7 +172,7 @@ <data key="city">London</data> <data key="postcode">SE1 7RW</data> <data key="country_id">GB</data> - <data key="telephone">444-44-444-44</data> + <data key="telephone">444-444-4444</data> </entity> <entity name="US_Address_Utah" type="address"> <data key="firstname">John</data> @@ -227,7 +227,7 @@ <data key="firstname">John</data> <data key="lastname">Doe</data> <data key="company">Magento</data> - <data key="telephone">0123456789-02134567</data> + <data key="telephone">888-777-7890</data> <array key="street"> <item>172, Westminster Bridge Rd</item> <item>7700 xyz street</item> @@ -305,7 +305,7 @@ <data key="firstname">Jane</data> <data key="lastname">Miller</data> <data key="company">Magento</data> - <data key="telephone">44 20 7123 1234</data> + <data key="telephone">123-456-7899</data> <array key="street"> <item>1 London Bridge Street</item> </array> diff --git a/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml b/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml index 06c7b74aef002..d9f738e9d8235 100644 --- a/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml +++ b/app/code/Magento/Customer/Test/Mftf/Metadata/CustomerExtensionAttributeMeta.xml @@ -9,10 +9,12 @@ <operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> <operation name="CreateCustomerExtensionAttribute" dataType="customer_extension_attribute" type="create"> + <field key="assistance_allowed">integer</field> <field key="is_subscribed">boolean</field> <field key="extension_attribute">customer_nested_extension_attribute</field> </operation> <operation name="UpdateCustomerExtensionAttribute" dataType="customer_extension_attribute" type="update"> + <field key="assistance_allowed">integer</field> <field key="is_subscribed">boolean</field> <field key="extension_attribute">customer_nested_extension_attribute</field> </operation> diff --git a/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomersNowOnlinePage.xml b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomersNowOnlinePage.xml new file mode 100644 index 0000000000000..158ffabd7dd24 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Page/AdminCustomersNowOnlinePage.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="AdminCustomersNowOnlinePage" url="/customer/online/" area="admin" module="Magento_Customer"> + <section name="AdminCustomersNowOnlineGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml new file mode 100644 index 0000000000000..39a67968c66e4 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomerWishlistSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerWishlistSection"> + <element name="productName" type="input" selector="#wishlistGrid_filter_product_name"/> + <element name="searchButton" type="button" selector="#wishlistGrid button[data-action='grid-filter-apply']"/> + <element name="deleteButton" type="text" selector="//*[@id='wishlistGrid_table']//*[@data-column='action']//*[text()='Delete']"/> + <element name="deleteConfirm" type="button" selector=".modal-popup.confirm .action-primary.action-accept"/> + <element name="gridTable" type="text" selector="#wishlistGrid_table"/> + </section> +</sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomersNowOnlineGridSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomersNowOnlineGridSection.xml new file mode 100644 index 0000000000000..a4b67c950fe7b --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Section/AdminCustomersNowOnlineGridSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomersNowOnlineGridSection"> + <element name="columnTitle" type="text" selector=".admin__action-dropdown-menu-content label[title='{{column}}']" parameterized="true"/> + <element name="gridCell" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., '{{column}}')]/preceding-sibling::th) +1 ]" parameterized="true"/> + </section> +</sections> + diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml index ec5141d84b1bd..61ce050aa3ef2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderSection.xml @@ -17,5 +17,9 @@ <element name="viewOrder" type="button" selector="//td[contains(concat(' ',normalize-space(@class),' '),' col actions ')]/a[contains(concat(' ',normalize-space(@class),' '),' action view ')]"/> <element name="tabRefund" type="button" selector="//a[text()='Refunds']"/> <element name="grandTotalRefund" type="text" selector="td[data-th='Grand Total'] > strong > span.price"/> + <element name="currentPage" type="text" selector=".order-products-toolbar .pages .current span:nth-of-type(2)"/> + <element name="pageNumber" type="text" selector="//*[@class='order-products-toolbar toolbar bottom']//a[contains(@class, 'page')]//span[2][contains(text() ,'{{var1}}')]" parameterized="true"/> + <element name="perPage" type="select" selector="//*[@class='order-products-toolbar toolbar bottom']//select[@id='limiter']"/> + <element name="rowsInColumn" type="text" selector="//tbody/tr/td[contains(@class, '{{column}}')]" parameterized="true"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml index 09b79fe831188..42c6f5cea082f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml +++ b/app/code/Magento/Customer/Test/Mftf/Section/StorefrontCustomerOrderViewSection.xml @@ -18,5 +18,9 @@ <element name="billingAddress" type="text" selector=".box.box-order-billing-address"/> <element name="orderStatusInGrid" type="text" selector="//td[contains(.,'{{orderId}}')]/../td[contains(.,'{{status}}')]" parameterized="true"/> <element name="pager" type="block" selector=".pager"/> + <element name="createdDate" type="text" selector=".block-order-details-comments .comment-date"/> + <element name="orderPlacedBy" type="text" selector=".block-order-details-comments .comment-content"/> + <element name="productName" type="text" selector="//td[@data-th='Product Name']"/> + <element name="grandTotal" type="text" selector="//tr[@class='grand_total']//td[@data-th='Grand Total']"/> </section> </sections> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml index ab5e332aeed64..6aec5440193a2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsNoTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml index 0bf221d49ab74..d48fb90b24ec2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCheckDefaultValueDisableAutoGroupChangeIsYesTest.xml @@ -20,12 +20,16 @@ </annotations> <before> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 1" stepKey="setConfigDefaultIsYes"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> </before> <after> <magentoCLI command="config:set customer/create_account/viv_disable_auto_group_assign_default 0" stepKey="setConfigDefaultIsNo"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml index 3488d2c94dd69..c8e3bc10cc769 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerRetailerWithoutAddressTest.xml @@ -29,8 +29,7 @@ </after> <!--Filter the customer From grid--> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad time="30" stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Retailer" stepKey="fillCustomerGroup"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml index 64e8520323184..cb003ed837294 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerTest.xml @@ -20,7 +20,9 @@ <group value="create"/> </annotations> <before> - <magentoCLI command="indexer:reindex customer_grid" stepKey="reindexCustomerGrid"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexCustomerGrid"> + <argument name="indices" value="customer_grid"/> + </actionGroup> </before> <after> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -35,7 +37,9 @@ <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <reloadPage stepKey="reloadPage"/> <waitForPageLoad stepKey="waitForLoad2"/> <click selector="{{AdminCustomerFiltersSection.filtersButton}}" stepKey="openFilter"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml index 52a2483096aaf..8afd1648d26e0 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithCustomGroupTest.xml @@ -31,15 +31,16 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="$$customerGroup.code$$" stepKey="fillCustomerGroup"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> <seeElement selector="{{AdminCustomerMessagesSection.successMessage}}" stepKey="assertSuccessMessage"/> - <magentoCLI stepKey="flushMagentoCache" command="cache:flush" /> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <reloadPage stepKey="reloadPage"/> <!--Verify Customer in grid --> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml index 591cb2dd2845a..e9250be637534 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithPrefixTest.xml @@ -29,8 +29,7 @@ </after> <!--Open New Customer Page and create a customer with Prefix and Suffix--> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <selectOption selector="{{AdminCustomerAccountInformationSection.group}}" userInput="Wholesale" stepKey="fillCustomerGroup"/> <fillField selector="{{AdminCustomerAccountInformationSection.namePrefix}}" userInput="{{CustomerEntityOne.prefix}}" stepKey="fillNamePrefix"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml index 081695f7ebe1e..5033f2882af42 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateCustomerWithoutAddressTest.xml @@ -29,8 +29,7 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="waitToCustomerPageLoad"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml index da25139ee8e60..6b484e857d276 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminCreateNewCustomerTest.xml @@ -29,8 +29,7 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="waitToCustomerPageLoad"/> <fillField userInput="{{CustomerEntityOne.firstname}}" selector="{{AdminCustomerAccountInformationSection.firstName}}" stepKey="fillFirstName"/> <fillField userInput="{{CustomerEntityOne.lastname}}" selector="{{AdminCustomerAccountInformationSection.lastName}}" stepKey="fillLastName"/> <fillField userInput="{{CustomerEntityOne.email}}" selector="{{AdminCustomerAccountInformationSection.email}}" stepKey="fillEmail"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml index 8494a94f0c122..615a6ebcf24cc 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml index 340295df04da2..57446a1ee0c72 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteCustomerAddressesFromTheGridViaMassActionsTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml index 1630743da4922..f08ea83a70da6 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminDeleteDefaultBillingCustomerAddressTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml index 5721c46d5e4b9..257d4c9b2e3c2 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminResetCustomerPasswordTest.xml @@ -25,8 +25,12 @@ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Edit customer info--> <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> <argument name="customer" value="$$customer$$"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml index 10da9284d45dc..b13a06b9ef858 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminSearchCustomerAddressByKeywordTest.xml @@ -20,7 +20,9 @@ <before> <createData entity="Simple_US_Customer_Multiple_Addresses" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml index c9805ebcc90ed..3f95e55c56132 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminDeleteCustomerAddressTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <createData stepKey="customer" entity="Simple_US_Customer_Multiple_Addresses"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml index 0d550416167aa..8af07bc2c2d53 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminUpdateCustomerTest/AdminUpdateCustomerInfoFromDefaultToNonDefaultTest.xml @@ -20,7 +20,9 @@ </annotations> <before> <createData stepKey="customer" entity="Simple_Customer_Without_Address"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> </before> <after> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml index 72064617ef33b..78a2fe721453f 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AdminVerifyCreateCustomerRequiredFieldsTest.xml @@ -26,8 +26,7 @@ </after> <!--Open New Customer Page --> - <amOnPage url="{{AdminNewCustomerPage.url}}" stepKey="navigateToNewCustomerPage"/> - <waitForPageLoad stepKey="waitToCustomerPageLoad"/> + <actionGroup ref="AdminNavigateNewCustomerActionGroup" stepKey="navigateToNewCustomerPage"/> <click selector="{{AdminCustomerMainActionsSection.saveButton}}" stepKey="saveCustomer"/> <waitForPageLoad stepKey="waitForPageToLoad"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml index 60caaf64f05b7..781d721fd5132 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/AllowedCountriesRestrictionApplyOnBackendTest.xml @@ -26,7 +26,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <!--Create new website,store and store view--> <comment userInput="Create new website,store and store view" stepKey="createWebsite"/> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="goToAdminSystemStorePage"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="goToAdminSystemStorePage"/> <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="adminCreateNewWebsite"> <argument name="newWebsiteName" value="{{NewWebSiteData.name}}"/> <argument name="websiteCode" value="{{NewWebSiteData.code}}"/> @@ -42,7 +42,9 @@ <!--Set account sharing option - Default value is 'Per Website'--> <comment userInput="Set account sharing option - Default value is 'Per Website'" stepKey="setAccountSharingOption"/> <createData entity="CustomerAccountSharingDefault" stepKey="setToAccountSharingToDefault"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!--delete all created data and set main website country options to default--> @@ -60,8 +62,12 @@ <actionGroup ref="SetWebsiteCountryOptionsToDefaultActionGroup" stepKey="setCountryOptionsToDefault"/> <createData entity="CustomerAccountSharingSystemValue" stepKey="setAccountSharingToSystemValue"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Check that all countries are allowed initially and get amount--> <comment userInput="Check that all countries are allowed initially and get amount" stepKey="checkAllCountriesAreAllowed"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml index 80cdeadb391da..5a75d5d272295 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCheckTaxAddingValidVATIdTest.xml @@ -83,9 +83,7 @@ <see userInput="$$createProduct.name$$" selector="{{StorefrontProductInfoMainSection.productName}}" stepKey="assertFirstProductNameTitle"/> <!--Add a product to the cart--> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddProductToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!--Proceed to checkout--> <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="GoToCheckoutFromMinicartActionGroup"/> <!-- Click next button to open payment section --> @@ -103,8 +101,7 @@ <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="{{TaxRule.name}}"/> @@ -117,8 +114,7 @@ </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml index 317f2c2825ca7..81208da18373c 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontClearAllCompareProductsTest.xml @@ -102,8 +102,12 @@ <requiredEntity createDataKey="createDownloadableProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Login --> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml index 8cd35f4147636..47b61b332f571 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCreateCustomerWithDateOfBirthTest.xml @@ -35,7 +35,9 @@ <argument name="Customer" value="CustomerEntityOne"/> <argument name="dob" value="{{EN_US_DATE.short4DigitYear}}"/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <actionGroup ref="DeleteCustomerByEmailActionGroup" stepKey="deleteNewUser"> <argument name="email" value="{{CustomerEntityOne.email}}"/> </actionGroup> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml new file mode 100644 index 0000000000000..ba113c739d706 --- /dev/null +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontCustomerAccountOrderListTest.xml @@ -0,0 +1,144 @@ +<?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="StorefrontCustomerAccountOrderListTest"> + <annotations> + <stories value="Frontend Customer Account Orders list"/> + <title value="Verify that the list of Orders is displayed in the grid after changing the number of items on the page"/> + <description value="Verify that the list of Orders is displayed in the grid after changing the number of items on the page."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-34953"/> + <group value="customer"/> + </annotations> + + <before> + + <!--Create Product via API--> + <createData entity="SimpleProduct2" stepKey="Product"/> + + <!--Create Customer via API--> + <createData entity="Simple_US_Customer" stepKey="Customer"/> + + <!--Create Orders via API--> + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder1"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder2"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder3"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder4"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder5"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder6"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder7"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder8"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder9"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder10"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder11"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder12"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder13"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder14"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + + <actionGroup ref="CreateCustomerOrderActionGroup" stepKey="createCustomerOrder15"> + <argument name="Customer" value="Customer"/> + <argument name="Product" value="Product"/> + </actionGroup> + <!--Create Orders via API--> + + </before> + + <after> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogout"/> + <deleteData createDataKey="Product" stepKey="deleteProduct"/> + <deleteData createDataKey="Customer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefront"> + <argument name="Customer" value="$$Customer$$"/> + </actionGroup> + + <actionGroup ref="StorefrontCustomerGoToSidebarMenu" stepKey="goToSidebarMenu"> + <argument name="menu" value="My Orders"/> + </actionGroup> + + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" stepKey="waitOrderHistoryPage"/> + + <scrollTo selector="{{StorefrontCustomerOrderSection.currentPage}}" stepKey="scrollToBottomToolbarSection"/> + + <click selector="{{StorefrontCustomerOrderSection.pageNumber('2')}}" stepKey="clickOnPage2"/> + + <scrollTo selector="{{StorefrontCustomerOrderSection.perPage}}" stepKey="scrollToLimiter"/> + + <selectOption userInput="20" selector="{{StorefrontCustomerOrderSection.perPage}}" stepKey="selectLimitOnPage"/> + + <waitForPageLoad stepKey="waitForLoadPage"/> + + <seeElement selector="{{StorefrontCustomerOrderSection.isMyOrdersSection}}" + stepKey="seeElementOrderHistoryPage"/> + + <dontSee selector="{{StorefrontOrderInformationMainSection.emptyMessage}}" + userInput="You have placed no orders." stepKey="dontSeeEmptyMessage"/> + + <seeNumberOfElements selector="{{StorefrontCustomerOrderSection.rowsInColumn('id')}}" userInput="15" + stepKey="seeRowsCount"/> + + </test> +</tests> diff --git a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml index f504af2334e10..410070234b9c0 100644 --- a/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml +++ b/app/code/Magento/Customer/Test/Mftf/Test/StorefrontVerifySecureURLRedirectCustomerTest.xml @@ -25,11 +25,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <executeJS function="return window.location.host" stepKey="hostname"/> <amOnUrl url="http://{$hostname}/customer" stepKey="goToUnsecureCustomerURL"/> diff --git a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php index c63395ed501a9..a0a8718319bf5 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/AccountManagementTest.php @@ -12,6 +12,7 @@ use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Api\SessionCleanerInterface; use Magento\Customer\Api\Data\ValidationResultsInterfaceFactory; use Magento\Customer\Helper\View; use Magento\Customer\Model\AccountConfirmation; @@ -203,6 +204,11 @@ class AccountManagementTest extends TestCase */ private $allowedCountriesReader; + /** + * @var SessionCleanerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + private $sessionCleanerMock; + /** * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -1519,6 +1525,7 @@ private function reInitModel() ->setMethods(['create']) ->getMock(); $dateTimeFactory->expects($this->any())->method('create')->willReturn($dateTimeMock); + $this->sessionCleanerMock = $this->createMock(SessionCleanerInterface::class); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->accountManagement = $this->objectManagerHelper->getObject( @@ -1539,6 +1546,7 @@ private function reInitModel() 'storeManager' => $this->storeManager, 'addressRegistry' => $this->addressRegistryMock, 'transportBuilder' => $this->transportBuilder, + 'sessionCleaner' => $this->sessionCleanerMock, ] ); $this->objectManagerHelper->setBackwardCompatibleProperty( @@ -1617,35 +1625,13 @@ public function testChangePassword() ->with($newPassword) ->willReturn(7); + $this->sessionCleanerMock->expects($this->once())->method('clearFor')->with($customerId)->willReturnSelf(); + $this->customerRepository ->expects($this->once()) ->method('save') ->with($customer); - $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); - $this->sessionManager->expects($this->atLeastOnce())->method('regenerateId'); - - $visitor = $this->getMockBuilder(Visitor::class) - ->disableOriginalConstructor() - ->setMethods(['getSessionId']) - ->getMock(); - $visitor->expects($this->atLeastOnce())->method('getSessionId') - ->willReturnOnConsecutiveCalls('session_id_1', 'session_id_2'); - $visitorCollection = $this->getMockBuilder( - CollectionFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['addFieldToFilter', 'getItems'])->getMock(); - $visitorCollection->expects($this->atLeastOnce())->method('addFieldToFilter')->willReturnSelf(); - $visitorCollection->expects($this->atLeastOnce())->method('getItems')->willReturn([$visitor, $visitor]); - $this->visitorCollectionFactory->expects($this->atLeastOnce())->method('create') - ->willReturn($visitorCollection); - $this->saveHandler->expects($this->atLeastOnce())->method('destroy') - ->withConsecutive( - ['session_id_1'], - ['session_id_2'] - ); - $this->assertTrue($this->accountManagement->changePassword($email, $currentPassword, $newPassword)); } @@ -1702,30 +1688,8 @@ function ($string) { $this->customerSecure->expects($this->once())->method('setRpToken')->with(null); $this->customerSecure->expects($this->once())->method('setRpTokenCreatedAt')->with(null); $this->customerSecure->expects($this->any())->method('setPasswordHash')->willReturn(null); + $this->sessionCleanerMock->expects($this->once())->method('clearFor')->with($customerId)->willReturnSelf(); - $this->sessionManager->method('isSessionExists')->willReturn(false); - $this->sessionManager->expects($this->atLeastOnce())->method('getSessionId'); - $this->sessionManager->expects($this->atLeastOnce())->method('regenerateId'); - $visitor = $this->getMockBuilder(Visitor::class) - ->disableOriginalConstructor() - ->setMethods(['getSessionId']) - ->getMock(); - $visitor->expects($this->atLeastOnce())->method('getSessionId') - ->willReturnOnConsecutiveCalls('session_id_1', 'session_id_2'); - $visitorCollection = $this->getMockBuilder( - CollectionFactory::class - ) - ->disableOriginalConstructor() - ->setMethods(['addFieldToFilter', 'getItems'])->getMock(); - $visitorCollection->expects($this->atLeastOnce())->method('addFieldToFilter')->willReturnSelf(); - $visitorCollection->expects($this->atLeastOnce())->method('getItems')->willReturn([$visitor, $visitor]); - $this->visitorCollectionFactory->expects($this->atLeastOnce())->method('create') - ->willReturn($visitorCollection); - $this->saveHandler->expects($this->atLeastOnce())->method('destroy') - ->withConsecutive( - ['session_id_1'], - ['session_id_2'] - ); $this->assertTrue($this->accountManagement->resetPassword($customerEmail, $resetToken, $newPassword)); } diff --git a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php index ec154e2c657b1..8017c367c081d 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Metadata/Form/ImageTest.php @@ -12,18 +12,26 @@ use Magento\Customer\Api\Data\ValidationRuleInterface; use Magento\Customer\Model\FileProcessor; use Magento\Customer\Model\FileProcessorFactory; -use Magento\Customer\Model\Metadata\Form\File; use Magento\Customer\Model\Metadata\Form\Image; use Magento\Framework\Api\Data\ImageContentInterface; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; +use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\Request\Http; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\UploaderFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteFactory; +use Magento\Framework\Filesystem\Directory\Write; +use Magento\Framework\Filesystem\Driver\File as Driver; +use Magento\Framework\Filesystem\Io\File; use Magento\Framework\Url\EncoderInterface; use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; use PHPUnit\Framework\MockObject\MockObject; /** + * Tests Metadata/Form/Image class + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ImageTest extends AbstractFormTestCase @@ -68,6 +76,34 @@ class ImageTest extends AbstractFormTestCase */ private $fileProcessorFactoryMock; + /** + * @var File|\PHPUnit\Framework\MockObject\MockObject + */ + private $ioFileSystemMock; + + /** + * @var DirectoryList|\PHPUnit\Framework\MockObject\MockObject + */ + private $directoryListMock; + + /** + * @var WriteFactory|\PHPUnit\Framework\MockObject\MockObject + */ + private $writeFactoryMock; + + /** + * @var Write|\PHPUnit\Framework\MockObject\MockObject + */ + private $mediaEntityTmpDirectoryMock; + + /** + * @var Driver|\PHPUnit\Framework\MockObject\MockObject + */ + private $driverMock; + + /** + * @inheritdoc + */ protected function setUp(): void { parent::setUp(); @@ -101,13 +137,38 @@ protected function setUp(): void $this->fileProcessorFactoryMock->expects($this->any()) ->method('create') ->willReturn($this->fileProcessorMock); + $this->ioFileSystemMock = $this->getMockBuilder(File::class) + ->disableOriginalConstructor() + ->getMock(); + $this->directoryListMock = $this->getMockBuilder(DirectoryList::class) + ->disableOriginalConstructor() + ->getMock(); + $this->writeFactoryMock = $this->getMockBuilder(WriteFactory::class) + ->setMethods(['create']) + ->disableOriginalConstructor() + ->getMock(); + $this->mediaEntityTmpDirectoryMock = $this->getMockBuilder(Write::class) + ->disableOriginalConstructor() + ->getMock(); + $this->driverMock = $this->getMockBuilder(Driver::class) + ->disableOriginalConstructor() + ->getMock(); + $this->writeFactoryMock->expects($this->any()) + ->method('create') + ->willReturn($this->mediaEntityTmpDirectoryMock); + $this->mediaEntityTmpDirectoryMock->expects($this->any()) + ->method('getDriver') + ->willReturn($this->driverMock); } /** + * Initializes an image instance + * * @param array $data - * @return File + * @return Image + * @throws FileSystemException */ - private function initialize(array $data) + private function initialize(array $data): Image { return new Image( $this->localeMock, @@ -122,10 +183,17 @@ private function initialize(array $data) $this->fileSystemMock, $this->uploaderFactoryMock, $this->fileProcessorFactoryMock, - $this->imageContentFactory + $this->imageContentFactory, + $this->ioFileSystemMock, + $this->directoryListMock, + $this->writeFactoryMock ); } + /** + * Test for validateValue method for not valid file + * @throws LocalizedException + */ public function testValidateIsNotValidFile() { $value = [ @@ -151,6 +219,10 @@ public function testValidateIsNotValidFile() $this->assertEquals(['"realFileName" is not a valid file.'], $model->validateValue($value)); } + /** + * Test for validateValue method + * @throws LocalizedException + */ public function testValidate() { $value = [ @@ -158,6 +230,14 @@ public function testValidate() 'name' => 'logo.gif', ]; + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -167,6 +247,11 @@ public function testValidate() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -176,6 +261,10 @@ public function testValidate() $this->assertTrue($model->validateValue($value)); } + /** + * Test for validateValue method for max file size + * @throws LocalizedException + */ public function testValidateMaxFileSize() { $value = [ @@ -196,6 +285,14 @@ public function testValidateMaxFileSize() ->method('getValue') ->willReturn($maxFileSize); + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -208,6 +305,11 @@ public function testValidateMaxFileSize() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -217,6 +319,10 @@ public function testValidateMaxFileSize() $this->assertEquals(['"logo.gif" exceeds the allowed file size.'], $model->validateValue($value)); } + /** + * Test for validateValue method for max image width + * @throws LocalizedException + */ public function testValidateMaxImageWidth() { $value = [ @@ -236,6 +342,14 @@ public function testValidateMaxImageWidth() ->method('getValue') ->willReturn($maxImageWidth); + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -248,6 +362,11 @@ public function testValidateMaxImageWidth() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -257,6 +376,10 @@ public function testValidateMaxImageWidth() $this->assertEquals(['"logo.gif" width exceeds allowed value of 1 px.'], $model->validateValue($value)); } + /** + * Test for validateValue method for max image height + * @throws LocalizedException + */ public function testValidateMaxImageHeight() { $value = [ @@ -276,6 +399,14 @@ public function testValidateMaxImageHeight() ->method('getValue') ->willReturn($maxImageHeight); + $this->ioFileSystemMock->expects($this->any()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn([ + 'filename' => 'logo', + 'extension' => 'gif' + ]); + $this->attributeMetadataMock->expects($this->once()) ->method('getStoreLabel') ->willReturn('File Input Field Label'); @@ -288,6 +419,11 @@ public function testValidateMaxImageHeight() ->with(FileProcessor::TMP_DIR . '/' . $value['name']) ->willReturn(true); + $this->ioFileSystemMock->expects($this->once()) + ->method('getPathInfo') + ->with($value['name']) + ->willReturn(['extension' => 'gif']); + $model = $this->initialize([ 'value' => $value, 'isAjax' => false, @@ -297,6 +433,10 @@ public function testValidateMaxImageHeight() $this->assertEquals(['"logo.gif" height exceeds allowed value of 1 px.'], $model->validateValue($value)); } + /** + * Test for compactValue method + * @throws LocalizedException + */ public function testCompactValueNoChanges() { $originValue = 'filename.ext1'; @@ -314,6 +454,10 @@ public function testCompactValueNoChanges() $this->assertEquals($originValue, $model->compactValue($value)); } + /** + * Test for compactValue method for address image + * @throws LocalizedException + */ public function testCompactValueUiComponentAddress() { $originValue = 'filename.ext1'; @@ -322,20 +466,33 @@ public function testCompactValueUiComponentAddress() 'file' => 'filename.ext2', ]; + $this->driverMock->expects($this->once()) + ->method('getRealPathSafety') + ->with($value['file']) + ->willReturn($value['file']); + $this->mediaEntityTmpDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->willReturn($value['file']); + $this->mediaEntityTmpDirectoryMock->expects($this->once()) + ->method('getRelativePath') + ->willReturn($value['file']); $this->fileProcessorMock->expects($this->once()) ->method('moveTemporaryFile') ->with($value['file']) - ->willReturn(true); - + ->willReturn($value['file']); $model = $this->initialize([ 'value' => $originValue, 'isAjax' => false, 'entityTypeCode' => AddressMetadataInterface::ENTITY_TYPE_ADDRESS, ]); - $this->assertTrue($model->compactValue($value)); + $this->assertEquals($value['file'], $model->compactValue($value)); } + /** + * Test for compactValue method for image + * @throws LocalizedException + */ public function testCompactValueUiComponentCustomer() { $originValue = 'filename.ext1'; @@ -348,9 +505,9 @@ public function testCompactValueUiComponentCustomer() $base64EncodedData = 'encoded_data'; - $this->fileProcessorMock->expects($this->once()) + $this->mediaEntityTmpDirectoryMock->expects($this->once()) ->method('isExist') - ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->with($value['file']) ->willReturn(true); $this->fileProcessorMock->expects($this->once()) ->method('getBase64EncodedData') @@ -390,6 +547,10 @@ public function testCompactValueUiComponentCustomer() $this->assertEquals($imageContentMock, $model->compactValue($value)); } + /** + * Test for compactValue method for non-existing customer + * @throws LocalizedException + */ public function testCompactValueUiComponentCustomerNotExists() { $originValue = 'filename.ext1'; @@ -400,9 +561,9 @@ public function testCompactValueUiComponentCustomerNotExists() 'type' => 'image', ]; - $this->fileProcessorMock->expects($this->once()) + $this->mediaEntityTmpDirectoryMock->expects($this->once()) ->method('isExist') - ->with(FileProcessor::TMP_DIR . '/' . $value['file']) + ->with($value['file']) ->willReturn(false); $model = $this->initialize([ diff --git a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php index 807c1084e979d..ae48926f12612 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/Renderer/RegionTest.php @@ -17,9 +17,30 @@ use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; class RegionTest extends TestCase { + /** + * Simulate "serialize" method of a form element. + * + * @param string[] $keys + * @param array $data + * @return string + */ + private function mockSerialize(array $keys, array $data): string + { + $attributes = []; + foreach ($keys as $key) { + if (empty($data[$key])) { + continue; + } + $attributes[] = $key .'="' .$data[$key] .'"'; + } + + return implode(' ', $attributes); + } + /** * @param array $regionCollection * @dataProvider renderDataProvider @@ -34,14 +55,25 @@ public function testRender($regionCollection) ['isRegionRequired'] ); $escaperMock = $this->createMock(Escaper::class); + /** @var MockObject|AbstractElement $elementMock */ $elementMock = $this->createPartialMock( AbstractElement::class, - ['getForm', 'getHtmlAttributes'] + ['getForm', 'getHtmlAttributes', 'serialize'] + ); + $elementMock->method('serialize')->willReturnCallback( + function (array $attributes) use ($elementMock): string { + return $this->mockSerialize($attributes, $elementMock->getData()); + } ); $countryMock = $this->getMockBuilder(AbstractElement::class) - ->addMethods(['getValue']) + ->addMethods(['getValue', 'serialize']) ->disableOriginalConstructor() ->getMockForAbstractClass(); + $countryMock->method('serialize')->willReturnCallback( + function (array $attributes) use ($countryMock): string { + return $this->mockSerialize($attributes, $countryMock->getData()); + } + ); $regionMock = $this->createMock( AbstractElement::class ); diff --git a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php index 7466505d2cca5..b7ed01ee9da8b 100644 --- a/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php +++ b/app/code/Magento/Customer/Test/Unit/Model/ResourceModel/CustomerRepositoryTest.php @@ -249,7 +249,8 @@ public function testSave() 'getEmail', 'getWebsiteId', 'getAddresses', - 'setAddresses' + 'setAddresses', + 'getGroupId', ] ); $customerSecureData = $this->getMockBuilder(CustomerSecure::class) @@ -433,7 +434,8 @@ public function testSaveWithPasswordHash() 'getEmail', 'getWebsiteId', 'getAddresses', - 'setAddresses' + 'setAddresses', + 'getGroupId' ] ); $customerModel->expects($this->atLeastOnce()) diff --git a/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php new file mode 100644 index 0000000000000..d05c10c00e6c3 --- /dev/null +++ b/app/code/Magento/Customer/Test/Unit/Observer/UpgradeOrderCustomerEmailObserverTest.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Test\Unit\Observer; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Observer\UpgradeOrderCustomerEmailObserver; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * For testing upgrade order customer email + */ +class UpgradeOrderCustomerEmailObserverTest extends TestCase +{ + private const NEW_CUSTOMER_EMAIL = "test@test.com"; + private const ORIGINAL_CUSTOMER_EMAIL = "origtest@test.com"; + + /** + * @var UpgradeOrderCustomerEmailObserver + */ + private $orderCustomerEmailObserver; + + /** + * @var Observer|MockObject + */ + private $observerMock; + + /** + * @var OrderRepositoryInterface|MockObject + */ + private $orderRepositoryMock; + + /** + * @var SearchCriteriaBuilder|MockObject + */ + private $searchCriteriaBuilderMock; + + /** + * @var Event|MockObject + */ + private $eventMock; + + /** + * @var ObjectManagerHelper + */ + private $objectManagerHelper; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->orderRepositoryMock = $this->getMockBuilder(OrderRepositoryInterface::class) + ->getMock(); + + $this->searchCriteriaBuilderMock = $this->getMockBuilder(SearchCriteriaBuilder::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->eventMock = $this->getMockBuilder(Event::class) + ->disableOriginalConstructor() + ->setMethods(['getCustomerDataObject', 'getOrigCustomerDataObject']) + ->getMock(); + + $this->observerMock = $this->getMockBuilder(Observer::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->observerMock->expects($this->any())->method('getEvent')->willReturn($this->eventMock); + + $this->objectManagerHelper = new ObjectManagerHelper($this); + + $this->orderCustomerEmailObserver = $this->objectManagerHelper->getObject( + UpgradeOrderCustomerEmailObserver::class, + [ + 'orderRepository' => $this->orderRepositoryMock, + 'searchCriteriaBuilder' => $this->searchCriteriaBuilderMock, + ] + ); + } + + /** + * Verifying that the order email is not updated when the customer email is not updated + * + */ + public function testUpgradeOrderCustomerEmailWhenMailIsNotChanged(): void + { + $customer = $this->createCustomerMock(); + $originalCustomer = $this->createCustomerMock(); + + $this->setCustomerToEventMock($customer); + $this->setOriginalCustomerToEventMock($originalCustomer); + + $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL); + $this->setCustomerEmail($customer, self::ORIGINAL_CUSTOMER_EMAIL); + + $this->whenOrderRepositoryGetListIsNotCalled(); + + $this->orderCustomerEmailObserver->execute($this->observerMock); + } + + /** + * Verifying that the order email is updated after the customer updates their email + * + */ + public function testUpgradeOrderCustomerEmail(): void + { + $customer = $this->createCustomerMock(); + $originalCustomer = $this->createCustomerMock(); + $orderCollectionMock = $this->createOrderMock(); + + $this->setCustomerToEventMock($customer); + $this->setOriginalCustomerToEventMock($originalCustomer); + + $this->setCustomerEmail($originalCustomer, self::ORIGINAL_CUSTOMER_EMAIL); + $this->setCustomerEmail($customer, self::NEW_CUSTOMER_EMAIL); + + $this->whenOrderRepositoryGetListIsCalled($orderCollectionMock); + + $this->whenOrderCollectionSetDataToAllIsCalled($orderCollectionMock); + + $this->whenOrderCollectionSaveIsCalled($orderCollectionMock); + + $this->orderCustomerEmailObserver->execute($this->observerMock); + } + + private function createCustomerMock(): MockObject + { + $customer = $this->getMockBuilder(CustomerInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + return $customer; + } + + private function createOrderMock(): MockObject + { + $orderCollectionMock = $this->getMockBuilder(Collection::class) + ->disableOriginalConstructor() + ->getMock(); + + return $orderCollectionMock; + } + + private function setCustomerToEventMock(MockObject $customer): void + { + $this->eventMock->expects($this->once()) + ->method('getCustomerDataObject') + ->willReturn($customer); + } + + private function setOriginalCustomerToEventMock(MockObject $originalCustomer): void + { + $this->eventMock->expects($this->once()) + ->method('getOrigCustomerDataObject') + ->willReturn($originalCustomer); + } + + private function setCustomerEmail(MockObject $originalCustomer, string $email): void + { + $originalCustomer->expects($this->once()) + ->method('getEmail') + ->willReturn($email); + } + + private function whenOrderRepositoryGetListIsCalled(MockObject $orderCollectionMock): void + { + $searchCriteriaMock = $this->getMockBuilder(SearchCriteria::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('create') + ->willReturn($searchCriteriaMock); + + $this->searchCriteriaBuilderMock->expects($this->once()) + ->method('addFilter') + ->willReturn($this->searchCriteriaBuilderMock); + + $this->orderRepositoryMock->expects($this->once()) + ->method('getList') + ->with($searchCriteriaMock) + ->willReturn($orderCollectionMock); + } + + private function whenOrderCollectionSetDataToAllIsCalled(MockObject $orderCollectionMock): void + { + $orderCollectionMock->expects($this->once()) + ->method('setDataToAll') + ->with(OrderInterface::CUSTOMER_EMAIL, self::NEW_CUSTOMER_EMAIL); + } + + private function whenOrderCollectionSaveIsCalled(MockObject $orderCollectionMock): void + { + $orderCollectionMock->expects($this->once()) + ->method('save'); + } + + private function whenOrderRepositoryGetListIsNotCalled(): void + { + $this->searchCriteriaBuilderMock->expects($this->never()) + ->method('addFilter'); + $this->searchCriteriaBuilderMock->expects($this->never()) + ->method('create'); + + $this->orderRepositoryMock->expects($this->never()) + ->method('getList'); + } +} diff --git a/app/code/Magento/Customer/etc/adminhtml/system.xml b/app/code/Magento/Customer/etc/adminhtml/system.xml index fca625d847a1d..569f9d09c2087 100644 --- a/app/code/Magento/Customer/etc/adminhtml/system.xml +++ b/app/code/Magento/Customer/etc/adminhtml/system.xml @@ -40,6 +40,7 @@ <field id="default_group" translate="label" type="select" sortOrder="20" showInDefault="1" showInWebsite="1" showInStore="1" canRestore="1"> <label>Default Group</label> <source_model>Magento\Customer\Model\Config\Source\Group</source_model> + <validate>required-entry</validate> </field> <field id="viv_domestic_group" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" showInStore="1"> <label>Group for Valid VAT ID - Domestic</label> diff --git a/app/code/Magento/Customer/etc/di.xml b/app/code/Magento/Customer/etc/di.xml index ba0bb3bac6a98..437912d29a334 100644 --- a/app/code/Magento/Customer/etc/di.xml +++ b/app/code/Magento/Customer/etc/di.xml @@ -61,6 +61,7 @@ type="Magento\Customer\Block\Account\SortLink"/> <preference for="Magento\Customer\Model\Group\RetrieverInterface" type="Magento\Customer\Model\Group\Retriever"/> + <preference for="Magento\Customer\Api\SessionCleanerInterface" type="Magento\Customer\Model\Session\SessionCleaner"/> <type name="Magento\Customer\Model\Session"> <arguments> <argument name="configShare" xsi:type="object">Magento\Customer\Model\Config\Share\Proxy</argument> diff --git a/app/code/Magento/Customer/etc/events.xml b/app/code/Magento/Customer/etc/events.xml index 2a724498a0359..0194f91c591f5 100644 --- a/app/code/Magento/Customer/etc/events.xml +++ b/app/code/Magento/Customer/etc/events.xml @@ -16,6 +16,7 @@ <observer name="customer_visitor" instance="Magento\Customer\Observer\Visitor\BindQuoteCreateObserver" /> </event> <event name="customer_save_after_data_object"> + <observer name="upgrade_order_customer_email" instance="Magento\Customer\Observer\UpgradeOrderCustomerEmailObserver"/> <observer name="upgrade_quote_customer_email" instance="Magento\Customer\Observer\UpgradeQuoteCustomerEmailObserver"/> </event> </config> diff --git a/app/code/Magento/Customer/etc/webapi_rest/di.xml b/app/code/Magento/Customer/etc/webapi_rest/di.xml index f2457963a5f3d..a349d07a5e222 100644 --- a/app/code/Magento/Customer/etc/webapi_rest/di.xml +++ b/app/code/Magento/Customer/etc/webapi_rest/di.xml @@ -13,10 +13,13 @@ <arguments> <argument name="userContexts" xsi:type="array"> <item name="customerSessionUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\Customer\Model\Authorization\CustomerSessionUserContext</item> + <item name="type" xsi:type="object">Magento\Customer\Model\Authorization\CustomerSessionUserContext\Proxy</item> <item name="sortOrder" xsi:type="string">20</item> </item> </argument> </arguments> </type> + <type name="Magento\Customer\Api\CustomerRepositoryInterface"> + <plugin name="updateCustomerByIdFromRequest" type="Magento\Customer\Model\Plugin\UpdateCustomer" /> + </type> </config> diff --git a/app/code/Magento/Customer/i18n/en_US.csv b/app/code/Magento/Customer/i18n/en_US.csv index 7c88ffec1170a..0a81e70964b4c 100644 --- a/app/code/Magento/Customer/i18n/en_US.csv +++ b/app/code/Magento/Customer/i18n/en_US.csv @@ -542,3 +542,4 @@ Addresses,Addresses "The store view is not in the associated website.","The store view is not in the associated website." "The Store View selected for sending Welcome email from is not related to the customer's associated website.","The Store View selected for sending Welcome email from is not related to the customer's associated website." "Add/Update Address","Add/Update Address" +"The specified customer group id does not exist.","The specified customer group id does not exist." diff --git a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml index 6525e7f29f36b..f4a3d2db6b687 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/system/config/validatevat.phtml @@ -5,8 +5,16 @@ */ /** @var \Magento\Customer\Block\Adminhtml\System\Config\Validatevat $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php + $merchantCountryField = $block->escapeJs($block->getMerchantCountryField()); + $merchantVatNumberField = $block->escapeJs($block->getMerchantVatNumberField()); + $ajaxUrl = $block->escapeJs($block->getAjaxUrl()); + $errorMessage = $block->escapeJs($block->escapeHtml(__('Error during VAT Number verification.'))); + + $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ @@ -14,21 +22,22 @@ require(['prototype'], function(){ var validationMessage = $('validation_result'); params = { - country: $('<?= $block->escapeJs($block->getMerchantCountryField()) ?>').value, - vat: $('<?= $block->escapeJs($block->getMerchantVatNumberField()) ?>').value + country: $('{$merchantCountryField}').value, + vat: $('{$merchantVatNumberField}').value }; - new Ajax.Request('<?= $block->escapeJs($block->escapeUrl($block->getAjaxUrl())) ?>', { + new Ajax.Request('{$ajaxUrl}', { parameters: params, onSuccess: function(response) { - var result = '<?= $block->escapeJs($block->escapeHtml(__('Error during VAT Number verification.'))) ?>'; + var result = '{$errorMessage}'; try { if (response.responseText.isJSON()) { response = response.responseText.evalJSON(); result = response.message; } if (response.valid == 1) { - validationMessage.removeClassName('hidden').removeClassName('admin__field-error').addClassName('note'); + validationMessage.removeClassName('hidden').removeClassName('admin__field-error'). + addClassName('note'); validationMessage.setStyle({color:'green'}); } else { validationMessage.removeClassName('hidden').addClassName('admin__field-error'); @@ -45,11 +54,19 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> + <div class="actions actions-validate-vat"> - <p class="admin__field-error hidden" id="validation_result" style="margin-bottom:10px;"></p> - <button onclick="javascript:validateVat(); return false;" class="action-validate-vat" type="button" id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>"> + <p class="admin__field-error hidden" id="validation_result"></p> + <button class="action-validate-vat" type="button" id="<?= /* @noEscape */ $block->getHtmlId() ?>"> <span><?= $block->escapeHtml($block->getButtonLabel()) ?></span> </button> </div> - +<?= /* @noEscape */ $secureRenderer->renderTag('style', [], '#validation_result {margin-bottom: 10px;}', false); ?> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'validateVat();event.preventDefault();', + '#' . /* @noEscape */ $block->getHtmlId() +); ?> diff --git a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml index 434e5606cd032..81ad513351841 100644 --- a/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml +++ b/app/code/Magento/Customer/view/adminhtml/templates/tab/cart.phtml @@ -5,39 +5,41 @@ */ /* @var \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getCartHeader()) : ?> +<?php if ($block->getCartHeader()): ?> <div class="content-header skip-header"> <table> <tr> - <td style="width:50%;"><h4><?= $block->escapeHtml($block->getCartHeader()) ?></h4></td> + <td><h4><?= $block->escapeHtml($block->getCartHeader()) ?></h4></td> </tr> </table> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("width:50%;", 'div.content-header.skip-header table tr td') ?> </div> <?php endif ?> <?= $block->getGridParentHtml() ?> -<?php if ($block->canDisplayContainer()) : ?> +<?php if ($block->canDisplayContainer()): ?> <?php $listType = $block->getJsObjectName(); ?> - <script> + <?php $scriptString = <<<script require([ "Magento_Ui/js/modal/alert", "Magento_Ui/js/modal/confirm", "Magento_Catalog/catalog/product/composite/configure" ], function(alert, confirm){ - <?= $block->escapeJs($block->getJsObjectName()) ?>cartControl = { + {$block->escapeJs($block->getJsObjectName())}cartControl = { reload: function (params) { if (!params) { params = {}; } - <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = params; - <?= $block->escapeJs($block->getJsObjectName()) ?>.reload(); - <?= $block->escapeJs($block->getJsObjectName()) ?>.reloadParams = {}; + {$block->escapeJs($block->getJsObjectName())}.reloadParams = params; + {$block->escapeJs($block->getJsObjectName())}.reload(); + {$block->escapeJs($block->getJsObjectName())}.reloadParams = {}; }, configureItem: function (itemId) { - productConfigure.setOnLoadIFrameCallback('<?= $block->escapeJs($listType) ?>', this.cbOnLoadIframe.bind(this)); - productConfigure.showItemConfiguration('<?= $block->escapeJs($listType) ?>', itemId); + productConfigure.setOnLoadIFrameCallback('{$block->escapeJs($listType)}', this.cbOnLoadIframe.bind(this)); + productConfigure.showItemConfiguration('{$block->escapeJs($listType)}', itemId); return false; }, @@ -53,14 +55,14 @@ if (!itemId) { alert({ - content: '<?= $block->escapeJs(__('No item specified.')) ?>' + content: '{$block->escapeJs(__('No item specified.'))}' }); return false; } confirm({ - content: '<?= $block->escapeJs(__('Are you sure you want to remove this item?')) ?>', + content: '{$block->escapeJs(__('Are you sure you want to remove this item?'))}', actions: { confirm: function(){ self.reload({'delete':itemId}); @@ -70,21 +72,24 @@ } }; - <?php +script; + $params = [ 'customer_id' => $block->getCustomerId(), 'website_id' => $block->getWebsiteId(), ]; - ?> + $scriptString .= <<<script productConfigure.addListType( - '<?= $block->escapeJs($listType) ?>', + '{$block->escapeJs($listType)}', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/configure', $params))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/cart_product_composite_cart/update', $params))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('customer/cart_product_composite_cart/configure', $params))}', + urlConfirm: '{$block->escapeJs($block->getUrl('customer/cart_product_composite_cart/update', $params))}' } ); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif ?> <br /> diff --git a/app/code/Magento/Customer/view/frontend/email/password_reset.html b/app/code/Magento/Customer/view/frontend/email/password_reset.html index a6c54842a1573..e83c484afaec9 100644 --- a/app/code/Magento/Customer/view/frontend/email/password_reset.html +++ b/app/code/Magento/Customer/view/frontend/email/password_reset.html @@ -6,7 +6,6 @@ --> <!--@subject {{trans "Your %store_name password has been changed" store_name=$store.frontend_name}} @--> <!--@vars { -"var customer.name":"Customer Name", "var store.frontend_name":"Store Name", "var store_email":"Store Email", "var store_phone":"Store Phone", diff --git a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml index 0d4cf3c721d14..8355e229fe452 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/authentication-popup.phtml @@ -5,11 +5,11 @@ */ /** @var \Magento\Customer\Block\Account\AuthenticationPopup $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="authenticationPopup" data-bind="scope:'authenticationPopup'" style="display: none;"> - <script> - window.authenticationPopup = <?= /* @noEscape */ $block->getSerializedConfig() ?>; - </script> +<div id="authenticationPopup" data-bind="scope:'authenticationPopup', style: {display: 'none'}"> + <?php $scriptString = 'window.authenticationPopup = ' . /* @noEscape */ $block->getSerializedConfig(); ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> <!-- ko template: getTemplate() --><!-- /ko --> <script type="text/x-magento-init"> { @@ -17,7 +17,9 @@ "Magento_Ui/js/core/app": <?= /* @noEscape */ $block->getJsLayout() ?> }, "*": { - "Magento_Ui/js/block-loader": "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('images/loader-1.gif'))) ?>" + "Magento_Ui/js/block-loader": "<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl( + 'images/loader-1.gif' + ))) ?>" } } </script> diff --git a/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml b/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml index 14827388e3894..e9df83a5913c1 100644 --- a/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/account/link/authorization.phtml @@ -4,15 +4,16 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Customer\Block\Account\AuthorizationLink $block */ - +/** + * @var \Magento\Customer\Block\Account\AuthorizationLink $block + * @var \Magento\Framework\Escaper $escaper + */ $dataPostParam = ''; if ($block->isLoggedIn()) { $dataPostParam = sprintf(" data-post='%s'", $block->getPostParams()); } ?> -<li class="authorization-link" data-label="<?= $block->escapeHtml(__('or')) ?>"> - <a <?= /* @noEscape */ $block->getLinkAttributes() ?><?= /* @noEscape */ $dataPostParam ?>> - <?= $block->escapeHtml($block->getLabel()) ?> - </a> +<li class="link authorization-link" data-label="<?= $escaper->escapeHtml(__('or')) ?>"> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?> + <?= /* @noEscape */ $dataPostParam ?>><?= $escaper->escapeHtml($block->getLabel()) ?></a> </li> diff --git a/app/code/Magento/Customer/view/frontend/templates/account/link/my-account.phtml b/app/code/Magento/Customer/view/frontend/templates/account/link/my-account.phtml new file mode 100644 index 0000000000000..5649ac7c7d7b4 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/templates/account/link/my-account.phtml @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * @var \Magento\Customer\Block\Account\Link $block + * @var \Magento\Framework\Escaper $escaper + */ +?> +<li class="link my-account-link"> + <a <?= /* @noEscape */ $block->getLinkAttributes() ?>><?= $escaper->escapeHtml($block->getLabel()) ?></a> +</li> diff --git a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml index e7519c7c3320b..7f09361e4d505 100644 --- a/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/address/edit.phtml @@ -7,6 +7,7 @@ /** @var \Magento\Customer\Block\Address\Edit $block */ /** @var \Magento\Customer\ViewModel\Address $viewModel */ /** @var \Magento\Framework\Escaper $escaper */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $viewModel = $block->getViewModel(); ?> <?php $_company = $block->getLayout()->createBlock(\Magento\Customer\Block\Widget\Company::class) ?> @@ -152,9 +153,10 @@ $viewModel = $block->getViewModel(); id="zip" class="input-text validate-zip-international <?= $escaper->escapeHtmlAttr($_postcodeValidationClass) ?>"> - <div role="alert" class="message warning" style="display:none"> + <div role="alert" class="message warning"> <span></span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div.message.warning') ?> </div> </div> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml index 89b86f8af8e55..9821cff73a3dd 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/edit.phtml @@ -7,6 +7,7 @@ use Magento\Customer\Block\Widget\Name; /** @var \Magento\Customer\Block\Form\Edit $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form class="form form-edit-account" action="<?= $block->escapeUrl($block->getUrl('customer/account/editPost')) ?>" @@ -46,6 +47,7 @@ use Magento\Customer\Block\Widget\Name; <span><?= $block->escapeHtml(__('Change Password')) ?></span> </label> </div> + <?= $block->getChildHtml('fieldset_edit_info_additional') ?> </fieldset> <fieldset class="fieldset password" data-container="change-email-password"> @@ -117,16 +119,19 @@ use Magento\Customer\Block\Widget\Name; </div> </div> </form> -<script> +<?php $ignore = /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null'; +$scriptString = <<<script require([ "jquery", "mage/mage" ], function($){ var dataForm = $('#form-validate'); - var ignore = <?= /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null' ?>; + var ignore = {$ignore}; dataForm.mage('validation', { - <?php if ($_dob->isEnabled()): ?> +script; +if ($_dob->isEnabled()): + $scriptString .= <<<script errorPlacement: function(error, element) { if (element.prop('id').search('full') !== -1) { var dobElement = $(element).parents('.customer-dob'), @@ -140,13 +145,19 @@ use Magento\Customer\Block\Widget\Name; } }, ignore: ':hidden:not(' + ignore + ')' - <?php else: ?> +script; +else: + $scriptString .= <<<script ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden' - <?php endif ?> +script; +endif; +$scriptString .= <<<script }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php $changeEmailAndPasswordTitle = $block->escapeHtml(__('Change Email and Password')) ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml index be201afa8f66c..caa501d48da83 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/forgotpassword.phtml @@ -6,6 +6,8 @@ * @var $block \Magento\Customer\Block\Account\Forgotpassword */ +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Customer\Block\Account\Forgotpassword $block */ ?> <form class="form password forget" @@ -32,3 +34,12 @@ </div> </div> </form> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "form-validate" + } + } + } +</script> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml index ef74b0062c023..a1d1a0260672a 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/login.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/login.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ +// phpcs:disable Generic.Files.LineLength.TooLong + /** @var \Magento\Customer\Block\Form\Login $block */ ?> <div class="block block-customer-login"> @@ -22,13 +24,22 @@ <div class="field email required"> <label class="label" for="email"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> - <input name="login[username]" value="<?= $block->escapeHtmlAttr($block->getUsername()) ?>" <?php if ($block->isAutocompleteDisabled()) : ?> autocomplete="off"<?php endif; ?> id="email" type="email" class="input-text" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"> + <input name="login[username]" value="<?= $block->escapeHtmlAttr($block->getUsername()) ?>" + <?php if ($block->isAutocompleteDisabled()): ?> autocomplete="off"<?php endif; ?> + id="email" type="email" class="input-text" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" + data-mage-init='{"mage/trim-input":{}}' + data-validate="{required:true, 'validate-email':true}"> </div> </div> <div class="field password required"> <label for="pass" class="label"><span><?= $block->escapeHtml(__('Password')) ?></span></label> <div class="control"> - <input name="login[password]" type="password" <?php if ($block->isAutocompleteDisabled()) : ?> autocomplete="off"<?php endif; ?> class="input-text" id="pass" title="<?= $block->escapeHtmlAttr(__('Password')) ?>" data-validate="{required:true}"> + <input name="login[password]" type="password" + <?php if ($block->isAutocompleteDisabled()): ?> autocomplete="off"<?php endif; ?> + class="input-text" id="pass" + title="<?= $block->escapeHtmlAttr(__('Password')) ?>" + data-validate="{required:true}"> </div> </div> <?= $block->getChildHtml('form_additional_info') ?> @@ -41,3 +52,12 @@ </div> </div> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "login-form" + } + } + } +</script> diff --git a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml index f7d10f6df1728..99040706e50ac 100644 --- a/app/code/Magento/Customer/view/frontend/templates/form/register.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/form/register.phtml @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis use Magento\Customer\Helper\Address; /** @var \Magento\Customer\Block\Form\Register $block */ /** @var \Magento\Framework\Escaper $escaper */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var Magento\Customer\Helper\Address $addressHelper */ +$addressHelper = $block->getData('addressHelper'); +/** @var \Magento\Directory\Helper\Data $directoryHelper */ +$directoryHelper = $block->getData('directoryHelper'); $formData = $block->getFormData(); ?> <?php $displayAll = $block->getConfig('general/region/display_all'); ?> @@ -63,11 +67,12 @@ $formData = $block->getFormData(); <?php if ($_gender->isEnabled()): ?> <?= $_gender->setGender($formData->getGender())->toHtml() ?> <?php endif ?> + <?= $block->getChildHtml('fieldset_create_info_additional') ?> </fieldset> <?php if ($block->getShowAddressFields()): ?> - <?php $cityValidationClass = $this->helper(Address::class)->getAttributeValidationClass('city'); ?> - <?php $postcodeValidationClass = $this->helper(Address::class)->getAttributeValidationClass('postcode'); ?> - <?php $regionValidationClass = $this->helper(Address::class)->getAttributeValidationClass('region'); ?> + <?php $cityValidationClass = $addressHelper->getAttributeValidationClass('city'); ?> + <?php $postcodeValidationClass = $addressHelper->getAttributeValidationClass('postcode'); ?> + <?php $regionValidationClass = $addressHelper->getAttributeValidationClass('region'); ?> <fieldset class="fieldset address"> <legend class="legend"><span><?= $escaper->escapeHtml(__('Address Information')) ?></span></legend><br> <input type="hidden" name="create_address" value="1" /> @@ -88,8 +93,7 @@ $formData = $block->getFormData(); <?php endif ?> <?php - $_streetValidationClass = $this->helper(Address::class) - ->getAttributeValidationClass('street'); + $_streetValidationClass = $addressHelper->getAttributeValidationClass('street'); ?> <div class="field street required"> @@ -106,7 +110,7 @@ $formData = $block->getFormData(); <div class="nested"> <?php $_streetValidationClass = trim(str_replace('required-entry', '', $_streetValidationClass)); - $streetLines = $this->helper(Address::class)->getStreetLines(); + $streetLines = $addressHelper->getStreetLines(); ?> <?php for ($_i = 2, $_n = $streetLines; $_i <= $_n; $_i++): ?> <div class="field additional"> @@ -144,19 +148,19 @@ $formData = $block->getFormData(); <select id="region_id" name="region_id" title="<?= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('region') ?>" - class="validate-select region_id" - style="display: none;"> + class="validate-select region_id"> <option value=""> <?= $escaper->escapeHtml(__('Please select a region, state or province.')) ?> </option> </select> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'select#region_id') ?> <input type="text" id="region" name="region" value="<?= $escaper->escapeHtml($block->getRegion()) ?>" title="<?= /* @noEscape */ $block->getAttributeData()->getFrontendLabel('region') ?>" - class="input-text <?= $escaper->escapeHtmlAttr($regionValidationClass) ?>" - style="display:none;"> + class="input-text <?= $escaper->escapeHtmlAttr($regionValidationClass) ?>"> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'input#region') ?> </div> </div> @@ -273,17 +277,20 @@ $formData = $block->getFormData(); </div> </div> </form> -<script> +<?php $ignore = /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null'; +$scriptString = <<<script require([ 'jquery', 'mage/mage' ], function($){ var dataForm = $('#form-validate'); - var ignore = <?= /* @noEscape */ $_dob->isEnabled() ? '\'input[id$="full"]\'' : 'null' ?>; + var ignore = {$ignore}; dataForm.mage('validation', { - <?php if ($_dob->isEnabled()): ?> +script; +if ($_dob->isEnabled()): + $scriptString .= <<<script errorPlacement: function(error, element) { if (element.prop('id').search('full') !== -1) { var dobElement = $(element).parents('.customer-dob'), @@ -297,20 +304,24 @@ require([ } }, ignore: ':hidden:not(' + ignore + ')' - <?php else: ?> +script; +else: + $scriptString .= <<<script ignore: ignore ? ':hidden:not(' + ignore + ')' : ':hidden' - <?php endif ?> +script; +endif; +$scriptString .= <<<script }).find('input:text').attr('autocomplete', 'off'); - dataForm.submit(function () { - $(this).find(':submit').attr('disabled', 'disabled'); - }); - dataForm.bind("invalid-form.validate", function () { - $(this).find(':submit').prop('disabled', false); - }); - }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php if ($block->getShowAddressFields()): ?> + <?php + $regionJson = /* @noEscape */ $directoryHelper->getRegionJson(); + $regionId = (int) $formData->getRegionId(); + $countriesWithOptionalZip = /* @noEscape */ $directoryHelper->getCountriesWithOptionalZip(true); + ?> <script type="text/x-magento-init"> { "#country": { @@ -320,11 +331,9 @@ require([ "regionInputId": "#region", "postcodeId": "#zip", "form": "#form-validate", - "regionJson": <?= /* @noEscape */ $this->helper(\Magento\Directory\Helper\Data::class) - ->getRegionJson() ?>, - "defaultRegion": "<?= (int) $formData->getRegionId() ?>", - "countriesWithOptionalZip": <?= /* @noEscape */ $this->helper(\Magento\Directory\Helper\Data::class) - ->getCountriesWithOptionalZip(true) ?> + "regionJson": {$regionJson}, + "defaultRegion": "{$regionId}", + "countriesWithOptionalZip": {$countriesWithOptionalZip} } } } @@ -337,6 +346,11 @@ require([ "passwordStrengthIndicator": { "formSelector": "form.form-create-account" } + }, + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "form-validate" + } } } </script> diff --git a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml index a1a784076bac3..eb50ea6454788 100644 --- a/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml +++ b/app/code/Magento/Customer/view/frontend/templates/js/customer-data.phtml @@ -5,16 +5,21 @@ */ /** @var \Magento\Customer\Block\CustomerData $block */ + +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper ?> <script type="text/x-magento-init"> { "*": { "Magento_Customer/js/customer-data": { - "sectionLoadUrl": "<?= $block->escapeJs($block->escapeUrl($block->getCustomerDataUrl('customer/section/load'))) ?>", + "sectionLoadUrl": "<?= $block->escapeJs($block->getCustomerDataUrl('customer/section/load')) ?>", "expirableSectionLifetime": <?= (int)$block->getExpirableSectionLifetime() ?>, - "expirableSectionNames": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getExpirableSectionNames()) ?>, + "expirableSectionNames": <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class) + ->jsonEncode($block->getExpirableSectionNames()) ?>, "cookieLifeTime": "<?= $block->escapeJs($block->getCookieLifeTime()) ?>", - "updateSessionUrl": "<?= $block->escapeJs($block->escapeUrl($block->getCustomerDataUrl('customer/account/updateSession'))) ?>" + "updateSessionUrl": "<?= $block->escapeJs( + $block->getCustomerDataUrl('customer/account/updateSession') + ) ?>" } } } diff --git a/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js new file mode 100644 index 0000000000000..b941ec7a254d8 --- /dev/null +++ b/app/code/Magento/Customer/view/frontend/web/js/block-submit-on-send.js @@ -0,0 +1,22 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'mage/mage' +], function ($) { + 'use strict'; + + return function (config) { + var dataForm = $('#' + config.formId); + + dataForm.submit(function () { + $(this).find(':submit').attr('disabled', 'disabled'); + }); + dataForm.bind('invalid-form.validate', function () { + $(this).find(':submit').prop('disabled', false); + }); + }; +}); diff --git a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js index 770ea47d754d3..5c9bf431bac1d 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/customer-data.js +++ b/app/code/Magento/Customer/view/frontend/web/js/customer-data.js @@ -24,20 +24,12 @@ define([ invalidateCacheByCloseCookieSession, dataProvider, buffer, - customerData; + customerData, + deferred = $.Deferred(); url.setBaseUrl(window.BASE_URL); options.sectionLoadUrl = url.build('customer/section/load'); - //TODO: remove global change, in this case made for initNamespaceStorage - $.cookieStorage.setConf({ - path: '/', - expires: 1 - }); - - storage = $.initNamespaceStorage('mage-cache-storage').localStorage; - storageInvalidation = $.initNamespaceStorage('mage-cache-storage-section-invalidation').localStorage; - /** * @param {Object} invalidateOptions */ @@ -221,6 +213,18 @@ define([ } }, + /** + * Storage init + */ + initStorage: function () { + $.cookieStorage.setConf({ + path: '/', + expires: new Date(Date.now() + parseInt(options.cookieLifeTime, 10) * 1000) + }); + storage = $.initNamespaceStorage('mage-cache-storage').localStorage; + storageInvalidation = $.initNamespaceStorage('mage-cache-storage-section-invalidation').localStorage; + }, + /** * Retrieve the list of sections that has expired since last page reload. * @@ -341,15 +345,26 @@ define([ $.cookieStorage.set('section_data_ids', sectionDataIds); }, + /** + * Checks if customer data is initialized. + * + * @returns {jQuery.Deferred} + */ + getInitCustomerData: function () { + return deferred.promise(); + }, + /** * @param {Object} settings * @constructor */ 'Magento_Customer/js/customer-data': function (settings) { options = settings; + customerData.initStorage(); invalidateCacheBySessionTimeOut(settings); invalidateCacheByCloseCookieSession(); customerData.init(); + deferred.resolve(); } }; diff --git a/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js b/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js index a6d1de5fde255..eba9a8c3ea7ae 100644 --- a/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js +++ b/app/code/Magento/Customer/view/frontend/web/js/model/customer/address.js @@ -6,7 +6,7 @@ /** * @api */ -define([], function () { +define(['underscore'], function (_) { 'use strict'; /** @@ -44,7 +44,7 @@ define([], function () { vatId: addressData['vat_id'], sameAsBilling: addressData['same_as_billing'], saveInAddressBook: addressData['save_in_address_book'], - customAttributes: addressData['custom_attributes'], + customAttributes: _.toArray(addressData['custom_attributes']).reverse(), /** * @return {*} diff --git a/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php new file mode 100644 index 0000000000000..ef3e86788c43f --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Api/ValidateCustomerDataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Api; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Interface for customer data validator + */ +interface ValidateCustomerDataInterface +{ + /** + * Validate customer data + * + * @param array $customerData + * @throws GraphQlInputException + */ + public function execute(array $customerData): void; +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php index 3861ce324ea7d..95d68d69d71e7 100644 --- a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData.php @@ -7,6 +7,9 @@ namespace Magento\CustomerGraphQl\Model\Customer; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; @@ -27,53 +30,41 @@ class ValidateCustomerData */ private $emailAddressValidator; + /** + * @var ValidateCustomerDataInterface[] + */ + private $validators = []; + /** * ValidateCustomerData constructor. * * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes * @param EmailAddressValidator $emailAddressValidator + * @param array $validators */ public function __construct( GetAllowedCustomerAttributes $getAllowedCustomerAttributes, - EmailAddressValidator $emailAddressValidator + EmailAddressValidator $emailAddressValidator, + $validators = [] ) { $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; $this->emailAddressValidator = $emailAddressValidator; + $this->validators = $validators; } /** * Validate customer data * * @param array $customerData - * - * @return void - * * @throws GraphQlInputException + * @throws LocalizedException + * @throws NoSuchEntityException */ - public function execute(array $customerData): void + public function execute(array $customerData) { - $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); - $errorInput = []; - - foreach ($attributes as $attributeInfo) { - if ($attributeInfo->getIsRequired() - && (!isset($customerData[$attributeInfo->getAttributeCode()]) - || $customerData[$attributeInfo->getAttributeCode()] == '') - ) { - $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); - } - } - - if ($errorInput) { - throw new GraphQlInputException( - __('Required parameters are missing: %1', [implode(', ', $errorInput)]) - ); - } - - if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { - throw new GraphQlInputException( - __('"%1" is not a valid email address.', $customerData['email']) - ); + /** @var ValidateCustomerDataInterface $validator */ + foreach ($this->validators as $validator) { + $validator->execute($customerData); } } } diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php new file mode 100644 index 0000000000000..87f8831550f04 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateEmail.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\Validator\EmailAddress as EmailAddressValidator; + +/** + * Validates an email + */ +class ValidateEmail implements ValidateCustomerDataInterface +{ + /** + * @var EmailAddressValidator + */ + private $emailAddressValidator; + + /** + * ValidateEmail constructor. + * + * @param EmailAddressValidator $emailAddressValidator + */ + public function __construct(EmailAddressValidator $emailAddressValidator) + { + $this->emailAddressValidator = $emailAddressValidator; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['email']) && !$this->emailAddressValidator->isValid($customerData['email'])) { + throw new GraphQlInputException( + __('"%1" is not a valid email address.', $customerData['email']) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php new file mode 100644 index 0000000000000..463372a63d8d5 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateGender.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\Customer\Api\CustomerMetadataInterface; +use Magento\Customer\Model\Data\AttributeMetadata; +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates gender value + */ +class ValidateGender implements ValidateCustomerDataInterface +{ + /** + * @var CustomerMetadataInterface + */ + private $customerMetadata; + + /** + * ValidateGender constructor. + * + * @param CustomerMetadataInterface $customerMetadata + */ + public function __construct(CustomerMetadataInterface $customerMetadata) + { + $this->customerMetadata = $customerMetadata; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + if (isset($customerData['gender']) && $customerData['gender']) { + /** @var AttributeMetadata $genderData */ + $options = $this->customerMetadata->getAttributeMetadata('gender')->getOptions(); + + $isValid = false; + foreach ($options as $optionData) { + if ($optionData->getValue() && $optionData->getValue() == $customerData['gender']) { + $isValid = true; + } + } + + if (!$isValid) { + throw new GraphQlInputException( + __('"%1" is not a valid gender value.', $customerData['gender']) + ); + } + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php new file mode 100644 index 0000000000000..fdf4fa1c824f2 --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Customer/ValidateCustomerData/ValidateRequiredArguments.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData; + +use Magento\CustomerGraphQl\Api\ValidateCustomerDataInterface; +use Magento\CustomerGraphQl\Model\Customer\GetAllowedCustomerAttributes; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Validates required attributes + */ +class ValidateRequiredArguments implements ValidateCustomerDataInterface +{ + /** + * Get allowed/required customer attributes + * + * @var GetAllowedCustomerAttributes + */ + private $getAllowedCustomerAttributes; + + /** + * ValidateRequiredArguments constructor. + * + * @param GetAllowedCustomerAttributes $getAllowedCustomerAttributes + */ + public function __construct(GetAllowedCustomerAttributes $getAllowedCustomerAttributes) + { + $this->getAllowedCustomerAttributes = $getAllowedCustomerAttributes; + } + + /** + * @inheritDoc + */ + public function execute(array $customerData): void + { + $attributes = $this->getAllowedCustomerAttributes->execute(array_keys($customerData)); + $errorInput = []; + + foreach ($attributes as $attributeInfo) { + if ($attributeInfo->getIsRequired() + && (!isset($customerData[$attributeInfo->getAttributeCode()]) + || $customerData[$attributeInfo->getAttributeCode()] == '') + ) { + $errorInput[] = $attributeInfo->getDefaultFrontendLabel(); + } + } + + if ($errorInput) { + throw new GraphQlInputException( + __('Required parameters are missing: %1', [implode(', ', $errorInput)]) + ); + } + } +} diff --git a/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerEmail.php b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerEmail.php new file mode 100644 index 0000000000000..e77cea69a3f9d --- /dev/null +++ b/app/code/Magento/CustomerGraphQl/Model/Resolver/UpdateCustomerEmail.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\CustomerGraphQl\Model\Resolver; + +use Magento\CustomerGraphQl\Model\Customer\ExtractCustomerData; +use Magento\CustomerGraphQl\Model\Customer\GetCustomer; +use Magento\CustomerGraphQl\Model\Customer\UpdateCustomerAccount; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; + +/** + * Customer email update, used for GraphQL request processing + */ +class UpdateCustomerEmail implements ResolverInterface +{ + /** + * @var GetCustomer + */ + private $getCustomer; + + /** + * @var UpdateCustomerAccount + */ + private $updateCustomerAccount; + + /** + * @var ExtractCustomerData + */ + private $extractCustomerData; + + /** + * @param GetCustomer $getCustomer + * @param UpdateCustomerAccount $updateCustomerAccount + * @param ExtractCustomerData $extractCustomerData + */ + public function __construct( + GetCustomer $getCustomer, + UpdateCustomerAccount $updateCustomerAccount, + ExtractCustomerData $extractCustomerData + ) { + $this->getCustomer = $getCustomer; + $this->updateCustomerAccount = $updateCustomerAccount; + $this->extractCustomerData = $extractCustomerData; + } + + /** + * Resolve customer email update mutation + * + * @param \Magento\Framework\GraphQl\Config\Element\Field $field + * @param \Magento\Framework\GraphQl\Query\Resolver\ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array|Value + * @throws \Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + /** @var ContextInterface $context */ + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + $customer = $this->getCustomer->execute($context); + $this->updateCustomerAccount->execute( + $customer, + [ + 'email' => $args['email'] ?? null, + 'password' => $args['password'] ?? null + ], + $context->getExtensionAttributes()->getStore() + ); + + $data = $this->extractCustomerData->execute($customer); + + return ['customer' => $data]; + } +} diff --git a/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml b/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml index 26840551eaeb8..b8bdb5a46ca81 100644 --- a/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml +++ b/app/code/Magento/CustomerGraphQl/etc/extension_attributes.xml @@ -8,5 +8,6 @@ <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> <extension_attributes for="Magento\GraphQl\Model\Query\ContextInterface"> <attribute code="is_customer" type="boolean"/> + <attribute code="customer_group_id" type="integer"/> </extension_attributes> -</config> \ No newline at end of file +</config> diff --git a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml index 1ba0e457430e0..3ed77a2ad563c 100644 --- a/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/CustomerGraphQl/etc/graphql/di.xml @@ -29,4 +29,14 @@ </argument> </arguments> </type> + <!-- Validate input customer data --> + <type name="Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData"> + <arguments> + <argument name="validators" xsi:type="array"> + <item name="validateRequiredArguments" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateRequiredArguments</item> + <item name="validateEmail" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateEmail</item> + <item name="validateGender" xsi:type="object">Magento\CustomerGraphQl\Model\Customer\ValidateCustomerData\ValidateGender</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/module.xml b/app/code/Magento/CustomerGraphQl/etc/module.xml index eeed4862bbbfd..b15df7fc0be6b 100644 --- a/app/code/Magento/CustomerGraphQl/etc/module.xml +++ b/app/code/Magento/CustomerGraphQl/etc/module.xml @@ -6,5 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> - <module name="Magento_CustomerGraphQl"/> + <module name="Magento_CustomerGraphQl" > + <sequence> + <module name="Magento_Customer"/> + </sequence> + </module> </config> diff --git a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls index 42a52bd5a99a7..5eed9a38a0350 100644 --- a/app/code/Magento/CustomerGraphQl/etc/schema.graphqls +++ b/app/code/Magento/CustomerGraphQl/etc/schema.graphqls @@ -18,13 +18,16 @@ type Mutation { generateCustomerToken(email: String!, password: String!): CustomerToken @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\GenerateCustomerToken") @doc(description:"Retrieve the customer token") changeCustomerPassword(currentPassword: String!, newPassword: String!): Customer @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ChangePassword") @doc(description:"Changes the password for the logged-in customer") createCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") - updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") + createCustomerV2 (input: CustomerCreateInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomer") @doc(description:"Create customer account") + updateCustomer (input: CustomerInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Deprecated. Use UpdateCustomerV2 instead.") + updateCustomerV2 (input: CustomerUpdateInput!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomer") @doc(description:"Update the customer's personal information") revokeCustomerToken: RevokeCustomerTokenOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RevokeCustomerToken") @doc(description:"Revoke the customer token") createCustomerAddress(input: CustomerAddressInput!): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\CreateCustomerAddress") @doc(description: "Create customer address") updateCustomerAddress(id: Int!, input: CustomerAddressInput): CustomerAddress @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerAddress") @doc(description: "Update customer address") deleteCustomerAddress(id: Int!): Boolean @resolver(class: "Magento\\CustomerGraphQl\\Model\\Resolver\\DeleteCustomerAddress") @doc(description: "Delete customer address") requestPasswordResetEmail(email: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\RequestPasswordResetEmail") @doc(description: "Request an email with a reset password token for the registered customer identified by the specified email.") resetPassword(email: String!, resetPasswordToken: String!, newPassword: String!): Boolean @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\ResetPassword") @doc(description: "Reset a customer's password using the reset password token that the customer received in an email after requesting it using requestPasswordResetEmail.") + updateCustomerEmail(email: String!, password: String!): CustomerOutput @resolver(class: "\\Magento\\CustomerGraphQl\\Model\\Resolver\\UpdateCustomerEmail") @doc(description: "") } input CustomerAddressInput { @@ -78,6 +81,34 @@ input CustomerInput { is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") } +input CustomerCreateInput { + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + firstname: String! @doc(description: "The customer's first name") + middlename: String @doc(description: "The customer's middle name") + lastname: String! @doc(description: "The customer's family name") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + email: String! @doc(description: "The customer's email address. Required for customer creation") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + date_of_birth: String @doc(description: "The customer's date of birth") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + password: String @doc(description: "The customer's password") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") +} + +input CustomerUpdateInput { + date_of_birth: String @doc(description: "The customer's date of birth") + dob: String @doc(description: "Deprecated: Use `date_of_birth` instead") + firstname: String @doc(description: "The customer's first name") + gender: Int @doc(description: "The customer's gender (Male - 1, Female - 2)") + is_subscribed: Boolean @doc(description: "Indicates whether the customer is subscribed to the company's newsletter") + lastname: String @doc(description: "The customer's family name") + middlename: String @doc(description: "The customer's middle name") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + taxvat: String @doc(description: "The customer's Tax/VAT number (for corporate customers)") +} + type CustomerOutput { customer: Customer! } diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Address.php b/app/code/Magento/CustomerImportExport/Model/Import/Address.php index a5947f48bea5f..f15f920fe95f4 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Address.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Address.php @@ -169,7 +169,7 @@ class Address extends AbstractCustomer * Array of region parameters * * @var array - * @deprecated field not in use + * @deprecated 100.3.4 field not in use */ protected $_regionParameters; @@ -199,19 +199,19 @@ class Address extends AbstractCustomer /** * @var \Magento\Eav\Model\Config - * @deprecated field not-in use + * @deprecated 100.3.4 field not-in use */ protected $_eavConfig; /** * @var \Magento\Customer\Model\AddressFactory - * @deprecated not utilized anymore + * @deprecated 100.3.4 not utilized anymore */ protected $_addressFactory; /** * @var \Magento\Framework\Stdlib\DateTime - * @deprecated the property isn't used + * @deprecated 100.3.4 the property isn't used */ protected $dateTime; @@ -667,8 +667,6 @@ protected function _prepareDataForUpdate(array $rowData): array $defaults[$table][$customerId][$attributeCode] = $addressId; } } - // let's try to find region ID - $entityRow[self::COLUMN_REGION_ID] = null; if (!empty($entityRow[self::COLUMN_REGION]) && !empty($entityRow[self::COLUMN_COUNTRY_ID])) { $entityRow[self::COLUMN_REGION_ID] = $this->getCountryRegionId( @@ -679,6 +677,8 @@ protected function _prepareDataForUpdate(array $rowData): array $entityRow[self::COLUMN_REGION] = $entityRow[self::COLUMN_REGION_ID] !== null ? $this->_regions[$entityRow[self::COLUMN_REGION_ID]] : $entityRow[self::COLUMN_REGION]; + } elseif ($newAddress) { + $entityRow[self::COLUMN_REGION_ID] = null; } if ($newAddress) { $entityRowNew = $entityRow; @@ -908,7 +908,7 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) } } - if (isset($rowData[self::COLUMN_COUNTRY_ID])) { + if (!empty($rowData[self::COLUMN_COUNTRY_ID])) { if (isset($rowData[self::COLUMN_POSTCODE]) && !$this->postcodeValidator->isValid( $rowData[self::COLUMN_COUNTRY_ID], @@ -918,8 +918,7 @@ protected function _validateRowForUpdate(array $rowData, $rowNumber) $this->addRowError(self::ERROR_VALUE_IS_REQUIRED, $rowNumber, self::COLUMN_POSTCODE); } - if (isset($rowData[self::COLUMN_REGION]) - && !empty($rowData[self::COLUMN_REGION]) + if (!empty($rowData[self::COLUMN_REGION]) && count($this->getCountryRegions($rowData[self::COLUMN_COUNTRY_ID])) > 0 && null === $this->getCountryRegionId( $rowData[self::COLUMN_COUNTRY_ID], diff --git a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php index 8f5bb951ce737..5ebf242bd6ac4 100644 --- a/app/code/Magento/CustomerImportExport/Model/Import/Customer.php +++ b/app/code/Magento/CustomerImportExport/Model/Import/Customer.php @@ -367,6 +367,7 @@ protected function _getNextEntityId() * @param array|AbstractSource $rows * * @return void + * @since 100.2.3 */ public function prepareCustomerData($rows): void { @@ -387,6 +388,7 @@ public function prepareCustomerData($rows): void /** * @inheritDoc + * @since 100.2.3 */ public function validateData() { diff --git a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php index 11f326e6dfc8f..f08278af864e7 100644 --- a/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php +++ b/app/code/Magento/CustomerImportExport/Model/ResourceModel/Import/Customer/Storage.php @@ -130,7 +130,7 @@ public function addCustomerByArray(array $customer): Storage /** * Add customer to array * - * @deprecated @see addCustomerByArray + * @deprecated 100.3.0 @see addCustomerByArray * @param DataObject $customer * @return $this */ diff --git a/app/code/Magento/Deploy/Package/LocaleResolver.php b/app/code/Magento/Deploy/Package/LocaleResolver.php new file mode 100644 index 0000000000000..d9909edf31284 --- /dev/null +++ b/app/code/Magento/Deploy/Package/LocaleResolver.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Deploy\Package; + +use InvalidArgumentException; +use Magento\Framework\App\Area; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\AppInterface; +use Magento\Framework\Config\ConfigOptionsListConstants; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Validator\Locale; +use Magento\Store\Model\Config\StoreView; +use Magento\User\Api\Data\UserInterface; +use Magento\User\Model\ResourceModel\User\Collection as UserCollection; +use Magento\User\Model\ResourceModel\User\CollectionFactory as UserCollectionFactory; +use Psr\Log\LoggerInterface; + +/** + * Deployment Package Locale Resolver class + */ +class LocaleResolver +{ + /** + * Parameter to force deploying certain languages for the admin, without any users having configured them yet. + */ + const ADMIN_LOCALES_FOR_DEPLOY = 'admin_locales_for_deploy'; + + /** + * @var StoreView + */ + private $storeView; + + /** + * @var UserCollectionFactory + */ + private $userCollFactory; + + /** + * @var DeploymentConfig + */ + private $deploymentConfig; + + /** + * @var Locale + */ + private $locale; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var array|null + */ + private $usedStoreLocales; + + /** + * @var array|null + */ + private $usedAdminLocales; + + /** + * LocaleResolver constructor. + * + * @param StoreView $storeView + * @param UserCollectionFactory $userCollectionFactory + * @param DeploymentConfig $deploymentConfig + * @param Locale $locale + * @param LoggerInterface $logger + */ + public function __construct( + StoreView $storeView, + UserCollectionFactory $userCollectionFactory, + DeploymentConfig $deploymentConfig, + Locale $locale, + LoggerInterface $logger + ) { + $this->storeView = $storeView; + $this->userCollFactory = $userCollectionFactory; + $this->deploymentConfig = $deploymentConfig; + $this->locale = $locale; + $this->logger = $logger; + } + + /** + * Get locales that are used for a given theme. + * If it is a frontend theme, return supported frontend languages. + * If it is an adminhtml theme, return languages that admin users have configured together with deployment config. + * + * @param Package $package + * + * @return array + */ + public function getUsedPackageLocales(Package $package): array + { + switch ($package->getArea()) { + case Area::AREA_ADMINHTML: + $locales = $this->getUsedAdminLocales(); + break; + case Area::AREA_FRONTEND: + $locales = $this->getUsedStoreLocales(); + break; + default: + $locales = array_merge($this->getUsedAdminLocales(), $this->getUsedStoreLocales()); + } + return $this->validateLocales($locales); + } + + /** + * Get used admin user locales, en_US is always included by default. + * + * @return array + */ + private function getUsedAdminLocales(): array + { + if ($this->usedAdminLocales === null) { + $deploymentConfig = $this->getDeploymentAdminLocales(); + $this->usedAdminLocales = array_merge([AppInterface::DISTRO_LOCALE_CODE], $deploymentConfig); + + if (!$this->isDbConnectionAvailable()) { + return $this->usedAdminLocales; + } + + /** @var UserCollection $userCollection */ + $userCollection = $this->userCollFactory->create(); + /** @var UserInterface $adminUser */ + foreach ($userCollection as $adminUser) { + $this->usedAdminLocales[] = $adminUser->getInterfaceLocale(); + } + } + return $this->usedAdminLocales; + } + + /** + * Get used store locales. + * + * @return array + */ + private function getUsedStoreLocales(): array + { + if ($this->usedStoreLocales === null) { + $this->usedStoreLocales = $this->isDbConnectionAvailable() + ? $this->storeView->retrieveLocales() + : [AppInterface::DISTRO_LOCALE_CODE]; + } + return $this->usedStoreLocales; + } + + /** + * Strip out duplicates and break on invalid locale codes. + * + * @param array $usedLocales + * + * @return array + * @throws InvalidArgumentException if unknown locale is provided by the store configuration + */ + private function validateLocales(array $usedLocales): array + { + return array_map( + function ($locale) { + if (!$this->locale->isValid($locale)) { + throw new InvalidArgumentException( + $locale . ' argument has invalid value, run info:language:list for list of available locales' + ); + } + + return $locale; + }, + array_unique($usedLocales) + ); + } + + /** + * Check if a database connection is already set up. + * + * @return bool + */ + private function isDbConnectionAvailable(): bool + { + try { + $connections = $this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS, []); + } catch (LocalizedException $exception) { + $this->logger->critical($exception); + } + return !empty($connections); + } + + /** + * Retrieve deployment configuration for admin locales that have to be deployed. + * + * @return array|mixed|string|null + */ + private function getDeploymentAdminLocales(): array + { + try { + return $this->deploymentConfig + ->get(self::ADMIN_LOCALES_FOR_DEPLOY, []); + } catch (LocalizedException $exception) { + return []; + } + } +} diff --git a/app/code/Magento/Deploy/Package/PackagePool.php b/app/code/Magento/Deploy/Package/PackagePool.php index 9057f50fb3c91..f31af8f9e081f 100644 --- a/app/code/Magento/Deploy/Package/PackagePool.php +++ b/app/code/Magento/Deploy/Package/PackagePool.php @@ -7,7 +7,7 @@ use Magento\Deploy\Collector\Collector; use Magento\Deploy\Console\DeployStaticOptions as Options; -use Magento\Framework\AppInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\View\Design\ThemeInterface; use Magento\Framework\View\Design\Theme\ListInterface; @@ -41,22 +41,30 @@ class PackagePool */ private $collected = false; + /** + * @var LocaleResolver|null + */ + private $localeResolver; + /** * PackagePool constructor * * @param Collector $collector * @param ListInterface $themeCollection * @param PackageFactory $packageFactory + * @param LocaleResolver|null $localeResolver */ public function __construct( Collector $collector, ListInterface $themeCollection, - PackageFactory $packageFactory + PackageFactory $packageFactory, + ?LocaleResolver $localeResolver = null ) { $this->collector = $collector; $themeCollection->clear()->resetConstraints(); $this->themes = $themeCollection->getItems(); $this->packageFactory = $packageFactory; + $this->localeResolver = $localeResolver ?: ObjectManager::getInstance()->get(LocaleResolver::class); } /** @@ -221,17 +229,18 @@ private function ensurePackagesForRequiredLocales(array $options) private function ensureRequiredLocales(array $options) { if (empty($options[Options::LANGUAGE]) || $options[Options::LANGUAGE][0] === 'all') { - $forcedLocales = [AppInterface::DISTRO_LOCALE_CODE]; + $forcedLocales = []; } else { $forcedLocales = $options[Options::LANGUAGE]; } - $resultPackages = $this->packages; - foreach ($resultPackages as $package) { + foreach ($this->packages as $package) { if ($package->getTheme() === Package::BASE_THEME) { continue; } - foreach ($forcedLocales as $locale) { + + $locales = $forcedLocales ?: $this->localeResolver->getUsedPackageLocales($package); + foreach ($locales as $locale) { $this->ensurePackage([ 'area' => $package->getArea(), 'theme' => $package->getTheme(), diff --git a/app/code/Magento/Deploy/Process/Queue.php b/app/code/Magento/Deploy/Process/Queue.php index 6c8db345187cc..35d85c390b9c4 100644 --- a/app/code/Magento/Deploy/Process/Queue.php +++ b/app/code/Magento/Deploy/Process/Queue.php @@ -29,7 +29,7 @@ class Queue /** * Default max execution time */ - const DEFAULT_MAX_EXEC_TIME = 400; + const DEFAULT_MAX_EXEC_TIME = 900; /** * @var array @@ -96,6 +96,11 @@ class Queue */ private $lastJobStarted = 0; + /** + * @var int + */ + private $logDelay; + /** * @param AppState $appState * @param LocaleResolver $localeResolver @@ -157,11 +162,12 @@ public function getPackages() * Process jobs * * @return int + * @throws TimeoutException */ public function process() { $returnStatus = 0; - $logDelay = 10; + $this->logDelay = 10; $this->start = $this->lastJobStarted = time(); $packages = $this->packages; while (count($packages) && $this->checkTimeout()) { @@ -170,13 +176,7 @@ public function process() $this->assertAndExecute($name, $packages, $packageJob); } - // refresh current status in console once in 10 iterations (once in 5 sec) - if ($logDelay >= 10) { - $this->logger->info('.'); - $logDelay = 0; - } else { - $logDelay++; - } + $this->refreshStatus(); if ($this->isCanBeParalleled()) { // in parallel mode sleep before trying to check status and run new jobs @@ -193,9 +193,28 @@ public function process() $this->awaitForAllProcesses(); + if (!empty($packages)) { + throw new TimeoutException('Not all packages are deployed.'); + } + return $returnStatus; } + /** + * Refresh current status in console once in 10 iterations (once in 5 sec) + * + * @return void + */ + private function refreshStatus(): void + { + if ($this->logDelay >= 10) { + $this->logger->info('.'); + $this->logDelay = 0; + } else { + $this->logDelay++; + } + } + /** * Check that all depended packages deployed and execute * @@ -204,7 +223,7 @@ public function process() * @param array $packageJob * @return void */ - private function assertAndExecute($name, array & $packages, array $packageJob) + private function assertAndExecute($name, array &$packages, array $packageJob) { /** @var Package $package */ $package = $packageJob['package']; @@ -256,7 +275,6 @@ private function executePackage(Package $package, string $name, array &$packages */ private function awaitForAllProcesses() { - $logDelay = 10; while ($this->inProgress && $this->checkTimeout()) { foreach ($this->inProgress as $name => $package) { if ($this->isDeployed($package)) { @@ -264,13 +282,7 @@ private function awaitForAllProcesses() } } - // refresh current status in console once in 10 iterations (once in 5 sec) - if ($logDelay >= 10) { - $this->logger->info('.'); - $logDelay = 0; - } else { - $logDelay++; - } + $this->refreshStatus(); // sleep before checking parallel jobs status // phpcs:ignore Magento2.Functions.DiscouragedFunction diff --git a/app/code/Magento/Deploy/Process/TimeoutException.php b/app/code/Magento/Deploy/Process/TimeoutException.php new file mode 100644 index 0000000000000..2d8eb3ab2aad2 --- /dev/null +++ b/app/code/Magento/Deploy/Process/TimeoutException.php @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Deploy\Process; + +/** + * Exception is thrown if deploy process is finished due to timeout. + */ +class TimeoutException extends \RuntimeException +{ +} diff --git a/app/code/Magento/Deploy/Test/Unit/Package/LocaleResolverTest.php b/app/code/Magento/Deploy/Test/Unit/Package/LocaleResolverTest.php new file mode 100644 index 0000000000000..3c911d4c0c533 --- /dev/null +++ b/app/code/Magento/Deploy/Test/Unit/Package/LocaleResolverTest.php @@ -0,0 +1,227 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Deploy\Test\Unit\Package; + +use Magento\Deploy\Package\LocaleResolver; +use Magento\Deploy\Package\Package; +use Magento\Framework\App\Area; +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\AppInterface; +use Magento\Framework\Validator\Locale; +use Magento\Store\Model\Config\StoreView; +use Magento\User\Api\Data\UserInterface; +use Magento\User\Model\ResourceModel\User\Collection; +use Magento\User\Model\ResourceModel\User\CollectionFactory; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Deployment Package LocaleResolver class unit tests + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class LocaleResolverTest extends TestCase +{ + /** + * @var LocaleResolver + */ + private $localeResolver; + + /** + * @var Package|MockObject + */ + private $package; + + /** + * @var StoreView|MockObject + */ + private $storeView; + + /** + * @var CollectionFactory|MockObject + */ + private $userCollectionFactory; + + /** + * @var DeploymentConfig|MockObject + */ + private $deploymentConfig; + + /** + * @var Locale|MockObject + */ + private $locale; + + /** + * @var MockObject|LoggerInterface + */ + private $logger; + + /** + * @var Collection|MockObject + */ + private $userCollection; + + /** + * @var UserInterface|MockObject + */ + private $adminUser; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->package = $this->createMock(Package::class); + $this->storeView = $this->createMock(StoreView::class); + $this->adminUser = $this->getMockForAbstractClass(UserInterface::class); + $this->userCollection = $this->createMock(Collection::class); + $this->userCollectionFactory = $this->createMock(CollectionFactory::class); + $this->userCollectionFactory->method('create')->willReturn($this->userCollection); + $this->deploymentConfig = $this->createMock(DeploymentConfig::class); + $this->locale = $this->createMock(Locale::class); + $this->logger = $this->getMockForAbstractClass( + LoggerInterface::class, + ['critical'], + '', + false + ); + $this->localeResolver = new LocaleResolver( + $this->storeView, + $this->userCollectionFactory, + $this->deploymentConfig, + $this->locale, + $this->logger + ); + } + + /** + * Test Get Used Package Locales when there is no DB connection set up yet + * Should only return en_US by default + */ + public function testGetUsedPackageLocalesNoDb() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_FRONTEND); + $this->deploymentConfig->expects(static::exactly(1))->method('get')->willReturn([]); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE], + $locales + ); + } + + /** + * Test Get Used Package Locales when there is no DB connection set up yet + * Should only return en_US by default + */ + public function testGetUsedPackageLocalesNoDbWithDeployment() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_ADMINHTML); + $this->deploymentConfig->expects(static::exactly(2))->method('get')->willReturn(['zh_SG'], []); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE, 'zh_SG'], + $locales + ); + } + + /** + * Test Get Used Package Locales when there is no DB connection set up yet + * Should only return en_US by default + */ + public function testGetUsedPackageLocalesIllegalLocale() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_ADMINHTML); + $this->deploymentConfig->expects(static::exactly(2))->method('get')->willReturn(['en_DE'], []); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true, false); + $this->expectException('InvalidArgumentException'); + $this->expectExceptionMessage( + 'en_DE argument has invalid value, run info:language:list for list of available locales' + ); + $this->localeResolver->getUsedPackageLocales($this->package); + } + + /** + * Test Get Used Package Locales for a frontend theme + * Should return used frontend languages + */ + public function testGetUsedPackageLocalesFrontend() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_FRONTEND); + $this->deploymentConfig->expects(static::exactly(1))->method('get')->willReturn(['default' => []]); + $this->storeView->expects(static::exactly(1))->method('retrieveLocales')->willReturn(['de_DE', 'en_GB']); + $this->userCollectionFactory->expects(static::exactly(0))->method('create'); + $this->locale->method('isValid')->willReturn(true); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + ['de_DE', 'en_GB'], + $locales + ); + } + + /** + * Test Get Used Package Locales for an admin theme + * Should return used admin languages, admin deployment configuration languages and en_US by default + */ + public function testGetUsedPackageLocalesAdmin() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_ADMINHTML); + $this->deploymentConfig->expects(static::exactly(2))->method('get')->willReturn(['de_AT'], ['default' => []]); + $this->storeView->expects(static::exactly(0))->method('retrieveLocales'); + $this->userCollectionFactory->expects(static::exactly(1))->method('create'); + $this->locale->method('isValid')->willReturn(true); + $this->adminUser->expects(static::exactly(2))->method('getInterfaceLocale')->willReturn('nl_NL', 'fr_FR'); + $this->userCollection->method('getIterator')->willReturn(new \ArrayIterator([ + $this->adminUser, + $this->adminUser, + ])); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE, 'de_AT', 'nl_NL', 'fr_FR'], + $locales + ); + } + + /** + * Test Get Used Package Locales for a theme that is neither frontend nor admin (hypothetical) + * Should return both used admin and used frontend languages, plus en_US by default + */ + public function testGetUsedPackageLocalesDefault() + { + $this->package->expects(static::exactly(1))->method('getArea')->willReturn(Area::AREA_GLOBAL); + $this->deploymentConfig->expects(static::exactly(3))->method('get') + ->willReturn(['de_AT'], ['default' => []], ['default' => []]); + $this->storeView->expects(static::exactly(1))->method('retrieveLocales')->willReturn(['en_IE', 'fr_LU']); + $this->userCollectionFactory->expects(static::exactly(1))->method('create'); + $this->locale->method('isValid')->willReturn(true); + $this->adminUser->expects(static::exactly(2))->method('getInterfaceLocale')->willReturn('nl_NL', 'fr_FR'); + $this->userCollection->method('getIterator')->willReturn(new \ArrayIterator([ + $this->adminUser, + $this->adminUser, + ])); + + $locales = $this->localeResolver->getUsedPackageLocales($this->package); + static::assertEquals( + [AppInterface::DISTRO_LOCALE_CODE, 'de_AT', 'nl_NL', 'fr_FR', 'en_IE', 'fr_LU'], + $locales + ); + } +} diff --git a/app/code/Magento/Developer/Console/Command/patch_template.php.dist b/app/code/Magento/Developer/Console/Command/patch_template.php.dist index f4fc25abcb29a..8e14b24bdc933 100644 --- a/app/code/Magento/Developer/Console/Command/patch_template.php.dist +++ b/app/code/Magento/Developer/Console/Command/patch_template.php.dist @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace %moduleName%\Setup\Patch\%patchType%; @@ -36,7 +37,7 @@ class %class% implements %implementsInterfaces% } %revertFunction% /** - * {@inheritdoc} + * @inheritdoc */ public function getAliases() { @@ -44,12 +45,10 @@ class %class% implements %implementsInterfaces% } /** - * {@inheritdoc} + * @inheritdoc */ public static function getDependencies() { - return [ - - ]; + return []; } } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php index ba98524bb665e..fc659c773c0af 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Debug.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Debug.php @@ -21,11 +21,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug */ private $state; - /** - * @var ScopeConfigInterface - */ - private $scopeConfig; - /** * @var DeploymentConfig */ @@ -34,7 +29,6 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug /** * @param DriverInterface $filesystem * @param State $state - * @param ScopeConfigInterface $scopeConfig * @param DeploymentConfig $deploymentConfig * @param string $filePath * @throws \Exception @@ -42,14 +36,12 @@ class Debug extends \Magento\Framework\Logger\Handler\Debug public function __construct( DriverInterface $filesystem, State $state, - ScopeConfigInterface $scopeConfig, DeploymentConfig $deploymentConfig, $filePath = null ) { parent::__construct($filesystem, $filePath); $this->state = $state; - $this->scopeConfig = $scopeConfig; $this->deploymentConfig = $deploymentConfig; } diff --git a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php index 3f5ff58640313..c6ee70fb9ce40 100644 --- a/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php +++ b/app/code/Magento/Developer/Model/Logger/Handler/Syslog.php @@ -29,13 +29,10 @@ class Syslog extends \Magento\Framework\Logger\Handler\Syslog private $deploymentConfig; /** - * @param ScopeConfigInterface $scopeConfig Scope config * @param DeploymentConfig $deploymentConfig Deployment config * @param string $ident The string ident to be added to each message - * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( - ScopeConfigInterface $scopeConfig, DeploymentConfig $deploymentConfig, string $ident ) { diff --git a/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php b/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php index c689bc7aee80a..f331923f4b696 100644 --- a/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php +++ b/app/code/Magento/Developer/Model/TemplateEngine/Decorator/DebugHints.php @@ -8,6 +8,10 @@ namespace Magento\Developer\Model\TemplateEngine\Decorator; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Decorates block with block and template hints * @@ -26,20 +30,39 @@ class DebugHints implements \Magento\Framework\View\TemplateEngineInterface */ private $_showBlockHints; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param \Magento\Framework\View\TemplateEngineInterface $subject * @param bool $showBlockHints Whether to include block into the debugging information or not + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ - public function __construct(\Magento\Framework\View\TemplateEngineInterface $subject, $showBlockHints) - { + public function __construct( + \Magento\Framework\View\TemplateEngineInterface $subject, + $showBlockHints, + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { $this->_subject = $subject; $this->_showBlockHints = $showBlockHints; + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** * Insert debugging hints into the rendered block contents * - * {@inheritdoc} + * Insert debugging hints into the rendered block contents + * @inheritdoc */ public function render(\Magento\Framework\View\Element\BlockInterface $block, $templateFile, array $dictionary = []) { @@ -60,14 +83,33 @@ public function render(\Magento\Framework\View\Element\BlockInterface $block, $t */ protected function _renderTemplateHints($blockHtml, $templateFile) { - // @codingStandardsIgnoreStart - return <<<HTML -<div class="debugging-hints" style="position: relative; border: 1px dotted red; margin: 6px 2px; padding: 18px 2px 2px 2px;"> -<div class="debugging-hint-template-file" style="position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; left: 0; color: white; white-space: nowrap;" onmouseover="this.style.zIndex = 999;" onmouseout="this.style.zIndex = 'auto';" title="{$templateFile}">{$templateFile}</div> + $hintsId = 'hintsId_' .$this->random->getRandomString(32); + $hintsTemplateFileId = 'hintsTemplateFileId_' .$this->random->getRandomString(32); + + $scriptString = <<<HTML +<div class="debugging-hints" id="{$hintsId}"> +<div class="debugging-hint-template-file" id="{$hintsTemplateFileId}" title="{$templateFile}">{$templateFile}</div> {$blockHtml} </div> HTML; - // @codingStandardsIgnoreEnd + + return $scriptString . + $this->secureRenderer->renderStyleAsTag( + "position: relative; border: 1px dotted red; margin: 6px 2px; padding: 18px 2px 2px 2px;", + '#' . $hintsId + ) . $this->secureRenderer->renderStyleAsTag( + "position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; left: 0;" . + " color: white; white-space: nowrap;", + '#' . $hintsTemplateFileId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseover', + "this.style.zIndex = 999;", + '#' . $hintsTemplateFileId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseout', + "this.style.zIndex = 'auto';", + '#' . $hintsTemplateFileId + ); } /** @@ -80,11 +122,25 @@ protected function _renderTemplateHints($blockHtml, $templateFile) protected function _renderBlockHints($blockHtml, \Magento\Framework\View\Element\BlockInterface $block) { $blockClass = get_class($block); - // @codingStandardsIgnoreStart - return <<<HTML -<div class="debugging-hint-block-class" style="position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; right: 0; color: blue; white-space: nowrap;" onmouseover="this.style.zIndex = 999;" onmouseout="this.style.zIndex = 'auto';" title="{$blockClass}">{$blockClass}</div> + $hintsId = 'hintsBlockId_' .$this->random->getRandomString(32); + $scriptString = <<<HTML +<div class="debugging-hint-block-class" id="{$hintsId}" title="{$blockClass}">{$blockClass}</div> {$blockHtml} HTML; - // @codingStandardsIgnoreEnd + + return $scriptString . + $this->secureRenderer->renderStyleAsTag( + "position: absolute; top: 0; padding: 2px 5px; font: normal 11px Arial; background: red; right: 0;" . + " color: blue; white-space: nowrap;", + '#' . $hintsId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseover', + "this.style.zIndex = 999;", + '#' . $hintsId + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onmouseout', + "this.style.zIndex = 'auto';", + '#' . $hintsId + ); } } diff --git a/app/code/Magento/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php b/app/code/Magento/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php index 919ee0e060468..a9e42cd01f2d5 100644 --- a/app/code/Magento/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php +++ b/app/code/Magento/Developer/Test/Unit/Console/Command/XmlCatalogGenerateCommandTest.php @@ -97,7 +97,7 @@ public function testExecuteVsCodeFormat() ->with( $this->equalTo(['urn:magento:framework:Module/etc/module.xsd' => $fixtureXmlFile]), $this->equalTo('test') - )->willReturn(null); + ); $formats = ['vscode' => $vscodeFormatMock]; $readFactory = $this->createMock(ReadFactory::class); diff --git a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php index 8bb0b1f176313..5e824e43764de 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/Logger/Handler/SyslogTest.php @@ -44,7 +44,6 @@ protected function setUp(): void $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); $this->model = new Syslog( - $this->scopeConfigMock, $this->deploymentConfigMock, 'Magento' ); diff --git a/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php b/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php index c1ba20191d0c8..e4e60f95146df 100644 --- a/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php +++ b/app/code/Magento/Developer/Test/Unit/Model/TemplateEngine/Decorator/DebugHintsTest.php @@ -1,14 +1,20 @@ -<?php declare(strict_types=1); +<?php /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Developer\Test\Unit\Model\TemplateEngine\Decorator; use Magento\Developer\Model\TemplateEngine\Decorator\DebugHints; use Magento\Framework\View\Element\BlockInterface; use Magento\Framework\View\TemplateEngineInterface; use PHPUnit\Framework\TestCase; +use Magento\Framework\DataObject; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class DebugHintsTest extends TestCase { @@ -33,7 +39,30 @@ public function testRender($showBlockHints) )->willReturn( '<div id="fixture"/>' ); - $model = new DebugHints($subject, $showBlockHints); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('random'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); + $model = new DebugHints($subject, $showBlockHints, $secureRendererMock, $randomMock); $actualResult = $model->render($block, 'template.phtml', ['var' => 'val']); $this->assertNotNull($actualResult); } diff --git a/app/code/Magento/Dhl/Model/Carrier.php b/app/code/Magento/Dhl/Model/Carrier.php index 6587b462099be..d9b89b23d3d69 100644 --- a/app/code/Magento/Dhl/Model/Carrier.php +++ b/app/code/Magento/Dhl/Model/Carrier.php @@ -1084,7 +1084,7 @@ function () use ($deferredResponses, $responseBodies) { * * @param string $request * @return string - * @deprecated Use asynchronous client. + * @deprecated 100.3.3 Use asynchronous client. * @see _getQuotes() */ protected function _getQuotesFromServer($request) @@ -1396,7 +1396,7 @@ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) * * @param \Magento\Framework\DataObject $request * @return $this|\Magento\Framework\DataObject|boolean - * @deprecated + * @deprecated 100.2.3 */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) { diff --git a/app/code/Magento/Dhl/view/adminhtml/templates/unitofmeasure.phtml b/app/code/Magento/Dhl/view/adminhtml/templates/unitofmeasure.phtml index 36ff8138f3955..2d9c8c91e1b76 100644 --- a/app/code/Magento/Dhl/view/adminhtml/templates/unitofmeasure.phtml +++ b/app/code/Magento/Dhl/view/adminhtml/templates/unitofmeasure.phtml @@ -5,24 +5,26 @@ */ /** * @var \Magento\Dhl\Block\Adminhtml\Unitofmeasure $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script //<![CDATA[ require(["prototype"], function(){ function changeDimensions() { - var dimensionUnit = "(<?= $block->escapeHtml($block->getInch()) ?>)"; - var dhlUnitOfMeasureNote = "<?= $block->escapeHtml($block->getDivideOrderWeightNoteLbp()) ?>"; + var dimensionUnit = "({$block->escapeHtml($block->getInch())})"; + var dhlUnitOfMeasureNote = "{$block->escapeHtml($block->getDivideOrderWeightNoteLbp())}"; if ($("carriers_dhl_unit_of_measure").value == "K") { - dimensionUnit = "(<?= $block->escapeHtml($block->getCm()) ?>)"; - dhlUnitOfMeasureNote = "<?= $block->escapeHtml($block->getDivideOrderWeightNoteKg()) ?>"; + dimensionUnit = "({$block->escapeHtml($block->getCm())})"; + dhlUnitOfMeasureNote = "{$block->escapeHtml($block->getDivideOrderWeightNoteKg())}"; } - $$('[for="carriers_dhl_height"]')[0].innerHTML = "<?= - $block->escapeHtml($block->getHeight()); ?> " + dimensionUnit; - $$('[for="carriers_dhl_depth"]')[0].innerHTML = "<?= - $block->escapeHtml($block->getDepth()); ?> " + dimensionUnit; - $$('[for="carriers_dhl_width"]')[0].innerHTML = "<?= - $block->escapeHtml($block->getWidth()); ?> " + dimensionUnit; + \$$('[for="carriers_dhl_height"]')[0].innerHTML = "{$block->escapeHtml($block->getHeight())} " + + dimensionUnit; + \$$('[for="carriers_dhl_depth"]')[0].innerHTML = "{$block->escapeHtml($block->getDepth())} " + + dimensionUnit; + \$$('[for="carriers_dhl_width"]')[0].innerHTML = "{$block->escapeHtml($block->getWidth())} " + + dimensionUnit; $("carriers_dhl_divide_order_weight").next().down().innerHTML = dhlUnitOfMeasureNote; } @@ -33,4 +35,6 @@ }); }); //]]> -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Directory/Block/Data.php b/app/code/Magento/Directory/Block/Data.php index 66c962d6656a6..0424b824e4454 100644 --- a/app/code/Magento/Directory/Block/Data.php +++ b/app/code/Magento/Directory/Block/Data.php @@ -142,7 +142,7 @@ public function getCountryHtmlSelect($defValue = null, $name = 'country_id', $id )->setId( $id )->setTitle( - __($title) + $this->escapeHtmlAttr(__($title)) )->setValue( $defValue )->setOptions( @@ -175,7 +175,7 @@ public function getRegionCollection() * Returns region html select * * @return string - * @deprecated + * @deprecated 100.3.3 * @see getRegionSelect() method for more configurations */ public function getRegionHtmlSelect() diff --git a/app/code/Magento/Directory/Model/ResourceModel/Currency.php b/app/code/Magento/Directory/Model/ResourceModel/Currency.php index 5db880c00343a..aec2da291f1b3 100644 --- a/app/code/Magento/Directory/Model/ResourceModel/Currency.php +++ b/app/code/Magento/Directory/Model/ResourceModel/Currency.php @@ -165,7 +165,7 @@ public function saveRates($rates) * @param string $path * @return array * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated because doesn't take into consideration scopes and system config values. + * @deprecated 100.2.3 because doesn't take into consideration scopes and system config values. * @see \Magento\Directory\Model\CurrencyConfig::getConfigCurrencies() */ public function getConfigCurrencies($model, $path) diff --git a/app/code/Magento/Directory/Test/Unit/Block/DataTest.php b/app/code/Magento/Directory/Test/Unit/Block/DataTest.php index bf71419744e7c..af64d7119f077 100644 --- a/app/code/Magento/Directory/Test/Unit/Block/DataTest.php +++ b/app/code/Magento/Directory/Test/Unit/Block/DataTest.php @@ -21,6 +21,7 @@ use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\Escaper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -62,9 +63,16 @@ class DataTest extends TestCase /** @var SerializerInterface|MockObject */ private $serializerMock; + /** @var \Magento\Framework\Escaper */ + private $escaper; + protected function setUp(): void { $objectManagerHelper = new ObjectManager($this); + $this->escaper = $this->getMockBuilder(Escaper::class) + ->disableOriginalConstructor() + ->setMethods(['escapeHtmlAttr']) + ->getMock(); $this->prepareContext(); $this->helperDataMock = $this->getMockBuilder(\Magento\Directory\Helper\Data::class) @@ -129,6 +137,10 @@ protected function prepareContext() $this->contextMock->expects($this->any()) ->method('getLayout') ->willReturn($this->layoutMock); + + $this->contextMock->expects($this->once()) + ->method('getEscaper') + ->willReturn($this->escaper); } protected function prepareCountryCollection() @@ -142,9 +154,11 @@ protected function prepareCountryCollection() \Magento\Directory\Model\ResourceModel\Country\CollectionFactory::class ) ->disableOriginalConstructor() - ->setMethods([ - 'create' - ]) + ->setMethods( + [ + 'create' + ] + ) ->getMock(); $this->countryCollectionFactoryMock->expects($this->any()) @@ -292,15 +306,17 @@ protected function mockElementHtmlSelect($defaultCountry, $options, $resultHtml) $elementHtmlSelect = $this->getMockBuilder(Select::class) ->disableOriginalConstructor() - ->setMethods([ - 'setName', - 'setId', - 'setTitle', - 'setValue', - 'setOptions', - 'setExtraParams', - 'getHtml', - ]) + ->setMethods( + [ + 'setName', + 'setId', + 'setTitle', + 'setValue', + 'setOptions', + 'setExtraParams', + 'getHtml', + ] + ) ->getMock(); $elementHtmlSelect->expects($this->once()) @@ -330,6 +346,10 @@ protected function mockElementHtmlSelect($defaultCountry, $options, $resultHtml) $elementHtmlSelect->expects($this->once()) ->method('getHtml') ->willReturn($resultHtml); + $this->escaper->expects($this->once()) + ->method('escapeHtmlAttr') + ->with(__($title)) + ->willReturn(__($title)); return $elementHtmlSelect; } diff --git a/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml b/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml index 524c86fbaf604..4037dc19bde33 100644 --- a/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml +++ b/app/code/Magento/Directory/view/adminhtml/templates/js/optional_zip_countries.phtml @@ -10,15 +10,24 @@ * @see \Magento\Backend\Block\Template */ +/** + * @var \Magento\Backend\Block\Template $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> +<?php +/** @var \Magento\Directory\Helper\Data $directoryHelper */ +$directoryHelper = $block->getData('directoryHelper'); +$countriesWithOptionalZip = /* @noEscape */ $directoryHelper->getCountriesWithOptionalZip(true); +$scriptString = <<<script + require([ "prototype", "mage/adminhtml/events" ], function(){ //<![CDATA[ -optionalZipCountries = <?= /* @noEscape */ $this->helper(\Magento\Directory\Helper\Data::class)->getCountriesWithOptionalZip(true) ?>; +optionalZipCountries = {$countriesWithOptionalZip}; function onAddressCountryChanged (countryElement) { var zipElementId = countryElement.id.replace(/country_id/, 'postcode'); @@ -55,4 +64,6 @@ window.optionalZipCountries = optionalZipCountries; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php index 973d52e865dc9..b3c906c1bb9ad 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Composite/Fieldset/Downloadable.php @@ -11,7 +11,7 @@ * * @api * @since 100.0.2 - * @deprecated + * @deprecated 100.3.1 * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends \Magento\Downloadable\Block\Catalog\Product\Links diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php index 8fdf1d395308e..707e9788141c4 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable.php @@ -15,7 +15,7 @@ * Adminhtml catalog product downloadable items tab and form * * @author Magento Core Team <core@magentocommerce.com> - * @deprecated + * @deprecated 100.3.1 * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite */ class Downloadable extends Widget implements TabInterface diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php index e0026765f269b..5895f3a92c54c 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Links.php @@ -11,7 +11,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * - * @deprecated in favor of new class which adds grid links + * @deprecated 100.3.1 in favor of new class which adds grid links * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Links */ class Links extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php index 83a5a93405158..04210d54f38aa 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Catalog/Product/Edit/Tab/Downloadable/Samples.php @@ -10,7 +10,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * - * @deprecated because of new class which adds grids samples + * @deprecated 100.3.1 because of new class which adds grids samples * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Samples */ class Samples extends \Magento\Backend\Block\Widget diff --git a/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php b/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php index fced70593704c..40599efef4f05 100644 --- a/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php +++ b/app/code/Magento/Downloadable/Block/Adminhtml/Sales/Items/Column/Downloadable/Name.php @@ -8,7 +8,9 @@ use Magento\Downloadable\Model\Link; use Magento\Downloadable\Model\Link\Purchased; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\ScopeInterface; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Sales Order downloadable items name column renderer @@ -42,6 +44,7 @@ class Name extends \Magento\Sales\Block\Adminhtml\Items\Column\Name * @param \Magento\Downloadable\Model\Link\PurchasedFactory $purchasedFactory * @param \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\CollectionFactory $itemsFactory * @param array $data + * @param CatalogHelper|null $catalogHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -51,14 +54,18 @@ public function __construct( \Magento\Catalog\Model\Product\OptionFactory $optionFactory, \Magento\Downloadable\Model\Link\PurchasedFactory $purchasedFactory, \Magento\Downloadable\Model\ResourceModel\Link\Purchased\Item\CollectionFactory $itemsFactory, - array $data = [] + array $data = [], + ?CatalogHelper $catalogHelper = null ) { $this->_purchasedFactory = $purchasedFactory; $this->_itemsFactory = $itemsFactory; + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct($context, $stockRegistry, $stockConfiguration, $registry, $optionFactory, $data); } /** + * Return purchased links. + * * @return Purchased */ public function getLinks() @@ -73,6 +80,8 @@ public function getLinks() } /** + * Retunrn links title. + * * @return null|string */ public function getLinksTitle() diff --git a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php index fe430566d63ce..dbd236d5e8827 100644 --- a/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php +++ b/app/code/Magento/Downloadable/Controller/Adminhtml/Downloadable/Product/Edit/Form.php @@ -9,7 +9,7 @@ /** * Class Form * - * @deprecated since downloadable information rendering moved to UI components. + * @deprecated 100.3.1 since downloadable information rendering moved to UI components. * @see \Magento\Downloadable\Ui\DataProvider\Product\Form\Modifier\Composite * @package Magento\Downloadable\Controller\Adminhtml\Downloadable\Product\Edit */ diff --git a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php index c0bc825a8285b..c449f8f54872f 100644 --- a/app/code/Magento/Downloadable/Controller/Download/LinkSample.php +++ b/app/code/Magento/Downloadable/Controller/Download/LinkSample.php @@ -7,8 +7,9 @@ namespace Magento\Downloadable\Controller\Download; -use Magento\Catalog\Model\Product\SalabilityChecker; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Model\Link as LinkModel; +use Magento\Downloadable\Model\RelatedProductRetriever; use Magento\Framework\App\Action\Context; use Magento\Framework\App\ResponseInterface; @@ -20,20 +21,21 @@ class LinkSample extends \Magento\Downloadable\Controller\Download { /** - * @var SalabilityChecker + * @var RelatedProductRetriever */ - private $salabilityChecker; + private $relatedProductRetriever; /** * @param Context $context - * @param SalabilityChecker|null $salabilityChecker + * @param RelatedProductRetriever $relatedProductRetriever */ public function __construct( Context $context, - SalabilityChecker $salabilityChecker = null + RelatedProductRetriever $relatedProductRetriever ) { parent::__construct($context); - $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + + $this->relatedProductRetriever = $relatedProductRetriever; } /** @@ -44,9 +46,10 @@ public function __construct( 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() && $this->salabilityChecker->isSalable($link->getProductId())) { + /** @var LinkModel $link */ + $link = $this->_objectManager->create(LinkModel::class); + $link->load($linkId); + if ($link->getId() && $this->isProductSalable($link)) { $resource = ''; $resourceType = ''; if ($link->getSampleType() == DownloadHelper::LINK_TYPE_URL) { @@ -74,4 +77,16 @@ public function execute() return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } + + /** + * Check is related product salable. + * + * @param LinkModel $link + * @return bool + */ + private function isProductSalable(LinkModel $link): bool + { + $product = $this->relatedProductRetriever->getProduct((int) $link->getProductId()); + return $product ? $product->isSalable() : false; + } } diff --git a/app/code/Magento/Downloadable/Controller/Download/Sample.php b/app/code/Magento/Downloadable/Controller/Download/Sample.php index b95ec510fdd9b..839083b320878 100644 --- a/app/code/Magento/Downloadable/Controller/Download/Sample.php +++ b/app/code/Magento/Downloadable/Controller/Download/Sample.php @@ -3,13 +3,21 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Downloadable\Controller\Download; -use Magento\Catalog\Model\Product\SalabilityChecker; +use Magento\Downloadable\Controller\Download; use Magento\Downloadable\Helper\Download as DownloadHelper; +use Magento\Downloadable\Helper\File; +use Magento\Downloadable\Model\RelatedProductRetriever; +use Magento\Downloadable\Model\Sample as SampleModel; +use Magento\Downloadable\Model\SampleFactory; use Magento\Framework\App\Action\Context; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\CatalogInventory\Api\StockConfigurationInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResponseInterface; /** @@ -17,23 +25,49 @@ * * @SuppressWarnings(PHPMD.AllPurposeAction) */ -class Sample extends \Magento\Downloadable\Controller\Download +class Sample extends Download { /** - * @var SalabilityChecker + * @var RelatedProductRetriever + */ + private $relatedProductRetriever; + + /** + * @var File */ - private $salabilityChecker; + private $file; + + /** + * @var SampleFactory + */ + private $sampleFactory; + + /** + * @var StockConfigurationInterface + */ + private $stockConfiguration; /** * @param Context $context - * @param SalabilityChecker|null $salabilityChecker + * @param RelatedProductRetriever $relatedProductRetriever + * @param File|null $file + * @param SampleFactory|null $sampleFactory + * @param StockConfigurationInterface|null $stockConfiguration */ public function __construct( Context $context, - SalabilityChecker $salabilityChecker = null + RelatedProductRetriever $relatedProductRetriever, + ?File $file = null, + ?SampleFactory $sampleFactory = null, + ?StockConfigurationInterface $stockConfiguration = null ) { parent::__construct($context); - $this->salabilityChecker = $salabilityChecker ?: $this->_objectManager->get(SalabilityChecker::class); + + $this->relatedProductRetriever = $relatedProductRetriever; + $this->file = $file ?: ObjectManager::getInstance()->get(File::class); + $this->sampleFactory = $sampleFactory ?: ObjectManager::getInstance()->get(SampleFactory::class); + $this->stockConfiguration = $stockConfiguration + ?: ObjectManager::getInstance()->get(StockConfigurationInterface::class); } /** @@ -44,31 +78,61 @@ public function __construct( 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() && $this->salabilityChecker->isSalable($sample->getProductId())) { - $resource = ''; - $resourceType = ''; - if ($sample->getSampleType() == DownloadHelper::LINK_TYPE_URL) { - $resource = $sample->getSampleUrl(); - $resourceType = DownloadHelper::LINK_TYPE_URL; - } elseif ($sample->getSampleType() == DownloadHelper::LINK_TYPE_FILE) { - /** @var \Magento\Downloadable\Helper\File $helper */ - $helper = $this->_objectManager->get(\Magento\Downloadable\Helper\File::class); - $resource = $helper->getFilePath($sample->getBasePath(), $sample->getSampleFile()); - $resourceType = DownloadHelper::LINK_TYPE_FILE; - } - try { - $this->_processDownload($resource, $resourceType); - // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage - exit(0); - } catch (\Exception $e) { - $this->messageManager->addError( - __('Sorry, there was an error getting requested content. Please contact the store owner.') - ); - } + /** @var SampleModel $sample */ + $sample = $this->sampleFactory->create(); + $sample->load($sampleId); + if ($this->isCanDownload($sample)) { + $this->download($sample); } return $this->getResponse()->setRedirect($this->_redirect->getRedirectUrl()); } + + /** + * Is sample can be downloaded + * + * @param SampleModel $sample + * @return bool + */ + private function isCanDownload(SampleModel $sample): bool + { + $product = $this->relatedProductRetriever->getProduct((int) $sample->getProductId()); + if ($product && $sample->getId()) { + $isProductEnabled = (int) $product->getStatus() === Status::STATUS_ENABLED; + + return $product->isSalable() || $this->stockConfiguration->isShowOutOfStock() && $isProductEnabled; + } + + return false; + } + + /** + * Download process + * + * @param SampleModel $sample + * @return void + */ + private function download(SampleModel $sample): void + { + $resource = ''; + $resourceType = ''; + + if ($sample->getSampleType() === DownloadHelper::LINK_TYPE_URL) { + $resource = $sample->getSampleUrl(); + $resourceType = DownloadHelper::LINK_TYPE_URL; + } elseif ($sample->getSampleType() === DownloadHelper::LINK_TYPE_FILE) { + $resource = $this->file->getFilePath($sample->getBasePath(), $sample->getSampleFile()); + $resourceType = DownloadHelper::LINK_TYPE_FILE; + } + + try { + $this->_processDownload($resource, $resourceType); + // phpcs:ignore Magento2.Security.LanguageConstruct.ExitUsage + exit(0); + } catch (\Exception $e) { + $this->messageManager->addErrorMessage( + __('Sorry, there was an error getting requested content. Please contact the store owner.') + ); + } + } } diff --git a/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php b/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php index 8e351b3dfb0a5..21bc0a121f5e2 100644 --- a/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php +++ b/app/code/Magento/Downloadable/Model/Link/UpdateHandler.php @@ -3,17 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Downloadable\Model\Link; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Downloadable\Api\LinkRepositoryInterface as LinkRepository; use Magento\Downloadable\Model\Product\Type; use Magento\Framework\EntityManager\Operation\ExtensionInterface; /** - * Class UpdateHandler + * UpdateHandler for downloadable product links */ class UpdateHandler implements ExtensionInterface { + private const GLOBAL_SCOPE_ID = 0; + /** * @var LinkRepository */ @@ -28,35 +34,48 @@ public function __construct(LinkRepository $linkRepository) } /** - * @param object $entity + * Update links for downloadable product if exist + * + * @param ProductInterface $entity * @param array $arguments - * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @return ProductInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($entity, $arguments = []) + public function execute($entity, $arguments = []): ProductInterface { - /** @var $entity \Magento\Catalog\Api\Data\ProductInterface */ - if ($entity->getTypeId() != Type::TYPE_DOWNLOADABLE) { - return $entity; + $links = $entity->getExtensionAttributes()->getDownloadableProductLinks(); + + if ($links && $entity->getTypeId() === Type::TYPE_DOWNLOADABLE) { + $this->updateLinks($entity, $links); } - /** @var \Magento\Downloadable\Api\Data\LinkInterface[] $links */ - $links = $entity->getExtensionAttributes()->getDownloadableProductLinks() ?: []; - $updatedLinks = []; + return $entity; + } + + /** + * Update product links + * + * @param ProductInterface $entity + * @param array $links + * @return void + */ + private function updateLinks(ProductInterface $entity, array $links): void + { + $isGlobalScope = (int) $entity->getStoreId() === self::GLOBAL_SCOPE_ID; $oldLinks = $this->linkRepository->getList($entity->getSku()); + + $updatedLinks = []; foreach ($links as $link) { if ($link->getId()) { $updatedLinks[$link->getId()] = true; } - $this->linkRepository->save($entity->getSku(), $link, !(bool)$entity->getStoreId()); + $this->linkRepository->save($entity->getSku(), $link, $isGlobalScope); } - /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */ + foreach ($oldLinks as $link) { if (!isset($updatedLinks[$link->getId()])) { $this->linkRepository->delete($link->getId()); } } - - return $entity; } } diff --git a/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php new file mode 100644 index 0000000000000..f701f96b910e7 --- /dev/null +++ b/app/code/Magento/Downloadable/Model/RelatedProductRetriever.php @@ -0,0 +1,68 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Downloadable\Model; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\EntityManager\MetadataPool; + +/** + * Related parent product retriever. + */ +class RelatedProductRetriever +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @param ProductRepositoryInterface $productRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param MetadataPool $metadataPool + */ + public function __construct( + ProductRepositoryInterface $productRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + MetadataPool $metadataPool + ) { + $this->productRepository = $productRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->metadataPool = $metadataPool; + } + + /** + * Get related product. + * + * @param int $productId + * @return ProductInterface|null + */ + public function getProduct(int $productId): ?ProductInterface + { + $productMetadata = $this->metadataPool->getMetadata(ProductInterface::class); + + $searchCriteria = $this->searchCriteriaBuilder->addFilter($productMetadata->getLinkField(), $productId) + ->create(); + $items = $this->productRepository->getList($searchCriteria) + ->getItems(); + $product = $items ? array_shift($items) : null; + + return $product; + } +} diff --git a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php index 8d30322745b8d..b7b079d208d97 100644 --- a/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php +++ b/app/code/Magento/Downloadable/Model/ResourceModel/Sample.php @@ -24,7 +24,7 @@ class Sample extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb /** * @param \Magento\Framework\Model\ResourceModel\Db\Context $context * @param \Magento\Framework\EntityManager\MetadataPool $metadataPool - * @param null $connectionName + * @param string|null $connectionName */ public function __construct( \Magento\Framework\Model\ResourceModel\Db\Context $context, @@ -126,7 +126,7 @@ public function getSearchableData($productId, $storeId) )->join( ['cpe' => $this->getTable('catalog_product_entity')], sprintf( - 'cpe.entity_id = m.product_id', + 'cpe.%s = m.product_id', $this->metadataPool->getMetadata(ProductInterface::class)->getLinkField() ), [] diff --git a/app/code/Magento/Downloadable/Model/Sample/Builder.php b/app/code/Magento/Downloadable/Model/Sample/Builder.php index 368d190319766..163e641935db0 100644 --- a/app/code/Magento/Downloadable/Model/Sample/Builder.php +++ b/app/code/Magento/Downloadable/Model/Sample/Builder.php @@ -76,7 +76,8 @@ public function __construct( * Init data for builder * * @param array $data - * @return $this; + * @return $this + * @since 100.1.0 * @since 100.1.0 */ public function setData(array $data) diff --git a/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php b/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php index 80294032aea1b..cb7ff725a21d3 100644 --- a/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php +++ b/app/code/Magento/Downloadable/Model/Sample/UpdateHandler.php @@ -3,17 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Downloadable\Model\Sample; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Downloadable\Api\SampleRepositoryInterface as SampleRepository; use Magento\Downloadable\Model\Product\Type; use Magento\Framework\EntityManager\Operation\ExtensionInterface; /** - * Class UpdateHandler + * UpdateHandler for downloadable product samples */ class UpdateHandler implements ExtensionInterface { + private const GLOBAL_SCOPE_ID = 0; + /** * @var SampleRepository */ @@ -28,35 +34,48 @@ public function __construct(SampleRepository $sampleRepository) } /** - * @param object $entity + * Update samples for downloadable product if exist + * + * @param ProductInterface $entity * @param array $arguments - * @return \Magento\Catalog\Api\Data\ProductInterface|object + * @return ProductInterface * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function execute($entity, $arguments = []) + public function execute($entity, $arguments = []): ProductInterface { - /** @var $entity \Magento\Catalog\Api\Data\ProductInterface */ - if ($entity->getTypeId() != Type::TYPE_DOWNLOADABLE) { - return $entity; + $samples = $entity->getExtensionAttributes()->getDownloadableProductSamples(); + + if ($samples && $entity->getTypeId() === Type::TYPE_DOWNLOADABLE) { + $this->updateSamples($entity, $samples); } - /** @var \Magento\Downloadable\Api\Data\SampleInterface[] $samples */ - $samples = $entity->getExtensionAttributes()->getDownloadableProductSamples() ?: []; - $updatedSamples = []; + return $entity; + } + + /** + * Update product samples + * + * @param ProductInterface $entity + * @param array $samples + * @return void + */ + private function updateSamples(ProductInterface $entity, array $samples): void + { + $isGlobalScope = (int) $entity->getStoreId() === self::GLOBAL_SCOPE_ID; $oldSamples = $this->sampleRepository->getList($entity->getSku()); + + $updatedSamples = []; foreach ($samples as $sample) { if ($sample->getId()) { $updatedSamples[$sample->getId()] = true; } - $this->sampleRepository->save($entity->getSku(), $sample, !(bool)$entity->getStoreId()); + $this->sampleRepository->save($entity->getSku(), $sample, $isGlobalScope); } - /** @var \Magento\Catalog\Api\Data\ProductInterface $entity */ + foreach ($oldSamples as $sample) { if (!isset($updatedSamples[$sample->getId()])) { $this->sampleRepository->delete($sample->getId()); } } - - return $entity; } } diff --git a/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php b/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php index 7f77c8b5f10bf..ea8df05e6a79a 100644 --- a/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php +++ b/app/code/Magento/Downloadable/Observer/IsAllowedGuestCheckoutObserver.php @@ -3,65 +3,140 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Downloadable\Observer; +use Magento\Downloadable\Model\Link; +use Magento\Downloadable\Model\Product\Type; +use Magento\Downloadable\Model\ResourceModel\Link\CollectionFactory as LinkCollectionFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Model\Quote; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +/** + * Checks if guest checkout is allowed then quote contains downloadable products. + */ class IsAllowedGuestCheckoutObserver implements ObserverInterface { + private const XML_PATH_DISABLE_GUEST_CHECKOUT = 'catalog/downloadable/disable_guest_checkout'; + + private const XML_PATH_DOWNLOADABLE_SHAREABLE = 'catalog/downloadable/shareable'; + /** - * Xml path to disable checkout + * @var ScopeConfigInterface */ - const XML_PATH_DISABLE_GUEST_CHECKOUT = 'catalog/downloadable/disable_guest_checkout'; + private $scopeConfig; /** - * Core store config + * Downloadable link collection factory * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var LinkCollectionFactory */ - protected $_scopeConfig; + private $linkCollectionFactory; /** - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param LinkCollectionFactory $linkCollectionFactory + * @param StoreManagerInterface $storeManager */ public function __construct( - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig + ScopeConfigInterface $scopeConfig, + LinkCollectionFactory $linkCollectionFactory, + StoreManagerInterface $storeManager ) { - $this->_scopeConfig = $scopeConfig; + $this->scopeConfig = $scopeConfig; + $this->linkCollectionFactory = $linkCollectionFactory; + $this->storeManager = $storeManager; } /** * Check is allowed guest checkout if quote contain downloadable product(s) * - * @param \Magento\Framework\Event\Observer $observer + * @param Observer $observer * @return $this */ - public function execute(\Magento\Framework\Event\Observer $observer) + public function execute(Observer $observer) { - $store = $observer->getEvent()->getStore(); + $storeId = (int)$this->storeManager->getStore($observer->getEvent()->getStore())->getId(); $result = $observer->getEvent()->getResult(); - if (!$this->_scopeConfig->isSetFlag( + /* @var $quote Quote */ + $quote = $observer->getEvent()->getQuote(); + $isGuestCheckoutDisabled = $this->scopeConfig->isSetFlag( self::XML_PATH_DISABLE_GUEST_CHECKOUT, ScopeInterface::SCOPE_STORE, - $store - )) { - return $this; - } - - /* @var $quote \Magento\Quote\Model\Quote */ - $quote = $observer->getEvent()->getQuote(); + $storeId + ); foreach ($quote->getAllItems() as $item) { - if (($product = $item->getProduct()) - && $product->getTypeId() == \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE - ) { - $result->setIsAllowed(false); - break; + $product = $item->getProduct(); + + if ((string)$product->getTypeId() === Type::TYPE_DOWNLOADABLE) { + if ($isGuestCheckoutDisabled || !$this->checkForShareableLinks($item, $storeId)) { + $result->setIsAllowed(false); + break; + } } } return $this; } + + /** + * Check for shareable link + * + * @param CartItemInterface $item + * @param int $storeId + * @return boolean + */ + private function checkForShareableLinks(CartItemInterface $item, int $storeId): bool + { + $isSharable = true; + $option = $item->getOptionByCode('downloadable_link_ids'); + + if (!empty($option)) { + $downloadableLinkIds = explode(',', $option->getValue()); + + $linkCollection = $this->linkCollectionFactory->create(); + $linkCollection->addFieldToFilter('link_id', ['in' => $downloadableLinkIds]); + $linkCollection->addFieldToFilter('is_shareable', ['in' => $this->getNotSharableValues($storeId)]); + + // We don't have not sharable links + $isSharable = $linkCollection->getSize() === 0; + } + + return $isSharable; + } + + /** + * Returns not sharable values depending on configuration + * + * @param int $storeId + * @return array + */ + private function getNotSharableValues(int $storeId): array + { + $configIsSharable = $this->scopeConfig->isSetFlag( + self::XML_PATH_DOWNLOADABLE_SHAREABLE, + ScopeInterface::SCOPE_STORE, + $storeId + ); + + $notShareableValues = [Link::LINK_SHAREABLE_NO]; + + if (!$configIsSharable) { + $notShareableValues[] = Link::LINK_SHAREABLE_CONFIG; + } + + return $notShareableValues; + } } diff --git a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php index 4f7939da478fa..9351568c5a757 100644 --- a/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php +++ b/app/code/Magento/Downloadable/Observer/SaveDownloadableOrderItemObserver.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Downloadable\Observer; use Magento\Framework\Event\ObserverInterface; @@ -81,12 +83,14 @@ public function __construct( */ public function execute(\Magento\Framework\Event\Observer $observer) { + /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderItem = $observer->getEvent()->getItem(); if (!$orderItem->getId()) { //order not saved in the database return $this; } - if ($orderItem->getProductType() != \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { + $productType = $orderItem->getRealProductType() ?: $orderItem->getProductType(); + if ($productType !== \Magento\Downloadable\Model\Product\Type::TYPE_DOWNLOADABLE) { return $this; } $product = $orderItem->getProduct(); @@ -112,13 +116,13 @@ public function execute(\Magento\Framework\Event\Observer $observer) if ($linkIds = $orderItem->getProductOptionByCode('links')) { $linkPurchased = $this->_createPurchasedModel(); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_order::class, + 'downloadable_sales_copy_order', 'to_downloadable', $orderItem->getOrder(), $linkPurchased ); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_order_item::class, + 'downloadable_sales_copy_order_item', 'to_downloadable', $orderItem, $linkPurchased @@ -131,14 +135,12 @@ public function execute(\Magento\Framework\Event\Observer $observer) ScopeInterface::SCOPE_STORE ); $linkPurchased->setLinkSectionTitle($linkSectionTitle)->save(); - $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_PENDING; if ($orderStatusToEnableItem == \Magento\Sales\Model\Order\Item::STATUS_PENDING || $orderItem->getOrder()->getState() == \Magento\Sales\Model\Order::STATE_COMPLETE ) { $linkStatus = \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE; } - foreach ($linkIds as $linkId) { if (isset($links[$linkId])) { $linkPurchasedItem = $this->_createPurchasedItemModel()->setPurchasedId( @@ -148,7 +150,7 @@ public function execute(\Magento\Framework\Event\Observer $observer) ); $this->_objectCopyService->copyFieldsetToTarget( - \downloadable_sales_copy_link::class, + 'downloadable_sales_copy_link', 'to_purchased', $links[$linkId], $linkPurchasedItem diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/CatalogConfigData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/CatalogConfigData.xml index 8bb81f9c7579d..d8fb9a0497332 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/CatalogConfigData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/CatalogConfigData.xml @@ -18,4 +18,9 @@ <data key="scope_id">0</data> <data key="value">1</data> </entity> + <entity name="EnableShareableDownloadableItems"> + <data key="path">catalog/downloadable/shareable</data> + <data key="scope_id">0</data> + <data key="value">1</data> + </entity> </entities> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml index eb3ad674a0fdf..4c0382e0d444d 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/LinkData.xml @@ -31,6 +31,17 @@ <data key="file">magento-logo.png</data> <data key="sample">https://static.magento.com/sites/all/themes/mag_redesign/images/magento-logo.svg</data> </entity> + <entity name="downloadableLinkSharable" type="downloadable_link"> + <data key="title" unique="suffix">link-1</data> + <data key="price">2.43</data> + <data key="number_of_downloads">2</data> + <data key="sample_type">url</data> + <data key="sample_url">http://example.com</data> + <data key="link_type">url</data> + <data key="link_url">http://example.com</data> + <data key="is_shareable">1</data> + <data key="sort_order">1</data> + </entity> <entity name="downloadableLink1" type="downloadable_link"> <data key="title" unique="suffix">link-1</data> <data key="price">2.43</data> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml index 2986532ef1138..9dca730dfd5c5 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Data/ProductData.xml @@ -105,4 +105,15 @@ <requiredEntity type="downloadable_link">downloadableLink1</requiredEntity> <requiredEntity type="downloadable_link">downloadableLink2</requiredEntity> </entity> + <entity name="DownloadableProductWithoutLinksOutOfStock" type="product"> + <data key="sku" unique="suffix">downloadableproduct</data> + <data key="type_id">downloadable</data> + <data key="attribute_set_id">4</data> + <data key="name" unique="suffix">DownloadableProduct</data> + <data key="price">99.99</data> + <data key="quantity">50</data> + <data key="status">1</data> + <data key="is_in_stock">0</data> + <data key="urlKey" unique="suffix">downloadableproduct</data> + </entity> </entities> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml index c634a8426eac0..44cc15272ff64 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminAddDefaultImageDownloadableProductTest.xml @@ -27,12 +27,11 @@ <argument name="product" value="DownloadableProduct"/> </actionGroup> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="adminProductIndexPageAdd"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="DownloadableProduct"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml index 2f43c6f8278cc..e53c05cfb92cf 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductAndAssignItToCustomStoreTest.xml @@ -48,8 +48,7 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"/> <!-- Create Downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml index d3933ae4fae7d..bfa0c77280f42 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductSwitchToSimpleTest.xml @@ -26,6 +26,10 @@ <actionGroup ref="FillMainProductFormActionGroup" stepKey="fillProductForm"> <argument name="product" value="_defaultProduct"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Simple Product" stepKey="seeProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Simple Product"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml index f1ea344d4e45c..7685017adc426 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithCustomOptionsTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create Downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml index 850a73cd354a5..e711add69b8c8 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithDefaultSetLinksTest.xml @@ -24,8 +24,12 @@ <!-- Create category --> <createData entity="SimpleSubCategory" stepKey="createCategory"/> <!-- Reindex and clear page cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="full_page"/> + </actionGroup> <!-- Login as admin --> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> </before> @@ -44,8 +48,7 @@ </after> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> @@ -76,17 +79,19 @@ <!-- Save product --> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Find downloadable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="DownloadableProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProduct"/> <!-- Assert downloadable links in product form --> <scrollTo selector="{{AdminProductDownloadableSection.sectionLinkGrid}}" stepKey="scrollToLinks"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml index ba2e5e89005cf..ea4c58a17fdd6 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithGroupPriceTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> @@ -82,13 +81,11 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Find downloadable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiDownloadableProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> - <waitForPageLoad stepKey="waitForProductFormPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProduct"/> <!-- Assert downloadable links in product form --> <scrollTo selector="{{AdminProductDownloadableSection.sectionLinkGrid}}" stepKey="scrollToLinks"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml index 9ad20385519d1..34b9701f2dca5 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithInvalidDomainLinkUrlTest.xml @@ -37,7 +37,7 @@ <argument name="link" value="downloadableLink"/> <argument name="index" value="0"/> </actionGroup> - <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductAfterAddingDomainToWhitelist" after="addDownloadableProductLinkAgain" /> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductAfterAddingDomainToAllowlist" after="addDownloadableProductLinkAgain" /> <scrollTo selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="scrollToLinks"/> <click selector="{{StorefrontDownloadableProductSection.downloadableLinkByTitle(downloadableLink.title)}}" stepKey="selectProductLink"/> <see selector="{{CheckoutCartProductSection.ProductPriceByName(DownloadableProduct.name)}}" userInput="$52.99" stepKey="assertProductPriceInCart"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml index 317f2abdf2f23..e7e00d2fb81ef 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithLinkTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml index 0ff7c9bab26ca..1c573ddef79a7 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithManageStockTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> @@ -89,13 +88,11 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Find downloadable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="DownloadableProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> - <waitForPageLoad stepKey="waitForProductFormPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProduct"/> <!-- Assert downloadable links in product form --> <scrollTo selector="{{AdminProductDownloadableSection.sectionLinkGrid}}" stepKey="scrollToLinks"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml index 5615c66762c52..779864aafea55 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithOutOfStockStatusTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create Downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> @@ -81,13 +80,11 @@ <see selector="{{StorefrontProductInfoMainSection.productStockStatus}}" userInput="OUT OF STOCK" stepKey="seeProductStatusInStoreFront"/> <!-- Find downloadable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="DownloadableProductOutOfStock"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> - <waitForPageLoad stepKey="waitForProductFormPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProduct"/> <!-- Assert downloadable links in product form --> <scrollTo selector="{{AdminProductDownloadableSection.sectionLinkGrid}}" stepKey="scrollToLinks"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml index f1d00d83b6666..48a7283cececd 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithSpecialPriceTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> @@ -81,13 +80,11 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Find downloadable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="ApiDownloadableProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> - <waitForPageLoad stepKey="waitForProductFormPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProduct"/> <!-- Assert downloadable links in product form --> <scrollTo selector="{{AdminProductDownloadableSection.sectionLinkGrid}}" stepKey="scrollToLinks"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml index fb0532d9d1fbe..5c5e59bd99689 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutFillingQuantityAndStockTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> @@ -78,13 +77,11 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Find downloadable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="DownloadableProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> - <waitForPageLoad stepKey="waitForProductFormPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProduct"/> <!-- Assert downloadable links in product form --> <scrollTo selector="{{AdminProductDownloadableSection.sectionLinkGrid}}" stepKey="scrollToLinks"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml index 50a2215d441ad..71da70ec823d9 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminCreateDownloadableProductWithoutTaxClassIdTest.xml @@ -42,8 +42,7 @@ </after> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> @@ -79,13 +78,11 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!-- Find downloadable product in grid --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedConfigurableProduct"> <argument name="product" value="DownloadableProduct"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProduct"/> - <waitForPageLoad stepKey="waitForProductFormPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProduct"/> <!-- Assert downloadable links in product form --> <scrollTo selector="{{AdminProductDownloadableSection.sectionLinkGrid}}" stepKey="scrollToLinks"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml index 7062b15aeedbf..e04b53ff208af 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminDeleteDownloadableProductTest.xml @@ -44,10 +44,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createDownloadableProduct.name$$)}}" stepKey="amOnDownloadableProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!-- Search for the product by sku --> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createDownloadableProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createDownloadableProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createDownloadableProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml index aa94de681de1d..0f03a6a47c795 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminProductTypeSwitchingOnEditingTest/AdminSimpleProductTypeSwitchingToDownloadableProductTest.xml @@ -53,8 +53,16 @@ <actionGroup ref="FilterProductGridBySku2ActionGroup" stepKey="filterProductGridBySku"> <argument name="sku" value="$$createProduct.sku$$"/> </actionGroup> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Name')}}" userInput="$$createProduct.name$$" stepKey="seeDownloadableProductNameInGrid"/> - <see selector="{{AdminProductGridSection.productGridCell('1', 'Type')}}" userInput="Downloadable Product" stepKey="seeDownloadableProductTypeInGrid"/> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeDownloadableProductNameInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Name"/> + <argument name="value" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AssertAdminProductGridCellActionGroup" stepKey="seeDownloadableProductTypeInGrid"> + <argument name="row" value="1"/> + <argument name="column" value="Type"/> + <argument name="value" value="Downloadable Product"/> + </actionGroup> <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearDownloadableProductFilters"/> <!--Assert downloadable product on storefront--> <comment userInput="Assert downloadable product on storefront" stepKey="commentAssertDownloadableProductOnStorefront"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml index 27d3d3d10a0b7..a2b418e510482 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/AdminRemoveDefaultImageDownloadableProductTest.xml @@ -24,12 +24,11 @@ </before> <after> <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove static.magento.com"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="adminProductIndexPageAdd"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="DownloadableProduct"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml index 0ac2dc9b04825..6e981794a9b9c 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/EditDownloadableProductWithSeparateLinksFromCartTest.xml @@ -28,8 +28,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml index b9773415059ec..37db99fc2f55b 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/LinkDownloadableProductFromGuestToCustomerTest.xml @@ -20,11 +20,12 @@ <before> <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> <magentoCLI command="config:set {{EnableGuestCheckoutWithDownloadableItems.path}} {{EnableGuestCheckoutWithDownloadableItems.value}}" stepKey="enableGuestCheckoutWithDownloadableItems" /> + <magentoCLI command="config:set {{EnableShareableDownloadableItems.path}} {{EnableShareableDownloadableItems.value}}" stepKey="enableShareableDownloadableItems" /> <createData entity="_defaultCategory" stepKey="createCategory"/> <createData entity="DownloadableProductWithOneLink" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> </createData> - <createData entity="downloadableLink1" stepKey="addDownloadableLink"> + <createData entity="downloadableLinkSharable" stepKey="addDownloadableLink"> <requiredEntity createDataKey="createProduct"/> </createData> </before> @@ -35,7 +36,7 @@ <deleteData stepKey="deleteCategory" createDataKey="createCategory"/> </after> <!--Step 1: Go to Storefront as Guest--> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStorefrontPage"/> <!--Step 2: Add downloadable product to shopping cart--> <amOnPage url="{{StorefrontProductPage.url($$createProduct.custom_attributes[url_key]$$)}}" stepKey="amOnStorefrontProductPage"/> <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml index 4a7f1dde227da..45c8cc71486f3 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/ManualSelectAllDownloadableLinksDownloadableProductTest.xml @@ -28,8 +28,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml index 55c673146021d..0caa23f0f2a7f 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/NewProductsListWidgetDownloadableProductTest.xml @@ -31,8 +31,7 @@ <!-- Create a Downloadable product to appear in the widget --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickAddProduct"/> <click selector="{{AdminProductGridActionSection.addDownloadableProduct}}" stepKey="clickAddDownloadableProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> @@ -49,7 +48,7 @@ <actionGroup ref="AddDownloadableProductLinkWithMaxDownloadsActionGroup" stepKey="addDownloadableLinkWithMaxDownloads"> <argument name="link" value="downloadableLinkWithMaxDownloads"/> </actionGroup> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml index 0ed826e944a4f..64449b9436e11 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/SelectAllDownloadableLinksDownloadableProductTest.xml @@ -28,8 +28,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <!-- Create downloadable product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToSpecifiedCreateProductPageActionGroup" stepKey="createProduct"> <argument name="productType" value="downloadable"/> </actionGroup> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml index d7e0ce3b2ca22..5f89db581a7c1 100644 --- a/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/StorefrontVerifySecureURLRedirectDownloadableTest.xml @@ -28,11 +28,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest.xml b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest.xml new file mode 100644 index 0000000000000..337d4c7dd38b5 --- /dev/null +++ b/app/code/Magento/Downloadable/Test/Mftf/Test/VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="VerifyOutOfStockDownloadableProductSamplesAreAccessibleTest"> + <annotations> + <features value="Downloadable"/> + <stories value="Downloadable product"/> + <title value="Samples of Downloadable Products are accessible, if product is out of stock"/> + <description value="Samples of Downloadable Products are accessible, if product is out of stock"/> + <severity value="MAJOR"/> + <testCaseId value="MC-35639"/> + <group value="downloadable"/> + <group value="catalog"/> + </annotations> + <before> + <!-- Enable show out of stock product --> + <magentoCLI stepKey="enableShowOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 1"/> + + <!-- Add downloadable domains --> + <magentoCLI stepKey="addDownloadableDomain" command="downloadable:domains:add example.com static.magento.com"/> + + <!-- Create category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + + <!-- Create downloadable product --> + <createData entity="DownloadableProductWithoutLinksOutOfStock" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Add downloadable link --> + <createData entity="downloadableLink1" stepKey="addDownloadableLink"> + <requiredEntity createDataKey="createProduct"/> + </createData> + + <!-- Add downloadable sample --> + <createData entity="DownloadableSample" stepKey="addDownloadableSample"> + <requiredEntity createDataKey="createProduct"/> + </createData> + </before> + <after> + <!-- Disable show out of stock product --> + <magentoCLI stepKey="enableShowOutOfStockProduct" command="config:set cataloginventory/options/show_out_of_stock 0"/> + + <!-- Remove downloadable domains --> + <magentoCLI stepKey="removeDownloadableDomain" command="downloadable:domains:remove example.com static.magento.com"/> + + <!-- Delete product --> + <deleteData createDataKey="createProduct" stepKey="deleteDownloadableProduct"/> + + <!-- Delete category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Admin logout --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <!-- Open Downloadable product from precondition on Storefront --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openStorefrontProductPage"> + <argument name="productUrl" value="$createProduct.custom_attributes[url_key]$"/> + </actionGroup> + + <!-- Sample url is accessible --> + <actionGroup ref="AssertStorefrontSeeElementActionGroup" stepKey="seeDownloadableSample"> + <argument name="selector" value="{{StorefrontDownloadableProductSection.downloadableSampleLabel(DownloadableSample.title)}}"/> + </actionGroup> + <click selector="{{StorefrontDownloadableProductSection.downloadableSampleLabel(DownloadableSample.title)}}" stepKey="clickDownloadableSample"/> + <switchToNextTab stepKey="switchToSampleTab"/> + <wait time="2" stepKey="waitToMakeSureThereWillBeNoRedirectToHomePage"/> + <seeInCurrentUrl url="downloadable/download/sample/sample_id/" stepKey="amOnSampleDownloadPage"/> + <closeTab stepKey="closeSampleTab"/> + </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 deleted file mode 100644 index 725c06004f117..0000000000000 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/LinkSampleTest.php +++ /dev/null @@ -1,237 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Test\Unit\Controller\Download; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\SalabilityChecker; -use Magento\Downloadable\Controller\Download\LinkSample; -use Magento\Downloadable\Helper\Data; -use Magento\Downloadable\Helper\Download; -use Magento\Downloadable\Helper\File; -use Magento\Downloadable\Model\Link; -use Magento\Framework\App\Request\Http; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\App\Response\RedirectInterface; -use Magento\Framework\App\ResponseInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Unit tests for \Magento\Downloadable\Controller\Download\LinkSample. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class LinkSampleTest extends TestCase -{ - /** @var LinkSample */ - protected $linkSample; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** - * @var MockObject|Http - */ - protected $request; - - /** - * @var MockObject|ResponseInterface - */ - protected $response; - - /** - * @var MockObject|\Magento\Framework\ObjectManager\ObjectManager - */ - protected $objectManager; - - /** - * @var MockObject|ManagerInterface - */ - protected $messageManager; - - /** - * @var MockObject|RedirectInterface - */ - protected $redirect; - - /** - * @var MockObject|Data - */ - protected $helperData; - - /** - * @var MockObject|\Magento\Downloadable\Helper\Download - */ - protected $downloadHelper; - - /** - * @var MockObject|Product - */ - protected $product; - - /** - * @var MockObject|UrlInterface - */ - protected $urlInterface; - - /** - * @var SalabilityChecker|MockObject - */ - private $salabilityCheckerMock; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp(): void - { - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->request = $this->getMockForAbstractClass(RequestInterface::class); - $this->response = $this->getMockBuilder(ResponseInterface::class) - ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect']) - ->onlyMethods(['sendResponse']) - ->getMockForAbstractClass(); - - $this->helperData = $this->createPartialMock( - Data::class, - ['getIsShareable'] - ); - $this->downloadHelper = $this->createPartialMock( - Download::class, - [ - 'setResource', - 'getFilename', - 'getContentType', - 'getFileSize', - 'getContentDisposition', - 'output' - ] - ); - $this->product = $this->getMockBuilder(Product::class) - ->addMethods(['_wakeup']) - ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); - $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class); - $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class); - $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->linkSample = $this->objectManagerHelper->getObject( - LinkSample::class, - [ - 'objectManager' => $this->objectManager, - 'request' => $this->request, - 'response' => $this->response, - 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect, - 'salabilityChecker' => $this->salabilityCheckerMock, - ] - ); - } - - /** - * Execute Download link's sample action with Url link. - * - * @return void - */ - public function testExecuteLinkTypeUrl() - { - $linkMock = $this->getMockBuilder(Link::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id'); - $this->objectManager->expects($this->once()) - ->method('create') - ->with(Link::class) - ->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( - Download::LINK_TYPE_URL - ); - $linkMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url'); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->linkSample->execute()); - } - - /** - * Execute Download link's sample action with File link. - * - * @return void - */ - public function testExecuteLinkTypeFile() - { - $linkMock = $this->getMockBuilder(Link::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath']) - ->getMock(); - $fileMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['getFilePath', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('link_id', 0)->willReturn('some_link_id'); - $this->objectManager->expects($this->at(0)) - ->method('create') - ->with(Link::class) - ->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( - Download::LINK_TYPE_FILE - ); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(File::class) - ->willReturn($fileMock); - $this->objectManager->expects($this->at(2)) - ->method('get') - ->with(Link::class) - ->willReturn($linkMock); - $linkMock->expects($this->once())->method('getBaseSamplePath')->willReturn('downloadable/files/link_samples'); - $this->objectManager->expects($this->at(3)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->linkSample->execute()); - } -} diff --git a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php b/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php deleted file mode 100644 index 6dcd09a91dd2e..0000000000000 --- a/app/code/Magento/Downloadable/Test/Unit/Controller/Download/SampleTest.php +++ /dev/null @@ -1,232 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\Downloadable\Test\Unit\Controller\Download; - -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\SalabilityChecker; -use Magento\Downloadable\Controller\Download\Sample; -use Magento\Downloadable\Helper\Data; -use Magento\Downloadable\Helper\Download; -use Magento\Downloadable\Helper\File; -use Magento\Framework\App\Request\Http; -use Magento\Framework\App\RequestInterface; -use Magento\Framework\App\Response\RedirectInterface; -use Magento\Framework\App\ResponseInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\Framework\UrlInterface; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * Unit tests for \Magento\Downloadable\Controller\Download\Sample. - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class SampleTest extends TestCase -{ - /** @var \Magento\Downloadable\Controller\Download\Sample */ - protected $sample; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** - * @var MockObject|Http - */ - protected $request; - - /** - * @var MockObject|ResponseInterface - */ - protected $response; - - /** - * @var MockObject|\Magento\Framework\ObjectManager\ObjectManager - */ - protected $objectManager; - - /** - * @var MockObject|ManagerInterface - */ - protected $messageManager; - - /** - * @var MockObject|RedirectInterface - */ - protected $redirect; - - /** - * @var MockObject|Data - */ - protected $helperData; - - /** - * @var MockObject|\Magento\Downloadable\Helper\Download - */ - protected $downloadHelper; - - /** - * @var MockObject|Product - */ - protected $product; - - /** - * @var MockObject|UrlInterface - */ - protected $urlInterface; - - /** - * @var SalabilityChecker|MockObject - */ - private $salabilityCheckerMock; - - /** - * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - */ - protected function setUp(): void - { - $this->objectManagerHelper = new ObjectManagerHelper($this); - - $this->request = $this->getMockForAbstractClass(RequestInterface::class); - $this->response = $this->getMockBuilder(ResponseInterface::class) - ->addMethods(['setHttpResponseCode', 'clearBody', 'sendHeaders', 'setHeader', 'setRedirect']) - ->onlyMethods(['sendResponse']) - ->getMockForAbstractClass(); - - $this->helperData = $this->createPartialMock( - Data::class, - ['getIsShareable'] - ); - $this->downloadHelper = $this->createPartialMock( - Download::class, - [ - 'setResource', - 'getFilename', - 'getContentType', - 'getFileSize', - 'getContentDisposition', - 'output' - ] - ); - $this->product = $this->getMockBuilder(Product::class) - ->addMethods(['_wakeup']) - ->onlyMethods(['load', 'getId', 'getProductUrl', 'getName']) - ->disableOriginalConstructor() - ->getMock(); - $this->messageManager = $this->getMockForAbstractClass(ManagerInterface::class); - $this->redirect = $this->getMockForAbstractClass(RedirectInterface::class); - $this->urlInterface = $this->getMockForAbstractClass(UrlInterface::class); - $this->salabilityCheckerMock = $this->createMock(SalabilityChecker::class); - $this->objectManager = $this->createPartialMock( - \Magento\Framework\ObjectManager\ObjectManager::class, - ['create', 'get'] - ); - $this->sample = $this->objectManagerHelper->getObject( - Sample::class, - [ - 'objectManager' => $this->objectManager, - 'request' => $this->request, - 'response' => $this->response, - 'messageManager' => $this->messageManager, - 'redirect' => $this->redirect, - 'salabilityChecker' => $this->salabilityCheckerMock, - ] - ); - } - - /** - * Execute Download sample action with Sample Url. - * - * @return void - */ - public function testExecuteSampleWithUrlType() - { - $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id'); - $this->objectManager->expects($this->once()) - ->method('create') - ->with(\Magento\Downloadable\Model\Sample::class) - ->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( - Download::LINK_TYPE_URL - ); - $sampleMock->expects($this->once())->method('getSampleUrl')->willReturn('sample_url'); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->sample->execute()); - } - - /** - * Execute Download sample action with Sample File. - * - * @return void - */ - public function testExecuteSampleWithFileType() - { - $sampleMock = $this->getMockBuilder(\Magento\Downloadable\Model\Sample::class) - ->disableOriginalConstructor() - ->setMethods(['getId', 'load', 'getSampleType', 'getSampleUrl', 'getBaseSamplePath']) - ->getMock(); - $fileHelperMock = $this->getMockBuilder(File::class) - ->disableOriginalConstructor() - ->setMethods(['getFilePath']) - ->getMock(); - - $this->request->expects($this->once())->method('getParam')->with('sample_id', 0)->willReturn('some_sample_id'); - $this->objectManager->expects($this->at(0)) - ->method('create') - ->with(\Magento\Downloadable\Model\Sample::class) - ->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( - Download::LINK_TYPE_FILE - ); - $this->objectManager->expects($this->at(1)) - ->method('get') - ->with(File::class) - ->willReturn($fileHelperMock); - $fileHelperMock->expects($this->once())->method('getFilePath')->willReturn('file_path'); - $this->objectManager->expects($this->at(2)) - ->method('get') - ->with(Download::class) - ->willReturn($this->downloadHelper); - $this->response->expects($this->once())->method('setHttpResponseCode')->with(200)->willReturnSelf(); - $this->response->expects($this->any())->method('setHeader')->willReturnSelf(); - $this->downloadHelper->expects($this->once())->method('output')->willThrowException(new \Exception()); - $this->messageManager->expects($this->once()) - ->method('addError') - ->with('Sorry, there was an error getting requested content. Please contact the store owner.') - ->willReturnSelf(); - $this->redirect->expects($this->once())->method('getRedirectUrl')->willReturn('redirect_url'); - $this->response->expects($this->once())->method('setRedirect')->with('redirect_url')->willReturnSelf(); - - $this->assertEquals($this->response, $this->sample->execute()); - } -} diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php index 069e8a4e1a3d9..22cf4b9abf7ca 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Link/UpdateHandlerTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Downloadable\Test\Unit\Model\Link; @@ -16,37 +17,72 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Downloadable\Model\Link\UpdateHandler. + */ class UpdateHandlerTest extends TestCase { - /** @var UpdateHandler */ - protected $model; - - /** @var LinkRepositoryInterface|MockObject */ - protected $linkRepositoryMock; - + /** + * @var UpdateHandler + */ + private $model; + + /** + * @var LinkRepositoryInterface|MockObject + */ + private $linkRepositoryMock; + + /** + * @var LinkInterface|MockObject + */ + private $linkMock; + + /** + * @var ProductExtensionInterface|MockObject + */ + private $productExtensionMock; + + /** + * @var ProductInterface|MockObject + */ + private $entityMock; + + /** + * @inheritdoc + */ protected function setUp(): void { $this->linkRepositoryMock = $this->getMockBuilder(LinkRepositoryInterface::class) ->getMockForAbstractClass(); + $this->linkMock = $this->getMockBuilder(LinkInterface::class) + ->getMock(); + $this->productExtensionMock = $this->createMock(ProductExtensionInterface::class); + $this->productExtensionMock->expects($this->once()) + ->method('getDownloadableProductLinks') + ->willReturn([$this->linkMock]); + $this->entityMock = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getStoreId']) + ->getMockForAbstractClass(); $this->model = new UpdateHandler( $this->linkRepositoryMock ); } - public function testExecute() + /** + * Update links for downloadable product + * + * @return void + */ + public function testExecute(): void { $entitySku = 'sku'; $entityStoreId = 0; - $linkId = 11; $linkToDeleteId = 22; - /** @var LinkInterface|MockObject $linkMock */ - $linkMock = $this->getMockBuilder(LinkInterface::class) - ->getMock(); - $linkMock->expects($this->exactly(3)) + $this->linkMock->expects($this->exactly(3)) ->method('getId') - ->willReturn($linkId); + ->willReturn(1); /** @var LinkInterface|MockObject $linkToDeleteMock */ $linkToDeleteMock = $this->getMockBuilder(LinkInterface::class) @@ -55,59 +91,49 @@ public function testExecute() ->method('getId') ->willReturn($linkToDeleteId); - /** @var ProductExtensionInterface|MockObject $productExtensionMock */ - $productExtensionMock = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods(['getDownloadableProductLinks']) - ->getMockForAbstractClass(); - $productExtensionMock->expects($this->once()) - ->method('getDownloadableProductLinks') - ->willReturn([$linkMock]); - - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getExtensionAttributes') - ->willReturn($productExtensionMock); - $entityMock->expects($this->exactly(2)) + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->exactly(2)) ->method('getSku') ->willReturn($entitySku); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getStoreId') ->willReturn($entityStoreId); $this->linkRepositoryMock->expects($this->once()) ->method('getList') ->with($entitySku) - ->willReturn([$linkMock, $linkToDeleteMock]); + ->willReturn([$this->linkMock, $linkToDeleteMock]); $this->linkRepositoryMock->expects($this->once()) ->method('save') - ->with($entitySku, $linkMock, !$entityStoreId); + ->with($entitySku, $this->linkMock, !$entityStoreId); $this->linkRepositoryMock->expects($this->once()) ->method('delete') ->with($linkToDeleteId); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } - public function testExecuteNonDownloadable() + /** + * Update links for non downloadable product + * + * @return void + */ + public function testExecuteNonDownloadable(): void { - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE . 'some'); - $entityMock->expects($this->never()) - ->method('getExtensionAttributes'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->never()) ->method('getSku'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->never()) ->method('getStoreId'); $this->linkRepositoryMock->expects($this->never()) @@ -117,6 +143,6 @@ public function testExecuteNonDownloadable() $this->linkRepositoryMock->expects($this->never()) ->method('delete'); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php b/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php index 34d313a175b55..0f8fe92e467ce 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Model/Sample/UpdateHandlerTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Downloadable\Test\Unit\Model\Sample; @@ -16,37 +17,72 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Downloadable\Model\Sample\UpdateHandler. + */ class UpdateHandlerTest extends TestCase { - /** @var UpdateHandler */ - protected $model; - - /** @var SampleRepositoryInterface|MockObject */ - protected $sampleRepositoryMock; - + /** + * @var UpdateHandler + */ + private $model; + + /** + * @var SampleRepositoryInterface|MockObject + */ + private $sampleRepositoryMock; + + /** + * @var SampleInterface|MockObject + */ + private $sampleMock; + + /** + * @var ProductExtensionInterface|MockObject + */ + private $productExtensionMock; + + /** + * @var ProductInterface|MockObject + */ + private $entityMock; + + /** + * @inheritdoc + */ protected function setUp(): void { $this->sampleRepositoryMock = $this->getMockBuilder(SampleRepositoryInterface::class) ->getMockForAbstractClass(); + $this->sampleMock = $this->getMockBuilder(SampleInterface::class) + ->getMock(); + $this->productExtensionMock = $this->createMock(ProductExtensionInterface::class); + $this->productExtensionMock//->expects($this->once()) + ->method('getDownloadableProductSamples') + ->willReturn([$this->sampleMock]); + $this->entityMock = $this->getMockBuilder(ProductInterface::class) + ->addMethods(['getStoreId']) + ->getMockForAbstractClass(); $this->model = new UpdateHandler( $this->sampleRepositoryMock ); } - public function testExecute() + /** + * Update samples for downloadable product + * + * @return void + */ + public function testExecute(): void { $entitySku = 'sku'; $entityStoreId = 0; - $sampleId = 11; $sampleToDeleteId = 22; - /** @var SampleInterface|MockObject $sampleMock */ - $sampleMock = $this->getMockBuilder(SampleInterface::class) - ->getMock(); - $sampleMock->expects($this->exactly(3)) + $this->sampleMock->expects($this->exactly(3)) ->method('getId') - ->willReturn($sampleId); + ->willReturn(1); /** @var SampleInterface|MockObject $sampleToDeleteMock */ $sampleToDeleteMock = $this->getMockBuilder(SampleInterface::class) @@ -55,59 +91,49 @@ public function testExecute() ->method('getId') ->willReturn($sampleToDeleteId); - /** @var ProductExtensionInterface|MockObject $productExtensionMock */ - $productExtensionMock = $this->getMockBuilder(ProductExtensionInterface::class) - ->setMethods(['getDownloadableProductSamples']) - ->getMockForAbstractClass(); - $productExtensionMock->expects($this->once()) - ->method('getDownloadableProductSamples') - ->willReturn([$sampleMock]); - - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getExtensionAttributes') - ->willReturn($productExtensionMock); - $entityMock->expects($this->exactly(2)) + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->exactly(2)) ->method('getSku') ->willReturn($entitySku); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getStoreId') ->willReturn($entityStoreId); $this->sampleRepositoryMock->expects($this->once()) ->method('getList') ->with($entitySku) - ->willReturn([$sampleMock, $sampleToDeleteMock]); + ->willReturn([$this->sampleMock, $sampleToDeleteMock]); $this->sampleRepositoryMock->expects($this->once()) ->method('save') - ->with($entitySku, $sampleMock, !$entityStoreId); + ->with($entitySku, $this->sampleMock, !$entityStoreId); $this->sampleRepositoryMock->expects($this->once()) ->method('delete') ->with($sampleToDeleteId); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } - public function testExecuteNonDownloadable() + /** + * Update samples for non downloadable product + * + * @return void + */ + public function testExecuteNonDownloadable(): void { - /** @var ProductInterface|MockObject $entityMock */ - $entityMock = $this->getMockBuilder(ProductInterface::class) - ->setMethods(['getTypeId', 'getExtensionAttributes', 'getSku', 'getStoreId']) - ->getMockForAbstractClass(); - $entityMock->expects($this->once()) + $this->entityMock->expects($this->once()) ->method('getTypeId') ->willReturn(Type::TYPE_DOWNLOADABLE . 'some'); - $entityMock->expects($this->never()) - ->method('getExtensionAttributes'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->once()) + ->method('getExtensionAttributes') + ->willReturn($this->productExtensionMock); + $this->entityMock->expects($this->never()) ->method('getSku'); - $entityMock->expects($this->never()) + $this->entityMock->expects($this->never()) ->method('getStoreId'); $this->sampleRepositoryMock->expects($this->never()) @@ -117,6 +143,6 @@ public function testExecuteNonDownloadable() $this->sampleRepositoryMock->expects($this->never()) ->method('delete'); - $this->assertEquals($entityMock, $this->model->execute($entityMock)); + $this->assertEquals($this->entityMock, $this->model->execute($this->entityMock)); } } diff --git a/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php index 1973715bfb645..6040b301a60a8 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Observer/IsAllowedGuestCheckoutObserverTest.php @@ -16,8 +16,9 @@ use Magento\Framework\Event\Observer; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Quote\Model\Quote; -use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,13 +27,17 @@ */ class IsAllowedGuestCheckoutObserverTest extends TestCase { + private const XML_PATH_DISABLE_GUEST_CHECKOUT = 'catalog/downloadable/disable_guest_checkout'; + + private const STUB_STORE_ID = 1; + /** @var IsAllowedGuestCheckoutObserver */ private $isAllowedGuestCheckoutObserver; /** * @var MockObject|Config */ - private $scopeConfig; + private $scopeConfigMock; /** * @var MockObject|DataObject @@ -54,13 +59,18 @@ class IsAllowedGuestCheckoutObserverTest extends TestCase */ private $storeMock; + /** + * @var MockObject|StoreManagerInterface + */ + private $storeManagerMock; + /** * Sets up the fixture, for example, open a network connection. * This method is called before a test is executed. */ protected function setUp(): void { - $this->scopeConfig = $this->getMockBuilder(Config::class) + $this->scopeConfigMock = $this->getMockBuilder(Config::class) ->disableOriginalConstructor() ->setMethods(['isSetFlag', 'getValue']) ->getMock(); @@ -84,12 +94,20 @@ protected function setUp(): void ->disableOriginalConstructor() ->getMock(); - $this->isAllowedGuestCheckoutObserver = (new ObjectManagerHelper($this))->getObject( - IsAllowedGuestCheckoutObserver::class, - [ - 'scopeConfig' => $this->scopeConfig, - ] - ); + $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->storeManagerMock + ->method('getStore') + ->with(self::STUB_STORE_ID) + ->willReturn($this->storeMock); + + $this->isAllowedGuestCheckoutObserver = (new ObjectManagerHelper($this)) + ->getObject( + IsAllowedGuestCheckoutObserver::class, + [ + 'scopeConfig' => $this->scopeConfigMock, + 'storeManager'=> $this->storeManagerMock + ] + ); } /** @@ -99,7 +117,7 @@ protected function setUp(): void * @param $productType * @param $isAllowed */ - public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllowed) + public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllowed): void { if ($isAllowed) { $this->resultMock->expects($this->at(0)) @@ -116,7 +134,7 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow ->method('getTypeId') ->willReturn($productType); - $item = $this->getMockBuilder(Item::class) + $item = $this->getMockBuilder(QuoteItem::class) ->disableOriginalConstructor() ->setMethods(['getProduct']) ->getMock(); @@ -138,6 +156,10 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow ->method('getStore') ->willReturn($this->storeMock); + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn(self::STUB_STORE_ID); + $this->eventMock->expects($this->once()) ->method('getResult') ->willReturn($this->resultMock); @@ -146,12 +168,12 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow ->method('getQuote') ->willReturn($quote); - $this->scopeConfig->expects($this->once()) + $this->scopeConfigMock->expects($this->any()) ->method('isSetFlag') ->with( - IsAllowedGuestCheckoutObserver::XML_PATH_DISABLE_GUEST_CHECKOUT, + self::XML_PATH_DISABLE_GUEST_CHECKOUT, ScopeInterface::SCOPE_STORE, - $this->storeMock + self::STUB_STORE_ID ) ->willReturn(true); @@ -168,7 +190,7 @@ public function testIsAllowedGuestCheckoutConfigSetToTrue($productType, $isAllow /** * @return array */ - public function dataProviderForTestisAllowedGuestCheckoutConfigSetToTrue() + public function dataProviderForTestisAllowedGuestCheckoutConfigSetToTrue(): array { return [ 1 => [Type::TYPE_DOWNLOADABLE, true], @@ -176,26 +198,61 @@ public function dataProviderForTestisAllowedGuestCheckoutConfigSetToTrue() ]; } - public function testIsAllowedGuestCheckoutConfigSetToFalse() + public function testIsAllowedGuestCheckoutConfigSetToFalse(): void { + $product = $this->getMockBuilder(Product::class) + ->disableOriginalConstructor() + ->setMethods(['getTypeId']) + ->getMock(); + + $product->expects($this->once()) + ->method('getTypeId') + ->willReturn(Type::TYPE_DOWNLOADABLE); + + $item = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getProduct']) + ->getMock(); + + $item->expects($this->once()) + ->method('getProduct') + ->willReturn($product); + + $quote = $this->getMockBuilder(Quote::class) + ->disableOriginalConstructor() + ->setMethods(['getAllItems']) + ->getMock(); + + $quote->expects($this->once()) + ->method('getAllItems') + ->willReturn([$item]); + $this->eventMock->expects($this->once()) ->method('getStore') ->willReturn($this->storeMock); + $this->storeMock->expects($this->any()) + ->method('getId') + ->willReturn(self::STUB_STORE_ID); + $this->eventMock->expects($this->once()) ->method('getResult') ->willReturn($this->resultMock); - $this->scopeConfig->expects($this->once()) + $this->eventMock->expects($this->once()) + ->method('getQuote') + ->will($this->returnValue($quote)); + + $this->scopeConfigMock->expects($this->once()) ->method('isSetFlag') ->with( - IsAllowedGuestCheckoutObserver::XML_PATH_DISABLE_GUEST_CHECKOUT, + self::XML_PATH_DISABLE_GUEST_CHECKOUT, ScopeInterface::SCOPE_STORE, - $this->storeMock + self::STUB_STORE_ID ) ->willReturn(false); - $this->observerMock->expects($this->exactly(2)) + $this->observerMock->expects($this->exactly(3)) ->method('getEvent') ->willReturn($this->eventMock); diff --git a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php index 80f23c859a031..09edbf4935fe4 100644 --- a/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php +++ b/app/code/Magento/Downloadable/Test/Unit/Observer/SaveDownloadableOrderItemObserverTest.php @@ -176,6 +176,9 @@ public function testSaveDownloadableOrderItem() $itemMock->expects($this->any()) ->method('getProductType') ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); + $itemMock->expects($this->any()) + ->method('getRealProductType') + ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); $this->orderMock->expects($this->once()) ->method('getStoreId') @@ -311,6 +314,9 @@ public function testSaveDownloadableOrderItemSavedPurchasedLink() $itemMock->expects($this->any()) ->method('getProductType') ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); + $itemMock->expects($this->any()) + ->method('getRealProductType') + ->willReturn(DownloadableProductType::TYPE_DOWNLOADABLE); $purchasedLink = $this->getMockBuilder(Purchased::class) ->disableOriginalConstructor() diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml index 94c8405c718a8..91dd22bc3ce5c 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/creditmemo/name.phtml @@ -3,42 +3,53 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** + * @var \Magento\Downloadable\Block\Adminhtml\Sales\Items\Column\Downloadable\Name $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($_item = $block->getItem()) : ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +if ($_item = $block->getItem()): ?> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($block->getSku())) ?></div> - <?php if ($block->getOrderOptions()) : ?> + <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->getSku())) ?></div> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= $block->escapeHtml($_option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($_option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); - + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){ $('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){ $('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> - <?php if ($block->getLinks()) : ?> + <?php if ($block->getLinks()): ?> <dl class="item-options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> - <?php foreach ($block->getLinks()->getPurchasedItems() as $_link) : ?> + <?php foreach ($block->getLinks()->getPurchasedItems() as $_link): ?> <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?></dd> <?php endforeach; ?> </dl> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml index 9a45066f64d15..a0b710bdb5d17 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/invoice/name.phtml @@ -3,42 +3,55 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var \Magento\Downloadable\Block\Adminhtml\Sales\Items\Column\Downloadable\Name $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($_item = $block->getItem()) : ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +if ($_item = $block->getItem()): ?> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> - <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($block->getSku())) ?></div> - <?php if ($block->getOrderOptions()) : ?> + <div><strong><?= $block->escapeHtml(__('SKU')) ?>:</strong> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->getSku())) ?></div> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?></dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= $block->escapeHtml($_option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($_option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){ $('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){ $('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> - <?php if ($block->getLinks()) : ?> + <?php if ($block->getLinks()): ?> <dl class="item-options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?></dt> - <?php foreach ($block->getLinks()->getPurchasedItems() as $_link) : ?> - <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> (<?= $block->escapeHtml($_link->getNumberOfDownloadsBought() ? $_link->getNumberOfDownloadsBought() : __('Unlimited')) ?>)</dd> + <?php foreach ($block->getLinks()->getPurchasedItems() as $_link): ?> + <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> + (<?= $block->escapeHtml($_link->getNumberOfDownloadsBought() ? + $_link->getNumberOfDownloadsBought() : __('Unlimited')) ?>) + </dd> <?php endforeach; ?> </dl> <?php endif; ?> diff --git a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml index b5fe7b3385630..7808a214dd76a 100644 --- a/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml +++ b/app/code/Magento/Downloadable/view/adminhtml/templates/sales/items/column/downloadable/name.phtml @@ -3,45 +3,57 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var \Magento\Downloadable\Block\Adminhtml\Sales\Items\Column\Downloadable\Name $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($_item = $block->getItem()) : ?> +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); +if ($_item = $block->getItem()): ?> <div class="product-title"><?= $block->escapeHtml($_item->getName()) ?></div> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(Magento\Catalog\Helper\Data::class)->splitSku($block->getSku())) ?> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->getSku())) ?> </div> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?>:</dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= $block->escapeHtml($_option['value']) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->truncateString($_option['value'], 55, '', $_remainder)) ?> - <?php if ($_remainder) :?> - ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"><?= $block->escapeHtml($_remainder) ?></span> - <script> + <?php if ($_remainder):?> + ... <span id="<?= $block->escapeHtmlAttr($_id = 'id' . uniqid()) ?>"> + <?= $block->escapeHtml($_remainder) ?> + </span> + <?php $escapedId = /* @noEscape */ $block->escapeJs($_id); + $scriptString = <<<script require(['prototype'], function(){ - <?php $escapedId = $block->escapeJs($_id) ?> - $('<?= /* @noEscape */ $escapedId ?>').hide(); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $escapedId ?>').show();}); - $('<?= /* @noEscape */ $escapedId ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $escapedId ?>').hide();}); + $('{$escapedId}').hide(); + $('{$escapedId}').up().observe('mouseover', function(){ $('{$escapedId}').show();}); + $('{$escapedId}').up().observe('mouseout', function(){ $('{$escapedId}').hide();}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> <?php endif;?> </dd> <?php endforeach; ?> </dl> <?php endif; ?> - <?php if ($block->getLinks()) : ?> + <?php if ($block->getLinks()): ?> <dl class="item-options"> <dt><?= $block->escapeHtml($block->getLinksTitle()) ?>:</dt> - <?php foreach ($block->getLinks()->getPurchasedItems() as $_link) : ?> - <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> (<?= $block->escapeHtml($_link->getNumberOfDownloadsUsed() . ' / ' . ($_link->getNumberOfDownloadsBought() ? $_link->getNumberOfDownloadsBought() : __('U'))) ?>)</dd> + <?php foreach ($block->getLinks()->getPurchasedItems() as $_link): ?> + <dd><?= $block->escapeHtml($_link->getLinkTitle()) ?> + (<?= $block->escapeHtml($_link->getNumberOfDownloadsUsed() . ' / ' . + ($_link->getNumberOfDownloadsBought() ? $_link->getNumberOfDownloadsBought() : __('U'))) ?>) + </dd> <?php endforeach; ?> </dl> <?php endif; ?> diff --git a/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml b/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml index eca72b3500924..4935743c2de7d 100644 --- a/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml +++ b/app/code/Magento/Downloadable/view/frontend/templates/customer/products/list.phtml @@ -3,14 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php + +use Magento\Downloadable\Model\Link\Purchased\Item; + /** * @var $block \Magento\Downloadable\Block\Customer\Products\ListProducts + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_items = $block->getItems(); ?> -<?php if (count($_items)) : ?> +<?php if (count($_items)): ?> <div class="table-wrapper downloadable-products"> <table id="my-downloadable-products-table" class="data table table-downloadable-products"> <caption class="table-caption"><?= $block->escapeHtml(__('Downloadable Products')) ?></caption> @@ -24,35 +26,57 @@ </tr> </thead> <tbody> - <?php foreach ($_items as $_item) : ?> + <?php foreach ($_items as $_item): ?> <tr> <td data-th="<?= $block->escapeHtmlAttr(__('Order #')) ?>" class="col id"> - <a href="<?= $block->escapeUrl($block->getOrderViewUrl($_item->getPurchased()->getOrderId())) ?>" + <a href="<?= $block->escapeUrl($block->getOrderViewUrl($_item->getPurchased()->getOrderId()))?>" title="<?= $block->escapeHtml(__('View Order')) ?>"> <?= $block->escapeHtml($_item->getPurchased()->getOrderIncrementId()) ?> </a> </td> - <td data-th="<?= $block->escapeHtmlAttr(__('Date')) ?>" class="col date"><?= $block->escapeHtml($block->formatDate($_item->getPurchased()->getCreatedAt())) ?></td> + <td data-th="<?= $block->escapeHtmlAttr(__('Date')) ?>" class="col date"> + <?= $block->escapeHtml($block->formatDate($_item->getPurchased()->getCreatedAt())) ?> + </td> <td data-th="<?= $block->escapeHtmlAttr(__('Title')) ?>" class="col title"> - <strong class="product-name"><?= $block->escapeHtml($_item->getPurchased()->getProductName()) ?></strong> - <?php if ($_item->getStatus() == \Magento\Downloadable\Model\Link\Purchased\Item::LINK_STATUS_AVAILABLE) : ?> - <a href="<?= $block->escapeUrl($block->getDownloadUrl($_item)) ?>" title="<?= $block->escapeHtmlAttr(__('Start Download')) ?>" class="action download" <?= /* @noEscape */ $block->getIsOpenInNewWindow() ? 'onclick="this.target=\'_blank\'"' : '' ?>><?= $block->escapeHtml($_item->getLinkTitle()) ?></a> + <strong class="product-name"> + <?= $block->escapeHtml($_item->getPurchased()->getProductName()) ?> + </strong> + <?php if ($_item->getStatus() == Item::LINK_STATUS_AVAILABLE): ?> + <a href="<?= $block->escapeUrl($block->getDownloadUrl($_item)) ?>" + id="download_<?= /* @noEscape */ $_item->getPurchased()->getProductId() ?>" + title="<?= $block->escapeHtmlAttr(__('Start Download')) ?>" + class="action download"> + <?= $block->escapeHtml($_item->getLinkTitle()) ?> + </a> + <?php if ($block->getIsOpenInNewWindow()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "this.target='_blank'", + 'a#download_' . $_item->getPurchased()->getProductId() + ) ?> + <?php endif; ?> <?php endif; ?> </td> - <td data-th="<?= $block->escapeHtmlAttr(__('Status')) ?>" class="col status"><?= $block->escapeHtml(__(ucfirst($_item->getStatus()))) ?></td> - <td data-th="<?= $block->escapeHtmlAttr(__('Remaining Downloads')) ?>" class="col remaining"><?= $block->escapeHtml($block->getRemainingDownloads($_item)) ?></td> + <td data-th="<?= $block->escapeHtmlAttr(__('Status')) ?>" class="col status"> + <?= $block->escapeHtml(__(ucfirst($_item->getStatus()))) ?> + </td> + <td data-th="<?= $block->escapeHtmlAttr(__('Remaining Downloads')) ?>" class="col remaining"> + <?= $block->escapeHtml($block->getRemainingDownloads($_item)) ?> + </td> </tr> <?php endforeach; ?> </tbody> </table> </div> - <?php if ($block->getChildHtml('pager')) : ?> + <?php if ($block->getChildHtml('pager')): ?> <div class="toolbar downloadable-products-toolbar bottom"> <?= $block->getChildHtml('pager') ?> </div> <?php endif; ?> -<?php else : ?> - <div class="message info empty"><span><?= $block->escapeHtml(__('You have not purchased any downloadable products yet.')) ?></span></div> +<?php else: ?> + <div class="message info empty"> + <span><?= $block->escapeHtml(__('You have not purchased any downloadable products yet.')) ?></span> + </div> <?php endif; ?> <div class="actions-toolbar"> diff --git a/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php b/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php new file mode 100644 index 0000000000000..03727597104fd --- /dev/null +++ b/app/code/Magento/DownloadableGraphQl/Resolver/Product/DownloadableLinksValueUid.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\DownloadableGraphQl\Resolver\Product; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Formatting the uid for downloadable link + */ +class DownloadableLinksValueUid implements ResolverInterface +{ + /** + * Option type name + */ + private const OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['id']) || empty($value['id'])) { + throw new GraphQlInputException(__('"id" value should be specified.')); + } + + $optionDetails = [ + self::OPTION_TYPE, + $value['id'] + ]; + + $content = implode('/', $optionDetails); + + // phpcs:ignore Magento2.Functions.DiscouragedFunction + return base64_encode($content); + } +} diff --git a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls index 2226f1acd8501..5863e62e81b1b 100644 --- a/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls +++ b/app/code/Magento/DownloadableGraphQl/etc/schema.graphqls @@ -53,6 +53,7 @@ type DownloadableProductLinks @doc(description: "DownloadableProductLinks define link_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_type: DownloadableFileTypeEnum @deprecated(reason: "`sample_url` serves to get the downloadable sample") sample_file: String @deprecated(reason: "`sample_url` serves to get the downloadable sample") + uid: ID! @doc(description: "A string that encodes option details.") @resolver(class: "Magento\\DownloadableGraphQl\\Resolver\\Product\\DownloadableLinksValueUid") # A Base64 string that encodes option details. } type DownloadableProductSamples @doc(description: "DownloadableProductSamples defines characteristics of a downloadable product") { diff --git a/app/code/Magento/Eav/Api/AttributeOptionManagementInterface.php b/app/code/Magento/Eav/Api/AttributeOptionManagementInterface.php index 84aefa700a52a..5359230c08c2a 100644 --- a/app/code/Magento/Eav/Api/AttributeOptionManagementInterface.php +++ b/app/code/Magento/Eav/Api/AttributeOptionManagementInterface.php @@ -15,8 +15,8 @@ interface AttributeOptionManagementInterface /** * Add option to attribute * - * @param string $attributeCode * @param int $entityType + * @param string $attributeCode * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option * @throws \Magento\Framework\Exception\StateException * @throws \Magento\Framework\Exception\InputException diff --git a/app/code/Magento/Eav/Api/AttributeOptionUpdateInterface.php b/app/code/Magento/Eav/Api/AttributeOptionUpdateInterface.php new file mode 100644 index 0000000000000..fd755a08fdf9a --- /dev/null +++ b/app/code/Magento/Eav/Api/AttributeOptionUpdateInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Eav\Api; + +/** + * Interface to update attribute option + * + * @api + */ +interface AttributeOptionUpdateInterface +{ + /** + * Update attribute option + * + * @param string $entityType + * @param string $attributeCode + * @param int $optionId + * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @return bool + * @throws \Magento\Framework\Exception\StateException + * @throws \Magento\Framework\Exception\InputException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function update( + string $entityType, + string $attributeCode, + int $optionId, + \Magento\Eav\Api\Data\AttributeOptionInterface $option + ): bool; +} diff --git a/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php b/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php index 56ae16c53402c..45022e25c5c31 100644 --- a/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php +++ b/app/code/Magento/Eav/Api/Data/AttributeDefaultValueInterface.php @@ -11,7 +11,7 @@ * Allows to manage attribute default value through interface * @api * @package Magento\Eav\Api\Data - * @since 100.2.0 + * @since 101.0.0 */ interface AttributeDefaultValueInterface { @@ -20,13 +20,13 @@ interface AttributeDefaultValueInterface /** * @param string $defaultValue * @return \Magento\Framework\Api\MetadataObjectInterface - * @since 100.2.0 + * @since 101.0.0 */ public function setDefaultValue($defaultValue); /** * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getDefaultValue(); } diff --git a/app/code/Magento/Eav/Api/Data/AttributeInterface.php b/app/code/Magento/Eav/Api/Data/AttributeInterface.php index 55d6e58b64b71..d96c2329ec594 100644 --- a/app/code/Magento/Eav/Api/Data/AttributeInterface.php +++ b/app/code/Magento/Eav/Api/Data/AttributeInterface.php @@ -316,6 +316,7 @@ public function getExtensionAttributes(); * * @param \Magento\Eav\Api\Data\AttributeExtensionInterface $extensionAttributes * @return $this + * @since 102.0.0 */ public function setExtensionAttributes( \Magento\Eav\Api\Data\AttributeExtensionInterface $extensionAttributes diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php index 7dd6b0a19ec02..577dac5b0c28b 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Js.php @@ -38,7 +38,7 @@ public function __construct( } /** - * @deprecated Misspelled method + * @deprecated 102.0.0 Misspelled method * @see getCompatibleInputTypes */ public function getComaptibleInputTypes() diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php index 9b44b2c7395ac..70fd5b2914600 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Edit/Options/AbstractOptions.php @@ -9,7 +9,7 @@ * Attribute add/edit form options tab * * @api - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @since 100.0.2 */ abstract class AbstractOptions extends \Magento\Framework\View\Element\AbstractBlock diff --git a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php index 55c0583191492..839ee7584cf03 100644 --- a/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php +++ b/app/code/Magento/Eav/Block/Adminhtml/Attribute/Grid/AbstractGrid.php @@ -10,7 +10,7 @@ * * @api * @SuppressWarnings(PHPMD.DepthOfInheritance) - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @since 100.0.2 */ abstract class AbstractGrid extends \Magento\Backend\Block\Widget\Grid\Extended diff --git a/app/code/Magento/Eav/Model/Attribute/Data/File.php b/app/code/Magento/Eav/Model/Attribute/Data/File.php index a52c88261166e..be237c70ffd93 100644 --- a/app/code/Magento/Eav/Model/Attribute/Data/File.php +++ b/app/code/Magento/Eav/Model/Attribute/Data/File.php @@ -7,12 +7,14 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\RequestInterface; +use Magento\Framework\Filesystem\Io\File as FileIo; /** * EAV Entity Attribute File Data Model * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class File extends \Magento\Eav\Model\Attribute\Data\AbstractData { @@ -38,6 +40,11 @@ class File extends \Magento\Eav\Model\Attribute\Data\AbstractData */ protected $_directory; + /** + * @var FileIo + */ + private $fileIo; + /** * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate * @param \Psr\Log\LoggerInterface $logger @@ -45,6 +52,7 @@ class File extends \Magento\Eav\Model\Attribute\Data\AbstractData * @param \Magento\Framework\Url\EncoderInterface $urlEncoder * @param \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator * @param \Magento\Framework\Filesystem $filesystem + * @param FileIo $fileIo * @codeCoverageIgnore */ public function __construct( @@ -53,12 +61,14 @@ public function __construct( \Magento\Framework\Locale\ResolverInterface $localeResolver, \Magento\Framework\Url\EncoderInterface $urlEncoder, \Magento\MediaStorage\Model\File\Validator\NotProtectedExtension $fileValidator, - \Magento\Framework\Filesystem $filesystem + \Magento\Framework\Filesystem $filesystem, + FileIo $fileIo ) { parent::__construct($localeDate, $logger, $localeResolver); $this->urlEncoder = $urlEncoder; $this->_fileValidator = $fileValidator; $this->_directory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->fileIo = $fileIo; } /** @@ -86,7 +96,7 @@ public function extractValue(RequestInterface $request) $mainScope = $this->_requestScope; $scopes = []; } - + // phpcs:disable Magento2.Security.Superglobal if (!empty($_FILES[$mainScope])) { foreach ($_FILES[$mainScope] as $fileKey => $scopeData) { foreach ($scopes as $scopeName) { @@ -104,12 +114,15 @@ public function extractValue(RequestInterface $request) } else { $value = []; } + // phpcs:enable Magento2.Security.Superglobal } else { + // phpcs:disable Magento2.Security.Superglobal if (isset($_FILES[$attrCode])) { $value = $_FILES[$attrCode]; } else { $value = []; } + // phpcs:enable Magento2.Security.Superglobal } if (!empty($extend['delete'])) { @@ -129,7 +142,7 @@ protected function _validateByRules($value) { $label = $this->getAttribute()->getStoreLabel(); $rules = $this->getAttribute()->getValidateRules(); - $extension = pathinfo($value['name'], PATHINFO_EXTENSION); + $extension = $this->fileIo->getPathInfo($value['name'])['extension']; if (!empty($rules['file_extensions'])) { $extensions = explode(',', $rules['file_extensions']); @@ -146,7 +159,9 @@ protected function _validateByRules($value) return $this->_fileValidator->getMessages(); } - if (!empty($value['tmp_name']) && !file_exists($value['tmp_name'])) { + if (!empty($value['tmp_name']) + && !$this->_directory->getDriver()->isExists($value['tmp_name']) + ) { return [__('"%1" is not a valid file.', $label)]; } @@ -177,8 +192,9 @@ public function validateValue($value) if (is_string($value) && !empty($value)) { $dir = $this->_directory->getAbsolutePath($this->getAttribute()->getEntityType()->getEntityTypeCode()); + $stat = $this->_directory->getDriver()->stat($dir . $value); $fileData = [ - 'size' => filesize($dir . $value), + 'size' => $stat['size'], 'name' => $value, 'tmp_name' => $dir . $value ]; @@ -209,8 +225,6 @@ public function validateValue($value) if (count($errors) == 0) { return true; - } elseif (is_string($value) && !empty($value)) { - $this->_directory->delete($dir . $value); } return $errors; diff --git a/app/code/Magento/Eav/Model/Attribute/GroupRepository.php b/app/code/Magento/Eav/Model/Attribute/GroupRepository.php index 07ca71d95eba5..f717a01a4384f 100644 --- a/app/code/Magento/Eav/Model/Attribute/GroupRepository.php +++ b/app/code/Magento/Eav/Model/Attribute/GroupRepository.php @@ -181,7 +181,7 @@ public function deleteById($groupId) /** * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return null|string - * @deprecated + * @deprecated 101.0.3 */ protected function retrieveAttributeSetIdFromSearchCriteria( \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria @@ -199,7 +199,7 @@ protected function retrieveAttributeSetIdFromSearchCriteria( /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Eav/Model/AttributeRepository.php b/app/code/Magento/Eav/Model/AttributeRepository.php index 337ae7334486e..bb307d5581121 100644 --- a/app/code/Magento/Eav/Model/AttributeRepository.php +++ b/app/code/Magento/Eav/Model/AttributeRepository.php @@ -208,7 +208,7 @@ public function deleteById($attributeId) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Eav/Model/AttributeSetRepository.php b/app/code/Magento/Eav/Model/AttributeSetRepository.php index caab82da3910d..73e8749952812 100644 --- a/app/code/Magento/Eav/Model/AttributeSetRepository.php +++ b/app/code/Magento/Eav/Model/AttributeSetRepository.php @@ -126,7 +126,7 @@ public function getList(\Magento\Framework\Api\SearchCriteriaInterface $searchCr /** * Retrieve entity type code from search criteria * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @param \Magento\Framework\Api\SearchCriteriaInterface $searchCriteria * @return null|string */ @@ -188,7 +188,7 @@ public function deleteById($attributeSetId) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Eav/Model/Config.php b/app/code/Magento/Eav/Model/Config.php index 718ef1a748590..8522700adbb6d 100644 --- a/app/code/Magento/Eav/Model/Config.php +++ b/app/code/Magento/Eav/Model/Config.php @@ -509,12 +509,12 @@ protected function _initAttributes($entityType) /** * Get attributes by entity type * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Eav\Model\Config::getEntityAttributes * * @param string $entityType * @return AbstractAttribute[] - * @since 100.2.0 + * @since 101.0.0 */ public function getAttributes($entityType) { @@ -724,7 +724,7 @@ private function createAttribute($model) /** * Get codes of all entity type attributes * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Eav\Model\Config::getEntityAttributes * * @param mixed $entityType @@ -745,7 +745,7 @@ public function getEntityAttributeCodes($entityType, $object = null) * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) - * @since 100.2.0 + * @since 101.0.0 */ public function getEntityAttributes($entityType, $object = null) { @@ -839,6 +839,7 @@ protected function _createAttribute($entityType, $attributeData) } /** @var AbstractAttribute $attribute */ $attribute = $this->createAttribute($model)->setData($attributeData); + $attribute->setOrigData('entity_type_id', $attribute->getEntityTypeId()); $this->_addAttributeReference( $attributeData['attribute_id'], $code, diff --git a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php index a298aad6356c3..f2f767b4e41fa 100644 --- a/app/code/Magento/Eav/Model/Entity/AbstractEntity.php +++ b/app/code/Magento/Eav/Model/Entity/AbstractEntity.php @@ -1013,7 +1013,7 @@ public function load($object, $entityId, $attributes = []) /** * Loads attributes metadata. * - * @deprecated 100.2.0 Use self::loadAttributesForObject instead + * @deprecated 101.0.0 Use self::loadAttributesForObject instead * @param array|null $attributes * @return $this * @since 100.1.0 @@ -1991,7 +1991,7 @@ public function afterDelete(DataObject $object) * @param array $attributes * @param AbstractEntity|null $object * @return void - * @since 100.2.0 + * @since 101.0.0 */ protected function loadAttributesForObject($attributes, $object = null) { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute.php b/app/code/Magento/Eav/Model/Entity/Attribute.php index 651bc96193780..04175c2da94d1 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute.php @@ -260,6 +260,8 @@ public function beforeSave() ); } + $this->validateEntityType(); + $defaultValue = $this->getDefaultValue(); $hasDefaultValue = (string)$defaultValue != ''; @@ -535,4 +537,21 @@ public function __wakeup() $this->reservedAttributeList = $objectManager->get(\Magento\Catalog\Model\Product\ReservedAttributeList::class); $this->dateTimeFormatter = $objectManager->get(DateTimeFormatterInterface::class); } + + /** + * Entity type for existing attribute shouldn't be changed. + * + * @return void + * @throws LocalizedException + */ + private function validateEntityType(): void + { + if ($this->getId() !== null) { + $origEntityTypeId = $this->getOrigData('entity_type_id'); + + if (($origEntityTypeId !== null) && ((int)$this->getEntityTypeId() !== (int)$origEntityTypeId)) { + throw new LocalizedException(__('Do not change entity type.')); + } + } + } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php index 7066a752fe2a2..af621e17f4249 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AbstractAttribute.php @@ -130,7 +130,7 @@ abstract class AbstractAttribute extends \Magento\Framework\Model\AbstractExtens * Serializer Instance. * * @var Json - * @since 100.2.0 + * @since 101.0.0 */ protected $serializer; @@ -219,10 +219,10 @@ public function __construct( /** * Get Serializer instance. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * * @return Json - * @since 100.2.0 + * @since 101.0.0 */ protected function getSerializer() { @@ -929,7 +929,7 @@ public function _getFlatColumnsDdlDefinition() * * Used in database compatible mode * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return array * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php b/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php index fa50aa588b4ed..892018983cd1c 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/AttributeGroupAlreadyExistsException.php @@ -9,7 +9,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ class AttributeGroupAlreadyExistsException extends AlreadyExistsException { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php index 156c0326f2b6f..b9fbb876dd6c3 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Backend/JsonEncoded.php @@ -11,7 +11,7 @@ * Backend model for attribute that stores structures in json format * * @api - * @since 100.2.0 + * @since 101.0.0 */ class JsonEncoded extends \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend { @@ -35,7 +35,7 @@ public function __construct(Json $jsonSerializer) * * @param \Magento\Framework\DataObject $object * @return $this - * @since 100.2.0 + * @since 101.0.0 */ public function beforeSave($object) { @@ -52,7 +52,7 @@ public function beforeSave($object) * * @param \Magento\Framework\DataObject $object * @return $this - * @since 100.2.0 + * @since 101.0.0 */ public function afterLoad($object) { diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php index 0ea4c324fe5c9..e99f4395953ad 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/OptionManagement.php @@ -7,7 +7,12 @@ namespace Magento\Eav\Model\Entity\Attribute; +use Magento\Eav\Api\AttributeOptionManagementInterface; +use Magento\Eav\Api\AttributeOptionUpdateInterface; use Magento\Eav\Api\Data\AttributeInterface as EavAttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\AttributeRepository; +use Magento\Eav\Model\ResourceModel\Entity\Attribute; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; @@ -15,26 +20,26 @@ /** * Eav Option Management */ -class OptionManagement implements \Magento\Eav\Api\AttributeOptionManagementInterface +class OptionManagement implements AttributeOptionManagementInterface, AttributeOptionUpdateInterface { /** - * @var \Magento\Eav\Model\AttributeRepository + * @var AttributeRepository */ protected $attributeRepository; /** - * @var \Magento\Eav\Model\ResourceModel\Entity\Attribute + * @var Attribute */ protected $resourceModel; /** - * @param \Magento\Eav\Model\AttributeRepository $attributeRepository - * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute $resourceModel + * @param AttributeRepository $attributeRepository + * @param Attribute $resourceModel * @codeCoverageIgnore */ public function __construct( - \Magento\Eav\Model\AttributeRepository $attributeRepository, - \Magento\Eav\Model\ResourceModel\Entity\Attribute $resourceModel + AttributeRepository $attributeRepository, + Attribute $resourceModel ) { $this->attributeRepository = $attributeRepository; $this->resourceModel = $resourceModel; @@ -45,45 +50,100 @@ public function __construct( * * @param int $entityType * @param string $attributeCode - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @param AttributeOptionInterface $option * @return string * @throws InputException * @throws NoSuchEntityException * @throws StateException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ public function add($entityType, $attributeCode, $option) { - if (empty($attributeCode)) { - throw new InputException(__('The attribute code is empty. Enter the code and try again.')); + $attribute = $this->loadAttribute($entityType, (string)$attributeCode); + + $label = trim($option->getLabel() ?: ''); + if (empty($label)) { + throw new InputException(__('The attribute option label is empty. Enter the value and try again.')); } - $attribute = $this->attributeRepository->get($entityType, $attributeCode); - if (!$attribute->usesSource()) { - throw new StateException(__('The "%1" attribute doesn\'t work with options.', $attributeCode)); + if ($attribute->getSource()->getOptionId($label) !== null) { + throw new InputException( + __( + 'Admin store attribute option label "%1" is already exists.', + $option->getLabel() + ) + ); } - $optionLabel = $option->getLabel(); - $optionId = $this->getOptionId($option); - $options = []; - $options['value'][$optionId][0] = $optionLabel; - $options['order'][$optionId] = $option->getSortOrder(); + $optionId = $this->getNewOptionId($option); + $this->saveOption($attribute, $option, $optionId); - if (is_array($option->getStoreLabels())) { - foreach ($option->getStoreLabels() as $label) { - $options['value'][$optionId][$label->getStoreId()] = $label->getLabel(); - } - } + return $this->retrieveOptionId($attribute, $option); + } - if (!$this->isAttributeOptionLabelExists($attribute, (string) $options['value'][$optionId][0])) { + /** + * @inheritdoc + */ + public function update( + string $entityType, + string $attributeCode, + int $optionId, + AttributeOptionInterface $option + ): bool { + $attribute = $this->loadAttribute($entityType, (string)$attributeCode); + if (empty($optionId)) { + throw new InputException(__('The option id is empty. Enter the value and try again.')); + } + $label = trim($option->getLabel() ?: ''); + if (empty($label)) { + throw new InputException(__('The attribute option label is empty. Enter the value and try again.')); + } + if ($attribute->getSource()->getOptionText($optionId) === false) { throw new InputException( __( - 'Admin store attribute option label "%1" is already exists.', - $options['value'][$optionId][0] + 'The \'%1\' attribute doesn\'t include an option id \'%2\'.', + $attribute->getAttributeCode(), + $optionId + ) + ); + } + $optionIdByLabel = $attribute->getSource()->getOptionId($label); + if (!empty($optionIdByLabel) && (int)$optionIdByLabel !== (int)$optionId) { + throw new InputException( + __( + 'Admin store attribute option label \'%1\' is already exists.', + $option->getLabel() ) ); } + $this->saveOption($attribute, $option, $optionId); + + return true; + } + + /** + * Save attribute option + * + * @param EavAttributeInterface $attribute + * @param AttributeOptionInterface $option + * @param int|string $optionId + * @return AttributeOptionInterface + * @throws StateException + */ + private function saveOption( + EavAttributeInterface $attribute, + AttributeOptionInterface $option, + $optionId + ): AttributeOptionInterface { + $optionLabel = trim($option->getLabel()); + $options = []; + $options['value'][$optionId][0] = $optionLabel; + $options['order'][$optionId] = $option->getSortOrder(); + if (is_array($option->getStoreLabels())) { + foreach ($option->getStoreLabels() as $label) { + $options['value'][$optionId][$label->getStoreId()] = $label->getLabel(); + } + } if ($option->getIsDefault()) { $attribute->setDefault([$optionId]); } @@ -91,29 +151,35 @@ public function add($entityType, $attributeCode, $option) $attribute->setOption($options); try { $this->resourceModel->save($attribute); - if ($optionLabel && $attribute->getAttributeCode()) { - $this->setOptionValue($option, $attribute, $optionLabel); - } } catch (\Exception $e) { - throw new StateException(__('The "%1" attribute can\'t be saved.', $attributeCode)); + throw new StateException(__('The "%1" attribute can\'t be saved.', $attribute->getAttributeCode())); } - return $this->getOptionId($option); + return $option; } /** - * @inheritdoc + * Get option id to create new option + * + * @param AttributeOptionInterface $option + * @return string */ - public function delete($entityType, $attributeCode, $optionId) + private function getNewOptionId(AttributeOptionInterface $option): string { - if (empty($attributeCode)) { - throw new InputException(__('The attribute code is empty. Enter the code and try again.')); + $optionId = trim($option->getValue() ?: ''); + if (empty($optionId)) { + $optionId = 'new_option'; } - $attribute = $this->attributeRepository->get($entityType, $attributeCode); - if (!$attribute->usesSource()) { - throw new StateException(__('The "%1" attribute has no option.', $attributeCode)); - } + return 'id_' . $optionId; + } + + /** + * @inheritdoc + */ + public function delete($entityType, $attributeCode, $optionId) + { + $attribute = $this->loadAttribute($entityType, $attributeCode); $this->validateOption($attribute, $optionId); $removalMarker = [ @@ -173,63 +239,55 @@ protected function validateOption($attribute, $optionId) } /** - * Returns option id + * Load attribute * - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option - * @return string + * @param string|int $entityType + * @param string $attributeCode + * @return EavAttributeInterface + * @throws InputException + * @throws NoSuchEntityException + * @throws StateException */ - private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string + private function loadAttribute($entityType, string $attributeCode): EavAttributeInterface { - return 'id_' . ($option->getValue() ?: 'new_option'); + if (empty($attributeCode)) { + throw new InputException(__('The attribute code is empty. Enter the code and try again.')); + } + + $attribute = $this->attributeRepository->get($entityType, $attributeCode); + if (!$attribute->usesSource()) { + throw new StateException(__('The "%1" attribute doesn\'t work with options.', $attributeCode)); + } + + $attribute->setStoreId(0); + + return $attribute; } /** - * Set option value + * Retrieve option id * - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option * @param EavAttributeInterface $attribute - * @param string $optionLabel - * @return void + * @param AttributeOptionInterface $option + * @return string */ - private function setOptionValue( - \Magento\Eav\Api\Data\AttributeOptionInterface $option, + private function retrieveOptionId( EavAttributeInterface $attribute, - string $optionLabel - ) { - $optionId = $attribute->getSource()->getOptionId($optionLabel); + AttributeOptionInterface $option + ) : string { + $label = trim($option->getLabel()); + $optionId = $attribute->getSource()->getOptionId($label); if ($optionId) { - $option->setValue($attribute->getSource()->getOptionId($optionId)); + $option->setValue($optionId); } elseif (is_array($option->getStoreLabels())) { foreach ($option->getStoreLabels() as $label) { - if ($optionId = $attribute->getSource()->getOptionId($label->getLabel())) { - $option->setValue($attribute->getSource()->getOptionId($optionId)); + $optionId = $attribute->getSource()->getOptionId($label->getLabel()); + if ($optionId) { break; } } } - } - - /** - * Checks if the incoming attribute option label for admin store is already exists. - * - * @param EavAttributeInterface $attribute - * @param string $adminStoreLabel - * @param int $storeId - * @return bool - */ - private function isAttributeOptionLabelExists( - EavAttributeInterface $attribute, - string $adminStoreLabel, - int $storeId = 0 - ) :bool { - $attribute->setStoreId($storeId); - - foreach ($attribute->getSource()->toOptionArray() as $existingAttributeOption) { - if ($existingAttributeOption['label'] === $adminStoreLabel) { - return false; - } - } - return true; + return (string) $optionId; } } diff --git a/app/code/Magento/Eav/Model/Entity/Attribute/Set.php b/app/code/Magento/Eav/Model/Entity/Attribute/Set.php index c3725ac580dcf..71c090c359fd4 100644 --- a/app/code/Magento/Eav/Model/Entity/Attribute/Set.php +++ b/app/code/Magento/Eav/Model/Entity/Attribute/Set.php @@ -375,7 +375,7 @@ public function getDefaultGroupId($setId = null) * Get resource instance * * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - * @deprecated 100.2.0 because resource models should be used directly + * @deprecated 101.0.0 because resource models should be used directly */ protected function _getResource() { diff --git a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php index 1fc513ed0ea80..0e1e4f035fc14 100644 --- a/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php +++ b/app/code/Magento/Eav/Model/Entity/Collection/AbstractCollection.php @@ -1604,6 +1604,7 @@ protected function _reset() * * @param string $attributeCode * @return bool + * @since 102.0.0 */ public function isAttributeAdded($attributeCode) : bool { diff --git a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php index c8780341271ac..637d4e17e852d 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php +++ b/app/code/Magento/Eav/Model/ResourceModel/Entity/Attribute.php @@ -114,6 +114,7 @@ public function loadByCode(AbstractModel $object, $entityTypeId, $code) if ($data) { $object->setData($data); + $object->setOrigData('entity_type_id', $object->getEntityTypeId()); $this->_afterLoad($object); return true; } @@ -204,6 +205,7 @@ protected function _beforeSave(AbstractModel $object) * @param AbstractModel $attribute * @return AbstractDb * @throws CouldNotDeleteException + * @since 102.0.2 */ protected function _beforeDelete(AbstractModel $attribute) { diff --git a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php index bf1405fa64122..1971eeeff3147 100644 --- a/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php +++ b/app/code/Magento/Eav/Model/ResourceModel/ReadHandler.php @@ -71,7 +71,7 @@ public function __construct( * @param string $entityType * @return \Magento\Eav\Api\Data\AttributeInterface[] * @throws Exception if for unknown entity type - * @deprecated Not used anymore + * @deprecated 101.0.5 Not used anymore * @see ReadHandler::getEntityAttributes */ protected function getAttributes($entityType) diff --git a/app/code/Magento/Eav/Model/TypeLocator/ServiceClassLocator.php b/app/code/Magento/Eav/Model/TypeLocator/ServiceClassLocator.php index a4225b550ab10..7ffcf689c4381 100644 --- a/app/code/Magento/Eav/Model/TypeLocator/ServiceClassLocator.php +++ b/app/code/Magento/Eav/Model/TypeLocator/ServiceClassLocator.php @@ -14,7 +14,7 @@ /** * Class to find type based off of ServiceTypeToEntityTypeMap. This locator is introduced for backwards compatibility. - * @deprecated + * @deprecated 102.0.0 */ class ServiceClassLocator implements CustomAttributeTypeLocatorInterface { diff --git a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php index 15dcea077c887..7e434166a15be 100644 --- a/app/code/Magento/Eav/Model/Validator/Attribute/Data.php +++ b/app/code/Magento/Eav/Model/Validator/Attribute/Data.php @@ -23,12 +23,12 @@ class Data extends \Magento\Framework\Validator\AbstractValidator /** * @var array */ - protected $_attributesWhiteList = []; + protected $allowedAttributesList = []; /** * @var array */ - protected $_attributesBlackList = []; + protected $deniedAttributesList = []; /** * @var array @@ -68,9 +68,9 @@ public function setAttributes(array $attributes) * @param array $attributesCodes * @return $this */ - public function setAttributesWhiteList(array $attributesCodes) + public function setAllowedAttributesList(array $attributesCodes) { - $this->_attributesWhiteList = $attributesCodes; + $this->allowedAttributesList = $attributesCodes; return $this; } @@ -82,9 +82,9 @@ public function setAttributesWhiteList(array $attributesCodes) * @param array $attributesCodes * @return $this */ - public function setAttributesBlackList(array $attributesCodes) + public function setDeniedAttributesList(array $attributesCodes) { - $this->_attributesBlackList = $attributesCodes; + $this->deniedAttributesList = $attributesCodes; return $this; } @@ -171,11 +171,11 @@ protected function _getAttributes($entity) $attributesCodes[] = $attributeCode; } - $ignoreAttributes = $this->_attributesBlackList; - if ($this->_attributesWhiteList) { + $ignoreAttributes = $this->deniedAttributesList; + if ($this->allowedAttributesList) { $ignoreAttributes = array_merge( $ignoreAttributes, - array_diff($attributesCodes, $this->_attributesWhiteList) + array_diff($attributesCodes, $this->allowedAttributesList) ); } diff --git a/app/code/Magento/Eav/Setup/EavSetup.php b/app/code/Magento/Eav/Setup/EavSetup.php index d440a84fc8e65..96c7b86a8682d 100644 --- a/app/code/Magento/Eav/Setup/EavSetup.php +++ b/app/code/Magento/Eav/Setup/EavSetup.php @@ -125,7 +125,7 @@ public function __construct( /** * Gets setup model. * - * @deprecated + * @deprecated 102.0.0 * @return ModuleDataSetupInterface */ public function getSetup() diff --git a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php index ccea3ea4ab950..3cdb30ec2606f 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Attribute/Data/FileTest.php @@ -11,15 +11,21 @@ use Magento\Eav\Model\Attribute\Data\File; use Magento\Eav\Model\AttributeDataFactory; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File as FileIo; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\Model\AbstractModel; use Magento\Framework\Stdlib\DateTime\TimezoneInterface; use Magento\Framework\Url\EncoderInterface; use Magento\MediaStorage\Model\File\Validator\NotProtectedExtension; +use Psr\Log\LoggerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; +/** + * Test for Magento\Eav\Model\Attribute\Data\File class. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class FileTest extends TestCase { /** @@ -37,6 +43,9 @@ class FileTest extends TestCase */ protected $fileValidatorMock; + /** + * @inheritDoc + */ protected function setUp(): void { $timezoneMock = $this->getMockForAbstractClass(TimezoneInterface::class); @@ -48,6 +57,7 @@ protected function setUp(): void ['isValid', 'getMessages'] ); $filesystemMock = $this->createMock(Filesystem::class); + $fileIo = $this->createMock(FileIo::class); $this->model = new File( $timezoneMock, @@ -55,7 +65,8 @@ protected function setUp(): void $localeResolverMock, $this->urlEncoder, $this->fileValidatorMock, - $filesystemMock + $filesystemMock, + $fileIo ); } diff --git a/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php b/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php index e4a0e935b325d..83fb1253aba96 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/ConfigTest.php @@ -223,11 +223,13 @@ public function testGetAttributes($cacheEnabled) ->method('getData') ->willReturn([$attributeData]); $entityAttributeMock = $this->getMockBuilder(Attribute::class) - ->setMethods(['setData', 'load', 'toArray']) + ->setMethods(['setData', 'setOrigData', 'load', 'toArray']) ->disableOriginalConstructor() ->getMock(); $entityAttributeMock->method('setData') ->willReturnSelf(); + $entityAttributeMock->method('setOrigData') + ->willReturn($attributeData); $entityAttributeMock->method('load') ->willReturnSelf(); $entityAttributeMock->method('toArray') diff --git a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php index 2084db08a1afb..b96b1e26696cd 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Entity/Attribute/OptionManagementTest.php @@ -15,10 +15,17 @@ use Magento\Eav\Model\Entity\Attribute\Source\SourceInterface; use Magento\Eav\Model\Entity\Attribute\Source\Table as EavAttributeSource; use Magento\Eav\Model\ResourceModel\Entity\Attribute; -use Magento\Framework\Model\AbstractModel; -use PHPUnit\Framework\MockObject\MockObject; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Exception\StateException; +use PHPUnit\Framework\MockObject\MockObject as MockObject; use PHPUnit\Framework\TestCase; +/** + * Tests for Eav Option Management functionality + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class OptionManagementTest extends TestCase { /** @@ -27,15 +34,18 @@ class OptionManagementTest extends TestCase protected $model; /** - * @var \PHPUnit\Framework\MockObject\MockObject + * @var MockObject|AttributeRepository */ protected $attributeRepositoryMock; /** - * @var \PHPUnit\Framework\MockObject\MockObject + * @var MockObject|Attribute */ protected $resourceModelMock; + /** + * @inheritdoc + */ protected function setUp(): void { $this->attributeRepositoryMock = $this->createMock(AttributeRepository::class); @@ -47,124 +57,189 @@ protected function setUp(): void ); } + /** + * Test to add attribute option + */ public function testAdd() { $entityType = 42; + $storeId = 4; $attributeCode = 'atrCde'; - $attributeMock = $this->getAttribute(); - $optionMock = $this->getAttributeOption(); - $labelMock = $this->getAttributeOptionLabel(); - $option = - ['value' => [ + $label = 'optionLabel'; + $storeLabel = 'labelLabel'; + $sortOder = 'optionSortOrder'; + $option = [ + 'value' => [ 'id_new_option' => [ - 0 => 'optionLabel', - 42 => 'labelLabel', + 0 => $label, + $storeId => $storeLabel, ], ], - 'order' => [ - 'id_new_option' => 'optionSortOrder', - ], - ]; + 'order' => [ + 'id_new_option' => $sortOder, + ] + ]; + $newOptionId = 10; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) - ->willReturn($attributeMock); - $attributeMock->expects($this->once())->method('usesSource')->willReturn(true); - $optionMock->expects($this->once())->method('getLabel')->willReturn('optionLabel'); - $optionMock->expects($this->once())->method('getSortOrder')->willReturn('optionSortOrder'); - $optionMock->expects($this->exactly(2))->method('getStoreLabels')->willReturn([$labelMock]); - $labelMock->expects($this->once())->method('getStoreId')->willReturn(42); - $labelMock->expects($this->once())->method('getLabel')->willReturn('labelLabel'); - $optionMock->expects($this->once())->method('getIsDefault')->willReturn(true); + $optionMock = $this->getAttributeOption(); + $labelMock = $this->getAttributeOptionLabel(); + /** @var SourceInterface|MockObject $sourceMock */ + $sourceMock = $this->createMock(EavAttributeSource::class); + $sourceMock->method('getOptionId') + ->willReturnMap( + [ + [$label, null], + [$storeLabel, $newOptionId], + [$newOptionId, $newOptionId], + ] + ); + + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['setDefault', 'setOption']) + ->onlyMethods(['usesSource', 'getSource']) + ->getMock(); + $attributeMock->method('usesSource')->willReturn(true); $attributeMock->expects($this->once())->method('setDefault')->with(['id_new_option']); $attributeMock->expects($this->once())->method('setOption')->with($option); + $attributeMock->method('getSource')->willReturn($sourceMock); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) + ->willReturn($attributeMock); + $optionMock->method('getLabel')->willReturn($label); + $optionMock->method('getSortOrder')->willReturn($sortOder); + $optionMock->method('getIsDefault')->willReturn(true); + $optionMock->method('getStoreLabels')->willReturn([$labelMock]); + $labelMock->method('getStoreId')->willReturn($storeId); + $labelMock->method('getLabel')->willReturn($storeLabel); $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock); - $this->assertEquals('id_new_option', $this->model->add($entityType, $attributeCode, $optionMock)); + $this->assertEquals( + $newOptionId, + $this->model->add($entityType, $attributeCode, $optionMock) + ); } + /** + * Test to add attribute option with empty attribute code + */ public function testAddWithEmptyAttributeCode() { - $this->expectException('Magento\Framework\Exception\InputException'); - $this->expectExceptionMessage('The attribute code is empty. Enter the code and try again.'); + $this->expectExceptionMessage("The attribute code is empty. Enter the code and try again."); + $this->expectException(InputException::class); $entityType = 42; $attributeCode = ''; $optionMock = $this->getAttributeOption(); $this->resourceModelMock->expects($this->never())->method('save'); $this->model->add($entityType, $attributeCode, $optionMock); } - + /** + * Test to add attribute option without use source + */ public function testAddWithWrongOptions() { - $this->expectException('Magento\Framework\Exception\StateException'); $this->expectExceptionMessage('The "testAttribute" attribute doesn\'t work with options.'); + $this->expectException(StateException::class); $entityType = 42; $attributeCode = 'testAttribute'; - $attributeMock = $this->getAttribute(); + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['setDefault', 'setOption', 'setStoreId']) + ->onlyMethods(['usesSource', 'getSource']) + ->getMock(); $optionMock = $this->getAttributeOption(); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('usesSource')->willReturn(false); $this->resourceModelMock->expects($this->never())->method('save'); $this->model->add($entityType, $attributeCode, $optionMock); } + /** + * Test to add attribute option wit save exception + */ public function testAddWithCannotSaveException() { - $this->expectException('Magento\Framework\Exception\StateException'); + $this->expectException(StateException::class); $this->expectExceptionMessage('The "atrCde" attribute can\'t be saved.'); + $entityType = 42; + $storeId = 4; $attributeCode = 'atrCde'; - $optionMock = $this->getAttributeOption(); - $attributeMock = $this->getAttribute(); - $labelMock = $this->getAttributeOptionLabel(); - $option = - ['value' => [ + $label = 'optionLabel'; + $storeLabel = 'labelLabel'; + $sortOder = 'optionSortOrder'; + $option = [ + 'value' => [ 'id_new_option' => [ - 0 => 'optionLabel', - 42 => 'labelLabel', + 0 => $label, + $storeId => $storeLabel, ], ], - 'order' => [ - 'id_new_option' => 'optionSortOrder', - ], - ]; + 'order' => [ + 'id_new_option' => $sortOder, + ] + ]; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) - ->willReturn($attributeMock); - $attributeMock->expects($this->once())->method('usesSource')->willReturn(true); - $optionMock->expects($this->once())->method('getLabel')->willReturn('optionLabel'); - $optionMock->expects($this->once())->method('getSortOrder')->willReturn('optionSortOrder'); - $optionMock->expects($this->exactly(2))->method('getStoreLabels')->willReturn([$labelMock]); - $labelMock->expects($this->once())->method('getStoreId')->willReturn(42); - $labelMock->expects($this->once())->method('getLabel')->willReturn('labelLabel'); - $optionMock->expects($this->once())->method('getIsDefault')->willReturn(true); + $optionMock = $this->getAttributeOption(); + $labelMock = $this->getAttributeOptionLabel(); + /** @var SourceInterface|MockObject $sourceMock */ + $sourceMock = $this->createMock(EavAttributeSource::class); + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['setDefault', 'setOption', 'setStoreId']) + ->onlyMethods(['usesSource', 'getSource', 'getAttributeCode']) + ->getMock(); + $attributeMock->method('usesSource')->willReturn(true); $attributeMock->expects($this->once())->method('setDefault')->with(['id_new_option']); $attributeMock->expects($this->once())->method('setOption')->with($option); + $attributeMock->method('getSource')->willReturn($sourceMock); + $attributeMock->method('getAttributeCode')->willReturn($attributeCode); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) + ->willReturn($attributeMock); + $optionMock->method('getLabel')->willReturn($label); + $optionMock->method('getSortOrder')->willReturn($sortOder); + $optionMock->method('getIsDefault')->willReturn(true); + $optionMock->method('getStoreLabels')->willReturn([$labelMock]); + $labelMock->method('getStoreId')->willReturn($storeId); + $labelMock->method('getLabel')->willReturn($storeLabel); + $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock) ->willThrowException(new \Exception()); $this->model->add($entityType, $attributeCode, $optionMock); } + /** + * Test to delete attribute option + */ public function testDelete() { $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getId', 'getOptionText', 'addData'] - ); + + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['getOptionText']) + ->onlyMethods(['usesSource', 'getSource', 'getId', 'addData']) + ->getMock(); $removalMarker = [ 'option' => [ 'value' => [$optionId => []], 'delete' => [$optionId => '1'], ], ]; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('usesSource')->willReturn(true); $attributeMock->expects($this->once())->method('getSource')->willReturnSelf(); @@ -175,22 +250,23 @@ public function testDelete() $this->assertTrue($this->model->delete($entityType, $attributeCode, $optionId)); } + /** + * Test to delete attribute option with save exception + */ public function testDeleteWithCannotSaveException() { - $this->expectException('Magento\Framework\Exception\StateException'); $this->expectExceptionMessage('The "atrCode" attribute can\'t be saved.'); + $this->expectException(StateException::class); + $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getId', 'getOptionText', 'addData'] - ); + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['getOptionText']) + ->onlyMethods(['usesSource', 'getSource', 'getId', 'addData']) + ->getMock(); $removalMarker = [ 'option' => [ 'value' => [$optionId => []], @@ -204,28 +280,29 @@ public function testDeleteWithCannotSaveException() $attributeMock->expects($this->once())->method('getOptionText')->willReturn('optionText'); $attributeMock->expects($this->never())->method('getId'); $attributeMock->expects($this->once())->method('addData')->with($removalMarker); - $this->resourceModelMock->expects($this->once())->method('save')->with($attributeMock) + $this->resourceModelMock->expects($this->once()) + ->method('save') + ->with($attributeMock) ->willThrowException(new \Exception()); $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to delete with wrong option + */ public function testDeleteWithWrongOption() { - $this->expectException('Magento\Framework\Exception\NoSuchEntityException'); $this->expectExceptionMessage('The "atrCode" attribute doesn\'t include an option with "option" ID.'); + $this->expectException(NoSuchEntityException::class); + $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getAttributeCode'] - ); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->createMock(EavAbstractAttribute::class); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $sourceMock = $this->getMockForAbstractClass(SourceInterface::class); $sourceMock->expects($this->once())->method('getOptionText')->willReturn(false); @@ -236,33 +313,40 @@ public function testDeleteWithWrongOption() $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to delete with absent option + */ public function testDeleteWithAbsentOption() { - $this->expectException('Magento\Framework\Exception\StateException'); - $this->expectExceptionMessage('The "atrCode" attribute has no option.'); + $this->expectExceptionMessage('The "atrCode" attribute doesn\'t work with options.'); + $this->expectException(StateException::class); + $entityType = 42; $attributeCode = 'atrCode'; $optionId = 'option'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['usesSource', 'getSource', 'getId', 'getOptionText', 'addData'] - ); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + /** @var EavAbstractAttribute|MockObject $attributeMock */ + $attributeMock = $this->getMockBuilder(EavAbstractAttribute::class) + ->disableOriginalConstructor() + ->addMethods(['getOptionText']) + ->onlyMethods(['usesSource', 'getSource', 'getId', 'addData']) + ->getMock(); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('usesSource')->willReturn(false); $this->resourceModelMock->expects($this->never())->method('save'); $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to delete with empty attribute code + */ public function testDeleteWithEmptyAttributeCode() { - $this->expectException('Magento\Framework\Exception\InputException'); - $this->expectExceptionMessage('The attribute code is empty. Enter the code and try again.'); + $this->expectExceptionMessage("The attribute code is empty. Enter the code and try again."); + $this->expectException(InputException::class); + $entityType = 42; $attributeCode = ''; $optionId = 'option'; @@ -270,86 +354,56 @@ public function testDeleteWithEmptyAttributeCode() $this->model->delete($entityType, $attributeCode, $optionId); } + /** + * Test to get items + */ public function testGetItems() { $entityType = 42; $attributeCode = 'atrCode'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['getOptions'] - ); - $optionsMock = [$this->getMockForAbstractClass(EavAttributeOptionInterface::class)]; - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $attributeMock = $this->createMock(EavAbstractAttribute::class); + $optionsMock = [$this->createMock(EavAttributeOptionInterface::class)]; + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); $attributeMock->expects($this->once())->method('getOptions')->willReturn($optionsMock); $this->assertEquals($optionsMock, $this->model->getItems($entityType, $attributeCode)); } + /** + * Test to get items with load exception + */ public function testGetItemsWithCannotLoadException() { - $this->expectException('Magento\Framework\Exception\StateException'); $this->expectExceptionMessage('The options for "atrCode" attribute can\'t be loaded.'); + $this->expectException(StateException::class); $entityType = 42; $attributeCode = 'atrCode'; - $attributeMock = $this->getMockForAbstractClass( - AbstractModel::class, - [], - '', - false, - false, - true, - ['getOptions'] - ); - $this->attributeRepositoryMock->expects($this->once())->method('get')->with($entityType, $attributeCode) + $attributeMock = $this->createMock(EavAbstractAttribute::class); + $this->attributeRepositoryMock->expects($this->once()) + ->method('get') + ->with($entityType, $attributeCode) ->willReturn($attributeMock); - $attributeMock->expects($this->once())->method('getOptions')->willThrowException(new \Exception()); + $attributeMock->expects($this->once()) + ->method('getOptions') + ->willThrowException(new \Exception()); $this->model->getItems($entityType, $attributeCode); } + /** + * Test to get items with empty attribute code + */ public function testGetItemsWithEmptyAttributeCode() { - $this->expectException('Magento\Framework\Exception\InputException'); - $this->expectExceptionMessage('The attribute code is empty. Enter the code and try again.'); + $this->expectExceptionMessage("The attribute code is empty. Enter the code and try again."); + $this->expectException(InputException::class); + $entityType = 42; $attributeCode = ''; $this->model->getItems($entityType, $attributeCode); } - /** - * Returns attribute entity mock. - * - * @param array $attributeOptions attribute options for return - * @return MockObject|EavAbstractAttribute - */ - private function getAttribute(array $attributeOptions = []) - { - $attribute = $this->getMockBuilder(EavAbstractAttribute::class) - ->disableOriginalConstructor() - ->setMethods( - [ - 'usesSource', - 'setDefault', - 'setOption', - 'setStoreId', - 'getSource', - ] - ) - ->getMock(); - $source = $this->getMockBuilder(EavAttributeSource::class) - ->disableOriginalConstructor() - ->getMock(); - - $attribute->method('getSource')->willReturn($source); - $source->method('toOptionArray')->willReturn($attributeOptions); - - return $attribute; - } - /** * Return attribute option entity mock. * diff --git a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php index 774b968f1b697..a8ecbb8371ac9 100644 --- a/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php +++ b/app/code/Magento/Eav/Test/Unit/Model/Validator/Attribute/DataTest.php @@ -249,10 +249,10 @@ public function testIsValidAttributesFromCollection() } /** - * @dataProvider whiteBlackListProvider + * @dataProvider allowDenyListProvider * @param callable $callback */ - public function testIsValidBlackListWhiteListChecks($callback) + public function testIsValidExclusionInclusionListChecks($callback) { $attribute = $this->_getAttributeMock( [ @@ -302,19 +302,19 @@ public function testIsValidBlackListWhiteListChecks($callback) /** * @return array */ - public function whiteBlackListProvider() + public function allowDenyListProvider() { - $whiteCallback = function ($validator) { - $validator->setAttributesWhiteList(['attribute']); + $allowedCallbackList = function ($validator) { + $validator->setAllowedAttributesList(['attribute']); }; - $blackCallback = function ($validator) { - $validator->setAttributesBlackList(['attribute2']); + $deniedCallbackList = function ($validator) { + $validator->setDeniedAttributesList(['attribute2']); }; - return ['white_list' => [$whiteCallback], 'black_list' => [$blackCallback]]; + return ['allowed' => [$allowedCallbackList], 'denied' => [$deniedCallbackList]]; } - public function testSetAttributesWhiteList() + public function testSetAttributesAllowedList() { $this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties'); @@ -328,12 +328,14 @@ public function testSetAttributesWhiteList() ) ->getMock(); $validator = new Data($attrDataFactory); - $result = $validator->setAttributesWhiteList($attributes); - $this->assertAttributeEquals($attributes, '_attributesWhiteList', $validator); + $result = $validator->setIncludedAttributesList($attributes); + + // phpstan:ignore + $this->assertAttributeEquals($attributes, '_attributesAllowed', $validator); $this->assertEquals($validator, $result); } - public function testSetAttributesBlackList() + public function testSetAttributesDeniedList() { $this->markTestSkipped('Skipped in #27500 due to testing protected/private methods and properties'); @@ -347,8 +349,9 @@ public function testSetAttributesBlackList() ) ->getMock(); $validator = new Data($attrDataFactory); - $result = $validator->setAttributesBlackList($attributes); - $this->assertAttributeEquals($attributes, '_attributesBlackList', $validator); + $result = $validator->setDeniedAttributesList($attributes); + // phpstan:ignore + $this->assertAttributeEquals($attributes, '_attributesDenied', $validator); $this->assertEquals($validator, $result); } diff --git a/app/code/Magento/Eav/etc/di.xml b/app/code/Magento/Eav/etc/di.xml index 21f248f1b1094..4f5d7d7112961 100644 --- a/app/code/Magento/Eav/etc/di.xml +++ b/app/code/Magento/Eav/etc/di.xml @@ -20,6 +20,7 @@ <preference for="Magento\Eav\Api\Data\AttributeFrontendLabelInterface" type="Magento\Eav\Model\Entity\Attribute\FrontendLabel" /> <preference for="Magento\Eav\Api\Data\AttributeOptionInterface" type="Magento\Eav\Model\Entity\Attribute\Option" /> <preference for="Magento\Eav\Api\AttributeOptionManagementInterface" type="Magento\Eav\Model\Entity\Attribute\OptionManagement" /> + <preference for="Magento\Eav\Api\AttributeOptionUpdateInterface" type="Magento\Eav\Model\Entity\Attribute\OptionManagement" /> <preference for="Magento\Eav\Api\Data\AttributeOptionLabelInterface" type="Magento\Eav\Model\Entity\Attribute\OptionLabel" /> <preference for="Magento\Eav\Api\Data\AttributeValidationRuleInterface" type="Magento\Eav\Model\Entity\Attribute\ValidationRule" /> <preference for="Magento\Eav\Api\Data\AttributeSearchResultsInterface" type="Magento\Eav\Model\AttributeSearchResults" /> diff --git a/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php b/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php index ef21a26f1f62e..7ee87681dc630 100644 --- a/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php +++ b/app/code/Magento/EavGraphQl/Model/Resolver/Query/Type.php @@ -15,6 +15,7 @@ * Translate type names found by the custom type locator to GraphQL type names. * * @api + * @since 100.3.0 */ class Type { @@ -55,6 +56,7 @@ public function __construct( * @param string $entityType * @return string * @throws GraphQlInputException + * @since 100.3.0 */ public function getType(string $attributeCode, string $entityType) : string { diff --git a/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php b/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php index 2b2da7522dfa6..41a5edc900af8 100644 --- a/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php +++ b/app/code/Magento/Elasticsearch/Block/Adminhtml/System/Config/Elasticsearch5/TestConnection.php @@ -8,7 +8,7 @@ /** * Elasticsearch 5x test connection block * @codeCoverageIgnore - * @deprecated because of EOL for Elasticsearch5 + * @deprecated 100.3.5 because of EOL for Elasticsearch5 */ class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection { diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 1f6e05c9e02fc..8576d8df0cc95 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -19,7 +19,7 @@ class Converter implements ConverterInterface */ private const ES_DATA_TYPE_TEXT = 'text'; private const ES_DATA_TYPE_KEYWORD = 'keyword'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -32,7 +32,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_TEXT, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_KEYWORD, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php index b912446acd63e..840a4e16e8ab2 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Adapter/FieldMapper/ProductFieldMapperProxy.php @@ -50,7 +50,6 @@ private function getProductFieldMapper() * @param string $attributeCode * @param array $context * @return string - * @since 100.1.0 */ public function getFieldName($attributeCode, $context = []) { @@ -62,7 +61,6 @@ public function getFieldName($attributeCode, $context = []) * * @param array $context * @return array - * @since 100.1.0 */ public function getAllAttributesTypes($context = []) { diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php index bd9a380230652..2560d7e26e7d9 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/Model/Client/Elasticsearch.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Elasticsearch5\Model\Client; use Magento\Framework\Exception\LocalizedException; @@ -11,7 +12,7 @@ /** * Elasticsearch client * - * @deprecated the Elasticsearch 5 doesn't supported due to EOL + * @deprecated 100.3.5 the Elasticsearch 5 doesn't supported due to EOL */ class Elasticsearch implements ClientInterface { @@ -48,8 +49,10 @@ public function __construct( $options = [], $elasticsearchClient = null ) { - if (empty($options['hostname']) || ((!empty($options['enableAuth']) && - ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + if (empty($options['hostname']) + || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) + && (empty($options['username']) || empty($options['password']))) + ) { throw new LocalizedException( __('The search failed because of a search engine misconfiguration.') ); @@ -163,6 +166,23 @@ public function createIndex($index, $settings) ); } + /** + * Add/update an Elasticsearch index settings. + * + * @param string $index + * @param array $settings + * @return void + */ + public function putIndexSettings(string $index, array $settings): void + { + $this->getClient()->indices()->putSettings( + [ + 'index' => $index, + 'body' => $settings, + ] + ); + } + /** * Delete an Elasticsearch index. * @@ -276,7 +296,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -302,7 +322,15 @@ public function addFieldsMapping(array $fields, $index, $entityType) ] ), ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -323,7 +351,6 @@ public function addFieldsMapping(array $fields, $index, $entityType) */ private function prepareFieldInfo($fieldInfo) { - if (strcmp($this->getServerVersion(), '5') < 0) { if ($fieldInfo['type'] == 'keyword') { $fieldInfo['type'] = 'string'; @@ -338,6 +365,17 @@ private function prepareFieldInfo($fieldInfo) return $fieldInfo; } + /** + * Get mapping from Elasticsearch index. + * + * @param array $params + * @return array + */ + public function getMapping(array $params): array + { + return $this->getClient()->indices()->getMapping($params); + } + /** * Delete mapping in Elasticsearch index * diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php index abd27abdac8a7..9db1375f16c71 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Mapper.php @@ -17,25 +17,25 @@ /** * Mapper class * @api - * @since 100.1.0 + * @since 100.2.2 */ class Mapper { /** * @var QueryBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $queryBuilder; /** * @var MatchQueryBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $matchQueryBuilder; /** * @var FilterBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $filterBuilder; @@ -59,7 +59,7 @@ public function __construct( * * @param RequestInterface $request * @return array - * @since 100.1.0 + * @since 100.2.2 */ public function buildQuery(RequestInterface $request) { @@ -89,7 +89,7 @@ public function buildQuery(RequestInterface $request) * @param string $conditionType * @return array * @throws \InvalidArgumentException - * @since 100.1.0 + * @since 100.2.2 */ protected function processQuery( RequestQueryInterface $requestQuery, @@ -126,7 +126,7 @@ protected function processQuery( * @param BoolQuery $query * @param array $selectQuery * @return array - * @since 100.1.0 + * @since 100.2.2 */ protected function processBoolQuery( BoolQuery $query, @@ -160,7 +160,7 @@ protected function processBoolQuery( * @param array $selectQuery * @param string $conditionType * @return array - * @since 100.1.0 + * @since 100.2.2 */ protected function processBoolQueryCondition( array $subQueryList, diff --git a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php index b75621191dae7..ac99c91dcfac1 100644 --- a/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php +++ b/app/code/Magento/Elasticsearch/Elasticsearch5/SearchAdapter/Query/Builder.php @@ -18,31 +18,31 @@ * Query builder for search adapter. * * @api - * @since 100.1.0 + * @since 100.2.2 */ class Builder { /** * @var Config - * @since 100.1.0 + * @since 100.2.2 */ protected $clientConfig; /** * @var SearchIndexNameResolver - * @since 100.1.0 + * @since 100.2.2 */ protected $searchIndexNameResolver; /** * @var AggregationBuilder - * @since 100.1.0 + * @since 100.2.2 */ protected $aggregationBuilder; /** * @var ScopeResolverInterface - * @since 100.1.0 + * @since 100.2.2 */ protected $scopeResolver; @@ -77,7 +77,7 @@ public function __construct( * * @param RequestInterface $request * @return array - * @since 100.1.0 + * @since 100.2.2 */ public function initQuery(RequestInterface $request) { @@ -104,7 +104,7 @@ public function initQuery(RequestInterface $request) * @param RequestInterface $request * @param array $searchQuery * @return array - * @since 100.1.0 + * @since 100.2.2 */ public function initAggregations( RequestInterface $request, diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php index 245e4d494afe1..9fa001097df87 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapper.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch\Model\Adapter\BatchDataMapper; use Magento\CatalogSearch\Model\Indexer\Fulltext\Action\DataProvider; @@ -74,7 +75,7 @@ class ProductDataMapper implements BatchDataMapperInterface private $attributesExcludedFromMerge = [ 'status', 'visibility', - 'tax_class_id' + 'tax_class_id', ]; /** @@ -85,8 +86,11 @@ class ProductDataMapper implements BatchDataMapperInterface ]; /** - * Construction for DocumentDataMapper - * + * @var string[] + */ + private $filterableAttributeTypes; + + /** * @param Builder $builder * @param FieldMapperInterface $fieldMapper * @param DateFieldType $dateFieldType @@ -94,6 +98,7 @@ class ProductDataMapper implements BatchDataMapperInterface * @param DataProvider $dataProvider * @param array $excludedAttributes * @param array $sortableAttributesValuesToImplode + * @param array $filterableAttributeTypes */ public function __construct( Builder $builder, @@ -102,7 +107,8 @@ public function __construct( AdditionalFieldsProviderInterface $additionalFieldsProvider, DataProvider $dataProvider, array $excludedAttributes = [], - array $sortableAttributesValuesToImplode = [] + array $sortableAttributesValuesToImplode = [], + array $filterableAttributeTypes = [] ) { $this->builder = $builder; $this->fieldMapper = $fieldMapper; @@ -115,6 +121,7 @@ public function __construct( $this->additionalFieldsProvider = $additionalFieldsProvider; $this->dataProvider = $dataProvider; $this->attributeOptionsCache = []; + $this->filterableAttributeTypes = $filterableAttributeTypes; } /** @@ -212,7 +219,7 @@ private function convertAttribute(Attribute $attribute, array $attributeValues, if ($retrievedValue !== null) { $productAttributes[$attribute->getAttributeCode()] = $retrievedValue; - if ($attribute->getIsSearchable()) { + if ($this->isAttributeLabelsShouldBeMapped($attribute)) { $attributeLabels = $this->getValuesLabels($attribute, $attributeValues, $storeId); $retrievedLabel = $this->retrieveFieldValue($attributeLabels); if ($retrievedLabel) { @@ -224,6 +231,26 @@ private function convertAttribute(Attribute $attribute, array $attributeValues, return $productAttributes; } + /** + * Check if an attribute has one of the next storefront properties enabled for mapping labels: + * - "Use in Search" (is_searchable) + * - "Visible in Advanced Search" (is_visible_in_advanced_search) + * - "Use in Layered Navigation" (is_filterable) + * - "Use in Search Results Layered Navigation" (is_filterable_in_search) + * + * @param Attribute $attribute + * @return bool + */ + private function isAttributeLabelsShouldBeMapped(Attribute $attribute): bool + { + return ( + $attribute->getIsSearchable() + || $attribute->getIsVisibleInAdvancedSearch() + || $attribute->getIsFilterable() + || $attribute->getIsFilterableInSearch() + ); + } + /** * Prepare attribute values. * @@ -249,6 +276,15 @@ private function prepareAttributeValues( $attributeValues = $this->prepareMultiselectValues($attributeValues); } + if (in_array($attribute->getFrontendInput(), $this->filterableAttributeTypes)) { + $attributeValues = array_map( + function (string $valueId) { + return (int)$valueId; + }, + $attributeValues + ); + } + if ($this->isAttributeDate($attribute)) { foreach ($attributeValues as $key => $attributeValue) { $attributeValues[$key] = $this->dateFieldType->formatDate($storeId, $attributeValue); diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php index 5ab6669a34cc4..25e691972d81d 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/Elasticsearch.php @@ -6,6 +6,12 @@ namespace Magento\Elasticsearch\Model\Adapter; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Stdlib\ArrayManager; + /** * Elasticsearch adapter * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -69,6 +75,31 @@ class Elasticsearch */ private $batchDocumentDataMapper; + /** + * @var array + */ + private $mappedAttributes = []; + + /** + * @var string[] + */ + private $indexByCode = []; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $productAttributeRepository; + + /** + * @var StaticField + */ + private $staticFieldProvider; + + /** + * @var ArrayManager + */ + private $arrayManager; + /** * @param \Magento\Elasticsearch\SearchAdapter\ConnectionManager $connectionManager * @param FieldMapperInterface $fieldMapper @@ -78,7 +109,11 @@ class Elasticsearch * @param Index\IndexNameResolver $indexNameResolver * @param BatchDataMapperInterface $batchDocumentDataMapper * @param array $options + * @param ProductAttributeRepositoryInterface|null $productAttributeRepository + * @param StaticField|null $staticFieldProvider + * @param ArrayManager|null $arrayManager * @throws \Magento\Framework\Exception\LocalizedException + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( \Magento\Elasticsearch\SearchAdapter\ConnectionManager $connectionManager, @@ -88,7 +123,10 @@ public function __construct( \Psr\Log\LoggerInterface $logger, \Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver $indexNameResolver, BatchDataMapperInterface $batchDocumentDataMapper, - $options = [] + $options = [], + ProductAttributeRepositoryInterface $productAttributeRepository = null, + StaticField $staticFieldProvider = null, + ArrayManager $arrayManager = null ) { $this->connectionManager = $connectionManager; $this->fieldMapper = $fieldMapper; @@ -97,6 +135,12 @@ public function __construct( $this->logger = $logger; $this->indexNameResolver = $indexNameResolver; $this->batchDocumentDataMapper = $batchDocumentDataMapper; + $this->productAttributeRepository = $productAttributeRepository ?: + ObjectManager::getInstance()->get(ProductAttributeRepositoryInterface::class); + $this->staticFieldProvider = $staticFieldProvider ?: + ObjectManager::getInstance()->get(StaticField::class); + $this->arrayManager = $arrayManager ?: + ObjectManager::getInstance()->get(ArrayManager::class); try { $this->client = $this->connectionManager->getConnection($options); @@ -322,6 +366,93 @@ public function updateAlias($storeId, $mappedIndexerId) // remove obsolete index if ($oldIndex) { $this->client->deleteIndex($oldIndex); + unset($this->indexByCode[$mappedIndexerId . '_' . $storeId]); + } + + return $this; + } + + /** + * Update Elasticsearch mapping for index. + * + * @param int $storeId + * @param string $mappedIndexerId + * @param string $attributeCode + * @return $this + */ + public function updateIndexMapping(int $storeId, string $mappedIndexerId, string $attributeCode): self + { + $indexName = $this->getIndexFromAlias($storeId, $mappedIndexerId); + if (empty($indexName)) { + return $this; + } + + $attribute = $this->productAttributeRepository->get($attributeCode); + $newAttributeMapping = $this->staticFieldProvider->getField($attribute); + $mappedAttributes = $this->getMappedAttributes($indexName); + + $attrToUpdate = array_diff_key($newAttributeMapping, $mappedAttributes); + if (!empty($attrToUpdate)) { + $settings['index']['mapping']['total_fields']['limit'] = $this + ->getMappingTotalFieldsLimit(array_merge($mappedAttributes, $attrToUpdate)); + $this->client->putIndexSettings($indexName, ['settings' => $settings]); + + $this->client->addFieldsMapping( + $attrToUpdate, + $indexName, + $this->clientConfig->getEntityType() + ); + $this->setMappedAttributes($indexName, $attrToUpdate); + } + + return $this; + } + + /** + * Retrieve index definition from class. + * + * @param int $storeId + * @param string $mappedIndexerId + * @return string + */ + private function getIndexFromAlias(int $storeId, string $mappedIndexerId): string + { + $indexCode = $mappedIndexerId . '_' . $storeId; + if (!isset($this->indexByCode[$indexCode])) { + $this->indexByCode[$indexCode] = $this->indexNameResolver->getIndexFromAlias($storeId, $mappedIndexerId); + } + + return $this->indexByCode[$indexCode]; + } + + /** + * Retrieve mapped attributes from class. + * + * @param string $indexName + * @return array + */ + private function getMappedAttributes(string $indexName): array + { + if (empty($this->mappedAttributes[$indexName])) { + $mappedAttributes = $this->client->getMapping(['index' => $indexName]); + $pathField = $this->arrayManager->findPath('properties', $mappedAttributes); + $this->mappedAttributes[$indexName] = $this->arrayManager->get($pathField, $mappedAttributes, []); + } + + return $this->mappedAttributes[$indexName]; + } + + /** + * Set mapped attributes to class. + * + * @param string $indexName + * @param array $mappedAttributes + * @return $this + */ + private function setMappedAttributes(string $indexName, array $mappedAttributes): self + { + foreach ($mappedAttributes as $attributeCode => $attributeParams) { + $this->mappedAttributes[$indexName][$attributeCode] = $attributeParams; } return $this; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php index 88dab83698794..2067dcdc7fe9f 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/Converter.php @@ -16,7 +16,7 @@ class Converter implements ConverterInterface * Text flags for Elasticsearch field types */ private const ES_DATA_TYPE_STRING = 'string'; - private const ES_DATA_TYPE_FLOAT = 'float'; + private const ES_DATA_TYPE_DOUBLE = 'double'; private const ES_DATA_TYPE_INT = 'integer'; private const ES_DATA_TYPE_DATE = 'date'; /**#@-*/ @@ -29,7 +29,7 @@ class Converter implements ConverterInterface private $mapping = [ self::INTERNAL_DATA_TYPE_STRING => self::ES_DATA_TYPE_STRING, self::INTERNAL_DATA_TYPE_KEYWORD => self::ES_DATA_TYPE_STRING, - self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_FLOAT, + self::INTERNAL_DATA_TYPE_FLOAT => self::ES_DATA_TYPE_DOUBLE, self::INTERNAL_DATA_TYPE_INT => self::ES_DATA_TYPE_INT, self::INTERNAL_DATA_TYPE_DATE => self::ES_DATA_TYPE_DATE, ]; diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php index 3e7c3e9b592bd..a1563f75e6607 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterInterface.php @@ -10,6 +10,7 @@ /** * @api * Field type converter from internal data types to elastic service. + * @since 100.3.0 */ interface ConverterInterface { @@ -28,6 +29,7 @@ interface ConverterInterface * * @param string $internalType * @return string + * @since 100.3.0 */ public function convert(string $internalType): string; } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php index f7dfcd29e5036..bc031fc988fb0 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldMapper/Product/FieldProvider/StaticField.php @@ -7,8 +7,9 @@ namespace Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider; -use Magento\Eav\Model\Config; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\AttributeProvider; use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\FieldIndex\ConverterInterface as IndexTypeConverterInterface; @@ -109,67 +110,82 @@ public function getFields(array $context = []): array $allAttributes = []; foreach ($attributes as $attribute) { - if (in_array($attribute->getAttributeCode(), $this->excludedAttributes, true)) { - continue; - } - $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); - $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); + $allAttributes += $this->getField($attribute); + } - $allAttributes[$fieldName] = [ - 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), - ]; + $allAttributes['store_id'] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), + 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE), + ]; - $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); - if (null !== $index) { - $allAttributes[$fieldName]['index'] = $index; - } + return $allAttributes; + } - if ($attributeAdapter->isSortable()) { - $sortFieldName = $this->fieldNameResolver->getFieldName( - $attributeAdapter, - ['type' => FieldMapperInterface::TYPE_SORT] - ); - $allAttributes[$fieldName]['fields'][$sortFieldName] = [ - 'type' => $this->fieldTypeConverter->convert( - FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD - ), - 'index' => $this->indexTypeConverter->convert( - IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE - ) - ]; - } + /** + * Get field mapping for specific attribute. + * + * @param AbstractAttribute $attribute + * @return array + */ + public function getField(AbstractAttribute $attribute): array + { + $fieldMapping = []; + if (in_array($attribute->getAttributeCode(), $this->excludedAttributes, true)) { + return $fieldMapping; + } + + $attributeAdapter = $this->attributeAdapterProvider->getByAttributeCode($attribute->getAttributeCode()); + $fieldName = $this->fieldNameResolver->getFieldName($attributeAdapter); - if ($attributeAdapter->isTextType()) { - $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; - $index = $this->indexTypeConverter->convert( + $fieldMapping[$fieldName] = [ + 'type' => $this->fieldTypeResolver->getFieldType($attributeAdapter), + ]; + + $index = $this->fieldIndexResolver->getFieldIndex($attributeAdapter); + if (null !== $index) { + $fieldMapping[$fieldName]['index'] = $index; + } + + if ($attributeAdapter->isSortable()) { + $sortFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_SORT] + ); + $fieldMapping[$fieldName]['fields'][$sortFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ), + 'index' => $this->indexTypeConverter->convert( IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE - ); - $allAttributes[$fieldName]['fields'][$keywordFieldName] = [ - 'type' => $this->fieldTypeConverter->convert( - FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD - ) - ]; - if ($index) { - $allAttributes[$fieldName]['fields'][$keywordFieldName]['index'] = $index; - } - } + ) + ]; + } - if ($attributeAdapter->isComplexType()) { - $childFieldName = $this->fieldNameResolver->getFieldName( - $attributeAdapter, - ['type' => FieldMapperInterface::TYPE_QUERY] - ); - $allAttributes[$childFieldName] = [ - 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) - ]; + if ($attributeAdapter->isTextType()) { + $keywordFieldName = FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD; + $index = $this->indexTypeConverter->convert( + IndexTypeConverterInterface::INTERNAL_NO_ANALYZE_VALUE + ); + $fieldMapping[$fieldName]['fields'][$keywordFieldName] = [ + 'type' => $this->fieldTypeConverter->convert( + FieldTypeConverterInterface::INTERNAL_DATA_TYPE_KEYWORD + ) + ]; + if ($index) { + $fieldMapping[$fieldName]['fields'][$keywordFieldName]['index'] = $index; } } - $allAttributes['store_id'] = [ - 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING), - 'index' => $this->indexTypeConverter->convert(IndexTypeConverterInterface::INTERNAL_NO_INDEX_VALUE), - ]; + if ($attributeAdapter->isComplexType()) { + $childFieldName = $this->fieldNameResolver->getFieldName( + $attributeAdapter, + ['type' => FieldMapperInterface::TYPE_QUERY] + ); + $fieldMapping[$childFieldName] = [ + 'type' => $this->fieldTypeConverter->convert(FieldTypeConverterInterface::INTERNAL_DATA_TYPE_STRING) + ]; + } - return $allAttributes; + return $fieldMapping; } } diff --git a/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php b/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php index e7d8d0672aaf0..069bf6e2ab33a 100644 --- a/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php +++ b/app/code/Magento/Elasticsearch/Model/Adapter/FieldType.php @@ -14,7 +14,7 @@ * @api * @since 100.1.0 * - * @deprecated This class provide not full data about field type. Only basic rules apply in this class. + * @deprecated 100.3.0 This class provide not full data about field type. Only basic rules apply in this class. * @see ResolverInterface */ class FieldType @@ -37,11 +37,12 @@ class FieldType /** * Get field type. * - * @deprecated + * @deprecated 100.3.0 * @see ResolverInterface::getFieldType * * @param AbstractAttribute $attribute * @return string + * @since 100.1.0 */ public function getFieldType($attribute) { diff --git a/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php b/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php index 8364b6c116b7d..2d4f8abeb8ecd 100644 --- a/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php +++ b/app/code/Magento/Elasticsearch/Model/DataProvider/Base/Suggestions.php @@ -5,19 +5,23 @@ */ namespace Magento\Elasticsearch\Model\DataProvider\Base; -use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; -use Magento\Store\Model\ScopeInterface; -use Magento\Search\Model\QueryInterface; +use Elasticsearch\Common\Exceptions\BadRequest400Exception; use Magento\AdvancedSearch\Model\SuggestedQueriesInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; -use Magento\Search\Model\QueryResultFactory; -use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Search\Model\QueryInterface; +use Magento\Search\Model\QueryResultFactory; +use Magento\Store\Model\ScopeInterface; use Magento\Store\Model\StoreManagerInterface as StoreManager; +use Psr\Log\LoggerInterface; /** * Default implementation to provide suggestions mechanism for Elasticsearch + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Suggestions implements SuggestedQueriesInterface { @@ -56,6 +60,11 @@ class Suggestions implements SuggestedQueriesInterface */ private $fieldProvider; + /** + * @var LoggerInterface + */ + private $logger; + /** * Suggestions constructor. * @@ -66,6 +75,7 @@ class Suggestions implements SuggestedQueriesInterface * @param SearchIndexNameResolver $searchIndexNameResolver * @param StoreManager $storeManager * @param FieldProviderInterface $fieldProvider + * @param LoggerInterface|null $logger */ public function __construct( ScopeConfigInterface $scopeConfig, @@ -74,7 +84,8 @@ public function __construct( ConnectionManager $connectionManager, SearchIndexNameResolver $searchIndexNameResolver, StoreManager $storeManager, - FieldProviderInterface $fieldProvider + FieldProviderInterface $fieldProvider, + LoggerInterface $logger = null ) { $this->queryResultFactory = $queryResultFactory; $this->connectionManager = $connectionManager; @@ -83,6 +94,7 @@ public function __construct( $this->searchIndexNameResolver = $searchIndexNameResolver; $this->storeManager = $storeManager; $this->fieldProvider = $fieldProvider; + $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); } /** @@ -93,8 +105,14 @@ public function getItems(QueryInterface $query) $result = []; if ($this->isSuggestionsAllowed()) { $isResultsCountEnabled = $this->isResultsCountEnabled(); + try { + $suggestions = $this->getSuggestions($query); + } catch (BadRequest400Exception $e) { + $this->logger->critical($e); + $suggestions = []; + } - foreach ($this->getSuggestions($query) as $suggestion) { + foreach ($suggestions as $suggestion) { $count = null; if ($isResultsCountEnabled) { $count = isset($suggestion['freq']) ? $suggestion['freq'] : null; diff --git a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php b/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php index 54e9890e02e59..56cdebdfc2813 100644 --- a/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php +++ b/app/code/Magento/Elasticsearch/Model/DataProvider/Suggestions.php @@ -20,7 +20,7 @@ /** * The implementation to provide suggestions mechanism for Elasticsearch5 * - * @deprecated because of EOL for Elasticsearch5 + * @deprecated 100.3.5 because of EOL for Elasticsearch5 * @see \Magento\Elasticsearch\Model\DataProvider\Base\Suggestions */ class Suggestions implements SuggestedQueriesInterface diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php new file mode 100644 index 0000000000000..53f036a3b8e38 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/Attribute.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product; + +use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\Indexer\IndexerHandler as ElasticsearchIndexerHandler; +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; +use Magento\Framework\Model\AbstractModel; +use Magento\Framework\Exception\LocalizedException; + +/** + * Catalog search indexer plugin for catalog attribute. + */ +class Attribute +{ + /** + * @var Config + */ + private $config; + + /** + * @var Processor + */ + private $indexerProcessor; + + /** + * @var DimensionProviderInterface + */ + private $dimensionProvider; + + /** + * @var IndexerHandlerFactory + */ + private $indexerHandlerFactory; + + /** + * @var bool + */ + private $isNewObject; + + /** + * @var string + */ + private $attributeCode; + + /** + * @param Config $config + * @param Processor $indexerProcessor + * @param DimensionProviderInterface $dimensionProvider + * @param IndexerHandlerFactory $indexerHandlerFactory + */ + public function __construct( + Config $config, + Processor $indexerProcessor, + DimensionProviderInterface $dimensionProvider, + IndexerHandlerFactory $indexerHandlerFactory + ) { + $this->config = $config; + $this->indexerProcessor = $indexerProcessor; + $this->dimensionProvider = $dimensionProvider; + $this->indexerHandlerFactory = $indexerHandlerFactory; + } + + /** + * Update catalog search indexer mapping if third party search engine is used. + * + * @param AttributeResourceModel $subject + * @param AttributeResourceModel $result + * @return AttributeResourceModel + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws LocalizedException + */ + public function afterSave( + AttributeResourceModel $subject, + AttributeResourceModel $result + ): AttributeResourceModel { + $indexer = $this->indexerProcessor->getIndexer(); + if ($this->isNewObject + && !$indexer->isScheduled() + && $this->config->isElasticsearchEnabled() + ) { + $indexerHandler = $this->indexerHandlerFactory->create(['data' => $indexer->getData()]); + if (!$indexerHandler instanceof ElasticsearchIndexerHandler) { + throw new LocalizedException( + __('Created indexer handler must be instance of %1.', ElasticsearchIndexerHandler::class) + ); + } + foreach ($this->dimensionProvider->getIterator() as $dimension) { + $indexerHandler->updateIndex($dimension, $this->attributeCode); + } + } + + return $result; + } + + /** + * Set class variables before saving attribute. + * + * @param AttributeResourceModel $subject + * @param AbstractModel $attribute + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeSave( + AttributeResourceModel $subject, + AbstractModel $attribute + ): void { + $this->isNewObject = $attribute->isObjectNew(); + $this->attributeCode = $attribute->getAttributeCode(); + } +} diff --git a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php index 847710eaa445a..90e21e9e3ea1e 100644 --- a/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php +++ b/app/code/Magento/Elasticsearch/Model/Indexer/IndexerHandler.php @@ -5,12 +5,13 @@ */ namespace Magento\Elasticsearch\Model\Indexer; -use Magento\Framework\Indexer\SaveHandler\IndexerInterface; -use Magento\Framework\Indexer\SaveHandler\Batch; -use Magento\Framework\Indexer\IndexStructureInterface; use Magento\Elasticsearch\Model\Adapter\Elasticsearch as ElasticsearchAdapter; use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\Indexer\IndexStructureInterface; +use Magento\Framework\Indexer\SaveHandler\Batch; +use Magento\Framework\Indexer\SaveHandler\IndexerInterface; +use Magento\Framework\Search\Request\Dimension; /** * Indexer Handler for Elasticsearch engine. @@ -18,7 +19,7 @@ class IndexerHandler implements IndexerInterface { /** - * Default batch size + * Size of default batch */ const DEFAULT_BATCH_SIZE = 500; @@ -132,6 +133,22 @@ public function isAvailable($dimensions = []) return $this->adapter->ping(); } + /** + * Update mapping data for index. + * + * @param Dimension[] $dimensions + * @param string $attributeCode + * @return IndexerInterface + */ + public function updateIndex(array $dimensions, string $attributeCode): IndexerInterface + { + $dimension = current($dimensions); + $scopeId = (int)$this->scopeResolver->getScope($dimension->getValue())->getId(); + $this->adapter->updateIndexMapping($scopeId, $this->getIndexerId(), $attributeCode); + + return $this; + } + /** * Returns indexer id. * diff --git a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php index 21ff9a53e4f96..12887207e2c5e 100644 --- a/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php +++ b/app/code/Magento/Elasticsearch/Model/ResourceModel/Fulltext/Collection/DefaultFilterStrategyApplyChecker.php @@ -10,7 +10,7 @@ /** * This class add in backward compatibility purposes to check if need to apply old strategy for filter prepare process. - * @deprecated + * @deprecated 100.3.2 */ class DefaultFilterStrategyApplyChecker implements DefaultFilterStrategyApplyCheckerInterface { diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php index 1e106023ea00d..548a57e55f3e2 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Aggregation/Builder/Dynamic.php @@ -35,7 +35,7 @@ public function __construct(Repository $algorithmRepository, EntityStorageFactor } /** - * {@inheritdoc} + * @inheritdoc */ public function build( RequestBucketInterface $bucket, @@ -46,9 +46,7 @@ public function build( /** @var DynamicBucket $bucket */ $algorithm = $this->algorithmRepository->get($bucket->getMethod(), ['dataProvider' => $dataProvider]); $data = $algorithm->getItems($bucket, $dimensions, $this->getEntityStorage($queryResult)); - $resultData = $this->prepareData($data); - - return $resultData; + return $this->prepareData($data); } /** @@ -77,12 +75,9 @@ private function prepareData($data) { $resultData = []; foreach ($data as $value) { - $from = is_numeric($value['from']) ? $value['from'] : '*'; - $to = is_numeric($value['to']) ? $value['to'] : '*'; - unset($value['from'], $value['to']); - - $rangeName = "{$from}_{$to}"; - $resultData[$rangeName] = array_merge(['value' => $rangeName], $value); + $rangeName = "{$value['from']}_{$value['to']}"; + $value['value'] = $rangeName; + $resultData[$rangeName] = $value; } return $resultData; diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php index 496a77e4c5ac3..7bc64b59ffe78 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Dynamic/DataProvider.php @@ -235,11 +235,9 @@ public function prepareData($range, array $dbRanges) { $data = []; if (!empty($dbRanges)) { - $lastIndex = array_keys($dbRanges); - $lastIndex = $lastIndex[count($lastIndex) - 1]; foreach ($dbRanges as $index => $count) { - $fromPrice = $index == 1 ? '' : ($index - 1) * $range; - $toPrice = $index == $lastIndex ? '' : $index * $range; + $fromPrice = $index == 1 ? 0 : ($index - 1) * $range; + $toPrice = $index * $range; $data[] = [ 'from' => $fromPrice, 'to' => $toPrice, diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php b/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php index d76086ee2f809..1654e02558a83 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Mapper.php @@ -19,7 +19,7 @@ * * @api * @since 100.1.0 - * @deprecated because of EOL for Elasticsearch2 + * @deprecated 100.3.5 because of EOL for Elasticsearch2 */ class Mapper extends Elasticsearch5Mapper { diff --git a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php index 68bec2580f621..3de88ff9f0307 100644 --- a/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php +++ b/app/code/Magento/Elasticsearch/SearchAdapter/Query/ValueTransformer/TextTransformer.php @@ -57,7 +57,7 @@ public function transform(string $value): string */ private function escape(string $value): string { - $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\*|\?|:|\\\)/'; + $pattern = '/(\+|-|&&|\|\||!|\(|\)|\{|}|\[|]|\^|"|~|\/|\*|\?|:|\\\)/'; $replace = '\\\$1'; return preg_replace($pattern, $replace, $value); diff --git a/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php new file mode 100644 index 0000000000000..7cd72c322d647 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Setup/Patch/Data/InvalidateIndex.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Elasticsearch\Setup\Patch\Data; + +use Magento\Framework\Setup\ModuleDataSetupInterface; +use Magento\Framework\Setup\Patch\DataPatchInterface; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\CatalogSearch\Model\Indexer\Fulltext as FulltextIndexer; +use Magento\Framework\Setup\Patch\PatchInterface; + +/** + * Invalidate fulltext index + */ +class InvalidateIndex implements DataPatchInterface +{ + /** + * @var ModuleDataSetupInterface + */ + private $moduleDataSetup; + + /** + * @var IndexerRegistry + */ + private $indexerRegistry; + + /** + * @param ModuleDataSetupInterface $moduleDataSetup + * @param IndexerRegistry $indexerRegistry + */ + public function __construct(ModuleDataSetupInterface $moduleDataSetup, IndexerRegistry $indexerRegistry) + { + $this->moduleDataSetup = $moduleDataSetup; + $this->indexerRegistry = $indexerRegistry; + } + + /** + * @inheritDoc + */ + public function apply(): PatchInterface + { + $this->indexerRegistry->get(FulltextIndexer::INDEXER_ID)->invalidate(); + return $this; + } + + /** + * @inheritDoc + */ + public static function getDependencies(): array + { + return []; + } + + /** + * @inheritDoc + */ + public function getAliases(): array + { + return []; + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml index d612f5bd17a2f..c2c8644a6fcf5 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Suite/SearchEngineElasticsearchSuite.xml @@ -9,8 +9,12 @@ <suite name="SearchEngineElasticsearchSuite"> <before> <magentoCLI stepKey="setSearchEngineToElasticsearch" command="config:set {{SearchEngineElasticsearchConfigData.path}} {{SearchEngineElasticsearchConfigData.value}}"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after></after> <include> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml index e8a0df9b9dc87..8d1b420f3c17f 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/ProductQuickSearchUsingElasticSearchTest.xml @@ -46,9 +46,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> <!--Navigate to storefront and do a quick search for the product --> <comment userInput="Navigate to Storefront to check if quick search works" stepKey="commentCheckQuickSearch" /> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePage"/> - - <waitForPageLoad stepKey="waitForHomePageToLoad" time="30"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> <fillField userInput="Simple" selector="{{StorefrontQuickSearchSection.searchPhrase}}" stepKey="fillSearchBar"/> <waitForPageLoad stepKey="wait2" time="30"/> <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml index a94a6a2e3d133..1e067f1560404 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontCheckAdvancedSearchOnElasticSearchTest.xml @@ -36,7 +36,9 @@ <!-- Reindex invalidated indices after product attribute has been created/deleted --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> - <magentoCLI command="cache:flush" arguments="full_page" stepKey="flushFullPageCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushFullPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml index d9988577009bc..1c4d53b273661 100644 --- a/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch/Test/Mftf/Test/StorefrontProductQuickSearchWithDecimalAttributeUsingElasticSearchTest.xml @@ -55,8 +55,12 @@ <!--Delete attribute--> <deleteData createDataKey="customAttribute" stepKey="deleteCustomAttribute"/> <!--Reindex and clear cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:clean" stepKey="cleanCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutOfAdmin"/> </after> <!--Navigate to backend and update value for custom attribute --> @@ -77,8 +81,12 @@ </actionGroup> <!--Reindex and clear cache--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:clean" stepKey="cleanCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> <!-- Navigate to Storefront and search --> 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 49a894f1295c7..398c79f056810 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 @@ -69,6 +69,7 @@ protected function setUp(): void 'create', 'delete', 'putMapping', + 'getMapping', 'deleteMapping', 'stats', 'updateAliases', @@ -329,7 +330,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -340,7 +341,7 @@ public function testAddFieldsMapping() 'match_mapping_type' => 'string', 'mapping' => [ 'type' => 'integer', - 'index' => true + 'index' => true, ], ], ], @@ -354,6 +355,14 @@ public function testAddFieldsMapping() ], ], ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -400,7 +409,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -424,7 +433,15 @@ public function testAddFieldsMappingFailure() 'index' => true, ], ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -517,6 +534,22 @@ public function testDeleteMappingFailure() ); } + /** + * Test get Elasticsearch mapping process. + * + * @return void + */ + public function testGetMapping(): void + { + $params = ['index' => 'indexName']; + $this->indicesMock->expects($this->once()) + ->method('getMapping') + ->with($params) + ->willReturn([]); + + $this->model->getMapping($params); + } + /** * Test query() method * @return void diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index 2c87549da6075..9f1b59b1bfc81 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -21,6 +21,8 @@ use PHPUnit\Framework\TestCase; /** + * Unit tests for \Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper class. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductDataMapperTest extends TestCase @@ -56,12 +58,12 @@ class ProductDataMapperTest extends TestCase private $additionalFieldsProvider; /** - * @var MockObject + * @var DataProvider|MockObject */ private $dataProvider; /** - * Set up test environment. + * @inheritdoc */ protected function setUp(): void { @@ -71,6 +73,11 @@ protected function setUp(): void $this->attribute = $this->createMock(Attribute::class); $this->additionalFieldsProvider = $this->getMockForAbstractClass(AdditionalFieldsProviderInterface::class); $this->dateFieldTypeMock = $this->createMock(Date::class); + $filterableAttributeTypes = [ + 'boolean' => 'boolean', + 'multiselect' => 'multiselect', + 'select' => 'select', + ]; $objectManager = new ObjectManagerHelper($this); $this->model = $objectManager->getObject( @@ -81,6 +88,7 @@ protected function setUp(): void 'dateFieldType' => $this->dateFieldTypeMock, 'dataProvider' => $this->dataProvider, 'additionalFieldsProvider' => $this->additionalFieldsProvider, + 'filterableAttributeTypes' => $filterableAttributeTypes, ] ); } @@ -159,8 +167,8 @@ public function testGetMap(int $productId, array $attributeData, $attributeValue $productId => [$attributeId => $attributeValue], ]; $documents = $this->model->map($documentData, $storeId, $context); - $returnAttributeData['store_id'] = $storeId; - $this->assertEquals($returnAttributeData, $documents[$productId]); + $returnAttributeData = ['store_id' => $storeId] + $returnAttributeData; + $this->assertSame($returnAttributeData, $documents[$productId]); } /** @@ -305,8 +313,8 @@ public static function mapProvider(): array ['value' => '2', 'label' => 'Disabled'], ], ], - [10 => '1', 11 => '2'], - ['status' => '1'], + [10 => '1', 11 => '2'], + ['status' => 1], ], 'select without options' => [ 10, @@ -318,7 +326,7 @@ public static function mapProvider(): array 'options' => [], ], '44', - ['color' => '44'], + ['color' => 44], ], 'unsearchable select with options' => [ 10, @@ -333,7 +341,7 @@ public static function mapProvider(): array ], ], '44', - ['color' => '44'], + ['color' => 44], ], 'searchable select with options' => [ 10, @@ -348,7 +356,7 @@ public static function mapProvider(): array ], ], '44', - ['color' => '44', 'color_value' => 'red'], + ['color' => 44, 'color_value' => 'red'], ], 'composite select with options' => [ 10, @@ -363,7 +371,7 @@ public static function mapProvider(): array ], ], [10 => '44', 11 => '45'], - ['color' => ['44', '45'], 'color_value' => ['red', 'black']], + ['color' => [44, 45], 'color_value' => ['red', 'black']], ], 'multiselect without options' => [ 10, @@ -430,10 +438,10 @@ public static function mapProvider(): array 'backend_type' => 'int', 'frontend_input' => 'int', 'is_searchable' => false, - 'options' => [] + 'options' => [], ], 15, - [] + [], ], ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php index dd4bffe8e7c33..5abe800884ced 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/ElasticsearchTest.php @@ -11,14 +11,18 @@ use Elasticsearch\Namespaces\IndicesNamespace; use Magento\AdvancedSearch\Model\Client\ClientInterface as ElasticsearchClient; use Magento\AdvancedSearch\Model\Client\ClientOptionsInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Eav\Model\Entity\Attribute\AbstractAttribute; use Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface; use Magento\Elasticsearch\Model\Adapter\Elasticsearch as ElasticsearchAdapter; use Magento\Elasticsearch\Model\Adapter\FieldMapperInterface; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProvider\StaticField; use Magento\Elasticsearch\Model\Adapter\Index\BuilderInterface; use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; use Magento\Elasticsearch\Model\Config; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Stdlib\ArrayManager; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -81,6 +85,21 @@ class ElasticsearchTest extends TestCase */ protected $indexNameResolver; + /** + * @var ProductAttributeRepositoryInterface|MockObject + */ + private $productAttributeRepository; + + /** + * @var StaticField|MockObject + */ + private $staticFieldProvider; + + /** + * @var ArrayManager|MockObject + */ + private $arrayManager; + /** * Setup * @@ -177,9 +196,17 @@ protected function setUp(): void ) ->disableOriginalConstructor() ->getMock(); - $this->batchDocumentDataMapper = $this->getMockBuilder( - BatchDataMapperInterface::class - )->disableOriginalConstructor() + $this->batchDocumentDataMapper = $this->getMockBuilder(BatchDataMapperInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->productAttributeRepository = $this->getMockBuilder(ProductAttributeRepositoryInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + $this->staticFieldProvider = $this->getMockBuilder(StaticField::class) + ->disableOriginalConstructor() + ->getMock(); + $this->arrayManager = $this->getMockBuilder(ArrayManager::class) + ->disableOriginalConstructor() ->getMock(); $this->model = $this->objectManager->getObject( \Magento\Elasticsearch\Model\Adapter\Elasticsearch::class, @@ -192,6 +219,9 @@ protected function setUp(): void 'logger' => $this->logger, 'indexNameResolver' => $this->indexNameResolver, 'options' => [], + 'productAttributeRepository' => $this->productAttributeRepository, + 'staticFieldProvider' => $this->staticFieldProvider, + 'arrayManager' => $this->arrayManager, ] ); } @@ -459,6 +489,81 @@ public function testUpdateAliasWithoutOldIndex() $this->assertEquals($this->model, $this->model->updateAlias(1, 'product')); } + /** + * Test update Elasticsearch mapping for index without alias definition. + * + * @return void + */ + public function testUpdateIndexMappingWithoutAliasDefinition(): void + { + $storeId = 1; + $mappedIndexerId = 'product'; + + $this->indexNameResolver->expects($this->once()) + ->method('getIndexFromAlias') + ->with($storeId, $mappedIndexerId) + ->willReturn(''); + + $this->productAttributeRepository->expects($this->never()) + ->method('get'); + + $this->model->updateIndexMapping($storeId, $mappedIndexerId, 'attribute_code'); + } + + /** + * Test update Elasticsearch mapping for index with alias definition. + * + * @return void + */ + public function testUpdateIndexMappingWithAliasDefinition(): void + { + $storeId = 1; + $mappedIndexerId = 'product'; + $indexName = '_product_1_v1'; + $attributeCode = 'example_attribute_code'; + + $this->indexNameResolver->expects($this->once()) + ->method('getIndexFromAlias') + ->with($storeId, $mappedIndexerId) + ->willReturn($indexName); + + $attribute = $this->getMockBuilder(AbstractAttribute::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->productAttributeRepository->expects($this->once()) + ->method('get') + ->with($attributeCode) + ->willReturn($attribute); + + $this->staticFieldProvider->expects($this->once()) + ->method('getField') + ->with($attribute) + ->willReturn([$attributeCode => ['type' => 'text']]); + + $mappedAttributes = ['another_attribute_code' => 'attribute_mapping']; + $this->client->expects($this->once()) + ->method('getMapping') + ->with(['index' => $indexName]) + ->willReturn(['properties' => $mappedAttributes]); + + $this->arrayManager->expects($this->once()) + ->method('findPath') + ->with('properties', ['properties' => $mappedAttributes]) + ->willReturn('example/path/to/properties'); + + $this->arrayManager->expects($this->once()) + ->method('get') + ->with('example/path/to/properties', ['properties' => $mappedAttributes], []) + ->willReturn($mappedAttributes); + + $this->client->expects($this->once()) + ->method('addFieldsMapping') + ->with([$attributeCode => ['type' => 'text']], $indexName, 'product'); + + $this->model->updateIndexMapping($storeId, $mappedIndexerId, $attributeCode); + } + /** * Get elasticsearch client options * 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 87f072836544e..a9bcd1a20a1b2 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 @@ -246,7 +246,7 @@ function ($type) use ($complexType) { if ($type === 'string') { return 'string'; } elseif ($type === 'float') { - return 'float'; + return 'double'; } elseif ($type === 'integer') { return 'integer'; } else { @@ -281,7 +281,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] @@ -300,7 +300,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ], @@ -319,7 +319,7 @@ public function attributeProvider() 'index' => 'no_index' ], 'price_1_1' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true ] ] diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php index 75b79bc43e805..718adf255254f 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Adapter/FieldMapper/Product/FieldProvider/FieldType/ConverterTest.php @@ -56,7 +56,7 @@ public function convertProvider() { return [ ['string', 'string'], - ['float', 'float'], + ['float', 'double'], ['integer', 'integer'], ]; } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php index 7151677db3405..9f1c5db60b3d8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/DataProvider/Base/SuggestionsTest.php @@ -7,7 +7,10 @@ namespace Magento\Elasticsearch\Test\Unit\Model\DataProvider\Base; +use Elasticsearch\Common\Exceptions\BadRequest400Exception; +use Magento\Elasticsearch\Model\Adapter\FieldMapper\Product\FieldProviderInterface; use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\DataProvider\Base\Suggestions; use Magento\Elasticsearch\Model\DataProvider\Suggestions as SuggestionsDataProvider; use Magento\Elasticsearch\SearchAdapter\ConnectionManager; use Magento\Elasticsearch\SearchAdapter\SearchIndexNameResolver; @@ -21,6 +24,7 @@ use Magento\Store\Model\StoreManagerInterface as StoreManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -62,6 +66,21 @@ class SuggestionsTest extends TestCase */ private $storeManager; + /** + * @var FieldProviderInterface|MockObject + */ + private $fieldProvider; + + /** + * @var LoggerInterface|MockObject + */ + private $logger; + + /** + * @var Elasticsearch|MockObject + */ + private $client; + /** * @var QueryInterface|MockObject */ @@ -99,7 +118,19 @@ protected function setUp(): void ->setMethods(['getIndexName']) ->getMock(); - $this->storeManager = $this->getMockBuilder(\Magento\Store\Model\StoreManagerInterface::class) + $this->storeManager = $this->getMockBuilder(StoreManager::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->fieldProvider = $this->getMockBuilder(FieldProviderInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->logger = $this->getMockBuilder(LoggerInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $this->client = $this->getMockBuilder(Elasticsearch::class) ->disableOriginalConstructor() ->getMock(); @@ -110,81 +141,155 @@ protected function setUp(): void $objectManager = new ObjectManagerHelper($this); $this->model = $objectManager->getObject( - \Magento\Elasticsearch\Model\DataProvider\Base\Suggestions::class, + Suggestions::class, [ 'queryResultFactory' => $this->queryResultFactory, 'connectionManager' => $this->connectionManager, 'scopeConfig' => $this->scopeConfig, 'config' => $this->config, 'searchIndexNameResolver' => $this->searchIndexNameResolver, - 'storeManager' => $this->storeManager + 'storeManager' => $this->storeManager, + 'fieldProvider' => $this->fieldProvider, + 'logger' => $this->logger, ] ); } /** - * Test getItems() method + * Test get items process with search suggestions disabled. + * @return void */ - public function testGetItems() + public function testGetItemsWithDisabledSearchSuggestion(): void { - $this->scopeConfig->expects($this->any()) - ->method('getValue') - ->willReturn(1); - - $this->config->expects($this->any()) - ->method('isElasticsearchEnabled') - ->willReturn(1); - - $store = $this->getMockBuilder(StoreInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + $this->scopeConfig->expects($this->once()) + ->method('isSetFlag') + ->willReturn(false); - $this->storeManager->expects($this->any()) - ->method('getStore') - ->willReturn($store); - - $store->expects($this->any()) - ->method('getId') - ->willReturn(1); + $this->scopeConfig->expects($this->never()) + ->method('getValue'); - $this->searchIndexNameResolver->expects($this->any()) - ->method('getIndexName') - ->willReturn('magento2_product_1'); + $this->config->expects($this->once()) + ->method('isElasticsearchEnabled') + ->willReturn(true); - $this->query->expects($this->any()) - ->method('getQueryText') - ->willReturn('query'); + $this->logger->expects($this->never()) + ->method('critical'); - $client = $this->getMockBuilder(Elasticsearch::class) - ->disableOriginalConstructor() - ->getMock(); + $this->queryResultFactory->expects($this->never()) + ->method('create'); - $this->connectionManager->expects($this->any()) - ->method('getConnection') - ->willReturn($client); + $this->assertEmpty($this->model->getItems($this->query)); + } - $client->expects($this->any()) + /** + * Test get items process with search suggestions enabled. + * @return void + */ + public function testGetItemsWithEnabledSearchSuggestion(): void + { + $this->prepareSearchQuery(); + $this->client->expects($this->once()) ->method('query') ->willReturn([ 'suggest' => [ 'phrase_field' => [ - 'options' => [ - 'text' => 'query', - 'score' => 1, - 'freq' => 1, + [ + 'options' => [ + 'suggestion' => [ + 'text' => 'query', + 'score' => 1, + 'freq' => 1, + ] + ] ] ], ], ]); + $this->logger->expects($this->never()) + ->method('critical'); + $query = $this->getMockBuilder(QueryResult::class) ->disableOriginalConstructor() ->getMock(); - $this->queryResultFactory->expects($this->any()) + $this->queryResultFactory->expects($this->once()) ->method('create') ->willReturn($query); - $this->assertIsArray($this->model->getItems($this->query)); + $this->assertEquals([$query], $this->model->getItems($this->query)); + } + + /** + * Test get items process when throwing an exception. + * @return void + */ + public function testGetItemsException(): void + { + $this->prepareSearchQuery(); + $exception = new BadRequest400Exception(); + + $this->client->expects($this->once()) + ->method('query') + ->willThrowException($exception); + + $this->logger->expects($this->once()) + ->method('critical') + ->with($exception); + + $this->queryResultFactory->expects($this->never()) + ->method('create'); + + $this->assertEmpty($this->model->getItems($this->query)); + } + + /** + * Prepare Mocks for default get items process. + * @return void + */ + private function prepareSearchQuery(): void + { + $storeId = 1; + + $this->scopeConfig->expects($this->exactly(2)) + ->method('isSetFlag') + ->willReturn(true); + + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->willReturn(1); + + $this->config->expects($this->once()) + ->method('isElasticsearchEnabled') + ->willReturn(true); + + $store = $this->getMockBuilder(StoreInterface::class) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $store->expects($this->once()) + ->method('getId') + ->willReturn($storeId); + + $this->storeManager->expects($this->once()) + ->method('getStore') + ->willReturn($store); + + $this->searchIndexNameResolver->expects($this->once()) + ->method('getIndexName') + ->with($storeId, Config::ELASTICSEARCH_TYPE_DEFAULT) + ->willReturn('magento2_product_1'); + + $this->query->expects($this->once()) + ->method('getQueryText') + ->willReturn('query'); + + $this->fieldProvider->expects($this->once()) + ->method('getFields') + ->willReturn([]); + + $this->connectionManager->expects($this->once()) + ->method('getConnection') + ->willReturn($this->client); } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php new file mode 100644 index 0000000000000..801c7ca3be216 --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php @@ -0,0 +1,173 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\Model\Indexer\Fulltext\Plugin\Category\Product; + +use ArrayIterator; +use Magento\Catalog\Model\ResourceModel\Attribute as AttributeResourceModel; +use Magento\Catalog\Model\ResourceModel\Eav\Attribute as AttributeModel; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\CatalogSearch\Model\Indexer\IndexerHandlerFactory; +use Magento\Elasticsearch\Model\Config; +use Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute as AttributePlugin; +use Magento\Elasticsearch\Model\Indexer\IndexerHandler; +use Magento\Framework\Indexer\DimensionProviderInterface; +use Magento\Framework\Indexer\IndexerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\MockObject\Rule\InvokedCount as InvokedCountMatcher; +use PHPUnit\Framework\TestCase; + +/** + * Tests for catalog search indexer plugin. + */ +class AttributeTest extends TestCase +{ + /** + * @var Config|MockObject + */ + private $configMock; + + /** + * @var Processor|MockObject + */ + private $indexerProcessorMock; + + /** + * @var DimensionProviderInterface|MockObject + */ + private $dimensionProviderMock; + + /** + * @var IndexerHandlerFactory|MockObject + */ + private $indexerHandlerFactoryMock; + + /** + * @var AttributePlugin + */ + private $attributePlugin; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->configMock = $this->createMock(Config::class); + $this->indexerProcessorMock = $this->createMock(Processor::class); + $this->dimensionProviderMock = $this->getMockBuilder(DimensionProviderInterface::class) + ->getMockForAbstractClass(); + $this->indexerHandlerFactoryMock = $this->createMock(IndexerHandlerFactory::class); + + $this->attributePlugin = (new ObjectManager($this))->getObject( + AttributePlugin::class, + [ + 'config' => $this->configMock, + 'indexerProcessor' => $this->indexerProcessorMock, + 'dimensionProvider' => $this->dimensionProviderMock, + 'indexerHandlerFactory' => $this->indexerHandlerFactoryMock, + ] + ); + } + + /** + * Test update catalog search indexer process. + * + * @param bool $isNewObject + * @param bool $isElasticsearchEnabled + * @param array $dimensions + * @return void + * @dataProvider afterSaveDataProvider + * + */ + public function testAfterSave(bool $isNewObject, bool $isElasticsearchEnabled, array $dimensions): void + { + /** @var AttributeModel|MockObject $attributeMock */ + $attributeMock = $this->createMock(AttributeModel::class); + $attributeMock->expects($this->once()) + ->method('isObjectNew') + ->willReturn($isNewObject); + + $attributeMock->expects($this->once()) + ->method('getAttributeCode') + ->willReturn('example_attribute_code'); + + /** @var AttributeResourceModel|MockObject $subjectMock */ + $subjectMock = $this->createMock(AttributeResourceModel::class); + $this->attributePlugin->beforeSave($subjectMock, $attributeMock); + + $indexerData = ['indexer_example_data']; + + /** @var IndexerInterface|MockObject $indexerMock */ + $indexerMock = $this->getMockBuilder(IndexerInterface::class) + ->setMethods(['getData']) + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $indexerMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled)) + ->method('getData') + ->willReturn($indexerData); + + $this->indexerProcessorMock->expects($this->once()) + ->method('getIndexer') + ->willReturn($indexerMock); + + $this->configMock->expects($isNewObject ? $this->once() : $this->never()) + ->method('isElasticsearchEnabled') + ->willReturn($isElasticsearchEnabled); + + /** @var IndexerHandler|MockObject $indexerHandlerMock */ + $indexerHandlerMock = $this->createMock(IndexerHandler::class); + + $indexerHandlerMock + ->expects(($isNewObject && $isElasticsearchEnabled) ? $this->exactly(count($dimensions)) : $this->never()) + ->method('updateIndex') + ->willReturnSelf(); + + $this->indexerHandlerFactoryMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled)) + ->method('create') + ->with(['data' => $indexerData]) + ->willReturn($indexerHandlerMock); + + $this->dimensionProviderMock->expects($this->getExpectsCount($isNewObject, $isElasticsearchEnabled)) + ->method('getIterator') + ->willReturn(new ArrayIterator($dimensions)); + + $this->assertEquals($subjectMock, $this->attributePlugin->afterSave($subjectMock, $subjectMock)); + } + + /** + * DataProvider for testAfterSave(). + * + * @return array + */ + public function afterSaveDataProvider(): array + { + $dimensions = [['scope' => 1], ['scope' => 2]]; + + return [ + 'save_existing_object' => [false, false, $dimensions], + 'save_with_another_search_engine' => [true, false, $dimensions], + 'save_with_elasticsearch' => [true, true, []], + 'save_with_elasticsearch_and_dimensions' => [true, true, $dimensions], + ]; + } + + /** + * Retrieves how many times method is executed. + * + * @param bool $isNewObject + * @param bool $isElasticsearchEnabled + * @return InvokedCountMatcher + */ + private function getExpectsCount(bool $isNewObject, bool $isElasticsearchEnabled): InvokedCountMatcher + { + return ($isNewObject && $isElasticsearchEnabled) ? $this->once() : $this->never(); + } +} diff --git a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php index a147ca1b42b3b..3a9aef68c328c 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/Model/Indexer/IndexerHandlerTest.php @@ -267,4 +267,45 @@ public function testCleanIndex() $this->assertEquals($model, $result); } + + /** + * Test mapping data is updated for index. + * + * @return void + */ + public function testUpdateIndex(): void + { + $dimensionValue = 'SomeDimension'; + $indexMapping = 'some_index_mapping'; + $attributeCode = 'example_attribute_code'; + + $dimension = $this->getMockBuilder(Dimension::class) + ->disableOriginalConstructor() + ->getMock(); + + $dimension->expects($this->once()) + ->method('getValue') + ->willReturn($dimensionValue); + + $this->scopeResolver->expects($this->once()) + ->method('getScope') + ->with($dimensionValue) + ->willReturn($this->scopeInterface); + + $this->scopeInterface->expects($this->once()) + ->method('getId') + ->willReturn(1); + + $this->indexNameResolver->expects($this->once()) + ->method('getIndexMapping') + ->with('catalogsearch_fulltext') + ->willReturn($indexMapping); + + $this->adapter->expects($this->once()) + ->method('updateIndexMapping') + ->with(1, $indexMapping, $attributeCode) + ->willReturnSelf(); + + $this->model->updateIndex([$dimension], $attributeCode); + } } diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php index c5b9089acd91c..0595b667f4ee8 100644 --- a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Dynamic/DataProviderTest.php @@ -390,13 +390,13 @@ public function testPrepareData() { $expectedResult = [ [ - 'from' => '', + 'from' => 0, 'to' => 10, 'count' => 1, ], [ 'from' => 10, - 'to' => '', + 'to' => 20, 'count' => 1, ], ]; diff --git a/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/ValueTransformer/TextTransformerTest.php b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/ValueTransformer/TextTransformerTest.php new file mode 100644 index 0000000000000..66c0ce624fbfd --- /dev/null +++ b/app/code/Magento/Elasticsearch/Test/Unit/SearchAdapter/Query/ValueTransformer/TextTransformerTest.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Test\Unit\SearchAdapter\Query\ValueTransformer; + +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use Magento\Elasticsearch\SearchAdapter\Query\ValueTransformer\TextTransformer; +use PHPUnit\Framework\TestCase; + +/** + * Test value transformer + */ +class TextTransformerTest extends TestCase +{ + /** + * @var TextTransformer + */ + protected $model; + + /** + * Setup method + * @return void + */ + public function setUp(): void + { + $objectManagerHelper = new ObjectManagerHelper($this); + $this->model = $objectManagerHelper->getObject( + TextTransformer::class, + [ + '$preprocessors' => [], + ] + ); + } + + /** + * Test transform value + * + * @param string $value + * @param string $expected + * @return void + * @dataProvider valuesDataProvider + */ + public function testTransform(string $value, string $expected): void + { + $result = $this->model->transform($value); + $this->assertEquals($expected, $result); + } + + /** + * Values data provider + * + * @return array + */ + public function valuesDataProvider(): array + { + return [ + ['Laptop^camera{microphone}', 'Laptop\^camera\{microphone\}'], + ['Birthday 25-Pack w/ Greatest of All Time Cupcake', 'Birthday 25\-Pack w\/ Greatest of All Time Cupcake'], + ['Retro vinyl record ~d123 *star', 'Retro vinyl record \~d123 \*star'], + ]; + } +} diff --git a/app/code/Magento/Elasticsearch/composer.json b/app/code/Magento/Elasticsearch/composer.json index 386bb1af298bb..b79ae7bc5cc47 100644 --- a/app/code/Magento/Elasticsearch/composer.json +++ b/app/code/Magento/Elasticsearch/composer.json @@ -12,7 +12,7 @@ "magento/module-store": "*", "magento/module-catalog-inventory": "*", "magento/framework": "*", - "elasticsearch/elasticsearch": "~7.6" + "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch/etc/di.xml b/app/code/Magento/Elasticsearch/etc/di.xml index 633889e70591b..edec07cb5d51e 100644 --- a/app/code/Magento/Elasticsearch/etc/di.xml +++ b/app/code/Magento/Elasticsearch/etc/di.xml @@ -153,6 +153,11 @@ <type name="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\ProductDataMapper"> <arguments> <argument name="additionalFieldsProvider" xsi:type="object">additionalFieldsProviderForElasticsearch</argument> + <argument name="filterableAttributeTypes" xsi:type="array"> + <item name="boolean" xsi:type="string">boolean</item> + <item name="multiselect" xsi:type="string">multiselect</item> + <item name="select" xsi:type="string">select</item> + </argument> </arguments> </type> <preference for="Magento\Elasticsearch\Model\Adapter\BatchDataMapperInterface" type="Magento\Elasticsearch\Model\Adapter\BatchDataMapper\DataMapperResolver" /> @@ -470,6 +475,11 @@ <argument name="fieldTypeConverter" xsi:type="object">Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\Product\FieldProvider\FieldType\Converter</argument> </arguments> </virtualType> + <type name="Magento\Elasticsearch\Model\Adapter\Elasticsearch"> + <arguments> + <argument name="staticFieldProvider" xsi:type="object">elasticsearch5StaticFieldProvider</argument> + </arguments> + </type> <type name="Magento\Elasticsearch\Elasticsearch5\Model\Adapter\FieldMapper\ProductFieldMapper"> <arguments> <argument name="fieldProvider" xsi:type="object">elasticsearch5FieldProvider</argument> @@ -537,7 +547,7 @@ </type> <type name="Magento\Search\Model\SearchEngine\Validator"> <arguments> - <argument name="engineBlacklist" xsi:type="array"> + <argument name="excludedEngineList" xsi:type="array"> <item name="elasticsearch" xsi:type="string">Elasticsearch 2</item> </argument> <argument name="engineValidators" xsi:type="array"> @@ -545,4 +555,12 @@ </argument> </arguments> </type> + <type name="Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute"> + <arguments> + <argument name="dimensionProvider" xsi:type="object" shared="false">Magento\Store\Model\StoreDimensionProvider</argument> + </arguments> + </type> + <type name="Magento\Catalog\Model\ResourceModel\Attribute"> + <plugin name="updateElasticsearchIndexerMapping" type="Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product\Attribute"/> + </type> </config> diff --git a/app/code/Magento/Elasticsearch/i18n/en_US.csv b/app/code/Magento/Elasticsearch/i18n/en_US.csv index 3a0ec556dbf8d..e36b3e054accd 100644 --- a/app/code/Magento/Elasticsearch/i18n/en_US.csv +++ b/app/code/Magento/Elasticsearch/i18n/en_US.csv @@ -11,3 +11,4 @@ "Elasticsearch Server Timeout","Elasticsearch Server Timeout" "Test Connection","Test Connection" "Minimum Terms to Match","Minimum Terms to Match" +"Created indexer handler must be instance of %1.", "Created indexer handler must be instance of %1." diff --git a/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php b/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php index 1b17db1a00f6e..c192b43bdc081 100644 --- a/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php +++ b/app/code/Magento/Elasticsearch6/Block/Adminhtml/System/Config/TestConnection.php @@ -7,6 +7,7 @@ /** * Elasticsearch 6.x test connection block + * @deprecated the new minor release supports compatibility with Elasticsearch 7 */ class TestConnection extends \Magento\AdvancedSearch\Block\Adminhtml\System\Config\TestConnection { diff --git a/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php b/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php index 7532927f1dc85..cc8f69e92a858 100644 --- a/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php +++ b/app/code/Magento/Elasticsearch6/Model/Adapter/FieldMapper/Product/FieldProvider/FieldName/Resolver/DefaultResolver.php @@ -12,6 +12,8 @@ /** * Default name resolver. + * + * @deprecated the new minor release supports compatibility with Elasticsearch 7 */ class DefaultResolver extends Base { diff --git a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php index 2c1c283c5b24d..2d787f4d4377d 100644 --- a/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch6/Model/Client/Elasticsearch.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Elasticsearch6\Model\Client; use Magento\AdvancedSearch\Model\Client\ClientInterface; @@ -11,6 +12,8 @@ /** * Elasticsearch client + * + * @deprecated the new minor release supports compatibility with Elasticsearch 7 */ class Elasticsearch implements ClientInterface { @@ -48,8 +51,10 @@ public function __construct( $elasticsearchClient = null, $fieldsMappingPreprocessors = [] ) { - if (empty($options['hostname']) || ((!empty($options['enableAuth']) && - ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + if (empty($options['hostname']) + || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) + && (empty($options['username']) || empty($options['password']))) + ) { throw new LocalizedException( __('The search failed because of a search engine misconfiguration.') ); @@ -174,6 +179,23 @@ public function createIndex($index, $settings) ); } + /** + * Add/update an Elasticsearch index settings. + * + * @param string $index + * @param array $settings + * @return void + */ + public function putIndexSettings(string $index, array $settings): void + { + $this->getClient()->indices()->putSettings( + [ + 'index' => $index, + 'body' => $settings, + ] + ); + } + /** * Delete an Elasticsearch index. * @@ -281,7 +303,7 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -303,7 +325,15 @@ public function addFieldsMapping(array $fields, $index, $entityType) 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], ], @@ -319,6 +349,17 @@ public function addFieldsMapping(array $fields, $index, $entityType) $this->getClient()->indices()->putMapping($params); } + /** + * Get mapping from Elasticsearch index. + * + * @param array $params + * @return array + */ + public function getMapping(array $params): array + { + return $this->getClient()->indices()->getMapping($params); + } + /** * Delete mapping in Elasticsearch index * diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml index a15c8f5e30e86..75e355126ff2b 100644 --- a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticSearchForChineseLocaleTest.xml @@ -26,7 +26,9 @@ <magentoCLI command="config:set --scope={{GeneralLocalCodeConfigsForChina.scope}} --scope-code={{GeneralLocalCodeConfigsForChina.scope_code}} {{GeneralLocalCodeConfigsForChina.path}} {{GeneralLocalCodeConfigsForChina.value}}" stepKey="setLocaleToChina"/> <comment userInput="Moved to appropriate test suite" stepKey="enableElasticsearch6"/> <comment userInput="Moved to appropriate test suite" stepKey="checkConnection"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="ApiCategory" stepKey="createCategory"/> <createData entity="ApiSimpleProduct" stepKey="createProduct"> <requiredEntity createDataKey="createCategory"/> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml index e173090bfa318..627105751507a 100644 --- a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontElasticsearchSearchInvalidValueTest.xml @@ -26,8 +26,12 @@ <!--Set Minimal Query Length--> <magentoCLI command="config:set {{SetMinQueryLength2Config.path}} {{SetMinQueryLength2Config.value}}" stepKey="setMinQueryLength"/> <!--Reindex indexes and clear cache--> - <magentoCLI command="indexer:reindex catalogsearch_fulltext" stepKey="reindex"/> - <magentoCLI command="cache:flush config" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <!--Set configs to default--> @@ -41,19 +45,21 @@ <argument name="productAttributeCode" value="{{textProductAttribute.attribute_code}}"/> </actionGroup> <actionGroup ref="AssertProductAttributeRemovedSuccessfullyActionGroup" stepKey="deleteProductAttributeSuccess"/> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="navigateToProductAttributeGrid"/> - <waitForPageLoad stepKey="waitForAttributePageLoad"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="navigateToProductAttributeGrid"/> <click selector="{{AdminProductAttributeGridSection.ResetFilter}}" stepKey="resetFiltersOnGrid"/> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="goToProductCatalog"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="goToProductCatalog"/> <actionGroup ref="DeleteProductsIfTheyExistActionGroup" stepKey="deleteProduct"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> - <magentoCLI command="indexer:reindex catalogsearch_fulltext" stepKey="reindex"/> - <magentoCLI command="cache:flush config" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalogsearch_fulltext"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="config"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!--Create new searchable product attribute--> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <actionGroup ref="AdminCreateSearchableProductAttributeActionGroup" stepKey="createAttribute"> <argument name="attribute" value="textProductAttribute"/> </actionGroup> @@ -78,10 +84,14 @@ <argument name="categoryName" value="$$createCategory.name$$"/> </actionGroup> <fillField selector="{{AdminProductFormSection.attributeRequiredInput(textProductAttribute.attribute_code)}}" userInput="searchable" stepKey="fillTheAttributeRequiredInputField"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveButton"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveButton"/> <!-- TODO: REMOVE AFTER FIX MC-21717 --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush eav" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value="eav"/> + </actionGroup> <!--Assert search results on storefront--> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefrontPage"/> <actionGroup ref="StorefrontCheckQuickSearchStringActionGroup" stepKey="quickSearchForFirstSearchTerm"> diff --git a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml index 9a025a6d04b14..653c460733976 100644 --- a/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml +++ b/app/code/Magento/Elasticsearch6/Test/Mftf/Test/StorefrontProductQuickSearchUsingElasticSearchTest.xml @@ -31,7 +31,9 @@ </createData> <magentoCLI command="config:set {{CustomGridPerPageValuesConfigData.path}} {{CustomGridPerPageValuesConfigData.value}}" stepKey="setCustomGridPerPageValues"/> <magentoCLI command="config:set {{CustomGridPerPageDefaultConfigData.path}} {{CustomGridPerPageDefaultConfigData.value}}" stepKey="setCustomGridPerPageDefaults"/> - <magentoCLI stepKey="flushConfigCache" command="cache:flush config"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> <magentoCron groups="index" stepKey="runCronIndex"/> </before> 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 aa0b49400c517..f52e24d72e4d4 100644 --- a/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch6/Test/Unit/Model/Client/ElasticsearchTest.php @@ -73,6 +73,7 @@ protected function setUp(): void 'delete', 'putMapping', 'deleteMapping', + 'getMapping', 'stats', 'updateAliases', 'existsAlias', @@ -439,7 +440,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -461,10 +462,18 @@ public function testAddFieldsMapping() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', ], ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -509,7 +518,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -531,10 +540,18 @@ public function testAddFieldsMappingFailure() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], - ] + ], ], ], ], @@ -592,6 +609,22 @@ public function testDeleteMappingFailure() ); } + /** + * Test get Elasticsearch mapping process. + * + * @return void + */ + public function testGetMapping(): void + { + $params = ['index' => 'indexName']; + $this->indicesMock->expects($this->once()) + ->method('getMapping') + ->with($params) + ->willReturn([]); + + $this->model->getMapping($params); + } + /** * Test query() method * @return void diff --git a/app/code/Magento/Elasticsearch6/composer.json b/app/code/Magento/Elasticsearch6/composer.json index 36297b03198e2..1ee92c0b0a3b3 100644 --- a/app/code/Magento/Elasticsearch6/composer.json +++ b/app/code/Magento/Elasticsearch6/composer.json @@ -8,7 +8,7 @@ "magento/module-catalog-search": "*", "magento/module-search": "*", "magento/module-elasticsearch": "*", - "elasticsearch/elasticsearch": "~7.6" + "elasticsearch/elasticsearch": "~7.7.0" }, "suggest": { "magento/module-config": "*" diff --git a/app/code/Magento/Elasticsearch6/etc/di.xml b/app/code/Magento/Elasticsearch6/etc/di.xml index 7263ae01f0f6d..e60f331f9ee8d 100644 --- a/app/code/Magento/Elasticsearch6/etc/di.xml +++ b/app/code/Magento/Elasticsearch6/etc/di.xml @@ -17,7 +17,7 @@ <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> <arguments> <argument name="engines" xsi:type="array"> - <item sortOrder="20" name="elasticsearch6" xsi:type="string">Elasticsearch 6.x</item> + <item sortOrder="20" name="elasticsearch6" xsi:type="string">Elasticsearch 6.x (Deprecated)</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php index feacca8d62804..d193c8aa108c8 100644 --- a/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php +++ b/app/code/Magento/Elasticsearch7/Model/Client/Elasticsearch.php @@ -51,8 +51,10 @@ public function __construct( $elasticsearchClient = null, $fieldsMappingPreprocessors = [] ) { - if (empty($options['hostname']) || ((!empty($options['enableAuth']) && - ($options['enableAuth'] == 1)) && (empty($options['username']) || empty($options['password'])))) { + if (empty($options['hostname']) + || ((!empty($options['enableAuth']) && ($options['enableAuth'] == 1)) + && (empty($options['username']) || empty($options['password']))) + ) { throw new LocalizedException( __('The search failed because of a search engine misconfiguration.') ); @@ -71,7 +73,7 @@ public function __construct( * @param array $query * @return array */ - public function suggest(array $query) : array + public function suggest(array $query): array { return $this->getElasticsearchClient()->suggest($query); } @@ -96,7 +98,7 @@ private function getElasticsearchClient(): \Elasticsearch\Client * * @return bool */ - public function ping() : bool + public function ping(): bool { if ($this->pingResult === null) { $this->pingResult = $this->getElasticsearchClient() @@ -111,7 +113,7 @@ public function ping() : bool * * @return bool */ - public function testConnection() : bool + public function testConnection(): bool { return $this->ping(); } @@ -122,7 +124,7 @@ public function testConnection() : bool * @param array $options * @return array */ - private function buildESConfig(array $options = []) : array + private function buildESConfig(array $options = []): array { $hostname = preg_replace('/http[s]?:\/\//i', '', $options['hostname']); // @codingStandardsIgnoreStart @@ -177,6 +179,23 @@ public function createIndex(string $index, array $settings) ); } + /** + * Add/update an Elasticsearch index settings. + * + * @param string $index + * @param array $settings + * @return void + */ + public function putIndexSettings(string $index, array $settings): void + { + $this->getElasticsearchClient()->indices()->putSettings( + [ + 'index' => $index, + 'body' => $settings, + ] + ); + } + /** * Delete an Elasticsearch 7 index. * @@ -194,12 +213,13 @@ public function deleteIndex(string $index) * @param string $index * @return bool */ - public function isEmptyIndex(string $index) : bool + public function isEmptyIndex(string $index): bool { $stats = $this->getElasticsearchClient()->indices()->stats(['index' => $index, 'metric' => 'docs']); - if ($stats['indices'][$index]['primaries']['docs']['count'] === 0) { + if ($stats['indices'][$index]['primaries']['docs']['count'] === 0) { return true; } + return false; } @@ -234,7 +254,7 @@ public function updateAlias(string $alias, string $newIndex, string $oldIndex = * @param string $index * @return bool */ - public function indexExists(string $index) : bool + public function indexExists(string $index): bool { return $this->getElasticsearchClient()->indices()->exists(['index' => $index]); } @@ -246,12 +266,13 @@ public function indexExists(string $index) : bool * @param string $index * @return bool */ - public function existsAlias(string $alias, string $index = '') : bool + public function existsAlias(string $alias, string $index = ''): bool { $params = ['name' => $alias]; if ($index) { $params['index'] = $index; } + return $this->getElasticsearchClient()->indices()->existsAlias($params); } @@ -261,7 +282,7 @@ public function existsAlias(string $alias, string $index = '') : bool * @param string $alias * @return array */ - public function getAlias(string $alias) : array + public function getAlias(string $alias): array { return $this->getElasticsearchClient()->indices()->getAlias(['name' => $alias]); } @@ -289,7 +310,7 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -311,7 +332,15 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], ], @@ -333,11 +362,22 @@ public function addFieldsMapping(array $fields, string $index, string $entityTyp * @param array $query * @return array */ - public function query(array $query) : array + public function query(array $query): array { return $this->getElasticsearchClient()->search($query); } + /** + * Get mapping from Elasticsearch index. + * + * @param array $params + * @return array + */ + public function getMapping(array $params): array + { + return $this->getElasticsearchClient()->indices()->getMapping($params); + } + /** * Delete mapping in Elasticsearch 7 index * diff --git a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php index 593bbd7792f46..3b3cbcfbb15f8 100644 --- a/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php +++ b/app/code/Magento/Elasticsearch7/Test/Unit/Model/Client/ElasticsearchTest.php @@ -73,6 +73,7 @@ protected function setUp(): void 'delete', 'putMapping', 'deleteMapping', + 'getMapping', 'stats', 'updateAliases', 'existsAlias', @@ -438,7 +439,7 @@ public function testAddFieldsMapping() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -460,10 +461,18 @@ public function testAddFieldsMapping() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', ], ], - ] + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', + ], + ], + ], ], ], ], @@ -509,7 +518,7 @@ public function testAddFieldsMappingFailure() 'match' => 'price_*', 'match_mapping_type' => 'string', 'mapping' => [ - 'type' => 'float', + 'type' => 'double', 'store' => true, ], ], @@ -531,10 +540,18 @@ public function testAddFieldsMappingFailure() 'mapping' => [ 'type' => 'text', 'index' => true, - 'copy_to' => '_search' + 'copy_to' => '_search', + ], + ], + ], + [ + 'integer_mapping' => [ + 'match_mapping_type' => 'long', + 'mapping' => [ + 'type' => 'integer', ], ], - ] + ], ], ], ], @@ -592,6 +609,22 @@ public function testDeleteMappingFailure() ); } + /** + * Test get Elasticsearch mapping process. + * + * @return void + */ + public function testGetMapping(): void + { + $params = ['index' => 'indexName']; + $this->indicesMock->expects($this->once()) + ->method('getMapping') + ->with($params) + ->willReturn([]); + + $this->model->getMapping($params); + } + /** * Test query() method * @return void diff --git a/app/code/Magento/Elasticsearch7/composer.json b/app/code/Magento/Elasticsearch7/composer.json index 739ac1019c5ae..1e59ceaebaf84 100644 --- a/app/code/Magento/Elasticsearch7/composer.json +++ b/app/code/Magento/Elasticsearch7/composer.json @@ -5,7 +5,7 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-elasticsearch": "*", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/module-advanced-search": "*", "magento/module-catalog-search": "*" }, diff --git a/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml b/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml index 52e5be5a5beeb..9e818ff61eb89 100644 --- a/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml +++ b/app/code/Magento/Elasticsearch7/etc/adminhtml/system.xml @@ -10,7 +10,7 @@ <system> <section id="catalog"> <group id="search"> - <!-- Elasticsearch 7.0+ --> + <!-- Elasticsearch 7 --> <field id="elasticsearch7_server_hostname" translate="label" type="text" sortOrder="61" showInDefault="1" showInWebsite="0" showInStore="0"> <label>Elasticsearch Server Hostname</label> diff --git a/app/code/Magento/Elasticsearch7/etc/di.xml b/app/code/Magento/Elasticsearch7/etc/di.xml index b5d013a294e26..446331edc63fb 100644 --- a/app/code/Magento/Elasticsearch7/etc/di.xml +++ b/app/code/Magento/Elasticsearch7/etc/di.xml @@ -17,7 +17,7 @@ <type name="Magento\Search\Model\Adminhtml\System\Config\Source\Engine"> <arguments> <argument name="engines" xsi:type="array"> - <item sortOrder="30" name="elasticsearch7" xsi:type="string">Elasticsearch 7.0+</item> + <item sortOrder="30" name="elasticsearch7" xsi:type="string">Elasticsearch 7</item> </argument> </arguments> </type> diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php b/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php index 112813c3b096c..7beb266508cc9 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Edit.php @@ -20,7 +20,7 @@ class Edit extends Widget implements ContainerInterface { /** * @var \Magento\Framework\Registry - * @deprecated since 2.3.0 in favor of stateful global objects elimination. + * @deprecated 101.0.0 since 2.3.0 in favor of stateful global objects elimination. */ protected $_registryManager; diff --git a/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php b/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php index 2cd3ea42649c1..ec97d462c0f74 100644 --- a/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php +++ b/app/code/Magento/Email/Block/Adminhtml/Template/Edit/Form.php @@ -9,8 +9,13 @@ */ namespace Magento\Email\Block\Adminhtml\Template\Edit; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml email template edit form block + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Form extends \Magento\Backend\Block\Widget\Form\Generic { @@ -29,6 +34,11 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic */ private $serializer; + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Registry $registry @@ -37,6 +47,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Variable\Model\Source\Variables $variables * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param SecureHtmlRenderer|null $secureRenderer * @throws \RuntimeException */ public function __construct( @@ -46,12 +57,14 @@ public function __construct( \Magento\Variable\Model\VariableFactory $variableFactory, \Magento\Variable\Model\Source\Variables $variables, array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_variableFactory = $variableFactory; $this->_variables = $variables; $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); parent::__construct($context, $registry, $formFactory, $data); } @@ -93,11 +106,16 @@ protected function _prepareForm() [ 'label' => __('Currently Used For'), 'container_id' => 'currently_used_for', - 'after_element_html' => '<script>require(["prototype"], function () {' . - (!$this->getEmailTemplate()->getSystemConfigPathsWhereCurrentlyUsed() ? '$(\'' . - 'currently_used_for' . - '\').hide(); ' : '') . - '});</script>' + 'after_element_html' => $this->secureRenderer->renderTag( + 'script', + [], + 'require(["prototype"], function () {' . + (!$this->getEmailTemplate()->getSystemConfigPathsWhereCurrentlyUsed() ? '$(\'' . + 'currently_used_for' . + '\').hide(); ' : '') . + '});', + false + ), ] ); diff --git a/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php b/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php index 50153b2bb6520..5af5230b0e33d 100644 --- a/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php +++ b/app/code/Magento/Email/Controller/Adminhtml/Email/Template.php @@ -23,7 +23,7 @@ abstract class Template extends \Magento\Backend\App\Action * Core registry * * @var \Magento\Framework\Registry - * @deprecated since 2.3.0 in favor of stateful global objects elimination. + * @deprecated 101.0.0 since 2.3.0 in favor of stateful global objects elimination. */ protected $_coreRegistry = null; diff --git a/app/code/Magento/Email/Model/Template/Filter.php b/app/code/Magento/Email/Model/Template/Filter.php index 82ebc8c78d8b2..648e4ab8fc380 100644 --- a/app/code/Magento/Email/Model/Template/Filter.php +++ b/app/code/Magento/Email/Model/Template/Filter.php @@ -6,8 +6,9 @@ namespace Magento\Email\Model\Template; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; use Magento\Framework\Filter\VariableResolverInterface; use Magento\Framework\View\Asset\ContentProcessorException; use Magento\Framework\View\Asset\ContentProcessorInterface; @@ -44,6 +45,7 @@ class Filter extends \Magento\Framework\Filter\Template * Whether to allow SID in store directive: NO * * @var bool + * @deprecated SID is not being used as query parameter anymore. */ protected $_useSessionInUrl = false; @@ -51,7 +53,7 @@ class Filter extends \Magento\Framework\Filter\Template * Modifier Callbacks * * @var array - * @deprecated Use the new Directive Processor interfaces + * @deprecated 101.0.4 Use the new Directive Processor interfaces */ protected $_modifiers = ['nl2br' => '']; @@ -263,10 +265,14 @@ public function setUseAbsoluteLinks($flag) * * @param bool $flag * @return $this + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @deprecated SID query parameter is not used in URLs anymore. */ public function setUseSessionInUrl($flag) { - $this->_useSessionInUrl = $flag; + // phpcs:disable Magento2.Functions.DiscouragedFunction + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return $this; } @@ -659,7 +665,7 @@ public function varDirective($construction) * @param string $value * @param string $default assumed modifier if none present * @return array - * @deprecated Use the new FilterApplier or Directive Processor interfaces + * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces */ protected function explodeModifiers($value, $default = null) { @@ -678,7 +684,7 @@ protected function explodeModifiers($value, $default = null) * @param string $value * @param string $modifiers * @return string - * @deprecated Use the new FilterApplier or Directive Processor interfaces + * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces */ protected function applyModifiers($value, $modifiers) { @@ -706,7 +712,7 @@ protected function applyModifiers($value, $modifiers) * @param string $value * @param string $type * @return string - * @deprecated Use the new FilterApplier or Directive Processor interfaces + * @deprecated 101.0.4 Use the new FilterApplier or Directive Processor interfaces */ public function modifierEscape($value, $type = 'html') { @@ -734,27 +740,32 @@ public function modifierEscape($value, $type = 'html') * {{protocol store="1"}} - Optional parameter which gets protocol from provide store based on store ID or code * * @param string[] $construction - * @throws \Magento\Framework\Exception\MailException * @return string + * @throws MailException + * @throws NoSuchEntityException */ public function protocolDirective($construction) { $params = $this->getParameters($construction[2]); + $store = null; if (isset($params['store'])) { try { $store = $this->_storeManager->getStore($params['store']); } catch (\Exception $e) { - throw new \Magento\Framework\Exception\MailException( + throw new MailException( __('Requested invalid store "%1"', $params['store']) ); } } + $isSecure = $this->_storeManager->getStore($store)->isCurrentlySecure(); $protocol = $isSecure ? 'https' : 'http'; if (isset($params['url'])) { return $protocol . '://' . $params['url']; } elseif (isset($params['http']) && isset($params['https'])) { + $this->validateProtocolDirectiveHttpScheme($params); + if ($isSecure) { return $params['https']; } @@ -764,6 +775,37 @@ public function protocolDirective($construction) return $protocol; } + /** + * Validate protocol directive HTTP parameters. + * + * @param string[] $params + * @return void + * @throws MailException + */ + private function validateProtocolDirectiveHttpScheme(array $params) : void + { + $parsed_http = parse_url($params['http']); + $parsed_https = parse_url($params['https']); + + if (empty($parsed_http)) { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['http']) + ); + } elseif (empty($parsed_https)) { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['https']) + ); + } elseif ($parsed_http['scheme'] !== 'http') { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['http']) + ); + } elseif ($parsed_https['scheme'] !== 'https') { + throw new MailException( + __('Contents of %1 could not be loaded or is empty', $params['https']) + ); + } + } + /** * Store config directive * diff --git a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php index 2589d88476725..ac890dd3d4a73 100644 --- a/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php +++ b/app/code/Magento/Email/Test/Unit/Model/Template/FilterTest.php @@ -14,6 +14,8 @@ use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\MailException; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\App\State; use Magento\Framework\Css\PreProcessor\Adapter\CssInliner; use Magento\Framework\Escaper; @@ -452,4 +454,45 @@ public function testConfigDirectiveUnavailable() $this->assertEquals($scopeConfigValue, $this->getModel()->configDirective($construction)); } + + /** + * @throws MailException + * @throws NoSuchEntityException + */ + public function testProtocolDirectiveWithValidSchema() + { + $model = $this->getModel(); + $storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $storeMock->expects($this->once())->method('isCurrentlySecure')->willReturn(true); + $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); + + $data = [ + "{{protocol http=\"http://url\" https=\"https://url\"}}", + "protocol", + " http=\"http://url\" https=\"https://url\"" + ]; + $this->assertEquals('https://url', $model->protocolDirective($data)); + } + + /** + * @throws NoSuchEntityException + */ + public function testProtocolDirectiveWithInvalidSchema() + { + $this->expectException( + \Magento\Framework\Exception\MailException::class + ); + + $model = $this->getModel(); + $storeMock = $this->createMock(\Magento\Store\Model\Store::class); + $storeMock->expects($this->once())->method('isCurrentlySecure')->willReturn(true); + $this->storeManager->expects($this->once())->method('getStore')->willReturn($storeMock); + + $data = [ + "{{protocol http=\"https://url\" https=\"http://url\"}}", + "protocol", + " http=\"https://url\" https=\"http://url\"" + ]; + $model->protocolDirective($data); + } } diff --git a/app/code/Magento/Email/composer.json b/app/code/Magento/Email/composer.json index a85c6177c8a48..334bbcf9d4617 100644 --- a/app/code/Magento/Email/composer.json +++ b/app/code/Magento/Email/composer.json @@ -14,7 +14,8 @@ "magento/module-theme": "*", "magento/module-require-js": "*", "magento/module-media-storage": "*", - "magento/module-variable": "*" + "magento/module-variable": "*", + "magento/module-ui": "*" }, "suggest": { "magento/module-theme": "*" diff --git a/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml index 29ceb71a138e4..900c527dcff17 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -5,6 +5,7 @@ */ /** @var \Magento\Backend\Block\Page $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="preview" class="cms-revision-preview"> <iframe name="preview_iframe" @@ -20,12 +21,12 @@ target="preview_iframe" > <input type="hidden" name="form_key" value="<?= /* @noEscape */ $block->getFormKey() ?>" /> - <?php foreach ($block->getPreviewFormViewModel()->getFormFields() as $name => $value) : ?> + <?php foreach ($block->getPreviewFormViewModel()->getFormFields() as $name => $value): ?> <input type="hidden" name="<?= $block->escapeHtmlAttr($name) ?>" value="<?= $block->escapeHtmlAttr($value) ?>"/> <?php endforeach; ?> </form> </div> -<script> +<?php $scriptString = <<<script require([ 'jquery' ], function($) { @@ -37,4 +38,6 @@ require([ $(this).height($(this).contents().height()); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> 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 0dceb9d51a99e..a16a3aae14b49 100644 --- a/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Email/view/adminhtml/templates/template/edit.phtml @@ -9,6 +9,7 @@ use Magento\Framework\App\TemplateTypesInterface; // phpcs:disable Generic.Files.LineLength.TooLong /** @var $block \Magento\Email\Block\Adminhtml\Template\Edit */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php if (!$block->getEditMode()): ?> <form action="<?= $block->escapeUrl($block->getLoadUrl()) ?>" method="post" id="email_template_load_form"> @@ -16,7 +17,9 @@ use Magento\Framework\App\TemplateTypesInterface; <fieldset class="admin__fieldset form-inline"> <legend class="admin__legend"><span><?= $block->escapeHtml(__('Load Default Template')) ?></span></legend><br> <div class="admin__field required"> - <label class="admin__field-label" for="template_select"><span><?= $block->escapeHtml(__('Template')) ?></span></label> + <label class="admin__field-label" for="template_select"> + <span><?= $block->escapeHtml(__('Template')) ?></span> + </label> <div class="admin__field-control"> <select id="template_select" name="code" class="admin__control-select required-entry"> <?php foreach ($block->getTemplateOptions() as $group => $options): ?> @@ -24,7 +27,10 @@ use Magento\Framework\App\TemplateTypesInterface; <optgroup label="<?= $block->escapeHtmlAttr($group) ?>"> <?php endif; ?> <?php foreach ($options as $option): ?> - <option value="<?= $block->escapeHtmlAttr($option['value']) ?>"<?= /* @noEscape */ $block->getOrigTemplateCode() == $option['value'] ? ' selected="selected"' : '' ?>><?= $block->escapeHtml($option['label']) ?></option> + <option value="<?= $block->escapeHtmlAttr($option['value']) ?>" + <?= /* @noEscape */ $block->getOrigTemplateCode() == $option['value'] ? + ' selected="selected"' : '' ?>><?= $block->escapeHtml($option['label']) ?> + </option> <?php endforeach; ?> <?php if ($group): ?> </optgroup> @@ -46,19 +52,26 @@ use Magento\Framework\App\TemplateTypesInterface; <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="email_template_edit_form"> <?= /* @noEscape */ $block->getBlockHtml('formkey') ?> <input type="hidden" id="change_flag_element" name="_change_type_flag" value="" /> - <input type="hidden" id="orig_template_code" name="orig_template_code" value="<?= $block->escapeHtmlAttr($block->getOrigTemplateCode()) ?>" /> + <input type="hidden" id="orig_template_code" name="orig_template_code" + value="<?= $block->escapeHtmlAttr($block->getOrigTemplateCode()) ?>" /> <?= /* @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="post" 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 ?>" /> + <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>"/> <input type="hidden" id="preview_text" name="text" value="" /> <input type="hidden" id="preview_styles" name="styles" value="" /> </div> </form> -<script> +<?php +$currentlyUsedForPaths = /* @noEscape */ $block->getCurrentlyUsedForPaths(); +$templateType = (int)$block->getTemplateType(); +$typeText = /* @noEscape */ TemplateTypesInterface::TYPE_TEXT; +$scriptString = <<<script + require([ "jquery", "wysiwygAdapter", @@ -94,7 +107,7 @@ require([ this.bindEvents(); - this.renderPaths(<?= /* @noEscape */ $block->getCurrentlyUsedForPaths() ?>, 'currently_used_for'); + this.renderPaths({$currentlyUsedForPaths}, 'currently_used_for'); }, bindEvents: function(){ @@ -119,7 +132,7 @@ require([ var self = this; confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to strip tags?'))) ?>", + content: "{$block->escapeJs(__('Are you sure you want to strip tags?'))}", actions: { confirm: function () { this.unconvertedText = $('template_text').value; @@ -153,9 +166,9 @@ require([ }, preview: function() { if (this.typeChange) { - $('preview_type').value = <?= /* @noEscape */ TemplateTypesInterface::TYPE_TEXT ?>; + $('preview_type').value = {$typeText}; } else { - $('preview_type').value = <?= (int) $block->getTemplateType() ?>; + $('preview_type').value = {$templateType}; } if (typeof tinyMCE == 'undefined' || !tinyMCE.get('template_text')) { @@ -175,10 +188,10 @@ require([ deleteTemplate: function() { confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>", + content: "{$block->escapeJs(__('Are you sure you want to delete this template?'))}", actions: { confirm: function () { - window.location.href = '<?= $block->escapeJs($block->escapeUrl($block->getDeleteUrl())) ?>'; + window.location.href = '{$block->escapeJs($block->getDeleteUrl())}'; } } }); @@ -197,7 +210,7 @@ require([ area: $('email_template_load_form'), onComplete: function (transport) { if (transport.responseText.isJSON()) { - var fields = $H(transport.responseText.evalJSON()); + var fields = \$H(transport.responseText.evalJSON()); fields.each(function(pair) { if ($(pair.key)) { $(pair.key).value = pair.value.strip(); @@ -225,7 +238,9 @@ require([ }.bind(this)); } else { alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('The template did not load. Please review the log for details.'))) ?>' + content: '{$block->escapeJs(__( + 'The template did not load. Please review the log for details.' + ))}' }); } }.bind(this) @@ -236,7 +251,8 @@ require([ renderPaths: function(paths, fieldId) { var field = $(fieldId); if (field) { - field.down('div').down('div').update(this.parsePath(paths, '<span class="path-delimiter"> -> </span>', '<br />')); + field.down('div').down('div') + .update(this.parsePath(paths, '<span class="path-delimiter"> -> </span>', '<br />')); } }, @@ -250,7 +266,8 @@ require([ } if(!Object.isString(value) && value.title) { - value = (value.url ? '<a href="' + value.url + '">' + value.title + '</a>' : value.title) + (value.scope ? '  <span class="path-scope-label">(' + value.scope + ')</span>' : ''); + value = (value.url ? '<a href="' + value.url + '">' + value.title + '</a>' : value.title) + + (value.scope ? '  <span class="path-scope-label">(' + value.scope + ')</span>' : ''); } return value; @@ -278,4 +295,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml b/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml index 7ce37af60fd7f..b0bf115915c2d 100644 --- a/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml +++ b/app/code/Magento/EncryptionKey/Test/Mftf/Section/AdminEncryptionKeyChangeFormSection.xml @@ -10,7 +10,7 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="AdminEncryptionKeyChangeFormSection"> <element name="autoGenerate" type="select" selector="#generate_random"/> - <element name="cryptKey" type="input" selector="#crypt_key"/> + <element name="cryptKey" type="input" selector="input#crypt_key"/> <element name="changeEncryptionKey" type="button" selector=".page-actions-buttons #save" timeout="10"/> </section> </sections> diff --git a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml index 1c61bd290f005..2144f486f9bde 100644 --- a/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml +++ b/app/code/Magento/Fedex/Test/Mftf/Test/AdminCreatingShippingLabelTest.xml @@ -50,8 +50,12 @@ <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} {{DE_Address_Berlin_Not_Default_Address.postcode}}" stepKey="setOriginZipCode"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} '{{DE_Address_Berlin_Not_Default_Address.street[0]}}'" stepKey="setOriginStreetAddress"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} '{{US_Address_California.street[0]}}'" stepKey="setOriginStreetAddress2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Reset configs--> @@ -70,8 +74,12 @@ <magentoCLI command="config:set {{AdminShippingSettingsOriginZipCodeConfigData.path}} ''" stepKey="setOriginZipCode"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddressConfigData.path}} ''" stepKey="setOriginStreetAddress"/> <magentoCLI command="config:set {{AdminShippingSettingsOriginStreetAddress2ConfigData.path}} ''" stepKey="setOriginStreetAddress2"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Delete created data--> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -105,7 +113,7 @@ </actionGroup> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> <!--Open created order in admin--> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchOrder"> <argument name="keyword" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl index 62795f07239a6..3629bb424f207 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v10.wsdl @@ -472,7 +472,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -983,7 +983,7 @@ </xs:element> <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions</xs:documentation> + <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> </xs:annotation> </xs:element> <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> @@ -1005,7 +1005,7 @@ <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -4867,4 +4867,4 @@ <s1:address location="https://wsbeta.fedex.com:443/web-services/rate"/> </port> </service> -</definitions> \ No newline at end of file +</definitions> diff --git a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl index 17a6f74cc09b8..2f3feecb58084 100644 --- a/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/RateService_v9.wsdl @@ -471,7 +471,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment commitment more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -983,7 +983,7 @@ </xs:element> <xs:element name="CustomsValue" type="ns:Money" minOccurs="0"> <xs:annotation> - <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the master transaction and all child transactions</xs:documentation> + <xs:documentation>The total customs value for the shipment. This total will rrepresent th esum of the values of all commodities, and may include freight, miscellaneous, and insurance charges. Must contain 2 explicit decimal positions with a max length of 17 including the decimal. For Express International MPS, the Total Customs Value is in the main transaction and all child transactions</xs:documentation> </xs:annotation> </xs:element> <xs:element name="FreightOnValue" type="ns:FreightOnValueType" minOccurs="0"> @@ -1005,7 +1005,7 @@ <xs:element name="Commodities" type="ns:Commodity" minOccurs="0" maxOccurs="99"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl index 54bb57d490c76..439d032a61fd0 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v10.wsdl @@ -497,7 +497,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -724,7 +724,7 @@ </xs:element> <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> <xs:annotation> - <xs:documentation>The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> + <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> diff --git a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl index d8dc0fdfed4ab..a449bf41dbd68 100644 --- a/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl +++ b/app/code/Magento/Fedex/etc/wsdl/ShipService_v9.wsdl @@ -497,7 +497,7 @@ <xs:complexType name="Commodity"> <xs:annotation> <xs:documentation> - For international multiple piece shipments, commodity information must be passed in the Master and on each child transaction. + For international multiple piece shipments, commodity information must be passed in the Main and on each child transaction. If this shipment contains more than four commodity line items, the four highest valued should be included in the first 4 occurrences for this request. </xs:documentation> </xs:annotation> @@ -724,7 +724,7 @@ </xs:element> <xs:element name="MasterTrackingId" type="ns:TrackingId" minOccurs="0"> <xs:annotation> - <xs:documentation>The master tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> + <xs:documentation>The main tracking number and form id of this multiple piece shipment. This information is to be provided for each subsequent of a multiple piece shipment.</xs:documentation> </xs:annotation> </xs:element> <xs:element name="ServiceTypeDescription" type="xs:string" minOccurs="0"> diff --git a/app/code/Magento/GiftMessage/Block/Message/Inline.php b/app/code/Magento/GiftMessage/Block/Message/Inline.php index 4a9311c1b4ba2..475f1c2b717ae 100644 --- a/app/code/Magento/GiftMessage/Block/Message/Inline.php +++ b/app/code/Magento/GiftMessage/Block/Message/Inline.php @@ -280,7 +280,7 @@ public function countItems() /** * Call method getItemsHasMessages * - * @deprecated Misspelled method + * @deprecated 100.2.4 Misspelled method * @see getItemsHasMessages */ public function getItemsHasMesssages() diff --git a/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml b/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml index 908d6c91bfb5f..e45446450f872 100644 --- a/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml +++ b/app/code/Magento/GiftMessage/view/adminhtml/templates/popup.phtml @@ -3,26 +3,42 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getChildHtml()) :?> -<div id="gift_options_configure_new" class="gift-options-popup product-configure-popup" style="display: none;"> +<?php if ($block->getChildHtml()):?> +<div id="gift_options_configure_new" class="gift-options-popup product-configure-popup no-display"> <div id="gift_options_form_contents"> <div class="content"> <?= $block->getChildHtml() ?> </div> <div class="ui-dialog-buttonset"> - <button type="button" class="action-close" id="gift_options_cancel_button"><span><?= $block->escapeHtml(__('Cancel')) ?></span></button> - <button type="button" class="action-primary" id="gift_options_ok_button"><span><?= $block->escapeHtml(__('OK')) ?></span></button> + <button type="button" class="action-close" id="gift_options_cancel_button"> + <span><?= $block->escapeHtml(__('Cancel')) ?></span> + </button> + <button type="button" class="action-primary" id="gift_options_ok_button"> + <span><?= $block->escapeHtml(__('OK')) ?></span> + </button> </div> </div> </div> + <div id="giftoptions_tooltip_window" class="gift-options-tooltip no-display"> + <div id="giftoptions_tooltip_window_content"> </div> + </div> + <?php $scriptString = <<<script + require(['jquery'], function($){ + 'use strict'; + $('div#gift_options_configure_new').css('display', 'none'); + $('div#gift_options_configure_new').removeClass('no-display'); -<div id="giftoptions_tooltip_window" class="gift-options-tooltip" style="display: none;"> - <div id="giftoptions_tooltip_window_content"> </div> -</div> - -<script> + $('div#giftoptions_tooltip_window').css('display', 'none'); + $('div#giftoptions_tooltip_window').removeClass('no-display'); + }); +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <?php $scriptString = <<<script require([ "Magento_Sales/order/create/giftmessage", "Magento_Sales/order/giftoptions_tooltip" @@ -36,5 +52,7 @@ giftOptionsTooltip.setTooltipWindow('giftoptions_tooltip_window','giftoptions_to //]]> window.giftMessageSet = giftMessageSet; }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif;?> diff --git a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml index 1833ae0d2e339..41b77ad74d148 100644 --- a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml +++ b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/create/giftoptions.phtml @@ -3,24 +3,32 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_item = $block->getItem() ?> -<?php if ($_item) : ?> +<?php if ($_item): ?> <?php $_childHtml = trim($block->getChildHtml('', false));?> - <?php if ($_childHtml) : ?> + <?php if ($_childHtml): ?> <tr class="row-gift-options"> <td colspan="7"> - <a class="action-link" href="#" id="gift_options_link_<?= (int) $_item->getId() ?>"><?= $block->escapeHtml(__('Gift Options')) ?></a> - <script> + <a class="action-link" href="#" id="gift_options_link_<?= (int) $_item->getId() ?>"> + <?= $block->escapeHtml(__('Gift Options')) ?> + </a> + <?php $itemId = (int) ($_item->getId()); + $scriptString = <<<script + require([ "Magento_Sales/order/giftoptions_tooltip" ], function(){ - giftOptionsTooltip.addTargetLink('gift_options_link_<?= (int) $_item->getId() ?>', <?= (int) $_item->getId() ?>); + giftOptionsTooltip.addTargetLink('gift_options_link_{$itemId}', {$itemId}); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="gift_options_data_<?= (int) $_item->getId() ?>"> <?= /* @noEscape */ $_childHtml ?> </div> diff --git a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml index 60a6e1b222b17..1c2486d5471e4 100644 --- a/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml +++ b/app/code/Magento/GiftMessage/view/adminhtml/templates/sales/order/view/giftoptions.phtml @@ -3,26 +3,30 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_childHtml = trim($block->getChildHtml('', false)); ?> -<?php if ($_childHtml) : ?> +<?php if ($_childHtml): ?> <?php $_item = $block->getItem() ?> <tr> <td colspan="10" class="last"> <a class="action-link" href="#" id="gift_options_link_<?= (int) $_item->getId() ?>"> <?= $block->escapeHtml(__('Gift Options')) ?> </a> - <script> + <?php $itemId = (int) ($_item->getId()); + $scriptString = <<<script require([ "Magento_Sales/order/giftoptions_tooltip" ], function(){ giftOptionsTooltip.addTargetLink( - 'gift_options_link_<?= (int) ($_item->getId()) ?>', - <?= (int) $_item->getId() ?> + 'gift_options_link_{$itemId}', {$itemId} ); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="gift_options_data_<?= (int) $_item->getId() ?>"> <?= /* @noEscape */ $_childHtml ?> </div> diff --git a/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml b/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml index 4d8a054e67fc5..3d814ac41302d 100644 --- a/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml +++ b/app/code/Magento/GiftMessage/view/frontend/templates/cart/gift_options.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="gift-options-cart" data-bind="scope:'giftOptionsCart'"> <!-- ko template: getTemplate() --><!-- /ko --> @@ -13,7 +15,10 @@ } } </script> - <script> - window.giftOptionsConfig = <?= /* @noEscape */ $block->getGiftOptionsConfigJson() ?>; - </script> +<?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + [], + 'window.giftOptionsConfig = '. /* @noEscape */ $block->getGiftOptionsConfigJson(), + false +) ?> </div> diff --git a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml index cb462a630e3a6..f89657d3c5d90 100644 --- a/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml +++ b/app/code/Magento/GiftMessage/view/frontend/templates/inline.phtml @@ -3,73 +3,118 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature -//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace -//phpcs:disable PSR2.ControlStructures.SwitchDeclaration -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php $_giftMessage = false; ?> -<?php switch ($block->getCheckoutType()) : case 'onepage_checkout': ?> +<?php $_giftMessage = false; +switch ($block->getCheckoutType()): + case 'onepage_checkout': + ?> <fieldset class="fieldset gift-message"> - <legend class="legend"><span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span></legend><br> + <legend class="legend"> + <span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span> + </legend><br> <div class="field choice" id="add-gift-options-<?= (int) $block->getEntity()->getId() ?>"> - <input type="checkbox" name="allow_gift_options" id="allow_gift_options" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-container"}'<?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options" class="label"><span><?= $block->escapeHtml(__('Add Gift Options')) ?></span></label> + <input type="checkbox" name="allow_gift_options" id="allow_gift_options" data-mage-init='{"giftOptions":{}}' + value="1" data-selector='{"id":"#allow-gift-options-container"}' + <?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()):?> + checked="checked"<?php endif; ?> class="checkbox" /> + <label for="allow_gift_options" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options')) ?></span> + </label> </div> <dl class="options-items" id="allow-gift-options-container"> <?php if ($block->isMessagesAvailable()): ?> <dt id="add-gift-options-for-order-<?= (int) $block->getEntity()->getId() ?>" class="order-title"> <div class="field choice"> - <input type="checkbox" name="allow_gift_messages_for_order" id="allow_gift_options_for_order" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-order-container"}'<?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_order" class="label"><span><?= $block->escapeHtml(__('Gift Options for the Entire Order')) ?></span></label> + <input type="checkbox" name="allow_gift_messages_for_order" id="allow_gift_options_for_order" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-order-container"}' + <?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> + class="checkbox" /> + <label for="allow_gift_options_for_order" class="label"> + <span><?= $block->escapeHtml(__('Gift Options for the Entire Order')) ?></span> + </label> </div> </dt> <dd id="allow-gift-options-for-order-container" class="order-options"> - <div class="options-order-container" id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> + <div class="options-order-container" + id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#allow-gift-messages-for-order-container"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#allow-gift-messages-for-order-container"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> <div id="allow-gift-messages-for-order-container" class="gift-messages-order hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('Leave this box blank if you don\'t want to leave a gift message for the entire order.')) ?></p> + <p><?= $block->escapeHtml(__( + 'Leave this box blank if you don\'t want to leave a gift message for the entire order.' + )) ?></p> <div class="field from"> - <label for="gift-message-whole-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-whole-from" class="label"> + <span><?= $block->escapeHtml(__('From')) ?></span></label> <div class="control"> - <input type="text" name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][from]" id="gift-message-whole-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][from]" + id="gift-message-whole-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value="<?= /* @noEscape */ $block + ->getEscaped($block->getMessage()->getSender(), $block->getDefaultFrom()) + ?>" + class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-whole-to" class="label"><span><?= $block->escapeHtml(__('To')) ?></span></label> + <label for="gift-message-whole-to" class="label"> + <span><?= $block->escapeHtml(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][to]" id="gift-message-whole-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][to]" + id="gift-message-whole-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value="<?= /* @noEscape */ $block + ->getEscaped($block->getMessage()->getRecipient(), $block->getDefaultTo()) + ?>" class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-whole-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-whole-message" class="label"> + <span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-whole-message" class="input-text" name="giftmessage[quote][<?= (int) $block->getEntity()->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> + <textarea id="gift-message-whole-message" class="input-text" + name="giftmessage[quote][<?=(int)$block->getEntity()->getId()?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> </div> </div> </fieldset> - <script> + <?php $entityId = (int) $block->getEntity()->getId(); + $scriptString = <<<script require(['jquery'], function(jQuery){ - jQuery('#add-gift-options-<?= (int) $block->getEntity()->getId() ?>') - .add('#add-gift-options-for-order-<?= (int) $block->getEntity()->getId() ?>') + jQuery('#add-gift-options-{$entityId}') + .add('#add-gift-options-for-order-{$entityId}') .removeClass('hidden'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </dd> <?php endif ?> <?php if ($block->isItemsAvailable()): ?> - <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId() ?>" class="order-title individual"> + <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId()?>" class="order-title individual"> <div class="field choice"> - <input type="checkbox" name="allow_gift_options_for_items" id="allow_gift_options_for_items" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-items-container"}'<?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_items" class="label"><span><?= $block->escapeHtml(__('Gift Options for Individual Items')) ?></span></label> + <input type="checkbox" name="allow_gift_options_for_items" id="allow_gift_options_for_items" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-items-container"}' + <?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> + class="checkbox" /> + <label for="allow_gift_options_for_items" class="label"> + <span><?= $block->escapeHtml(__('Gift Options for Individual Items')) ?></span> + </label> </div> </dt> @@ -80,7 +125,11 @@ <li class="item"> <div class="product"> <div class="number"> - <?= $block->escapeHtml(__('<span>Item %1</span> of %2', $_index+1, $block->countItems()), ['span']) ?> + <?= $block->escapeHtml(__( + '<span>Item %1</span> of %2', + $_index+1, + $block->countItems() + ), ['span']) ?> </div> <div class="img photo container"> <?= $block->getImage($_product, 'gift_messages_checkout_thumbnail')->toHtml() ?> @@ -88,31 +137,64 @@ <strong class="product name"><?= $block->escapeHtml($_product->getName()) ?></strong> </div> <div class="options"> - <div class="options-items-container" id="options-items-container-<?= (int) $block->getEntity()->getId() ?>-<?= (int) $_item->getId() ?>"></div> + <div class="options-items-container" + id="options-items-container-<?= (int) $block->getEntity()->getId() + ?>-<?= (int) $_item->getId() ?>"></div> <?php if ($block->isItemMessagesAvailable($_item)): ?> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() ?>"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() + ?>"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> - <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" class="block message hidden"> + <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" + class="block message hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('Leave a box blank if you don\'t want to add a gift message for that item.')) ?></p> + <p><?= $block->escapeHtml(__( + 'Leave a box blank if you don\'t want to add a gift message for that item.' + )) ?></p> <div class="field from"> - <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"> + <span><?= $block->escapeHtml(__('From')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][from]" id="gift-message-<?= (int) $_item->getId() ?>-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][from]" + id="gift-message-<?= (int) $_item->getId() ?>-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value= + "<?= /* @noEscape */ + $block->getEscaped( + $block->getMessage($_item)->getSender(), + $block->getDefaultFrom() + ) ?>" class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"><span><?= $block->escapeHtmlAttr(__('To')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"> + <span><?= $block->escapeHtmlAttr(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][to]" id="gift-message-<?= (int) $_item->getId() ?>-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][to]" + id="gift-message-<?= (int) $_item->getId() ?>-to" + title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block + ->getMessage($_item)->getRecipient(), $block->getDefaultTo()) ?>" + class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-<?= (int) $_item->getId() ?>-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-message" class="label"> + <span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" class="input-text giftmessage-area" name="giftmessage[quote_item][<?= (int) $_item->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> + <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" + class="input-text giftmessage-area" + name="giftmessage[quote_item][<?= (int) $_item->getId() + ?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" + rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> </div> </div> </fieldset> @@ -123,15 +205,20 @@ <?php endforeach; ?> </ol> </dd> - <script> + <?php $entityId = (int) $block->getEntity()->getId(); + $scriptString = <<<script require(['jquery'], function(jQuery){ - jQuery('#add-gift-options-<?= (int) $block->getEntity()->getId() ?>') - .add('#add-gift-options-for-items-<?= (int) $block->getEntity()->getId() ?>') + jQuery('#add-gift-options-{$entityId}') + .add('#add-gift-options-for-items-{$entityId}') .removeClass('hidden'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> - <dt class="extra-options-container" id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"></dt> + <dt class="extra-options-container" + id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"> + </dt> </dl> </fieldset> <script type="text/x-magento-init"> @@ -141,52 +228,95 @@ } } </script> -<?php break; -case 'multishipping_address': ?> + <?php + break; + case 'multishipping_address': + ?> <fieldset id="add-gift-options-<?= (int) $block->getEntity()->getId() ?>" class="fieldset gift-message"> - <legend class="legend"><span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span></legend><br> + <legend class="legend"> + <span><?= $block->escapeHtml(__('Do you have any gift items in your order?')) ?></span> + </legend><br> <div class="field choice" id="add-gift-options-<?= (int) $block->getEntity()->getId() ?>"> - <input type="checkbox" name="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" id="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-container-<?= (int) $block->getEntity()->getId() ?>"}'<?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" class="label"><span><?= $block->escapeHtml(__('Add Gift Options')) ?></span></label> + <input type="checkbox" name="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" + id="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' + value="1" + data-selector='{"id":"#allow-gift-options-container-<?= (int) $block->getEntity()->getId() ?>"}' + <?php if ($block->getItemsHasMesssages() || $block->getEntityHasMessage()):?> checked="checked" + <?php endif; ?> class="checkbox" /> + <label for="allow_gift_options_<?= (int) $block->getEntity()->getId() ?>" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options')) ?></span> + </label> </div> <dl class="options-items" id="allow-gift-options-container-<?= (int) $block->getEntity()->getId() ?>"> <?php if ($block->isMessagesOrderAvailable() || $block->isMessagesAvailable()): ?> <dt id="add-gift-options-for-order-<?= (int) $block->getEntity()->getId() ?>" class="order-title"> <div class="field choice"> - <input type="checkbox" name="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" id="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-order-container-<?= (int) $block->getEntity()->getId() ?>"}'<?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" class="label"><span><?= $block->escapeHtml(__('Add Gift Options for the Entire Order')) ?></span></label> + <input type="checkbox" name="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" + id="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-order-container-<?= (int) $block->getEntity() + ->getId() ?>"}' + <?php if ($block->getEntityHasMessage()): ?> checked="checked"<?php endif; ?> class="checkbox"/> + <label for="allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options for the Entire Order')) ?></span> + </label> </div> </dt> - <dd id="allow-gift-options-for-order-container-<?= (int) $block->getEntity()->getId() ?>" class="order-options"> - <div class="options-order-container" id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> - <?php if ($block->isMessagesAvailable()): ?> - <?php $_giftMessage = true; ?> + <dd id="allow-gift-options-for-order-container-<?= (int) $block->getEntity()->getId() ?>" + class="order-options"> + <div class="options-order-container" + id="options-order-container-<?= (int) $block->getEntity()->getId() ?>"></div> + <?php if ($block->isMessagesAvailable()): ?> + <?php $_giftMessage = true; ?> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#gift-messages-for-order-container-<?= (int) $block->getEntity()->getId() ?>"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#gift-messages-for-order-container-<?= (int) $block->getEntity() + ->getId() ?>"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> - <div id="gift-messages-for-order-container-<?= (int) $block->getEntity()->getId() ?>" class="gift-messages-order hidden"> + <div id="gift-messages-for-order-container-<?= (int) $block->getEntity()->getId() ?>" + class="gift-messages-order hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('You can leave this box blank if you don\'t want to add a gift message for this address.')) ?></p> + <p><?= $block->escapeHtml(__('You can leave this box blank if you don\'t want to add a ' . + 'gift message for this address.')) ?></p> <div class="field from"> - <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" + class="label"><span><?= $block->escapeHtml(__('From')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity()->getId() ?>][from]" id="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity() + ->getId() ?>][from]" + id="gift-message-<?= (int) $block->getEntity()->getId() ?>-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block->getMessage() + ->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" class="label"><span><?= $block->escapeHtml(__('To')) ?></span></label> + <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" + class="label"><span><?= $block->escapeHtml(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity()->getId() ?>][to]" id="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage()->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" name="giftmessage[quote_address][<?= (int) $block->getEntity() + ->getId() ?>][to]" + id="gift-message-<?= (int) $block->getEntity()->getId() ?>-to" + title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block->getMessage() + ->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" + class="label"><span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" class="input-text" name="giftmessage[quote_address][<?= (int) $block->getEntity()->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> + <textarea id="gift-message-<?= (int) $block->getEntity()->getId() ?>-message" + class="input-text" name="giftmessage[quote_address][<?= (int) $block + ->getEntity()->getId() ?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="40"><?= /* @noEscape */ $block->getEscaped($block->getMessage()->getMessage()) ?></textarea> </div> </div> </fieldset> @@ -195,54 +325,103 @@ case 'multishipping_address': ?> </dd> <?php endif; ?> <?php if ($block->isItemsAvailable()): ?> - <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId() ?>" class="order-title individual"> + <dt id="add-gift-options-for-items-<?= (int) $block->getEntity()->getId()?>" class="order-title individual"> <div class="field choice"> - <input type="checkbox" name="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" id="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" data-mage-init='{"giftOptions":{}}' value="1" data-selector='{"id":"#allow-gift-options-for-items-container-<?= (int) $block->getEntity()->getId() ?>"}'<?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> class="checkbox" /> - <label for="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" class="label"><span><?= $block->escapeHtml(__('Add Gift Options for Individual Items')) ?></span></label> + <input type="checkbox" name="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" + id="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" + data-mage-init='{"giftOptions":{}}' value="1" + data-selector='{"id":"#allow-gift-options-for-items-container-<?= (int) $block->getEntity() + ->getId() ?>"}' + <?php if ($block->getItemsHasMesssages()): ?> checked="checked"<?php endif; ?> + class="checkbox" /> + <label for="allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>" class="label"> + <span><?= $block->escapeHtml(__('Add Gift Options for Individual Items')) ?></span> + </label> </div> </dt> - <dd id="allow-gift-options-for-items-container-<?= (int) $block->getEntity()->getId() ?>" class="order-options individual"> + <dd id="allow-gift-options-for-items-container-<?= (int) $block->getEntity()->getId() ?>" + class="order-options individual"> <ol class="items"> <?php foreach ($block->getItems() as $_index => $_item): ?> <?php $_product = $_item->getProduct() ?> <li class="item"> <div class="product"> - <div class="number"><?= $block->escapeHtml(__('<span>Item %1</span> of %2', $_index+1, $block->countItems()), ['span']) ?></div> + <div class="number"> + <?= $block->escapeHtml( + __('<span>Item %1</span> of %2', $_index+1, $block->countItems()), + ['span'] + ) ?></div> <div class="img photo container"> <?= $block->getImage($_product, 'gift_messages_checkout_thumbnail')->toHtml() ?> </div> <strong class="product-name"><?= $block->escapeHtml($_product->getName()) ?></strong> </div> <div class="options"> - <div class="options-items-container" id="options-items-container-<?= (int) $block->getEntity()->getId() ?>-<?= (int) $_item->getId() ?>"></div> - <input type="hidden" name="giftoptions[quote_address_item][<?= (int) $_item->getId() ?>][address]" value="<?= (int) $block->getEntity()->getId() ?>" /> + <div class="options-items-container" + id="options-items-container-<?= (int) $block->getEntity()->getId()?>-<?= (int)$_item + ->getId() ?>"> + </div> + <input type="hidden" + name="giftoptions[quote_address_item][<?= (int) $_item->getId() ?>][address]" + value="<?= (int) $block->getEntity()->getId() ?>" /> <?php if ($block->isItemMessagesAvailable($_item)): ?> <?php $_giftMessage = true; ?> <button class="action action-gift" - data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() ?>"}}'> + data-mage-init='{"toggleAdvanced": {"selectorsToggleClass":"hidden", + "toggleContainers":"#gift-messages-for-item-container-<?= (int) $_item->getId() + ?>"}}'> <span><?= $block->escapeHtml(__('Gift Message')) ?></span> </button> - <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" class="block message hidden"> + <div id="gift-messages-for-item-container-<?= (int) $_item->getId() ?>" + class="block message hidden"> <fieldset class="fieldset"> - <p><?= $block->escapeHtml(__('You can leave this box blank if you don\'t want to add a gift message for the item.')) ?></p> - <input type="hidden" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][address]" value="<?= (int) $block->getEntity()->getId() ?>" /> + <p><?= $block->escapeHtml(__( + 'You can leave this box blank if you don\'t want to add a gift message ' . + 'for the item.' + )) ?></p> + <input type="hidden" name="giftmessage[quote_address_item][<?= (int) $_item + ->getId() ?>][address]" value="<?= (int) $block->getEntity()->getId() ?>" /> <div class="field from"> - <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"><span><?= $block->escapeHtml(__('From')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-from" class="label"> + <span><?= $block->escapeHtml(__('From')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][from]" id="gift-message-<?= (int) $_item->getId() ?>-from" title="<?= $block->escapeHtmlAttr(__('From')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getSender(), $block->getDefaultFrom()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_address_item][<?= (int) $_item->getId() + ?>][from]" id="gift-message-<?= (int) $_item->getId() ?>-from" + title="<?= $block->escapeHtmlAttr(__('From')) ?>" + value="<?= /* @noEscape */ $block->getEscaped($block + ->getMessage($_item)->getSender(), $block->getDefaultFrom()) + ?>" class="input-text"> </div> </div> <div class="field to"> - <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"><span><?= $block->escapeHtml(__('To')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId() ?>-to" class="label"> + <span><?= $block->escapeHtml(__('To')) ?></span> + </label> <div class="control"> - <input type="text" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][to]" id="gift-message-<?= (int) $_item->getId() ?>-to" title="<?= $block->escapeHtmlAttr(__('To')) ?>" value="<?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getRecipient(), $block->getDefaultTo()) ?>" class="input-text"> + <input type="text" + name="giftmessage[quote_address_item][<?= (int) $_item->getId() + ?>][to]" id="gift-message-<?= (int) $_item->getId() ?>-to" + title="<?= $block->escapeHtmlAttr(__('To')) ?>" + value= + "<?= /* @noEscape */ $block->getEscaped($block + ->getMessage($_item)->getRecipient(), $block->getDefaultTo()) + ?>" class="input-text"> </div> </div> <div class="field text"> - <label for="gift-message-<?= (int) $_item->getId() ?>-message" class="label"><span><?= $block->escapeHtml(__('Message')) ?></span></label> + <label for="gift-message-<?= (int) $_item->getId()?>-message" class="label"> + <span><?= $block->escapeHtml(__('Message')) ?></span> + </label> <div class="control"> - <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" class="input-text giftmessage-area" name="giftmessage[quote_address_item][<?= (int) $_item->getId() ?>][message]" title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> + <textarea id="gift-message-<?= (int) $_item->getId() ?>-message" + class="input-text giftmessage-area" + name="giftmessage[quote_address_item][<?= (int) $_item + ->getId() ?>][message]" + title="<?= $block->escapeHtmlAttr(__('Message')) ?>" rows="5" + cols="10"><?= /* @noEscape */ $block->getEscaped($block->getMessage($_item)->getMessage()) ?></textarea> </div> </div> </fieldset> @@ -254,19 +433,22 @@ case 'multishipping_address': ?> </ol> </dd> <?php endif; ?> - <dt class="extra-options-container" id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"></dt> + <dt class="extra-options-container" id="extra-options-container-<?= (int) $block->getEntity()->getId() ?>"> + </dt> </dl> </fieldset> + <?php $entityId = (int) $block->getEntity()->getId(); ?> <script type="text/x-magento-init"> { - "#allow_gift_options_<?= (int) $block->getEntity()->getId() ?>, #allow_gift_options_for_order_<?= (int) $block->getEntity()->getId() ?>, #allow_gift_options_for_items_<?= (int) $block->getEntity()->getId() ?>": { + "#allow_gift_options_<?= /* @noEscape */ $entityId ?>, #allow_gift_options_for_order_<?= /* @noEscape */ $entityId ?>, #allow_gift_options_for_items_<?= /* @noEscape */ $entityId ?>": { "giftOptions": {} } } </script> - <?php break; ?> -<?php endswitch ?> -<?php if ($_giftMessage): ?> + <?php + break; + endswitch; +if ($_giftMessage): ?> <script type="text/x-magento-init"> { "#shipping_method_form": { diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php index c317221fb6ef7..2ce51c8bbf19d 100644 --- a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/GiftMessage.php @@ -66,7 +66,7 @@ public function resolve( array $args = null ) { if (!isset($value['model'])) { - throw new GraphQlInputException(__('"model" value should be specified')); + throw new GraphQlInputException(__('"model" value must be specified')); } $cart = $value['model']; diff --git a/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php new file mode 100644 index 0000000000000..a9a8e682612cc --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/Model/Resolver/Cart/Item/GiftMessage.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GiftMessageGraphQl\Model\Resolver\Cart\Item; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + +/** + * Class provides ability to get GiftMessage for cart item + */ +class GiftMessage implements ResolverInterface +{ + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + */ + public function __construct( + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper + ) { + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + } + + /** + * Return information about Gift message for item cart + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('"model" value must be specified')); + } + + $quoteItem = $value['model']; + + if (!$this->giftMessageHelper->isMessagesAllowed('items', $quoteItem)) { + return null; + } + + if (!$this->giftMessageHelper->isMessagesAllowed('item', $quoteItem)) { + return null; + } + + try { + $giftItemMessage = $this->itemRepository->get($quoteItem->getQuoteId(), $quoteItem->getItemId()); + } catch (LocalizedException $e) { + throw new GraphQlInputException(__('Can\'t load cart item')); + } + + if (!isset($giftItemMessage)) { + return null; + } + + return [ + 'to' => $giftItemMessage->getRecipient() ?? '', + 'from' => $giftItemMessage->getSender() ?? '', + 'message'=> $giftItemMessage->getMessage() ?? '' + ]; + } +} diff --git a/app/code/Magento/GiftMessageGraphQl/README.md b/app/code/Magento/GiftMessageGraphQl/README.md index fa2e02116b66c..d73a058e0db9c 100644 --- a/app/code/Magento/GiftMessageGraphQl/README.md +++ b/app/code/Magento/GiftMessageGraphQl/README.md @@ -1,3 +1,3 @@ # GiftMessageGraphQl -**GiftMessageGraphQl** provides information about gift messages for cart, cart items, order and order items. +**GiftMessageGraphQl** provides information about gift messages for carts, cart items, orders and order items. diff --git a/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..bce5b7063e6b9 --- /dev/null +++ b/app/code/Magento/GiftMessageGraphQl/etc/graphql/di.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="allow_order" xsi:type="string">sales/gift_options/allow_order</item> + <item name="allow_items" xsi:type="string">sales/gift_options/allow_items</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls index f14c812a9a5f3..ad18054abca13 100644 --- a/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls +++ b/app/code/Magento/GiftMessageGraphQl/etc/schema.graphqls @@ -1,20 +1,47 @@ # Copyright © Magento, Inc. All rights reserved. # See COPYING.txt for license details. +type StoreConfig { + allow_order : String @doc(description: "The value of the Allow Gift Messages on Order Level option") + allow_items : String @doc(description: "The value of the Allow Gift Messages for Order Items option") +} + type Cart { gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\GiftMessage") @doc(description: "The entered gift message for the cart") } -type SalesItemInterface { - gift_message: GiftMessage @doc(description: "The entered gift message for the order item") +type SimpleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") } -type CustomerOrder { - gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order") +type ConfigurableCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type BundleCartItem { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Cart\\Item\\GiftMessage") @doc(description: "The entered gift message for the cart item") +} + +type GiftMessage @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") + from: String! @doc(description: "Sender name") + message: String! @doc(description: "Gift message text") +} + +input CartItemUpdateInput { + gift_message: GiftMessageInput @doc(description: "Gift message details for the cart item") } -type GiftMessage { - to: String! @doc(description: "Recepient name") +input GiftMessageInput @doc(description: "Contains the text of a gift message, its sender, and recipient") { + to: String! @doc(description: "Recipient name") from: String! @doc(description: "Sender name") message: String! @doc(description: "Gift message text") } + +type SalesItemInterface { + gift_message: GiftMessage @doc(description: "The entered gift message for the order item") +} + +type CustomerOrder { + gift_message: GiftMessage @resolver (class: "\\Magento\\GiftMessageGraphQl\\Model\\Resolver\\Order\\GiftMessage") @doc(description: "The entered gift message for the order") +} diff --git a/app/code/Magento/GoogleAdwords/Helper/Data.php b/app/code/Magento/GoogleAdwords/Helper/Data.php index 0e95859193d42..e3b85822059d8 100644 --- a/app/code/Magento/GoogleAdwords/Helper/Data.php +++ b/app/code/Magento/GoogleAdwords/Helper/Data.php @@ -280,6 +280,7 @@ public function getConversionValue() * Get send order currency to Google Adwords * * @return boolean + * @since 100.3.0 */ public function hasSendConversionValueCurrency() { @@ -293,6 +294,7 @@ public function hasSendConversionValueCurrency() * Get Google AdWords conversion value currency * * @return string|false + * @since 100.3.0 */ public function getConversionValueCurrency() { diff --git a/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..d700b0e9e7668 --- /dev/null +++ b/app/code/Magento/GoogleAdwords/etc/csp_whitelist.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="google_ad_services" type="host">www.googleadservices.com</value> + <value id="google_analytics" type="host">www.google-analytics.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="google_ad_services" type="host">www.googleadservices.com</value> + <value id="google_analytics" type="host">www.google-analytics.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml b/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml index e3c46bc27834c..0de78ca8b62c2 100644 --- a/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml +++ b/app/code/Magento/GoogleAdwords/view/frontend/templates/code.phtml @@ -5,27 +5,39 @@ */ ?> <?php -/** @var $block \Magento\GoogleAdwords\Block\Code */ +/** + * @var $block \Magento\GoogleAdwords\Block\Code + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <!-- Google Code for Sale Conversion Page --> -<script> +<?php +/** @var \Magento\GoogleAdwords\Helper\Data $helper */ +$helper = $block->getHelper(); +$scriptString = <<<script /* <![CDATA[ */ - var google_conversion_id = <?= $block->escapeJs($block->getHelper()->getConversionId()) ?>; - var google_conversion_language = "<?= $block->escapeJs($block->getHelper()->getConversionLanguage()) ?>"; - var google_conversion_format = "<?= $block->escapeJs($block->getHelper()->getConversionFormat()) ?>"; - var google_conversion_color = "<?= $block->escapeJs($block->getHelper()->getConversionColor()) ?>"; - var google_conversion_label = "<?= $block->escapeJs($block->getHelper()->getConversionLabel()) ?>"; - var google_conversion_value = <?= $block->escapeJs($block->getHelper()->getConversionValue()) ?>; - <?php if ($block->getHelper()->hasSendConversionValueCurrency() && $block->getHelper()->getConversionValueCurrency()) : ?> - var google_conversion_currency = "<?= $block->escapeJs($block->getHelper()->getConversionValueCurrency()) ?>"; - <?php endif; ?> + var google_conversion_id = {$block->escapeJs($helper->getConversionId())}; + var google_conversion_language = "{$block->escapeJs($helper->getConversionLanguage())}"; + var google_conversion_format = "{$block->escapeJs($helper->getConversionFormat())}"; + var google_conversion_color = "{$block->escapeJs($helper->getConversionColor())}"; + var google_conversion_label = "{$block->escapeJs($helper->getConversionLabel())}"; + var google_conversion_value = {$block->escapeJs($helper->getConversionValue())}; +script; +if ($helper->hasSendConversionValueCurrency() && $helper->getConversionValueCurrency()): + $scriptString .= <<<script + var google_conversion_currency = "{$block->escapeJs($helper->getConversionValueCurrency())}"; +script; +endif; +$scriptString .= <<<script /* ]]> */ -</script> -<script src="<?= $block->escapeHtmlAttr($block->getHelper()->getConversionJsSrc()) ?>"></script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<script src="<?= $block->escapeHtmlAttr($helper->getConversionJsSrc()) ?>"></script> <noscript> <div style="display:inline;"> <img height="1" width="1" style="border-style:none;" alt="" - src="<?= $block->escapeHtmlAttr($block->getHelper()->getConversionImgSrc()) ?>"/> + src="<?= $block->escapeHtmlAttr($helper->getConversionImgSrc()) ?>"/> </div> </noscript> <!-- END Google Code for Sale Conversion Page --> diff --git a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php index 135c8c92c6aa9..975788abe52e4 100644 --- a/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php +++ b/app/code/Magento/GoogleOptimizer/Observer/AbstractSave.php @@ -5,12 +5,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\GoogleOptimizer\Observer; use Magento\Framework\Event\Observer; use Magento\Framework\Event\ObserverInterface; /** + * Abstract entity for saving codes + * * @api * @since 100.0.2 */ @@ -96,7 +100,9 @@ protected function _processCode() $this->_initRequestParams(); if ($this->_isNewCode()) { - $this->_saveCode(); + if (!$this->_isEmptyCode()) { + $this->_saveCode(); + } } else { $this->_loadCode(); if ($this->_isEmptyCode()) { @@ -185,6 +191,8 @@ protected function _deleteCode() } /** + * Check data availability + * * @return bool */ private function isDataAvailable() @@ -194,6 +202,8 @@ private function isDataAvailable() } /** + * Get request data + * * @return mixed */ private function getRequestData() diff --git a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php index 8a5c247369657..c6d02957c4be9 100644 --- a/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php +++ b/app/code/Magento/GoogleOptimizer/Test/Unit/Observer/Product/SaveGoogleExperimentScriptObserverTest.php @@ -127,6 +127,39 @@ public function testCreatingCodeIfRequestIsValid() $this->_modelObserver->execute($this->_eventObserverMock); } + /** + * Test that code is not saving when request is empty + * + * @return void + */ + public function testCreatingCodeIfRequestIsEmpty(): void + { + $this->_helperMock->expects( + $this->once() + )->method( + 'isGoogleExperimentActive' + )->with( + $this->_storeId + )->willReturn( + true + ); + + $this->_requestMock->expects( + $this->exactly(3) + )->method( + 'getParam' + )->with( + 'google_experiment' + )->willReturn( + ['code_id' => '', 'experiment_script' => ''] + ); + + $this->_codeMock->expects($this->never())->method('addData'); + $this->_codeMock->expects($this->never())->method('save'); + + $this->_modelObserver->execute($this->_eventObserverMock); + } + /** * @param array $params * @dataProvider dataProviderWrongRequestForCreating diff --git a/app/code/Magento/GraphQl/Controller/GraphQl.php b/app/code/Magento/GraphQl/Controller/GraphQl.php index 2d72fde91b031..34dbeaa2ed0a2 100644 --- a/app/code/Magento/GraphQl/Controller/GraphQl.php +++ b/app/code/Magento/GraphQl/Controller/GraphQl.php @@ -28,12 +28,13 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.3.0 */ class GraphQl implements FrontControllerInterface { /** * @var \Magento\Framework\Webapi\Response - * @deprecated + * @deprecated 100.3.2 */ private $response; @@ -59,7 +60,7 @@ class GraphQl implements FrontControllerInterface /** * @var ContextInterface - * @deprecated $contextFactory is used for creating Context object + * @deprecated 100.3.3 $contextFactory is used for creating Context object */ private $resolverContext; @@ -133,6 +134,7 @@ public function __construct( * * @param RequestInterface $request * @return ResponseInterface + * @since 100.3.0 */ public function dispatch(RequestInterface $request) : ResponseInterface { diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php new file mode 100644 index 0000000000000..ba2e995d4f704 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowCredentialsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Credentials header if CORS is enabled + */ +class CorsAllowCredentialsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Get value for header + * + * @return string + */ + public function getValue(): string + { + return "1"; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->corsConfiguration->isCredentialsAllowed(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php new file mode 100644 index 0000000000000..68760de543daa --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowHeadersHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Headers header if CORS is enabled + */ +class CorsAllowHeadersHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedHeaders(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php new file mode 100644 index 0000000000000..233839b9deb74 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowMethodsHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Methods header if CORS is enabled + */ +class CorsAllowMethodsHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedMethods(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php new file mode 100644 index 0000000000000..21850f18db1f2 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsAllowOriginHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Allow-Origin header if CORS is enabled + */ +class CorsAllowOriginHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return $this->corsConfiguration->getAllowedOrigins(); + } +} diff --git a/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php new file mode 100644 index 0000000000000..e30209ae25e68 --- /dev/null +++ b/app/code/Magento/GraphQl/Controller/HttpResponse/Cors/CorsMaxAgeHeaderProvider.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Controller\HttpResponse\Cors; + +use Magento\Framework\App\Response\HeaderProvider\HeaderProviderInterface; +use Magento\GraphQl\Model\Cors\ConfigurationInterface; + +/** + * Provides value for Access-Control-Max-Age header if CORS is enabled + */ +class CorsMaxAgeHeaderProvider implements HeaderProviderInterface +{ + /** + * @var string + */ + private $headerName; + + /** + * CORS configuration provider + * + * @var \Magento\GraphQl\Model\Cors\ConfigurationInterface + */ + private $corsConfiguration; + + /** + * @param ConfigurationInterface $corsConfiguration + * @param string $headerName + */ + public function __construct( + ConfigurationInterface $corsConfiguration, + string $headerName + ) { + $this->corsConfiguration = $corsConfiguration; + $this->headerName = $headerName; + } + + /** + * Get name of header + * + * @return string + */ + public function getName(): string + { + return $this->headerName; + } + + /** + * Check if header can be applied + * + * @return bool + */ + public function canApply(): bool + { + return $this->corsConfiguration->isEnabled() && $this->getValue(); + } + + /** + * Get value for header + * + * @return string|null + */ + public function getValue(): ?string + { + return (string) $this->corsConfiguration->getMaxAge(); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/Configuration.php b/app/code/Magento/GraphQl/Model/Cors/Configuration.php new file mode 100644 index 0000000000000..dd5a0b426e22d --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/Configuration.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +use Magento\Framework\App\Config\ScopeConfigInterface; + +/** + * Configuration provider for GraphQL CORS settings + */ +class Configuration implements ConfigurationInterface +{ + public const XML_PATH_CORS_HEADERS_ENABLED = 'graphql/cors/enabled'; + public const XML_PATH_CORS_ALLOWED_ORIGINS = 'graphql/cors/allowed_origins'; + public const XML_PATH_CORS_ALLOWED_HEADERS = 'graphql/cors/allowed_headers'; + public const XML_PATH_CORS_ALLOWED_METHODS = 'graphql/cors/allowed_methods'; + public const XML_PATH_CORS_MAX_AGE = 'graphql/cors/max_age'; + public const XML_PATH_CORS_ALLOW_CREDENTIALS = 'graphql/cors/allow_credentials'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_HEADERS_ENABLED); + } + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_ORIGINS); + } + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_HEADERS); + } + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string + { + return $this->scopeConfig->getValue(self::XML_PATH_CORS_ALLOWED_METHODS); + } + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int + { + return (int) $this->scopeConfig->getValue(self::XML_PATH_CORS_MAX_AGE); + } + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_CORS_ALLOW_CREDENTIALS); + } +} diff --git a/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php new file mode 100644 index 0000000000000..b40b64f48e51f --- /dev/null +++ b/app/code/Magento/GraphQl/Model/Cors/ConfigurationInterface.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Model\Cors; + +/** + * Interface for configuration provider for GraphQL CORS settings + */ +interface ConfigurationInterface +{ + /** + * Are CORS headers enabled + * + * @return bool + */ + public function isEnabled(): bool; + + /** + * Get allowed origins or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedOrigins(): ?string; + + /** + * Get allowed headers or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedHeaders(): ?string; + + /** + * Get allowed methods or null if stored configuration is empty + * + * @return string|null + */ + public function getAllowedMethods(): ?string; + + /** + * Get max age header value + * + * @return int + */ + public function getMaxAge(): int; + + /** + * Are credentials allowed + * + * @return bool + */ + public function isCredentialsAllowed() : bool; +} diff --git a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php index 2b8e3fabd6863..9403ccaf07099 100644 --- a/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php +++ b/app/code/Magento/GraphQl/Model/Query/Resolver/Context.php @@ -12,7 +12,7 @@ /** * Do not use this class. It was kept for backward compatibility. * - * @deprecated \Magento\GraphQl\Model\Query\Context is used instead of this + * @deprecated 100.3.3 \Magento\GraphQl\Model\Query\Context is used instead of this */ class Context extends \Magento\Framework\Model\AbstractExtensibleModel implements ContextInterface { diff --git a/app/code/Magento/GraphQl/composer.json b/app/code/Magento/GraphQl/composer.json index 904d41c97953e..401e77a787acf 100644 --- a/app/code/Magento/GraphQl/composer.json +++ b/app/code/Magento/GraphQl/composer.json @@ -5,10 +5,10 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/module-eav": "*", - "magento/framework": "*" + "magento/framework": "*", + "magento/module-webapi": "*" }, "suggest": { - "magento/module-webapi": "*", "magento/module-graph-ql-cache": "*" }, "license": [ diff --git a/app/code/Magento/GraphQl/etc/adminhtml/system.xml b/app/code/Magento/GraphQl/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..ddee7596eca3e --- /dev/null +++ b/app/code/Magento/GraphQl/etc/adminhtml/system.xml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="graphql" translate="label" type="text" sortOrder="300" showInDefault="1" showInWebsite="1" showInStore="1"> + <label>GraphQL</label> + <tab>service</tab> + <resource>Magento_Integration::config_oauth</resource> + <group id="cors" translate="label" type="text" sortOrder="60" showInDefault="1" showInWebsite="1"> + <label>CORS Settings</label> + <field id="enabled" translate="label" type="select" sortOrder="1" showInDefault="1" canRestore="1"> + <label>CORS Headers Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + </field> + + <field id="allowed_origins" translate="label" type="text" sortOrder="10" showInDefault="1" canRestore="1"> + <label>Allowed origins</label> + <comment>The Access-Control-Allow-Origin response header indicates whether the response can be shared with requesting code from the given origin. Fill this field with one or more origins (comma separated) or use '*' to allow access from all origins.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_methods" translate="label" type="text" sortOrder="20" showInDefault="1" canRestore="1"> + <label>Allowed methods</label> + <comment>The Access-Control-Allow-Methods response header specifies the method or methods allowed when accessing the resource in response to a preflight request. Use comma separated methods (e.g. GET,POST)</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allowed_headers" translate="label" type="text" sortOrder="30" showInDefault="1" canRestore="1"> + <label>Allowed headers</label> + <comment>The Access-Control-Allow-Headers response header is used in response to a preflight request which includes the Access-Control-Request-Headers to indicate which HTTP headers can be used during the actual request. Use comma separated headers.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="max_age" translate="label" type="text" sortOrder="40" showInDefault="1" canRestore="1"> + <label>Max Age</label> + <validate>validate-digits</validate> + <comment>The Access-Control-Max-Age response header indicates how long the results of a preflight request (that is the information contained in the Access-Control-Allow-Methods and Access-Control-Allow-Headers headers) can be cached.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + + <field id="allow_credentials" translate="label" type="select" sortOrder="50" showInDefault="1" canRestore="1"> + <label>Credentials Allowed</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <comment>The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to frontend code when the request's credentials mode is include.</comment> + <depends> + <field id="graphql/cors/enabled">1</field> + </depends> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/GraphQl/etc/config.xml b/app/code/Magento/GraphQl/etc/config.xml new file mode 100644 index 0000000000000..39caacbec42d2 --- /dev/null +++ b/app/code/Magento/GraphQl/etc/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <graphql> + <cors> + <enabled>0</enabled> + <allowed_origins></allowed_origins> + <allowed_methods></allowed_methods> + <allowed_headers></allowed_headers> + <max_age>86400</max_age> + <allow_credentials>0</allow_credentials> + </cors> + </graphql> + </default> +</config> diff --git a/app/code/Magento/GraphQl/etc/di.xml b/app/code/Magento/GraphQl/etc/di.xml index b356f33c4f4bf..fca6c425e2507 100644 --- a/app/code/Magento/GraphQl/etc/di.xml +++ b/app/code/Magento/GraphQl/etc/di.xml @@ -98,4 +98,31 @@ <argument name="queryComplexity" xsi:type="number">300</argument> </arguments> </type> + + <preference for="Magento\GraphQl\Model\Cors\ConfigurationInterface" type="Magento\GraphQl\Model\Cors\Configuration" /> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Max-Age</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Credentials</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Headers</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Methods</argument> + </arguments> + </type> + <type name="Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider"> + <arguments> + <argument name="headerName" xsi:type="string">Access-Control-Allow-Origin</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/graphql/di.xml b/app/code/Magento/GraphQl/etc/graphql/di.xml index 77fce336374dd..23d49124d1a02 100644 --- a/app/code/Magento/GraphQl/etc/graphql/di.xml +++ b/app/code/Magento/GraphQl/etc/graphql/di.xml @@ -30,4 +30,15 @@ </argument> </arguments> </type> + <type name="Magento\Framework\App\Response\HeaderManager"> + <arguments> + <argument name="headerProviderList" xsi:type="array"> + <item name="CorsAllowOrigins" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowOriginHeaderProvider</item> + <item name="CorsAllowHeaders" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowHeadersHeaderProvider</item> + <item name="CorsAllowMethods" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowMethodsHeaderProvider</item> + <item name="CorsAllowCredentials" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsAllowCredentialsHeaderProvider</item> + <item name="CorsMaxAge" xsi:type="object">Magento\GraphQl\Controller\HttpResponse\Cors\CorsMaxAgeHeaderProvider</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/GraphQl/etc/schema.graphqls b/app/code/Magento/GraphQl/etc/schema.graphqls index fccde015c3388..2595ad09c072a 100644 --- a/app/code/Magento/GraphQl/etc/schema.graphqls +++ b/app/code/Magento/GraphQl/etc/schema.graphqls @@ -79,6 +79,12 @@ input FilterMatchTypeInput @doc(description: "Defines a filter that performs a f match: String @doc(description: "One or more words to filter on") } +input FilterStringTypeInput @doc(description: "Defines a filter for an input string.") { + in: [String] @doc(description: "Filters items that are exactly the same as entries specified in an array of strings.") + eq: String @doc(description: "Filters items that are exactly the same as the specified string.") + match: String @doc(description: "Defines a filter that performs a fuzzy search using the specified string.") +} + type SearchResultPageInfo @doc(description: "SearchResultPageInfo provides navigation for the query response") { page_size: Int @doc(description: "Specifies the maximum number of items to return") current_page: Int @doc(description: "Specifies which page of results to return") @@ -271,3 +277,8 @@ enum CurrencyEnum @doc(description: "The list of available currency codes") { TRL XPF } + +input EnteredOptionInput @doc(description: "Defines a customer-entered option") { + uid: ID! @doc(description: "An encoded ID") + value: String! @doc(description: "Text the customer entered") +} diff --git a/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php b/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php index 6df890b3e94dc..2b4ee3a7d27da 100644 --- a/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php +++ b/app/code/Magento/GroupedProduct/Block/Order/Email/Items/CreditMemo/Grouped.php @@ -13,6 +13,7 @@ * Class renders grouped product(s) in the CreditMemo email * * @api + * @since 100.4.0 */ class Grouped extends DefaultItems { @@ -22,6 +23,7 @@ class Grouped extends DefaultItems * This method uses renderer for real product type * * @return string + * @since 100.4.0 */ protected function _toHtml() { diff --git a/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php b/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php index d84df510195f3..48e1a5053ac69 100644 --- a/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php +++ b/app/code/Magento/GroupedProduct/Model/Wishlist/Product/Item.php @@ -7,6 +7,7 @@ namespace Magento\GroupedProduct\Model\Wishlist\Product; +use Magento\Catalog\Model\Product\Configuration\Item\Option\OptionInterface; use Magento\Wishlist\Model\Item as WishlistItem; use Magento\GroupedProduct\Model\Product\Type\Grouped as TypeGrouped; use Magento\Catalog\Model\Product; @@ -36,7 +37,7 @@ public function beforeRepresentProduct( $diff = array_diff_key($itemOptions, $productOptions); - if (!$diff) { + if (!$diff && $this->isAddAction($productOptions['info_buyRequest'])) { $buyRequest = $subject->getBuyRequest(); $superGroupInfo = $buyRequest->getData('super_group'); @@ -78,10 +79,13 @@ public function beforeCompareOptions( array $options2 ): array { $diff = array_diff_key($options1, $options2); + $productOptions = isset($options1['info_buyRequest']['product']) ? $options1 : $options2; if (!$diff) { foreach (array_keys($options1) as $key) { - if (preg_match('/associated_product_\d+/', $key)) { + if (preg_match('/associated_product_\d+/', $key) + && $this->isAddAction($productOptions['info_buyRequest']) + ) { unset($options1[$key]); unset($options2[$key]); } @@ -90,4 +94,18 @@ public function beforeCompareOptions( return [$options1, $options2]; } + + /** + * Check that current request belongs to add to wishlist action. + * + * @param OptionInterface $buyRequest + * + * @return bool + */ + private function isAddAction(OptionInterface $buyRequest): bool + { + $requestValue = json_decode($buyRequest->getValue(), true); + + return $requestValue['action'] === 'add'; + } } diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml index 04b704b9193ca..23b1499c2d4e8 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAddDefaultImageGroupedProductTest.xml @@ -32,15 +32,14 @@ <actionGroup ref="DeleteProductBySkuActionGroup" stepKey="deleteGroupedProduct"> <argument name="sku" value="{{GroupedProduct.sku}}"/> </actionGroup> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml index bd6785eb5e41b..a909582c32b3d 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminAssociateGroupedProductToWebsitesTest.xml @@ -53,7 +53,9 @@ </actionGroup> <!-- Reindex --> - <magentoCLI command="indexer:reindex" stepKey="reindexAllIndexes"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAllIndexes"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml index b88f909d977ab..a00a341c4e6af 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminDeleteGroupedProductTest.xml @@ -42,10 +42,9 @@ <amOnPage url="{{StorefrontProductPage.url($$createGroupedProduct.name$$)}}" stepKey="amOnGroupedProductPage"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="Whoops, our bad..." stepKey="seeWhoops"/> <!--Search for the product by sku--> - <fillField selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="$$createGroupedProduct.sku$$" stepKey="fillSearchBarByProductSku"/> - <waitForPageLoad stepKey="waitForSearchButton"/> - <click selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="clickSearchButton"/> - <waitForPageLoad stepKey="waitForSearchResults"/> + <actionGroup ref="StoreFrontQuickSearchActionGroup" stepKey="searchByCreatedTerm"> + <argument name="query" value="$$createGroupedProduct.sku$$"/> + </actionGroup> <!-- Should not see any search results --> <dontSee userInput="$$createGroupedProduct.sku$$" selector="{{StorefrontCatalogSearchMainSection.searchResults}}" stepKey="dontSeeProduct"/> <see selector="{{StorefrontCatalogSearchMainSection.message}}" userInput="Your search returned no results." stepKey="seeCantFindProductOneMessage"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml index 6514b5ddc5f78..ef1665d965200 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminGroupedProductsListTest.xml @@ -37,8 +37,7 @@ </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="adminProductIndexPageAdd"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="GroupedProduct"/> </actionGroup> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml index 053949fa20fb2..0dc622a82aaae 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminRemoveDefaultImageGroupedProductTest.xml @@ -29,15 +29,14 @@ </createData> </before> <after> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="createProductOne" stepKey="deleteProductOne"/> <deleteData createDataKey="createProductTwo" stepKey="deleteProductTwo"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> </after> <!-- Create product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="GroupedProduct"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml index 7f03765720069..fbe77270a6177 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdminSortingAssociatedProductsTest.xml @@ -127,8 +127,7 @@ </after> <!--Create grouped Product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGridColumnsInitial"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="GroupedProduct"/> @@ -156,8 +155,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Open created Product group--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfExist"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGridForm"> <argument name="keyword" value="GroupedProduct.name"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml index 599736e7e817e..4aa4e99e79489 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByDescriptionTest.xml @@ -30,8 +30,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml index 853304c557c8f..06dde74de20f9 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByNameTest.xml @@ -30,8 +30,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml index 4e67f2bd50439..66e1b60331e97 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByPriceTest.xml @@ -39,8 +39,12 @@ <getData entity="GetProduct" stepKey="arg3"> <requiredEntity createDataKey="simple2"/> </getData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml index 4b86a2c085003..79b465abe2ac6 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductByShortDescriptionTest.xml @@ -30,8 +30,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml index 6e67e41fa447b..c196abbce99ed 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/AdvanceCatalogSearchGroupedProductTest/AdvanceCatalogSearchGroupedProductBySkuTest.xml @@ -29,8 +29,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml index d563796b21da9..b783525e9fe18 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/NewProductsListWidgetGroupedProductTest.xml @@ -40,8 +40,7 @@ <!-- Create a grouped product to appear in the widget --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductList"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="toggleAddProductButton"/> <click selector="{{AdminProductGridActionSection.addGroupedProduct}}" stepKey="clickAddGroupedProduct"/> <fillField selector="{{AdminProductFormSection.productName}}" userInput="{{_defaultProduct.name}}" stepKey="fillProductName"/> @@ -58,7 +57,7 @@ <checkOption selector="{{AdminAddProductsToGroupPanel.nThCheckbox('0')}}" stepKey="checkFilterResult1"/> <checkOption selector="{{AdminAddProductsToGroupPanel.nThCheckbox('1')}}" stepKey="checkFilterResult2"/> <click selector="{{AdminAddProductsToGroupPanel.addSelectedProducts}}" stepKey="clickAddSelectedGroupProducts"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="clickSaveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="clickSaveProduct"/> <!-- If PageCache is enabled, Cache clearing happens here, via merge --> diff --git a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml index e966ccf82648f..c141c179330f5 100644 --- a/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml +++ b/app/code/Magento/GroupedProduct/Test/Mftf/Test/StorefrontAdvanceCatalogSearchGroupedProductBySkuWithHyphenTest.xml @@ -34,8 +34,12 @@ <requiredEntity createDataKey="product"/> <requiredEntity createDataKey="simple2"/> </updateData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simple1" stepKey="deleteSimple1" before="deleteSimple2"/> diff --git a/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php b/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php index 7dc25c3de1245..6006ed639c92e 100644 --- a/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php +++ b/app/code/Magento/GroupedProduct/Test/Unit/Model/Wishlist/Product/ItemTest.php @@ -115,13 +115,29 @@ public function testBeforeRepresentProduct() */ public function testBeforeCompareOptionsSameKeys() { - $options1 = ['associated_product_34' => 3]; - $options2 = ['associated_product_34' => 2]; + $infoBuyRequestMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option::class, + [ + 'getValue', + ] + ); + + $infoBuyRequestMock->expects($this->atLeastOnce()) + ->method('getValue') + ->willReturn('{"product":"3","action":"add"}'); + $options1 = [ + 'associated_product_34' => 3, + 'info_buyRequest' => $infoBuyRequestMock, + ]; + $options2 = [ + 'associated_product_34' => 3, + 'info_buyRequest' => $infoBuyRequestMock, + ]; $res = $this->model->beforeCompareOptions($this->subjectMock, $options1, $options2); - $this->assertEquals([], $res[0]); - $this->assertEquals([], $res[1]); + $this->assertEquals(['info_buyRequest' => $infoBuyRequestMock], $res[0]); + $this->assertEquals(['info_buyRequest' => $infoBuyRequestMock], $res[1]); } /** @@ -175,16 +191,26 @@ private function getProductAssocOption($initVal, $prodId) { $items = []; - $optionMock = $this->createPartialMock( + $associatedProductMock = $this->createPartialMock( + \Magento\Catalog\Model\Product\Configuration\Item\Option::class, + [ + 'getValue', + ] + ); + $infoBuyRequestMock = $this->createPartialMock( \Magento\Catalog\Model\Product\Configuration\Item\Option::class, [ 'getValue', ] ); - $optionMock->expects($this->once())->method('getValue')->willReturn($initVal); + $associatedProductMock->expects($this->once())->method('getValue')->willReturn($initVal); + $infoBuyRequestMock->expects($this->once()) + ->method('getValue') + ->willReturn('{"product":"'. $prodId . '","action":"add"}'); - $items['associated_product_' . $prodId] = $optionMock; + $items['associated_product_' . $prodId] = $associatedProductMock; + $items['info_buyRequest'] = $infoBuyRequestMock; return $items; } diff --git a/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml b/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml index 991ef2b5f4c7c..4e62b5539549f 100644 --- a/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml +++ b/app/code/Magento/GroupedProduct/view/adminhtml/templates/product/stock/disabler.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ $('[data-tab-panel=product-details]').on('stockbeforedisable', function(e) { if (e.productType === 'grouped') { @@ -13,4 +16,6 @@ require(['jquery'], function($){ } }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php index 92cfb375fea41..29fa2bffabb3b 100644 --- a/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php +++ b/app/code/Magento/GroupedProductGraphQl/Model/GroupedProductLinksTypeResolver.php @@ -10,7 +10,7 @@ use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; /** - * {@inheritdoc} + * @inheritdoc */ class GroupedProductLinksTypeResolver implements TypeResolverInterface { @@ -20,14 +20,14 @@ class GroupedProductLinksTypeResolver implements TypeResolverInterface private $linkTypes = ['associated']; /** - * {@inheritdoc} + * @inheritdoc */ - public function resolveType(array $data) : string + public function resolveType(array $data): string { if (isset($data['link_type'])) { $linkType = $data['link_type']; if (in_array($linkType, $this->linkTypes)) { - return 'GroupedProductLinks'; + return 'ProductLinks'; } } return ''; diff --git a/app/code/Magento/GroupedProductGraphQl/etc/di.xml b/app/code/Magento/GroupedProductGraphQl/etc/di.xml index 35b63370baf2f..717bc14826f70 100644 --- a/app/code/Magento/GroupedProductGraphQl/etc/di.xml +++ b/app/code/Magento/GroupedProductGraphQl/etc/di.xml @@ -13,4 +13,11 @@ </argument> </arguments> </type> + <type name="\Magento\CatalogGraphQl\Model\Resolver\Product\BatchProductLinks"> + <arguments> + <argument name="linkTypes" xsi:type="array"> + <item name="associated" xsi:type="string">associated</item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php b/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php index 01c41e35fc4eb..f670d97626725 100644 --- a/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php +++ b/app/code/Magento/ImportExport/Api/Data/ExportInfoInterface.php @@ -10,6 +10,7 @@ /** * Basic interface with data needed for export operation. * @api + * @since 100.3.2 */ interface ExportInfoInterface { @@ -17,6 +18,7 @@ interface ExportInfoInterface * Return filename. * * @return string + * @since 100.3.2 */ public function getFileName(); @@ -25,6 +27,7 @@ public function getFileName(); * * @param string $fileName * @return void + * @since 100.3.2 */ public function setFileName($fileName); @@ -32,6 +35,7 @@ public function setFileName($fileName); * Override standard entity getter. * * @return string + * @since 100.3.2 */ public function getFileFormat(); @@ -40,6 +44,7 @@ public function getFileFormat(); * * @param string $fileFormat * @return void + * @since 100.3.2 */ public function setFileFormat($fileFormat); @@ -47,6 +52,7 @@ public function setFileFormat($fileFormat); * Return content type. * * @return string + * @since 100.3.2 */ public function getContentType(); @@ -55,6 +61,7 @@ public function getContentType(); * * @param string $contentType * @return void + * @since 100.3.2 */ public function setContentType($contentType); @@ -62,6 +69,7 @@ public function setContentType($contentType); * Returns entity. * * @return string + * @since 100.3.2 */ public function getEntity(); @@ -70,6 +78,7 @@ public function getEntity(); * * @param string $entity * @return void + * @since 100.3.2 */ public function setEntity($entity); @@ -77,6 +86,7 @@ public function setEntity($entity); * Returns export filter. * * @return string + * @since 100.3.2 */ public function getExportFilter(); @@ -85,6 +95,7 @@ public function getExportFilter(); * * @param string $exportFilter * @return void + * @since 100.3.2 */ public function setExportFilter($exportFilter); } diff --git a/app/code/Magento/ImportExport/Api/ExportManagementInterface.php b/app/code/Magento/ImportExport/Api/ExportManagementInterface.php index 39bb89b43c838..0383b2a43b45e 100644 --- a/app/code/Magento/ImportExport/Api/ExportManagementInterface.php +++ b/app/code/Magento/ImportExport/Api/ExportManagementInterface.php @@ -12,6 +12,7 @@ /** * Describes how to do export operation with data interface. * @api + * @since 100.3.2 */ interface ExportManagementInterface { @@ -20,6 +21,7 @@ interface ExportManagementInterface * * @param ExportInfoInterface $exportInfo * @return string + * @since 100.3.2 */ public function export(ExportInfoInterface $exportInfo); } diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php index 81fa9767fbaa4..5a9e3a2a8504a 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Download.php @@ -20,9 +20,9 @@ class Download extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text */ public function _getValue(\Magento\Framework\DataObject $row) { - return '<p> ' . $row->getData('imported_file') . '</p><a href="' + return '<p> ' . $this->escapeHtml($row->getData('imported_file')) . '</p><a href="' . $this->getUrl('*/*/download', ['filename' => $row->getData('imported_file')]) . '">' - . __('Download') + . $this->escapeHtml(__('Download')) . '</a>'; } } diff --git a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php index 527bc5025d139..d493fc3fd9531 100644 --- a/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php +++ b/app/code/Magento/ImportExport/Block/Adminhtml/Grid/Column/Renderer/Error.php @@ -22,9 +22,9 @@ public function _getValue(\Magento\Framework\DataObject $row) { $result = ''; if ($row->getData('error_file') != '') { - $result = '<p> ' . $row->getData('error_file') . '</p><a href="' + $result = '<p> ' . $this->escapeHtml($row->getData('error_file')) . '</p><a href="' . $this->getUrl('*/*/download', ['filename' => $row->getData('error_file')]) . '">' - . __('Download') + . $this->escapeHtml(__('Download')) . '</a>'; } return $result; 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 1b7fdc2881073..e31f36e627a66 100644 --- a/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php +++ b/app/code/Magento/ImportExport/Controller/Adminhtml/Export/File/Delete.php @@ -11,9 +11,11 @@ use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\Exception\FileSystemException; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\ValidatorException; use Magento\ImportExport\Controller\Adminhtml\Export as ExportController; use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Controller\ResultInterface; +use Magento\Framework\Filesystem\Directory\WriteFactory; /** * Controller that delete file by name. @@ -31,54 +33,58 @@ class Delete extends ExportController implements HttpPostActionInterface private $filesystem; /** - * @var DriverInterface + * @var WriteFactory */ - private $file; + private $writeFactory; /** * Delete constructor. + * * @param Action\Context $context * @param Filesystem $filesystem - * @param DriverInterface $file + * @param WriteFactory $writeFactory */ public function __construct( Action\Context $context, Filesystem $filesystem, - DriverInterface $file + WriteFactory $writeFactory ) { $this->filesystem = $filesystem; - $this->file = $file; + $this->writeFactory = $writeFactory; parent::__construct($context); } /** * Controller basic method implementation. * - * @return \Magento\Framework\Controller\ResultInterface + * @return ResultInterface */ public function execute() { /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ $resultRedirect = $this->resultRedirectFactory->create(); $resultRedirect->setPath('adminhtml/export/index'); - $fileName = $this->getRequest()->getParam('filename'); - if (empty($fileName) || preg_match('/\.\.(\\\|\/)/', $fileName) !== 0) { - $this->messageManager->addErrorMessage(__('Please provide valid export file name')); - - return $resultRedirect; - } try { - $directory = $this->filesystem->getDirectoryRead(DirectoryList::VAR_DIR); - $path = $directory->getAbsolutePath() . 'export/' . $fileName; + if (empty($fileName = $this->getRequest()->getParam('filename'))) { + $this->messageManager->addErrorMessage(__('Please provide valid export file name')); - if ($directory->isFile($path)) { - $this->file->deleteFile($path); + return $resultRedirect; + } + $directoryWrite = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_EXPORT); + try { + $directoryWrite->delete($directoryWrite->getAbsolutePath($fileName)); $this->messageManager->addSuccessMessage(__('File %1 deleted', $fileName)); - } else { - $this->messageManager->addErrorMessage(__('%1 is not a valid file', $fileName)); + } catch (ValidatorException $exception) { + $this->messageManager->addErrorMessage( + __('Sorry, but the data is invalid or the file is not uploaded.') + ); + } catch (FileSystemException $exception) { + $this->messageManager->addErrorMessage( + __('Sorry, but the data is invalid or the file is not uploaded.') + ); } } catch (FileSystemException $exception) { - $this->messageManager->addErrorMessage($exception->getMessage()); + $this->messageManager->addErrorMessage(__('There are no export file with such name %1', $fileName)); } return $resultRedirect; diff --git a/app/code/Magento/ImportExport/Helper/Report.php b/app/code/Magento/ImportExport/Helper/Report.php index 02bc4d8b8a047..29d1928d837d6 100644 --- a/app/code/Magento/ImportExport/Helper/Report.php +++ b/app/code/Magento/ImportExport/Helper/Report.php @@ -140,6 +140,7 @@ protected function getFilePath($filename) * Get csv delimiter from request. * * @return string + * @since 100.2.2 */ public function getDelimiter() { diff --git a/app/code/Magento/ImportExport/Model/Export.php b/app/code/Magento/ImportExport/Model/Export.php index 850ded7c8f256..3d7a190919525 100644 --- a/app/code/Magento/ImportExport/Model/Export.php +++ b/app/code/Magento/ImportExport/Model/Export.php @@ -13,7 +13,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @since 100.0.2 - * @deprecated + * @deprecated 100.3.2 */ class Export extends \Magento\ImportExport\Model\AbstractModel { diff --git a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php index 09b17371ae4e8..085d24ca3a572 100644 --- a/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php +++ b/app/code/Magento/ImportExport/Model/Export/Adapter/Csv.php @@ -39,6 +39,7 @@ class Csv extends AbstractAdapter /** * Object destructor + * @since 100.3.5 */ public function __destruct() { @@ -54,6 +55,19 @@ public function destruct() { if (is_object($this->_fileHandler)) { $this->_fileHandler->close(); + $this->resolveDestination(); + } + } + + /** + * Remove temporary destination + * + * @return void + */ + private function resolveDestination(): void + { + // only temporary file located directly in var folder + if (strpos($this->_destination, '/') === false) { $this->_directoryHandle->delete($this->_destination); } } diff --git a/app/code/Magento/ImportExport/Model/History.php b/app/code/Magento/ImportExport/Model/History.php index b85bf7da81a35..9a97367ba8453 100644 --- a/app/code/Magento/ImportExport/Model/History.php +++ b/app/code/Magento/ImportExport/Model/History.php @@ -44,6 +44,7 @@ class History extends \Magento\Framework\Model\AbstractModel /** * @var \Magento\Backend\Model\Auth\Session + * @since 100.3.1 */ protected $session; diff --git a/app/code/Magento/ImportExport/Model/Import.php b/app/code/Magento/ImportExport/Model/Import.php index cf20001882c0d..fba7c6860bbb5 100644 --- a/app/code/Magento/ImportExport/Model/Import.php +++ b/app/code/Magento/ImportExport/Model/Import.php @@ -614,6 +614,7 @@ public function uploadSource() * * @return Import\AbstractSource * @throws LocalizedException + * @since 100.2.7 */ public function uploadFileAndGetSource() { diff --git a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php index 5bd956c1bc322..9bf5b945c8fbd 100644 --- a/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php +++ b/app/code/Magento/ImportExport/Model/Import/AbstractEntity.php @@ -15,6 +15,7 @@ /** * Import entity abstract model * + * phpcs:disable Magento2.Classes.AbstractApi * @api * * @SuppressWarnings(PHPMD.TooManyFields) @@ -335,6 +336,8 @@ public function __construct( } /** + * Returns Error aggregator + * * @return ProcessingErrorAggregatorInterface */ public function getErrorAggregator() @@ -413,7 +416,7 @@ protected function _saveValidatedBunches() $source->rewind(); $this->_dataSourceModel->cleanBunches(); - $masterAttributeCode = $this->getMasterAttributeCode(); + $mainAttributeCode = $this->getMasterAttributeCode(); while ($source->valid() || count($bunchRows) || isset($entityGroup)) { if ($startNewBunch || !$source->valid()) { @@ -453,7 +456,7 @@ protected function _saveValidatedBunches() continue; } - if (isset($rowData[$masterAttributeCode]) && trim($rowData[$masterAttributeCode])) { + if (isset($rowData[$mainAttributeCode]) && trim($rowData[$mainAttributeCode])) { /* Add entity group that passed validation to bunch */ if (isset($entityGroup)) { foreach ($entityGroup as $key => $value) { @@ -590,6 +593,7 @@ public function getBehavior(array $rowData = null) * Get default import behavior * * @return string + * phpcs:disable Magento2.Functions.StaticFunction */ public static function getDefaultBehavior() { @@ -652,7 +656,9 @@ public function isAttributeParticular($attributeCode) } /** - * @return string the master attribute code to use in an import + * Returns the master attribute code to use in an import + * + * @return string */ public function getMasterAttributeCode() { diff --git a/app/code/Magento/ImportExport/Model/Report/Csv.php b/app/code/Magento/ImportExport/Model/Report/Csv.php index 7279092265cbb..e7ddef1008444 100644 --- a/app/code/Magento/ImportExport/Model/Report/Csv.php +++ b/app/code/Magento/ImportExport/Model/Report/Csv.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\ImportExport\Model\Report; @@ -60,22 +61,16 @@ public function __construct( } /** - * @param string $originalFileName - * @param ProcessingErrorAggregatorInterface $errorAggregator - * @param bool $writeOnlyErrorItems - * @return string - * @throws \Magento\Framework\Exception\LocalizedException + * @inheritDoc */ public function createReport( $originalFileName, ProcessingErrorAggregatorInterface $errorAggregator, $writeOnlyErrorItems = false ) { - $sourceCsv = $this->createSourceCsvModel($originalFileName); - - $outputFileName = $this->generateOutputFileName($originalFileName); - $outputCsv = $this->createOutputCsvModel($outputFileName); + $outputCsv = $this->outputCsvFactory->create(); + $sourceCsv = $this->createSourceCsvModel($originalFileName); $columnsName = $sourceCsv->getColNames(); $columnsName[] = self::REPORT_ERROR_COLUMN_NAME; $outputCsv->setHeaderCols($columnsName); @@ -88,10 +83,16 @@ public function createReport( } } + $directory = $this->filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $outputFileName = $this->generateOutputFileName($originalFileName); + $directory->writeFile(Import::IMPORT_HISTORY_DIR . $outputFileName, $outputCsv->getContents()); + return $outputFileName; } /** + * Retrieve error messages + * * @param int $rowNumber * @param ProcessingErrorAggregatorInterface $errorAggregator * @return string @@ -112,16 +113,21 @@ public function retrieveErrorMessagesByRowNumber($rowNumber, ProcessingErrorAggr } /** + * Generate output filename based on source filename + * * @param string $sourceFile * @return string */ protected function generateOutputFileName($sourceFile) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction $fileName = basename($sourceFile, self::ERROR_REPORT_FILE_EXTENSION); return $fileName . self::ERROR_REPORT_FILE_SUFFIX . self::ERROR_REPORT_FILE_EXTENSION; } /** + * Create source CSV model + * * @param string $sourceFile * @return \Magento\ImportExport\Model\Import\Source\Csv */ @@ -135,18 +141,4 @@ protected function createSourceCsvModel($sourceFile) ] ); } - - /** - * @param string $outputFileName - * @return \Magento\ImportExport\Model\Export\Adapter\Csv - */ - protected function createOutputCsvModel($outputFileName) - { - return $this->outputCsvFactory->create( - [ - 'destination' => Import::IMPORT_HISTORY_DIR . $outputFileName, - 'destinationDirectoryCode' => DirectoryList::VAR_DIR, - ] - ); - } } diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml index a45783767e6a2..25331ae3cd058 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminCheckThatSomeAttributesChangedValueToEmptyAfterImportTest.xml @@ -33,8 +33,7 @@ </before> <after> <!--Delete Product and Category--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductGridPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="DeleteProductActionGroup" stepKey="deleteProduct1"> <argument name="productName" value="simpleProductWithShortNameAndSku.name"/> </actionGroup> diff --git a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml index 99622caf0697e..fe5f4358bfca3 100644 --- a/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml +++ b/app/code/Magento/ImportExport/Test/Mftf/Test/AdminURLKeyWorksWhenUpdatingProductThroughImportingCSVTest.xml @@ -44,6 +44,8 @@ <waitForPageLoad stepKey="waitForProductPageLoad"/> <seeInCurrentUrl url="{{StorefrontProductPage.url('simpleprod')}}" stepKey="seeUpdatedUrl"/> <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="$$createProduct.name$$" stepKey="assertProductName"/> - <see selector="{{StorefrontProductInfoMainSection.productSku}}" userInput="$$createProduct.sku$$" stepKey="assertProductSku"/> + <actionGroup ref="StorefrontAssertProductSkuOnProductPageActionGroup" stepKey="assertProductSku"> + <argument name="productSku" value="$$createProduct.sku$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php index 93bc4be3bb423..d9a00c16cb0af 100644 --- a/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php +++ b/app/code/Magento/ImportExport/Test/Unit/Block/Adminhtml/Grid/Column/Renderer/DownloadTest.php @@ -10,14 +10,19 @@ use Magento\Backend\Block\Context; use Magento\Backend\Model\Url; use Magento\Framework\DataObject; +use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\ImportExport\Block\Adminhtml\Grid\Column\Renderer\Download; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\ImportExport\Block\Adminhtml\Grid\Column\Renderer\Download class. + */ class DownloadTest extends TestCase { /** - * @var Context + * @var Context|MockObject */ protected $context; @@ -32,14 +37,21 @@ class DownloadTest extends TestCase protected $download; /** - * Set up + * @var Escaper|MockObject + */ + private $escaperMock; + + /** + * @inheritdoc */ protected function setUp(): void { + $this->escaperMock = $this->createMock(Escaper::class); $urlModel = $this->createPartialMock(Url::class, ['getUrl']); $urlModel->expects($this->any())->method('getUrl')->willReturn('url'); - $this->context = $this->createPartialMock(Context::class, ['getUrlBuilder']); + $this->context = $this->createPartialMock(Context::class, ['getUrlBuilder', 'getEscaper']); $this->context->expects($this->any())->method('getUrlBuilder')->willReturn($urlModel); + $this->context->expects($this->any())->method('getEscaper')->willReturn($this->escaperMock); $data = []; $this->objectManagerHelper = new ObjectManagerHelper($this); @@ -47,7 +59,7 @@ protected function setUp(): void Download::class, [ 'context' => $this->context, - 'data' => $data + 'data' => $data, ] ); } @@ -59,6 +71,14 @@ public function testGetValue() { $data = ['imported_file' => 'file.csv']; $row = new DataObject($data); + $this->escaperMock->expects($this->at(0)) + ->method('escapeHtml') + ->with('file.csv') + ->willReturn('file.csv'); + $this->escaperMock->expects($this->at(1)) + ->method('escapeHtml') + ->with('Download') + ->willReturn('Download'); $this->assertEquals('<p> file.csv</p><a href="url">Download</a>', $this->download->_getValue($row)); } } diff --git a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Export/File/DeleteTest.php b/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Export/File/DeleteTest.php deleted file mode 100644 index 5796f6d3bf4be..0000000000000 --- a/app/code/Magento/ImportExport/Test/Unit/Controller/Adminhtml/Export/File/DeleteTest.php +++ /dev/null @@ -1,198 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ImportExport\Test\Unit\Controller\Adminhtml\Export\File; - -use Magento\Backend\App\Action\Context; -use Magento\Backend\Model\View\Result\Redirect; -use Magento\Framework\App\Request\Http; -use Magento\Framework\Controller\Result\Raw; -use Magento\Framework\Controller\Result\RedirectFactory; -use Magento\Framework\Filesystem; -use Magento\Framework\Filesystem\Directory\ReadInterface; -use Magento\Framework\Filesystem\DriverInterface; -use Magento\Framework\Message\ManagerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; -use Magento\ImportExport\Controller\Adminhtml\Export\File\Delete; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -/** - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - */ -class DeleteTest extends TestCase -{ - /** - * @var Context|MockObject - */ - private $contextMock; - - /** - * @var ObjectManagerHelper - */ - private $objectManagerHelper; - - /** - * @var Http|MockObject - */ - private $requestMock; - - /** - * @var Raw|MockObject - */ - private $redirectMock; - - /** - * @var RedirectFactory|MockObject - */ - private $resultRedirectFactoryMock; - - /** - * @var Filesystem|MockObject - */ - private $fileSystemMock; - - /** - * @var DriverInterface|MockObject - */ - private $fileMock; - - /** - * @var Delete|MockObject - */ - private $deleteControllerMock; - - /** - * @var ManagerInterface|MockObject - */ - private $messageManagerMock; - - /** - * @var ReadInterface|MockObject - */ - private $directoryMock; - - /** - * Set up - */ - protected function setUp(): void - { - $this->requestMock = $this->getMockBuilder(Http::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->fileSystemMock = $this->getMockBuilder(Filesystem::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->directoryMock = $this->getMockBuilder(ReadInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->fileMock = $this->getMockBuilder(DriverInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->messageManagerMock = $this->getMockBuilder(ManagerInterface::class) - ->disableOriginalConstructor() - ->getMockForAbstractClass(); - - $this->contextMock = $this->createPartialMock( - Context::class, - ['getRequest', 'getResultRedirectFactory', 'getMessageManager'] - ); - - $this->redirectMock = $this->createPartialMock(Redirect::class, ['setPath']); - - $this->resultRedirectFactoryMock = $this->createPartialMock( - RedirectFactory::class, - ['create'] - ); - $this->resultRedirectFactoryMock->expects($this->any())->method('create')->willReturn($this->redirectMock); - $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->any()) - ->method('getResultRedirectFactory') - ->willReturn($this->resultRedirectFactoryMock); - - $this->contextMock->expects($this->any()) - ->method('getMessageManager') - ->willReturn($this->messageManagerMock); - - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->deleteControllerMock = $this->objectManagerHelper->getObject( - Delete::class, - [ - 'context' => $this->contextMock, - 'filesystem' => $this->fileSystemMock, - 'file' => $this->fileMock - ] - ); - } - - /** - * Tests download controller with different file names in request. - */ - public function testExecuteSuccess() - { - $this->requestMock->method('getParam') - ->with('filename') - ->willReturn('sampleFile'); - - $this->fileSystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->willReturn($this->directoryMock); - $this->directoryMock->expects($this->once())->method('isFile')->willReturn(true); - $this->fileMock->expects($this->once())->method('deleteFile')->willReturn(true); - $this->messageManagerMock->expects($this->once())->method('addSuccessMessage'); - - $this->deleteControllerMock->execute(); - } - - /** - * Tests download controller with different file names in request. - */ - public function testExecuteFileDoesntExists() - { - $this->requestMock->method('getParam') - ->with('filename') - ->willReturn('sampleFile'); - - $this->fileSystemMock->expects($this->once()) - ->method('getDirectoryRead') - ->willReturn($this->directoryMock); - $this->directoryMock->expects($this->once())->method('isFile')->willReturn(false); - $this->messageManagerMock->expects($this->once())->method('addErrorMessage'); - - $this->deleteControllerMock->execute(); - } - - /** - * Test execute() with invalid file name - * @param string $requestFilename - * @dataProvider invalidFileDataProvider - */ - public function testExecuteInvalidFileName($requestFilename) - { - $this->requestMock->method('getParam')->with('filename')->willReturn($requestFilename); - $this->messageManagerMock->expects($this->once())->method('addErrorMessage'); - - $this->deleteControllerMock->execute(); - } - - /** - * Data provider to test possible invalid filenames - * @return array - */ - public function invalidFileDataProvider() - { - return [ - 'Relative file name' => ['../.htaccess'], - 'Empty file name' => [''], - 'Null file name' => [null], - ]; - } -} diff --git a/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php b/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php deleted file mode 100644 index 9ca7f2bcbc9c7..0000000000000 --- a/app/code/Magento/ImportExport/Test/Unit/Model/Report/CsvTest.php +++ /dev/null @@ -1,124 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -namespace Magento\ImportExport\Test\Unit\Model\Report; - -use Magento\Framework\Filesystem; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\ImportExport\Helper\Report; -use Magento\ImportExport\Model\Export\Adapter\Csv; -use Magento\ImportExport\Model\Export\Adapter\CsvFactory; -use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; -use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregator; -use PHPUnit\Framework\MockObject\MockObject; -use PHPUnit\Framework\TestCase; - -class CsvTest extends TestCase -{ - /** - * @var Report|MockObject - */ - protected $reportHelperMock; - - /** - * @var CsvFactory|MockObject - */ - protected $outputCsvFactoryMock; - - /** - * @var Csv|MockObject - */ - protected $outputCsvMock; - - /** - * @var \Magento\ImportExport\Model\Import\Source\CsvFactory|MockObject - */ - protected $sourceCsvFactoryMock; - - /** - * @var Csv|MockObject - */ - protected $sourceCsvMock; - - /** - * @var Filesystem|MockObject - */ - protected $filesystemMock; - - /** - * @var \Magento\ImportExport\Model\Report\Csv|ObjectManager - */ - protected $csvModel; - - protected function setUp(): void - { - $objectManager = new ObjectManager($this); - $testDelimiter = 'some_delimiter'; - - $this->reportHelperMock = $this->createMock(Report::class); - $this->reportHelperMock->expects($this->any())->method('getDelimiter')->willReturn($testDelimiter); - - $this->outputCsvFactoryMock = $this->createPartialMock( - CsvFactory::class, - ['create'] - ); - $this->outputCsvMock = $this->createMock(Csv::class); - $this->outputCsvFactoryMock->expects($this->any())->method('create')->willReturn($this->outputCsvMock); - - $this->sourceCsvFactoryMock = $this->createPartialMock( - \Magento\ImportExport\Model\Import\Source\CsvFactory::class, - ['create'] - ); - $this->sourceCsvMock = $this->createMock(\Magento\ImportExport\Model\Import\Source\Csv::class); - $this->sourceCsvMock->expects($this->any())->method('valid')->willReturnOnConsecutiveCalls(true, true, false); - $this->sourceCsvMock->expects($this->any())->method('current')->willReturnOnConsecutiveCalls( - [23 => 'first error'], - [27 => 'second error'] - ); - $this->sourceCsvFactoryMock - ->expects($this->any()) - ->method('create') - ->with( - [ - 'file' => 'some_file_name', - 'directory' => null, - 'delimiter' => $testDelimiter - ] - ) - ->willReturn($this->sourceCsvMock); - - $this->filesystemMock = $this->createMock(Filesystem::class); - - $this->csvModel = $objectManager->getObject( - \Magento\ImportExport\Model\Report\Csv::class, - [ - 'reportHelper' => $this->reportHelperMock, - 'sourceCsvFactory' => $this->sourceCsvFactoryMock, - 'outputCsvFactory' => $this->outputCsvFactoryMock, - 'filesystem' => $this->filesystemMock - ] - ); - } - - public function testCreateReport() - { - $errorAggregatorMock = $this->createMock( - ProcessingErrorAggregator::class - ); - $errorProcessingMock = $this->createPartialMock( - ProcessingError::class, - ['getErrorMessage'] - ); - $errorProcessingMock->expects($this->any())->method('getErrorMessage')->willReturn('some_error_message'); - $errorAggregatorMock->expects($this->any())->method('getErrorByRowNumber')->willReturn([$errorProcessingMock]); - $this->sourceCsvMock->expects($this->any())->method('getColNames')->willReturn([]); - - $name = $this->csvModel->createReport('some_file_name', $errorAggregatorMock, true); - - $this->assertEquals($name, 'some_file_name_error_report.csv'); - } -} diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml index 150f7dbeb1046..784e140041004 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/after.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\ImportExport\Block\Adminhtml\Form\After $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset class="admin__fieldset" id="export_filter_container" style="display:none;"> +<fieldset class="admin__fieldset" id="export_filter_container"> <legend class="admin__legend"> <span><?= $block->escapeHtml(__('Entity Attributes')) ?></span> </legend> @@ -13,11 +16,17 @@ <input name="form_key" type="hidden" value="<?= /* @noEscape */ $block->getFormKey() ?>" /> <div id="export_filter_grid_container"><!-- --></div> </form> - <button class="action- scalable" type="button" onclick="getFile();"><span><?= - $block->escapeHtml(__('Continue')) - ?></span></button> + <button class="action- scalable" type="button"> + <span><?= $block->escapeHtml(__('Continue')) ?></span> + </button> </fieldset> -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'fieldset#export_filter_container') ?> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "getFile();", + 'fieldset#export_filter_container button' +) ?> +<?php $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ @@ -25,4 +34,6 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml index 3e7a19a0c0d82..b569518d9d239 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/before.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'Magento_Ui/js/modal/alert', 'prototype' @@ -26,14 +30,14 @@ require([ * Handle value change in entity type selector */ modifyFilterGrid: function() { - if ($('entity') && $F('entity') && $F('entity') != 'catalog_product') { - $$('col:first-child').each(function(el) { + if ($('entity') && \$F('entity') && \$F('entity') != 'catalog_product') { + \$$('col:first-child').each(function(el) { el.show(); }); - $$('th.no-link:first-child').each(function(el) { + \$$('th.no-link:first-child').each(function(el) { el.show(); }); - $$('td.a-center').each(function(el) { + \$$('td.a-center').each(function(el) { el.show(); }); } @@ -43,9 +47,9 @@ require([ * Post form data and process response via AJAX */ getFilter: function() { - if ($('entity') && $F('entity')) { - var url = "<?= $block->escapeJs($block->escapeUrl($block->getUrl('*/*/getFilter'))) ?>"; - var entity = $F('entity'); + if ($('entity') && \$F('entity')) { + var url = "{$block->escapeJs($block->getUrl('*/*/getFilter'))}"; + var entity = \$F('entity'); if (entity != this.previousGridEntity) { this.previousGridEntity = entity; url += ((url.slice(-1) != '/') ? '/' : '') + 'entity/' + entity; @@ -76,20 +80,20 @@ require([ * return void */ getFile = function() { - if ($('entity') && $F('entity')) { + if ($('entity') && \$F('entity')) { var form = $('export_filter_form'); var oldAction = form.action; - var url = oldAction + ((oldAction.slice(-1) != '/') ? '/' : '') + 'entity/' + $F('entity') - + '/file_format/' + $F('file_format'); - if ($F('fields_enclosure')) { - url += '/fields_enclosure/' + $F('fields_enclosure'); + var url = oldAction + ((oldAction.slice(-1) != '/') ? '/' : '') + 'entity/' + \$F('entity') + + '/file_format/' + \$F('file_format'); + if (\$F('fields_enclosure')) { + url += '/fields_enclosure/' + \$F('fields_enclosure'); } form.action = url; form.submit(); form.action = oldAction; } else { alert({ - content: '<?= $block->escapeHtml(__('Invalid data')); ?>' + content: '{$block->escapeHtml(__('Invalid data'))}' }); } }; @@ -98,4 +102,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml index 704b88b0c0f69..a34eaf09c0058 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/export/form/filter/after.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'mage/adminhtml/grid' ], function(){ @@ -17,4 +21,6 @@ require([ }; } }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml index f629e6c9e9f59..5a59ffca17cb5 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/after.phtml @@ -3,19 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\ImportExport\Block\Adminhtml\Form\After $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div class="entry-edit fieldset" id="import_validation_container" style="display:none;"> + +<div class="entry-edit fieldset" id="import_validation_container"> <div class="entry-edit-head legend"> <span class="icon-head head-edit-form fieldset-legend" id="import_validation_container_header"><?= $block->escapeHtml(__('Validation Results')) ?></span> </div><br> <div id="import_validation_messages" class="fieldset"><!-- --></div> </div> -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'div#import_validation_container') ?> +<?php $scriptString = <<<script require(['jquery', 'Magento_Ui/js/modal/alert', 'prototype'], function(jQuery){ //<![CDATA[ varienImport.resetSelectIndex('entity'); // forced resetting entity selector after page refresh //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml index bd88ec419d848..69779baba381d 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/form/before.phtml @@ -6,8 +6,10 @@ ?> <?php /** @var $block \Magento\ImportExport\Block\Adminhtml\Import\Edit\Before */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'jquery', 'Magento_Ui/js/modal/alert', @@ -27,27 +29,25 @@ require([ * List of existing behavior sets * @type {Array} */ - uniqueBehaviors: <?= /* @noEscape */ $block->getUniqueBehaviors() ?>, + uniqueBehaviors: {$block->getUniqueBehaviors()}, /** * Behaviour codes for import entities * @type {Array} */ - entityBehaviors: <?= /* @noEscape */ $block->getEntityBehaviors() ?>, + entityBehaviors: {$block->getEntityBehaviors()}, /** * Behaviour notes for import entities * @type {Array} */ - entityBehaviorsNotes: <?= /* @noEscape */ $block->getEntityBehaviorsNotes() ?>, + entityBehaviorsNotes: {$block->getEntityBehaviorsNotes()}, /** * Base url * @type {string} */ - sampleFilesBaseUrl: '<?= $block->escapeJs( - $block->escapeUrl($block->getUrl('*/*/download/', ['filename' => 'entity-name'])) - ) ?>', + sampleFilesBaseUrl: '{$block->escapeJs($block->getUrl('*/*/download/', ['filename' => 'entity-name']))}', /** * Reset selected index @@ -168,8 +168,8 @@ require([ */ postToFrame: function(newActionUrl) { if (!jQuery('[name="' + this.ifrElemName + '"]').length) { - jQuery('body').append('<iframe name="' + this.ifrElemName + '" id="' + this.ifrElemName - + '" style="display:none;"/>'); + jQuery('body').append('<iframe name="' + this.ifrElemName + '" id="' + this.ifrElemName + '"/>'); + jQuery('iframe#' + this.ifrElemName).attr('display', 'none'); } jQuery('body') .loader({ @@ -209,17 +209,17 @@ require([ postToFrameProcessResponse: function(response) { if ('object' != typeof(response)) { alert({ - content: '<?= $block->escapeHtml(__('Invalid response')); ?>' + content: '{$block->escapeHtml(__('Invalid response'))}' }); return false; } - $H(response).each(function(pair) { + \$H(response).each(function(pair) { switch (pair.key) { case 'show': case 'clear': case 'hide': - $H(pair.value).each(function(val) { + \$H(pair.value).each(function(val) { if ($(val.value)) { $(val.value)[pair.key](); } @@ -227,7 +227,7 @@ require([ break; case 'innerHTML': case 'value': - $H(pair.value).each(function(val) { + \$H(pair.value).each(function(val) { var el = $(val.key); if (el) { el[pair.key] = val.value; @@ -238,7 +238,7 @@ require([ break; case 'removeClassName': case 'addClassName': - $H(pair.value).each(function(val) { + \$H(pair.value).each(function(val) { if ($(val.key)) $(val.key)[pair.key](val.value); }); break; @@ -265,4 +265,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml b/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml index 57f521fba946f..08b8b0414e81e 100644 --- a/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml +++ b/app/code/Magento/ImportExport/view/adminhtml/templates/import/frame/result.phtml @@ -3,9 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script type='text/javascript'> +<?php $scriptString = <<<script + //<![CDATA[ - top.varienImport.postToFrameComplete(<?= /* @noEscape */ $block->getResponseJson() ?>); + top.varienImport.postToFrameComplete({$block->getResponseJson()}); //]]> -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup/UpdateIndexerOnSaveActionGroup.xml b/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup/UpdateIndexerOnSaveActionGroup.xml deleted file mode 100644 index efa6291d5de63..0000000000000 --- a/app/code/Magento/Indexer/Test/Mftf/ActionGroup/IndexerActionGroup/UpdateIndexerOnSaveActionGroup.xml +++ /dev/null @@ -1,11 +0,0 @@ -<?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="updateIndexerOnSave" extends="AdminIndexerSetUpdateOnSaveActionGroup" deprecated="Use AdminIndexerSetUpdateOnSaveActionGroup"/> -</actionGroups> diff --git a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml index 84619a5213128..51cf4aa26a1b1 100644 --- a/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml +++ b/app/code/Magento/Indexer/Test/Mftf/Test/AdminSystemIndexManagementGridChangesTest.xml @@ -23,15 +23,23 @@ <!--Open Index Management Page and Select Index mode "Update by Schedule" --> <magentoCLI command="indexer:set-mode" arguments="schedule" stepKey="setIndexerModeSchedule"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/></before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> <magentoCLI command="indexer:set-mode" arguments="realtime" stepKey="setIndexerModeRealTime"/> - <magentoCLI command="indexer:reindex" stepKey="indexerReindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="indexerReindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/InstantPurchase/Block/Button.php b/app/code/Magento/InstantPurchase/Block/Button.php index e6ea50073ed48..d4f19918afaf3 100644 --- a/app/code/Magento/InstantPurchase/Block/Button.php +++ b/app/code/Magento/InstantPurchase/Block/Button.php @@ -13,6 +13,7 @@ * Configuration for JavaScript instant purchase button component. * * @api + * @since 100.2.0 */ class Button extends Template { @@ -40,6 +41,7 @@ public function __construct( * Checks if button enabled. * * @return bool + * @since 100.2.0 */ public function isEnabled(): bool { @@ -48,6 +50,7 @@ public function isEnabled(): bool /** * @inheritdoc + * @since 100.2.0 */ public function getJsLayout(): string { diff --git a/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php b/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php index cfed97cfefd36..91eb91e2cd33c 100644 --- a/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/BillingAddressChoose/BillingAddressChooserInterface.php @@ -12,12 +12,14 @@ * Interface to choose billing address for a customer if available. * * @api + * @since 100.2.0 */ interface BillingAddressChooserInterface { /** * @param Customer $customer * @return Address|null + * @since 100.2.0 */ public function choose(Customer $customer); } diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php index b205ccab5067d..24a9569128064 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseInterface.php @@ -12,6 +12,7 @@ * Interface for detecting customer option to make instant purchase in a store. * * @api + * @since 100.2.0 */ interface InstantPurchaseInterface { @@ -21,6 +22,7 @@ interface InstantPurchaseInterface * @param Store $store * @param Customer $customer * @return InstantPurchaseOption + * @since 100.2.0 */ public function getOption( Store $store, diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php index 0748c5818c857..11ab119d6e5a5 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOption.php @@ -16,6 +16,7 @@ * Option to make instant purchase. * * @api + * @since 100.2.0 */ class InstantPurchaseOption { @@ -82,6 +83,7 @@ public function __construct( * Checks if option available * * @return bool + * @since 100.2.0 */ public function isAvailable(): bool { @@ -98,6 +100,7 @@ public function isAvailable(): bool * * @return PaymentTokenInterface * @throws LocalizedException if payment token is not defined + * @since 100.2.0 */ public function getPaymentToken(): PaymentTokenInterface { @@ -114,6 +117,7 @@ public function getPaymentToken(): PaymentTokenInterface * * @return Address * @throws LocalizedException if shipping address is not defined + * @since 100.2.0 */ public function getShippingAddress(): Address { @@ -128,6 +132,7 @@ public function getShippingAddress(): Address * * @return Address * @throws LocalizedException if billing address is not defined + * @since 100.2.0 */ public function getBillingAddress(): Address { @@ -142,6 +147,7 @@ public function getBillingAddress(): Address * * @return ShippingMethodInterface * @throws LocalizedException if shipping method is not defined + * @since 100.2.0 */ public function getShippingMethod(): ShippingMethodInterface { diff --git a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php index bd5811578ab1c..6e9490c9edb69 100644 --- a/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php +++ b/app/code/Magento/InstantPurchase/Model/InstantPurchaseOptionFactory.php @@ -14,6 +14,7 @@ * Create instances of instant purchase option. * * @api + * @since 100.2.0 */ class InstantPurchaseOptionFactory { @@ -39,6 +40,7 @@ public function __construct(ObjectManagerInterface $objectManager) * @param Address|null $billingAddress * @param ShippingMethodInterface|null $shippingMethod * @return InstantPurchaseOption + * @since 100.2.0 */ public function create( PaymentTokenInterface $paymentToken = null, @@ -58,6 +60,7 @@ public function create( * Creates new empty instance (no option available). * * @return InstantPurchaseOption + * @since 100.2.0 */ public function createDisabledOption(): InstantPurchaseOption { diff --git a/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php b/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php index 2a4f1adeb4155..b96173081164c 100644 --- a/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/PaymentMethodChoose/PaymentTokenChooserInterface.php @@ -13,6 +13,7 @@ * Interface to choose one of the stored payment methods for a customer if available. * * @api + * @since 100.2.0 */ interface PaymentTokenChooserInterface { @@ -20,6 +21,7 @@ interface PaymentTokenChooserInterface * @param Store $store * @param Customer $customer * @return PaymentTokenInterface|null + * @since 100.2.0 */ public function choose(Store $store, Customer $customer); } diff --git a/app/code/Magento/InstantPurchase/Model/PlaceOrder.php b/app/code/Magento/InstantPurchase/Model/PlaceOrder.php index 2ad56a8859cf3..bd30d19d6456b 100644 --- a/app/code/Magento/InstantPurchase/Model/PlaceOrder.php +++ b/app/code/Magento/InstantPurchase/Model/PlaceOrder.php @@ -21,6 +21,7 @@ * Place an order using instant purchase option. * * @api + * @since 100.2.0 */ class PlaceOrder { @@ -90,6 +91,7 @@ public function __construct( * @return int order identifier * @throws LocalizedException if order can not be placed. * @throws Throwable if unpredictable error occurred. + * @since 100.2.0 */ public function placeOrder( Store $store, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php index 9c8c44231e843..d1dfb11851500 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/PaymentConfiguration.php @@ -15,6 +15,7 @@ * Configure payment method for quote. * * @api May be used for pluginization. + * @since 100.2.0 */ class PaymentConfiguration { @@ -42,6 +43,7 @@ public function __construct( * @param PaymentTokenInterface $paymentToken * @return Quote * @throws LocalizedException if payment method can not be configured for a quote. + * @since 100.2.0 */ public function configurePayment( Quote $quote, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php index b8220b4ca87eb..d05a4f2a1621a 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/Purchase.php @@ -14,6 +14,7 @@ * Purchase products from quote. * * @api May be used for pluginization. + * @since 100.2.0 */ class Purchase { @@ -46,6 +47,7 @@ public function __construct( * @param Quote $quote * @return int Order id * @throws LocalizedException if order can not be placed for a quote. + * @since 100.2.0 */ public function purchase(Quote $quote): int { diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php index 993a64a3f0d7d..4db9c86f7184e 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteCreation.php @@ -16,6 +16,7 @@ * Create Quote for instance purchase. * * @api May be used for pluginization. + * @since 100.2.0 */ class QuoteCreation { @@ -43,6 +44,7 @@ public function __construct( * @param Address $billingAddress * @return Quote * @throws LocalizedException if quote can not be created. + * @since 100.2.0 */ public function createQuote( Store $store, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php index 4afa964050fcd..727917f9e3406 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/QuoteFilling.php @@ -14,6 +14,7 @@ * Fill quote with products for instant purchase. * * @api May be used for pluginization. + * @since 100.2.0 */ class QuoteFilling { @@ -25,6 +26,7 @@ class QuoteFilling * @param array $productRequest * @return Quote * @throws LocalizedException if product can not be added to quote. + * @since 100.2.0 */ public function fillQuote( Quote $quote, diff --git a/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php b/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php index 796fb924b31c9..772d6c6a033fa 100644 --- a/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php +++ b/app/code/Magento/InstantPurchase/Model/QuoteManagement/ShippingConfiguration.php @@ -16,6 +16,7 @@ * Configure shipping method for instant purchase * * @api May be used for pluginization. + * @since 100.2.0 */ class ShippingConfiguration { @@ -41,6 +42,7 @@ public function __construct( * @param ShippingMethodInterface $shippingMethod * @return Quote * @throws LocalizedException if shipping can not be configured for a quote. + * @since 100.2.0 */ public function configureShippingMethod( Quote $quote, diff --git a/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php b/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php index a65c8036a1e82..06f4358ecfa92 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingAddressChoose/ShippingAddressChooserInterface.php @@ -12,12 +12,14 @@ * Interface to choose shipping address for a customer if available. * * @api + * @since 100.2.0 */ interface ShippingAddressChooserInterface { /** * @param Customer $customer * @return Address|null + * @since 100.2.0 */ public function choose(Customer $customer); } diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php index 3339ca34ee29b..0476f2c690d4d 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserInterface.php @@ -11,6 +11,7 @@ * Provides mechanism to defer shipping method choose to the moment when quote is defined. * * @api + * @since 100.2.0 */ interface DeferredShippingMethodChooserInterface { @@ -24,6 +25,7 @@ interface DeferredShippingMethodChooserInterface * * @param Address $address * @return string|null Quote shipping method code if available + * @since 100.2.0 */ public function choose(Address $address); } diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php index ca0e9351967ad..6e2e8e562167c 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/DeferredShippingMethodChooserPool.php @@ -10,6 +10,7 @@ * Use deferred shipping method code as a key for a deferred chooser. * * @api + * @since 100.2.0 */ class DeferredShippingMethodChooserPool { @@ -34,6 +35,7 @@ public function __construct(array $choosers) /** * @param string $type * @return DeferredShippingMethodChooserInterface + * @since 100.2.0 */ public function get($type) : DeferredShippingMethodChooserInterface { diff --git a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php index c227ad793f255..dd3f9d0a6cd52 100644 --- a/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php +++ b/app/code/Magento/InstantPurchase/Model/ShippingMethodChoose/ShippingMethodChooserInterface.php @@ -20,12 +20,14 @@ * DeferredShippingMethodChooserPool. * * @api + * @since 100.2.0 */ interface ShippingMethodChooserInterface { /** * @param Address $address * @return ShippingMethodInterface|null + * @since 100.2.0 */ public function choose(Address $address); } diff --git a/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php b/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php index 31c98143385a6..3b886fb03929e 100644 --- a/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php +++ b/app/code/Magento/InstantPurchase/Model/Ui/CustomerAddressesFormatter.php @@ -11,6 +11,7 @@ * Address string presentation. * * @api May be used for pluginization. + * @since 100.2.0 */ class CustomerAddressesFormatter { @@ -19,6 +20,7 @@ class CustomerAddressesFormatter * * @param Address $address * @return string + * @since 100.2.0 */ public function format(Address $address): string { diff --git a/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php b/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php index 4c2a78e3a6024..16992ff6bf3f1 100644 --- a/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php +++ b/app/code/Magento/InstantPurchase/Model/Ui/PaymentTokenFormatter.php @@ -12,6 +12,7 @@ * Payment token string presentation. * * @api May be used for pluginization. + * @since 100.2.0 */ class PaymentTokenFormatter { @@ -34,6 +35,7 @@ public function __construct(IntegrationsManager $integrationsManager) * * @param PaymentTokenInterface $paymentToken * @return string + * @since 100.2.0 */ public function format(PaymentTokenInterface $paymentToken): string { diff --git a/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php b/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php index ec56b3e3dae02..eaefbe4c5f104 100644 --- a/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php +++ b/app/code/Magento/InstantPurchase/Model/Ui/ShippingMethodFormatter.php @@ -11,12 +11,14 @@ * Ship[ping method string presentation. * * @api May be used for pluginization. + * @since 100.2.0 */ class ShippingMethodFormatter { /** * @param ShippingMethodInterface $shippingMethod * @return string + * @since 100.2.0 */ public function format(ShippingMethodInterface $shippingMethod) : string { diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php index 825c6830b3aa6..7fe336b7a6a66 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/AvailabilityCheckerInterface.php @@ -12,6 +12,7 @@ * instant_purchase/available configuration option in vault payment config. * * @api + * @since 100.2.0 */ interface AvailabilityCheckerInterface { @@ -19,6 +20,7 @@ interface AvailabilityCheckerInterface * Checks if payment method may be used for instant purchase. * * @return bool + * @since 100.2.0 */ public function isAvailable(): bool; } diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php index 6f1e291a9a83b..9119b54329486 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentAdditionalInformationProviderInterface.php @@ -14,6 +14,7 @@ * instant_purchase/additionalInformation configuration option in vault payment config. * * @api + * @since 100.2.0 */ interface PaymentAdditionalInformationProviderInterface { @@ -22,6 +23,7 @@ interface PaymentAdditionalInformationProviderInterface * * @param PaymentTokenInterface $paymentToken * @return array + * @since 100.2.0 */ public function getAdditionalInformation(PaymentTokenInterface $paymentToken): array; } diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php index 1ee583c45cf40..c6d31a86b622d 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/PaymentTokenFormatterInterface.php @@ -15,6 +15,7 @@ * instant_purchase/tokenFormat configuration option in vault payment config. * * @api + * @since 100.2.0 */ interface PaymentTokenFormatterInterface { @@ -23,6 +24,7 @@ interface PaymentTokenFormatterInterface * * @param PaymentTokenInterface $paymentToken * @return string + * @since 100.2.0 */ public function formatPaymentToken(PaymentTokenInterface $paymentToken): string; } diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php index 56079823e9e91..0e788079794d6 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAdditionalInformationProvider.php @@ -11,6 +11,7 @@ * Payment additional information provider that returns predefined value. * * @api + * @since 100.2.0 */ class StaticAdditionalInformationProvider implements PaymentAdditionalInformationProviderInterface { @@ -30,6 +31,7 @@ public function __construct(array $value = []) /** * @inheritdoc + * @since 100.2.0 */ public function getAdditionalInformation(PaymentTokenInterface $paymentToken): array { diff --git a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php index 02e24b78f741c..78f421db6aaa8 100644 --- a/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php +++ b/app/code/Magento/InstantPurchase/PaymentMethodIntegration/StaticAvailabilityChecker.php @@ -9,6 +9,7 @@ * Availability checker with predefined result. * * @api + * @since 100.2.0 */ class StaticAvailabilityChecker implements AvailabilityCheckerInterface { @@ -28,6 +29,7 @@ public function __construct(bool $value = true) /** * @inheritdoc + * @since 100.2.0 */ public function isAvailable(): bool { diff --git a/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php b/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php index 1345a04b357ab..b34858d098494 100644 --- a/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php +++ b/app/code/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button.php @@ -6,8 +6,12 @@ namespace Magento\Integration\Block\Adminhtml\Widget\Grid\Column\Renderer; use Magento\Backend\Block\Widget\Grid\Column\Renderer\AbstractRenderer; +use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Integration\Model\Integration; +use Magento\Backend\Block\Context; /** * Render HTML <button> tag. @@ -16,19 +20,75 @@ class Button extends AbstractRenderer { /** - * {@inheritdoc} + * @var SecureHtmlRenderer */ - public function render(\Magento\Framework\DataObject $row) + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * Button constructor. + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + + /** + * @inheritDoc + */ + public function render(DataObject $row) { - /** @var array $attributes */ - $attributes = $this->_prepareAttributes($row); - return sprintf('<button %s>%s</button>', $this->_getAttributesStr($attributes), $this->_getValue($row)); + $attributes = $this->extractAttributes($row); + $attributes['button-renderer-hook-id'] = 'hook' .$this->random->getRandomString(10); + + return sprintf('<button %s>%s</button>', $this->renderAttributes($attributes), $this->_getValue($row)) + .$this->renderSpecialAttributes($attributes); + } + + /** + * Extract attributes to render. + * + * @param DataObject $row + * @return string[] + */ + private function extractAttributes(DataObject $row): array + { + $attributes = []; + foreach ($this->_getValidAttributes() as $attributeName) { + $methodName = sprintf('_get%sAttribute', ucfirst($attributeName)); + $rowMethodName = sprintf('get%s', ucfirst($attributeName)); + $attributeValue = method_exists( + $this, + $methodName + ) ? $this->{$methodName}( + $row + ) : $this->getColumn()->{$rowMethodName}(); + if ($attributeValue) { + $attributes[$attributeName] = $attributeValue; + } + } + + return $attributes; } /** * Determine whether current integration came from config file * - * @param \Magento\Framework\DataObject $row + * @param DataObject $row * @return bool */ protected function _isConfigBasedIntegration(DataObject $row) @@ -43,7 +103,7 @@ protected function _isConfigBasedIntegration(DataObject $row) /** * Whether current item is disabled. * - * @param \Magento\Framework\DataObject $row + * @param DataObject $row * @return bool * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -53,7 +113,9 @@ protected function _isDisabled(DataObject $row) } /** - * @param \Magento\Framework\DataObject $row + * Retrieve "disabled" attribute value for the row. + * + * @param DataObject $row * @return string */ protected function _getDisabledAttribute(DataObject $row) @@ -68,33 +130,47 @@ protected function _getDisabledAttribute(DataObject $row) * - Then it tries to get it from the button's column layout description. * If received attribute value is empty - attribute is not added to final HTML. * - * @param \Magento\Framework\DataObject $row + * @param DataObject $row * @return array */ protected function _prepareAttributes(DataObject $row) { - $attributes = []; - foreach ($this->_getValidAttributes() as $attributeName) { - $methodName = sprintf('_get%sAttribute', ucfirst($attributeName)); - $rowMethodName = sprintf('get%s', ucfirst($attributeName)); - $attributeValue = method_exists( - $this, - $methodName - ) ? $this->{$methodName}( - $row - ) : $this->getColumn()->{$rowMethodName}(); - - if ($attributeValue) { - $attributes[] = sprintf( - '%s="%s"', - $attributeName, - $this->escapeHtmlAttr($attributeValue, false) - ); + $attributes = $this->extractAttributes($row); + foreach ($attributes as $attributeName => $attributeValue) { + if ($attributeName === 'style' || mb_strpos($attributeName, 'on') === 0) { + //Will render event handlers and style as separate tags + continue; } + $attributes[] = sprintf( + '%s="%s"', + $attributeName, + $this->escapeHtmlAttr($attributeValue, false) + ); } + return $attributes; } + /** + * Render HTML attributes. + * + * @param array $attributes + * @return string + */ + private function renderAttributes(array $attributes): string + { + $html = ''; + foreach ($attributes as $attributeName => $attributeValue) { + if ($attributeName === 'style' || mb_strpos($attributeName, 'on') === 0) { + //Will render event handlers and style as separate tags + continue; + } + $html .= ($html ? ' ' : '') ."{$attributeName}=\"{$this->escapeHtmlAttr($attributeValue)}\""; + } + + return $html; + } + /** * Get list of available HTML attributes for this element. * @@ -140,4 +216,36 @@ protected function _getAttributesStr($attributes) { return join(' ', $attributes); } + + /** + * Render special attributes as separate tags. + * + * @param string[] $attributes + * @return string + */ + private function renderSpecialAttributes(array $attributes): string + { + if (!$hookId = $attributes['button-renderer-hook-id']) { + return ''; + } + + $html = ''; + if (!empty($attributes['style'])) { + $html .= $this->secureRenderer->renderStyleAsTag( + $attributes['style'], + "[button-renderer-hook-id='$hookId']" + ); + } + foreach ($this->_getValidAttributes() as $attr) { + if (!empty($attributes[$attr]) && mb_strpos($attr, 'on') === 0) { + $html .= $this->secureRenderer->renderEventListenerAsTag( + $attr, + $attributes[$attr], + "*[button-renderer-hook-id='$hookId']" + ); + } + } + + return $html; + } } diff --git a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php index 8bcbb45653494..ea255487b9df1 100644 --- a/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php +++ b/app/code/Magento/Integration/Controller/Adminhtml/Integration/Save.php @@ -74,7 +74,7 @@ public function execute() $this->_redirectOnSaveError(); } catch (IntegrationException $e) { $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); - $this->_getSession()->setIntegrationData($integrationData); + $this->_getSession()->setIntegrationData($this->getRequest()->getPostValue()); $this->_redirectOnSaveError(); } catch (\Magento\Framework\Exception\LocalizedException $e) { $this->messageManager->addError($this->escaper->escapeHtml($e->getMessage())); diff --git a/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminIntegrationNameInFormActionGroup.xml b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminIntegrationNameInFormActionGroup.xml new file mode 100644 index 0000000000000..70903d524a4c1 --- /dev/null +++ b/app/code/Magento/Integration/Test/Mftf/ActionGroup/AssertAdminIntegrationNameInFormActionGroup.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="AssertAdminIntegrationNameInFormActionGroup"> + <arguments> + <argument name="name" type="string"/> + </arguments> + <seeInField userInput="{{name}}" selector="{{AdminNewIntegrationSection.name}}" stepKey="checkEnteredValueIsPreserved"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml index 7bc1c9b5a274f..92133d617f626 100644 --- a/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml +++ b/app/code/Magento/Integration/Test/Mftf/Test/AdminCreateIntegrationEntityWithDuplicatedNameTest.xml @@ -54,5 +54,8 @@ <argument name="message" value="The integration with name "{{defaultIntegrationData.name}}" exists."/> <argument value="error" name="messageType"/> </actionGroup> + <actionGroup ref="AssertAdminIntegrationNameInFormActionGroup" stepKey="checkEnteredValueIsPreserved"> + <argument name="name" value="{{defaultIntegrationData.name}}"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php b/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php index ce8a785b902d7..3813682eed004 100644 --- a/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php +++ b/app/code/Magento/Integration/Test/Unit/Block/Adminhtml/Widget/Grid/Column/Renderer/ButtonTest.php @@ -15,6 +15,8 @@ use Magento\Integration\Block\Adminhtml\Widget\Grid\Column\Renderer\Button; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class ButtonTest extends TestCase { @@ -42,13 +44,37 @@ protected function setUp(): void { $this->escaperMock = $this->createMock(Escaper::class); $this->escaperMock->expects($this->any())->method('escapeHtml')->willReturnArgument(0); + $this->escaperMock->expects($this->any())->method('escapeHtmlAttr')->willReturnArgument(0); $this->contextMock = $this->createPartialMock(Context::class, ['getEscaper']); $this->contextMock->expects($this->any())->method('getEscaper')->willReturn($this->escaperMock); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('random'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $this->objectManagerHelper = new ObjectManager($this); $this->buttonRenderer = $this->objectManagerHelper->getObject( Button::class, - ['context' => $this->contextMock] + ['context' => $this->contextMock, 'random' => $randomMock, 'secureRenderer' => $secureRendererMock] ); } @@ -57,10 +83,9 @@ protected function setUp(): void */ public function testRender() { - $expectedResult = '<button id="1" type="bigButton">my button</button>'; $column = $this->getMockBuilder(Column::class) ->disableOriginalConstructor() - ->setMethods(['getType', 'getId', 'getIndex']) + ->setMethods(['getType', 'getId', 'getIndex', 'getStyle', 'getOnclick']) ->getMock(); $column->expects($this->any()) ->method('getType') @@ -68,15 +93,25 @@ public function testRender() $column->expects($this->any()) ->method('getId') ->willReturn('1'); - $this->escaperMock->expects($this->at(0))->method('escapeHtmlAttr')->willReturn('1'); - $this->escaperMock->expects($this->at(1))->method('escapeHtmlAttr')->willReturn('bigButton'); $column->expects($this->any()) ->method('getIndex') ->willReturn('name'); + $column->expects($this->any()) + ->method('getStyle') + ->willReturn('display: block;'); + $column->expects($this->any()) + ->method('getOnclick') + ->willReturn('alert(1);'); $this->buttonRenderer->setColumn($column); $object = new DataObject(['name' => 'my button']); $actualResult = $this->buttonRenderer->render($object); - $this->assertEquals($expectedResult, $actualResult); + $this->assertEquals( + '<button id="1" type="bigButton" button-renderer-hook-id="hookrandom">my button</button>' + .'<style>[button-renderer-hook-id=\'hookrandom\'] { display: block; }</style>' + .'<script>document.querySelector(\'*[button-renderer-hook-id=\'hookrandom\']\').onclick = ' + .'function () { alert(1); };</script>', + $actualResult + ); } } diff --git a/app/code/Magento/Integration/composer.json b/app/code/Magento/Integration/composer.json index 0b9752c743213..c85e84284b43f 100644 --- a/app/code/Magento/Integration/composer.json +++ b/app/code/Magento/Integration/composer.json @@ -12,7 +12,8 @@ "magento/module-customer": "*", "magento/module-security": "*", "magento/module-store": "*", - "magento/module-user": "*" + "magento/module-user": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml b/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml index 1730509a65910..6dd7d1b4a2421 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/integration/activate/permissions/tab/webapi.phtml @@ -7,11 +7,13 @@ * * @var \Magento\Integration\Block\Adminhtml\Integration\Activate\Permissions\Tab\Webapi $block */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset class="admin__fieldset form-inline entry-edit"> - <?php if ($block->isTreeEmpty()) : ?> + <?php if ($block->isTreeEmpty()): ?> <p class="empty"><?= $block->escapeHtml(__('No permissions requested')) ?></p> - <?php else : ?> + <?php else: ?> <div class="field" data-role="tree-resources-container"> <div class="control"> <div id="resource-tree" class="tree x-tree" data-role="resource-tree"></div> @@ -19,8 +21,11 @@ </div> <?php endif ?> </fieldset> -<?php if (!$block->isTreeEmpty()) : ?> - <script> +<?php +if (!$block->isTreeEmpty()): + $treeJson = /* @noEscape */ $block->getResourcesTreeJson(); + $selectedJson = /* @noEscape */ $block->getSelectedResourcesJson(); + $scriptString = <<<script require(["jquery", "Magento_User/js/roles-tree"], function($){ $.widget('mage.rolesTree', $.mage.rolesTree, { _checkNode: function(event) {}, @@ -32,9 +37,11 @@ }); $('[data-role="resource-tree"]').rolesTree({ - 'treeInitData': <?= /* @noEscape */ $block->getResourcesTreeJson() ?>, - 'treeInitSelectedData': <?= /* @noEscape */ $block->getSelectedResourcesJson() ?> + 'treeInitData': {$treeJson}, + 'treeInitSelectedData': {$selectedJson} }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif ?> diff --git a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml index ef0a667d2de47..b56ad208071d8 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/integration/popup_container.phtml @@ -7,8 +7,12 @@ * * @var \Magento\Backend\Block\Template $block */ + +/** @var \Magento\Backend\Block\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ "jquery", 'Magento_Ui/js/modal/confirm', @@ -18,34 +22,34 @@ ], function ($, Confirm) { window.integration = new Integration( - '<?= $block->escapeUrl( + '{$block->escapeJs( $block->getUrl( '*/*/permissionsDialog', ['id' => ':id', 'reauthorize' => ':isReauthorize', '_escape_params' => false] ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*/tokensDialog', ['id' => ':id', 'reauthorize' => ':isReauthorize', '_escape_params' => false] ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*/tokensExchange', ['id' => ':id', 'reauthorize' => ':isReauthorize', '_escape_params' => false] ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*' ) - ) ?>', - '<?= $block->escapeUrl( + )}', + '{$block->escapeJs( $block->getUrl( '*/*/loginSuccessCallback' ) - ) ?>' + )}' ); /** @@ -55,8 +59,9 @@ $('div#integrationGrid').on('click', 'button#delete', function (e) { new Confirm({ - title: '<?= $block->escapeHtml(__('Are you sure?')) ?>', - content: "<?= $block->escapeHtml(__("Are you sure you want to delete this integration? You can't undo this action.")) ?>", + title: '{$block->escapeJs(__('Are you sure?'))}', + content: "{$block->escapeJs(__("Are you sure you want to delete this integration? " . + "You can't undo this action."))}", actions: { confirm: function () { $.mage.dataPost().postData({action: $(e.target).data('url'), data: {}}); @@ -67,6 +72,9 @@ }); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> -<div id="integration-popup-container" style="display: none;"></div> +<div id="integration-popup-container"></div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", '#integration-popup-container') ?> diff --git a/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml b/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml index 1737f66ce4a1b..25caf5060cb5f 100644 --- a/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml +++ b/app/code/Magento/Integration/view/adminhtml/templates/resourcetree.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Integration\Block\Adminhtml\Integration\Edit\Tab\Webapi */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getChildHtml() ?> @@ -18,8 +19,7 @@ <label class="label" for="all_resources"><span><?= $block->escapeHtml(__('Resource Access')) ?></span></label> <div class="control"> - <select id="all_resources" name="all_resources" - onchange="jQuery('[data-role=tree-resources-container]').toggle()" class="select"> + <select id="all_resources" name="all_resources" class="select"> <option value="0" <?= ($block->isEverythingAllowed() ? '' : 'selected="selected"') ?>> <?= $block->escapeHtml(__('Custom')) ?> </option> @@ -27,11 +27,16 @@ <?= $block->escapeHtml(__('All')) ?> </option> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "jQuery('[data-role=tree-resources-container]').toggle()", + 'select#all_resources' + ) ?> </div> </div> <div class="field - <?php if ($block->isEverythingAllowed()) :?> + <?php if ($block->isEverythingAllowed()):?> no-display <?php endif ?>" data-role="tree-resources-container"> diff --git a/app/code/Magento/LayeredNavigation/Block/Navigation.php b/app/code/Magento/LayeredNavigation/Block/Navigation.php index e394fe7f6cf5b..85d3dd2a2a01d 100644 --- a/app/code/Magento/LayeredNavigation/Block/Navigation.php +++ b/app/code/Magento/LayeredNavigation/Block/Navigation.php @@ -76,6 +76,7 @@ protected function _prepareLayout() /** * @inheritdoc + * @since 100.3.4 */ protected function _beforeToHtml() { diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Data/ConfigData.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Data/ConfigData.xml new file mode 100644 index 0000000000000..85eb8830dffca --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Data/ConfigData.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="DisplayProductCountDefaultValue"> + <data key="path">catalog/layered_navigation/display_product_count</data> + <data key="value">1</data> + </entity> + <entity name="PriceNavigationStepCalculationDefaultValue"> + <data key="path">catalog/layered_navigation/price_range_calculation</data> + <data key="value">auto</data> + </entity> +</entities> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml index cdd692763d399..49f2294b978ee 100644 --- a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/ShopByButtonInMobileTest.xml @@ -50,8 +50,12 @@ </actionGroup> <selectOption selector="{{AdminProductFormSection.customSelectField($$attribute.attribute[attribute_code]$$)}}" userInput="option1" stepKey="selectAttribute"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Check storefront mobile view for shop by button is functioning as expected --> <comment userInput="Check storefront mobile view for shop by button is functioning as expected" stepKey="commentCheckShopByButton" /> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePage"/> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml new file mode 100644 index 0000000000000..cb7e683605a68 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest.xml @@ -0,0 +1,397 @@ +<?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="StorefrontAllAttributeOptionsAreShownInLayeredNavigationTest"> + <annotations> + <features value="LayeredNavigation"/> + <stories value="Product attributes in Layered Navigation"/> + <title value="Limitation of displayed attribute options number in layered navigation with ElasticSearch"/> + <description value="All attribute options are shown in Layered navigation"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-28963"/> + <group value="layeredNavigation"/> + <group value="catalog"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + + <before> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <magentoCLI command="config:set {{DisplayProductCountDefaultValue.path}} {{DisplayProductCountDefaultValue.value}}" stepKey="enableDisplayProductCount"/> + <magentoCLI command="config:set {{PriceNavigationStepCalculationDefaultValue.path}} {{PriceNavigationStepCalculationDefaultValue.value}}" stepKey="setPriceNavigationStepCalculationDefaultValue"/> + <createData entity="ApiCategory" stepKey="createCategory"/> + <!-- Create an attribute --> + <createData entity="productAttributeWithTwoOptions" stepKey="createConfigProductAttribute"/> + <!--Create 15 attribute options--> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption4"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption5"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption6"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption7"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption8"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption9"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption10"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption11"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption12"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption13"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption14"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <createData entity="productAttributeOption" stepKey="createConfigProductAttributeOption15"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + <!--Get Created options data--> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getConfigAttributeOption1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getConfigAttributeOption2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="3" stepKey="getConfigAttributeOption3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="4" stepKey="getConfigAttributeOption4"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="5" stepKey="getConfigAttributeOption5"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="6" stepKey="getConfigAttributeOption6"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="7" stepKey="getConfigAttributeOption7"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="8" stepKey="getConfigAttributeOption8"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="9" stepKey="getConfigAttributeOption9"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="10" stepKey="getConfigAttributeOption10"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="11" stepKey="getConfigAttributeOption11"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="12" stepKey="getConfigAttributeOption12"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="13" stepKey="getConfigAttributeOption13"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="14" stepKey="getConfigAttributeOption14"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="15" stepKey="getConfigAttributeOption15"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </getData> + + <!-- Add attribute to attribute set--> + <createData entity="AddToDefaultSet" stepKey="createConfigAddToAttributeSet"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + </createData> + + <!-- Create Configurable product --> + <createData entity="BaseConfigurableProduct" stepKey="createConfigProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create simple products and set them created attribute value --> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct1"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct2"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct3"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct4"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption4"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct5"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption5"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct6"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption6"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct7"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption7"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct8"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption8"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct9"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption9"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct10"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption10"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct11"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption11"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct12"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption12"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct13"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption13"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct14"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption14"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProductWithCategory" stepKey="createConfigChildProduct15"> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption15"/> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Create the configurable product --> + <createData entity="ConfigurableProduct15Options" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigProductAttribute"/> + <requiredEntity createDataKey="getConfigAttributeOption1"/> + <requiredEntity createDataKey="getConfigAttributeOption2"/> + <requiredEntity createDataKey="getConfigAttributeOption3"/> + <requiredEntity createDataKey="getConfigAttributeOption4"/> + <requiredEntity createDataKey="getConfigAttributeOption5"/> + <requiredEntity createDataKey="getConfigAttributeOption6"/> + <requiredEntity createDataKey="getConfigAttributeOption7"/> + <requiredEntity createDataKey="getConfigAttributeOption8"/> + <requiredEntity createDataKey="getConfigAttributeOption9"/> + <requiredEntity createDataKey="getConfigAttributeOption10"/> + <requiredEntity createDataKey="getConfigAttributeOption11"/> + <requiredEntity createDataKey="getConfigAttributeOption12"/> + <requiredEntity createDataKey="getConfigAttributeOption13"/> + <requiredEntity createDataKey="getConfigAttributeOption14"/> + <requiredEntity createDataKey="getConfigAttributeOption15"/> + </createData> + + <!-- Add simple products to the configurable product --> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild1"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct1"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct2"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild3"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct3"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild4"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct4"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild5"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct5"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild6"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct6"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild7"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct7"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild8"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct8"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild9"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct9"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild10"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct10"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild11"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct11"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild12"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct12"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild13"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct13"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild14"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct14"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild15"> + <requiredEntity createDataKey="createConfigProduct"/> + <requiredEntity createDataKey="createConfigChildProduct15"/> + </createData> + </before> + + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createConfigProduct" stepKey="deleteConfigProduct"/> + <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> + <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> + <deleteData createDataKey="createConfigChildProduct3" stepKey="deleteConfigChildProduct3"/> + <deleteData createDataKey="createConfigChildProduct4" stepKey="deleteConfigChildProduct4"/> + <deleteData createDataKey="createConfigChildProduct5" stepKey="deleteConfigChildProduct5"/> + <deleteData createDataKey="createConfigChildProduct6" stepKey="deleteConfigChildProduct6"/> + <deleteData createDataKey="createConfigChildProduct7" stepKey="deleteConfigChildProduct7"/> + <deleteData createDataKey="createConfigChildProduct8" stepKey="deleteConfigChildProduct8"/> + <deleteData createDataKey="createConfigChildProduct9" stepKey="deleteConfigChildProduct9"/> + <deleteData createDataKey="createConfigChildProduct10" stepKey="deleteConfigChildProduct10"/> + <deleteData createDataKey="createConfigChildProduct11" stepKey="deleteConfigChildProduct11"/> + <deleteData createDataKey="createConfigChildProduct12" stepKey="deleteConfigChildProduct12"/> + <deleteData createDataKey="createConfigChildProduct13" stepKey="deleteConfigChildProduct13"/> + <deleteData createDataKey="createConfigChildProduct14" stepKey="deleteConfigChildProduct14"/> + <deleteData createDataKey="createConfigChildProduct15" stepKey="deleteConfigChildProduct15"/> + <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteAttribute"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="openCategory"> + <argument name="category" value="$createCategory$"/> + </actionGroup> + <!--Check filtration options for created attribute. All attribute options should be displayed --> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption1PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption1.label$"/> + <argument name="attributeOptionPosition" value="1"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption2PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption2.label$"/> + <argument name="attributeOptionPosition" value="2"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption3PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption3.label$"/> + <argument name="attributeOptionPosition" value="3"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption4PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption4.label$"/> + <argument name="attributeOptionPosition" value="4"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption5PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption5.label$"/> + <argument name="attributeOptionPosition" value="5"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption6PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption6.label$"/> + <argument name="attributeOptionPosition" value="6"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption7PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption7.label$"/> + <argument name="attributeOptionPosition" value="7"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption8PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption8.label$"/> + <argument name="attributeOptionPosition" value="8"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption9PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption9.label$"/> + <argument name="attributeOptionPosition" value="9"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption10PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption10.label$"/> + <argument name="attributeOptionPosition" value="10"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption11PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption11.label$"/> + <argument name="attributeOptionPosition" value="11"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption12PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption12.label$"/> + <argument name="attributeOptionPosition" value="12"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption13PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption13.label$"/> + <argument name="attributeOptionPosition" value="13"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption14PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption14.label$"/> + <argument name="attributeOptionPosition" value="14"/> + </actionGroup> + <actionGroup ref="AssertStorefrontAttributeOptionPresentInLayeredNavigationActionGroup" stepKey="assertAttributeOption15PresentInLayeredNavigation"> + <argument name="attributeLabel" value="$createConfigProductAttribute.attribute[frontend_labels][0][label]$"/> + <argument name="attributeOptionLabel" value="$getConfigAttributeOption15.label$"/> + <argument name="attributeOptionPosition" value="15"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml new file mode 100644 index 0000000000000..be56caa6d3246 --- /dev/null +++ b/app/code/Magento/LayeredNavigation/Test/Mftf/Test/StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest.xml @@ -0,0 +1,153 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontFilterableProductAttributeInLayeredNavigationWithoutReindexTest"> + <annotations> + <features value="LayeredNavigation"/> + <stories value="Product attributes in Layered Navigation"/> + <title value="Create and add new dropdown product attribute to existing set, assign it to existing product with that set and see it on layered navigation"/> + <description value="Verify that new dropdown attribute in existing attribute set shows on layered navigation on storefront without reindex"/> + <severity value="MAJOR"/> + <testCaseId value="MC-35954"/> + <group value="layeredNavigation"/> + <group value="SearchEngineElasticsearch"/> + </annotations> + + <before> + <!--Create category, attribute set with multiselect product attribute with two options--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="CatalogAttributeSet" stepKey="createAttributeSet"/> + <createData entity="multipleSelectProductAttribute" stepKey="createMultiselectAttribute"/> + <createData entity="ProductAttributeOption10" stepKey="firstMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </createData> + <createData entity="ProductAttributeOption11" stepKey="secondMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondMultiselectOption"> + <requiredEntity createDataKey="createMultiselectAttribute"/> + </getData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createAttributeSet.attribute_set_id$/" stepKey="onAttributeSetEdit"/> + <waitForPageLoad stepKey="waitForAttributeSetPageLoad"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignMultiselectAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createMultiselectAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSet"/> + <!--Create configurable product with created attribute set and multiselect attribute--> + <createData entity="SimpleOne" storeCode="all" stepKey="createFirstSimpleProduct"> + <field key="attribute_set_id">$createAttributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getFirstMultiselectOption"/> + </createData> + <createData entity="SimpleOne" storeCode="all" stepKey="createSecondSimpleProduct"> + <field key="attribute_set_id">$createAttributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getSecondMultiselectOption"/> + </createData> + <createData entity="BaseConfigurableProduct" stepKey="createConfigurableProduct"> + <field key="attribute_set_id">$createAttributeSet.attribute_set_id$</field> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ConfigurableProductOneOption" stepKey="createConfigProductOption"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getFirstMultiselectOption"/> + </createData> + <createData entity="ConfigurableProductOneOption" stepKey="createConfigProductOption2"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createMultiselectAttribute"/> + <requiredEntity createDataKey="getSecondMultiselectOption"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createFirstSimpleProduct"/> + </createData> + <createData entity="ConfigurableProductAddChild" stepKey="createConfigProductAddChild2"> + <requiredEntity createDataKey="createConfigurableProduct"/> + <requiredEntity createDataKey="createSecondSimpleProduct"/> + </createData> + <!--Create new dropdown attribute with two options and set Use in layered navigation "Filterable (no results)"--> + <createData entity="dropdownProductAttribute" stepKey="createDropdownAttribute"/> + <createData entity="ProductAttributeOption10" stepKey="firstDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <createData entity="ProductAttributeOption11" stepKey="secondDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </createData> + <getData entity="ProductAttributeOptionGetter" index="1" stepKey="getFirstDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </getData> + <getData entity="ProductAttributeOptionGetter" index="2" stepKey="getSecondDropdownOption"> + <requiredEntity createDataKey="createDropdownAttribute"/> + </getData> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="goToDropdownAttributePage"> + <argument name="productAttributeCode" value="$createDropdownAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup" stepKey="setDropdownUseInLayeredNavigationNoResults"> + <argument name="useInLayeredNavigationValue" value="Filterable (no results)"/> + </actionGroup> + <actionGroup ref="AdminProductAttributeSaveActionGroup" stepKey="saveDropdownAttribute"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </before> + + <after> + <deleteData createDataKey="createFirstSimpleProduct" stepKey="deleteFirstSimpleProduct"/> + <deleteData createDataKey="createSecondSimpleProduct" stepKey="deleteSecondSimpleProduct"/> + <deleteData createDataKey="createConfigurableProduct" stepKey="deleteConfigurableProduct"/> + <deleteData createDataKey="createMultiselectAttribute" stepKey="deleteMultiselectAttribute"/> + <deleteData createDataKey="createDropdownAttribute" stepKey="deleteDropdownAttribute"/> + <deleteData createDataKey="createAttributeSet" stepKey="deleteAttributeSet"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!--Set attribute option Use in layered navigation to "Filterable(no results)"--> + <actionGroup ref="OpenProductAttributeFromSearchResultInGridActionGroup" stepKey="goToMultiselectAttributePage"> + <argument name="productAttributeCode" value="$createMultiselectAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="AdminSetProductAttributeUseInLayeredNavigationOptionActionGroup" stepKey="setMultiselectUseInLayeredNavigationNoResults"> + <argument name="useInLayeredNavigationValue" value="Filterable (no results)"/> + </actionGroup> + <actionGroup ref="AdminProductAttributeSaveActionGroup" stepKey="saveMultiselectAttribute"/> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="onCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoad"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createMultiselectAttribute.default_frontend_label$)}}" stepKey="waitForMultiselectAttributeVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createMultiselectAttribute.default_frontend_label$)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" stepKey="waitForMultiselectAttributeOptionsVisible"/> + <seeElement selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel($getFirstMultiselectOption.label$)}}" stepKey="assertMultiselectAttributeFirstOptionInLayeredNavigation"/> + <seeElement selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel($getSecondMultiselectOption.label$)}}" stepKey="assertMultiselectAttributeSecondOptionInLayeredNavigation"/> + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/$createAttributeSet.attribute_set_id$/" stepKey="onAttributeSetEditPage"/> + <waitForPageLoad stepKey="waitForAttributeSetEditPageLoad"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignDropdownAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="$createDropdownAttribute.attribute_code$"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="saveAttributeSetWithDropdownAttribute"/> + <!--Assign dropdown attribute to child product of configurable--> + <amOnPage url="{{AdminProductEditPage.url($createFirstSimpleProduct.id$)}}" stepKey="visitAdminEditProductPage"/> + <waitForElementVisible selector="{{AdminProductFormSection.customSelectField('$createDropdownAttribute.attribute_code$')}}" stepKey="waitForDropdownAttributeSelectVisible"/> + <selectOption selector="{{AdminProductFormSection.customSelectField('$createDropdownAttribute.attribute_code$')}}" userInput="$getFirstDropdownOption.label$" stepKey="selectValueOfNewAttribute"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + <!--Assert that dropdown attribute is present on layered navigation with both options--> + <amOnPage url="{{StorefrontCategoryPage.url($createCategory.custom_attributes[url_key]$)}}" stepKey="goToCategoryPage"/> + <waitForPageLoad stepKey="waitForCategoryPageLoadWithDropdownAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createDropdownAttribute.default_frontend_label$)}}" stepKey="waitForDropdownAttributeVisible"/> + <conditionalClick selector="{{StorefrontCategorySidebarSection.filterOptionsTitle($createDropdownAttribute.default_frontend_label$)}}" dependentSelector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" visible="false" stepKey="clickToExpandDropdownAttribute"/> + <waitForElementVisible selector="{{StorefrontCategorySidebarSection.activeFilterOptions}}" stepKey="waitForDropdownAttributeOptionsVisible"/> + <seeElement selector="{{StorefrontCategorySidebarSection.enabledFilterOptionItemByLabel($getFirstDropdownOption.label$)}}" stepKey="assertDropdownAttributeFirstOptionInLayeredNavigation"/> + <seeElement selector="{{StorefrontCategorySidebarSection.disabledFilterOptionItemByLabel($getSecondDropdownOption.label$)}}" stepKey="assertDropdownAttributeSecondOptionInLayeredNavigation"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php index a728f4c3a4393..808b01bac58aa 100644 --- a/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php +++ b/app/code/Magento/LoginAsCustomer/Model/AuthenticateCustomerBySecret.php @@ -8,9 +8,11 @@ namespace Magento\LoginAsCustomer\Model; use Magento\Customer\Model\Session; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; use Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface; use Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface; /** * @inheritdoc @@ -29,16 +31,25 @@ class AuthenticateCustomerBySecret implements AuthenticateCustomerBySecretInterf */ private $customerSession; + /** + * @var SetLoggedAsCustomerAdminIdInterface + */ + private $setLoggedAsCustomerAdminId; + /** * @param GetAuthenticationDataBySecretInterface $getAuthenticationDataBySecret * @param Session $customerSession + * @param SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId */ public function __construct( GetAuthenticationDataBySecretInterface $getAuthenticationDataBySecret, - Session $customerSession + Session $customerSession, + ?SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId = null ) { $this->getAuthenticationDataBySecret = $getAuthenticationDataBySecret; $this->customerSession = $customerSession; + $this->setLoggedAsCustomerAdminId = $setLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerAdminIdInterface::class); } /** @@ -58,6 +69,6 @@ public function execute(string $secret): void } $this->customerSession->regenerateId(); - $this->customerSession->setLoggedAsCustomerAdmindId($authenticationData->getAdminId()); + $this->setLoggedAsCustomerAdminId->execute($authenticationData->getAdminId()); } } diff --git a/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerAdminId.php b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerAdminId.php new file mode 100644 index 0000000000000..17af8a3b5c11f --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerAdminId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Customer\Model\Session; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class GetLoggedAsCustomerAdminId implements GetLoggedAsCustomerAdminIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(): int + { + return (int)$this->session->getLoggedAsCustomerAdmindId(); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerCustomerId.php b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerCustomerId.php new file mode 100644 index 0000000000000..9783b04a5a03f --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/GetLoggedAsCustomerCustomerId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Backend\Model\Auth\Session; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerCustomerIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class GetLoggedAsCustomerCustomerId implements GetLoggedAsCustomerCustomerIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(): int + { + return (int)$this->session->getLoggedAsCustomerCustomerId(); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php new file mode 100644 index 0000000000000..0d2af8669777c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/IsLoginAsCustomerEnabledForCustomerResult.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerEnabledForCustomerResult implements IsLoginAsCustomerEnabledForCustomerResultInterface +{ + /** + * @var string[] + */ + private $messages; + + /** + * @param array $messages + */ + public function __construct(array $messages = []) + { + $this->messages = $messages; + } + + /** + * @inheritdoc + */ + public function isEnabled(): bool + { + return empty($this->messages); + } + + /** + * @inheritdoc + */ + public function getMessages(): array + { + return $this->messages; + } + + /** + * @inheritdoc + */ + public function setMessages(array $messages): void + { + $this->messages = $messages; + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php b/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php new file mode 100644 index 0000000000000..89cb960e78bb8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/Resolver/IsLoginAsCustomerEnabledResolver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model\Resolver; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerEnabledResolver implements IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory + */ + private $resultFactory; + + /** + * @param ConfigInterface $config + * @param IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + */ + public function __construct( + ConfigInterface $config, + IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + ) { + $this->config = $config; + $this->resultFactory = $resultFactory; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = []; + if (!$this->config->isEnabled()) { + $messages[] = __('Login as Customer is disabled.'); + } + + return $this->resultFactory->create(['messages' => $messages]); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerAdminId.php b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerAdminId.php new file mode 100644 index 0000000000000..aa16dbcd4f808 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerAdminId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Customer\Model\Session; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class SetLoggedAsCustomerAdminId implements SetLoggedAsCustomerAdminIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(int $adminId): void + { + $this->session->setLoggedAsCustomerAdmindId($adminId); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerCustomerId.php b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerCustomerId.php new file mode 100644 index 0000000000000..95e159bdeded3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Model/SetLoggedAsCustomerCustomerId.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomer\Model; + +use Magento\Backend\Model\Auth\Session; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface; + +/** + * @inheritdoc + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class SetLoggedAsCustomerCustomerId implements SetLoggedAsCustomerCustomerIdInterface +{ + /** + * @var Session + */ + private $session; + + /** + * @param Session $session + */ + public function __construct(Session $session) + { + $this->session = $session; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): void + { + $this->session->setLoggedAsCustomerCustomerId($customerId); + } +} diff --git a/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php b/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php index 3b8d26129a91e..9b8567663578d 100644 --- a/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php +++ b/app/code/Magento/LoginAsCustomer/Plugin/AdminLogoutPlugin.php @@ -10,9 +10,12 @@ use Magento\Backend\Model\Auth; use Magento\LoginAsCustomerApi\Api\ConfigInterface; use Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerCustomerIdInterface; /** * Delete all Login as Customer sessions for logging out admin. + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class AdminLogoutPlugin { @@ -26,16 +29,24 @@ class AdminLogoutPlugin */ private $deleteAuthenticationDataForUser; + /** + * @var GetLoggedAsCustomerCustomerIdInterface + */ + private $getLoggedAsCustomerCustomerId; + /** * @param ConfigInterface $config * @param DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser + * @param GetLoggedAsCustomerCustomerIdInterface $getLoggedAsCustomerCustomerId */ public function __construct( ConfigInterface $config, - DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser + DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, + GetLoggedAsCustomerCustomerIdInterface $getLoggedAsCustomerCustomerId ) { $this->config = $config; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; + $this->getLoggedAsCustomerCustomerId = $getLoggedAsCustomerCustomerId; } /** @@ -45,8 +56,10 @@ public function __construct( */ public function beforeLogout(Auth $subject): void { - if ($this->config->isEnabled()) { - $userId = (int)$subject->getUser()->getId(); + $user = $subject->getUser(); + $isLoggedAsCustomer = (bool)$this->getLoggedAsCustomerCustomerId->execute(); + if ($this->config->isEnabled() && $user && $isLoggedAsCustomer) { + $userId = (int)$user->getId(); $this->deleteAuthenticationDataForUser->execute($userId); } } diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertContainsMessageOrderCreatedByAdminActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertContainsMessageOrderCreatedByAdminActionGroup.xml new file mode 100644 index 0000000000000..bcf6fc96aa131 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertContainsMessageOrderCreatedByAdminActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertContainsMessageOrderCreatedByAdminActionGroup"> + <annotations> + <description>Assert Admin Order page contains message about Order created by a Store Administrator. + </description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + <argument name="adminUserFullName" type="string"/> + </arguments> + + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Order Placed by {{adminUserFullName}} using Login as Customer" + stepKey="seeMessageOrderCreatedByAdmin"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup.xml new file mode 100644 index 0000000000000..7e032b168f062 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup"> + <annotations> + <description>Verify Login as Customer config section is not available by direct url.</description> + </annotations> + + <amOnPage url="{{AdminLoginAsCustomerConfigPage.url}}" stepKey="navigateToLoginAsCustomerConfigSection"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <seeInCurrentUrl url="admin/system_config/index" stepKey="seeRedirectToConfigIndexPage"/> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotVisibleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotVisibleActionGroup.xml new file mode 100644 index 0000000000000..875869d9928a4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigNotVisibleActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertLoginAsCustomerConfigNotVisibleActionGroup"> + <annotations> + <description>Verify no Login as Customer config section available.</description> + </annotations> + + <!-- TODO: update --> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomerItem"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigVisibleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigVisibleActionGroup.xml new file mode 100644 index 0000000000000..cdc513651ad54 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerConfigVisibleActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertLoginAsCustomerConfigVisibleActionGroup"> + <annotations> + <description>Verify Login as Customer config section available.</description> + </annotations> + + <seeElement selector="{{AdminCustomerConfigSection.loginAsCustomerSettingsHead}}" stepKey="seeLoginAsCustomerSettingsHead"/> + <seeElement selector="{{AdminCustomerConfigSection.enableExtensionLabel}}" stepKey="seeEnableExtensionLabel"/> + <seeElement selector="{{AdminCustomerConfigSection.disablePageCacheLabel}}" stepKey="seeDisablePageCacheLabel"/> + <seeElement selector="{{AdminCustomerConfigSection.storeViewToLoginToLabel}}" stepKey="seeStoreViewToLoginToLabel"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerLogRecordActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerLogRecordActionGroup.xml new file mode 100644 index 0000000000000..da47864e28eac --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerLogRecordActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertLoginAsCustomerLogRecordActionGroup"> + <annotations> + <description>Assert Login as Customer Log record is correct.</description> + </annotations> + <arguments> + <argument name="rowNumber" type="string"/> + <argument name="adminId" type="string"/> + <argument name="customerId" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="checkUrl"/> + <see selector="{{AdminLoginAsCustomerLogGridSection.adminIdInRow(rowNumber)}}" userInput="{{adminId}}" + stepKey="seeCorrectAdminId"/> + <see selector="{{AdminLoginAsCustomerLogGridSection.customerIdInRow(rowNumber)}}" userInput="{{customerId}}" + stepKey="seeCorrectCustomerId"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup.xml new file mode 100644 index 0000000000000..779cb1e5c8899 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertLoginAsCustomerSectionLinkNotAvailableActionGroup"> + <annotations> + <description>Verify Login as Customer config section isn't available.</description> + </annotations> + + <conditionalClick selector="{{CaptchaFormsDisplayingSection.customer}}" dependentSelector="{{AdminCustomerConfigSection.loginAsCustomerSettingsSectionLink}}" visible="false" stepKey="expandCustomerGroup"/> + <dontSeeElement selector="{{AdminCustomerConfigSection.loginAsCustomerSettingsSectionLink}}" stepKey="dontSeeLoginAsCustomerSettingsLink"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebSiteAndGroupActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebSiteAndGroupActionGroup.xml new file mode 100644 index 0000000000000..e7f55e69b1cda --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminCreateCustomerWithWebSiteAndGroupActionGroup.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCreateCustomerWithWebSiteAndGroupActionGroup"> + <conditionalClick selector="{{AdminCustomerAccountInformationSection.assistanceAllowed}}" dependentSelector="{{AdminCustomerAccountInformationSection.assistanceAllowed}}" visible="true" stepKey="clickAllowAssistance" after="FillEmail"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml new file mode 100644 index 0000000000000..52f5b190c3cb8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminEditUserRoleActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEditUserRoleActionGroup"> + <annotations> + <description>Open User Role resources for edit.</description> + </annotations> + <arguments> + <argument name="roleName" type="string"/> + </arguments> + + <amOnPage url="{{AdminRolesPage.url}}" stepKey="navigateToUserRolesGrid"/> + <fillField selector="{{AdminRoleGridSection.roleNameFilterTextField}}" userInput="{{roleName}}" + stepKey="enterRoleName"/> + <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickSearch"/> + <see selector="{{AdminDataGridTableSection.row('1')}}" userInput="{{roleName}}" stepKey="seeUserRole"/> + <click selector="{{AdminDataGridTableSection.row('1')}}" stepKey="openRoleEditPage"/> + <waitForPageLoad stepKey="waitForRoleEditPageLoad"/> + <fillField selector="{{AdminEditRoleInfoSection.password}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterThePassword" /> + <click selector="{{AdminCreateRoleSection.roleResources}}" stepKey="clickToOpenRoleResources"/> + <waitForPageLoad stepKey="waitForRoleResourceTab"/> + <selectOption userInput="Custom" selector="{{AdminCreateRoleSection.resourceAccess}}" + stepKey="selectResourceAccess"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminFilterLoginAsCustomerLogActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminFilterLoginAsCustomerLogActionGroup.xml new file mode 100644 index 0000000000000..17eb351bf8f1b --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminFilterLoginAsCustomerLogActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFilterLoginAsCustomerLogActionGroup"> + <annotations> + <description>Filter Login as Customer Log records.</description> + </annotations> + <arguments> + <argument name="adminId" type="string"/> + <argument name="customerId" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="checkUrl"/> + <click selector="{{AdminLoginAsCustomerLogToolbarSection.resetFilter}}" stepKey="resetFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForResetFilter"/> + <fillField selector="{{AdminLoginAsCustomerLogFiltersSection.adminId}}" userInput="{{adminId}}" + stepKey="fillAdminId"/> + <fillField selector="{{AdminLoginAsCustomerLogFiltersSection.customerId}}" userInput="{{customerId}}" + stepKey="fillCustomerId"/> + <click selector="{{AdminLoginAsCustomerLogToolbarSection.search}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForApplyFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnCustomerPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnCustomerPageActionGroup.xml new file mode 100644 index 0000000000000..e56d898fa9197 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnCustomerPageActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is absent on Customer page.</description> + </annotations> + <arguments> + <argument name="customerId" type="string"/> + </arguments> + + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="gotoCustomerPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnOrderPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnOrderPageActionGroup.xml new file mode 100644 index 0000000000000..1119f6b05fac3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerAbsentOnOrderPageActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerAbsentOnOrderPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is absent on Order page.</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <dontSee userInput="Login as Customer" stepKey="dontSeeLoginAsCustomer"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogAbsentInMenuActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogAbsentInMenuActionGroup.xml new file mode 100644 index 0000000000000..beb0f4cba973c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogAbsentInMenuActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerLogAbsentInMenuActionGroup"> + <annotations> + <description>Verify Login as Customer is absent in admin menu.</description> + </annotations> + + <click selector="{{AdminMenuSection.menuItem(AdminMenuCustomers.dataUiId)}}" + stepKey="clickOnCustomersMenuItem"/> + <dontSeeElement selector="{{AdminMenuSection.menuItem(AdminMenuLoginAsCustomer.dataUiId)}}" + stepKey="dontSeeLoginAsCustomerLog"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogPageNotAvailableActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogPageNotAvailableActionGroup.xml new file mode 100644 index 0000000000000..939ff73199a63 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLogPageNotAvailableActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerLogPageNotAvailableActionGroup"> + <annotations> + <description>Verify Login as Customer is not available by direct url.</description> + </annotations> + + <amOnPage url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="openAdminLoginAsCustomerLogPage"/> + <waitForPageLoad stepKey="waitForLoginAsCustomerLogPageLoad"/> + <see userInput="404 Error" selector="{{AdminHeaderSection.pageHeading}}" stepKey="see404PageHeading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageActionGroup.xml new file mode 100644 index 0000000000000..599a6f8f9e270 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerLoginFromCustomerPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is works properly from Customer page.</description> + </annotations> + <arguments> + <argument name="customerId" type="string"/> + </arguments> + + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="gotoCustomerPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <click selector="{{AdminCustomerMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="You are about to Login as Customer" + stepKey="seeModal"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml new file mode 100644 index 0000000000000..8db34a05252ee --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is works properly from Customer page with manual Store View choose.</description> + </annotations> + <arguments> + <argument name="customerId" type="string"/> + <argument name="storeViewName" type="string" defaultValue="default"/> + </arguments> + + <amOnPage url="{{AdminEditCustomerPage.url(customerId)}}" stepKey="gotoCustomerPage"/> + <waitForPageLoad stepKey="waitForCustomerPageLoad"/> + <click selector="{{AdminCustomerMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="Login as Customer: Select Store View" stepKey="seeModal"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <selectOption selector="{{AdminLoginAsCustomerConfirmationModalSection.storeView}}" userInput="{{storeViewName}}" stepKey="selectStoreView"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml new file mode 100644 index 0000000000000..a478f8e9d18cd --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminLoginAsCustomerLoginFromOrderPageActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminLoginAsCustomerLoginFromOrderPageActionGroup"> + <annotations> + <description>Verify Login as Customer Login action is works properly from Order grid page.</description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + + <amOnPage url="{{AdminOrderPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <click selector="{{AdminOrderDetailsMainActionsSection.loginAsCustomer}}" stepKey="clickLoginAsCustomerLink"/> + <see selector="{{AdminConfirmationModalSection.title}}" userInput="You are about to Login as Customer" + stepKey="seeModal"/> + <see selector="{{AdminConfirmationModalSection.message}}" userInput="Actions taken while in "Login as Customer" will affect actual customer data." stepKey="seeModalMessage"/> + <click selector="{{AdminConfirmationModalSection.ok}}" stepKey="clickLogin"/> + <switchToNextTab stepKey="switchToNewTab"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogActionGroup.xml new file mode 100644 index 0000000000000..9130ba5b05c51 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenLoginAsCustomerLogActionGroup"> + <annotations> + <description>Navigate to Login as Customer Log page.</description> + </annotations> + + <amOnPage url="{{AdminLoginAsCustomerLogPage.url}}" stepKey="gotoLoginAsCustomerLogPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Login as Customer Log" stepKey="titleIsVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogFromMenuActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogFromMenuActionGroup.xml new file mode 100644 index 0000000000000..b1a99d67afe4f --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminOpenLoginAsCustomerLogFromMenuActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenLoginAsCustomerLogFromMenuActionGroup"> + <annotations> + <description>Navigate to Login as Customer Log from Menu.</description> + </annotations> + + <click selector="{{AdminMenuSection.menuItem(AdminMenuCustomers.dataUiId)}}" + stepKey="clickOnCustomersMenuItem"/> + <click selector="{{AdminMenuSection.menuItem(AdminMenuLoginAsCustomer.dataUiId)}}" + stepKey="openLoginAsCustomerLog"/> + <waitForPageLoad stepKey="waitForLoginAsCustomerLog"/> + <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Login as Customer Log" + stepKey="seeForLoginAsCustomerLog"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminRevokeRoleResourceActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminRevokeRoleResourceActionGroup.xml new file mode 100644 index 0000000000000..030b53408951e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/AdminRevokeRoleResourceActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminRevokeRoleResourceActionGroup"> + <annotations> + <description>Revoke access to resource from edit role page.</description> + </annotations> + <arguments> + <argument name="resourceName" type="string"/> + </arguments> + + <selectOption selector="{{AdminEditRoleResourcesSection.resourceAccess}}" userInput="0" + stepKey="selectResourceAccessCustom"/> + <waitForElementVisible selector="{{AdminEditRoleInfoSection.blockName(resourceName)}}" + stepKey="waitForElementVisible"/> + <scrollTo selector="{{AdminEditRoleInfoSection.blockName(resourceName)}}" x="0" y="-80" + stepKey="scrollToContentBlock"/> + <click selector="{{AdminEditRoleInfoSection.blockName(resourceName)}}" stepKey="clickContentBlockCheckbox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup.xml new file mode 100644 index 0000000000000..f40ea7f93c7a1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup"> + <annotations> + <description>Verify Storefront Order page contains message about Order created by a Store Administrator. + </description> + </annotations> + <arguments> + <argument name="orderId" type="string"/> + </arguments> + + <amOnPage url="{{StorefrontCustomerOrderViewPage.url(orderId)}}" stepKey="gotoOrderPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="Order Placed by Store Administrator" stepKey="seeMessageOrderCreatedByAdmin"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertCustomerOnStoreViewActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertCustomerOnStoreViewActionGroup.xml new file mode 100644 index 0000000000000..f63cda2303526 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertCustomerOnStoreViewActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertCustomerOnStoreViewActionGroup"> + <annotations> + <description>Assert Customer is on the provided Store View.</description> + </annotations> + <arguments> + <argument name="storeViewName" type="string" defaultValue="Default Store View"/> + </arguments> + + <see selector="{{StorefrontHeaderSection.storeViewSwitcher}}" userInput="{{storeViewName}}" stepKey="clickStoreViewSwitcher"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerLoggedInActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerLoggedInActionGroup.xml new file mode 100644 index 0000000000000..bb7e938bdfb59 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerLoggedInActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertLoginAsCustomerLoggedInActionGroup"> + <annotations> + <description>Verify Admin successfully logged in as Customer.</description> + </annotations> + <arguments> + <argument name="customerFullName" type="string"/> + <argument name="customerEmail" type="string"/> + </arguments> + + <seeInCurrentUrl url="{{StorefrontCustomerDashboardPage.url}}" stepKey="assertOnCustomerAccountPage"/> + <see selector="{{StorefrontPanelHeaderSection.welcomeMessage}}" userInput="Welcome, {{customerFullName}}!" stepKey="assertCorrectWelcomeMessage"/> + <see selector="{{StorefrontCustomerDashboardAccountInformationSection.ContactInformation}}" + userInput="{{customerEmail}}" stepKey="assertCustomerEmailInContactInformation"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerNotificationBannerActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerNotificationBannerActionGroup.xml new file mode 100644 index 0000000000000..ce2e261f10040 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontAssertLoginAsCustomerNotificationBannerActionGroup.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup"> + <annotations> + <description>Verify Login as Customer notification banner present on page.</description> + </annotations> + <arguments> + <argument name="customerFullName" type="string"/> + <argument name="websiteName" type="string" defaultValue="Main Website"/> + </arguments> + + <waitForElementVisible selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" stepKey="waitForNotificationBanner"/> + <see selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" + userInput="You are connected as {{customerFullName}} on {{websiteName}}" + stepKey="assertCorrectNotificationBannerMessage"/> + <seeElement selector="{{StorefrontLoginAsCustomerNotificationSection.closeLink}}" + stepKey="assertCloseNotificationBannerPresent"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutAndCloseTabActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutAndCloseTabActionGroup.xml new file mode 100644 index 0000000000000..87e5b264a6ed6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutAndCloseTabActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSignOutAndCloseTabActionGroup"> + <annotations> + <description>Customer sign out and close tab.</description> + </annotations> + + <click selector="{{StoreFrontSignOutSection.customerAccount}}" stepKey="clickCustomerButton"/> + <waitForElementVisible selector="{{StoreFrontSignOutSection.signOut}}" stepKey="waitForSignOut"/> + <click selector="{{StoreFrontSignOutSection.signOut}}" stepKey="clickToSignOut"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You are signed out" stepKey="signOut"/> + <closeTab stepKey="closeTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutNotificationBannerAndCloseTabActionGroup.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutNotificationBannerAndCloseTabActionGroup.xml new file mode 100644 index 0000000000000..e0e6973509eb3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/ActionGroup/StorefrontSignOutNotificationBannerAndCloseTabActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSignOutNotificationBannerAndCloseTabActionGroup"> + <annotations> + <description>Customer sign out by Notification Banner and close tab.</description> + </annotations> + + <waitForElementVisible selector="{{StorefrontLoginAsCustomerNotificationSection.notificationText}}" stepKey="waitForNotificationBanner"/> + <click selector="{{StorefrontLoginAsCustomerNotificationSection.closeLink}}" stepKey="clickToSignOut"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <see userInput="You are signed out" stepKey="signOut"/> + <closeTab stepKey="closeTab"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/AdminMenuData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/AdminMenuData.xml new file mode 100644 index 0000000000000..38779dd987c65 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/AdminMenuData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMenuLoginAsCustomer"> + <data key="pageTitle">Login as Customer Log</data> + <data key="title">Login as Customer Log</data> + <data key="dataUiId">magento-loginascustomer-login-log</data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/CustomerData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/CustomerData.xml new file mode 100644 index 0000000000000..10cdc87be6430 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/CustomerData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="Simple_US_Customer_Assistance_Allowed" type="customer" extends="Simple_US_Customer"> + <requiredEntity type="customer_extension_attribute">AssistanceAllowed</requiredEntity> + </entity> + <entity name="Simple_US_CA_Customer_Assistance_Allowed" type="customer" extends="Simple_US_CA_Customer"> + <requiredEntity type="customer_extension_attribute">AssistanceAllowed</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/ExtensionAttributeAssistanceData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/ExtensionAttributeAssistanceData.xml new file mode 100644 index 0000000000000..44582cfae5c36 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/ExtensionAttributeAssistanceData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AssistanceDisallowed" type="customer_extension_attribute"> + <data key="assistance_allowed">1</data> + </entity> + <entity name="AssistanceAllowed" type="customer_extension_attribute"> + <data key="assistance_allowed">2</data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/LoginAsCustomerConfigData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/LoginAsCustomerConfigData.xml new file mode 100644 index 0000000000000..316a810ce13e4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/LoginAsCustomerConfigData.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details.`` + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="LoginAsCustomerConfigDataEnabled"> + <data key="path">login_as_customer/general/enabled</data> + </entity> + <entity name="LoginAsCustomerDisablePageCache"> + <data key="path">login_as_customer/general/disable_page_cache</data> + </entity> + <entity name="LoginAsCustomerStoreViewLogin"> + <data key="path">login_as_customer/general/store_view_manual_choice_enabled</data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/UserRoleData.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/UserRoleData.xml new file mode 100644 index 0000000000000..720ae7eb2147e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Data/UserRoleData.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <!--This Role has access for all resources individually --> + <entity name="CustomRoleAllResources" type="user_role"> + <data key="name" unique="suffix">allAccessRole</data> + <data key="rolename">allAccessRole</data> + <data key="current_password">{{_ENV.MAGENTO_ADMIN_PASSWORD}}</data> + <data key="resourceAccess">Custom</data> + <data key="resource"> + [ + 'Magento_Backend::dashboard', + 'Magento_Analytics::analytics', + 'Magento_Sales::sales', + 'Magento_Catalog::catalog', + 'Magento_Customer::customer', + 'Magento_Cart::cart', + 'Magento_Backend::myaccount', + 'Magento_Backend::marketing', + 'Magento_Backend::content', + 'Magento_Reports::report', + 'Magento_Backend::stores', + 'Magento_Backend::system', + 'Magento_Backend::global_search', + ] + </data> + </entity> +</entities> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE.txt b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE_AFL.txt b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminCustomerConfigPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminCustomerConfigPage.xml new file mode 100644 index 0000000000000..b48e20237cee8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminCustomerConfigPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminLoginAsCustomerConfigPage" url="admin/system_config/edit/section/login_as_customer" area="admin" module="Magento_LoginAsCustomer"> + <section name="AdminCustomerConfigSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLogPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLogPage.xml new file mode 100644 index 0000000000000..a917ab6acb182 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLogPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="AdminLoginAsCustomerLogPage" url="loginascustomer_log/log/index/" area="admin" module="Magento_LoginAsCustomer"> + <section name="AdminLoginAsCustomerLogToolbarSection"/> + <section name="AdminLoginAsCustomerLogFiltersSection"/> + <section name="AdminLoginAsCustomerLogGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLoginManualPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLoginManualPage.xml new file mode 100644 index 0000000000000..ddb87ba83bc3a --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminLoginAsCustomerLoginManualPage.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="AdminLoginAsCustomerLoginManualPage" url="loginascustomer/login/manual/entity_id/{{id}}/" + area="storefront" module="Magento_LoginAsCustomer" parameterized="true"> + <section name="AdminLoginAsCustomerLoginManualActionsSection"/> + <section name="AdminLoginAsCustomerLoginManualContentSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminRoleEditPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminRoleEditPage.xml new file mode 100644 index 0000000000000..12eba90aea723 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/AdminRoleEditPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminRoleEditPage" url="admin/user_role/editrole/rid/{{roleId}}/" module="Magento_User" area="admin" parameterized="true"> + <section name="AdminRoleGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/StorefrontLoginAsCustomerLoginProceedPage.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/StorefrontLoginAsCustomerLoginProceedPage.xml new file mode 100644 index 0000000000000..05af5f506112e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Page/StorefrontLoginAsCustomerLoginProceedPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:/Page/etc/PageObject.xsd"> + <page name="StorefrontLoginAsCustomerLoginProceedPage" url="loginascustomer/login/proceed" area="storefront" module="Magento_LoginAsCustomer"/> +</pages> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/README.md b/app/code/Magento/LoginAsCustomer/Test/Mftf/README.md new file mode 100644 index 0000000000000..1d574fc35cdab --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/README.md @@ -0,0 +1,3 @@ +# Functional Tests + +The Functional Tests for **Magento Login as Customer** module. diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerAccountInformationSection.xml new file mode 100644 index 0000000000000..1a31afad3fbf0 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerAccountInformationSection.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="AdminCustomerAccountInformationSection"> + <element name="assistanceAllowed" type="button" selector="//input[@name='customer[extension_attributes][assistance_allowed]']/../label"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerConfigSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerConfigSection.xml new file mode 100644 index 0000000000000..14b3336ea5484 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerConfigSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminCustomerConfigSection"> + <element name="loginAsCustomerSettingsSectionLink" type="text" selector="//a/span[contains(text(),'Login as Customer')]" timeout="30"/> + <element name="loginAsCustomerSettingsHead" type="text" selector="#login_as_customer_general-head" timeout="30"/> + <element name="enableExtensionLabel" type="text" selector="//span[contains(text(),'Enable Extension') and contains(@data-config-scope,'[GLOBAL]')]" timeout="30"/> + <element name="disablePageCacheLabel" type="text" selector="//span[contains(text(),'Disable Page Cache For Admin User') and contains(@data-config-scope,'[GLOBAL]')]" timeout="30"/> + <element name="storeViewToLoginToLabel" type="text" selector="//span[contains(text(),'Store View To Login To') and contains(@data-config-scope,'[GLOBAL]')]" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerGridSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerGridSection.xml new file mode 100644 index 0000000000000..4d7c49644957d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerGridSection.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="AdminCustomerGridSection"> + <element name="customerLoginAsCustomerLinkByEmail" type="text" selector="//tr[contains(@class, 'data-row') and td/div[text()='{{customerEmail}}']]//a[@class='action-menu-item'][text() = 'Login as Customer']" parameterized="true" timeout="30"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerMainActionsSection.xml new file mode 100644 index 0000000000000..b7e35ba113795 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminCustomerMainActionsSection.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="AdminCustomerMainActionsSection"> + <element name="loginAsCustomer" type="button" selector="#login_as_customer" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.xml new file mode 100644 index 0000000000000..f400ba02a5392 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerConfirmationModalSection.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="AdminLoginAsCustomerConfirmationModalSection"> + <element name="storeView" type="select" selector="//select[@id='lac-confirmation-popup-store-id']"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogFiltersSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogFiltersSection.xml new file mode 100644 index 0000000000000..dce2204f86f82 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogFiltersSection.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminLoginAsCustomerLogFiltersSection"> + <element name="loginId" type="input" selector="//input[@id='subscriberGrid_filter_login_id']"/> + <element name="customerId" type="input" selector="//input[@id='subscriberGrid_filter_customer_id']"/> + <element name="customerEmail" type="input" selector="//input[@id='subscriberGrid_filter_email']"/> + <element name="adminId" type="input" selector="//input[@id='subscriberGrid_filter_admin_id']"/> + <element name="adminName" type="input" selector="//input[@id='subscriberGrid_filter_username']"/> + <element name="from" type="input" selector="//input[@name='created_at[from]']"/> + <element name="to" type="input" selector="//input[@name='created_at[to]']"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogGridSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogGridSection.xml new file mode 100644 index 0000000000000..032281a2277f4 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogGridSection.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="AdminLoginAsCustomerLogGridSection"> + <element name="loginIdInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[1]" parameterized="true"/> + <element name="customerIdInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[2]" parameterized="true"/> + <element name="customerEmailInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[3]" parameterized="true"/> + <element name="adminIdInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[4]" parameterized="true"/> + <element name="adminNameInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[5]" parameterized="true"/> + <element name="createdAtInRow" type="text" selector="//table[@data-role='grid']/tbody/tr[{{row}}]/td[6]" parameterized="true"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogToolbarSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogToolbarSection.xml new file mode 100644 index 0000000000000..a403367ee0d02 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLogToolbarSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminLoginAsCustomerLogToolbarSection"> + <element name="search" type="button" selector="button[data-action='grid-filter-apply']"/> + <element name="resetFilter" type="button" selector="button[data-action='grid-filter-reset']"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualActionsSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualActionsSection.xml new file mode 100644 index 0000000000000..a2d373d4d7ab3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualActionsSection.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="AdminLoginAsCustomerLoginManualActionsSection"> + <element name="loginAsCustomer" type="button" selector="#save" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualContentSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualContentSection.xml new file mode 100644 index 0000000000000..944bd2679e703 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminLoginAsCustomerLoginManualContentSection.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="AdminLoginAsCustomerLoginManualContentSection"> + <element name="storeView" type="select" selector="//select[@name='store_id']" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.xml new file mode 100644 index 0000000000000..629ec253d2778 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrderDetailsMainActionsSection.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="AdminOrderDetailsMainActionsSection"> + <element name="loginAsCustomer" type="button" selector="#guest_to_customer" timeout="30"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrdersGridSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrdersGridSection.xml new file mode 100644 index 0000000000000..34c4a14e81bb5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/AdminOrdersGridSection.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="AdminOrdersGridSection"> + <element name="loginAsCustomerLink" type="text" selector="//td/div[contains(.,'{{orderId}}')]/../..//a[@class='action-menu-item'][text() = 'Login as Customer']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/StorefrontLoginAsCustomerNotificationSection.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/StorefrontLoginAsCustomerNotificationSection.xml new file mode 100644 index 0000000000000..cee7609632e87 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Section/StorefrontLoginAsCustomerNotificationSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="StorefrontLoginAsCustomerNotificationSection"> + <element name="notificationText" type="text" selector="//div[contains(@class, 'lac-notification')]//div[contains(@class, 'lac-notification-text')]/span" timeout="30"/> + <element name="closeLink" type="button" selector="//div[contains(@class, 'lac-notification')]//div[contains(@class, 'lac-notification-links')]/a[contains(@class, 'lac-notification-close-link')]" timeout="30"/> + </section> +</sections> + diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml new file mode 100644 index 0000000000000..8902877b38e54 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerButtonTest.xml @@ -0,0 +1,104 @@ +<?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="AdminChangUserAccessToLoginAsCustomerButtonTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="Change admin user's access to 'Login as Customer Button'"/> + <description + value="Verify admin user's access to 'Login as Customer Button' can be changed"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUserAfter"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User has access to 'Login as Customer Button' --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="verifyLoginAsCustomerWorksOnCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="customerSignOutAndCloseTab"/> + + <!-- Revoke 'Login as Customer Button' access for new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUser"/> + + <actionGroup ref="AdminEditUserRoleActionGroup" stepKey="openEditUserRole"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="Allow Login as Customer Button"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveEditedRole"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutDefaultAdminUserAfterRevoke"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUserAfterRevoke"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User no longer has access to 'Login as Customer Button' --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnCustomerPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml new file mode 100644 index 0000000000000..c0aa3740309a6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminChangUserAccessToLoginAsCustomerLogTest.xml @@ -0,0 +1,98 @@ +<?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="AdminChangUserAccessToLoginAsCustomerLogTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="Change admin user's access to 'Login as Customer Log'"/> + <description + value="Verify admin user's access to 'Login as Customer Log' can be changed"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUserAfter"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User has access to 'Login as Customer Log' --> + <actionGroup ref="AdminOpenLoginAsCustomerLogFromMenuActionGroup" stepKey="openLoginAsCustomerLog"/> + + <!-- Revoke 'Login as Customer Log' access for new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutNewUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUser"/> + + <actionGroup ref="AdminEditUserRoleActionGroup" stepKey="openEditUserRole"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="View Login as Customer Log"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveEditedRole"/> + + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutDefaultAdminUserAfterRevoke"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUserAfterRevoke"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify new User no longer has access to 'Login as Customer Log' menu item --> + <actionGroup ref="AdminLoginAsCustomerLogAbsentInMenuActionGroup" stepKey="verifyLoginAsCustomerLogAbsentInMenu"/> + + <!-- Verify new User no longer has access to 'Login as Customer Log' --> + <actionGroup ref="AdminLoginAsCustomerLogPageNotAvailableActionGroup" stepKey="verifyLoginAsCustomerLogPageNotAvailable"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml new file mode 100644 index 0000000000000..c083383dd8861 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAddProductToWishlistTest.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerAddProductToWishlistTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Added product to wish-list"/> + <title value="Admin user login as customer and add products to customer's wish-list"/> + <description + value="Verify that Admin can add products to customer's wish-list using Login as Customer functionality"/> + <severity value="AVERAGE"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Admin Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Navigate to Product page and add it to Wishlist --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addProductToWishlist"> + <argument name="productVar" value="$$createSimpleProduct$$"/> + </actionGroup> + + <!-- Open Customer Wishlist and verify Product present there --> + <actionGroup ref="AssertProductIsPresentInWishListActionGroup" stepKey="assertProductInWishlist"> + <argument name="productName" value="$$createSimpleProduct.name$$"/> + <argument name="productPrice" value="$$createSimpleProduct.price$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml new file mode 100644 index 0000000000000..de9790894015c --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerAutoDetectionTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerAutoDetectionTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Select Store View based on 'Store View To Login In' setting"/> + <title + value="Admin user directly login into customer account with store View To Login In = Auto detection"/> + <description + value="Verify admin user can directly login into customer account to Default store view when Store View To Login In = Auto detection"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI + command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" + stepKey="enableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI + command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" + stepKey="disableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Assert Customer logged on on default store view --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerGird"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeDefaultStoreCodeInUrl"/> + + <!-- Log out Customer and close tab --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml new file mode 100644 index 0000000000000..f9418a9cf1e1b --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerDirectlyToCustomWebsiteTest.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerDirectlyToCustomWebsiteTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Login as Customer into additional website"/> + <title value="Admin user directly login into customer account on custom website"/> + <description + value="Verify admin user can directly login into customer account on custom website using 'Login as customer' functionality"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI + command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" + stepKey="enableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createCustomWebsite"> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> + </actionGroup> + <actionGroup ref="CreateCustomStoreActionGroup" stepKey="createCustomStore"> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="store" value="{{customStoreGroup.name}}"/> + <argument name="rootCategory" value="Default Category"/> + </actionGroup> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStoreEN"/> + </actionGroup> + <actionGroup ref="AdminCreateCustomerWithWebSiteAndGroupActionGroup" stepKey="createCustomer"> + <argument name="customerData" value="Simple_US_Customer_Assistance_Allowed"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeView" value="{{customStoreEN.name}}"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> + <argument name="customerEmail" value="Simple_US_Customer_Assistance_Allowed.email"/> + </actionGroup> + <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI + command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" + stepKey="disableAddStoreCodeToUrls"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="OpenEditCustomerFrom"> + <argument name="customer" value="Simple_US_Customer_Assistance_Allowed"/> + </actionGroup> + <grabFromCurrentUrl regex="~id/(\d+)/~" stepKey="customerId" /> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="${customerId}"/> + </actionGroup> + + <!-- Assert Customer logged on Custom Website --> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeStoreCodeInUrl"> + <argument name="storeCode" value="{{customStoreEN.code}}"/> + </actionGroup> + + <!-- Log out Customer and close tab --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml new file mode 100644 index 0000000000000..cf90f0b6a8511 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerEditCustomersAddressTest.xml @@ -0,0 +1,77 @@ +<?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="AdminLoginAsCustomerEditCustomersAddressTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Edit Customer addresses"/> + <title value="Admin user login as customer and edit customer's address"/> + <description + value="Verify Admin can access customer's personal cabinet and change his default shipping and billing addresses using Login as Customer functionality"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAdmin"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer Login from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Add new default address --> + <actionGroup ref="StorefrontAddCustomerDefaultAddressActionGroup" stepKey="addNewDefaultAddress"> + <argument name="Address" value="US_Address_CA"/> + </actionGroup> + + <!-- Open Customer edit page --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInAsCustomer"/> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!-- Assert Customer Default Billing Address --> + <actionGroup stepKey="checkDefaultBilling" ref="AdminAssertCustomerDefaultBillingAddress"> + <argument name="firstName" value="$$createCustomer.firstname$$"/> + <argument name="lastName" value="$$createCustomer.lastname$$"/> + <argument name="street1" value="{{US_Address_CA.street[0]}}"/> + <argument name="state" value="{{US_Address_CA.state}}"/> + <argument name="postcode" value="{{US_Address_CA.postcode}}"/> + <argument name="country" value="{{US_Address_CA.country}}"/> + <argument name="telephone" value="{{US_Address_CA.telephone}}"/> + </actionGroup> + + <!-- Assert Customer Default Shipping Address --> + <actionGroup stepKey="checkDefaultShipping" ref="AdminAssertCustomerDefaultShippingAddress"> + <argument name="firstName" value="$$createCustomer.firstname$$"/> + <argument name="lastName" value="$$createCustomer.lastname$$"/> + <argument name="street1" value="{{US_Address_CA.street[0]}}"/> + <argument name="state" value="{{US_Address_CA.state}}"/> + <argument name="postcode" value="{{US_Address_CA.postcode}}"/> + <argument name="country" value="{{US_Address_CA.country}}"/> + <argument name="telephone" value="{{US_Address_CA.telephone}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml new file mode 100644 index 0000000000000..4ef72d949065d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest.xml @@ -0,0 +1,34 @@ +<?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="AdminLoginAsCustomerLogNotShownIfLoginAsCustomerDisabledTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="'Login as Customer Log' not shown if 'Login as customer' functionality is disabled"/> + <description value="Verify that 'Login as Customer Log' not shown if 'Login as customer' functionality is disabled"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + <!-- Verify Login as Customer Log is absent in admin menu --> + <actionGroup ref="AdminLoginAsCustomerLogAbsentInMenuActionGroup" stepKey="verifyLoginAsCustomerLogAbsentInMenu"/> + + <!-- Verify Login as Customer Log is not available by direct url --> + <actionGroup ref="AdminLoginAsCustomerLogPageNotAvailableActionGroup" stepKey="verifyLoginAsCustomerLogNotAvailable"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml new file mode 100644 index 0000000000000..5b5e9e21113c8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerLoggingTest.xml @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerLoggingTest"> + <annotations> + <features value="Login as Customer"/> + <!-- TODO: change "stories" value --> + <stories value="Place order and reorder"/> + <title value="Using 'Login as Customer' is logged properly"/> + <description + value="Verify that 'Login as customer Log' record information about using 'Login as Customer' functionality properly"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="NewAdminUser" stepKey="createNewAdmin"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> + <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> + </before> + <after> + <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToDeleteNewAdmin"/> + <actionGroup ref="AdminDeleteUserViaCurlActionGroup" stepKey="deleteNewAdmin"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login into First Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsFirstCustomerByDefaultAdmin"> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutFirstCustomerDefaultAdmin"/> + + <!-- Login into Second Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsSecondCustomerByDefaultAdmin"> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutSecondCustomerDefaultAdmin"/> + + <!-- Log out as Default Admin User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsDefaultAdmin"/> + + <!-- Login as New Admin User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="$$createNewAdmin.username$$"/> + <argument name="password" value="$$createNewAdmin.password$$"/> + </actionGroup> + + <!-- Login into First Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsFirstCustomerByNewAdmin"> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutFirstCustomerNewAdmin"/> + + <!-- Login into Second Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsSecondCustomerByNewAdmin"> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutSecondCustomerNewAdmin"/> + + <!-- Navigate to Login as Customer Log page --> + <actionGroup ref="AdminOpenLoginAsCustomerLogActionGroup" stepKey="gotoLoginAsCustomerLog"/> + + <!-- Perform assertions --> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyDefaultAdminFirstCustomerLogRecord"> + <argument name="rowNumber" value="4"/> + <argument name="adminId" value="1"/> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyDefaultAdminSecondCustomerLogRecord"> + <argument name="rowNumber" value="3"/> + <argument name="adminId" value="1"/> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyNewAdminFirstCustomerLogRecord"> + <argument name="rowNumber" value="2"/> + <argument name="adminId" value="$$createNewAdmin.id$$"/> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertLoginAsCustomerLogRecordActionGroup" stepKey="verifyNewAdminSecondCustomerLogRecord"> + <argument name="rowNumber" value="1"/> + <argument name="adminId" value="$$createNewAdmin.id$$"/> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + + <!-- Log out as New Admin User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsNewAdmin"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml new file mode 100644 index 0000000000000..27aee2061f204 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseStoreCodeInUrlTest.xml @@ -0,0 +1,36 @@ +<?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="AdminLoginAsCustomerManualChooseStoreCodeInUrlTest" extends="AdminLoginAsCustomerManualChooseTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Select Store View based on 'Store View To Login In' setting"/> + <title + value="Admin user directly login into customer account with store View To Login In = Manual Choose when store code is added to url"/> + <description + value="Verify admin user can directly login into customer account to Custom store view when Store View To Login In = Manual Choose when store code is added to url"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI + command="config:set {{StorefrontEnableAddStoreCodeToUrls.path}} {{StorefrontEnableAddStoreCodeToUrls.value}}" + stepKey="enableAddStoreCodeToUrls" after="enableLoginAsCustomerManualChoose"/> + </before> + <after> + <magentoCLI + command="config:set {{StorefrontDisableAddStoreCodeToUrls.path}} {{StorefrontDisableAddStoreCodeToUrls.value}}" + stepKey="disableAddStoreCodeToUrls" after="enableLoginAsCustomerAutoDetection"/> + </after> + <actionGroup ref="AssertStorefrontStoreCodeInUrlActionGroup" stepKey="seeCustomStoreCodeInUrl" after="assertCustomStoreView"> + <argument name="storeCode" value="{{customStore.code}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml new file mode 100644 index 0000000000000..da966fdcc1291 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerManualChooseTest.xml @@ -0,0 +1,67 @@ +<?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="AdminLoginAsCustomerManualChooseTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Select Store View based on 'Store View To Login In' setting"/> + <title + value="Admin user directly login into customer account with store View To Login In = Manual Choose"/> + <description + value="Verify admin user can directly login into customer account to Custom store view when Store View To Login In = Manual Choose"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="https://github.com/magento/magento2-login-as-customer/issues/58"/> + </skip> + </annotations> + <before> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 1" + stepKey="enableLoginAsCustomerManualChoose"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"/> + </before> + <after> + <actionGroup ref="AdminDeleteStoreViewActionGroup" stepKey="deleteCustomStoreView"> + <argument name="customStore" value="customStore"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageManualChooseActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + + <!-- Assert Customer logged on on custom store view --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerGird"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerOnStoreViewActionGroup" stepKey="assertCustomStoreView"> + <argument name="storeViewName" value="{{customStore.name}}"/> + </actionGroup> + + <!-- Log out Customer and close tab --> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml new file mode 100644 index 0000000000000..79c7571a08cfb --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerMultishippingLoggingTest.xml @@ -0,0 +1,102 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerMultishippingLoggingTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Place order and reorder"/> + <title value="Admin User login as Customer and place Order with Multiple Addresses"/> + <description value="Verify that Admin user can place Order with Multiple Addresses using 'Login as customer' functionality "/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + <group value="multishipping"/> + <skip> + <issueId value="https://github.com/magento/magento2-login-as-customer/pull/192"/> + </skip> + </annotations> + + <before> + <magentoCLI command="config:set {{EnableFreeShippingMethod.path}} {{EnableFreeShippingMethod.value}}" stepKey="enableFreeShipping"/> + <magentoCLI command="config:set {{EnableFlatRateShippingMethod.path}} {{EnableFlatRateShippingMethod.value}}" stepKey="enableFlatRateShipping"/> + <magentoCLI command="config:set {{EnableCheckMoneyOrderPaymentMethod.path}} {{EnableCheckMoneyOrderPaymentMethod.value}}" stepKey="enableCheckMoneyOrderPaymentMethod"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="SimpleProduct2" stepKey="createProduct1"/> + <createData entity="SimpleProduct2" stepKey="createProduct2"/> + <createData entity="Simple_US_Customer_Assistance_Allowed_Two_Addresses" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + + <after> + <deleteData createDataKey="createProduct1" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct2" stepKey="deleteProduct2"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearAllOrdersGridFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <magentoCLI command="config:set {{DisableFreeShippingMethod.path}} {{DisableFreeShippingMethod.value}}" stepKey="disableFreeShipping"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <!-- Add Products to Cart --> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProduct1ToCart"> + <argument name="product" value="$$createProduct1$$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToCartActionGroup" stepKey="addSimpleProduct2ToCart"> + <argument name="product" value="$$createProduct2$$"/> + </actionGroup> + + <!-- Place Order --> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCart"/> + <actionGroup ref="CheckingWithMultipleAddressesActionGroup" stepKey="checkoutWithMultipleAddresses"/> + <waitForPageLoad stepKey="waitForShippingInfoPageLoad"/> + <actionGroup ref="SelectMultiShippingInfoActionGroup" stepKey="checkoutWithMultipleShipping"/> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> + <actionGroup ref="SelectBillingInfoActionGroup" stepKey="checkoutWithPaymentMethod"/> + <waitForPageLoad stepKey="waitForReviewOrderPageLoad"/> + <actionGroup ref="ReviewOrderForMultiShipmentActionGroup" stepKey="reviewOrderForMultiShipment"> + <argument name="totalNameForFirstOrder" value="Shipping & Handling"/> + <argument name="totalPositionForFirstOrder" value="1"/> + <argument name="totalNameForSecondOrder" value="Shipping & Handling"/> + <argument name="totalPositionForSecondOrder" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForPlaceOrderPageLoad"/> + <actionGroup ref="StorefrontPlaceOrderForMultipleAddressesActionGroup" stepKey="placeOrder"> + <argument name="firstOrderPosition" value="1"/> + <argument name="secondOrderPosition" value="2"/> + </actionGroup> + <waitForPageLoad stepKey="waitForOrderPageLoad"/> + + <!-- Assert Storefront Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageFirstOrder"> + <argument name="orderId" value="{$getFirstOrderIdPlaceOrder}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageSecondOrder"> + <argument name="orderId" value="{$getSecondOrderIdPlaceOrder}"/> + </actionGroup> + + <!-- Assert Admin Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageFirstOrder"> + <argument name="orderId" value="{$getFirstOrderIdPlaceOrder}"/> + <argument name="adminUserFullName" value="Magento User"/> + </actionGroup> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageSecondOrder"> + <argument name="orderId" value="{$getSecondOrderIdPlaceOrder}"/> + <argument name="adminUserFullName" value="Magento User"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml new file mode 100644 index 0000000000000..8169b9df4c43d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerPlaceOrderTest.xml @@ -0,0 +1,94 @@ +<?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="AdminLoginAsCustomerPlaceOrderTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Place order and reorder"/> + <title value="Admin user login as customer and place order"/> + <description + value="Verify that admin user can place order using 'Login as customer' functionality"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + + <!-- Create new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateUserWithRoleActionGroup" stepKey="createAdminUser"> + <argument name="user" value="activeAdmin"/> + <argument name="role" value="roleDefaultAdministrator"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMasterAdmin"/> + + <!-- Login as new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToNewAdmin"> + <argument name="username" value="{{activeAdmin.username}}"/> + <argument name="password" value="{{activeAdmin.password}}"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Delete new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="activeAdmin"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Place Order as Customer --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createProduct.sku$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openCart"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderId"/> + + <!-- Assert Storefront Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageOrderCreatedByAdmin"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + + <!-- Assert Admin Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageOrderCreatedByAdmin"> + <argument name="orderId" value="{$grabOrderId}"/> + <argument name="adminUserFullName" value="{{activeAdmin.firstname}} {{activeAdmin.lastname}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml new file mode 100644 index 0000000000000..11d622319af33 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerReorderTest.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerReorderTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Place order and reorder"/> + <title value="Admin user login as customer and reorder existing order"/> + <description + value="Verify that admin user can reorder using 'Login as customer' functionality"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + + <!-- Create new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminCreateUserWithRoleActionGroup" stepKey="createAdminUser"> + <argument name="user" value="activeAdmin"/> + <argument name="role" value="roleDefaultAdministrator"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMasterAdmin"/> + + <!-- Login as new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginToNewAdmin"> + <argument name="username" value="{{activeAdmin.username}}"/> + <argument name="password" value="{{activeAdmin.password}}"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <!-- Delete new User --> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + <actionGroup ref="AdminDeleteUserActionGroup" stepKey="deleteUser"> + <argument name="user" value="activeAdmin"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login to storefront as Customer --> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Place Order as Customer --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createProduct.sku$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openCart"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderId"/> + + <!-- Log out from storefront as Customer --> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="customerLogOut"/> + + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Make reorder --> + <actionGroup ref="StorefrontCustomerReorderActionGroup" stepKey="makeReorder"> + <argument name="orderNumber" value="{$grabOrderId}"/> + </actionGroup> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeReorder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabReorderId"/> + + <!-- Assert Storefront Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="StorefrontAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyStorefrontMessageOrderCreatedByAdmin"> + <argument name="orderId" value="${grabReorderId}"/> + </actionGroup> + + <!-- Assert Admin Order page contains message about Order created by a Store Administrator --> + <actionGroup ref="AdminAssertContainsMessageOrderCreatedByAdminActionGroup" stepKey="verifyAdminMessageOrderCreatedByAdmin"> + <argument name="orderId" value="${grabReorderId}"/> + <argument name="adminUserFullName" value="{{activeAdmin.firstname}} {{activeAdmin.lastname}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.xml new file mode 100644 index 0000000000000..bc4c4adc3ac5a --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerSubscribeToNewsletterTest.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="AdminLoginAsCustomerSubscribeToNewsletterTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Subscribe to newsletter"/> + <title value="Admin user login as customer and make subscription to newsletter"/> + <description + value="Verify that Admin can subscribe to newsletter using Login as Customer functionality"/> + <severity value="AVERAGE"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Admin Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="lLoginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Subscribe for newsletter --> + <actionGroup ref="StorefrontCustomerNavigateToNewsletterPageActionGroup" stepKey="navigateToNewsletterPage"/> + <actionGroup ref="StorefrontCustomerUpdateGeneralSubscriptionActionGroup" stepKey="subscribeToNewsletter"/> + <actionGroup ref="AssertStorefrontCustomerMessagesActionGroup" stepKey="assertMessage"> + <argument name="message" value="We have saved your subscription."/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInAsCustomer"/> + + <!-- Verify subscription successful --> + <actionGroup ref="AdminOpenCustomerEditPageActionGroup" stepKey="openCustomerEditPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="AdminAssertCustomerIsSubscribedToNewsletters" stepKey="assertSubscribedToNewsletter"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml new file mode 100644 index 0000000000000..e7b5de55a56cb --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserLogoutTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerUserLogoutTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Destroy impersonated customer sessions on admin logout"/> + <title + value="Login as Customer sessions are ended/invalidated when the related admin session is logged out."/> + <description + value="Verify Login as Customer session is ended/invalidated when the related admin session is logged out."/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login into Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomer"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Assert correctly logged in as Customer --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + + <!-- End Admin session --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <!-- Assert Customer session invalidated --> + <actionGroup ref="StorefrontOpenMyAccountPageActionGroup" stepKey="openCustomerAccountPage"/> + <actionGroup ref="StorefrontAssertOnCustomerLoginPageActionGroup" stepKey="AssertOnCustomerLoginPage"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml new file mode 100644 index 0000000000000..5bbc218e0a948 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminLoginAsCustomerUserSingleSessionTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminLoginAsCustomerUserSingleSessionTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Destroy impersonated customer sessions on admin logout"/> + <title value="Admin users can have only one 'Login as Customer' session at a time"/> + <description + value="Verify Admin users can have only one 'Login as Customer' session at a time"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createFirstCustomer"/> + <createData entity="Simple_US_CA_Customer_Assistance_Allowed" stepKey="createSecondCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultUser"/> + </before> + <after> + <deleteData createDataKey="createFirstCustomer" stepKey="deleteFirstCustomer"/> + <deleteData createDataKey="createSecondCustomer" stepKey="deleteSecondCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAfter"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login into First Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsFirstCustomer"> + <argument name="customerId" value="$$createFirstCustomer.id$$"/> + </actionGroup> + + <!-- Assert correctly logged in as First Customer --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromFirstCustomerPage"> + <argument name="customerFullName" value="$$createFirstCustomer.firstname$$ $$createFirstCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createFirstCustomer.email$$"/> + </actionGroup> + + <!-- Login into Second Customer account --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsSecondCustomer"> + <argument name="customerId" value="$$createSecondCustomer.id$$"/> + </actionGroup> + + <!-- Assert correctly logged in as Second Customer --> + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromSecondCustomerPage"> + <argument name="customerFullName" value="$$createSecondCustomer.firstname$$ $$createSecondCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createSecondCustomer.email$$"/> + </actionGroup> + + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutSecondCustomer"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml new file mode 100644 index 0000000000000..50513797d06e9 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerButtonTest.xml @@ -0,0 +1,97 @@ +<?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="AdminNoAccessToLoginAsCustomerButtonTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="User does not have access to 'Login as customer' button"/> + <description value="Login into Magento Admin panel as user that does not have access to 'Login as customer' button"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="Allow Login as Customer Button"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsSaleRoleUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Verify Login as Customer Login action is absent on Customer page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnCustomerPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!-- Create order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderId"/> + + <!-- Verify Login as Customer Login action is absent on Order page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnOrderPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnOrderPage"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml new file mode 100644 index 0000000000000..d48f167656301 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminNoAccessToLoginAsCustomerConfigurationTest.xml @@ -0,0 +1,96 @@ +<?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="AdminNoAccessToLoginAsCustomerConfigurationTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Permissions and ACl"/> + <title value="User does not have access to 'Login as customer' section in System Configuration"/> + <description + value="Login into Magento Admin panel as user that does not have access to 'Login as customer' section in System Configuration"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="MQE-1964"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserBefore"/> + + <!--Create New Role--> + <actionGroup ref="AdminOpenCreateRolePageActionGroup" stepKey="goToNewRolePage"/> + <actionGroup ref="AdminFillUserRoleFormActionGroup" stepKey="fillNewRoleForm"> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + <actionGroup ref="AdminRevokeRoleResourceActionGroup" stepKey="revokeLoginAsCustomerAccess"> + <argument name="resourceName" value="Login as Customer Section"/> + </actionGroup> + <actionGroup ref="AdminClickSaveButtonOnUserRoleFormActionGroup" stepKey="saveNewRole"/> + + <!--Create New User--> + <actionGroup ref="AdminCreateUserWithApiRoleActionGroup" stepKey="adminCreateUser"> + <argument name="user" value="NewAdminUser"/> + <argument name="role" value="CustomRoleAllResources"/> + </actionGroup> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + + <!--Delete new User--> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutAsSaleRoleUser"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsDefaultAdminUserAfter"/> + <actionGroup ref="AdminDeleteCustomUserActionGroup" stepKey="deleteNewUser"> + <argument name="user" value="NewAdminUser"/> + </actionGroup> + + <!--Delete new Role--> + <actionGroup ref="AdminDeleteUserRoleActionGroup" stepKey="deleteCustomRoleAllResources"> + <argument name="roleName" value="{{CustomRoleAllResources.rolename}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> + + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as new User --> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logOutFromAdminPanel"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsNewUser"> + <argument name="username" value="{{NewAdminUser.username}}"/> + <argument name="password" value="{{NewAdminUser.password}}"/> + </actionGroup> + + <!-- Navigate to Configuration page and open Customers tab --> + <actionGroup ref="AdminOpenStoreConfigPageActionGroup" stepKey="openStoreConfig"/> + <actionGroup ref="AdminExpandConfigTabActionGroup" stepKey="expandCustomersTab"> + <argument name="tabName" value="Customers"/> + </actionGroup> + + <!-- Assert no Login as Customer config section visible --> + <actionGroup ref="AdminAssertLoginAsCustomerConfigNotVisibleActionGroup" stepKey="assertConfigNotVisible"/> + + <!-- Assert Login as Customer config section is not available by direct url --> + <actionGroup ref="AdminAssertLoginAsCustomerConfigNotAvailableDirectlyActionGroup" + stepKey="assertConfigNotAvailableDirectly"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml new file mode 100644 index 0000000000000..e1ea363bdf6bc --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUINotShownIfLoginAsCustomerDisabledTest.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUINotShownIfLoginAsCustomerDisabledTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="UI elements are not shown if 'Login as customer' functionality is disabled"/> + <description value="Verify that UI elements are not shown if 'Login as customer' functionality is disabled"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <!-- Verify Login as Customer Login action is absent on Customer page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnCustomerPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnCustomerPage"> + <argument name="customerId" value="$createCustomer.id$"/> + </actionGroup> + + <!-- Create order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderId"/> + + <!-- Verify Login as Customer Login action is absent on Order page --> + <actionGroup ref="AdminLoginAsCustomerAbsentOnOrderPageActionGroup" stepKey="verifyLoginAsCustomerAbsentOnOrderPage"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml new file mode 100644 index 0000000000000..ea06263901b9e --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/AdminUIShownIfLoginAsCustomerEnabledTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminUIShownIfLoginAsCustomerEnabledTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="UI elements are shown if 'Login as customer' functionality is enabled"/> + <description + value="Verify that UI elements are present and links are working if 'Login as customer' functionality enabled"/> + <severity value="BLOCKER"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Verify Login as Customer Login action works correctly from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="verifyLoginAsCustomerWorksOnCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromCustomerPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInFromCustomerPage"/> + + <!-- Create order --> + <actionGroup ref="CreateOrderActionGroup" stepKey="createOrder"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + <grabTextFrom selector="{{AdminOrderDetailsInformationSection.orderId}}" stepKey="grabOrderId"/> + <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> + <argument name="orderId" value="$grabOrderId"/> + </actionGroup> + + <!-- Verify Login as Customer Login action works correctly from Order page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromOrderPageActionGroup" + stepKey="verifyLoginAsCustomerWorksOnOrderPage"> + <argument name="orderId" value="$grabOrderId"/> + </actionGroup> + + <actionGroup ref="StorefrontAssertLoginAsCustomerLoggedInActionGroup" stepKey="assertLoggedInFromOrderPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + <argument name="customerEmail" value="$$createCustomer.email$$"/> + </actionGroup> + <actionGroup ref="StorefrontSignOutAndCloseTabActionGroup" stepKey="signOutAfterLoggedInFromOrderPage"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml new file mode 100644 index 0000000000000..4f85b9167fa54 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest.xml @@ -0,0 +1,112 @@ +<?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="StorefrontLoginAsCustomerBannerPresentOnAllPagesInSessionTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Notification banner appears on all pages in session"/> + <title value="Verify banner is persistent and appears during all page views in session"/> + <description value="Banner is persistent and appears on all pages in session"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <closeTab stepKey="closeLoginAsCustomerTab"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </after> + + <!-- Admin Login as Customer from Customer page and assert notification banner --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Go to Wishlist and assert notification banner --> + <amOnPage url="{{StorefrontCustomerWishlistPage.url}}" stepKey="amOnWishListPage"/> + <waitForPageLoad stepKey="waitForWishlistPageLoad"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnWishList"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Go to category page and assert notification banner --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnCategoryPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Go to product page and assert notification banner --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnProductPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Add product to cart and assert notification banner --> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontOpenCartFromMinicartActionGroup" stepKey="openCartPage"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnCartPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Proceed to checkout and assert notification banner --> + <click selector="{{CheckoutCartSummarySection.proceedToCheckout}}" stepKey="clickProceedToCheckout"/> + <waitForPageLoad stepKey="waitForProceedToCheckout"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerOnCheckoutPage"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Assert notification banner before place order --> + <actionGroup ref="CheckoutSelectFlatRateShippingMethodActionGroup" stepKey="selectFlatRateShippingMethod"/> + <waitForElementVisible selector="{{CheckoutShippingSection.next}}" time="30" stepKey="waitForNextButton"/> + <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNext"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerBeforePlaceOrder"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Assert notification banner after place order --> + <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectPaymentMethod"/> + <actionGroup ref="CheckoutPlaceOrderActionGroup" stepKey="customerPlaceOrder"> + <argument name="orderNumberMessage" value="CONST.successCheckoutOrderNumberMessage"/> + <argument name="emailYouMessage" value="CONST.successCheckoutEmailYouMessage"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBannerAfterPlaceOrder"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml new file mode 100644 index 0000000000000..351a3c569ce24 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerNotificationBannerTest.xml @@ -0,0 +1,52 @@ +<?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="StorefrontLoginAsCustomerNotificationBannerTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Availability of UI elements if module enable/disable"/> + <title value="Notification Banner is present on Storefront page"/> + <description + value="Verify that Notification Banner is present on page if 'Login as customer' functionality used"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <magentoCLI command="config:set {{LoginAsCustomerStoreViewLogin.path}} 0" + stepKey="enableLoginAsCustomerAutoDetection"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheBeforeTestRun"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <magentoCLI command="cache:flush config" stepKey="flushCacheAfterTestRun"/> + </after> + + <!-- Login as Customer from Customer page --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" + stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + + <!-- Assert Notification Banner is present on page --> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Log out Customer by Notification Banner and close tab --> + <actionGroup ref="StorefrontSignOutNotificationBannerAndCloseTabActionGroup" stepKey="signOutAndCloseTab"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml new file mode 100644 index 0000000000000..3e70da8f8158d --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest.xml @@ -0,0 +1,117 @@ +<?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="StorefrontLoginAsCustomerSeeSpecialPriceOnCategoryTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="'Login as Customer' should see special prices on a category page"/> + <title value="Special prices shown on category when Admin user Login as customer account"/> + <description value="Login as customer sees special prices on category"/> + <severity value="CRITICAL"/> + <group value="login_as_customer"/> + <skip> + <issueId value="https://github.com/magento/magento2-login-as-customer/pull/193"/> + </skip> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + <field key="price">10</field> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"> + <field key="group_id">3</field> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <amOnPage stepKey="goToPriceRulePage" url="{{CatalogRulePage.url}}"/> + <actionGroup stepKey="deletePriceRule" ref="deleteEntitySecondaryGrid"> + <argument name="name" value="{{_defaultCatalogRule.name}}"/> + <argument name="searchInput" value="{{AdminSecondaryGridSection.catalogRuleIdentifierSearch}}"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </after> + + <!-- Creating a new catalog price rule with 50 percent discount for Retailer customer group --> + <actionGroup ref="NewCatalogPriceRuleByUIWithConditionIsCategoryActionGroup" stepKey="newCatalogPriceRuleByUIWithConditionIsCategory"> + <argument name="categoryId" value="$createCategory.id$"/> + </actionGroup> + <actionGroup ref="SelectRetailerCustomerGroupActionGroup" stepKey="selectRetailerCustomerGroup"/> + <actionGroup ref="AdminCatalogPriceRuleFillActionsActionGroup" stepKey="fillActionsForCatalogPriceRule"> + <argument name="apply" value="{{CatalogRuleToPercent.simple_action}}"/> + <argument name="discountAmount" value="50"/> + </actionGroup> + + <!-- Save and apply the new catalog price rule --> + <actionGroup ref="SaveAndApplyCatalogPriceRuleActionGroup" stepKey="saveAndApplyCatalogPriceRule"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + + <!-- Admin Login as Customer --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Check simple product prices on store front category page --> + <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1Price"> + <argument name="productInfo" value="$5.00"/> + <argument name="productNumber" value="1"/> + </actionGroup> + <actionGroup ref="AssertProductDetailsOnStorefrontActionGroup" stepKey="storefrontProduct1RegularPrice"> + <argument name="productInfo" value="$10.00"/> + <argument name="productNumber" value="1"/> + </actionGroup> + + <!-- Place order --> + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrl" value="$$createProduct.sku$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openCart"/> + <actionGroup ref="PlaceOrderWithLoggedUserActionGroup" stepKey="placeOrder"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderId"/> + <closeTab stepKey="closeLoginAsCustomerTab"/> + + <!-- Open order in admin --> + <actionGroup ref="OpenOrderByIdActionGroup" stepKey="addFilterToGridAndOpenOrder"> + <argument name="orderId" value="{$grabOrderId}"/> + </actionGroup> + + <!-- Assert order subtotal --> + <scrollTo selector="{{AdminOrderTotalSection.subTotal}}" stepKey="scrollToOrderTotalSection"/> + <see selector="{{AdminOrderTotalSection.subTotal}}" userInput="$5.00" stepKey="checkOrderTotalInBackend"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml new file mode 100644 index 0000000000000..b2c7c6c35db18 --- /dev/null +++ b/app/code/Magento/LoginAsCustomer/Test/Mftf/Test/StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest.xml @@ -0,0 +1,67 @@ +<?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="StorefrontLoginAsCustomerShoppingCartIsNotMergedWithGuestCartTest"> + <annotations> + <features value="Login as Customer"/> + <stories value="Customer shopping cart shouldn't merge with guest shopping cart"/> + <title value="Customer shopping cart is not merged with guest shopping cart"/> + <description value="Shopping cart customer is not merged with guest cart"/> + <severity value="MAJOR"/> + <group value="login_as_customer"/> + </annotations> + <before> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 1" + stepKey="enableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="Simple_US_Customer_Assistance_Allowed" stepKey="createCustomer"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <closeTab stepKey="closeLoginAsCustomerTab"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> + <magentoCLI command="config:set {{LoginAsCustomerConfigDataEnabled.path}} 0" + stepKey="disableLoginAsCustomer"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushConfigCache"> + <argument name="tags" value="config"/> + </actionGroup> + </after> + + <!-- Add product to guest cart --> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createSimpleProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="addProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + + <!-- Admin Login as Customer --> + <actionGroup ref="AdminLoginAsCustomerLoginFromCustomerPageActionGroup" stepKey="loginAsCustomerFromCustomerPage"> + <argument name="customerId" value="$$createCustomer.id$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertLoginAsCustomerNotificationBannerActionGroup" stepKey="assertNotificationBanner"> + <argument name="customerFullName" value="$$createCustomer.firstname$$ $$createCustomer.lastname$$"/> + </actionGroup> + + <!-- Check the mini cart is empty --> + <actionGroup ref="AssertMiniCartEmptyActionGroup" stepKey="miniCartEmpty"/> + </test> +</tests> diff --git a/app/code/Magento/LoginAsCustomer/composer.json b/app/code/Magento/LoginAsCustomer/composer.json index ec81374528e7b..e58ec90e8f8bb 100755 --- a/app/code/Magento/LoginAsCustomer/composer.json +++ b/app/code/Magento/LoginAsCustomer/composer.json @@ -4,6 +4,7 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", + "magento/module-backend": "*", "magento/module-customer": "*", "magento/module-login-as-customer-api": "*" }, diff --git a/app/code/Magento/LoginAsCustomer/etc/config.xml b/app/code/Magento/LoginAsCustomer/etc/config.xml index 936ae1ff2f05d..7e39cc39145eb 100644 --- a/app/code/Magento/LoginAsCustomer/etc/config.xml +++ b/app/code/Magento/LoginAsCustomer/etc/config.xml @@ -10,7 +10,7 @@ <default> <login_as_customer> <general> - <enabled>0</enabled> + <enabled>1</enabled> <store_view_manual_choice_enabled>0</store_view_manual_choice_enabled> <authentication_data_expiration_time>60</authentication_data_expiration_time> </general> diff --git a/app/code/Magento/LoginAsCustomer/etc/di.xml b/app/code/Magento/LoginAsCustomer/etc/di.xml index c0ba4901ba7b8..9927237c51db6 100755 --- a/app/code/Magento/LoginAsCustomer/etc/di.xml +++ b/app/code/Magento/LoginAsCustomer/etc/di.xml @@ -5,14 +5,35 @@ * See COPYING.txt for license details. */ --> -<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <preference for="Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface" type="Magento\LoginAsCustomer\Model\AuthenticationData"/> - <preference for="Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\SaveAuthenticationData"/> - <preference for="Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\GetAuthenticationDataBySecret"/> - <preference for="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface" type="Magento\LoginAsCustomer\Model\AuthenticateCustomerBySecret"/> - <preference for="Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\DeleteAuthenticationDataForUser"/> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface" + type="Magento\LoginAsCustomer\Model\AuthenticationData"/> + <preference for="Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface" + type="Magento\LoginAsCustomer\Model\ResourceModel\SaveAuthenticationData"/> + <preference for="Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface" + type="Magento\LoginAsCustomer\Model\ResourceModel\GetAuthenticationDataBySecret"/> + <preference for="Magento\LoginAsCustomerApi\Api\AuthenticateCustomerBySecretInterface" + type="Magento\LoginAsCustomer\Model\AuthenticateCustomerBySecret"/> + <preference for="Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface" + type="Magento\LoginAsCustomer\Model\ResourceModel\DeleteAuthenticationDataForUser"/> <preference for="Magento\LoginAsCustomerApi\Api\ConfigInterface" type="Magento\LoginAsCustomer\Model\Config"/> <preference for="Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerSessionActiveInterface" type="Magento\LoginAsCustomer\Model\ResourceModel\IsLoginAsCustomerSessionActive"/> + <preference for="Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface" type="Magento\LoginAsCustomer\Model\GetLoggedAsCustomerAdminId"/> + <preference for="Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerCustomerIdInterface" type="Magento\LoginAsCustomer\Model\GetLoggedAsCustomerCustomerId"/> + <preference for="Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface" type="Magento\LoginAsCustomer\Model\SetLoggedAsCustomerAdminId"/> + <preference for="Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface" + type="Magento\LoginAsCustomer\Model\IsLoginAsCustomerEnabledForCustomerResult"/> + <preference for="Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface" type="Magento\LoginAsCustomer\Model\SetLoggedAsCustomerCustomerId"/> + <type name="Magento\LoginAsCustomerApi\Model\IsLoginAsCustomerEnabledForCustomerChain"> + <arguments> + <argument name="resolvers" xsi:type="array"> + <item name="is_enabled" xsi:type="object"> + Magento\LoginAsCustomer\Model\Resolver\IsLoginAsCustomerEnabledResolver + </item> + </argument> + </arguments> + </type> <type name="Magento\Backend\Model\Auth"> <plugin name="login_as_customer_admin_logout" type="Magento\LoginAsCustomer\Plugin\AdminLogoutPlugin"/> </type> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php index e2d11b2c8cb80..ce5a5501fbe55 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Block/Adminhtml/ConfirmationPopup.php @@ -16,6 +16,7 @@ * Login confirmation pop-up * * @api + * @since 100.4.0 */ class ConfirmationPopup extends Template { @@ -56,6 +57,7 @@ public function __construct( /** * @inheritdoc + * @since 100.4.0 */ public function getJsLayout() { @@ -78,6 +80,7 @@ public function getJsLayout() /** * @inheritdoc + * @since 100.4.0 */ public function toHtml() { diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php index 7ccdcfe45e482..39a7055ed65bb 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Controller/Adminhtml/Login/Login.php @@ -12,6 +12,7 @@ use Magento\Backend\Model\Auth\Session; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Controller\ResultInterface; @@ -22,7 +23,9 @@ use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterface; use Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataInterfaceFactory; use Magento\LoginAsCustomerApi\Api\DeleteAuthenticationDataForUserInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; use Magento\LoginAsCustomerApi\Api\SaveAuthenticationDataInterface; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerCustomerIdInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -80,6 +83,16 @@ class Login extends Action implements HttpGetActionInterface */ private $url; + /** + * @var SetLoggedAsCustomerCustomerIdInterface + */ + private $setLoggedAsCustomerCustomerId; + + /** + * @var IsLoginAsCustomerEnabledForCustomerInterface + */ + private $isLoginAsCustomerEnabled; + /** * @param Context $context * @param Session $authSession @@ -87,9 +100,13 @@ class Login extends Action implements HttpGetActionInterface * @param CustomerRepositoryInterface $customerRepository * @param ConfigInterface $config * @param AuthenticationDataInterfaceFactory $authenticationDataFactory - * @param SaveAuthenticationDataInterface $saveAuthenticationData , + * @param SaveAuthenticationDataInterface $saveAuthenticationData * @param DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser * @param Url $url + * @param SetLoggedAsCustomerCustomerIdInterface $setLoggedAsCustomerCustomerId + * @param IsLoginAsCustomerEnabledForCustomerInterface $isLoginAsCustomerEnabled + * + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( Context $context, @@ -100,7 +117,9 @@ public function __construct( AuthenticationDataInterfaceFactory $authenticationDataFactory, SaveAuthenticationDataInterface $saveAuthenticationData, DeleteAuthenticationDataForUserInterface $deleteAuthenticationDataForUser, - Url $url + Url $url, + ?SetLoggedAsCustomerCustomerIdInterface $setLoggedAsCustomerCustomerId = null, + ?IsLoginAsCustomerEnabledForCustomerInterface $isLoginAsCustomerEnabled = null ) { parent::__construct($context); @@ -112,6 +131,10 @@ public function __construct( $this->saveAuthenticationData = $saveAuthenticationData; $this->deleteAuthenticationDataForUser = $deleteAuthenticationDataForUser; $this->url = $url; + $this->setLoggedAsCustomerCustomerId = $setLoggedAsCustomerCustomerId + ?? ObjectManager::getInstance()->get(SetLoggedAsCustomerCustomerIdInterface::class); + $this->isLoginAsCustomerEnabled = $isLoginAsCustomerEnabled + ?? ObjectManager::getInstance()->get(IsLoginAsCustomerEnabledForCustomerInterface::class); } /** @@ -126,20 +149,24 @@ public function execute(): ResultInterface /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); - if (!$this->config->isEnabled()) { - $this->messageManager->addErrorMessage(__('Login as Customer is disabled.')); - return $resultRedirect->setPath('customer/index/index'); - } - $customerId = (int)$this->_request->getParam('customer_id'); if (!$customerId) { $customerId = (int)$this->_request->getParam('entity_id'); } + $isLoginAsCustomerEnabled = $this->isLoginAsCustomerEnabled->execute($customerId); + if (!$isLoginAsCustomerEnabled->isEnabled()) { + foreach ($isLoginAsCustomerEnabled->getMessages() as $message) { + $this->messageManager->addErrorMessage(__($message)); + } + + return $resultRedirect->setPath('customer/index/index'); + } + try { $customer = $this->customerRepository->getById($customerId); } catch (NoSuchEntityException $e) { - $this->messageManager->addErrorMessage(__('Customer with this ID are no longer exist.')); + $this->messageManager->addErrorMessage('Customer with this ID are no longer exist.'); return $resultRedirect->setPath('customer/index/index'); } @@ -167,6 +194,7 @@ public function execute(): ResultInterface $this->deleteAuthenticationDataForUser->execute($userId); $secret = $this->saveAuthenticationData->execute($authenticationData); + $this->setLoggedAsCustomerCustomerId->execute($customerId); $redirectUrl = $this->getLoginProceedRedirectUrl($secret, $storeId); $resultRedirect->setUrl($redirectUrl); diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php index 89ee2791e38af..c67b0d9dd5273 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Plugin/Button/ToolbarPlugin.php @@ -8,10 +8,10 @@ namespace Magento\LoginAsCustomerAdminUi\Plugin\Button; use Magento\Backend\Block\Widget\Button\ButtonList; -use Magento\Backend\Block\Widget\Button\Toolbar; -use Magento\Framework\View\Element\AbstractBlock; -use Magento\Framework\Escaper; use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Escaper; +use Magento\Framework\View\Element\AbstractBlock; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; use Magento\LoginAsCustomerApi\Api\ConfigInterface; /** @@ -34,38 +34,71 @@ class ToolbarPlugin */ private $config; + /** + * @var DataProvider + */ + private $dataProvider; + /** * ToolbarPlugin constructor. * @param AuthorizationInterface $authorization * @param ConfigInterface $config * @param Escaper $escaper + * @param DataProvider $dataProvider */ public function __construct( AuthorizationInterface $authorization, ConfigInterface $config, - Escaper $escaper + Escaper $escaper, + DataProvider $dataProvider ) { $this->authorization = $authorization; $this->config = $config; $this->escaper = $escaper; + $this->dataProvider = $dataProvider; } /** * Add Login as Customer button. * - * @param \Magento\Backend\Block\Widget\Button\Toolbar $subject + * @param \Magento\Backend\Block\Widget\Button\ToolbarInterface $subject * @param \Magento\Framework\View\Element\AbstractBlock $context * @param \Magento\Backend\Block\Widget\Button\ButtonList $buttonList * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforePushButtons( - Toolbar $subject, + \Magento\Backend\Block\Widget\Button\ToolbarInterface $subject, AbstractBlock $context, ButtonList $buttonList - ):void { - $order = false; + ): void { $nameInLayout = $context->getNameInLayout(); + $order = $this->getOrder($nameInLayout, $context); + if ($order + && !empty($order['customer_id']) + && $this->config->isEnabled() + && $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button') + ) { + $customerId = (int)$order['customer_id']; + $buttonList->add( + 'guest_to_customer', + $this->dataProvider->getData($customerId), + -1 + ); + } + } + + /** + * Extract order data from context. + * + * @param string $nameInLayout + * @param AbstractBlock $context + * @return array|null + */ + private function getOrder(string $nameInLayout, AbstractBlock $context) + { + $order = null; + if ('sales_order_edit' == $nameInLayout) { $order = $context->getOrder(); } elseif ('sales_invoice_view' == $nameInLayout) { @@ -75,28 +108,7 @@ public function beforePushButtons( } elseif ('sales_creditmemo_view' == $nameInLayout) { $order = $context->getCreditmemo()->getOrder(); } - if ($order) { - $isAllowed = $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); - $isEnabled = $this->config->isEnabled(); - if ($isAllowed && $isEnabled) { - if (!empty($order['customer_id'])) { - $buttonUrl = $context->getUrl('loginascustomer/login/login', [ - 'customer_id' => $order['customer_id'] - ]); - $buttonList->add( - 'guest_to_customer', - [ - 'label' => __('Login as Customer'), - 'onclick' => 'window.lacConfirmationPopup("' - . $this->escaper->escapeHtml($this->escaper->escapeJs($buttonUrl)) - . '")', - 'class' => 'reset' - ], - -1 - ); - } - } - } + return $order; } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php new file mode 100644 index 0000000000000..24a70fc429467 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Button/DataProvider.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button; + +use Magento\Framework\Escaper; +use Magento\Framework\UrlInterface; + +/** + * Get data for Login as Customer button. + * + * Use this class as a base for virtual types declaration. + */ +class DataProvider +{ + /** + * @var Escaper + */ + private $escaper; + + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @var array + */ + private $data; + + /** + * @param Escaper $escaper + * @param UrlInterface $urlBuilder + * @param array $data + */ + public function __construct( + Escaper $escaper, + UrlInterface $urlBuilder, + array $data = [] + ) { + $this->escaper = $escaper; + $this->urlBuilder = $urlBuilder; + $this->data = $data; + } + + /** + * Get data for Login as Customer button. + * + * @param int $customerId + * @return array + */ + public function getData(int $customerId): array + { + $buttonData = [ + 'on_click' => 'window.lacConfirmationPopup("' + . $this->escaper->escapeHtml($this->escaper->escapeJs($this->getLoginUrl($customerId))) + . '")', + ]; + + return array_merge_recursive($buttonData, $this->data); + } + + /** + * Get Login as Customer login url. + * + * @param int $customerId + * @return string + */ + private function getLoginUrl(int $customerId): string + { + return $this->urlBuilder->getUrl('loginascustomer/login/login', ['customer_id' => $customerId]); + } +} diff --git a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php index 0f8f7750262f2..ab43fca3d447e 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php +++ b/app/code/Magento/LoginAsCustomerAdminUi/Ui/Customer/Component/Control/LoginAsCustomerButton.php @@ -7,12 +7,13 @@ namespace Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control; -use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; -use Magento\Framework\AuthorizationInterface; -use Magento\Framework\Escaper; -use Magento\Framework\Registry; use Magento\Backend\Block\Widget\Context; use Magento\Customer\Block\Adminhtml\Edit\GenericButton; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; use Magento\LoginAsCustomerApi\Api\ConfigInterface; /** @@ -31,26 +32,26 @@ class LoginAsCustomerButton extends GenericButton implements ButtonProviderInter private $config; /** - * Escaper - * - * @var Escaper + * @var DataProvider */ - private $escaper; + private $dataProvider; /** * @param Context $context * @param Registry $registry * @param ConfigInterface $config + * @param DataProvider $dataProvider */ public function __construct( Context $context, Registry $registry, - ConfigInterface $config + ConfigInterface $config, + ?DataProvider $dataProvider = null ) { parent::__construct($context, $registry); $this->authorization = $context->getAuthorization(); $this->config = $config; - $this->escaper = $context->getEscaper(); + $this->dataProvider = $dataProvider ?? ObjectManager::getInstance()->get(DataProvider::class); } /** @@ -58,31 +59,14 @@ public function __construct( */ public function getButtonData(): array { - $customerId = $this->getCustomerId(); + $customerId = (int)$this->getCustomerId(); $data = []; $isAllowed = $customerId && $this->authorization->isAllowed('Magento_LoginAsCustomer::login_button'); $isEnabled = $this->config->isEnabled(); if ($isAllowed && $isEnabled) { - $data = [ - 'label' => __('Login as Customer'), - 'class' => 'login login-button', - 'on_click' => 'window.lacConfirmationPopup("' - . $this->escaper->escapeHtml($this->escaper->escapeJs($this->getLoginUrl())) - . '")', - 'sort_order' => 70, - ]; + $data = $this->dataProvider->getData($customerId); } return $data; } - - /** - * Get Login as Customer login url. - * - * @return string - */ - public function getLoginUrl(): string - { - return $this->getUrl('loginascustomer/login/login', ['customer_id' => $this->getCustomerId()]); - } } diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml index dabab45205527..b73a1d856c888 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/adminhtml/di.xml @@ -6,7 +6,17 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Backend\Block\Widget\Button\Toolbar"> + <type name="Magento\Backend\Block\Widget\Button\ToolbarInterface"> <plugin name="login_as_customer_button_toolbar" type="Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin"/> </type> + <type name="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control\LoginAsCustomerButton"> + <arguments> + <argument name="dataProvider" xsi:type="object">Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control\LoginAsCustomerButton\DataProvider</argument> + </arguments> + </type> + <type name="Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin"> + <arguments> + <argument name="dataProvider" xsi:type="object">Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin\DataProvider</argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/csp_whitelist.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..b023d2adf03fd --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/csp_whitelist.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="braintree_js_gateway" type="host">js.braintreegateway.com</value> + <value id="paypal_tag_gateway" type="host">www.paypal.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="paypal_analytics" type="host">t.paypal.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml b/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml new file mode 100644 index 0000000000000..8ba8c5c6ead43 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAdminUi/etc/di.xml @@ -0,0 +1,31 @@ +<?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"> + <!-- Move to adminhtml area after https://github.com/magento/magento2/issues/17825 is fixed. --> + <virtualType name="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Control\LoginAsCustomerButton\DataProvider" + type="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="label" xsi:type="string" translatable="true">Login as Customer</item> + <item name="class" xsi:type="string">login login-button</item> + <item name="sort_order" xsi:type="number">15</item> + </argument> + </arguments> + </virtualType> + <!-- Move to adminhtml area after https://github.com/magento/magento2/issues/17825 is fixed. --> + <virtualType name="Magento\LoginAsCustomerAdminUi\Plugin\Button\ToolbarPlugin\DataProvider" + type="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider"> + <arguments> + <argument name="data" xsi:type="array"> + <item name="label" xsi:type="string" translatable="true">Login as Customer</item> + <item name="class" xsi:type="string">reset</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less index 2901f95f0e279..d702bc49f23ed 100644 --- a/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less +++ b/app/code/Magento/LoginAsCustomerAdminUi/view/adminhtml/web/css/source/_module.less @@ -39,4 +39,14 @@ } } } + + .page-actions { + .page-actions-buttons { + .login-button { + -ms-flex-order: -1; + -webkit-order: -1; + order: -1; + } + } + } } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php index aadcc8e1b566b..7048fa5a9e418 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/ConfigInterface.php @@ -11,6 +11,7 @@ * LoginAsCustomer config * * @api + * @since 100.4.0 */ interface ConfigInterface { @@ -18,6 +19,7 @@ interface ConfigInterface * Check if Login as Customer extension is enabled * * @return bool + * @since 100.4.0 */ public function isEnabled(): bool; @@ -25,6 +27,7 @@ public function isEnabled(): bool; * Check if store view manual choice is enabled * * @return bool + * @since 100.4.0 */ public function isStoreManualChoiceEnabled(): bool; @@ -32,6 +35,7 @@ public function isStoreManualChoiceEnabled(): bool; * Get authentication data expiration time (in seconds) * * @return int + * @since 100.4.0 */ public function getAuthenticationDataExpirationTime(): int; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php index f74f63c39f7ba..1126de140192a 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/Data/AuthenticationDataInterface.php @@ -13,6 +13,7 @@ * Authentication data * * @api + * @since 100.4.0 */ interface AuthenticationDataInterface extends ExtensibleDataInterface { @@ -20,6 +21,7 @@ interface AuthenticationDataInterface extends ExtensibleDataInterface * Get Customer Id * * @return int + * @since 100.4.0 */ public function getCustomerId(): int; @@ -27,6 +29,7 @@ public function getCustomerId(): int; * Get Admin Id * * @return int + * @since 100.4.0 */ public function getAdminId(): int; @@ -36,6 +39,7 @@ public function getAdminId(): int; * Fully qualified namespaces is needed for proper work of ccode generation * * @return \Magento\LoginAsCustomerApi\Api\Data\AuthenticationDataExtensionInterface|null + * @since 100.4.0 */ public function getExtensionAttributes(): ?AuthenticationDataExtensionInterface; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php new file mode 100644 index 0000000000000..b7d3a616176ef --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/Data/IsLoginAsCustomerEnabledForCustomerResultInterface.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api\Data; + +/** + * IsLoginAsCustomerEnabledForCustomerInterface results. + */ +interface IsLoginAsCustomerEnabledForCustomerResultInterface +{ + /** + * Check if no validation failures occurred. + * + * @return bool + */ + public function isEnabled(): bool; + + /** + * Get error messages as array in case of validation failure, else return empty array. + * + * @return string[] + */ + public function getMessages(): array; + + /** + * Set error messages as array in case of validation failure. + * + * @param string[] $messages + */ + public function setMessages(array $messages): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php index 47e8d72b3b47e..b5c7405d05cdd 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/GetAuthenticationDataBySecretInterface.php @@ -14,6 +14,7 @@ * Get authentication data by secret * * @api + * @since 100.4.0 */ interface GetAuthenticationDataBySecretInterface { @@ -23,6 +24,7 @@ interface GetAuthenticationDataBySecretInterface * @param string $secret * @return AuthenticationDataInterface * @throws LocalizedException + * @since 100.4.0 */ public function execute(string $secret): AuthenticationDataInterface; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerAdminIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerAdminIdInterface.php new file mode 100644 index 0000000000000..49c0f796be006 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerAdminIdInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Get id of Admin logged as Customer. + */ +interface GetLoggedAsCustomerAdminIdInterface +{ + /** + * Get id of Admin logged as Customer. + * + * @return int + */ + public function execute(): int; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerCustomerIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerCustomerIdInterface.php new file mode 100644 index 0000000000000..047061b3edd69 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/GetLoggedAsCustomerCustomerIdInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Get id of Customer Admin is logged as. + */ +interface GetLoggedAsCustomerCustomerIdInterface +{ + /** + * Get id of Customer Admin is logged as. + * + * @return int + */ + public function execute(): int; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php new file mode 100644 index 0000000000000..a5355fd4566d5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerEnabledForCustomerInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; + +/** + * Check if Login as Customer functionality is enabled for Customer. + */ +interface IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * Check if Login as Customer functionality is enabled for Customer. + * + * @param int $customerId + * @return IsLoginAsCustomerEnabledForCustomerResultInterface + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php index 30674375ed021..6e3688e586c7c 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/IsLoginAsCustomerSessionActiveInterface.php @@ -11,6 +11,7 @@ * Check if Login as Customer session is still active. * * @api + * @since 100.4.0 */ interface IsLoginAsCustomerSessionActiveInterface { @@ -20,6 +21,7 @@ interface IsLoginAsCustomerSessionActiveInterface * @param int $customerId * @param int $userId * @return bool + * @since 100.4.0 */ public function execute(int $customerId, int $userId): bool; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php index 88d4cb8056cf6..1d828d7f802c0 100644 --- a/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php +++ b/app/code/Magento/LoginAsCustomerApi/Api/SaveAuthenticationDataInterface.php @@ -14,6 +14,7 @@ * Save authentication data. Return secret key * * @api + * @since 100.4.0 */ interface SaveAuthenticationDataInterface { @@ -23,6 +24,7 @@ interface SaveAuthenticationDataInterface * @param Data\AuthenticationDataInterface $authenticationData * @return string * @throws LocalizedException + * @since 100.4.0 */ public function execute(AuthenticationDataInterface $authenticationData): string; } diff --git a/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerAdminIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerAdminIdInterface.php new file mode 100644 index 0000000000000..b921c2fc6e8d3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerAdminIdInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Set id of Admin logged as Customer. + */ +interface SetLoggedAsCustomerAdminIdInterface +{ + /** + * Set id of Admin logged as Customer. + * + * @param int $adminId + * @return void + */ + public function execute(int $adminId): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerCustomerIdInterface.php b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerCustomerIdInterface.php new file mode 100644 index 0000000000000..265ae1aa36c45 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Api/SetLoggedAsCustomerCustomerIdInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Api; + +/** + * Set id of Customer Admin is logged as. + */ +interface SetLoggedAsCustomerCustomerIdInterface +{ + /** + * Set id of Customer Admin is logged as. + * + * @param int $customerId + * @return void + */ + public function execute(int $customerId): void; +} diff --git a/app/code/Magento/LoginAsCustomerApi/Model/IsLoginAsCustomerEnabledForCustomerChain.php b/app/code/Magento/LoginAsCustomerApi/Model/IsLoginAsCustomerEnabledForCustomerChain.php new file mode 100644 index 0000000000000..c852327743760 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/Model/IsLoginAsCustomerEnabledForCustomerChain.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerApi\Model; + +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerEnabledForCustomerChain implements IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory + */ + private $resultFactory; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterface[] + */ + private $resolvers; + + /** + * @param ConfigInterface $config + * @param IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + * @param array $resolvers + */ + public function __construct( + ConfigInterface $config, + IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory, + array $resolvers = [] + ) { + $this->config = $config; + $this->resultFactory = $resultFactory; + $this->resolvers = $resolvers; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = [[]]; + /** @var IsLoginAsCustomerEnabledForCustomerInterface $resolver */ + foreach ($this->resolvers as $resolver) { + $resolverResult = $resolver->execute($customerId); + if (!$resolverResult->isEnabled()) { + $messages[] = $resolverResult->getMessages(); + } + } + + return $this->resultFactory->create(['messages' => array_merge(...$messages)]); + } +} diff --git a/app/code/Magento/LoginAsCustomerApi/etc/di.xml b/app/code/Magento/LoginAsCustomerApi/etc/di.xml new file mode 100644 index 0000000000000..18915f8f16267 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerApi/etc/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface" + type="Magento\LoginAsCustomerApi\Model\IsLoginAsCustomerEnabledForCustomerChain"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/Api/ConfigInterface.php b/app/code/Magento/LoginAsCustomerAssistance/Api/ConfigInterface.php new file mode 100644 index 0000000000000..7cd54567d26d5 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Api/ConfigInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Api; + +/** + * LoginAsCustomerAssistance config. + */ +interface ConfigInterface +{ + /** + * Get title for shopping assistance checkbox. + * + * @return string + */ + public function getShoppingAssistanceCheckboxTitle(): string; + + /** + * Get tooltip for shopping assistance checkbox. + * + * @return string + */ + public function getShoppingAssistanceCheckboxTooltip(): string; +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Api/IsAssistanceEnabledInterface.php b/app/code/Magento/LoginAsCustomerAssistance/Api/IsAssistanceEnabledInterface.php new file mode 100644 index 0000000000000..916d03477a5d3 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Api/IsAssistanceEnabledInterface.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Api; + +/** + * Get 'assistance_allowed' attribute from Customer. + */ +interface IsAssistanceEnabledInterface +{ + /** + * Merchant assistance denied by customer status code. + */ + public const DENIED = 1; + + /** + * Merchant assistance allowed by customer status code. + */ + public const ALLOWED = 2; + + /** + * Get 'assistance_allowed' attribute from Customer by id. + * + * @param int $customerId + * @return bool + */ + public function execute(int $customerId): bool; +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Api/SetAssistanceInterface.php b/app/code/Magento/LoginAsCustomerAssistance/Api/SetAssistanceInterface.php new file mode 100644 index 0000000000000..ce8d2020341be --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Api/SetAssistanceInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Api; + +/** + * Set 'assistance_allowed' attribute to Customer. + */ +interface SetAssistanceInterface +{ + /** + * Set 'assistance_allowed' attribute to Customer by id. + * + * @param int $customerId + * @param bool $isEnabled + */ + public function execute(int $customerId, bool $isEnabled): void; +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php b/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php new file mode 100644 index 0000000000000..547be1de5a008 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Block/Adminhtml/NotAllowedPopup.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Block\Adminhtml; + +use Magento\Backend\Block\Template; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; + +/** + * Pop-up for Login as Customer button then Login as Customer is not allowed. + * + * @api + */ +class NotAllowedPopup extends Template +{ + /** + * Config + * + * @var ConfigInterface + */ + private $config; + + /** + * Json Serializer + * + * @var Json + */ + private $json; + + /** + * @param Template\Context $context + * @param ConfigInterface $config + * @param Json $json + * @param array $data + */ + public function __construct( + Template\Context $context, + ConfigInterface $config, + Json $json, + array $data = [] + ) { + parent::__construct($context, $data); + $this->config = $config; + $this->json = $json; + } + + /** + * @inheritdoc + */ + public function getJsLayout() + { + $layout = $this->json->unserialize(parent::getJsLayout()); + + $layout['components']['lac-not-allowed-popup']['title'] = __('Login as Customer not enabled'); + $layout['components']['lac-not-allowed-popup']['content'] = __( + 'The user has not enabled the "Allow remote shopping assistance" functionality. ' + . 'Contact the customer to discuss this user configuration.' + ); + + return $this->json->serialize($layout); + } + + /** + * @inheritdoc + */ + public function toHtml() + { + if (!$this->config->isEnabled()) { + return ''; + } + return parent::toHtml(); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/LICENSE.txt b/app/code/Magento/LoginAsCustomerAssistance/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomerAssistance/LICENSE_AFL.txt b/app/code/Magento/LoginAsCustomerAssistance/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/Config.php b/app/code/Magento/LoginAsCustomerAssistance/Model/Config.php new file mode 100644 index 0000000000000..2fce39cd4e85e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/Config.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\LoginAsCustomerAssistance\Api\ConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * @inheritdoc + */ +class Config implements ConfigInterface +{ + /** + * Extension config path + */ + private const XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TITLE + = 'login_as_customer/general/shopping_assistance_checkbox_title'; + private const XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TOOLTIP + = 'login_as_customer/general/shopping_assistance_checkbox_tooltip'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * @inheritdoc + */ + public function getShoppingAssistanceCheckboxTitle(): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TITLE, + ScopeInterface::SCOPE_WEBSITE + ); + } + + /** + * @inheritdoc + */ + public function getShoppingAssistanceCheckboxTooltip(): string + { + return (string)$this->scopeConfig->getValue( + self::XML_PATH_SHOPPING_ASSISTANCE_CHECKBOX_TOOLTIP, + ScopeInterface::SCOPE_WEBSITE + ); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/IsAssistanceEnabled.php b/app/code/Magento/LoginAsCustomerAssistance/Model/IsAssistanceEnabled.php new file mode 100644 index 0000000000000..da77c96164228 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/IsAssistanceEnabled.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerExtensionFactory; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\GetLoginAsCustomerAssistanceAllowed; + +/** + * Check if customer allows Login as Customer assistance. + */ +class IsAssistanceEnabled implements IsAssistanceEnabledInterface +{ + /** + * @var array + */ + private $registry = []; + + /** + * @var GetLoginAsCustomerAssistanceAllowed + */ + private $getLoginAsCustomerAssistanceAllowed; + + /** + * @param GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + */ + public function __construct( + GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + ) { + $this->getLoginAsCustomerAssistanceAllowed = $getLoginAsCustomerAssistanceAllowed; + } + + /** + * Check if customer allows Login as Customer assistance by Customer id. + * + * @param int $customerId + * @return bool + */ + public function execute(int $customerId): bool + { + if (!isset($this->registry[$customerId])) { + $this->registry[$customerId] = $this->getLoginAsCustomerAssistanceAllowed->execute($customerId); + } + + return $this->registry[$customerId]; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/Processor/IsLoginAsCustomerAllowedResolver.php b/app/code/Magento/LoginAsCustomerAssistance/Model/Processor/IsLoginAsCustomerAllowedResolver.php new file mode 100644 index 0000000000000..966ea477b2394 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/Processor/IsLoginAsCustomerAllowedResolver.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\Processor; + +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory; +use Magento\LoginAsCustomerApi\Api\Data\IsLoginAsCustomerEnabledForCustomerResultInterface; +use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerEnabledForCustomerInterface; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; + +/** + * @inheritdoc + */ +class IsLoginAsCustomerAllowedResolver implements IsLoginAsCustomerEnabledForCustomerInterface +{ + /** + * @var IsAssistanceEnabledInterface + */ + private $isAssistanceEnabled; + + /** + * @var IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory + */ + private $resultFactory; + + /** + * @param IsAssistanceEnabledInterface $isAssistanceEnabled + * @param IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + */ + public function __construct( + IsAssistanceEnabledInterface $isAssistanceEnabled, + IsLoginAsCustomerEnabledForCustomerResultInterfaceFactory $resultFactory + ) { + $this->isAssistanceEnabled = $isAssistanceEnabled; + $this->resultFactory = $resultFactory; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId): IsLoginAsCustomerEnabledForCustomerResultInterface + { + $messages = []; + if (!$this->isAssistanceEnabled->execute($customerId)) { + $messages[] = __('Login as Customer assistance is disabled for this Customer.'); + } + + return $this->resultFactory->create(['messages' => $messages]); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/DeleteLoginAsCustomerAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/DeleteLoginAsCustomerAssistanceAllowed.php new file mode 100644 index 0000000000000..360f2e2799282 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/DeleteLoginAsCustomerAssistanceAllowed.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; + +/** + * Delete Login as Customer assistance allowed record. + */ +class DeleteLoginAsCustomerAssistanceAllowed +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Delete Login as Customer assistance allowed record by Customer id. + * + * @param int $customerId + * @return void + */ + public function execute(int $customerId): void + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer_assistance_allowed'); + + $connection->delete( + $tableName, + [ + 'customer_id = ?' => $customerId + ] + ); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/GetLoginAsCustomerAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/GetLoginAsCustomerAssistanceAllowed.php new file mode 100644 index 0000000000000..412fd86351988 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/GetLoginAsCustomerAssistanceAllowed.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; + +/** + * Get Login as Customer assistance allowed record. + */ +class GetLoginAsCustomerAssistanceAllowed +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Get Login as Customer assistance allowed record by Customer id. + * + * @param int $customerId + * @return bool + */ + public function execute(int $customerId): bool + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer_assistance_allowed'); + + $select = $connection->select() + ->from( + $tableName + ) + ->where('customer_id = ?', $customerId); + + return !!$connection->fetchOne($select); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/SaveLoginAsCustomerAssistanceAllowed.php b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/SaveLoginAsCustomerAssistanceAllowed.php new file mode 100644 index 0000000000000..c3b396bbe332d --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/ResourceModel/SaveLoginAsCustomerAssistanceAllowed.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; + +/** + * Save Login as Customer assistance allowed record. + */ +class SaveLoginAsCustomerAssistanceAllowed +{ + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @param ResourceConnection $resourceConnection + */ + public function __construct( + ResourceConnection $resourceConnection + ) { + $this->resourceConnection = $resourceConnection; + } + + /** + * Save Login as Customer assistance allowed record by Customer id. + * + * @param int $customerId + * @return void + */ + public function execute(int $customerId): void + { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName('login_as_customer_assistance_allowed'); + + $connection->insertOnDuplicate( + $tableName, + [ + 'customer_id' => $customerId + ] + ); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Model/SetAssistance.php b/app/code/Magento/LoginAsCustomerAssistance/Model/SetAssistance.php new file mode 100644 index 0000000000000..9131599d9cba0 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Model/SetAssistance.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerExtensionFactory; +use Magento\LoginAsCustomerAssistance\Api\SetAssistanceInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\DeleteLoginAsCustomerAssistanceAllowed; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\SaveLoginAsCustomerAssistanceAllowed; + +/** + * @inheritdoc + */ +class SetAssistance implements SetAssistanceInterface +{ + /** + * @var array + */ + private $registry = []; + + /** + * @var CustomerExtensionFactory + */ + private $customerExtensionFactory; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var DeleteLoginAsCustomerAssistanceAllowed + */ + private $deleteLoginAsCustomerAssistanceAllowed; + + /** + * @var SaveLoginAsCustomerAssistanceAllowed + */ + private $saveLoginAsCustomerAssistanceAllowed; + + /** + * @param CustomerExtensionFactory $customerExtensionFactory + * @param CustomerRepositoryInterface $customerRepository + * @param DeleteLoginAsCustomerAssistanceAllowed $deleteLoginAsCustomerAssistanceAllowed + * @param SaveLoginAsCustomerAssistanceAllowed $saveLoginAsCustomerAssistanceAllowed + */ + public function __construct( + CustomerExtensionFactory $customerExtensionFactory, + CustomerRepositoryInterface $customerRepository, + DeleteLoginAsCustomerAssistanceAllowed $deleteLoginAsCustomerAssistanceAllowed, + SaveLoginAsCustomerAssistanceAllowed $saveLoginAsCustomerAssistanceAllowed + ) { + $this->customerExtensionFactory = $customerExtensionFactory; + $this->customerRepository = $customerRepository; + $this->deleteLoginAsCustomerAssistanceAllowed = $deleteLoginAsCustomerAssistanceAllowed; + $this->saveLoginAsCustomerAssistanceAllowed = $saveLoginAsCustomerAssistanceAllowed; + } + + /** + * @inheritdoc + */ + public function execute(int $customerId, bool $isEnabled): void + { + if ($this->isUpdateRequired($customerId, $isEnabled)) { + if ($isEnabled) { + $this->saveLoginAsCustomerAssistanceAllowed->execute($customerId); + } else { + $this->deleteLoginAsCustomerAssistanceAllowed->execute($customerId); + } + $this->updateRegistry($customerId, $isEnabled); + } + } + + /** + * Check if 'assistance_allowed' cached value differs from actual. + * + * @param int $customerId + * @param bool $isEnabled + * @return bool + */ + private function isUpdateRequired(int $customerId, bool $isEnabled): bool + { + return !isset($this->registry[$customerId]) || $this->registry[$customerId] !== $isEnabled; + } + + /** + * Update 'assistance_allowed' cached value. + * + * @param int $customerId + * @param bool $isEnabled + */ + private function updateRegistry(int $customerId, bool $isEnabled): void + { + $this->registry[$customerId] = $isEnabled; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerDataValidatePlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerDataValidatePlugin.php new file mode 100644 index 0000000000000..9da329b4a3991 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerDataValidatePlugin.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Model\Metadata\Form; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\AuthorizationInterface; +use Magento\Framework\Message\MessageInterface; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\GetLoginAsCustomerAssistanceAllowed; + +/** + * Check if User have permission to change Customers Opt-In preference. + */ +class CustomerDataValidatePlugin +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var GetLoginAsCustomerAssistanceAllowed + */ + private $getLoginAsCustomerAssistanceAllowed; + + /** + * @param AuthorizationInterface $authorization + * @param GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + */ + public function __construct( + AuthorizationInterface $authorization, + GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + ) { + $this->authorization = $authorization; + $this->getLoginAsCustomerAssistanceAllowed = $getLoginAsCustomerAssistanceAllowed; + } + + /** + * Check if User have permission to change Customers Opt-In preference. + * + * @param Form $subject + * @param RequestInterface $request + * @param null|string $scope + * @param bool $scopeOnly + * @throws \Magento\Framework\Validator\Exception + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExtractData( + Form $subject, + RequestInterface $request, + $scope = null, + $scopeOnly = true + ): void { + if ($this->isSetAssistanceAllowedParam($request) + && !$this->authorization->isAllowed('Magento_LoginAsCustomer::allow_shopping_assistance') + ) { + $customerId = $request->getParam('customer_id'); + $assistanceAllowedParam = + (int)$request->getParam('customer')['extension_attributes']['assistance_allowed']; + $assistanceAllowed = $this->getLoginAsCustomerAssistanceAllowed->execute((int)$customerId); + $assistanceAllowedStatus = $this->resolveStatus($assistanceAllowed); + if ($this->isAssistanceAllowedChangeImportant($assistanceAllowedStatus, $assistanceAllowedParam)) { + $errorMessages = [ + MessageInterface::TYPE_ERROR => [ + __( + 'You have no permission to change Opt-In preference.' + ), + ], + ]; + + throw new \Magento\Framework\Validator\Exception( + null, + null, + $errorMessages + ); + } + } + } + + /** + * Check if assistance_allowed param is set. + * + * @param RequestInterface $request + * @return bool + */ + private function isSetAssistanceAllowedParam(RequestInterface $request): bool + { + return is_array($request->getParam('customer')) + && isset($request->getParam('customer')['extension_attributes']) + && isset($request->getParam('customer')['extension_attributes']['assistance_allowed']); + } + + /** + * Check if change of assistance_allowed attribute is important. + * + * E. g. if assistance_allowed is going to be disabled while now it's enabled + * or if it's going to be enabled while now it is disabled or not set at all. + * + * @param int $assistanceAllowed + * @param int $assistanceAllowedParam + * @return bool + */ + private function isAssistanceAllowedChangeImportant(int $assistanceAllowed, int $assistanceAllowedParam): bool + { + $result = false; + if (($assistanceAllowedParam === IsAssistanceEnabledInterface::DENIED + && $assistanceAllowed === IsAssistanceEnabledInterface::ALLOWED) + || + ($assistanceAllowedParam === IsAssistanceEnabledInterface::ALLOWED + && $assistanceAllowed !== IsAssistanceEnabledInterface::ALLOWED)) { + $result = true; + } + + return $result; + } + + /** + * Get integer status value from boolean. + * + * @param bool $assistanceAllowed + * @return int + */ + private function resolveStatus(bool $assistanceAllowed): int + { + return $assistanceAllowed ? IsAssistanceEnabledInterface::ALLOWED : IsAssistanceEnabledInterface::DENIED; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerExtractorPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerExtractorPlugin.php new file mode 100644 index 0000000000000..619036da8bb22 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerExtractorPlugin.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Api\Data\CustomerExtensionFactory; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerExtractor; +use Magento\Framework\App\RequestInterface; + +/** + * Plugin for Magento\Customer\Model\CustomerExtractor. + */ +class CustomerExtractorPlugin +{ + /** + * @var CustomerExtensionFactory + */ + private $customerExtensionFactory; + + /** + * @param CustomerExtensionFactory $customerExtensionFactory + */ + public function __construct( + CustomerExtensionFactory $customerExtensionFactory + ) { + $this->customerExtensionFactory = $customerExtensionFactory; + } + + /** + * Add assistance_allowed extension attribute value to Customer instance. + * + * @param CustomerExtractor $subject + * @param callable $proceed + * @param string $formCode + * @param RequestInterface $request + * @param array $attributeValues + * @return CustomerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundExtract( + CustomerExtractor $subject, + callable $proceed, + string $formCode, + RequestInterface $request, + array $attributeValues = [] + ) { + /** @var CustomerInterface $customer */ + $customer = $proceed( + $formCode, + $request, + $attributeValues + ); + + $assistanceAllowedStatus = $request->getParam('assistance_allowed'); + if (!empty($assistanceAllowedStatus)) { + $extensionAttributes = $customer->getExtensionAttributes(); + if (null === $extensionAttributes) { + $extensionAttributes = $this->customerExtensionFactory->create(); + } + $extensionAttributes->setAssistanceAllowed((int)$assistanceAllowedStatus); + $customer->setExtensionAttributes($extensionAttributes); + } + + return $customer; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerPlugin.php new file mode 100644 index 0000000000000..0bc22bf5d8869 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/CustomerPlugin.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\LoginAsCustomerAssistance\Api\SetAssistanceInterface; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +/** + * Plugin for Customer assistance_allowed extension attribute. + */ +class CustomerPlugin +{ + /** + * @var SetAssistanceInterface + */ + private $setAssistance; + + /** + * @param SetAssistanceInterface $setAssistance + */ + public function __construct( + SetAssistanceInterface $setAssistance + ) { + $this->setAssistance = $setAssistance; + } + + /** + * Save assistance_allowed extension attribute for Customer instance. + * + * @param CustomerRepositoryInterface $subject + * @param CustomerInterface $result + * @param CustomerInterface $customer + * @return CustomerInterface + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave( + CustomerRepositoryInterface $subject, + CustomerInterface $result, + CustomerInterface $customer + ): CustomerInterface { + $customerId = (int)$result->getId(); + $customerExtensionAttributes = $customer->getExtensionAttributes(); + if ($customerExtensionAttributes && $customerExtensionAttributes->getAssistanceAllowed()) { + $isEnabled = (int)$customerExtensionAttributes->getAssistanceAllowed() === IsAssistanceEnabled::ALLOWED; + $this->setAssistance->execute($customerId, $isEnabled); + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/DataProviderWithDefaultAddressesPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/DataProviderWithDefaultAddressesPlugin.php new file mode 100644 index 0000000000000..6653340285d32 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/DataProviderWithDefaultAddressesPlugin.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses; +use Magento\Framework\AuthorizationInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\Model\ResourceModel\GetLoginAsCustomerAssistanceAllowed; + +/** + * Plugin for managing assistance_allowed extension attribute in Customer form Data Provider. + */ +class DataProviderWithDefaultAddressesPlugin +{ + /** + * @var AuthorizationInterface + */ + private $authorization; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var GetLoginAsCustomerAssistanceAllowed + */ + private $getLoginAsCustomerAssistanceAllowed; + + /** + * @param AuthorizationInterface $authorization + * @param ConfigInterface $config + * @param GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + */ + public function __construct( + AuthorizationInterface $authorization, + ConfigInterface $config, + GetLoginAsCustomerAssistanceAllowed $getLoginAsCustomerAssistanceAllowed + ) { + $this->authorization = $authorization; + $this->config = $config; + $this->getLoginAsCustomerAssistanceAllowed = $getLoginAsCustomerAssistanceAllowed; + } + + /** + * Add assistance_allowed extension attribute data to Customer form Data Provider. + * + * @param DataProviderWithDefaultAddresses $subject + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetData( + DataProviderWithDefaultAddresses $subject, + array $result + ): array { + $isAssistanceAllowed = []; + + foreach ($result as $id => $entityData) { + if ($id) { + $assistanceAllowedStatus = $this->resolveStatus( + $this->getLoginAsCustomerAssistanceAllowed->execute((int)$entityData['customer_id']) + ); + $isAssistanceAllowed[$id]['customer']['extension_attributes']['assistance_allowed'] = + (string)$assistanceAllowedStatus; + } + } + + return array_replace_recursive($result, $isAssistanceAllowed); + } + + /** + * Modify assistance_allowed extension attribute metadata for Customer form Data Provider. + * + * @param DataProviderWithDefaultAddresses $subject + * @param array $result + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetMeta( + DataProviderWithDefaultAddresses $subject, + array $result + ): array { + if (!$this->config->isEnabled()) { + $assistanceAllowedConfig = ['visible' => false]; + } elseif (!$this->authorization->isAllowed('Magento_LoginAsCustomer::allow_shopping_assistance')) { + $assistanceAllowedConfig = [ + 'disabled' => true, + 'notice' => __('You have no permission to change Opt-In preference.'), + ]; + } else { + $assistanceAllowedConfig = []; + } + + $config = [ + 'customer' => [ + 'children' => [ + 'extension_attributes.assistance_allowed' => [ + 'arguments' => [ + 'data' => [ + 'config' => $assistanceAllowedConfig, + ], + ], + ], + ], + ], + ]; + + return array_replace_recursive($result, $config); + } + + /** + * Get integer status value from boolean. + * + * @param bool $assistanceAllowed + * @return int + */ + private function resolveStatus(bool $assistanceAllowed): int + { + return $assistanceAllowed ? IsAssistanceEnabledInterface::ALLOWED : IsAssistanceEnabledInterface::DENIED; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/Plugin/LoginAsCustomerButtonDataProviderPlugin.php b/app/code/Magento/LoginAsCustomerAssistance/Plugin/LoginAsCustomerButtonDataProviderPlugin.php new file mode 100644 index 0000000000000..45a3eb512e7f8 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/Plugin/LoginAsCustomerButtonDataProviderPlugin.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\Plugin; + +use Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +/** + * Change Login as Customer button behavior if Customer has not granted permission. + */ +class LoginAsCustomerButtonDataProviderPlugin +{ + /** + * @var IsAssistanceEnabled + */ + private $isAssistanceEnabled; + + /** + * @param IsAssistanceEnabled $isAssistanceEnabled + */ + public function __construct( + IsAssistanceEnabled $isAssistanceEnabled + ) { + $this->isAssistanceEnabled = $isAssistanceEnabled; + } + + /** + * Change Login as Customer button behavior if Customer has not granted permission. + * + * @param DataProvider $subject + * @param array $result + * @param int $customerId + * @return array + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterGetData(DataProvider $subject, array $result, int $customerId): array + { + if (isset($result['on_click']) && !$this->isAssistanceEnabled->execute($customerId)) { + $result['on_click'] = 'window.lacNotAllowedPopup()'; + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/README.md b/app/code/Magento/LoginAsCustomerAssistance/README.md new file mode 100644 index 0000000000000..b43dd6c8db43a --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/README.md @@ -0,0 +1,3 @@ +# Magento_LoginAsCustomerAssistance module + +The Magento_LoginAsCustomerAssistance module provides possibility to enable/disable LoginAsCustomer functionality per Customer. diff --git a/app/code/Magento/LoginAsCustomerAssistance/ViewModel/ShoppingAssistanceViewModel.php b/app/code/Magento/LoginAsCustomerAssistance/ViewModel/ShoppingAssistanceViewModel.php new file mode 100644 index 0000000000000..f7e224efaa19a --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/ViewModel/ShoppingAssistanceViewModel.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\LoginAsCustomerAssistance\ViewModel; + +use Magento\Customer\Model\Session; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerAssistance\Api\ConfigInterface as AssistanceConfigInterface; +use Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled; + +/** + * View model for Login as Customer Shopping Assistance block. + */ +class ShoppingAssistanceViewModel implements ArgumentInterface +{ + /** + * @var AssistanceConfigInterface + */ + private $assistanceConfig; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var IsAssistanceEnabled + */ + private $isAssistanceEnabled; + + /** + * @var Session + */ + private $session; + + /** + * @param AssistanceConfigInterface $assistanceConfig + * @param ConfigInterface $config + * @param IsAssistanceEnabled $isAssistanceEnabled + * @param Session $session + */ + public function __construct( + AssistanceConfigInterface $assistanceConfig, + ConfigInterface $config, + IsAssistanceEnabled $isAssistanceEnabled, + Session $session + ) { + $this->assistanceConfig = $assistanceConfig; + $this->config = $config; + $this->isAssistanceEnabled = $isAssistanceEnabled; + $this->session = $session; + } + + /** + * Is Login as Customer functionality enabled. + * + * @return bool + */ + public function isLoginAsCustomerEnabled(): bool + { + return $this->config->isEnabled(); + } + + /** + * Is merchant assistance allowed by Customer. + * + * @return bool + */ + public function isAssistanceAllowed(): bool + { + $customerId = $this->session->getId(); + + return $customerId && $this->isAssistanceEnabled->execute((int)$customerId); + } + + /** + * Get shopping assistance checkbox title from config. + * + * @return string + */ + public function getAssistanceCheckboxTitle() + { + return $this->assistanceConfig->getShoppingAssistanceCheckboxTitle(); + } + + /** + * Get shopping assistance checkbox tooltip text from config. + * + * @return string + */ + public function getAssistanceCheckboxTooltip() + { + return $this->assistanceConfig->getShoppingAssistanceCheckboxTooltip(); + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/composer.json b/app/code/Magento/LoginAsCustomerAssistance/composer.json new file mode 100644 index 0000000000000..a02852533b950 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-login-as-customer-assistance", + "description": "", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-customer": "*", + "magento/module-store": "*", + "magento/module-login-as-customer": "*", + "magento/module-login-as-customer-api": "*" + }, + "suggest": { + "magento/module-login-as-customer-admin-ui": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ "registration.php" ], + "psr-4": { + "Magento\\LoginAsCustomerAssistance\\": "" + } + } +} diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/acl.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/acl.xml new file mode 100644 index 0000000000000..2c16b5a9125df --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/acl.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Acl/etc/acl.xsd"> + <acl> + <resources> + <resource id="Magento_Backend::admin"> + <resource id="Magento_Customer::customer"> + <resource id="Magento_LoginAsCustomer::login"> + <resource id="Magento_LoginAsCustomer::allow_shopping_assistance" title="Allow remote shopping assistance" sortOrder="20" /> + </resource> + </resource> + </resource> + </resources> + </acl> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/di.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..3071e3038ffcc --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Customer\Model\Customer\DataProviderWithDefaultAddresses"> + <plugin name="login_as_customer_customer_data_provider_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\DataProviderWithDefaultAddressesPlugin"/> + </type> + <type name="Magento\Customer\Model\Metadata\Form"> + <plugin name="login_as_customer_customer_data_validate_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\CustomerDataValidatePlugin"/> + </type> + <type name="Magento\LoginAsCustomerAdminUi\Ui\Customer\Component\Button\DataProvider"> + <plugin name="login_as_customer_button_data_provider_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\LoginAsCustomerButtonDataProviderPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/system.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..bfdc5519937da --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/adminhtml/system.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="login_as_customer" showInWebsite="1"> + <group id="general" showInWebsite="1"> + <field id="shopping_assistance_checkbox_title" translate="label" type="text" sortOrder="50" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Title for Login as Customer opt-in checkbox</label> + </field> + <field id="shopping_assistance_checkbox_tooltip" translate="label" type="textarea" sortOrder="60" showInDefault="1" showInWebsite="1" canRestore="1"> + <label>Login as Customer checkbox tooltip</label> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/config.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/config.xml new file mode 100644 index 0000000000000..9b74e4734f00e --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/config.xml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + --> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <login_as_customer> + <general> + <shopping_assistance_checkbox_title>Allow remote shopping assistance</shopping_assistance_checkbox_title> + <shopping_assistance_checkbox_tooltip>This allows merchants to "see what you see" and take actions on your behalf in order to provide better assistance.</shopping_assistance_checkbox_tooltip> + </general> + </login_as_customer> + </default> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema.xml new file mode 100644 index 0000000000000..deaecc2bfb777 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema.xml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd"> + <table name="login_as_customer_assistance_allowed" resource="default" engine="innodb" comment="Magento Login as Customer Assistance Allowed Table"> + <column xsi:type="int" name="customer_id" padding="10" unsigned="true" nullable="false" comment="Customer ID"/> + <constraint xsi:type="foreign" referenceId="LOGIN_AS_CUSTOMER_ASSISTANCE_ALLOWED_CUSTOMER_ID_CUSTOMER_ENTITY_ENTITY_ID" + table="login_as_customer_assistance_allowed" column="customer_id" referenceTable="customer_entity" + referenceColumn="entity_id" onDelete="CASCADE"/> + <constraint xsi:type="unique" referenceId="LOGIN_AS_CUSTOMER_ASSISTANCE_ALLOWED_CUSTOMER_ID"> + <column name="customer_id"/> + </constraint> + </table> +</schema> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema_whitelist.json b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema_whitelist.json new file mode 100644 index 0000000000000..2c8aa79f3c7b1 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/db_schema_whitelist.json @@ -0,0 +1,11 @@ +{ + "login_as_customer_assistance_allowed": { + "column": { + "customer_id": true + }, + "constraint": { + "LOGIN_AS_CSTR_ASSISTANCE_ALLOWED_CSTR_ID_CSTR_ENTT_ENTT_ID": true, + "LOGIN_AS_CUSTOMER_ASSISTANCE_ALLOWED_CUSTOMER_ID": true + } + } +} \ No newline at end of file diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/di.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/di.xml new file mode 100755 index 0000000000000..0cbf3b4d10da6 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/di.xml @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\LoginAsCustomerAssistance\Api\ConfigInterface" + type="Magento\LoginAsCustomerAssistance\Model\Config"/> + <preference for="Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface" + type="Magento\LoginAsCustomerAssistance\Model\IsAssistanceEnabled"/> + <preference for="Magento\LoginAsCustomerAssistance\Api\SetAssistanceInterface" + type="Magento\LoginAsCustomerAssistance\Model\SetAssistance"/> + <type name="Magento\LoginAsCustomerApi\Model\IsLoginAsCustomerEnabledForCustomerChain"> + <arguments> + <argument name="resolvers" xsi:type="array"> + <item name="is_allowed" xsi:type="object"> + Magento\LoginAsCustomerAssistance\Model\Processor\IsLoginAsCustomerAllowedResolver + </item> + </argument> + </arguments> + </type> + <type name="Magento\Customer\Api\CustomerRepositoryInterface"> + <plugin name="login_as_customer_customer_repository_plugin" + type="Magento\LoginAsCustomerAssistance\Plugin\CustomerPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/extension_attributes.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/extension_attributes.xml new file mode 100644 index 0000000000000..ff47820faadaa --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/extension_attributes.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd"> + <extension_attributes for="Magento\Customer\Api\Data\CustomerInterface"> + <attribute code="assistance_allowed" type="integer"/> + </extension_attributes> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/frontend/di.xml new file mode 100644 index 0000000000000..bdb2f82eddd60 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/frontend/di.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Customer\Model\CustomerExtractor"> + <plugin name="add_assistance_allowed_to_customer_data" + type="Magento\LoginAsCustomerAssistance\Plugin\CustomerExtractorPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/etc/module.xml b/app/code/Magento/LoginAsCustomerAssistance/etc/module.xml new file mode 100644 index 0000000000000..f443691bcf126 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/etc/module.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" ?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_LoginAsCustomerAssistance"/> +</config> diff --git a/app/code/Magento/LoginAsCustomerAssistance/registration.php b/app/code/Magento/LoginAsCustomerAssistance/registration.php new file mode 100644 index 0000000000000..c2be7af4bd396 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +\Magento\Framework\Component\ComponentRegistrar::register( + \Magento\Framework\Component\ComponentRegistrar::MODULE, + 'Magento_LoginAsCustomerAssistance', + __DIR__ +); diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/layout/loginascustomer_confirmation_popup.xml b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/layout/loginascustomer_confirmation_popup.xml new file mode 100644 index 0000000000000..ef2e81cd37804 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/layout/loginascustomer_confirmation_popup.xml @@ -0,0 +1,22 @@ +<?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"> + <referenceContainer name="content"> + <block class="Magento\LoginAsCustomerAssistance\Block\Adminhtml\NotAllowedPopup" name="lac.not.allowed.popup" template="Magento_LoginAsCustomerAssistance::not-allowed-popup.phtml"> + <arguments> + <argument name="jsLayout" xsi:type="array"> + <item name="components" xsi:type="array"> + <item name="lac-not-allowed-popup" xsi:type="array"> + <item name="component" xsi:type="string">Magento_LoginAsCustomerAssistance/js/not-allowed-popup</item> + </item> + </item> + </argument> + </arguments> + </block> + </referenceContainer> +</layout> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/templates/not-allowed-popup.phtml b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/templates/not-allowed-popup.phtml new file mode 100644 index 0000000000000..42e19f9db4931 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/templates/not-allowed-popup.phtml @@ -0,0 +1,15 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/** @var \Magento\LoginAsCustomerAssistance\Block\Adminhtml\NotAllowedPopup $block */ +?> + +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/core/app": <?= /* @escapeNotVerified */ $block->getJsLayout();?> + } + } + </script> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/ui_component/customer_form.xml b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/ui_component/customer_form.xml new file mode 100644 index 0000000000000..b677becd66064 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/ui_component/customer_form.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<form xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <fieldset name="customer"> + <field name="extension_attributes.assistance_allowed" sortOrder="85" formElement="checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="source" xsi:type="string">customer</item> + </item> + </argument> + <settings> + <dataType>boolean</dataType> + <label translate="true">Allow remote shopping assistance</label> + </settings> + <formElements> + <checkbox> + <settings> + <valueMap> + <map name="false" xsi:type="number">1</map> + <map name="true" xsi:type="number">2</map> + </valueMap> + <prefer>toggle</prefer> + </settings> + </checkbox> + </formElements> + </field> + </fieldset> +</form> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/web/js/not-allowed-popup.js b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/web/js/not-allowed-popup.js new file mode 100644 index 0000000000000..59d8dd4a7ed49 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/adminhtml/web/js/not-allowed-popup.js @@ -0,0 +1,55 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'Magento_Ui/js/modal/confirm', + 'mage/translate' +], function (Component, confirm, $t) { + + 'use strict'; + + return Component.extend({ + /** + * Initialize Component + */ + initialize: function () { + var self = this, + content; + + this._super(); + + content = '<div class="message message-warning">' + self.content + '</div>'; + + /** + * Not Allowed popup + * + * @returns {Boolean} + */ + window.lacNotAllowedPopup = function () { + confirm({ + title: self.title, + content: content, + modalClass: 'confirm lac-confirm', + buttons: [ + { + text: $t('Cancel'), + class: 'action-secondary action-dismiss', + + /** + * Click handler. + */ + click: function (event) { + this.closeModal(event); + } + } + ] + }); + + return false; + }; + } + }); +}); diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_create.xml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_create.xml new file mode 100644 index 0000000000000..121d20395e295 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_create.xml @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" layout="1column" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="customer_form_register"> + <container name="fieldset.create.info.additional" as="fieldset_create_info_additional"/> + </referenceBlock> + <referenceContainer name="fieldset.create.info.additional"> + <block name="login_as_customer_opt_in_create" + template="Magento_LoginAsCustomerAssistance::shopping-assistance.phtml"> + <arguments> + <argument name="view_model" xsi:type="object"> + Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel + </argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_edit.xml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_edit.xml new file mode 100644 index 0000000000000..15b52cb6cf784 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/layout/customer_account_edit.xml @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceBlock name="customer_edit"> + <container name="fieldset.edit.info.additional" as="fieldset_edit_info_additional"/> + </referenceBlock> + <referenceContainer name="fieldset.edit.info.additional"> + <block name="login_as_customer_opt_in_edit" + template="Magento_LoginAsCustomerAssistance::shopping-assistance.phtml"> + <arguments> + <argument name="view_model" xsi:type="object"> + Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel + </argument> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml new file mode 100644 index 0000000000000..7765975863485 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/templates/shopping-assistance.phtml @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Escaper; +use Magento\LoginAsCustomerAssistance\Api\IsAssistanceEnabledInterface; +use Magento\LoginAsCustomerAssistance\ViewModel\ShoppingAssistanceViewModel; + +/** @var Escaper $escaper */ +/** @var ShoppingAssistanceViewModel $viewModel */ +$viewModel = $block->getViewModel(); +?> + +<script type="text/x-magento-init"> +{ + ".form-create-account, .form-edit-account": { + "Magento_LoginAsCustomerAssistance/js/opt-in": { + "allowAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::ALLOWED ?>", + "denyAccess": "<?= /* @noEscape */ IsAssistanceEnabledInterface::DENIED ?>" + } + } +} +</script> + +<?php if ($viewModel->isLoginAsCustomerEnabled()): ?> + <div class="field choice"> + <input type="checkbox" + name="assistance_allowed_checkbox" + title="<?= $escaper->escapeHtmlAttr(__($viewModel->getAssistanceCheckboxTitle())) ?>" + value="1" + id="assistance_allowed_checkbox" + <?php if ($viewModel->isAssistanceAllowed()): ?>checked="checked"<?php endif; ?> + class="checkbox"> + <label for="assistance_allowed_checkbox" class="label"> + <span><?= $escaper->escapeHtmlAttr(__($viewModel->getAssistanceCheckboxTitle())) ?></span> + </label> + + <input type="hidden" name="assistance_allowed" value=""/> + + <div class="field-tooltip toggle"> + <span id="tooltip-label" class="label"><span>Tooltip</span></span> + <span id="tooltip" class="field-tooltip-action action-help" tabindex="0" data-toggle="dropdown" + data-bind="mageInit: {'dropdown':{'activeClass': '_active', 'parent': '.field-tooltip.toggle'}}" + aria-labelledby="tooltip-label" aria-haspopup="true" aria-expanded="false" role="button"> + </span> + <div class="field-tooltip-content" data-target="dropdown" + aria-hidden="true"> + <?= $escaper->escapeHtmlAttr(__($viewModel->getAssistanceCheckboxTooltip())) ?> + </div> + </div> + </div> +<?php endif ?> diff --git a/app/code/Magento/LoginAsCustomerAssistance/view/frontend/web/js/opt-in.js b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/web/js/opt-in.js new file mode 100644 index 0000000000000..d225d298b7771 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerAssistance/view/frontend/web/js/opt-in.js @@ -0,0 +1,18 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery' +], function ($) { + 'use strict'; + + return function (config, element) { + $(element).on('submit', function () { + this.elements['assistance_allowed'].value = + this.elements['assistance_allowed_checkbox'].checked ? + config.allowAccess : config.denyAccess; + }); + }; +}); diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php b/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php index 6303989c0c667..140f31e3467f1 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/CustomerData/LoginAsCustomerUi.php @@ -9,7 +9,9 @@ use Magento\Customer\CustomerData\SectionSourceInterface; use Magento\Customer\Model\Session; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\LocalizedException; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\Store\Model\StoreManagerInterface; /** @@ -29,16 +31,25 @@ class LoginAsCustomerUi implements SectionSourceInterface */ private $storeManager; + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + /** * @param Session $customerSession * @param StoreManagerInterface $storeManager + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( Session $customerSession, - StoreManagerInterface $storeManager + StoreManagerInterface $storeManager, + ?GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId = null ) { $this->customerSession = $customerSession; $this->storeManager = $storeManager; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); } /** @@ -49,12 +60,14 @@ public function __construct( */ public function getSectionData(): array { - if (!$this->customerSession->getCustomerId()) { + $adminId = $this->getLoggedAsCustomerAdminId->execute(); + + if (!$adminId || !$this->customerSession->getCustomerId()) { return []; } return [ - 'adminUserId' => $this->customerSession->getLoggedAsCustomerAdmindId(), + 'adminUserId' => $adminId, 'websiteName' => $this->storeManager->getWebsite()->getName() ]; } diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php index b68e871c5f955..c1e035ac9637c 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/InvalidateExpiredSessionPlugin.php @@ -9,6 +9,7 @@ use Magento\Customer\Model\Session; use Magento\Framework\App\ActionInterface; use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\LoginAsCustomerApi\Api\IsLoginAsCustomerSessionActiveInterface; /** @@ -31,19 +32,27 @@ class InvalidateExpiredSessionPlugin */ private $isLoginAsCustomerSessionActive; + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + /** * @param ConfigInterface $config * @param Session $session * @param IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( ConfigInterface $config, Session $session, - IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive + IsLoginAsCustomerSessionActiveInterface $isLoginAsCustomerSessionActive, + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId ) { $this->session = $session; $this->isLoginAsCustomerSessionActive = $isLoginAsCustomerSessionActive; $this->config = $config; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; } /** @@ -57,11 +66,13 @@ public function __construct( public function beforeExecute(ActionInterface $subject) { if ($this->config->isEnabled()) { - $adminId = (int)$this->session->getLoggedAsCustomerAdmindId(); + $adminId = $this->getLoggedAsCustomerAdminId->execute(); $customerId = (int)$this->session->getCustomerId(); if ($adminId && $customerId) { if (!$this->isLoginAsCustomerSessionActive->execute($customerId, $adminId)) { - $this->session->destroy(); + $this->session->clearStorage(); + $this->session->expireSessionCookie(); + $this->session->regenerateId(); } } } diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/KeepLoginAsCustomerSessionDataPlugin.php b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/KeepLoginAsCustomerSessionDataPlugin.php new file mode 100644 index 0000000000000..9519f3a54077b --- /dev/null +++ b/app/code/Magento/LoginAsCustomerFrontendUi/Plugin/KeepLoginAsCustomerSessionDataPlugin.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\LoginAsCustomerFrontendUi\Plugin; + +use Magento\Framework\Session\SessionManagerInterface; +use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; +use Magento\LoginAsCustomerApi\Api\SetLoggedAsCustomerAdminIdInterface; + +/** + * Keep adminId in customer session if session data is cleared. + */ +class KeepLoginAsCustomerSessionDataPlugin +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + + /** + * @var SetLoggedAsCustomerAdminIdInterface + */ + private $setLoggedAsCustomerAdminId; + + /** + * @param ConfigInterface $config + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId + * @param SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId + */ + public function __construct( + ConfigInterface $config, + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId, + SetLoggedAsCustomerAdminIdInterface $setLoggedAsCustomerAdminId + ) { + $this->config = $config; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; + $this->setLoggedAsCustomerAdminId = $setLoggedAsCustomerAdminId; + } + + /** + * Keep adminId in customer session if session data is cleared. + * + * @param SessionManagerInterface $subject + * @param \Closure $proceed + * @return SessionManagerInterface + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function aroundClearStorage( + SessionManagerInterface $subject, + \Closure $proceed + ): SessionManagerInterface { + $enabled = $this->config->isEnabled(); + $adminId = $enabled ? $this->getLoggedAsCustomerAdminId->execute() : null; + $result = $proceed(); + if ($enabled && $adminId) { + $this->setLoggedAsCustomerAdminId->execute($adminId); + } + + return $result; + } +} diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php b/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php index 7d8738d06f54f..357ede238585b 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php +++ b/app/code/Magento/LoginAsCustomerFrontendUi/ViewModel/Configuration.php @@ -8,7 +8,10 @@ namespace Magento\LoginAsCustomerFrontendUi\ViewModel; use Magento\Customer\Model\Context; +use Magento\Framework\App\Http\Context as HttpContext; +use Magento\Framework\App\ObjectManager; use Magento\LoginAsCustomerApi\Api\ConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; /** * View model to get extension configuration in the template @@ -21,20 +24,29 @@ class Configuration implements \Magento\Framework\View\Element\Block\ArgumentInt private $config; /** - * @var \Magento\Framework\App\Http\Context + * @var HttpContext */ private $httpContext; + /** + * @var GetLoggedAsCustomerAdminIdInterface + */ + private $getLoggedAsCustomerAdminId; + /** * @param ConfigInterface $config - * @param \Magento\Framework\App\Http\Context $httpContext + * @param HttpContext $httpContext + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( ConfigInterface $config, - \Magento\Framework\App\Http\Context $httpContext + HttpContext $httpContext, + ?GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId = null ) { $this->config = $config; $this->httpContext = $httpContext; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId + ?? ObjectManager::getInstance()->get(GetLoggedAsCustomerAdminIdInterface::class); } /** @@ -44,7 +56,7 @@ public function __construct( */ public function isEnabled(): bool { - return $this->config->isEnabled() && $this->isLoggedIn(); + return $this->config->isEnabled() && $this->isLoggedIn() && $this->getLoggedAsCustomerAdminId->execute(); } /** diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml index 2204402b7dd30..bff511b6bb6e6 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml +++ b/app/code/Magento/LoginAsCustomerFrontendUi/etc/frontend/di.xml @@ -17,4 +17,8 @@ <plugin name="invalidate_expired_session_plugin" type="Magento\LoginAsCustomerFrontendUi\Plugin\InvalidateExpiredSessionPlugin"/> </type> + <type name="Magento\Framework\Session\SessionManagerInterface"> + <plugin name="keep_login_as_customer_session_data" + type="Magento\LoginAsCustomerFrontendUi\Plugin\KeepLoginAsCustomerSessionDataPlugin"/> + </type> </config> diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml index efb866690c401..768b63cbbecea 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/layout/loginascustomer_login_index.xml @@ -13,7 +13,7 @@ </action> </referenceBlock> <referenceContainer name="content"> - <block class="Magento\Framework\View\Element\Template" name="loginascustomer_login" template="Magento_LoginAsCustomerFrontendUi::login.phtml"/> + <block class="Magento\Framework\View\Element\Template" name="loginascustomer_login" template="Magento_LoginAsCustomerFrontendUi::login.phtml" cacheable="false"/> </referenceContainer> </body> </page> diff --git a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js index 7f6cad6ce3f2d..c19adbf0dfb4f 100644 --- a/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js +++ b/app/code/Magento/LoginAsCustomerFrontendUi/view/frontend/web/js/view/loginAsCustomer.js @@ -5,10 +5,11 @@ define([ 'jquery', + 'underscore', 'uiComponent', 'Magento_Customer/js/customer-data', 'mage/translate' -], function ($, Component, customerData) { +], function ($, _, Component, customer) { 'use strict'; return Component.extend({ @@ -19,23 +20,53 @@ define([ /** @inheritdoc */ initialize: function () { + var customerData, loggedAsCustomerData; + this._super(); - this.customer = customerData.get('customer'); - this.loginAsCustomer = customerData.get('loggedAsCustomer'); - this.isVisible(this.loginAsCustomer().adminUserId); + customerData = customer.get('customer'); + loggedAsCustomerData = customer.get('loggedAsCustomer'); + + customerData.subscribe(function (data) { + this.fullname = data.fullname; + this.updateBanner(); + }.bind(this)); + loggedAsCustomerData.subscribe(function (data) { + this.adminUserId = data.adminUserId; + this.websiteName = data.websiteName; + this.updateBanner(); + }.bind(this)); + + this.fullname = customerData().fullname; + this.adminUserId = loggedAsCustomerData().adminUserId; + this.websiteName = loggedAsCustomerData().websiteName; - this.notificationText = $.mage.__('You are connected as <strong>%1</strong> on %2') - .replace('%1', this.customer().fullname) - .replace('%2', this.loginAsCustomer().websiteName); + this.updateBanner(); }, /** @inheritdoc */ initObservable: function () { this._super() - .observe('isVisible'); + .observe(['isVisible', 'notificationText']); return this; + }, + + /** + * Update banner area + * + * @returns void + */ + updateBanner: function () { + if (this.adminUserId !== undefined) { + this.isVisible(this.adminUserId); + } + + if (this.fullname !== undefined && this.websiteName !== undefined) { + this.notificationText($.mage.__('You are connected as <strong>%1</strong> on %2') + .replace('%1', _.escape(this.fullname)) + .replace('%2', _.escape(this.websiteName))); + } } }); }); diff --git a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php index 10d793d6c4c0b..7a058ecfc5df8 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogInterface.php @@ -13,6 +13,7 @@ * Data interface for login as customer log. * * @api + * @since 100.4.0 */ interface LogInterface extends ExtensibleDataInterface { @@ -28,6 +29,7 @@ interface LogInterface extends ExtensibleDataInterface * * @param int $logId * @return void + * @since 100.4.0 */ public function setLogId(int $logId): void; @@ -35,6 +37,7 @@ public function setLogId(int $logId): void; * Retrieve login as customer log id. * * @return null|int + * @since 100.4.0 */ public function getLogId(): ?int; @@ -43,6 +46,7 @@ public function getLogId(): ?int; * * @param string $time * @return void + * @since 100.4.0 */ public function setTime(string $time): void; @@ -50,6 +54,7 @@ public function setTime(string $time): void; * Retrieve login as customer log time. * * @return null|string + * @since 100.4.0 */ public function getTime(): ?string; @@ -58,6 +63,7 @@ public function getTime(): ?string; * * @param int $userId * @return void + * @since 100.4.0 */ public function setUserId(int $userId): void; @@ -65,6 +71,7 @@ public function setUserId(int $userId): void; * Retrieve login as customer log user id. * * @return null|int + * @since 100.4.0 */ public function getUserId(): ?int; @@ -73,6 +80,7 @@ public function getUserId(): ?int; * * @param string $userName * @return void + * @since 100.4.0 */ public function setUserName(string $userName): void; @@ -80,6 +88,7 @@ public function setUserName(string $userName): void; * Retrieve login as customer log user name. * * @return null|string + * @since 100.4.0 */ public function getUserName(): ?string; @@ -88,6 +97,7 @@ public function getUserName(): ?string; * * @param int $customerId * @return void + * @since 100.4.0 */ public function setCustomerId(int $customerId): void; @@ -95,6 +105,7 @@ public function setCustomerId(int $customerId): void; * Retrieve login as customer log customer id. * * @return null|int + * @since 100.4.0 */ public function getCustomerId(): ?int; @@ -103,6 +114,7 @@ public function getCustomerId(): ?int; * * @param string $customerEmail * @return void + * @since 100.4.0 */ public function setCustomerEmail(string $customerEmail): void; @@ -110,6 +122,7 @@ public function setCustomerEmail(string $customerEmail): void; * Retrieve login as customer log customer email. * * @return null|string + * @since 100.4.0 */ public function getCustomerEmail(): ?string; @@ -118,6 +131,7 @@ public function getCustomerEmail(): ?string; * * @param \Magento\LoginAsCustomerLog\Api\Data\LogExtensionInterface $extensionAttributes * @return void + * @since 100.4.0 */ public function setExtensionAttributes(LogExtensionInterface $extensionAttributes): void; @@ -125,6 +139,7 @@ public function setExtensionAttributes(LogExtensionInterface $extensionAttribute * Retrieve log extension attributes. * * @return \Magento\LoginAsCustomerLog\Api\Data\LogExtensionInterface + * @since 100.4.0 */ public function getExtensionAttributes(): LogExtensionInterface; } diff --git a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php index 5b08d28af6335..0f6f06a3b3e43 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/Data/LogSearchResultsInterface.php @@ -13,6 +13,7 @@ * Login as customer log entity search results interface. * * @api + * @since 100.4.0 */ interface LogSearchResultsInterface extends SearchResultsInterface { @@ -20,6 +21,7 @@ interface LogSearchResultsInterface extends SearchResultsInterface * Get log list. * * @return \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] + * @since 100.4.0 */ public function getItems(); @@ -28,6 +30,7 @@ public function getItems(); * * @param \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] $items * @return void + * @since 100.4.0 */ public function setItems(array $items); } diff --git a/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php index 4b5ee382c908a..58b232f26e656 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/GetLogsListInterface.php @@ -14,6 +14,7 @@ * Get login as customer log list considering search criteria. * * @api + * @since 100.4.0 */ interface GetLogsListInterface { @@ -22,6 +23,7 @@ interface GetLogsListInterface * * @param SearchCriteriaInterface $searchCriteria * @return LogSearchResultsInterface + * @since 100.4.0 */ public function execute(SearchCriteriaInterface $searchCriteria): LogSearchResultsInterface; } diff --git a/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php b/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php index 67e1ece477727..0d76db641b421 100644 --- a/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php +++ b/app/code/Magento/LoginAsCustomerLog/Api/SaveLogsInterface.php @@ -11,6 +11,7 @@ * Save login as custom logs entities. * * @api + * @since 100.4.0 */ interface SaveLogsInterface { @@ -19,6 +20,7 @@ interface SaveLogsInterface * * @param \Magento\LoginAsCustomerLog\Api\Data\LogInterface[] $logs * @return void + * @since 100.4.0 */ public function execute(array $logs): void; } diff --git a/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml index 077fd6e18db7c..fdd1bf55c91b9 100644 --- a/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml +++ b/app/code/Magento/LoginAsCustomerLog/view/adminhtml/ui_component/login_as_customer_log_listing.xml @@ -38,7 +38,6 @@ </settings> <bookmark name="bookmarks"/> <columnsControls name="columns_controls"/> - <filterSearch name="name"/> <filters name="listing_filters"> <settings> <templates> diff --git a/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php b/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php index 6b36a0720ecb3..dabf8c62e1dee 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php +++ b/app/code/Magento/LoginAsCustomerPageCache/Plugin/PageCache/Model/Config/DisablePageCacheIfNeededPlugin.php @@ -7,8 +7,8 @@ namespace Magento\LoginAsCustomerPageCache\Plugin\PageCache\Model\Config; -use Magento\Customer\Model\Session; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\PageCache\Model\Config; use Magento\Store\Model\ScopeInterface; @@ -27,20 +27,20 @@ class DisablePageCacheIfNeededPlugin private $scopeConfig; /** - * @var Session + * @var GetLoggedAsCustomerAdminIdInterface */ - private $customerSession; + private $getLoggedAsCustomerAdminId; /** * @param ScopeConfigInterface $scopeConfig - * @param Session $customerSession + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( ScopeConfigInterface $scopeConfig, - Session $customerSession + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId ) { $this->scopeConfig = $scopeConfig; - $this->customerSession = $customerSession; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; } /** @@ -58,7 +58,7 @@ public function afterIsEnabled(Config $subject, $isEnabled): bool 'login_as_customer/general/disable_page_cache', ScopeInterface::SCOPE_STORE ); - $adminId = $this->customerSession->getLoggedAsCustomerAdmindId(); + $adminId = $this->getLoggedAsCustomerAdminId->execute(); if ($disable && $adminId) { $isEnabled = false; } diff --git a/app/code/Magento/LoginAsCustomerPageCache/composer.json b/app/code/Magento/LoginAsCustomerPageCache/composer.json index 195a08fc19d83..84d7f2e2a6730 100644 --- a/app/code/Magento/LoginAsCustomerPageCache/composer.json +++ b/app/code/Magento/LoginAsCustomerPageCache/composer.json @@ -4,8 +4,8 @@ "require": { "php": "~7.3.0||~7.4.0", "magento/framework": "*", - "magento/module-customer": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { "magento/module-page-cache": "*" diff --git a/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php b/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php index cf25962a104b2..4aa068a0ccc61 100644 --- a/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php +++ b/app/code/Magento/LoginAsCustomerQuote/Plugin/LoginAsCustomerApi/ProcessShoppingCartPlugin.php @@ -15,7 +15,7 @@ use Magento\LoginAsCustomerApi\Api\GetAuthenticationDataBySecretInterface; /** - * Remove all items from guest shopping cart before execute. Mark customer cart as not-guest after execute + * Remove all items from guest shopping cart and mark cart as not-guest * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -60,7 +60,7 @@ public function __construct( } /** - * Remove all items from guest shopping cart + * Remove all items from guest shopping cart and mark cart as not-guest * * @param AuthenticateCustomerBySecretInterface $subject * @param string $secret @@ -77,31 +77,9 @@ public function beforeExecute( $quote = $this->checkoutSession->getQuote(); /* Remove items from guest cart */ $quote->removeAllItems(); + $quote->setCustomerIsGuest(0); $this->quoteRepository->save($quote); } return null; } - - /** - * Mark customer cart as not-guest - * - * @param AuthenticateCustomerBySecretInterface $subject - * @param void $result - * @param string $secret - * @return void - * @throws LocalizedException - * - * @SuppressWarnings(PHPMD.UnusedFormalParameter) - */ - public function afterExecute( - AuthenticateCustomerBySecretInterface $subject, - $result, - string $secret - ) { - $this->checkoutSession->loadCustomerQuote(); - $quote = $this->checkoutSession->getQuote(); - - $quote->setCustomerIsGuest(0); - $this->quoteRepository->save($quote); - } } diff --git a/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php b/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php index dc7b295f61c4d..87ffe81998d58 100644 --- a/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php +++ b/app/code/Magento/LoginAsCustomerSales/Plugin/FrontAddCommentOnOrderPlacementPlugin.php @@ -7,7 +7,7 @@ namespace Magento\LoginAsCustomerSales\Plugin; -use Magento\Customer\Model\Session; +use Magento\LoginAsCustomerApi\Api\GetLoggedAsCustomerAdminIdInterface; use Magento\Sales\Model\Order; use Magento\User\Model\UserFactory; @@ -19,25 +19,25 @@ class FrontAddCommentOnOrderPlacementPlugin { /** - * @var Session + * @var UserFactory */ - private $customerSession; + private $userFactory; /** - * @var UserFactory + * @var GetLoggedAsCustomerAdminIdInterface */ - private $userFactory; + private $getLoggedAsCustomerAdminId; /** - * @param Session $session * @param UserFactory $userFactory + * @param GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId */ public function __construct( - Session $session, - UserFactory $userFactory + UserFactory $userFactory, + GetLoggedAsCustomerAdminIdInterface $getLoggedAsCustomerAdminId ) { - $this->customerSession = $session; $this->userFactory = $userFactory; + $this->getLoggedAsCustomerAdminId = $getLoggedAsCustomerAdminId; } /** @@ -49,7 +49,7 @@ public function __construct( */ public function afterPlace(Order $subject, Order $result): Order { - $adminId = $this->customerSession->getLoggedAsCustomerAdmindId(); + $adminId = $this->getLoggedAsCustomerAdminId->execute(); if ($adminId) { $adminUser = $this->userFactory->create()->load($adminId); $subject->addCommentToStatusHistory( diff --git a/app/code/Magento/LoginAsCustomerSales/composer.json b/app/code/Magento/LoginAsCustomerSales/composer.json index 3965e8acf87d8..3891504e54092 100644 --- a/app/code/Magento/LoginAsCustomerSales/composer.json +++ b/app/code/Magento/LoginAsCustomerSales/composer.json @@ -5,8 +5,8 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-backend": "*", - "magento/module-customer": "*", - "magento/module-user": "*" + "magento/module-user": "*", + "magento/module-login-as-customer-api": "*" }, "suggest": { "magento/module-sales": "*" diff --git a/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml new file mode 100644 index 0000000000000..1a010fcdead85 --- /dev/null +++ b/app/code/Magento/LoginAsCustomerSales/etc/frontend/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Sales\Model\Order"> + <plugin name="front-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\FrontAddCommentOnOrderPlacementPlugin"/> + </type> +</config> diff --git a/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml index 1a010fcdead85..6dda349f1e60d 100644 --- a/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml +++ b/app/code/Magento/LoginAsCustomerSales/etc/webapi_rest/di.xml @@ -7,6 +7,6 @@ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> <type name="Magento\Sales\Model\Order"> - <plugin name="front-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\FrontAddCommentOnOrderPlacementPlugin"/> + <plugin name="rest-order-placement-comment" type="Magento\LoginAsCustomerSales\Plugin\FrontAddCommentOnOrderPlacementPlugin"/> </type> </config> diff --git a/app/code/Magento/MediaContent/etc/di.xml b/app/code/Magento/MediaContent/etc/di.xml index f2a9459447001..df84ad7bb0f70 100644 --- a/app/code/Magento/MediaContent/etc/di.xml +++ b/app/code/Magento/MediaContent/etc/di.xml @@ -16,6 +16,7 @@ <preference for="Magento\MediaContentApi\Api\Data\ContentIdentityInterface" type="Magento\MediaContent\Model\ContentIdentity"/> <preference for="Magento\MediaContentApi\Api\Data\ContentAssetLinkInterface" type="Magento\MediaContent\Model\ContentAssetLink"/> <preference for="Magento\MediaContentApi\Model\SearchPatternConfigInterface" type="Magento\MediaContent\Model\Content\SearchPatternConfig"/> + <preference for="Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface" type="Magento\MediaContentApi\Model\Composite\GetAssetIdsByContentField"/> <type name="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface"> <plugin name="remove_media_content_after_asset_is_removed_by_path" type="Magento\MediaContent\Plugin\MediaGalleryAssetDeleteByPath" /> </type> diff --git a/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php b/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php index 5ff490655d464..c438d90aeb4f0 100644 --- a/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php +++ b/app/code/Magento/MediaContentApi/Api/Data/ContentAssetLinkInterface.php @@ -14,6 +14,7 @@ /** * Data interface representing the identificator of content. I.e. short description field of product entity with id 42 * @api + * @since 100.4.0 */ interface ContentAssetLinkInterface extends ExtensibleDataInterface { @@ -21,6 +22,7 @@ interface ContentAssetLinkInterface extends ExtensibleDataInterface * Return the object that represent content identity * * @return ContentIdentityInterface + * @since 100.4.0 */ public function getContentId(): ContentIdentityInterface; @@ -28,6 +30,7 @@ public function getContentId(): ContentIdentityInterface; * Array of assets related to the content entity * * @return int + * @since 100.4.0 */ public function getAssetId(): int; @@ -35,6 +38,7 @@ public function getAssetId(): int; * Retrieve existing extension attributes object or create a new one. * * @return \Magento\MediaContentApi\Api\Data\ContentAssetLinkExtensionInterface|null + * @since 100.4.0 */ public function getExtensionAttributes(): ?ContentAssetLinkExtensionInterface; @@ -43,6 +47,7 @@ public function getExtensionAttributes(): ?ContentAssetLinkExtensionInterface; * * @param \Magento\MediaContentApi\Api\Data\ContentAssetLinkExtensionInterface|null $extensionAttributes * @return void + * @since 100.4.0 */ public function setExtensionAttributes(?ContentAssetLinkExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php b/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php index f1b701fe9d964..16851b657ab90 100644 --- a/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php +++ b/app/code/Magento/MediaContentApi/Api/Data/ContentIdentityInterface.php @@ -14,6 +14,7 @@ /** * Data interface representing the identificator of content. I.e. short description field of product entity with id 42 * @api + * @since 100.4.0 */ interface ContentIdentityInterface extends ExtensibleDataInterface { @@ -21,6 +22,7 @@ interface ContentIdentityInterface extends ExtensibleDataInterface * Type of entity that can have a content with media. I.e. catalog_product or cms_page * * @return string + * @since 100.4.0 */ public function getEntityType(): string; @@ -28,6 +30,7 @@ public function getEntityType(): string; * Id of the entity containing content with media * * @return string + * @since 100.4.0 */ public function getEntityId(): string; @@ -35,6 +38,7 @@ public function getEntityId(): string; * Field of the entity where the content can be stored. I.e. short_description for product * * @return string + * @since 100.4.0 */ public function getField(): string; @@ -42,6 +46,7 @@ public function getField(): string; * Retrieve existing extension attributes object or create a new one. * * @return \Magento\MediaContentApi\Api\Data\ContentIdentityExtensionInterface|null + * @since 100.4.0 */ public function getExtensionAttributes(): ?ContentIdentityExtensionInterface; @@ -50,6 +55,7 @@ public function getExtensionAttributes(): ?ContentIdentityExtensionInterface; * * @param \Magento\MediaContentApi\Api\Data\ContentIdentityExtensionInterface|null $extensionAttributes * @return void + * @since 100.4.0 */ public function setExtensionAttributes(?ContentIdentityExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php index 8997e4b6e7e77..753dcc3ed7aae 100644 --- a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php +++ b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksByAssetIdsInterface.php @@ -13,6 +13,7 @@ /** * Delete the relation between media asset and the piece of content. I.e media asset no longer part of the content * @api + * @since 100.4.0 */ interface DeleteContentAssetLinksByAssetIdsInterface { @@ -21,6 +22,7 @@ interface DeleteContentAssetLinksByAssetIdsInterface * * @param int[] $assetIds * @throws \Magento\Framework\Exception\CouldNotDeleteException + * @since 100.4.0 */ public function execute(array $assetIds): void; } diff --git a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php index 9c50793f51303..433db3923ceb4 100644 --- a/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php +++ b/app/code/Magento/MediaContentApi/Api/DeleteContentAssetLinksInterface.php @@ -13,6 +13,7 @@ /** * Remove the relation between media asset and the piece of content. I.e media asset no longer part of the content * @api + * @since 100.4.0 */ interface DeleteContentAssetLinksInterface { @@ -21,6 +22,7 @@ interface DeleteContentAssetLinksInterface * * @param ContentAssetLinkInterface[] $contentAssetLinks * @throws \Magento\Framework\Exception\CouldNotDeleteException + * @since 100.4.0 */ public function execute(array $contentAssetLinks): void; } diff --git a/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php b/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php index 4f95571f30ffd..06ac7a5bd0778 100644 --- a/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php +++ b/app/code/Magento/MediaContentApi/Api/ExtractAssetsFromContentInterface.php @@ -12,6 +12,7 @@ /** * Parse the content string for references to media assets and return the list of identified media assets * @api + * @since 100.4.0 */ interface ExtractAssetsFromContentInterface { @@ -20,6 +21,7 @@ interface ExtractAssetsFromContentInterface * * @param string $content * @return AssetInterface[] + * @since 100.4.0 */ public function execute(string $content): array; } diff --git a/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentFieldInterface.php b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentFieldInterface.php new file mode 100644 index 0000000000000..f2f9ddbf11956 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentFieldInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Api; + +use Magento\Framework\Exception\InvalidArgumentException; + +/** + * Interface used to return Asset id by content field. + */ +interface GetAssetIdsByContentFieldInterface +{ + /** + * This function returns asset ids by content field + * + * @param string $field + * @param string $value + * @throws InvalidArgumentException + * @return int[] + */ + public function execute(string $field, string $value): array; +} diff --git a/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php index 4316a0d6ee33d..cca5127c3eb4a 100644 --- a/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php +++ b/app/code/Magento/MediaContentApi/Api/GetAssetIdsByContentIdentityInterface.php @@ -13,6 +13,7 @@ /** * Get media asset ids that are used in the piece of content identified by the specified content identity * @api + * @since 100.4.0 */ interface GetAssetIdsByContentIdentityInterface { @@ -22,6 +23,7 @@ interface GetAssetIdsByContentIdentityInterface * @param ContentIdentityInterface $contentIdentity * @return int[] * @throws \Magento\Framework\Exception\IntegrationException + * @since 100.4.0 */ public function execute(ContentIdentityInterface $contentIdentity): array; } diff --git a/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php b/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php index cb117545c257e..93d5901044e36 100644 --- a/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php +++ b/app/code/Magento/MediaContentApi/Api/GetContentByAssetIdsInterface.php @@ -13,6 +13,7 @@ /** * Get list of content identifiers for pieces of content that include the specified media asset * @api + * @since 100.4.0 */ interface GetContentByAssetIdsInterface { @@ -22,6 +23,7 @@ interface GetContentByAssetIdsInterface * @param int[] $assetIds * @return ContentIdentityInterface[] * @throws \Magento\Framework\Exception\IntegrationException + * @since 100.4.0 */ public function execute(array $assetIds): array; } diff --git a/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php b/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php index 1c86953ce6f84..8363a85ea83be 100644 --- a/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php +++ b/app/code/Magento/MediaContentApi/Api/SaveContentAssetLinksInterface.php @@ -13,6 +13,7 @@ /** * Save a media asset to content relation. * @api + * @since 100.4.0 */ interface SaveContentAssetLinksInterface { @@ -21,6 +22,7 @@ interface SaveContentAssetLinksInterface * * @param ContentAssetLinkInterface[] $contentAssetLinks * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 100.4.0 */ public function execute(array $contentAssetLinks): void; } diff --git a/app/code/Magento/MediaContentApi/Model/Composite/GetAssetIdsByContentField.php b/app/code/Magento/MediaContentApi/Model/Composite/GetAssetIdsByContentField.php new file mode 100644 index 0000000000000..61df8504b4c77 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/Composite/GetAssetIdsByContentField.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model\Composite; + +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface as GetAssetIdsByContentFieldApiInterface; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset ids by content field + */ +class GetAssetIdsByContentField implements GetAssetIdsByContentFieldApiInterface +{ + /** + * @var array + */ + private $fieldHandlers; + + /** + * GetAssetIdsByContentField constructor. + * + * @param array $fieldHandlers + */ + public function __construct(array $fieldHandlers = []) + { + $this->fieldHandlers = $fieldHandlers; + } + + /** + * @inheritDoc + */ + public function execute(string $field, string $value): array + { + if (!array_key_exists($field, $this->fieldHandlers)) { + throw new InvalidArgumentException(__('The field argument is invalid.')); + } + $ids = []; + /** @var GetAssetIdsByContentFieldInterface $fieldHandler */ + foreach ($this->fieldHandlers[$field] as $fieldHandler) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $ids = array_merge($ids, $fieldHandler->execute($value)); + } + return array_unique($ids); + } +} diff --git a/app/code/Magento/MediaContentApi/Model/GetAssetIdsByContentFieldInterface.php b/app/code/Magento/MediaContentApi/Model/GetAssetIdsByContentFieldInterface.php new file mode 100644 index 0000000000000..f38ffecedc202 --- /dev/null +++ b/app/code/Magento/MediaContentApi/Model/GetAssetIdsByContentFieldInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Interface used to return Asset id by content field. + */ +interface GetAssetIdsByContentFieldInterface +{ + /** + * This function returns asset ids by content field + * + * @param string $value + * @return int[] + * @throws LocalizedException + * @throws NoSuchEntityException + */ + public function execute(string $value): array; +} diff --git a/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php b/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php index c58d543a597b7..ac24a32d35475 100644 --- a/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php +++ b/app/code/Magento/MediaContentApi/Model/GetEntityContentsInterface.php @@ -12,6 +12,7 @@ /** * Get Entity Contents. * @api + * @since 100.4.0 */ interface GetEntityContentsInterface { @@ -20,6 +21,7 @@ interface GetEntityContentsInterface * * @param ContentIdentityInterface $contentIdentity * @return string[] + * @since 100.4.0 */ public function execute(ContentIdentityInterface $contentIdentity): array; } diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByCategoryStore.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByCategoryStore.php new file mode 100644 index 0000000000000..232577b77c802 --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByCategoryStore.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Catalog\Api\CategoryManagementInterface; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; +use Magento\Store\Api\GroupRepositoryInterface; +use Magento\Store\Api\StoreRepositoryInterface; + +/** + * Class responsible to return Asset id by category store + */ +class GetAssetIdsByCategoryStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const TABLE_CATALOG_CATEGORY = 'catalog_category_entity'; + private const ENTITY_TYPE = 'catalog_category'; + private const ID_COLUMN = 'entity_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var GroupRepositoryInterface + */ + private $storeGroupRepository; + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * GetAssetIdsByCategoryStore constructor. + * + * @param ResourceConnection $resource + * @param StoreRepositoryInterface $storeRepository + * @param GroupRepositoryInterface $storeGroupRepository + * @param CategoryRepositoryInterface $categoryRepository + */ + public function __construct( + ResourceConnection $resource, + StoreRepositoryInterface $storeRepository, + GroupRepositoryInterface $storeGroupRepository, + CategoryRepositoryInterface $categoryRepository + ) { + $this->connection = $resource; + $this->storeRepository = $storeRepository; + $this->storeGroupRepository = $storeGroupRepository; + $this->categoryRepository = $categoryRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + try { + $storeView = $this->storeRepository->getById($value); + $storeGroup = $this->storeGroupRepository->get($storeView->getStoreGroupId()); + $rootCategory = $this->categoryRepository->get($storeGroup->getRootCategoryId()); + } catch (NoSuchEntityException $exception) { + return []; + } + + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->joinInner( + ['category_table' => $this->connection->getTableName(self::TABLE_CATALOG_CATEGORY)], + 'asset_content_table.entity_id = category_table.' . self::ID_COLUMN, + [] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->where( + 'path LIKE ?', + $rootCategory->getPath() . '%' + ); + + return $this->connection->getConnection()->fetchCol($sql); + } +} diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByEavContentField.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByEavContentField.php new file mode 100644 index 0000000000000..00c6e2f180a6f --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByEavContentField.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Eav\Model\Config; +use Magento\Framework\App\ResourceConnection; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by eav content field + */ +class GetAssetIdsByEavContentField implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var Config + */ + private $config; + + /** + * @var string + */ + private $attributeCode; + + /** + * @var string + */ + private $entityType; + + /** + * @var string + */ + private $entityTable; + + /** + * @var array + */ + private $valueMap; + + /** + * GetAssetIdsByEavContentField constructor. + * + * @param ResourceConnection $resource + * @param Config $config + * @param string $attributeCode + * @param string $entityType + * @param string $entityTable + * @param array $valueMap + */ + public function __construct( + ResourceConnection $resource, + Config $config, + string $attributeCode, + string $entityType, + string $entityTable, + array $valueMap = [] + ) { + $this->connection = $resource; + $this->config = $config; + $this->attributeCode = $attributeCode; + $this->entityType = $entityType; + $this->entityTable = $entityTable; + $this->valueMap = $valueMap; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $attribute = $this->config->getAttribute($this->entityType, $this->attributeCode); + + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->joinInner( + ['entity_table' => $this->connection->getTableName($this->entityTable)], + 'asset_content_table.entity_id = entity_table.entity_id', + [] + )->joinInner( + ['entity_eav_type' => $this->connection->getTableName($attribute->getBackendTable())], + 'entity_table.' . $attribute->getEntityIdField() . ' = entity_eav_type.' . $attribute->getEntityIdField() . + ' AND entity_eav_type.attribute_id = ' . $attribute->getAttributeId(), + [] + )->where( + 'entity_eav_type.value = ?', + $this->getValueFromMap($value) + ); + + return $this->connection->getConnection()->fetchCol($sql); + } + + /** + * Get a value from a value map + * + * @param string $value + * @return string + */ + private function getValueFromMap(string $value): string + { + return $this->valueMap[$value] ?? $value; + } +} diff --git a/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByProductStore.php b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByProductStore.php new file mode 100644 index 0000000000000..6548b2964caaf --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByProductStore.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; +use Magento\Store\Api\StoreRepositoryInterface; + +/** + * Class responsible to return Asset ids by product store + */ +class GetAssetIdsByProductStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const ENTITY_TYPE = 'catalog_product'; + private const FIELD_TABLE = 'catalog_product_website'; + private const ID_COLUMN = 'product_id'; + private const FIELD_COLUMN = 'website_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * GetAssetIdsByProductStore constructor. + * + * @param ResourceConnection $resource + * @param StoreRepositoryInterface $storeRepository + */ + public function __construct( + ResourceConnection $resource, + StoreRepositoryInterface $storeRepository + ) { + $this->connection = $resource; + $this->storeRepository = $storeRepository; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $store = $this->storeRepository->getById($value); + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->joinInner( + ['field_table' => $this->connection->getTableName(self::FIELD_TABLE)], + 'asset_content_table.entity_id = field_table.' . self::ID_COLUMN, + [] + )->where( + 'field_table.' . self::FIELD_COLUMN . ' = ?', + $store->getWebsiteId() + ); + + return $this->connection->getConnection()->fetchCol($sql); + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php new file mode 100644 index 0000000000000..1565d455cc43f --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/CategoryDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Category as CatalogCategory; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the catalog_category_delete_after event and deletes relation between category content and media asset. + */ +class CategoryDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_category'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $category = $observer->getEvent()->getData('category'); + $contentAssetLinks = []; + + if ($category instanceof CatalogCategory) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $category->getEntityId(), + ] + ); + $content = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($content); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php new file mode 100644 index 0000000000000..421bb5a33fa1d --- /dev/null +++ b/app/code/Magento/MediaContentCatalog/Observer/ProductDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Observer; + +use Magento\Catalog\Model\Product as CatalogProduct; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the catalog_product_delete_before event and deletes relation between category content and media asset. + */ +class ProductDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'catalog_product'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted product and remove relation betwen product and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $product = $observer->getEvent()->getData('product'); + $contentAssetLinks = []; + + if ($product instanceof CatalogProduct) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $product->getEntityId(), + ] + ); + $productContent = implode(PHP_EOL, $this->getContent->execute($contentIdentity)); + $assets = $this->extractAssetsFromContent->execute($productContent); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCatalog/composer.json b/app/code/Magento/MediaContentCatalog/composer.json index 21e23e6b18bdc..2b19bc95f6ed3 100644 --- a/app/code/Magento/MediaContentCatalog/composer.json +++ b/app/code/Magento/MediaContentCatalog/composer.json @@ -6,6 +6,7 @@ "magento/module-media-content-api": "*", "magento/module-catalog": "*", "magento/module-eav": "*", + "magento/module-store": "*", "magento/framework": "*" }, "type": "magento2-module", diff --git a/app/code/Magento/MediaContentCatalog/etc/di.xml b/app/code/Magento/MediaContentCatalog/etc/di.xml index a2d300a2bb208..8c606a3cae49f 100644 --- a/app/code/Magento/MediaContentCatalog/etc/di.xml +++ b/app/code/Magento/MediaContentCatalog/etc/di.xml @@ -14,6 +14,22 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCatalog\Observer\ProductDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="description" xsi:type="string">description</item> + <item name="short_description" xsi:type="string">short_description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCatalog\Observer\CategoryDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="image" xsi:type="string">image</item> + <item name="description" xsi:type="string">description</item> + </argument> + </arguments> + </type> <type name="Magento\MediaContentCatalog\Observer\Category"> <arguments> <argument name="fields" xsi:type="array"> @@ -30,4 +46,36 @@ </argument> </arguments> </type> + <virtualType name="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByProductStatus" type="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByEavContentField"> + <arguments> + <argument name="attributeCode" xsi:type="string">status</argument> + <argument name="entityType" xsi:type="string">catalog_product</argument> + <argument name="entityTable" xsi:type="string">catalog_product_entity</argument> + <argument name="valueMap" xsi:type="array"> + <item name="1" xsi:type="string">1</item> + <item name="0" xsi:type="string">2</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByCategoryStatus" type="Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByEavContentField"> + <arguments> + <argument name="attributeCode" xsi:type="string">is_active</argument> + <argument name="entityType" xsi:type="string">catalog_category</argument> + <argument name="entityTable" xsi:type="string">catalog_category_entity</argument> + </arguments> + </virtualType> + <type name="Magento\MediaContentApi\Model\Composite\GetAssetIdsByContentField"> + <arguments> + <argument name="fieldHandlers" xsi:type="array"> + <item name="content_status" xsi:type="array"> + <item name="getAssetIdsByProductStatus" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByProductStatus</item> + <item name="getAssetIdsByCategoryStatus" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByCategoryStatus</item> + </item> + <item name="store_id" xsi:type="array"> + <item name="getAssetIdsByProductStore" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByProductStore</item> + <item name="getAssetIdsByCategoryStore" xsi:type="object">Magento\MediaContentCatalog\Model\ResourceModel\GetAssetIdsByCategoryStore</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentCatalog/etc/events.xml b/app/code/Magento/MediaContentCatalog/etc/events.xml index f68d66eb3cc40..8ec7a30b961ba 100644 --- a/app/code/Magento/MediaContentCatalog/etc/events.xml +++ b/app/code/Magento/MediaContentCatalog/etc/events.xml @@ -9,6 +9,12 @@ <event name="catalog_category_save_after"> <observer name="media_content_catalog_category_save_after" instance="Magento\MediaContentCatalog\Observer\Category" /> </event> + <event name="catalog_product_delete_before"> + <observer name="media_content_catalog_product_delete_before" instance="Magento\MediaContentCatalog\Observer\ProductDelete" /> + </event> + <event name="catalog_category_delete_before"> + <observer name="media_content_catalog_category_delete_before" instance="Magento\MediaContentCatalog\Observer\CategoryDelete" /> + </event> <event name="catalog_product_save_after"> <observer name="media_content_catalog_product_save_after" instance="Magento\MediaContentCatalog\Observer\Product" /> </event> diff --git a/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByBlockStore.php b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByBlockStore.php new file mode 100644 index 0000000000000..f1f8d81ec32f2 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByBlockStore.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by content field + */ +class GetAssetIdsByBlockStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const ENTITY_TYPE = 'cms_block'; + private const STORE_FIELD = 'store_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * GetAssetIdsByContentField constructor. + * + * @param ResourceConnection $resource + * @param BlockRepositoryInterface $blockRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + ResourceConnection $resource, + BlockRepositoryInterface $blockRepository, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->connection = $resource; + $this->blockRepository = $blockRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->where( + 'entity_id IN (?)', + $this->getBlockIdsByStore((int) $value) + ); + + return $this->connection->getConnection()->fetchCol($sql); + } + + /** + * Get block ids by store + * + * @param int $storeId + * @return array + * @throws LocalizedException + */ + private function getBlockIdsByStore(int $storeId): array + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(self::STORE_FIELD, $storeId) + ->create(); + + $searchResult = $this->blockRepository->getList($searchCriteria); + + return array_map(function (BlockInterface $block) { + return $block->getId(); + }, $searchResult->getItems()); + } +} diff --git a/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentField.php b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentField.php new file mode 100644 index 0000000000000..9c223fd870645 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentField.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by content field + */ +class GetAssetIdsByContentField implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var string + */ + private $entityType; + + /** + * @var string + */ + private $fieldTable; + + /** + * @var string + */ + private $fieldColumn; + + /** + * @var string + */ + private $idColumn; + + /** + * GetAssetIdsByContentField constructor. + * + * @param ResourceConnection $resource + * @param string $entityType + * @param string $fieldTable + * @param string $idColumn + * @param string $fieldColumn + */ + public function __construct( + ResourceConnection $resource, + string $entityType, + string $fieldTable, + string $idColumn, + string $fieldColumn + ) { + $this->connection = $resource; + $this->entityType = $entityType; + $this->fieldTable = $fieldTable; + $this->idColumn = $idColumn; + $this->fieldColumn = $fieldColumn; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->joinInner( + ['field_table' => $this->connection->getTableName($this->fieldTable)], + 'asset_content_table.entity_id = field_table.' . $this->idColumn, + [] + )->where( + 'field_table.' . $this->fieldColumn . ' = ?', + $value + ); + + return $this->connection->getConnection()->fetchCol($sql); + } +} diff --git a/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByPageStore.php b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByPageStore.php new file mode 100644 index 0000000000000..92cf67e7d03e4 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByPageStore.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Model\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to return Asset id by content field + */ +class GetAssetIdsByPageStore implements GetAssetIdsByContentFieldInterface +{ + private const TABLE_CONTENT_ASSET = 'media_content_asset'; + private const ENTITY_TYPE = 'cms_page'; + private const STORE_FIELD = 'store_id'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * GetAssetIdsByContentField constructor. + * + * @param ResourceConnection $resource + * @param PageRepositoryInterface $pageRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + */ + public function __construct( + ResourceConnection $resource, + PageRepositoryInterface $pageRepository, + SearchCriteriaBuilder $searchCriteriaBuilder + ) { + $this->connection = $resource; + $this->pageRepository = $pageRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + } + + /** + * @inheritDoc + */ + public function execute(string $value): array + { + $sql = $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + self::ENTITY_TYPE + )->where( + 'entity_id IN (?)', + $this->getPageIdsByStore((int) $value) + ); + + return $this->connection->getConnection()->fetchCol($sql); + } + + /** + * Get page ids by store + * + * @param int $storeId + * @return array + * @throws LocalizedException + */ + private function getPageIdsByStore(int $storeId): array + { + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter(self::STORE_FIELD, $storeId) + ->create(); + + $searchResult = $this->pageRepository->getList($searchCriteria); + + return array_map(function (PageInterface $page) { + return $page->getId(); + }, $searchResult->getItems()); + } +} diff --git a/app/code/Magento/MediaContentCms/Observer/BlockDelete.php b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php new file mode 100644 index 0000000000000..582f0a9ec6701 --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/BlockDelete.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Block as CmsBlock; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the adminhtml_cmspage_on_delete event and deletes relation between page content and media asset. + */ +class BlockDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_block'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param array $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $block = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($block instanceof CmsBlock) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $block->getId(), + ] + ); + $assets = $this->extractAssetsFromContent->execute((string) $block->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/Observer/PageDelete.php b/app/code/Magento/MediaContentCms/Observer/PageDelete.php new file mode 100644 index 0000000000000..96d2bf89873bd --- /dev/null +++ b/app/code/Magento/MediaContentCms/Observer/PageDelete.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Observer; + +use Magento\Cms\Model\Page as CmsPage; +use Magento\Framework\Event\Observer; +use Magento\Framework\Event\ObserverInterface; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentApi\Api\ExtractAssetsFromContentInterface; + +/** + * Observe the cms_page_delete_before event and deletes relation between page content and media asset. + */ +class PageDelete implements ObserverInterface +{ + private const CONTENT_TYPE = 'cms_page'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @var array + */ + private $fields; + + /** + * @var GetEntityContentsInterface + */ + private $getContent; + + /** + * @var ExtractAssetsFromContentInterface + */ + private $extractAssetsFromContent; + + /** + * @param ExtractAssetsFromContentInterface $extractAssetsFromContent + * @param GetEntityContentsInterface $getContent + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param arry $fields + */ + public function __construct( + ExtractAssetsFromContentInterface $extractAssetsFromContent, + GetEntityContentsInterface $getContent, + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + array $fields + ) { + $this->extractAssetsFromContent = $extractAssetsFromContent; + $this->getContent = $getContent; + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->fields = $fields; + } + + /** + * Retrieve the deleted category and remove relation betwen category and asset + * + * @param Observer $observer + * @throws \Exception + */ + public function execute(Observer $observer): void + { + $page = $observer->getEvent()->getData('object'); + $contentAssetLinks = []; + + if ($page instanceof CmsPage) { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => (string) $page->getId(), + ] + ); + + $assets = $this->extractAssetsFromContent->execute((string) $page->getData($field)); + + foreach ($assets as $asset) { + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset->getId(), + 'contentIdentity' => $contentIdentity + ] + ); + } + } + if (!empty($contentAssetLinks)) { + $this->deleteContentAssetLinks->execute($contentAssetLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentCms/etc/di.xml b/app/code/Magento/MediaContentCms/etc/di.xml index f980936465faf..c157fbf22b7ad 100644 --- a/app/code/Magento/MediaContentCms/etc/di.xml +++ b/app/code/Magento/MediaContentCms/etc/di.xml @@ -20,4 +20,48 @@ </argument> </arguments> </type> + <type name="Magento\MediaContentCms\Observer\PageDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentCms\Observer\BlockDelete"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <virtualType name="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByPageStatus" type="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByContentField"> + <arguments> + <argument name="entityType" xsi:type="string">cms_page</argument> + <argument name="fieldTable" xsi:type="string">cms_page</argument> + <argument name="idColumn" xsi:type="string">page_id</argument> + <argument name="fieldColumn" xsi:type="string">is_active</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByBlockStatus" type="Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByContentField"> + <arguments> + <argument name="entityType" xsi:type="string">cms_block</argument> + <argument name="fieldTable" xsi:type="string">cms_block</argument> + <argument name="idColumn" xsi:type="string">block_id</argument> + <argument name="fieldColumn" xsi:type="string">is_active</argument> + </arguments> + </virtualType> + <type name="Magento\MediaContentApi\Model\Composite\GetAssetIdsByContentField"> + <arguments> + <argument name="fieldHandlers" xsi:type="array"> + <item name="content_status" xsi:type="array"> + <item name="getAssetIdsByPageStatus" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByPageStatus</item> + <item name="getAssetIdsByBlockStatus" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByBlockStatus</item> + </item> + <item name="store_id" xsi:type="array"> + <item name="getAssetIdsByPageStore" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByPageStore</item> + <item name="getAssetIdsByBlockStore" xsi:type="object">Magento\MediaContentCms\Model\ResourceModel\GetAssetIdsByBlockStore</item> + </item> + </argument> + </arguments> + </type> </config> diff --git a/app/code/Magento/MediaContentCms/etc/events.xml b/app/code/Magento/MediaContentCms/etc/events.xml index 7e9abe3bf19c4..94f963f40be15 100644 --- a/app/code/Magento/MediaContentCms/etc/events.xml +++ b/app/code/Magento/MediaContentCms/etc/events.xml @@ -6,8 +6,14 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Event/etc/events.xsd"> + <event name="cms_page_delete_before"> + <observer name="media_content_cms_page_delete_before" instance="Magento\MediaContentCms\Observer\PageDelete" /> + </event> <event name="cms_page_save_after"> <observer name="media_content_cms_page_save_after" instance="Magento\MediaContentCms\Observer\Page" /> + </event> + <event name="cms_block_delete_before"> + <observer name="media_content_cms_block_delete_before" instance="Magento\MediaContentCms\Observer\BlockDelete" /> </event> <event name="cms_block_save_after"> <observer name="media_content_cms_block_save_after" instance="Magento\MediaContentCms\Observer\Block" /> diff --git a/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php new file mode 100644 index 0000000000000..55f99697c289b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Console/Command/Synchronize.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Console\Command; + +use Magento\Framework\App\Area; +use Magento\Framework\App\State; +use Magento\Framework\Console\Cli; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Synchronize content with assets + */ +class Synchronize extends Command +{ + /** + * @var SynchronizeInterface + */ + private $synchronizeContent; + + /** + * @var State $state + */ + private $state; + + /** + * @param SynchronizeInterface $synchronizeContent + * @param State $state + */ + public function __construct( + SynchronizeInterface $synchronizeContent, + State $state + ) { + $this->synchronizeContent = $synchronizeContent; + $this->state = $state; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('media-content:sync'); + $this->setDescription('Synchronize content with assets'); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Synchronizing content with assets...'); + $this->state->emulateAreaCode( + Area::AREA_ADMINHTML, + function () { + $this->synchronizeContent->execute(); + } + ); + $output->writeln('Completed content synchronization.'); + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/LICENSE.txt b/app/code/Magento/MediaContentSynchronization/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronization/Model/Consume.php b/app/code/Magento/MediaContentSynchronization/Model/Consume.php new file mode 100644 index 0000000000000..bcce3514e4ad9 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Consume.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; + +/** + * Media content synchronization queue consumer. + */ +class Consume +{ + /** + * @var SynchronizeInterface + */ + private $synchronize; + + /** + * @param SynchronizeInterface $synchronize + */ + public function __construct(SynchronizeInterface $synchronize) + { + $this->synchronize = $synchronize; + } + + /** + * Run media files synchronization. + */ + public function execute() : void + { + $this->synchronize->execute(); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/Publish.php b/app/code/Magento/MediaContentSynchronization/Model/Publish.php new file mode 100644 index 0000000000000..ad6fdd27d7067 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Publish.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\Framework\MessageQueue\PublisherInterface; + +/** + * Publish media content synchronization queue. + */ +class Publish +{ + /** + * Media content synchronization queue topic name. + */ + private const TOPIC_MEDIA_CONTENT_SYNCHRONIZATION = 'media.content.synchronization'; + + /** + * @var PublisherInterface + */ + private $publisher; + + /** + * @param PublisherInterface $publisher + */ + public function __construct(PublisherInterface $publisher) + { + $this->publisher = $publisher; + } + + /** + * Publish media content synchronization message to the message queue. + */ + public function execute() : void + { + $this->publisher->publish( + self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION, + [self::TOPIC_MEDIA_CONTENT_SYNCHRONIZATION] + ); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php new file mode 100644 index 0000000000000..e81817282dcc0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/RemoveObsoleteContentAsset.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\MediaContentApi\Api\DeleteContentAssetLinksInterface; +use Magento\MediaContentSynchronization\Model\ResourceModel\GetOutdatedRelations; +use Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface; + +/** + * Remove obsolete content asset from deleted entities + */ +class RemoveObsoleteContentAsset +{ + /** + * @var GetEntitiesInterface + */ + private $getEntities; + + /** + * @var GetOutdatedRelations + */ + private $getOutdatedRelations; + + /** + * @var DeleteContentAssetLinksInterface + */ + private $deleteContentAssetLinks; + + /** + * @param DeleteContentAssetLinksInterface $deleteContentAssetLinks + * @param GetEntitiesInterface $getEntities + * @param GetOutdatedRelations $getOutdatedRelations + */ + public function __construct( + DeleteContentAssetLinksInterface $deleteContentAssetLinks, + GetEntitiesInterface $getEntities, + GetOutdatedRelations $getOutdatedRelations + ) { + $this->deleteContentAssetLinks = $deleteContentAssetLinks; + $this->getEntities = $getEntities; + $this->getOutdatedRelations = $getOutdatedRelations; + } + + /** + * Remove media content if entity already deleted. + */ + public function execute(): void + { + foreach ($this->getEntities->execute() as $entity) { + $assetsLinks = $this->getOutdatedRelations->execute($entity); + if (!empty($assetsLinks)) { + $this->deleteContentAssetLinks->execute($assetsLinks); + } + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php new file mode 100644 index 0000000000000..37271ce469715 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/ResourceModel/GetOutdatedRelations.php @@ -0,0 +1,120 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model\ResourceModel; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\EntityManager\MetadataPool; +use Magento\Framework\Exception\CouldNotDeleteException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterface; +use Magento\MediaContentApi\Api\Data\ContentAssetLinkInterfaceFactory; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Psr\Log\LoggerInterface; + +/** + * Returns asset links which entities has been deleted. + */ +class GetOutdatedRelations +{ + private const MEDIA_CONTENT_ASSET_TABLE = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var MetadataPool + */ + private $metadataPool; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var ContentAssetLinkInterfaceFactory + */ + private $contentAssetLinkFactory; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param ContentAssetLinkInterfaceFactory $contentAssetLinkFactory + * @param MetadataPool $metadataPool + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + ContentAssetLinkInterfaceFactory $contentAssetLinkFactory, + MetadataPool $metadataPool, + ResourceConnection $resourceConnection, + LoggerInterface $logger + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->contentAssetLinkFactory = $contentAssetLinkFactory; + $this->metadataPool = $metadataPool; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Returns content asset links wichs entity_id not exist anymore. + * + * @param string $entityType + * @throws CouldNotDeleteException + * @return ContentAssetLinkInterface[] + */ + public function execute(string $entityType): array + { + $contentAssetLinks= []; + try { + $entityData = $this->metadataPool->getMetadata($entityType); + $connection = $this->resourceConnection->getConnection(); + $mediaContentTable = $this->resourceConnection->getTableName(self::MEDIA_CONTENT_ASSET_TABLE); + $select = $connection->select(); + + $select->from(['mca' => $mediaContentTable], ['asset_id', 'entity_id', 'entity_type', 'field']); + $select->joinLeft( + ['et' => $entityData->getEntityTable()], + 'et.' . $entityData->getIdentifierField() . ' = mca.entity_id ', + [$entityData->getIdentifierField() . ' AS entity_identifier'] + ); + $select->where('et.' . $entityData->getIdentifierField() . ' IS NULL'); + $select->where('mca.entity_type = ?', $entityData->getEavEntityType() ?? $entityData->getEntityTable()); + $assets = $connection->fetchAll($select); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException(__('Could not fetch media content links data'), $exception); + } + + foreach ($assets as $asset) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => $asset['entity_type'], + 'entityId' => $asset['entity_id'], + 'field' => $asset['field'] + ] + ); + $contentAssetLinks[] = $this->contentAssetLinkFactory->create( + [ + 'assetId' => $asset['asset_id'], + 'contentIdentity' => $contentIdentity + ] + ); + } + + return $contentAssetLinks; + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php new file mode 100644 index 0000000000000..cea8cc6ad44da --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Model/Synchronize.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\FlagManager; +use Magento\Framework\Stdlib\DateTime\DateTimeFactory; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaContentSynchronizationApi\Model\SynchronizerPool; +use Psr\Log\LoggerInterface; + +/** + * Synchronize content with assets + */ +class Synchronize implements SynchronizeInterface +{ + private const LAST_EXECUTION_TIME_CODE = 'media_content_last_execution'; + + /** + * @var DateTimeFactory + */ + private $dateFactory; + + /** + * @var FlagManager + */ + private $flagManager; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizerPool + */ + private $synchronizerPool; + + /** + * @var RemoveObsoleteContentAsset + */ + private $removeObsoleteContent; + + /** + * @param RemoveObsoleteContentAsset $removeObsoleteContent + * @param DateTimeFactory $dateFactory + * @param FlagManager $flagManager + * @param LoggerInterface $log + * @param SynchronizerPool $synchronizerPool + */ + public function __construct( + RemoveObsoleteContentAsset $removeObsoleteContent, + DateTimeFactory $dateFactory, + FlagManager $flagManager, + LoggerInterface $log, + SynchronizerPool $synchronizerPool + ) { + $this->removeObsoleteContent = $removeObsoleteContent; + $this->dateFactory = $dateFactory; + $this->flagManager = $flagManager; + $this->log = $log; + $this->synchronizerPool = $synchronizerPool; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $failed = []; + + foreach ($this->synchronizerPool->get() as $name => $synchronizer) { + try { + $synchronizer->execute(); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following content synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + + $this->setLastExecutionTime(); + $this->removeObsoleteContent->execute(); + } + + /** + * Set last synchronizer execution time + */ + private function setLastExecutionTime(): void + { + $currentTime = $this->dateFactory->create()->gmtDate(); + $this->flagManager->saveFlag(self::LAST_EXECUTION_TIME_CODE, $currentTime); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php new file mode 100644 index 0000000000000..e428f7d273bb4 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/Plugin/SynchronizeMediaContent.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronization\Plugin; + +use Magento\MediaContentSynchronization\Model\Publish; +use Magento\MediaGallerySynchronization\Model\Consume; + +/** + * Run media content synchronization after the media files consumer finish files synchronization. + */ +class SynchronizeMediaContent +{ + /** + * @var Publish + */ + private $publish; + + /** + * @param Publish $publish + */ + public function __construct(Publish $publish) + { + $this->publish = $publish; + } + + /** + * Publish content synchronization request message to the queue. + * + * @param Consume $subject + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute(Consume $subject): void + { + $this->publish->execute(); + } +} diff --git a/app/code/Magento/MediaContentSynchronization/README.md b/app/code/Magento/MediaContentSynchronization/README.md new file mode 100644 index 0000000000000..69098ab02eb0b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/README.md @@ -0,0 +1,14 @@ +# Magento_MediaContentSynchronization module + +The Magento_MediaContentSynchronization module represents implementation of synchronization between data and objects contains +media asset information. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronization module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronization/composer.json b/app/code/Magento/MediaContentSynchronization/composer.json new file mode 100644 index 0000000000000..3be5f535487ec --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/composer.json @@ -0,0 +1,27 @@ +{ + "name": "magento/module-media-content-synchronization", + "description": "Magento module provides implementation of the media content data synchronization.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/framework-message-queue": "*", + "magento/module-media-content-api": "*" + }, + "suggest": { + "magento/module-media-gallery-synchronization": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronization\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronization/etc/communication.xml b/app/code/Magento/MediaContentSynchronization/etc/communication.xml new file mode 100644 index 0000000000000..e3436aee85331 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/communication.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="media.content.synchronization" is_synchronous="false" request="string[]"> + <handler name="media.content.synchronization.handler" + type="Magento\MediaContentSynchronization\Model\Consume" method="execute"/> + </topic> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/di.xml b/app/code/Magento/MediaContentSynchronization/etc/di.xml new file mode 100644 index 0000000000000..d4615c15206e5 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/di.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaContentSynchronization\Model\Synchronize"/> + <type name="Magento\Framework\Console\CommandListInterface"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="mediaContentSynchronization" xsi:type="object">Magento\MediaContentSynchronization\Console\Command\Synchronize</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronization\Model\Consume"> + <plugin name="synchronize_media_content" + type="Magento\MediaContentSynchronization\Plugin\SynchronizeMediaContent"/> + </type> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/module.xml b/app/code/Magento/MediaContentSynchronization/etc/module.xml new file mode 100644 index 0000000000000..7f04d9b57d8a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaContentSynchronization" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml new file mode 100644 index 0000000000000..6a141c04c59a0 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?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-message-queue:etc/consumer.xsd"> + <consumer name="media.content.synchronization" queue="media.content.synchronization" + connection="db" handler="Magento\MediaContentSynchronization\Model\Consume::execute"/> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml new file mode 100644 index 0000000000000..9751d1161b2f2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<!-- +/** + * 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-message-queue:etc/publisher.xsd"> + <publisher topic="media.content.synchronization"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml b/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml new file mode 100644 index 0000000000000..4dc43ef1ac13f --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/etc/queue_topology.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="MediaContentSynchronization" topic="media.content.synchronization" + destinationType="queue" destination="media.content.synchronization"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaContentSynchronization/registration.php b/app/code/Magento/MediaContentSynchronization/registration.php new file mode 100644 index 0000000000000..a157f7ec90a6a --- /dev/null +++ b/app/code/Magento/MediaContentSynchronization/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaContentSynchronization', + __DIR__ +); diff --git a/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeInterface.php new file mode 100644 index 0000000000000..759f226660278 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Api/SynchronizeInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Api; + +/** + * Synchronize assets and contents + */ +interface SynchronizeInterface +{ + /** + * Synchronize assets and contents + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(): void; +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/LICENSE.txt b/app/code/Magento/MediaContentSynchronizationApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php new file mode 100644 index 0000000000000..38129b2b1c6b9 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntities.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +/** + * Configuration of entities used for media content. + */ +class GetEntities implements GetEntitiesInterface +{ + /** + * @var array + */ + private $entities; + + /** + * @param array $entities + */ + public function __construct( + array $entities = [] + ) { + $this->entities = $entities; + } + + /** + * Get all entities configuration used in media content. + * + * @return array + */ + public function execute(): array + { + return $this->entities; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php new file mode 100644 index 0000000000000..ad62ae4136378 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/GetEntitiesInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +/** + * Get entities for media content by provided configuration. + */ +interface GetEntitiesInterface +{ + /** + * Get entities that used for media content + * + * @return array + */ + public function execute(): array; +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerPool.php b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerPool.php new file mode 100644 index 0000000000000..ca18214201c6a --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/Model/SynchronizerPool.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationApi\Model; + +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; + +/** + * A pool that handles content and assets synchronization. + * @see SynchronizeFilesInterface + */ +class SynchronizerPool +{ + /** + * Content with assets synchronizers + * + * @var SynchronizeInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeInterface[] $synchronizers + */ + public function __construct( + array $synchronizers = [] + ) { + foreach ($synchronizers as $synchronizer) { + if (!$synchronizer instanceof SynchronizeInterface) { + throw new \InvalidArgumentException( + get_class($synchronizer) . ' must implement ' . SynchronizeInterface::class + ); + } + } + + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/README.md b/app/code/Magento/MediaContentSynchronizationApi/README.md new file mode 100644 index 0000000000000..25ceae24452f1 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentSynchronizationApi module + +The Magento_MediaContentSynchronizationApi module is responsible for the media gallery data synchronization implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaContentSynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContentSynchronizationApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationApi/composer.json b/app/code/Magento/MediaContentSynchronizationApi/composer.json new file mode 100644 index 0000000000000..1f1e5e4b51c5b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-content-synchronization-api", + "description": "Magento module responsible for the media content synchronization implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml new file mode 100644 index 0000000000000..76bdd9b1cb162 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/etc/di.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface" type="Magento\MediaContentSynchronizationApi\Model\GetEntities"/> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml b/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml new file mode 100644 index 0000000000000..3a149b31da3cb --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaContentSynchronizationApi" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationApi/registration.php b/app/code/Magento/MediaContentSynchronizationApi/registration.php new file mode 100644 index 0000000000000..965e31fa45516 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaContentSynchronizationApi', + __DIR__ +); diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE.txt b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php new file mode 100644 index 0000000000000..6b8f99ee6721c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Category.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize category content with assets + */ +class Category implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'catalog_category'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const CATEGORY_TABLE = 'catalog_category_entity'; + private const CATEGORY_IDENTITY_FIELD = 'entity_id'; + private const CATEGORY_UPDATED_AT_FIELD = 'updated_at'; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @var array + */ + private $fields; + + /** + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param GetEntityContentsInterface $getEntityContents + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param FetchBatchesInterface $fetchBatches + * @param array $fields + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + GetEntityContentsInterface $getEntityContents, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + FetchBatchesInterface $fetchBatches, + array $fields = [] + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [ + self::CATEGORY_IDENTITY_FIELD, + self::CATEGORY_UPDATED_AT_FIELD + ]; + foreach ($this->fetchBatches->execute(self::CATEGORY_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CATEGORY_IDENTITY_FIELD] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php new file mode 100644 index 0000000000000..486f3482b592d --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Model/Synchronizer/Product.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentApi\Model\GetEntityContentsInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize product content with assets + */ +class Product implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'catalog_product'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const PRODUCT_TABLE = 'catalog_product_entity'; + private const PRODUCT_TABLE_ENTITY_ID = 'entity_id'; + private const PRODUCT_TABLE_UPDATED_AT_FIELD = 'updated_at'; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var GetEntityContentsInterface + */ + private $getEntityContents; + + /** + * @var array + */ + private $fields; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param GetEntityContentsInterface $getEntityContents + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param FetchBatchesInterface $fetchBatches + * @param array $fields + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + GetEntityContentsInterface $getEntityContents, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + FetchBatchesInterface $fetchBatches, + array $fields = [] + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->getEntityContents = $getEntityContents; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fetchBatches = $fetchBatches; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = [self::PRODUCT_TABLE_ENTITY_ID, self::PRODUCT_TABLE_UPDATED_AT_FIELD]; + foreach ($this->fetchBatches->execute(self::PRODUCT_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize product entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $contentIdentity = $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::PRODUCT_TABLE_ENTITY_ID] + ] + ); + $this->updateContentAssetLinks->execute( + $contentIdentity, + implode(PHP_EOL, $this->getEntityContents->execute($contentIdentity)) + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/README.md b/app/code/Magento/MediaContentSynchronizationCatalog/README.md new file mode 100644 index 0000000000000..8395ffc10d4d2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCatalog module + +The Magento_MediaContentCatalog provides the implementation of MediaContentSyncronization functionality for Magento_Catalog module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php new file mode 100644 index 0000000000000..b8f12bad6bd77 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/CategoryTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Test\Integration\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Category; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for categories synchronization + */ +class CategoryTest extends TestCase +{ + /** + * @var Category + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Category::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between category and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $categoryId = 28767; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_category', + 'field' => 'description', + 'entityId' => $categoryId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($categoryId, $syncedContentIdentity->getEntityId()); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php new file mode 100644 index 0000000000000..247fdf4a770ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/Test/Integration/Model/Synchronizer/ProductTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCatalog\Test\Integration\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for products synchronization + */ +class ProductTest extends TestCase +{ + /** + * @var Product + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Product::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between products and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $productId = 1567; + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'catalog_product', + 'field' => 'description', + 'entityId' => $productId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(2, count($synchronizedContentIdentities)); + foreach ($synchronizedContentIdentities as $syncedContentIdentity) { + $this->assertEquals($productId, $syncedContentIdentity->getEntityId()); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/composer.json b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json new file mode 100644 index 0000000000000..733f29d3a42c2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-catalog", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Catalog module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCatalog\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml new file mode 100644 index 0000000000000..8cc86fde8fbcd --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/di.xml @@ -0,0 +1,41 @@ +<?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\MediaContentSynchronizationCatalog\Model\Synchronizer\Category"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="image" xsi:type="string">image</item> + <item name="description" xsi:type="string">description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface"> + <arguments> + <argument name="entities" xsi:type="array"> + <item name="catalog_product" xsi:type="string">Magento\Catalog\Api\Data\ProductInterface</item> + <item name="catalog_category" xsi:type="string">Magento\Catalog\Api\Data\CategoryInterface</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="description" xsi:type="string">description</item> + <item name="short_description" xsi:type="string">short_description</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationApi\Model\SynchronizerPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_category_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Category</item> + <item name="media_content_product_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCatalog\Model\Synchronizer\Product</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml new file mode 100644 index 0000000000000..9660dcb107b45 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaContentSynchronizationCatalog" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationCatalog/registration.php b/app/code/Magento/MediaContentSynchronizationCatalog/registration.php new file mode 100644 index 0000000000000..1e8b47dc15b50 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCatalog/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaContentSynchronizationCatalog', + __DIR__ +); diff --git a/app/code/Magento/MediaContentSynchronizationCms/LICENSE.txt b/app/code/Magento/MediaContentSynchronizationCms/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt b/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php new file mode 100644 index 0000000000000..c3da5d4ae5785 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Block.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize block content with assets + */ +class Block implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'cms_block'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const CMS_BLOCK_TABLE = 'cms_block'; + private const CMS_BLOCK_TABLE_ENTITY_ID = 'block_id'; + private const CMS_BLOCK_TABLE_UPDATED_AT_FIELD = 'update_time'; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var array + */ + private $fields; + + /** + * Synchronize block content with assets + * + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param FetchBatchesInterface $fetchBatches + * @param array $fields + */ + public function __construct( + ContentIdentityInterfaceFactory $contentIdentityFactory, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + FetchBatchesInterface $fetchBatches, + array $fields = [] + ) { + $this->contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + $this->fetchBatches = $fetchBatches; + } + + /** + * Synchronize assets and contents + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_BLOCK_TABLE_ENTITY_ID, + self::CMS_BLOCK_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_BLOCK_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize block entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_BLOCK_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php new file mode 100644 index 0000000000000..2d1b04d295973 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Model/Synchronizer/Page.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Model\Synchronizer; + +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\UpdateContentAssetLinksInterface; +use Magento\MediaContentSynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; + +/** + * Synchronize page content with assets + */ +class Page implements SynchronizeInterface +{ + private const CONTENT_TYPE = 'cms_page'; + private const TYPE = 'entityType'; + private const ENTITY_ID = 'entityId'; + private const FIELD = 'field'; + private const CMS_PAGE_TABLE = 'cms_page'; + private const CMS_PAGE_TABLE_ENTITY_ID = 'page_id'; + private const CMS_PAGE_TABLE_UPDATED_AT_FIELD = 'update_time'; + + /** + * @var FetchBatchesInterface + */ + private $fetchBatches; + + /** + * @var UpdateContentAssetLinksInterface + */ + private $updateContentAssetLinks; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @var array + */ + private $fields; + + /** + * Synchronize page content with assets + * + * @param FetchBatchesInterface $fetchBatches + * @param ContentIdentityInterfaceFactory $contentIdentityFactory + * @param UpdateContentAssetLinksInterface $updateContentAssetLinks + * @param array $fields + */ + public function __construct( + FetchBatchesInterface $fetchBatches, + ContentIdentityInterfaceFactory $contentIdentityFactory, + UpdateContentAssetLinksInterface $updateContentAssetLinks, + array $fields = [] + ) { + $this->fetchBatches = $fetchBatches; + $this->contentIdentityFactory = $contentIdentityFactory; + $this->updateContentAssetLinks = $updateContentAssetLinks; + $this->fields = $fields; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $columns = array_merge( + [ + self::CMS_PAGE_TABLE_ENTITY_ID, + self::CMS_PAGE_TABLE_UPDATED_AT_FIELD + ], + array_values($this->fields) + ); + foreach ($this->fetchBatches->execute(self::CMS_PAGE_TABLE, $columns, $columns[1]) as $batch) { + foreach ($batch as $item) { + $this->synchronizeItem($item); + } + } + } + + /** + * Synchronize page entity fields + * + * @param array $item + */ + private function synchronizeItem(array $item): void + { + foreach ($this->fields as $field) { + $this->updateContentAssetLinks->execute( + $this->contentIdentityFactory->create( + [ + self::TYPE => self::CONTENT_TYPE, + self::FIELD => $field, + self::ENTITY_ID => $item[self::CMS_PAGE_TABLE_ENTITY_ID] + ] + ), + (string) $item[$field] + ); + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/README.md b/app/code/Magento/MediaContentSynchronizationCms/README.md new file mode 100644 index 0000000000000..58582b1b2d706 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/README.md @@ -0,0 +1,13 @@ +# Magento_MediaContentCms module + +The Magento_MediaContentCms provides the implementation of MediaContentSyncronization functionality for Magento_Cms module + +## Extensibility + +Extension developers can interact with the Magento_MediaContent module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaContent module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php new file mode 100644 index 0000000000000..2737ab524584b --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/BlockTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Test\Integration\Model\Synchronizer; + +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Cms\Api\Data\BlockInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for blocks synchronization + */ +class BlockTest extends TestCase +{ + /** + * @var Block + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Block::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between blocks and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $blockId = $this->getBlock('fixture_block_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_block', + 'field' => 'content', + 'entityId' => $blockId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($blockId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * Get fixture block + * + * @param string $identifier + * @return BlockInterface + * @throws LocalizedException + */ + private function getBlock(string $identifier): BlockInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var BlockRepositoryInterface $blockRepository */ + $blockRepository = $objectManager->get(BlockRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(BlockInterface::IDENTIFIER, $identifier) + ->create(); + + return current($blockRepository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php new file mode 100644 index 0000000000000..1dcbb96dc7914 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/Test/Integration/Model/Synchronizer/PageTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentSynchronizationCms\Test\Integration\Model\Synchronizer; + +use Magento\Cms\Api\Data\PageInterface; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaContentApi\Api\Data\ContentIdentityInterfaceFactory; +use Magento\MediaContentApi\Api\GetAssetIdsByContentIdentityInterface; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for pages synchronization + */ +class PageTest extends TestCase +{ + /** + * @var Page + */ + private $synchronizer; + + /** + * @var GetAssetIdsByContentIdentityInterface + */ + private $getAssetIds; + + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @var ContentIdentityInterfaceFactory + */ + private $contentIdentityFactory; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $this->synchronizer = Bootstrap::getObjectManager()->get(Page::class); + $this->getAssetIds = Bootstrap::getObjectManager()->get(GetAssetIdsByContentIdentityInterface::class); + $this->getContentIdentities = Bootstrap::getObjectManager()->get(GetContentByAssetIdsInterface::class); + $this->contentIdentityFactory = Bootstrap::getObjectManager()->get(ContentIdentityInterfaceFactory::class); + } + + /** + * Test synchronization between pages and media assets (fixtures sequence does matter) + * + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(): void + { + $assetId = 2020; + $pageId = $this->getPage('fixture_page_with_asset')->getId(); + $contentIdentity = $this->contentIdentityFactory->create( + [ + 'entityType' => 'cms_page', + 'field' => 'content', + 'entityId' => $pageId + ] + ); + + $this->assertEmpty($this->getContentIdentities->execute([$assetId])); + $this->assertEmpty($this->getAssetIds->execute($contentIdentity)); + + $this->synchronizer->execute(); + + $this->assertEquals([$assetId], $this->getAssetIds->execute($contentIdentity)); + + $synchronizedContentIdentities = $this->getContentIdentities->execute([$assetId]); + $this->assertEquals(1, count($synchronizedContentIdentities)); + $this->assertEquals($pageId, $synchronizedContentIdentities[0]->getEntityId()); + } + + /** + * Get fixture page + * + * @param string $identifier + * @return PageInterface + * @throws LocalizedException + */ + private function getPage(string $identifier): PageInterface + { + $objectManager = Bootstrap::getObjectManager(); + + /** @var PageRepositoryInterface $repository */ + $repository = $objectManager->get(PageRepositoryInterface::class); + + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter(PageInterface::IDENTIFIER, $identifier) + ->create(); + + return current($repository->getList($searchCriteria)->getItems()); + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/composer.json b/app/code/Magento/MediaContentSynchronizationCms/composer.json new file mode 100644 index 0000000000000..9028b9dacd0a2 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-content-synchronization-cms", + "description": "Magento module provides the implementation of MediaContentSynchronization functionality for Magento_Cms module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaContentSynchronizationCms\\": "" + } + } +} diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml new file mode 100644 index 0000000000000..7def330298789 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/di.xml @@ -0,0 +1,39 @@ +<?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\MediaContentSynchronizationApi\Model\SynchronizerPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_content_block_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block</item> + <item name="media_content_page_synchronizer" xsi:type="object">Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationApi\Model\GetEntitiesInterface"> + <arguments> + <argument name="entities" xsi:type="array"> + <item name="cms_block" xsi:type="string">Magento\Cms\Api\Data\BlockInterface</item> + <item name="cms_page" xsi:type="string">Magento\Cms\Api\Data\PageInterface</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationCms\Model\Synchronizer\Block"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaContentSynchronizationCms\Model\Synchronizer\Page"> + <arguments> + <argument name="fields" xsi:type="array"> + <item name="content" xsi:type="string">content</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml b/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml new file mode 100644 index 0000000000000..58497b81a2174 --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaContentSynchronizationCms" /> +</config> diff --git a/app/code/Magento/MediaContentSynchronizationCms/registration.php b/app/code/Magento/MediaContentSynchronizationCms/registration.php new file mode 100644 index 0000000000000..13ed4b73f70ee --- /dev/null +++ b/app/code/Magento/MediaContentSynchronizationCms/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaContentSynchronizationCms', + __DIR__ +); diff --git a/app/code/Magento/MediaGallery/Model/Asset.php b/app/code/Magento/MediaGallery/Model/Asset.php index 78b9477a70b08..7a4e51709dc0a 100644 --- a/app/code/Magento/MediaGallery/Model/Asset.php +++ b/app/code/Magento/MediaGallery/Model/Asset.php @@ -32,11 +32,21 @@ class Asset implements AssetInterface */ private $title; + /** + * @var string|null + */ + private $description; + /** * @var string|null */ private $source; + /** + * @var string|null + */ + private $hash; + /** * @var string */ @@ -80,7 +90,9 @@ class Asset implements AssetInterface * @param int $size * @param int|null $id * @param string|null $title + * @param string|null $description * @param string|null $source + * @param string|null $hash * @param string|null $createdAt * @param string|null $updatedAt * @param AssetExtensionInterface|null $extensionAttributes @@ -93,7 +105,9 @@ public function __construct( int $size, ?int $id = null, ?string $title = null, + ?string $description = null, ?string $source = null, + ?string $hash = null, ?string $createdAt = null, ?string $updatedAt = null, ?AssetExtensionInterface $extensionAttributes = null @@ -105,7 +119,9 @@ public function __construct( $this->size = $size; $this->id = $id; $this->title = $title; + $this->description = $description; $this->source = $source; + $this->hash = $hash; $this->createdAt = $createdAt; $this->updatedAt = $updatedAt; $this->extensionAttributes = $extensionAttributes; @@ -135,6 +151,14 @@ public function getTitle(): ?string return $this->title; } + /** + * @inheritdoc + */ + public function getDescription(): ?string + { + return $this->description; + } + /** * @inheritdoc */ @@ -143,6 +167,14 @@ public function getSource(): ?string return $this->source; } + /** + * @inheritdoc + */ + public function getHash(): ?string + { + return $this->hash; + } + /** * @inheritdoc */ diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php index 3abe4cb50f2ea..51e4b0c1cb24d 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByDirectoryPath.php @@ -15,7 +15,7 @@ /** * Remove asset(s) that correspond the provided directory path - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterfac */ class DeleteByDirectoryPath implements DeleteByDirectoryPathInterface diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php index fc8e5d7c84bfd..898d31a304804 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/DeleteByPath.php @@ -16,7 +16,7 @@ /** * Delete media asset by path * - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface */ class DeleteByPath implements DeleteByPathInterface diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php index b2f900233e46a..81bcbb7fe28a8 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetById.php @@ -17,7 +17,7 @@ /** * Get media asset by id - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface */ class GetById implements GetByIdInterface @@ -94,7 +94,9 @@ public function execute(int $mediaAssetId): AssetInterface 'id' => $mediaAssetData['id'], 'path' => $mediaAssetData['path'], 'title' => $mediaAssetData['title'], + 'description' => $mediaAssetData['description'], 'source' => $mediaAssetData['source'], + 'hash' => $mediaAssetData['hash'], 'contentType' => $mediaAssetData['content_type'], 'width' => $mediaAssetData['width'], 'height' => $mediaAssetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php index d9faad62b2cd1..aabc3986a21d4 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/GetByPath.php @@ -18,7 +18,7 @@ /** * Provide media asset by path * - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface */ class GetByPath implements GetByPathInterface @@ -86,7 +86,9 @@ public function execute(string $path): AssetInterface 'id' => $data['id'], 'path' => $data['path'], 'title' => $data['title'], + 'description' => $data['description'], 'source' => $data['source'], + 'hash' => $data['hash'], 'contentType' => $data['content_type'], 'width' => $data['width'], 'height' => $data['height'], diff --git a/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php b/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php index 1710176c1b3af..ba8497fe49205 100644 --- a/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php +++ b/app/code/Magento/MediaGallery/Model/Asset/Command/Save.php @@ -16,7 +16,7 @@ /** * Save media asset * - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead * @see \Magento\MediaGalleryApi\Api\SaveAssetsInterface */ class Save implements SaveInterface diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php index 4d87c1aa95285..f33c22a18b4b8 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/CreateByPaths.php @@ -10,7 +10,7 @@ use Magento\Cms\Model\Wysiwyg\Images\Storage; use Magento\Framework\Exception\CouldNotSaveException; use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Psr\Log\LoggerInterface; /** @@ -29,23 +29,23 @@ class CreateByPaths implements CreateDirectoriesByPathsInterface private $storage; /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ - private $isPathBlacklisted; + private $isPathExcluded; /** * @param LoggerInterface $logger * @param Storage $storage - * @param IsPathBlacklistedInterface $isPathBlacklisted + * @param IsPathExcludedInterface $isPathExcluded */ public function __construct( LoggerInterface $logger, Storage $storage, - IsPathBlacklistedInterface $isPathBlacklisted + IsPathExcludedInterface $isPathExcluded ) { $this->logger = $logger; $this->storage = $storage; - $this->isPathBlacklisted = $isPathBlacklisted; + $this->isPathExcluded = $isPathExcluded; } /** @@ -55,7 +55,7 @@ public function execute(array $paths): void { $failedPaths = []; foreach ($paths as $path) { - if ($this->isPathBlacklisted->execute($path)) { + if ($this->isPathExcluded->execute($path)) { $failedPaths[] = $path; continue; } diff --git a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php index d46fb854fff22..2e45000c07225 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Command/DeleteByPaths.php @@ -10,7 +10,7 @@ use Magento\Cms\Model\Wysiwyg\Images\Storage; use Magento\Framework\Exception\CouldNotDeleteException; use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Psr\Log\LoggerInterface; /** @@ -29,23 +29,23 @@ class DeleteByPaths implements DeleteDirectoriesByPathsInterface private $storage; /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ - private $isPathBlacklisted; + private $isPathExcluded; /** * @param LoggerInterface $logger * @param Storage $storage - * @param IsPathBlacklistedInterface $isPathBlacklisted + * @param IsPathExcludedInterface $isPathExcluded */ public function __construct( LoggerInterface $logger, Storage $storage, - IsPathBlacklistedInterface $isPathBlacklisted + IsPathExcludedInterface $isPathExcluded ) { $this->logger = $logger; $this->storage = $storage; - $this->isPathBlacklisted = $isPathBlacklisted; + $this->isPathExcluded = $isPathExcluded; } /** @@ -55,7 +55,7 @@ public function execute(array $paths): void { $failedPaths = []; foreach ($paths as $path) { - if ($this->isPathBlacklisted->execute($path)) { + if ($this->isPathExcluded->execute($path)) { $failedPaths[] = $path; continue; } diff --git a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php index 91f16d246f636..3d9911c805efb 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php +++ b/app/code/Magento/MediaGallery/Model/Directory/Config/Converter.php @@ -15,9 +15,9 @@ class Converter implements ConverterInterface { /** - * Blacklist tag name + * Excluded list tag name */ - private const BLACKLIST_TAG_NAME = 'blacklist'; + private const EXCLUDED_LIST_TAG_NAME = 'exclude'; /** * Patterns tag name @@ -43,12 +43,12 @@ public function convert($source): array throw new \InvalidArgumentException('The source should be instance of DOMDocument'); } - foreach ($source->getElementsByTagName(self::BLACKLIST_TAG_NAME) as $blacklist) { - $result[self::BLACKLIST_TAG_NAME] = []; - foreach ($blacklist->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) { - $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] = []; + foreach ($source->getElementsByTagName(self::EXCLUDED_LIST_TAG_NAME) as $excludedList) { + $result[self::EXCLUDED_LIST_TAG_NAME] = []; + foreach ($excludedList->getElementsByTagName(self::PATTERNS_TAG_NAME) as $patterns) { + $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] = []; foreach ($patterns->getElementsByTagName(self::PATTERN_TAG_NAME) as $pattern) { - $result[self::BLACKLIST_TAG_NAME][self::PATTERNS_TAG_NAME] + $result[self::EXCLUDED_LIST_TAG_NAME][self::PATTERNS_TAG_NAME] [$pattern->attributes->getNamedItem('name')->nodeValue] = $pattern->nodeValue; } } diff --git a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php similarity index 68% rename from app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php rename to app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php index 8fdd4f70d5060..29ed5fbf04ecd 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/BlacklistPatternsConfig.php +++ b/app/code/Magento/MediaGallery/Model/Directory/ExcludedPatternsConfig.php @@ -8,14 +8,14 @@ namespace Magento\MediaGallery\Model\Directory; use Magento\Framework\Config\DataInterface; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; /** * Media gallery directory config */ -class BlacklistPatternsConfig implements BlacklistPatternsConfigInterface +class ExcludedPatternsConfig implements ExcludedPatternsConfigInterface { - private const XML_PATH_BLACKLIST_PATTERNS = 'blacklist/patterns'; + private const XML_PATH_EXCLUDED_PATTERNS = 'exclude/patterns'; /** * @var DataInterface @@ -37,6 +37,6 @@ public function __construct(DataInterface $data) */ public function get() : array { - return $this->data->get(self::XML_PATH_BLACKLIST_PATTERNS); + return $this->data->get(self::XML_PATH_EXCLUDED_PATTERNS); } } diff --git a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php similarity index 61% rename from app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php rename to app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php index 0191b357aaefa..8fb0e03b76548 100644 --- a/app/code/Magento/MediaGallery/Model/Directory/IsBlacklisted.php +++ b/app/code/Magento/MediaGallery/Model/Directory/IsExcluded.php @@ -7,23 +7,23 @@ namespace Magento\MediaGallery\Model\Directory; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; /** - * Check if the path is blacklisted for media gallery. Directory path may be blacklisted if it's reserved by the system + * Check if the path is excluded for media gallery. Directory path may be blacklisted if it's reserved by the system */ -class IsBlacklisted implements IsPathBlacklistedInterface +class IsExcluded implements IsPathExcludedInterface { /** - * @var BlacklistPatternsConfigInterface + * @var ExcludedPatternsConfigInterface */ private $config; /** - * @param BlacklistPatternsConfigInterface $config + * @param ExcludedPatternsConfigInterface $config */ - public function __construct(BlacklistPatternsConfigInterface $config) + public function __construct(ExcludedPatternsConfigInterface $config) { $this->config = $config; } diff --git a/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php b/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php index 4118fd1495dbb..a37eaa8fc11fa 100644 --- a/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php +++ b/app/code/Magento/MediaGallery/Model/Keyword/Command/GetAssetKeywords.php @@ -16,7 +16,7 @@ /** * Retrieve keywords for the media asset - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead */ class GetAssetKeywords implements GetAssetKeywordsInterface { diff --git a/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php index f21db25bac767..aa9f05af70b2f 100644 --- a/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php +++ b/app/code/Magento/MediaGallery/Model/Keyword/Command/SaveAssetKeywords.php @@ -18,7 +18,7 @@ /** * Save media asset keywords to database - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetKeywordsInterface instead + * @deprecated 100.4.0 use \Magento\MediaGalleryApi\Api\SaveAssetKeywordsInterface instead */ class SaveAssetKeywords implements SaveAssetKeywordsInterface { diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php index 53185939b2283..f73162b775683 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByIds.php @@ -65,7 +65,9 @@ public function execute(array $ids): array 'id' => $assetData['id'], 'path' => $assetData['path'], 'title' => $assetData['title'], + 'description' => $assetData['description'], 'source' => $assetData['source'], + 'hash' => $assetData['hash'], 'contentType' => $assetData['content_type'], 'width' => $assetData['width'], 'height' => $assetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php index 5593083d9673a..b25d2e22aabd4 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsByPaths.php @@ -66,7 +66,9 @@ public function execute(array $paths): array 'id' => $assetData['id'], 'path' => $assetData['path'], 'title' => $assetData['title'], + 'description' => $assetData['description'], 'source' => $assetData['source'], + 'hash' => $assetData['hash'], 'contentType' => $assetData['content_type'], 'width' => $assetData['width'], 'height' => $assetData['height'], diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsBySearchCriteria.php b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsBySearchCriteria.php new file mode 100644 index 0000000000000..3f3aaac17947d --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/GetAssetsBySearchCriteria.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model\ResourceModel; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\App\ResourceConnection; +use Psr\Log\LoggerInterface; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Api\Search\SearchResultFactory; +use Magento\Framework\DB\Select; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Get assets data by searchCriteria + */ +class GetAssetsBySearchCriteria +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var SearchResultFactory + */ + private $searchResultFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param SearchResultFactory $searchResultFactory + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + */ + public function __construct( + SearchResultFactory $searchResultFactory, + ResourceConnection $resourceConnection, + LoggerInterface $logger + ) { + $this->searchResultFactory = $searchResultFactory; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + } + + /** + * Retrieve assets data from database + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchResultInterface + */ + public function execute(SearchCriteriaInterface $searchCriteria): SearchResultInterface + { + $searchResult = $this->searchResultFactory->create(); + $fields = []; + $conditions = []; + + foreach ($searchCriteria->getFilterGroups() as $filterGroup) { + foreach ($filterGroup->getFilters() as $filter) { + $condition = $filter->getConditionType() ? $filter->getConditionType() : 'eq'; + $fields[] = $filter->getField(); + + if ($condition === 'fulltext') { + $condition = 'like'; + $filter->setValue('%' . $filter->getValue() . '%'); + } + + $conditions[] = [$condition => $filter->getValue()]; + } + } + + if ($fields) { + $resultCondition = $this->getResultCondition($fields, $conditions); + $select = $this->resourceConnection->getConnection()->select() + ->from( + $this->resourceConnection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET) + ) + ->where($resultCondition, null, Select::TYPE_CONDITION); + + if ($searchCriteria->getPageSize() || $searchCriteria->getCurrentPage()) { + $select->limit( + $searchCriteria->getPageSize(), + $searchCriteria->getCurrentPage() * $searchCriteria->getPageSize() + ); + } + + $data = $this->resourceConnection->getConnection()->fetchAll($select); + } + + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($data); + + return $searchResult; + } + + /** + * Get conditions data by searchCriteria + * + * @param string|array $field + * @param null|string|array $condition + */ + public function getResultCondition($field, $condition = null) + { + $resourceConnection = $this->resourceConnection->getConnection(); + if (is_array($field)) { + $conditions = []; + foreach ($field as $key => $value) { + $conditions[] = $resourceConnection->prepareSqlCondition( + $resourceConnection->quoteIdentifier($value), + isset($condition[$key]) ? $condition[$key] : null + ); + } + + $resultCondition = '(' . implode(') ' . Select::SQL_OR . ' (', $conditions) . ')'; + } else { + $resultCondition = $resourceConnection->prepareSqlCondition( + $resourceConnection->quoteIdentifier($field), + $condition + ); + } + return $resultCondition; + } +} diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php index 3437cc1c519e8..eb6bd2aad236c 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetLinks.php @@ -10,8 +10,10 @@ use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; +use Magento\Framework\Exception\CouldNotDeleteException; use Magento\Framework\Exception\CouldNotSaveException; use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use Psr\Log\LoggerInterface; /** @@ -22,55 +24,90 @@ class SaveAssetLinks private const TABLE_ASSET_KEYWORD = 'media_gallery_asset_keyword'; private const FIELD_ASSET_ID = 'asset_id'; private const FIELD_KEYWORD_ID = 'keyword_id'; + private const TABLE_MEDIA_ASSET = 'media_gallery_asset'; /** * @var ResourceConnection */ private $resourceConnection; + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetsKeywords; + /** * @var LoggerInterface */ private $logger; /** + * @param GetAssetsKeywordsInterface $getAssetsKeywords * @param ResourceConnection $resourceConnection * @param LoggerInterface $logger */ public function __construct( + GetAssetsKeywordsInterface $getAssetsKeywords, ResourceConnection $resourceConnection, LoggerInterface $logger ) { + $this->getAssetsKeywords = $getAssetsKeywords; $this->resourceConnection = $resourceConnection; $this->logger = $logger; } /** - * Save asset keywords links + * Process insert and deletion of asset keywords links * * @param int $assetId * @param KeywordInterface[] $keywordIds * + * @throws CouldNotDeleteException * @throws CouldNotSaveException */ public function execute(int $assetId, array $keywordIds): void { + $currentKeywordIds = $this->getCurrentKeywordIds($assetId); + + $obsoleteKeywordIds = array_diff($currentKeywordIds, $keywordIds); + $newKeywordIds = array_diff($keywordIds, $currentKeywordIds); + + $this->deleteAssetKeywords($assetId, $obsoleteKeywordIds); + $this->insertAssetKeywords($assetId, $newKeywordIds); + + if ($obsoleteKeywordIds || $newKeywordIds) { + $this->setAssetUpdatedAt($assetId); + } + } + + /** + * Save new asset keyword links + * + * @param int $assetId + * @param int[] $keywordIds + * + * @throws CouldNotSaveException + */ + private function insertAssetKeywords(int $assetId, array $keywordIds): void + { + if (empty($keywordIds)) { + return; + } try { $values = []; + foreach ($keywordIds as $keywordId) { $values[] = [$assetId, $keywordId]; } - if (!empty($values)) { - /** @var Mysql $connection */ - $connection = $this->resourceConnection->getConnection(); - $connection->insertArray( - $this->resourceConnection->getTableName(self::TABLE_ASSET_KEYWORD), - [self::FIELD_ASSET_ID, self::FIELD_KEYWORD_ID], - $values, - AdapterInterface::INSERT_IGNORE - ); - } + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->insertArray( + $this->resourceConnection->getTableName(self::TABLE_ASSET_KEYWORD), + [self::FIELD_ASSET_ID, self::FIELD_KEYWORD_ID], + $values, + AdapterInterface::INSERT_IGNORE + ); } catch (\Exception $exception) { $this->logger->critical($exception); throw new CouldNotSaveException( @@ -79,4 +116,96 @@ public function execute(int $assetId, array $keywordIds): void ); } } + + /** + * Delete obsolete asset keyword links + * + * @param int $assetId + * @param int[] $obsoleteKeywordIds + * @throws CouldNotDeleteException + */ + private function deleteAssetKeywords(int $assetId, array $obsoleteKeywordIds): void + { + if (empty($obsoleteKeywordIds)) { + return; + } + try { + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->delete( + $connection->getTableName( + self::TABLE_ASSET_KEYWORD + ), + [ + self::FIELD_KEYWORD_ID . ' in (?)' => $obsoleteKeywordIds, + self::FIELD_ASSET_ID . ' = ?' => $assetId + ] + ); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new CouldNotDeleteException( + __('Could not delete obsolete asset keyword links'), + $exception + ); + } + } + + /** + * Get current keyword ids of an asset + * + * @param int $assetId + * @return int[] + */ + private function getCurrentKeywordIds(int $assetId): array + { + $currentKeywordsData = $this->getAssetsKeywords->execute([$assetId]); + + if (empty($currentKeywordsData)) { + return []; + } + + return $this->getKeywordIdsFromKeywordData( + $currentKeywordsData[$assetId]->getKeywords() + ); + } + + /** + * Get keyword ids from keyword data + * + * @param KeywordInterface[] $keywordsData + * @return int[] + */ + private function getKeywordIdsFromKeywordData(array $keywordsData): array + { + return array_map( + function (KeywordInterface $keyword): int { + return $keyword->getId(); + }, + $keywordsData + ); + } + + /** + * Updates modified date of media asset + * + * @param int $assetId + * @throws CouldNotSaveException + */ + private function setAssetUpdatedAt(int $assetId): void + { + try { + $connection = $this->resourceConnection->getConnection(); + $connection->update( + $connection->getTableName(self::TABLE_MEDIA_ASSET), + ['updated_at' => null], + ['id =?' => $assetId] + ); + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new CouldNotSaveException( + __('Could not update assets modified date'), + $exception + ); + } + } } diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php index a97c5f602c5c7..56bdfda49d84c 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/Keyword/SaveAssetsKeywords.php @@ -93,19 +93,17 @@ private function saveAssetKeywords(array $keywords, int $assetId): void $data[] = $keyword->getKeyword(); } - if (empty($data)) { - return; + if (!empty($data)) { + /** @var Mysql $connection */ + $connection = $this->resourceConnection->getConnection(); + $connection->insertArray( + $this->resourceConnection->getTableName(self::TABLE_KEYWORD), + [self::KEYWORD], + $data, + AdapterInterface::INSERT_IGNORE + ); } - /** @var Mysql $connection */ - $connection = $this->resourceConnection->getConnection(); - $connection->insertArray( - $this->resourceConnection->getTableName(self::TABLE_KEYWORD), - [self::KEYWORD], - $data, - AdapterInterface::INSERT_IGNORE - ); - $this->saveAssetLinks->execute($assetId, $this->getKeywordIds($data)); } diff --git a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php index ec08addf93462..801279aa7fd7d 100644 --- a/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php +++ b/app/code/Magento/MediaGallery/Model/ResourceModel/SaveAssets.php @@ -60,7 +60,9 @@ public function execute(array $assets): void 'id' => $asset->getId(), 'path' => $asset->getPath(), 'title' => $asset->getTitle(), + 'description' => $asset->getDescription(), 'source' => $asset->getSource(), + 'hash' => $asset->getHash(), 'content_type' => $asset->getContentType(), 'width' => $asset->getWidth(), 'height' => $asset->getHeight(), diff --git a/app/code/Magento/MediaGallery/Model/SearchAssets.php b/app/code/Magento/MediaGallery/Model/SearchAssets.php new file mode 100644 index 0000000000000..69678e3cacc13 --- /dev/null +++ b/app/code/Magento/MediaGallery/Model/SearchAssets.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Psr\Log\LoggerInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\MediaGallery\Model\ResourceModel\GetAssetsBySearchCriteria; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; + +/** + * Get media assets by searchCriteria + */ +class SearchAssets implements SearchAssetsInterface +{ + /** + * @var GetAssetsBySearchCriteria + */ + private $getAssetsBySearchCriteria; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var AssetInterfaceFactory + */ + private $mediaAssetFactory; + + /** + * @param GetAssetsBySearchCriteria $getAssetsBySearchCriteria + * @param AssetInterfaceFactory $mediaAssetFactory + * @param LoggerInterface $logger + */ + public function __construct( + GetAssetsBySearchCriteria $getAssetsBySearchCriteria, + AssetInterfaceFactory $mediaAssetFactory, + LoggerInterface $logger + ) { + $this->getAssetsBySearchCriteria = $getAssetsBySearchCriteria; + $this->mediaAssetFactory = $mediaAssetFactory; + $this->logger = $logger; + } + + /** + * @inheritdoc + */ + public function execute(SearchCriteriaInterface $searchCriteria): array + { + $assets = []; + try { + foreach ($this->getAssetsBySearchCriteria->execute($searchCriteria)->getItems() as $assetData) { + $assets[] = $this->mediaAssetFactory->create( + [ + 'id' => $assetData['id'], + 'path' => $assetData['path'], + 'title' => $assetData['title'], + 'description' => $assetData['description'], + 'source' => $assetData['source'], + 'hash' => $assetData['hash'], + 'contentType' => $assetData['content_type'], + 'width' => $assetData['width'], + 'height' => $assetData['height'], + 'size' => $assetData['size'], + 'createdAt' => $assetData['created_at'], + 'updatedAt' => $assetData['updated_at'], + ] + ); + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException(__('Could not retrieve media assets'), $exception->getMessage()); + } + return $assets; + } +} diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php index 09ce7ffe8ff20..5f99163db8f12 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionDuringMediaAssetInitializationTest.php @@ -28,7 +28,9 @@ class GetByIdExceptionDuringMediaAssetInitializationTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php index 89efae07360b4..3b47b0036224b 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdExceptionOnGetDataTest.php @@ -29,7 +29,9 @@ class GetByIdExceptionOnGetDataTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php index 8b805d0256e37..2c24899746473 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Asset/Command/GetByIdSuccessfulTest.php @@ -29,7 +29,9 @@ class GetByIdSuccessfulTest extends TestCase 'id' => 45, 'path' => 'img.jpg', 'title' => 'Img', + 'description' => 'Img Description', 'source' => 'Adobe Stock', + 'hash' => 'hash', 'content_type' => 'image/jpeg', 'width' => 420, 'height' => 240, diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php similarity index 70% rename from app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php rename to app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php index c96fd2ee54512..cc57b043954d7 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsBlacklistedTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/Directory/IsExcludedTest.php @@ -8,45 +8,45 @@ namespace Magento\MediaGallery\Test\Unit\Model\Directory; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\MediaGallery\Model\Directory\IsBlacklisted; -use Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface; +use Magento\MediaGallery\Model\Directory\IsExcluded; +use Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** - * Test for IsBlacklisted + * Test for IsExcluded */ -class IsBlacklistedTest extends TestCase +class IsExcludedTest extends TestCase { /** - * @var IsBlacklisted + * @var IsExcluded */ private $object; /** - * @var BlacklistPatternsConfigInterface|MockObject + * @var ExcludedPatternsConfigInterface|MockObject */ - private $config; + private $configMock; /** * Initialize basic test class mocks */ protected function setUp(): void { - $this->config = $this->getMockBuilder(BlacklistPatternsConfigInterface::class) + $this->configMock = $this->getMockBuilder(ExcludedPatternsConfigInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->config->expects($this->at(0))->method('get')->willReturn([ + $this->configMock->expects($this->at(0))->method('get')->willReturn([ 'tmp' => '/pub\/media\/tmp/', 'captcha' => '/pub\/media\/captcha/' ]); - $this->object = (new ObjectManager($this))->getObject(IsBlacklisted::class, [ - 'config' => $this->config + $this->object = (new ObjectManager($this))->getObject(IsExcluded::class, [ + 'config' => $this->configMock ]); } /** - * Test if the directory path is blacklisted + * Test if the directory path is excluded * * @param string $path * @param bool $isExcluded diff --git a/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php b/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php index 95fdac5bdafa5..d027f0ed21b53 100644 --- a/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php +++ b/app/code/Magento/MediaGallery/Test/Unit/Model/ResourceModel/Keyword/SaveAssetLinksTest.php @@ -11,6 +11,7 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\Exception\CouldNotSaveException; use Magento\MediaGallery\Model\ResourceModel\Keyword\SaveAssetLinks; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Psr\Log\LoggerInterface; @@ -32,6 +33,11 @@ class SaveAssetLinksTest extends TestCase */ private $resourceConnectionMock; + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetsKeywords; + /** * @var LoggerInterface|MockObject */ @@ -44,9 +50,11 @@ protected function setUp(): void { $this->connectionMock = $this->getMockForAbstractClass(AdapterInterface::class); $this->resourceConnectionMock = $this->createMock(ResourceConnection::class); + $this->getAssetsKeywords = $this->getMockForAbstractClass(GetAssetsKeywordsInterface::class); $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); $this->sut = new SaveAssetLinks( + $this->getAssetsKeywords, $this->resourceConnectionMock, $this->loggerMock ); @@ -60,13 +68,14 @@ protected function setUp(): void * @param int $assetId * @param array $keywordIds * @param array $values + * @throws CouldNotSaveException */ public function testAssetKeywordsSave(int $assetId, array $keywordIds, array $values): void { $expectedCalls = (int) (count($keywordIds)); if ($expectedCalls) { - $this->resourceConnectionMock->expects($this->once()) + $this->resourceConnectionMock->expects($this->exactly(2)) ->method('getConnection') ->willReturn($this->connectionMock); $this->resourceConnectionMock->expects($this->once()) diff --git a/app/code/Magento/MediaGallery/etc/db_schema.xml b/app/code/Magento/MediaGallery/etc/db_schema.xml index 31a764ef00c4d..1001737daa8a7 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema.xml +++ b/app/code/Magento/MediaGallery/etc/db_schema.xml @@ -10,7 +10,9 @@ <column xsi:type="int" name="id" unsigned="true" nullable="false" identity="true" comment="Entity ID"/> <column xsi:type="varchar" name="path" length="255" nullable="true" comment="Path"/> <column xsi:type="varchar" name="title" length="255" nullable="true" comment="Title"/> + <column xsi:type="text" name="description" nullable="true" comment="Description"/> <column xsi:type="varchar" name="source" length="255" nullable="true" comment="Source"/> + <column xsi:type="varchar" name="hash" length="255" nullable="true" comment="File hash"/> <column xsi:type="varchar" name="content_type" length="255" nullable="true" comment="Content Type"/> <column xsi:type="int" name="width" unsigned="true" nullable="false" identity="false" default="0" comment="Width"/> <column xsi:type="int" name="height" unsigned="true" nullable="false" identity="false" default="0" comment="Height"/> diff --git a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json index 8f5098caa9753..b32dfbf082175 100644 --- a/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json +++ b/app/code/Magento/MediaGallery/etc/db_schema_whitelist.json @@ -4,7 +4,9 @@ "id": true, "path": true, "title": true, + "description": true, "source": true, + "hash": true, "content_type": true, "width": true, "height": true, diff --git a/app/code/Magento/MediaGallery/etc/di.xml b/app/code/Magento/MediaGallery/etc/di.xml index a85c26e275226..b040bf9b35da3 100644 --- a/app/code/Magento/MediaGallery/etc/di.xml +++ b/app/code/Magento/MediaGallery/etc/di.xml @@ -21,7 +21,7 @@ <preference for="Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface" type="Magento\MediaGallery\Model\Directory\Command\CreateByPaths"/> <preference for="Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface" type="Magento\MediaGallery\Model\Directory\Command\DeleteByPaths"/> - <preference for="Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface" type="Magento\MediaGallery\Model\Directory\IsBlacklisted"/> + <preference for="Magento\MediaGalleryApi\Api\IsPathExcludedInterface" type="Magento\MediaGallery\Model\Directory\IsExcluded"/> <preference for="Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface" type="Magento\MediaGallery\Model\ResourceModel\DeleteAssetsByPaths"/> <preference for="Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface" type="Magento\MediaGallery\Model\ResourceModel\GetAssetsByIds"/> @@ -29,6 +29,7 @@ <preference for="Magento\MediaGalleryApi\Api\SaveAssetsInterface" type="Magento\MediaGallery\Model\ResourceModel\SaveAssets"/> <preference for="Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface" type="Magento\MediaGallery\Model\ResourceModel\Keyword\GetAssetsKeywords"/> <preference for="Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface" type="Magento\MediaGallery\Model\ResourceModel\Keyword\SaveAssetsKeywords"/> + <preference for="Magento\MediaGalleryApi\Api\SearchAssetsInterface" type="Magento\MediaGallery\Model\SearchAssets"/> <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> <plugin name="media_gallery_image_remove_metadata_after_wysiwyg" type="Magento\MediaGallery\Plugin\Wysiwyg\Images\Storage" @@ -40,7 +41,7 @@ <argument name="converter" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\Converter</argument> <argument name="schemaLocator" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\SchemaLocator</argument> <argument name="idAttributes" xsi:type="array"> - <item name="/config/blacklist/patterns/pattern" xsi:type="string">name</item> + <item name="/config/exclude/patterns/pattern" xsi:type="string">name</item> </argument> </arguments> </virtualType> @@ -50,11 +51,10 @@ <argument name="cacheId" xsi:type="string">Media_Gallery_Patterns_CacheId</argument> </arguments> </virtualType> - <type name="Magento\MediaGallery\Model\Directory\BlacklistPatternsConfig"> + <type name="Magento\MediaGallery\Model\Directory\ExcludedPatternsConfig"> <arguments> <argument name="data" xsi:type="object">Magento\MediaGallery\Model\Directory\Config\Data</argument> </arguments> </type> - - <preference for="Magento\MediaGalleryApi\Model\BlacklistPatternsConfigInterface" type="Magento\MediaGallery\Model\Directory\BlacklistPatternsConfig"/> + <preference for="Magento\MediaGalleryApi\Model\ExcludedPatternsConfigInterface" type="Magento\MediaGallery\Model\Directory\ExcludedPatternsConfig"/> </config> diff --git a/app/code/Magento/MediaGallery/etc/directory.xml b/app/code/Magento/MediaGallery/etc/directory.xml index 92f50b2dd0a30..42094aff72640 100644 --- a/app/code/Magento/MediaGallery/etc/directory.xml +++ b/app/code/Magento/MediaGallery/etc/directory.xml @@ -6,7 +6,7 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_MediaGalleryApi:etc/directory.xsd"> - <blacklist> + <exclude> <patterns> <pattern name="captcha">/^captcha/</pattern> <pattern name="customer">/^customer/</pattern> @@ -17,5 +17,5 @@ <pattern name="tmp">/^tmp/</pattern> <pattern name="directories-with-dots">/^\./</pattern> </patterns> - </blacklist> + </exclude> </config> diff --git a/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php index a0a1ec891237f..20e57cfa2d138 100644 --- a/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/CreateDirectoriesByPathsInterface.php @@ -10,6 +10,7 @@ /** * Create folders by provided paths * @api + * @since 101.0.0 */ interface CreateDirectoriesByPathsInterface { @@ -19,6 +20,7 @@ interface CreateDirectoriesByPathsInterface * @param string[] $paths * @return void * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 101.0.0 */ public function execute(array $paths): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php index 5df420a274933..41d682ed1bc6b 100644 --- a/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetInterface.php @@ -14,6 +14,7 @@ * Represents a media gallery asset which contains information about a media asset entity such * as path to the media storage, media asset title and its content type, etc. * @api + * @since 100.3.0 */ interface AssetInterface extends ExtensibleDataInterface { @@ -21,6 +22,7 @@ interface AssetInterface extends ExtensibleDataInterface * Get ID * * @return int|null + * @since 100.3.0 */ public function getId(): ?int; @@ -28,6 +30,7 @@ public function getId(): ?int; * Get Path * * @return string + * @since 100.3.0 */ public function getPath(): string; @@ -35,20 +38,37 @@ public function getPath(): string; * Get title * * @return string|null + * @since 100.3.0 */ public function getTitle(): ?string; + /** + * Get description + * + * @return string|null + */ + public function getDescription(): ?string; + /** * Get the name of the channel/stock/integration file was retrieved from. null if not identified. * * @return string|null + * @since 100.3.0 */ public function getSource(): ?string; + /** + * Get file hash + * + * @return string|null + */ + public function getHash(): ?string; + /** * Get content type * * @return string + * @since 100.3.0 */ public function getContentType(): string; @@ -56,6 +76,7 @@ public function getContentType(): string; * Retrieve full licensed asset's height * * @return int + * @since 100.3.0 */ public function getHeight(): int; @@ -63,6 +84,7 @@ public function getHeight(): int; * Retrieve full licensed asset's width * * @return int + * @since 100.3.0 */ public function getWidth(): int; @@ -70,6 +92,7 @@ public function getWidth(): int; * Retrieve asset file size in bytes * * @return int + * @since 101.0.0 */ public function getSize(): int; @@ -77,6 +100,7 @@ public function getSize(): int; * Get created at * * @return string|null + * @since 100.3.0 */ public function getCreatedAt(): ?string; @@ -84,6 +108,7 @@ public function getCreatedAt(): ?string; * Get updated at * * @return string|null + * @since 100.3.0 */ public function getUpdatedAt(): ?string; @@ -91,6 +116,7 @@ public function getUpdatedAt(): ?string; * Retrieve existing extension attributes object or create a new one. * * @return \Magento\MediaGalleryApi\Api\Data\AssetExtensionInterface|null + * @since 100.3.0 */ public function getExtensionAttributes(): ?AssetExtensionInterface; @@ -99,6 +125,7 @@ public function getExtensionAttributes(): ?AssetExtensionInterface; * * @param \Magento\MediaGalleryApi\Api\Data\AssetExtensionInterface|null $extensionAttributes * @return void + * @since 100.3.0 */ public function setExtensionAttributes(?AssetExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php index 1c18225470493..f303f723981d5 100644 --- a/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/Data/AssetKeywordsInterface.php @@ -13,6 +13,7 @@ /** * Interface for asset's keywords aggregation * @api + * @since 101.0.0 */ interface AssetKeywordsInterface extends ExtensibleDataInterface { @@ -20,6 +21,7 @@ interface AssetKeywordsInterface extends ExtensibleDataInterface * Get ID * * @return int + * @since 101.0.0 */ public function getAssetId(): int; @@ -27,6 +29,7 @@ public function getAssetId(): int; * Get the keyword * * @return KeywordInterface[] + * @since 101.0.0 */ public function getKeywords(): array; @@ -34,6 +37,7 @@ public function getKeywords(): array; * Get extension attributes * * @return \Magento\MediaGalleryApi\Api\Data\AssetKeywordsExtensionInterface|null + * @since 101.0.0 */ public function getExtensionAttributes(): ?AssetKeywordsExtensionInterface; @@ -42,6 +46,7 @@ public function getExtensionAttributes(): ?AssetKeywordsExtensionInterface; * * @param \Magento\MediaGalleryApi\Api\Data\AssetKeywordsExtensionInterface|null $extensionAttributes * @return void + * @since 101.0.0 */ public function setExtensionAttributes(?AssetKeywordsExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php b/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php index 3cba118e03a1a..3f3c583fc182c 100644 --- a/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/Data/KeywordInterface.php @@ -13,6 +13,7 @@ /** * Represents a media gallery keyword. This object contains information about a media asset keyword entity. * @api + * @since 100.3.0 */ interface KeywordInterface extends ExtensibleDataInterface { @@ -20,6 +21,7 @@ interface KeywordInterface extends ExtensibleDataInterface * Get ID * * @return int|null + * @since 100.3.0 */ public function getId(): ?int; @@ -27,6 +29,7 @@ public function getId(): ?int; * Get the keyword * * @return string + * @since 100.3.0 */ public function getKeyword(): string; @@ -34,6 +37,7 @@ public function getKeyword(): string; * Get extension attributes * * @return \Magento\MediaGalleryApi\Api\Data\KeywordExtensionInterface|null + * @since 100.3.0 */ public function getExtensionAttributes(): ?KeywordExtensionInterface; @@ -42,6 +46,7 @@ public function getExtensionAttributes(): ?KeywordExtensionInterface; * * @param \Magento\MediaGalleryApi\Api\Data\KeywordExtensionInterface|null $extensionAttributes * @return void + * @since 100.3.0 */ public function setExtensionAttributes(?KeywordExtensionInterface $extensionAttributes): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php index 5370235a31b95..3e824bdaffd6f 100644 --- a/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/DeleteAssetsByPathsInterface.php @@ -11,6 +11,7 @@ /** * Delete media assets by exact or directory paths * @api + * @since 101.0.0 */ interface DeleteAssetsByPathsInterface { @@ -19,6 +20,7 @@ interface DeleteAssetsByPathsInterface * * @param string[] $paths * @return void + * @since 101.0.0 */ public function execute(array $paths): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php index fe3be88fa0073..c3c1c0ad577a7 100644 --- a/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/DeleteDirectoriesByPathsInterface.php @@ -10,6 +10,7 @@ /** * Delete folders by provided paths * @api + * @since 101.0.0 */ interface DeleteDirectoriesByPathsInterface { @@ -19,6 +20,7 @@ interface DeleteDirectoriesByPathsInterface * @param string[] $paths * @return void * @throws \Magento\Framework\Exception\CouldNotDeleteException + * @since 101.0.0 */ public function execute(array $paths): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php index 5df6722a190d4..0c0ea7c812ce9 100644 --- a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByIdsInterface.php @@ -11,6 +11,7 @@ /** * Get media gallery assets by id attribute * @api + * @since 101.0.0 */ interface GetAssetsByIdsInterface { @@ -20,6 +21,7 @@ interface GetAssetsByIdsInterface * @param int[] $ids * @return \Magento\MediaGalleryApi\Api\Data\AssetInterface[] * @throws \Magento\Framework\Exception\LocalizedException + * @since 101.0.0 */ public function execute(array $ids): array; } diff --git a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php index dbaed6e0e9123..458d004fe74f8 100644 --- a/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/GetAssetsByPathsInterface.php @@ -10,6 +10,7 @@ /** * Get media gallery assets by paths in media storage * @api + * @since 101.0.0 */ interface GetAssetsByPathsInterface { @@ -19,6 +20,7 @@ interface GetAssetsByPathsInterface * @param string[] $paths * @return \Magento\MediaGalleryApi\Api\Data\AssetInterface[] * @throws \Magento\Framework\Exception\LocalizedException + * @since 101.0.0 */ public function execute(array $paths): array; } diff --git a/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php index 99b05291f32a0..317559e447b60 100644 --- a/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/GetAssetsKeywordsInterface.php @@ -10,6 +10,7 @@ /** * Get a media gallery asset keywords related to media gallery asset ids provided * @api + * @since 101.0.0 */ interface GetAssetsKeywordsInterface { @@ -18,6 +19,7 @@ interface GetAssetsKeywordsInterface * * @param int[] $assetIds * @return \Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface[] + * @since 101.0.0 */ public function execute(array $assetIds): array; } diff --git a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php similarity index 71% rename from app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php rename to app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php index cbd23ec3fbde7..1e41debb1b1c5 100644 --- a/app/code/Magento/MediaGalleryApi/Api/IsPathBlacklistedInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/IsPathExcludedInterface.php @@ -8,12 +8,12 @@ namespace Magento\MediaGalleryApi\Api; /** - * Check if the path is blacklisted for media gallery. + * Check if the path is excluded for media gallery. * - * Directory path may be blacklisted if it's reserved by the system. + * Directory path may be excluded if it's reserved by the system. * @api */ -interface IsPathBlacklistedInterface +interface IsPathExcludedInterface { /** * Check if the path is excluded from displaying and processing in the media gallery diff --git a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php index c63f7bd8c0818..823c858342a62 100644 --- a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsInterface.php @@ -11,6 +11,7 @@ /** * Save media gallery assets to the database * @api + * @since 101.0.0 */ interface SaveAssetsInterface { @@ -20,6 +21,7 @@ interface SaveAssetsInterface * @param \Magento\MediaGalleryApi\Api\Data\AssetInterface[] $assets * @return void * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 101.0.0 */ public function execute(array $assets): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php index 04efe7d32ccc1..714a4bc605423 100644 --- a/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Api/SaveAssetsKeywordsInterface.php @@ -10,6 +10,7 @@ /** * Save keywords related to assets to the database * @api + * @since 101.0.0 */ interface SaveAssetsKeywordsInterface { @@ -19,6 +20,7 @@ interface SaveAssetsKeywordsInterface * @param \Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface[] $assetKeywords * @return void * @throws \Magento\Framework\Exception\CouldNotSaveException + * @since 101.0.0 */ public function execute(array $assetKeywords): void; } diff --git a/app/code/Magento/MediaGalleryApi/Api/SearchAssetsInterface.php b/app/code/Magento/MediaGalleryApi/Api/SearchAssetsInterface.php new file mode 100644 index 0000000000000..19c1a04f663e5 --- /dev/null +++ b/app/code/Magento/MediaGalleryApi/Api/SearchAssetsInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryApi\Api; + +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteriaInterface; +use Magento\Framework\Exception\LocalizedException; + +/** + * Search media gallery assets by search criteria + */ +interface SearchAssetsInterface +{ + /** + * Search media gallery assets + * + * @param SearchCriteriaInterface $searchCriteria + * @return AssetsSearchResultInterface[] + * @throws LocalizedException + */ + public function execute(SearchCriteriaInterface $searchCriteria): array; +} diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php index 79b209823aeb0..1ed46566cfb21 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByDirectoryPathInterface.php @@ -11,7 +11,7 @@ /** * A command represents the media gallery assets delete action. A media gallery asset is filtered by directory * path value. - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface */ interface DeleteByDirectoryPathInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php index f33022e75d2fe..7a307a2940a0e 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/DeleteByPathInterface.php @@ -10,7 +10,7 @@ /** * A command represents the media gallery asset delete action. A media gallery asset is filtered by path value. - * @deprecated use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\DeleteAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface */ interface DeleteByPathInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php index 65cc2e3eae109..db8fd7e2baa6c 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByIdInterface.php @@ -10,7 +10,7 @@ /** * A command represents the get media gallery asset by using media gallery asset id as a filter parameter. - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface */ interface GetByIdInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php index d8d5b6773fbbc..3163574336061 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/GetByPathInterface.php @@ -10,7 +10,7 @@ /** * A command represents the get media gallery asset by using media gallery asset path as a filter parameter. - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsByPathInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\GetAssetsByPathInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface */ interface GetByPathInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php index 610ecf0cd22bf..f00486116b9be 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Asset/Command/SaveInterface.php @@ -12,7 +12,7 @@ /** * A command which executes the media gallery asset save operation. - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\SaveAssetsInterface instead * @see \Magento\MediaGalleryApi\Api\SaveAssetsInterface */ interface SaveInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php similarity index 75% rename from app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php rename to app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php index b4710f32e0c46..dd82f87780a49 100644 --- a/app/code/Magento/MediaGalleryApi/Model/BlacklistPatternsConfigInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/ExcludedPatternsConfigInterface.php @@ -7,9 +7,9 @@ namespace Magento\MediaGalleryApi\Model; /** - * Returns list of blacklist regexp patterns + * Returns list of excluded regexp patterns */ -interface BlacklistPatternsConfigInterface +interface ExcludedPatternsConfigInterface { /** * Get regexp patterns diff --git a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php index e42c370c1c6f7..acb18f268d167 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/GetAssetKeywordsInterface.php @@ -9,7 +9,7 @@ /** * A command represents functionality to get a media gallery asset keywords filtered by media gallery asset id. - * @deprecated use \Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface instead * @see \Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface */ interface GetAssetKeywordsInterface diff --git a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php index 824cbca178988..03cc76cc1760b 100644 --- a/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php +++ b/app/code/Magento/MediaGalleryApi/Model/Keyword/Command/SaveAssetKeywordsInterface.php @@ -9,7 +9,7 @@ /** * A command represents the media gallery asset keywords save operation. - * @deprecated use \Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface instead + * @deprecated 101.0.0 use \Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface instead * @see \Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface */ interface SaveAssetKeywordsInterface diff --git a/app/code/Magento/MediaGalleryApi/etc/directory.xsd b/app/code/Magento/MediaGalleryApi/etc/directory.xsd index 2ad76c8fcc9f2..2fb4fed028469 100644 --- a/app/code/Magento/MediaGalleryApi/etc/directory.xsd +++ b/app/code/Magento/MediaGalleryApi/etc/directory.xsd @@ -11,14 +11,14 @@ <xs:complexType name="configType"> <xs:sequence> - <xs:element type="blacklistType" name="blacklist" maxOccurs="unbounded" minOccurs="1"/> + <xs:element type="excludeType" name="exclude" maxOccurs="unbounded" minOccurs="1"/> </xs:sequence> </xs:complexType> - <xs:complexType name="blacklistType"> + <xs:complexType name="excludeType"> <xs:annotation> <xs:documentation> - Blacklist used for excluding directories from media gallery rendering and operations + List used for excluding directories from media gallery rendering and operations </xs:documentation> </xs:annotation> <xs:sequence> diff --git a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml index eaced3f642f70..f1ec76a877368 100644 --- a/app/code/Magento/MediaGalleryCatalog/etc/directory.xml +++ b/app/code/Magento/MediaGalleryCatalog/etc/directory.xml @@ -6,9 +6,9 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_MediaGalleryApi:etc/directory.xsd"> - <blacklist> + <exclude> <patterns> <pattern name="catalog">/^catalog\/product/</pattern> </patterns> - </blacklist> + </exclude> </config> diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE.txt b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php new file mode 100644 index 0000000000000..d439b53c120cb --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/Plugin/SaveBaseCategoryImageInformation.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogIntegration\Plugin; + +use Magento\Catalog\Model\ImageUploader; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Save base category image by SaveAssetsInterface. + */ +class SaveBaseCategoryImageInformation +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var Storage + */ + private $storage; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param DeleteAssetsByPathsInterface $deleteAssetsByPath + * @param Filesystem $filesystem + * @param GetAssetsByPathsInterface $getAssetsByPaths + * @param Storage $storage + * @param SynchronizeFilesInterface $synchronizeFiles + * @param ConfigInterface $config + */ + public function __construct( + DeleteAssetsByPathsInterface $deleteAssetsByPath, + Filesystem $filesystem, + GetAssetsByPathsInterface $getAssetsByPaths, + Storage $storage, + SynchronizeFilesInterface $synchronizeFiles, + ConfigInterface $config + ) { + $this->deleteAssetsByPaths = $deleteAssetsByPath; + $this->filesystem = $filesystem; + $this->getAssetsByPaths = $getAssetsByPaths; + $this->storage = $storage; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + } + + /** + * Saves base category image information after moving from tmp folder. + * + * @param ImageUploader $subject + * @param string $imagePath + * @return string + * @throws LocalizedException + */ + public function afterMoveFileFromTmp(ImageUploader $subject, string $imagePath): string + { + if (!$this->config->isEnabled()) { + return $imagePath; + } + + $absolutePath = $this->storage->getCmsWysiwygImages()->getStorageRoot() . $imagePath; + $tmpPath = $subject->getBaseTmpPath() . '/' . substr(strrchr($imagePath, '/'), 1); + $tmpAssets = $this->getAssetsByPaths->execute([$tmpPath]); + + if (!empty($tmpAssets)) { + $this->deleteAssetsByPaths->execute([$tmpAssets[0]->getPath()]); + } + + $this->synchronizeFiles->execute( + [ + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getRelativePath($absolutePath) + ] + ); + + return $imagePath; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/README.md b/app/code/Magento/MediaGalleryCatalogIntegration/README.md new file mode 100644 index 0000000000000..bcb37bd486dab --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryCatalogIntegration + +The purpose of this module is for extending catalog image uploader functionality. diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/composer.json b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json new file mode 100644 index 0000000000000..efabb70da9f39 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-media-gallery-catalog-integration", + "description": "Magento module responsible for extending catalog image uploader functionality", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-gallery-ui-api": "*" + }, + "suggest": { + "magento/module-catalog": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..2f8fab34911d6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/etc/adminhtml/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Catalog\Model\ImageUploader"> + <plugin name="save_category_image" type="Magento\MediaGalleryCatalogIntegration\Plugin\SaveBaseCategoryImageInformation"/> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml b/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml new file mode 100644 index 0000000000000..c9f1164121e91 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryCatalogIntegration" /> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogIntegration/registration.php b/app/code/Magento/MediaGalleryCatalogIntegration/registration.php new file mode 100644 index 0000000000000..9495790092df1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogIntegration/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryCatalogIntegration', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php new file mode 100644 index 0000000000000..a541e9999b784 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Controller/Adminhtml/Category/Index.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogUi\Controller\Adminhtml\Category; + +use Magento\Backend\App\Action; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * Get the media gallery layout + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->getConfig()->getTitle()->prepend(__('Categories')); + + return $resultPage; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..e17b02ec40737 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Model/Listing/DataProvider.php @@ -0,0 +1,199 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogUi\Model\Listing; + +use Magento\Catalog\Api\CategoryListInterface; +use Magento\Framework\Api\AttributeValueFactory; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\Document; +use Magento\Framework\Api\Search\DocumentFactory; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchCriteriaInterface; +use Magento\Framework\Api\Search\SearchResultFactory; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider as UiComponentDataProvider; + +/** + * DataProvider of category grid. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DataProvider extends UiComponentDataProvider +{ + private const ENTITY_ID = 'entity_id'; + + /** + * @var SearchResultFactory + */ + private $searchResultFactory; + + /** + * @var CategoryListInterface + */ + private $categoryList; + + /** + * @var AttributeValueFactory + */ + private $attributeValueFactory; + + /** + * @var DocumentFactory + */ + private $documentFactory; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param ReportingInterface $reporting + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RequestInterface $request + * @param FilterBuilder $filterBuilder + * @param SearchResultFactory $searchResultFactory + * @param CategoryListInterface $categoryList + * @param AttributeValueFactory $attributeValueFactory + * @param DocumentFactory $documentFactory + * @param FilterGroupBuilder $filterGroupBuilder + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + string $name, + string $primaryFieldName, + string $requestFieldName, + ReportingInterface $reporting, + SearchCriteriaBuilder $searchCriteriaBuilder, + RequestInterface $request, + FilterBuilder $filterBuilder, + SearchResultFactory $searchResultFactory, + CategoryListInterface $categoryList, + AttributeValueFactory $attributeValueFactory, + DocumentFactory $documentFactory, + FilterGroupBuilder $filterGroupBuilder, + array $meta = [], + array $data = [] + ) { + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + $this->categoryList = $categoryList; + $this->searchResultFactory = $searchResultFactory; + $this->attributeValueFactory = $attributeValueFactory; + $this->documentFactory = $documentFactory; + $this->filterGroupBuilder = $filterGroupBuilder; + } + + /** + * @inheritdoc + */ + public function getData() + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + $searchCriteria = $this->getSearchCriteria(); + $searchCriteria = $this->skipRootCategory($searchCriteria); + $collection = $this->categoryList->getList($searchCriteria); + $items = []; + + foreach ($collection->getItems() as $category) { + $items[] = $this->createDocument( + [ + 'entity_id' => $category->getEntityId(), + 'name' => $category->getName(), + 'image' => $category->getImage(), + 'path' => $category->getPath(), + 'display_mode' => $category->getDisplayMode(), + 'products' => $category->getProductCount(), + 'include_in_menu' => $category->getIncludeInMenu(), + 'is_active' => $category->getIsActive() + ] + ); + } + + $searchResult = $this->searchResultFactory->create(); + $searchResult->setSearchCriteria($searchCriteria); + $searchResult->setItems($items); + $searchResult->setTotalCount($collection->getTotalCount()); + + return $searchResult; + } + + /** + * Skip empty root category in collection + * + * @param SearchCriteriaInterface $searchCriteria + * @return SearchCriteriaInterface + */ + private function skipRootCategory(SearchCriteriaInterface $searchCriteria): SearchCriteriaInterface + { + $filterGroups = $searchCriteria->getFilterGroups(); + + $filters[] = $this->filterBuilder + ->setField(self::ENTITY_ID) + ->setConditionType('neq') + ->setValue(1) + ->create(); + $filterGroups[] = $this->filterGroupBuilder->setFilters($filters)->create(); + $searchCriteria->setFilterGroups($filterGroups); + return $searchCriteria; + } + + /** + * Add attributes to grid result + * + * @param array $attributes [code => value] + */ + private function createDocument(array $attributes): Document + { + $item = $this->documentFactory->create(); + $customAttributes = []; + + foreach ($attributes as $code => $value) { + $attribute = $this->attributeValueFactory->create(); + $attribute->setAttributeCode($code); + $attribute->setValue($value); + $customAttributes[$code] = $attribute; + } + + $item->setCustomAttributes($customAttributes); + + return $item; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/README.md b/app/code/Magento/MediaGalleryCatalogUi/README.md new file mode 100644 index 0000000000000..f47b031875f5d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCatalogUi module + +The Magento_MediaGalleryCatalogUi module that implement category grid for media gallery. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml new file mode 100644 index 0000000000000..0788bbd60291a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryGridPageDetailsActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertCategoryGridPageDetailsActionGroup"> + <annotations> + <description>Assert category grid page basic columns values for default category</description> + </annotations> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.path('1')}}" stepKey="assertPathColumn"/> + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.name('1', 'Default Category')}}" stepKey="assertNameColumn"/> + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.displayMode('1', 'PRODUCTS')}}" stepKey="assertDisplayModeColumn"/> + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.products('1', '0')}}" stepKey="assertProductsColumn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryPageTitleActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryPageTitleActionGroup.xml new file mode 100644 index 0000000000000..ee1d7bb5af5f4 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminAssertCategoryPageTitleActionGroup.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="AdminAssertCategoryPageTitleActionGroup"> + <annotations> + <description>Assert's category page title for Simple Sub Category</description> + </annotations> + <see selector="{{AdminCategoryContentSection.categoryPageTitle}}" userInput="{{SimpleSubCategory.name}}" stepKey="seeCategoryTitle"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml new file mode 100644 index 0000000000000..ccdebccab4e65 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminEditCategoryInGridPageActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEditCategoryInGridPageActionGroup"> + <annotations> + <description>Clicks the Edit action from the Media Gallery Category Grid</description> + </annotations> + <click selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.edit('2', 'Edit')}}" stepKey="clickOnCategoryRow"/> + <waitForPageLoad time="30" stepKey="waitForCategoryDetailsPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml new file mode 100644 index 0000000000000..2444cb314ad22 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/ActionGroup/AdminOpenCategoryGridPageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenCategoryGridPageActionGroup"> + <annotations> + <description>Navigates to category grid page by link.</description> + </annotations> + + <amOnPage url="{{AdminMediaGalleryCatalogUiCategoryGridPage.url}}" stepKey="navigateToCategoryGridPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml new file mode 100644 index 0000000000000..99cee48f443c7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Page/AdminMediaGalleryCatalogUiCategoryGridPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminMediaGalleryCatalogUiCategoryGridPage" url="media_gallery_catalog/category/index" area="admin" module="Magento_MediaGalleryCatalogUi"> + <section name="AdminMediaGalleryCatalogUiCategoryGridSection"/> + </page> +</pages> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml new file mode 100644 index 0000000000000..5267a215c8edd --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Section/AdminMediaGalleryCatalogUiCategoryGridSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMediaGalleryCatalogUiCategoryGridSection"> + <element name="path" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Path')]/preceding-sibling::th)]" parameterized="true"/> + <element name="name" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Name')]/preceding-sibling::th) +1 ]//*[text()='{{categoryName}}']" parameterized="true"/> + <element name="displayMode" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Display Mode')]/preceding-sibling::th) +1 ]//*[text()='{{productsText}}']" parameterized="true"/> + <element name="products" type="text" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Products')]/preceding-sibling::th) +1 ]//*[text()='{{productsQty}}']" parameterized="true"/> + <element name="edit" type="button" selector="//tr[{{row}}]//td[count(//div[@data-role='grid-wrapper']//tr//th[contains(., 'Action')]/preceding-sibling::th) +1 ]//*[text()='{{edit}}']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml new file mode 100644 index 0000000000000..b20f63a005279 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiEditCategoryGridPageTest.xml @@ -0,0 +1,32 @@ +<?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="AdminMediaGalleryCatalogUiEditCategoryGridPageTest"> + <annotations> + <features value="AdminMediaGalleryCategoryGrid"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1667"/> + <title value="User Edits Category from Category grid"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/5034526"/> + <description value="Edit Category from Media Gallery Category Grid"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminEditCategoryInGridPageActionGroup" stepKey="editCategoryItem"/> + <actionGroup ref="AdminAssertCategoryPageTitleActionGroup" stepKey="assertCategoryByName"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml new file mode 100644 index 0000000000000..a495e2ff07e6a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInCategoryFilterTest.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiUsedInCategoryFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInCategoryFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in categories filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951846"/> + <description value="User filters assets used in categories"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderAgain"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Categories"/> + <argument name="optionName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml new file mode 100644 index 0000000000000..d68fd4cb7cca8 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiUsedInProductFilterTest.xml @@ -0,0 +1,73 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiUsedInProductFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInProductsFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in products filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951848"/> + <description value="User filters assets used in products"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <createData entity="SimpleSubCategory" stepKey="category"/> + <createData entity="SimpleProduct" stepKey="product"> + <requiredEntity createDataKey="category"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="product" stepKey="deleteProduct"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="openEditProduct"> + <argument name="product" value="$$product$$"/> + </actionGroup> + <click selector="{{AdminProductFormSection.contentTab}}" stepKey="clickContentTab"/> + <waitForElementVisible selector="{{CatalogWYSIWYGSection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{CatalogWYSIWYGSection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Products"/> + <argument name="optionName" value="$$product.name$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml new file mode 100644 index 0000000000000..6b7bd3ba11f45 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest.xml @@ -0,0 +1,31 @@ +<?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="AdminMediaGalleryCatalogUiVerifyCategoryGridPageTest"> + <annotations> + <features value="AdminMediaGalleryCategoryGrid"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User sees category entities where asset is used in"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User sees category entities where asset is used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminAssertCategoryGridPageDetailsActionGroup" stepKey="assertCategoryGridPageRendered"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml new file mode 100644 index 0000000000000..e761ef5cd08ba --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Test/Mftf/Test/AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCatalogUiVerifyUsedInLinkCategoryGridTest"> + <annotations> + <features value="AdminMediaGalleryCategoryGrid"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1503"/> + <title value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User can open each entity the asset is associated with in a separate tab to manage association"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploaderToVerifyLink"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="openViewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup" stepKey="clickUsedInCategories"> + <argument name="entityName" value="Categories"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php new file mode 100644 index 0000000000000..0e7edd53bb45d --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/CategoryActions.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; +use Magento\Framework\UrlInterface; + +/** + * Class CategoryActions for Category grid + */ +class CategoryActions extends Column +{ + /** + * @var UrlInterface + */ + private $urlBuilder; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param UrlInterface $urlBuilder + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + UrlInterface $urlBuilder, + array $components = [], + array $data = [] + ) { + $this->urlBuilder = $urlBuilder; + parent::__construct($context, $uiComponentFactory, $components, $data); + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')]['edit'] = [ + 'href' => $this->urlBuilder->getUrl( + 'catalog/category/edit', + [ + 'id' => $item['entity_id'] + ] + ), + 'label' => __('Edit'), + 'hidden' => false, + ]; + } + } + + return $dataSource; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php new file mode 100644 index 0000000000000..f780a116baf9e --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Path.php @@ -0,0 +1,84 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Model\Category; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Path column for Category grid + */ +class Path extends Column +{ + + /** + * @var CategoryRepositoryInterface + */ + private $categoryRepository; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param CategoryRepositoryInterface $categoryRepository + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + CategoryRepositoryInterface $categoryRepository, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->categoryRepository = $categoryRepository; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName] = $this->getCategoryPathWithNames($item[$fieldName]); + } + } + } + + return $dataSource; + } + + /** + * Replace category path ids with category names + * + * @param string $pathWithIds + * @return string + * @throws NoSuchEntityException + */ + private function getCategoryPathWithNames(string $pathWithIds): string + { + $categoryPathWithName = ''; + $categoryIds = explode('/', $pathWithIds); + foreach ($categoryIds as $id) { + if ($id == Category::TREE_ROOT_ID) { + continue; + } + $categoryName = $this->categoryRepository->get($id)->getName(); + $categoryPathWithName .= ' / ' . $categoryName; + } + return $categoryPathWithName; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php new file mode 100644 index 0000000000000..efb2ad2f8dae5 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Columns/Thumbnail.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns; + +use Magento\Catalog\Helper\Image; +use Magento\Framework\DataObject; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Class Thumbnail column for Category grid + */ +class Thumbnail extends Column +{ + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var Image + */ + private $imageHelper; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param Image $image + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + Image $image, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->imageHelper = $image; + $this->storeManager = $storeManager; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource) + { + if (isset($dataSource['data']['items'])) { + $fieldName = $this->getData('name'); + foreach ($dataSource['data']['items'] as & $item) { + if (isset($item[$fieldName])) { + $item[$fieldName . '_src'] = $this->getUrl($item[$fieldName]); + } else { + $category = new DataObject($item); + $imageHelper = $this->imageHelper->init($category, 'product_listing_thumbnail'); + $item[$fieldName . '_src'] = $imageHelper->getUrl(); + } + } + } + + return $dataSource; + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * @return string + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl() . $path; + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php new file mode 100644 index 0000000000000..254ebd047c954 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/Ui/Component/Listing/Filters/UsedInProducts.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\Component\Filters\Type\Select; +use Magento\Ui\Api\BookmarkManagementInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; + +/** + * Used in products filter + */ +class UsedInProducts extends Select +{ + /** + * @var BookmarkManagementInterface + */ + private $bookmarkManagement; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param FilterBuilder $filterBuilder + * @param FilterModifier $filterModifier + * @param OptionSourceInterface $optionsProvider + * @param BookmarkManagementInterface $bookmarkManagement + * @param ProductRepositoryInterface $productRepository + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + FilterBuilder $filterBuilder, + FilterModifier $filterModifier, + OptionSourceInterface $optionsProvider = null, + BookmarkManagementInterface $bookmarkManagement, + ProductRepositoryInterface $productRepository, + array $components = [], + array $data = [] + ) { + $this->uiComponentFactory = $uiComponentFactory; + $this->filterBuilder = $filterBuilder; + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $optionsProvider, + $components, + $data + ); + $this->bookmarkManagement = $bookmarkManagement; + $this->productRepository = $productRepository; + } + + /** + * Prepare component configuration + * + * @return void + */ + public function prepare() + { + $options = []; + $productIds = []; + $bookmarks = $this->bookmarkManagement->loadByNamespace($this->context->getNameSpace())->getItems(); + foreach ($bookmarks as $bookmark) { + if ($bookmark->getIdentifier() === 'current') { + $applied = $bookmark->getConfig()['current']['filters']['applied']; + if (isset($applied[$this->getName()])) { + $productIds = $applied[$this->getName()]; + } + } + } + + foreach ($productIds as $id) { + $product = $this->productRepository->getById($id); + $options[] = [ + 'value' => $id, + 'label' => $product->getName(), + 'is_active' => $product->getStatus(), + 'path' => $product->getSku(), + 'optgroup' => false + + ]; + } + + $this->wrappedComponent = $this->uiComponentFactory->create( + $this->getName(), + parent::COMPONENT, + [ + 'context' => $this->getContext(), + 'options' => $options + ] + ); + + $this->wrappedComponent->prepare(); + $productsFilterJsConfig = array_replace_recursive( + $this->getJsConfig($this->wrappedComponent), + $this->getJsConfig($this) + ); + $this->setData('js_config', $productsFilterJsConfig); + + $this->setData( + 'config', + array_replace_recursive( + (array)$this->wrappedComponent->getData('config'), + (array)$this->getData('config') + ) + ); + + $this->applyFilter(); + + parent::prepare(); + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/composer.json b/app/code/Magento/MediaGalleryCatalogUi/composer.json new file mode 100644 index 0000000000000..985d581beff25 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/composer.json @@ -0,0 +1,26 @@ +{ + "name": "magento/module-media-gallery-catalog-ui", + "description": "Magento module that implement category grid for media gallery.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*", + "magento/module-catalog": "*", + "magento/module-store": "*", + "magento/module-ui": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCatalogUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..500ac10f4745a --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/di.xml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="product_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product</item> + <item name="category_id" xsi:type="object">Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Product" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_product</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCatalogUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Category" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">catalog_category</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="catalog_category" xsi:type="array"> + <item name="name" xsi:type="string">Categories</item> + <item name="link" xsi:type="string">media_gallery_catalog/category/index</item> + </item> + <item name="catalog_product" xsi:type="array"> + <item name="name" xsi:type="string">Products</item> + <item name="link" xsi:type="string">catalog/product/index</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..45f1ccce1c64f --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_catalog" frontName="media_gallery_catalog"> + <module name="Magento_MediaGalleryCatalogUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml b/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml new file mode 100644 index 0000000000000..4a593cbf10901 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryCatalogUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCatalogUi/registration.php b/app/code/Magento/MediaGalleryCatalogUi/registration.php new file mode 100644 index 0000000000000..c0376e2a828d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryCatalogUi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml new file mode 100644 index 0000000000000..dad1cd8283eba --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/layout/media_gallery_catalog_category_index.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-category-container" name="content"> + <uiComponent name="media_gallery_category_listing"/> + <block class="Magento\Backend\Block\Template" template="Magento_MediaGalleryCatalogUi::url_filter_applier.phtml" name="category_list_url_filter_applier"/> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/templates/url_filter_applier.phtml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/templates/url_filter_applier.phtml new file mode 100644 index 0000000000000..fa3abd419a691 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/templates/url_filter_applier.phtml @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var $block \Magento\Backend\Block\Template */ +/** @var \Magento\Framework\Escaper $escaper */ +?> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Ui/js/grid/url-filter-applier": { + "listingNamespace": "media_gallery_category_listing" + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml new file mode 100644 index 0000000000000..9945643ccffef --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_category_listing.xml @@ -0,0 +1,186 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_category_listing.media_gallery_category_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_category_columns</spinner> + <deps> + <dep>media_gallery_category_listing.media_gallery_category_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_category_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">entity_id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryCatalogUi\Model\Listing\DataProvider" name="media_gallery_category_listing_data_source"> + <settings> + <requestFieldName>entity_id</requestFieldName> + <primaryFieldName>entity_id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="name" > + <settings> + <placeholder>Search by category name</placeholder> + <label>Name</label> + </settings> + </filterSearch> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">catalog_category</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="selectedPlaceholders" xsi:type="array"> + <item name="defaultPlaceholder" xsi:type="string">Select</item> + </item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string" translate="true">notifyWhenChangesStop</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + <filterInput name="entity_id" provider="${ $.parentName }" sortOrder="20"> + <settings> + <dataScope>entity_id</dataScope> + <label translate="true">ID</label> + <placeholder>ID</placeholder> + </settings> + </filterInput> + <filterSelect name="display_mode" provider="${ $.parentName }" sortOrder="30"> + <settings> + <options class="Magento\Catalog\Model\Category\Attribute\Source\Mode"/> + <caption translate="true">Select</caption> + <label translate="true">Display Mode</label> + <dataScope>display_mode</dataScope> + </settings> + </filterSelect> + <filterSelect name="include_in_menu" provider="${ $.parentName }" sortOrder="40"> + <settings> + <options class="Magento\Config\Model\Config\Source\Yesno"/> + <caption translate="true">Select</caption> + <label translate="true">In Menu</label> + <dataScope>include_in_menu</dataScope> + </settings> + </filterSelect> + <filterSelect name="is_active" provider="${ $.parentName }" sortOrder="50"> + <settings> + <options class="Magento\Config\Model\Config\Source\Yesno"/> + <caption translate="true">Select</caption> + <label translate="true">Enabled</label> + <dataScope>is_active</dataScope> + </settings> + </filterSelect> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + </listingToolbar> + <columns name="media_gallery_category_columns"> + <column name="entity_id"> + <settings> + <label translate="true">ID</label> + </settings> + </column> + <column name="image" component="Magento_Ui/js/grid/columns/thumbnail" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Thumbnail"> + <settings> + <sortable>false</sortable> + <label translate="true">Image</label> + </settings> + </column> + <column name="name"> + <settings> + <label translate="true">Name</label> + </settings> + </column> + <column name="path" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\Path"> + <settings> + <label translate="true">Path</label> + </settings> + </column> + <column name="display_mode"> + <settings> + <label translate="true">Display Mode</label> + </settings> + </column> + <column name="products"> + <settings> + <label translate="true">Products</label> + </settings> + </column> + <column name="include_in_menu" component="Magento_Ui/js/grid/columns/select"> + <settings> + <label translate="true">In Menu</label> + </settings> + </column> + <column name="is_active" component="Magento_Ui/js/grid/columns/select" > + <settings> + <label translate="true">Enabled</label> + </settings> + </column> + <actionsColumn name="actions" class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Columns\CategoryActions" sortOrder="1000"> + <settings> + <indexField>entity_id</indexField> + </settings> + </actionsColumn> + </columns> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..2ca58b6020fa7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters\UsedInProducts" + component="Magento_Catalog/js/components/product-ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> + <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..2ca58b6020fa7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="product_id" + provider="${ $.parentName }" + sortOrder="110" + class="Magento\MediaGalleryCatalogUi\Ui\Component\Listing\Filters\UsedInProducts" + component="Magento_Catalog/js/components/product-ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Product Name or SKU</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find products</item> + <item name="missingValuePlaceholder" xsi:type="string" translate="true">Product with ID: %s doesn\'t exist</item> + <item name="isDisplayMissingValuePlaceholder" xsi:type="boolean">true</item> + <item name="isDisplayEmptyPlaceholder" xsi:type="boolean">true</item> + <item name="isRemoveSelectedIcon" xsi:type="boolean">true</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="searchUrl" xsi:type="url" path="catalog/product/search"/> + <item name="validationUrl" xsi:type="url" path="catalog/product/getSelected"/> + </item> + </argument> + <settings> + <label translate="true">Used in Products</label> + <dataScope>product_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="category_id" + provider="${ $.parentName }" + sortOrder="100" + component="Magento_Catalog/js/components/new-category" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Category Name</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find categories</item> + </item> + </argument> + <settings> + <options class="Magento\Catalog\Ui\Component\Product\Form\Categories\Options"/> + <label translate="true">Used in Categories</label> + <dataScope>category_id</dataScope> + <listens> + <link name="${ $.namespace }.${ $.namespace }:responseData">setParsed</link> + </listens> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..51575bd496598 --- /dev/null +++ b/app/code/Magento/MediaGalleryCatalogUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,29 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +& when (@media-common = true) { + + .media-gallery-category-container { + + .admin__field-label { + text-align: left; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + + .admin__field:not(.admin__field-option) > .admin__field-label { + font-size: 1.3rem; + font-weight: bold; + line-height: 2.1rem; + } + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php new file mode 100644 index 0000000000000..7beb95375073e --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Block/Search.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Block; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\BlockRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search blocks for ui-select component + */ +class Search extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::block'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param BlockRepositoryInterface $blockRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + BlockRepositoryInterface $blockRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->blockRepository = $blockRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute() : ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->blockRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $block) { + $id = $block->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php new file mode 100644 index 0000000000000..b211e58a0e8c6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Controller/Adminhtml/Page/Search.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Controller\Adminhtml\Page; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Api\PageRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\JsonFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller to search pages for ui-select component + */ +class Search extends Action implements HttpGetActionInterface +{ + /** + * Authorization level of a basic admin session + * + * @see _isAllowed() + */ + const ADMIN_RESOURCE = 'Magento_Cms::page'; + + /** + * @var JsonFactory + */ + private $resultJsonFactory; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @param JsonFactory $resultFactory + * @param PageRepositoryInterface $pageRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param Context $context + */ + public function __construct( + JsonFactory $resultFactory, + PageRepositoryInterface $pageRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + Context $context + ) { + $this->resultJsonFactory = $resultFactory; + $this->pageRepository = $pageRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + parent::__construct($context); + } + + /** + * Execute pages search. + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + $searchKey = $this->getRequest()->getParam('searchKey'); + $currentPage = (int) $this->getRequest()->getParam('page'); + $limit = (int) $this->getRequest()->getParam('limit'); + + $searchResult = $this->pageRepository->getList( + $this->searchCriteriaBuilder->addFilter('title', '%' . $searchKey . '%', 'like') + ->setCurrentPage($currentPage) + ->setPageSize($limit) + ->create() + ); + + $options = []; + foreach ($searchResult->getItems() as $page) { + $id = $page->getId(); + $options[$id] = [ + 'value' => $id, + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } + + return $this->resultJsonFactory->create()->setData([ + 'options' => $options, + 'total' => $searchResult->getTotalCount() + ]); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt new file mode 100644 index 0000000000000..36b2459f6aa63 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryCmsUi/README.md b/app/code/Magento/MediaGalleryCmsUi/README.md new file mode 100644 index 0000000000000..a5c2eb24c6c15 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryCmsUi module + +The Magento_MediaGalleryCmsUi module provides Magento_Cms related UI elements to the media gallery user interface + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryRenditions module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryRenditions module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml new file mode 100644 index 0000000000000..f0938016d12f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/ActionGroup/FillOutCustomCMSPageContentActionGroup.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="FillOutCustomCMSPageContentActionGroup"> + <annotations> + <description>Fills out the Page details (Page Title, Content and URL Key)</description> + </annotations> + + <arguments> + <argument name="title" type="string"/> + <argument name="content" type="string"/> + <argument name="identifier" type="string"/> + </arguments> + + <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{title}}" stepKey="fillFieldTitle"/> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContentTabForPage"/> + <fillField selector="{{CmsNewPagePageContentSection.contentHeading}}" userInput="{{content}}" stepKey="fillFieldContentHeading"/> + <scrollTo selector="{{CmsNewPagePageContentSection.content}}" stepKey="scrollToPageContent"/> + <fillField selector="{{CmsNewPagePageContentSection.content}}" userInput="{{content}}" stepKey="fillFieldContent"/> + <click selector="{{CmsNewPagePageSeoSection.header}}" stepKey="clickExpandSearchEngineOptimisation"/> + <fillField selector="{{CmsNewPagePageSeoSection.urlKey}}" userInput="{{identifier}}" stepKey="fillFieldUrlKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml new file mode 100644 index 0000000000000..810d9eea4e261 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInBlocksFilterTest.xml @@ -0,0 +1,60 @@ +<?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="AdminMediaGalleryCmsUiUsedInBlocksFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInBlocksFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in blocks filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951850"/> + <description value="User filters assets used in blocks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultBlock" stepKey="block" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="block" stepKey="deleteBlock"/> + </after> + <actionGroup ref="NavigateToCreatedCMSBlockPageActionGroup" stepKey="navigateToCreatedCMSBlockPage1"> + <argument name="CMSBlockPage" value="$$block$$"/> + </actionGroup> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{BlockNewPagePageActionsSection.saveBlock}}" stepKey="saveBlock"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Blocks"/> + <argument name="optionName" value="$$block.title$$"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml new file mode 100644 index 0000000000000..a6bfdb781a734 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Test/Mftf/Test/AdminMediaGalleryCmsUiUsedInPagesFilterTest.xml @@ -0,0 +1,68 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryCmsUiUsedInPagesFilterTest"> + <annotations> + <features value="AdminMediaGalleryUsedInPagesFilter"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="Used in pages filter"/> + <stories value="Story 58: User sees entities where asset is used in" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4934276"/> + <description value="User filters assets used in pages"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> + </before> + + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="navigateToCreateNewPage"/> + <actionGroup ref="FillOutCustomCMSPageContentActionGroup" stepKey="fillBasicPageDataForPageWithDefaultStore"> + <argument name="title" value="Unique page title MediaGalleryUi"/> + <argument name="content" value="MediaGalleryUI content"/> + <argument name="identifier" value="test-page-1"/> + </actionGroup> + + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <waitForPageLoad stepKey="waitForPageLoad" /> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectContentImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="savePage"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Used in Pages"/> + <argument name="optionName" value="Unique page title MediaGalleryUi"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + + <actionGroup ref="AdminNavigateToPageGridActionGroup" stepKey="navigateToCmsPageGrid"/> + <actionGroup ref="AdminSearchCmsPageInGridByUrlKeyActionGroup" stepKey="findCreatedCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + <actionGroup ref="AdminDeleteCmsPageFromGridActionGroup" stepKey="deleteCmsPage"> + <argument name="urlKey" value="test-page-1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php new file mode 100644 index 0000000000000..09fea24c8a2a9 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInBlocks.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\Component\Filters\Type\Select; +use Magento\Ui\Api\BookmarkManagementInterface; +use Magento\Cms\Api\BlockRepositoryInterface; + +/** + * Used in blocks filter + */ +class UsedInBlocks extends Select +{ + /** + * @var BookmarkManagementInterface + */ + private $bookmarkManagement; + + /** + * @var BlockRepositoryInterface + */ + private $blockRepository; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param FilterBuilder $filterBuilder + * @param FilterModifier $filterModifier + * @param OptionSourceInterface $optionsProvider + * @param BookmarkManagementInterface $bookmarkManagement + * @param BlockRepositoryInterface $blockRepository + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + FilterBuilder $filterBuilder, + FilterModifier $filterModifier, + OptionSourceInterface $optionsProvider = null, + BookmarkManagementInterface $bookmarkManagement, + BlockRepositoryInterface $blockRepository, + array $components = [], + array $data = [] + ) { + $this->uiComponentFactory = $uiComponentFactory; + $this->filterBuilder = $filterBuilder; + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $optionsProvider, + $components, + $data + ); + $this->bookmarkManagement = $bookmarkManagement; + $this->blockRepository = $blockRepository; + } + + /** + * Prepare component configuration + * + * @return void + */ + public function prepare() + { + $options = []; + $blockIds = []; + $bookmarks = $this->bookmarkManagement->loadByNamespace($this->context->getNameSpace())->getItems(); + foreach ($bookmarks as $bookmark) { + if ($bookmark->getIdentifier() === 'current') { + $applied = $bookmark->getConfig()['current']['filters']['applied']; + if (isset($applied[$this->getName()])) { + $blockIds = $applied[$this->getName()]; + } + } + } + + foreach ($blockIds as $id) { + $block = $this->blockRepository->getById($id); + $options[] = [ + 'value' => $id, + 'label' => $block->getTitle(), + 'is_active' => $block->isActive(), + 'optgroup' => false + ]; + } + + $this->wrappedComponent = $this->uiComponentFactory->create( + $this->getName(), + parent::COMPONENT, + [ + 'context' => $this->getContext(), + 'options' => $options + ] + ); + + $this->wrappedComponent->prepare(); + $jsConfig = array_replace_recursive( + $this->getJsConfig($this->wrappedComponent), + $this->getJsConfig($this) + ); + $this->setData('js_config', $jsConfig); + + $this->setData( + 'config', + array_replace_recursive( + (array)$this->wrappedComponent->getData('config'), + (array)$this->getData('config') + ) + ); + + $this->applyFilter(); + + parent::prepare(); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php new file mode 100644 index 0000000000000..235a77cdcb8c5 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/Ui/Component/Listing/Filters/UsedInPages.php @@ -0,0 +1,131 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\Component\Filters\Type\Select; +use Magento\Ui\Api\BookmarkManagementInterface; +use Magento\Cms\Api\PageRepositoryInterface; + +/** + * Used in pages filter + */ +class UsedInPages extends Select +{ + /** + * @var BookmarkManagementInterface + */ + private $bookmarkManagement; + + /** + * @var PageRepositoryInterface + */ + private $pageRepository; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param FilterBuilder $filterBuilder + * @param FilterModifier $filterModifier + * @param OptionSourceInterface $optionsProvider + * @param BookmarkManagementInterface $bookmarkManagement + * @param PageRepositoryInterface $pageRepository + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + FilterBuilder $filterBuilder, + FilterModifier $filterModifier, + OptionSourceInterface $optionsProvider = null, + BookmarkManagementInterface $bookmarkManagement, + PageRepositoryInterface $pageRepository, + array $components = [], + array $data = [] + ) { + $this->uiComponentFactory = $uiComponentFactory; + $this->filterBuilder = $filterBuilder; + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $optionsProvider, + $components, + $data + ); + $this->bookmarkManagement = $bookmarkManagement; + $this->pageRepository = $pageRepository; + } + + /** + * Prepare component configuration + * + * @return void + */ + public function prepare() + { + $options = []; + $pageIds = []; + $bookmarks = $this->bookmarkManagement->loadByNamespace($this->context->getNameSpace())->getItems(); + foreach ($bookmarks as $bookmark) { + if ($bookmark->getIdentifier() === 'current') { + $applied = $bookmark->getConfig()['current']['filters']['applied']; + if (isset($applied[$this->getName()])) { + $pageIds = $applied[$this->getName()]; + } + } + } + + foreach ($pageIds as $id) { + $page = $this->pageRepository->getById($id); + $options[] = [ + 'value' => $id, + 'label' => $page->getTitle(), + 'is_active' => $page->isActive(), + 'optgroup' => false + ]; + } + + $this->wrappedComponent = $this->uiComponentFactory->create( + $this->getName(), + parent::COMPONENT, + [ + 'context' => $this->getContext(), + 'options' => $options + ] + ); + + $this->wrappedComponent->prepare(); + $pagesFilterjsConfig = array_replace_recursive( + $this->getJsConfig($this->wrappedComponent), + $this->getJsConfig($this) + ); + $this->setData('js_config', $pagesFilterjsConfig); + + $this->setData( + 'config', + array_replace_recursive( + (array)$this->wrappedComponent->getData('config'), + (array)$this->getData('config') + ) + ); + + $this->applyFilter(); + + parent::prepare(); + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/composer.json b/app/code/Magento/MediaGalleryCmsUi/composer.json new file mode 100644 index 0000000000000..73747a669c051 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/composer.json @@ -0,0 +1,24 @@ +{ + "name": "magento/module-media-gallery-cms-ui", + "description": "Cms related UI elements in the magento media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-cms": "*", + "magento/module-backend": "*", + "magento/module-ui": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryCmsUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..b06ad0fff1df6 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/di.xml @@ -0,0 +1,41 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="page_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page</item> + <item name="block_id" xsi:type="object">Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Page" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_page</argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryCmsUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Block" type="Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Entity"> + <arguments> + <argument name="entityType" xsi:type="string">cms_block</argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn"> + <arguments> + <argument name="contentTypes" xsi:type="array"> + <item name="cms_block" xsi:type="array"> + <item name="name" xsi:type="string">Blocks</item> + <item name="link" xsi:type="string">cms/block/index</item> + </item> + <item name="cms_page" xsi:type="array"> + <item name="name" xsi:type="string">Pages</item> + <item name="link" xsi:type="string">cms/page/index</item> + </item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..2dc8b3ade5be7 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery_cms" frontName="media_gallery_cms"> + <module name="Magento_MediaGalleryCmsUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/etc/module.xml b/app/code/Magento/MediaGalleryCmsUi/etc/module.xml new file mode 100644 index 0000000000000..8a39b8328b387 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryCmsUi" /> +</config> diff --git a/app/code/Magento/MediaGalleryCmsUi/registration.php b/app/code/Magento/MediaGalleryCmsUi/registration.php new file mode 100644 index 0000000000000..0e68935eba590 --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryCmsUi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..506a6cad5b68e --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInPages" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInBlocks" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..506a6cad5b68e --- /dev/null +++ b/app/code/Magento/MediaGalleryCmsUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="page_id" + provider="${ $.parentName }" + sortOrder="120" + class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInPages" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/page/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Page Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find pages</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Pages</label> + <dataScope>page_id</dataScope> + </settings> + </filterSelect> + <filterSelect + name="block_id" + provider="${ $.parentName }" + sortOrder="130" + class="Magento\MediaGalleryCmsUi\Ui\Component\Listing\Filters\UsedInBlocks" + component="Magento_Ui/js/form/element/ui-select" + template="ui/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery_cms/block/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + <item name="showPath" xsi:type="boolean">false</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Block Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find blocks</item> + <item name="filterRateLimit" xsi:type="string" translate="true">1000</item> + <item name="filterRateLimitMethod" xsi:type="string">notifyWhenChangesStop</item> + </item> + </argument> + <settings> + <label translate="true">Used in Blocks</label> + <dataScope>block_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryIntegration/LICENSE.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php b/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php new file mode 100644 index 0000000000000..317b811df5692 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Model/OpenDialogUrlProvider.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Model; + +use Magento\Framework\DataObject; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Provider to get open media gallery dialog URL for WYSIWYG and widgets + */ +class OpenDialogUrlProvider extends DataObject +{ + /** + * @var ConfigInterface + */ + private $config; + + /** + * @param ConfigInterface $config + */ + public function __construct(ConfigInterface $config) + { + $this->config = $config; + } + + /** + * Get Url based on media gallery configuration + * + * @return string + */ + public function getUrl(): string + { + return $this->config->isEnabled() ? 'media_gallery/index/index' : 'cms/wysiwyg_images/index'; + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php new file mode 100644 index 0000000000000..a999b9004d9e5 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Plugin/SaveImageInformation.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Plugin; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\File\Uploader; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Psr\Log\LoggerInterface; + +/** + * Save image information by SaveAssetsInterface. + */ +class SaveImageInformation +{ + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var ConfigInterface + */ + private $config; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var SynchronizeFilesInterface + */ + private $synchronizeFiles; + + /** + * @var string[] + */ + private $imageExtensions; + + /** + * @param Filesystem $filesystem + * @param LoggerInterface $log + * @param IsPathExcludedInterface $isPathExcluded + * @param SynchronizeFilesInterface $synchronizeFiles + * @param ConfigInterface $config + * @param array $imageExtensions + */ + public function __construct( + Filesystem $filesystem, + LoggerInterface $log, + IsPathExcludedInterface $isPathExcluded, + SynchronizeFilesInterface $synchronizeFiles, + ConfigInterface $config, + array $imageExtensions + ) { + $this->log = $log; + $this->isPathExcluded = $isPathExcluded; + $this->filesystem = $filesystem; + $this->synchronizeFiles = $synchronizeFiles; + $this->config = $config; + $this->imageExtensions = $imageExtensions; + } + + /** + * Saves asset to media gallery after save image. + * + * @param Uploader $subject + * @param array $result + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @return array + */ + public function afterSave(Uploader $subject, array $result): array + { + if (!$this->config->isEnabled()) { + return $result; + } + + $path = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getRelativePath(rtrim($result['path'], '/') . '/' . ltrim($result['file'], '/')); + if (!$this->isApplicable($path)) { + return $result; + } + $this->synchronizeFiles->execute([$path]); + + return $result; + } + + /** + * Can asset be saved with provided path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path + && !$this->isPathExcluded->execute($path) + && preg_match('#\.(' . implode("|", $this->imageExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/README.md b/app/code/Magento/MediaGalleryIntegration/README.md new file mode 100644 index 0000000000000..365cde86777f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryIntegration + +The purpose of this module is to keep the integration of enhanced media gallery to Magento separated from implementation. diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php new file mode 100644 index 0000000000000..dfeaa3eff56bd --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/ImageComponentOpenDialogUrlTest.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Ui\Component\Form\Element\DataType\Media\Image; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class ImageComponentOpenDialogUrlTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Image + */ + private $image; + + /** + * @var string + */ + private $mediaGalleryOpenDialogUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->image = $this->objectManger->create(Image::class); + $this->image->setData('config', ['initialMediaGalleryOpenSubpath' => 'wysiwyg']); + + $url = $this->objectManger->create(UrlInterface::class); + $this->mediaGalleryOpenDialogUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertNotEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $this->image->prepare(); + $expectedOpenDialogUrl = $this->image->getConfiguration()['mediaGallery']['openDialogUrl']; + self::assertEquals($this->mediaGalleryOpenDialogUrl, $expectedOpenDialogUrl); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php new file mode 100644 index 0000000000000..7a3316f293879 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/OpenDialogUrlProviderTest.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\ObjectManagerInterface; +use Magento\MediaGalleryIntegration\Model\OpenDialogUrlProvider; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide tests cover getting correct url based on the config settings. + * @magentoAppArea adminhtml + */ +class OpenDialogUrlProviderTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var OpenDialogUrlProvider + */ + private $openDialogUrlProvider; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $config = $this->objectManger->create(ConfigInterface::class); + $this->openDialogUrlProvider = $this->objectManger->create( + OpenDialogUrlProvider::class, + ['config' => $config] + ); + } + + /** + * Test getting open dialog url with enhanced media gallery disabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + self::assertEquals('cms/wysiwyg_images/index', $this->openDialogUrlProvider->getUrl()); + } + + /** + * Test getting open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + self::assertEquals('media_gallery/index/index', $this->openDialogUrlProvider->getUrl()); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php new file mode 100644 index 0000000000000..81a4dc642cfa0 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/TinyMceOpenDialogUrlTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tinymce3\Model\Config\Gallery\Config; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update open dialog url functionality for media editor. + * @magentoAppArea adminhtml + */ +class TinyMceOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var Config + */ + private $tinyMce3Config; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $fileBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->tinyMce3Config = $this->objectManger->create(Config::class); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $this->fileBrowserWindowUrl = $url->getUrl('media_gallery/index/index'); + } + + /** + * Test image open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertNotEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test image open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + $config = $this->tinyMce3Config->getConfig($this->configDataObject); + self::assertEquals($this->fileBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php new file mode 100644 index 0000000000000..aebf5927869d5 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/Test/Integration/Model/WysiwygDefaultConfigOpenDialogUrlTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryIntegration\Test\Integration\Model; + +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Config; +use Magento\Cms\Model\Wysiwyg\Gallery\DefaultConfigProvider; +use Magento\Framework\DataObject; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\UrlInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Provide integration tests cover update wysiwyg editor dialog url update when media gallery enabled. + * @magentoAppArea adminhtml + */ +class WysiwygDefaultConfigOpenDialogUrlTest extends TestCase +{ + private const FILES_BROWSER_WINDOW_URL = 'files_browser_window_url'; + + /** + * @var ObjectManagerInterface + */ + private $objectManger; + + /** + * @var DataObject + */ + private $configDataObject; + + /** + * @var string + */ + private $filesBrowserWindowUrl; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManger = Bootstrap::getObjectManager(); + $this->configDataObject = $this->objectManger->create(DataObject::class); + + $url = $this->objectManger->create(UrlInterface::class); + $imageHelper = $this->objectManger->create(Images::class); + $this->filesBrowserWindowUrl = $url->getUrl( + 'media_gallery/index/index', + ['current_tree_path' => $imageHelper->idEncode(Config::IMAGE_DIRECTORY)] + ); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery not enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 0 + */ + public function testWithEnhancedMediaGalleryDisabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertNotEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } + + /** + * Test update wysiwyg editor open dialog url when enhanced media gallery enabled. + * @magentoConfigFixture default/system/media_gallery/enabled 1 + */ + public function testWithEnhancedMediaGalleryEnabled(): void + { + /** @var DefaultConfigProvider $defaultConfigProvider */ + $defaultConfigProvider = $this->objectManger->create(DefaultConfigProvider::class); + $config = $defaultConfigProvider->getConfig($this->configDataObject); + self::assertEquals($this->filesBrowserWindowUrl, $config->getData(self::FILES_BROWSER_WINDOW_URL)); + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/composer.json b/app/code/Magento/MediaGalleryIntegration/composer.json new file mode 100644 index 0000000000000..c55d6e0b89733 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/composer.json @@ -0,0 +1,31 @@ +{ + "name": "magento/module-media-gallery-integration", + "description": "Magento module responsible for integration of enhanced media gallery", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*" + }, + "require-dev": { + "magento/module-cms": "*" + }, + "suggest": { + "magento/module-catalog": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryIntegration\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..1559a6d7dfcd5 --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/adminhtml/di.xml @@ -0,0 +1,27 @@ +<?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\Ui\Component\Form\Element\DataType\Media\OpenDialogUrl"> + <arguments> + <argument name="url" xsi:type="object">Magento\MediaGalleryIntegration\Model\OpenDialogUrlProvider</argument> + </arguments> + </type> + <type name="Magento\Framework\File\Uploader"> + <plugin name="save_asset_image" type="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"/> + </type> + <type name="Magento\MediaGalleryIntegration\Plugin\SaveImageInformation"> + <arguments> + <argument name="imageExtensions" xsi:type="array"> + <item name="jpg" xsi:type="string">jpg</item> + <item name="jpeg" xsi:type="string">jpeg</item> + <item name="gif" xsi:type="string">gif</item> + <item name="png" xsi:type="string">png</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/etc/module.xml b/app/code/Magento/MediaGalleryIntegration/etc/module.xml new file mode 100644 index 0000000000000..88af90477cc8a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryIntegration"> + <sequence> + <module name="Magento_Ui"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryIntegration/registration.php b/app/code/Magento/MediaGalleryIntegration/registration.php new file mode 100644 index 0000000000000..028f8d5b4288a --- /dev/null +++ b/app/code/Magento/MediaGalleryIntegration/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryIntegration', __DIR__); diff --git a/app/code/Magento/MediaGalleryMetadata/LICENSE.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php new file mode 100644 index 0000000000000..9935904468388 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddIptcMetadata.php @@ -0,0 +1,180 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Write iptc data to the file return updated FileInterface with iptc data + */ +class AddIptcMetadata +{ + private const IPTC_TITLE_SEGMENT = '2#005'; + private const IPTC_DESCRIPTION_SEGMENT = '2#120'; + private const IPTC_KEYWORDS_SEGMENT = '2#025'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param DriverInterface $driver + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + DriverInterface $driver, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->driver = $driver; + $this->fileReader = $fileReader; + } + + /** + * Write metadata + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @param null|SegmentInterface $segment + */ + public function execute(FileInterface $file, MetadataInterface $metadata, ?SegmentInterface $segment): FileInterface + { + if (!is_callable('iptcembed') && !is_callable('iptcparse')) { + throw new LocalizedException(__('iptcembed() && iptcparse() must be enabled in php configuration')); + } + + $iptcData = $segment ? iptcparse($segment->getData()) : []; + + if ($metadata->getTitle() !== null) { + $iptcData[self::IPTC_TITLE_SEGMENT][0] = $metadata->getTitle(); + } + + if ($metadata->getDescription() !== null) { + $iptcData[self::IPTC_DESCRIPTION_SEGMENT][0] = $metadata->getDescription(); + } + + if ($metadata->getKeywords() !== null) { + $iptcData = $this->writeKeywords($metadata->getKeywords(), $iptcData); + } + + $newData = ''; + + foreach ($iptcData as $tag => $values) { + foreach ($values as $value) { + $newData .= $this->iptcMaketag(2, (int) substr($tag, 2), $value); + } + } + + $this->writeFile($file->getPath(), iptcembed($newData, $file->getPath())); + + $fileWithIptc = $this->fileReader->execute($file->getPath()); + + return $this->fileFactory->create([ + 'path' => $fileWithIptc->getPath(), + 'segments' => $this->getSegmentsWithIptc($fileWithIptc, $file) + ]); + } + + /** + * Return iptc segment from file. + * + * @param FileInterface $fileWithIptc + * @param FileInterface $originFile + */ + private function getSegmentsWithIptc(FileInterface $fileWithIptc, $originFile): array + { + $segments = $fileWithIptc->getSegments(); + $originFileSegments = $originFile->getSegments(); + + foreach ($segments as $key => $segment) { + if ($segment->getName() === 'APP13') { + foreach ($originFileSegments as $originKey => $segment) { + if ($segment->getName() === 'APP13') { + $originFileSegments[$originKey] = $segments[$key]; + } + } + return $originFileSegments; + } + } + return $originFileSegments; + } + + /** + * Write keywords field to the iptc segment. + * + * @param array $keywords + * @param array $iptcData + */ + private function writeKeywords(array $keywords, array $iptcData): array + { + foreach ($keywords as $key => $keyword) { + $iptcData[self::IPTC_KEYWORDS_SEGMENT][$key] = $keyword; + } + return $iptcData; + } + + /** + * Write iptc data to the image directly to the file. + * + * @param string $filePath + * @param string $content + */ + private function writeFile(string $filePath, string $content): void + { + $resource = $this->driver->fileOpen($filePath, 'wb'); + + $this->driver->fileWrite($resource, $content); + $this->driver->fileClose($resource); + } + + /** + * Create new iptc tag text + * + * @param int $rec + * @param int $tag + * @param string $value + */ + private function iptcMaketag(int $rec, int $tag, string $value) + { + //phpcs:disable Magento2.Functions.DiscouragedFunction + $length = strlen($value); + $retval = chr(0x1C) . chr($rec) . chr($tag); + + if ($length < 0x8000) { + $retval .= chr($length >> 8) . chr($length & 0xFF); + } else { + $retval .= chr(0x80) . + chr(0x04) . + chr(($length >> 24) & 0xFF) . + chr(($length >> 16) & 0xFF) . + chr(($length >> 8) & 0xFF) . + chr($length & 0xFF); + } + //phpcs:enable Magento2.Functions.DiscouragedFunction + return $retval . $value; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php new file mode 100644 index 0000000000000..269df146f2c81 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/AddXmpMetadata.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to the XMP template + */ +class AddXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag'; + private const XMP_XPATH_SELECTOR_KEYWORDS_EACH = '//dc:subject/rdf:Bag/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORD_ITEM = 'rdf:li'; + + /** + * Parse metadata + * + * @param string $data + * @param MetadataInterface $metadata + * @return string + */ + public function execute(string $data, MetadataInterface $metadata): string + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + if ($metadata->getTitle() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_TITLE, $metadata->getTitle()); + } + if ($metadata->getDescription() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION); + } else { + $this->setValueByXpath($xml, self::XMP_XPATH_SELECTOR_DESCRIPTION, $metadata->getDescription()); + } + if ($metadata->getKeywords() === null) { + $this->deleteValueByXpath($xml, self::XMP_XPATH_SELECTOR_KEYWORDS); + } else { + $this->updateKeywords($xml, $metadata->getKeywords()); + } + + $data = $xml->asXML(); + return str_replace("<?xml version=\"1.0\"?>\n", '', $data); + } + + /** + * Update keywords + * + * @param \SimpleXMLElement $xml + * @param array $keywords + */ + private function updateKeywords(\SimpleXMLElement $xml, array $keywords): void + { + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS_EACH) as $keywordElement) { + unset($keywordElement[0]); + } + + foreach ($xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) as $element) { + foreach ($keywords as $keyword) { + $element->addChild(self::XMP_XPATH_SELECTOR_KEYWORD_ITEM, $keyword); + } + } + } + + /** + * Deletes xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + */ + private function deleteValueByXpath(\SimpleXMLElement $xml, string $xpath): void + { + foreach ($xml->xpath($xpath) as $element) { + unset($element[0]); + } + } + + /** + * Set value to xml node by xpath + * + * @param \SimpleXMLElement $xml + * @param string $xpath + * @param string $value + */ + private function setValueByXpath(\SimpleXMLElement $xml, string $xpath, string $value): void + { + foreach ($xml->xpath($xpath) as $element) { + $element[0] = $value; + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File.php b/app/code/Magento/MediaGalleryMetadata/Model/File.php new file mode 100644 index 0000000000000..4b7605e8ec839 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; + +/** + * File internal data transfer object + */ +class File implements FileInterface +{ + /** + * @var string + */ + private $path; + + /** + * @var array + */ + private $segments; + + /** + * @var FileExtensionInterface|null + */ + private $extensionAttributes; + + /** + * @param string $path + * @param array $segments + * @param FileExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $path, + array $segments, + ?FileExtensionInterface $extensionAttributes = null + ) { + $this->path = $path; + $this->segments = $segments; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getSegments(): array + { + return $this->segments; + } + + /** + * @inheritdoc + */ + public function getPath(): string + { + return $this->path; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?FileExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php new file mode 100644 index 0000000000000..d5918781135a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/AddMetadata.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Add metadata to the asset by path. Should be used as a virtual type with a file type specific configuration + */ +class AddMetadata implements AddMetadataInterface +{ + /** + * @var array + */ + private $segmentWriters; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var WriteFileInterface + */ + private $fileWriter; + + /** + * @param FileInterfaceFactory $fileFactory + * @param ReadFileInterface $fileReader + * @param WriteFileInterface $fileWriter + * @param array $segmentWriters + */ + public function __construct( + FileInterfaceFactory $fileFactory, + ReadFileInterface $fileReader, + WriteFileInterface $fileWriter, + array $segmentWriters + ) { + $this->fileFactory = $fileFactory; + $this->fileReader = $fileReader; + $this->fileWriter = $fileWriter; + $this->segmentWriters = $segmentWriters; + } + + /** + * @inheritdoc + */ + public function execute(string $path, MetadataInterface $metadata): void + { + try { + $file = $this->fileReader->execute($path); + } catch (ValidatorException $e) { + return; + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not parse the image file for metadata: %path', ['path' => $path]) + ); + } + + try { + $this->fileWriter->execute($this->writeMetadata($file, $metadata)); + } catch (\Exception $exception) { + throw new LocalizedException( + __('Could not update the image file metadata: %path', ['path' => $path]) + ); + } + } + + /** + * Write metadata by given metadata writer + * + * @param FileInterface $file + * @param MetadataInterface $metadata + */ + private function writeMetadata(FileInterface $file, MetadataInterface $metadata): FileInterface + { + foreach ($this->segmentWriters as $writer) { + if (!$writer instanceof WriteMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . WriteFileInterface::class) + ); + } + + $file = $writer->execute($file, $metadata); + } + return $file; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php new file mode 100644 index 0000000000000..00f2b07f5bb81 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/File/ExtractMetadata.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\File; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; + +/** + * Extract Metadata from asset file by given extractors + */ +class ExtractMetadata implements ExtractMetadataInterface +{ + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var array + */ + private $segmentReaders; + + /** + * @var ReadFileInterface + */ + private $fileReader; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param MetadataInterfaceFactory $metadataFactory + * @param ReadFileInterface $fileReader + * @param array $segmentReaders + */ + public function __construct( + FileInterfaceFactory $fileFactory, + MetadataInterfaceFactory $metadataFactory, + ReadFileInterface $fileReader, + array $segmentReaders + ) { + $this->fileFactory = $fileFactory; + $this->metadataFactory = $metadataFactory; + $this->fileReader = $fileReader; + $this->segmentReaders = $segmentReaders; + } + + /** + * @inheritdoc + */ + public function execute(string $path): MetadataInterface + { + try { + return $this->readSegments($this->fileReader->execute($path)); + } catch (\Exception $exception) { + return $this->metadataFactory->create(); + } + } + + /** + * Read file segments by given segmentReader + * + * @param FileInterface $file + */ + private function readSegments(FileInterface $file): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->segmentReaders as $segmentReader) { + if (!$segmentReader instanceof ReadMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($segmentReader) . ' must implement ' . ReadMetadataInterface::class) + ); + } + + $data = $segmentReader->execute($file); + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php new file mode 100644 index 0000000000000..d7290f31ee34e --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetIptcMetadata.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Get metadata from IPTC block + */ +class GetIptcMetadata +{ + private const IPTC_TITLE = '2#005'; + private const IPTC_DESCRIPTION = '2#120'; + private const IPTC_KEYWORDS = '2#025'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $title = ''; + $description = ''; + $keywords = []; + + if (is_callable('iptcparse')) { + $iptcData = iptcparse($data); + + if (!empty($iptcData[self::IPTC_TITLE])) { + $title = trim($iptcData[self::IPTC_TITLE][0]); + } + + if (!empty($iptcData[self::IPTC_DESCRIPTION][0])) { + $description = trim($iptcData[self::IPTC_DESCRIPTION][0]); + } + + if (!empty($iptcData[self::IPTC_KEYWORDS][0])) { + $keywords = array_values($iptcData[self::IPTC_KEYWORDS]); + } + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php new file mode 100644 index 0000000000000..bda01645ddfec --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/GetXmpMetadata.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; + +/** + * Get metadata from XMP block + */ +class GetXmpMetadata +{ + private const XMP_XPATH_SELECTOR_TITLE = '//dc:title/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_DESCRIPTION = '//dc:description/rdf:Alt/rdf:li'; + private const XMP_XPATH_SELECTOR_KEYWORDS = '//dc:subject/rdf:Bag/rdf:li'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct(MetadataInterfaceFactory $metadataFactory) + { + $this->metadataFactory = $metadataFactory; + } + + /** + * Parse metadata + * + * @param string $data + * @return MetadataInterface + */ + public function execute(string $data): MetadataInterface + { + $xml = simplexml_load_string($data); + $namespaces = $xml->getNamespaces(true); + + foreach ($namespaces as $prefix => $url) { + $xml->registerXPathNamespace($prefix, $url); + } + + $keywords = array_map( + function (\SimpleXMLElement $element): string { + return (string) $element; + }, + $xml->xpath(self::XMP_XPATH_SELECTOR_KEYWORDS) + ); + + $description = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_DESCRIPTION)); + $title = implode(' ', $xml->xpath(self::XMP_XPATH_SELECTOR_TITLE)); + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php new file mode 100644 index 0000000000000..88810d3ccf28f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/ReadFile.php @@ -0,0 +1,318 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + + $header = $this->read($resource, 3); + + if ($header != "GIF") { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a GIF image')); + } + + $version = $this->read($resource, 3); + + if (!in_array($version, ['87a', '89a'])) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('Unexpected GIF version')); + } + + $headerSegment = $this->segmentFactory->create([ + 'name' => 'header', + 'data' => $header . $version + ]); + + $width = $this->read($resource, 2); + $height = $this->read($resource, 2); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $backgroundAndAspectRatio = $this->read($resource, 2); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + + $generalSegment = $this->segmentFactory->create([ + 'name' => 'header2', + 'data' => $width . $height . $bitPerPixelBinary . $backgroundAndAspectRatio . $globalColorTable + ]); + + $segments = $this->getSegments($resource); + + array_unshift($segments, $headerSegment, $generalSegment); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read gif segments + * + * @param resource $resource + * @return SegmentInterface[] + * @throws FileSystemException + */ + private function getSegments($resource): array + { + $gifFrameSeparator = pack("C", ord(",")); + $gifExtensionSeparator = pack("C", ord("!")); + $gifTerminator = pack("C", ord(";")); + + $segments = []; + do { + $separator = $this->read($resource, 1); + + if ($separator == $gifTerminator) { + return $segments; + } + + if ($separator == $gifFrameSeparator) { + $segments[] = $this->segmentFactory->create([ + 'name' => 'frame', + 'data' => $gifFrameSeparator . $this->readFrame($resource) + ]); + continue; + } + + if ($separator != $gifExtensionSeparator) { + throw new LocalizedException(__('The file is corrupted')); + } + + $segments[] = $this->getExtensionSegment($resource); + } while (!$this->driver->endOfFile($resource)); + + return $segments; + } + + /** + * Read extension segment + * + * @param resource $resource + * @return SegmentInterface + * @throws FileSystemException + */ + private function getExtensionSegment($resource): SegmentInterface + { + $gifExtensionSeparator = pack("C", ord("!")); + $extensionCodeBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $extensionCode = unpack('C', $extensionCodeBinary)[1]; + + if ($extensionCode == 0xF9) { + return $this->segmentFactory->create([ + 'name' => 'Graphics Control Extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode == 0xFE) { + return $this->segmentFactory->create([ + 'name' => 'comment', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + if ($extensionCode != 0xFF) { + return $this->segmentFactory->create([ + 'name' => 'Programm extension', + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $this->readBlock($resource) + ]); + } + + $blockLengthBinary = $this->read($resource, 1); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $blockLength = unpack('C', $blockLengthBinary)[1]; + $name = $this->read($resource, $blockLength); + + if ($blockLength != 11) { + throw new LocalizedException(__('The file is corrupted')); + } + + if ($name == 'XMP DataXMP') { + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlockWithSubblocks($resource) + ]); + } + + return $this->segmentFactory->create([ + 'name' => $name, + 'data' => $gifExtensionSeparator . $extensionCodeBinary . $blockLengthBinary + . $name . $this->readBlock($resource) + ]); + } + + /** + * Read gif frame + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readFrame($resource): string + { + $boundingBox = $this->read($resource, 8); + $bitPerPixelBinary = $this->read($resource, 1); + $bitPerPixel = $this->getBitPerPixel($bitPerPixelBinary); + $globalColorTable = $this->getGlobalColorTable($resource, $bitPerPixel); + return $boundingBox . $bitPerPixelBinary . $globalColorTable . $this->read($resource, 1) + . $this->readBlockWithSubblocks($resource); + } + + /** + * Retrieve bits per pixel value + * + * @param string $data + * @return int + */ + private function getBitPerPixel(string $data): int + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $bitPerPixel = unpack('C', $data)[1]; + $bpp = ($bitPerPixel & 7) + 1; + $bitPerPixel >>= 7; + $haveMap = $bitPerPixel & 1; + return $haveMap ? $bpp : 0; + } + + /** + * Read global color table + * + * @param resource $resource + * @param int $bitPerPixel + * @return string + * @throws FileSystemException + */ + private function getGlobalColorTable($resource, int $bitPerPixel): string + { + $globalColorTable = ''; + if ($bitPerPixel > 0) { + $max = pow(2, $bitPerPixel); + for ($i = 1; $i <= $max; ++$i) { + $globalColorTable .= $this->read($resource, 3); + } + } + return $globalColorTable; + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } + + /** + * Read the block stored in multiple sections + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readBlockWithSubblocks($resource): string + { + $data = ''; + $subLength = $this->read($resource, 1); + + while ($subLength !== "\0") { + $data .= $subLength . $this->read($resource, ord($subLength)); + $subLength = $this->read($resource, 1); + } + + return $data . $subLength; + } + + /** + * Read gif block + * + * @param resource $resource + * @return string + * @throws FileSystemException] + */ + private function readBlock($resource): string + { + $blockLengthBinary = $this->read($resource, 1); + $blockLength = ord($blockLengthBinary); + if ($blockLength == 0) { + return ''; + } + return $blockLengthBinary . $this->read($resource, $blockLength) . $this->read($resource, 1); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php new file mode 100644 index 0000000000000..1b83554ef4df3 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/ReadXmp.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * XMP Reader for gif file format + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + /** + * see XMP Specification Part 3, 1.1.2 GIF + */ + private const MAGIC_TRAILER_LENGTH = 258; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + $xmp = substr($segment->getData(), 14); + + if (substr($xmp, -self::MAGIC_TRAILER_LENGTH, 3) !== self::MAGIC_TRAILER_START + || substr($xmp, -5) !== self::MAGIC_TRAILER_END + ) { + throw new LocalizedException(__('XMP data is corrupted')); + } + + return substr($xmp, 0, -self::MAGIC_TRAILER_LENGTH); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php new file mode 100644 index 0000000000000..2b5167eba596b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/Segment/WriteXmp.php @@ -0,0 +1,191 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for GIF format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'XMP DataXMP'; + private const XMP_DATA_START_POSITION = 14; + private const MAGIC_TRAILER_START = "\x01\xFF\xFE"; + private const MAGIC_TRAILER_END = "\x03\x02\x01\x00\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $gifSegments = $file->getSegments(); + $xmpGifSegments = []; + foreach ($gifSegments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpGifSegments[$key] = $segment; + } + } + + if (empty($xmpGifSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpGifSegment($gifSegments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpGifSegments as $key => $segment) { + $gifSegments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $gifSegments + ]); + } + + /** + * Insert XMP segment to gif image segments (at position 3) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpGifSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 4), [$xmpSegment], array_slice($segments, 4)); + } + + /** + * Return XMP template from string + * + * @param string $string + * @param string $start + * @param string $end + */ + private function getXmpData(string $string, string $start, string $end): string + { + $string = ' ' . $string; + $ini = strpos($string, $start); + if ($ini == 0) { + return ''; + } + $ini += strlen($start); + $len = strpos($string, $end, $ini) - $ini; + + return substr($string, $ini, $len); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + + $xmpSegment = pack("C", ord("!")) . pack("C", 255) . pack("C", 11) . + self::XMP_SEGMENT_NAME . $this->addXmpMetadata->execute($xmpData, $metadata) . "\x01"; + + /** + * Write Magic trailer 258 bytes see XMP Specification Part 3, 1.1.2 GIF + */ + $i = 255; + while ($i > 0) { + $xmpSegment .= pack("C", $i); + $i--; + } + + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => $xmpSegment . "\0\0" + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + public function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = $this->getXmpData($data, self::XMP_SEGMENT_NAME, "\x01"); + $end = substr($data, strpos($data, "\x01")); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) . $end + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php new file mode 100644 index 0000000000000..cbdc9fa286e85 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Gif/WriteFile.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Gif; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments writer + */ +class WriteFile implements WriteFileInterface +{ + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write gif segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite( + $resource, + $segment->getData() + ); + } + $this->driver->fileWrite($resource, pack("C", ord(";"))); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php new file mode 100644 index 0000000000000..ed241d03506c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/ReadFile.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; + +/** + * Jpeg file reader + */ +class ReadFile implements ReadFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + private const MARKER_IMAGE_START = "\xDA"; + + private const TWO_BYTES = 2; + private const ONE_MEGABYTE = 1048576; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->segmentNames = $segmentNames; + } + + /** + * Is reader applicable + * + * @param string $path + * @return bool + * @throws FileSystemException + */ + private function isApplicable(string $path): bool + { + $resource = $this->driver->fileOpen($path, 'rb'); + try { + $marker = $this->readMarker($resource); + } catch (LocalizedException $exception) { + return false; + } + $this->driver->fileClose($resource); + + return $marker == self::MARKER_IMAGE_FILE_START; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + if (!$this->isApplicable($path)) { + throw new ValidatorException(__('Not a JPEG image')); + } + + $resource = $this->driver->fileOpen($path, 'rb'); + $marker = $this->readMarker($resource); + + if ($marker != self::MARKER_IMAGE_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a JPEG image')); + } + + do { + $marker = $this->readMarker($resource); + $segments[] = $this->readSegment($resource, ord($marker)); + } while (($marker != self::MARKER_IMAGE_START) && (!$this->driver->endOfFile($resource))); + + if ($marker != self::MARKER_IMAGE_START) { + throw new LocalizedException(__('File is corrupted')); + } + + $segments[] = $this->segmentFactory->create([ + 'name' => 'CompressedImage', + 'data' => $this->readCompressedImage($resource) + ]); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read jpeg marker + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readMarker($resource): string + { + $data = $this->read($resource, self::TWO_BYTES); + + if ($data[0] != self::MARKER_PREFIX) { + $this->driver->fileClose($resource); + throw new LocalizedException(__('File is corrupted')); + } + + return $data[1]; + } + + /** + * Read compressed image + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readCompressedImage($resource): string + { + $compressedImage = ''; + do { + $compressedImage .= $this->read($resource, self::ONE_MEGABYTE); + } while (!$this->driver->endOfFile($resource)); + + $endOfImageMarkerPosition = strpos($compressedImage, self::MARKER_PREFIX . self::MARKER_IMAGE_END); + + if ($endOfImageMarkerPosition !== false) { + $compressedImage = substr($compressedImage, 0, $endOfImageMarkerPosition); + } + + return $compressedImage; + } + + /** + * Read jpeg segment + * + * @param resource $resource + * @param int $segmentType + * @return SegmentInterface + * @throws FileSystemException + */ + private function readSegment($resource, int $segmentType): SegmentInterface + { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentSize = unpack('nsize', $this->read($resource, 2))['size'] - 2; + return $this->segmentFactory->create([ + 'name' => $this->segmentNames->getSegmentName($segmentType), + 'data' => $this->read($resource, $segmentSize) + ]); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php new file mode 100644 index 0000000000000..94ccb400e5e0a --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadIptc.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetIptcMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for jpeg image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetIptcMetadata + */ + private $getIptcData; + + /** + * @param GetIptcMetadata $getIptcData + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + GetIptcMetadata $getIptcData, + MetadataInterfaceFactory $metadataFactory + ) { + $this->getIptcData = $getIptcData; + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + return $this->getIptcData->execute($segment->getData()); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php new file mode 100644 index 0000000000000..81ff7200c3475 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/ReadXmp.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Jpeg XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isSegmentXmp($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), self::XMP_DATA_START_POSITION); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php new file mode 100644 index 0000000000000..e9fcd500f1dca --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteIptc.php @@ -0,0 +1,100 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddIptcMetadata; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg IPTC Writer + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'APP13'; + private const IPTC_SEGMENT_START = 'Photoshop 3.0\0x00'; + private const IPTC_DATA_START_POSITION = 0; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddIPtcMetadata + */ + private $addIptcMetadata; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddIptcMetadata $addIptcMetadata + * @param ReadFile $fileReader + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddIptcMetadata $addIptcMetadata, + ReadFile $fileReader + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addIptcMetadata = $addIptcMetadata; + $this->fileReader = $fileReader; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $iptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $iptcSegments[$key] = $segment; + } + } + + foreach ($iptcSegments as $segment) { + return $this->addIptcMetadata->execute($file, $metadata, $segment); + } + return $this->addIptcMetadata->execute($file, $metadata, null); + } + + /** + * Check if segment contains IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp($segment->getData(), self::IPTC_SEGMENT_START, self::IPTC_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php new file mode 100644 index 0000000000000..e88cdd5b7b8f4 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/Segment/WriteXmp.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * Jpeg XMP Writer + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'APP1'; + private const XMP_SEGMENT_START = "http://ns.adobe.com/xap/1.0/\x00"; + private const XMP_DATA_START_POSITION = 29; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $xmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isSegmentXmp($segment)) { + $xmpSegments[$key] = $segment; + } + } + + if (empty($xmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertXmpSegment($segments, $this->createXmpSegment($metadata)) + ]); + } + + foreach ($xmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image segments (at position 1) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 2), [$xmpSegment], array_slice($segments, 2)); + } + + /** + * Write new segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function createXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $data = $segment->getData(); + $start = substr($data, 0, self::XMP_DATA_START_POSITION); + $xmpData = substr($data, self::XMP_DATA_START_POSITION); + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $start . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Check if segment contains XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isSegmentXmp(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strncmp($segment->getData(), self::XMP_SEGMENT_START, self::XMP_DATA_START_POSITION) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php new file mode 100644 index 0000000000000..403bc7f3d7449 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Jpeg/WriteFile.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Jpeg; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const MARKER_IMAGE_FILE_START = "\xD8"; + private const MARKER_IMAGE_PREFIX = "\xFF"; + private const MARKER_IMAGE_END = "\xD9"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write file object to the filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + foreach ($file->getSegments() as $segment) { + if ($segment->getName() != 'CompressedImage' && strlen($segment->getData()) > 0xfffd) { + throw new LocalizedException(__('A Header is too large to fit in the segment!')); + } + } + + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileWrite($resource, self::MARKER_IMAGE_PREFIX . self::MARKER_IMAGE_END); + $this->driver->fileClose($resource); + } + + /** + * Write jpeg segment + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + if ($segment->getName() !== 'CompressedImage') { + $this->driver->fileWrite( + $resource, + //phpcs:ignore Magento2.Functions.DiscouragedFunction + self::MARKER_IMAGE_PREFIX . chr($this->segmentNames->getSegmentType($segment->getName())) + ); + $this->driver->fileWrite($resource, pack("n", strlen($segment->getData()) + 2)); + } + $this->driver->fileWrite($resource, $segment->getData()); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php new file mode 100644 index 0000000000000..9e3ee5d29a495 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Metadata.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Media asset metadata data transfer object + */ +class Metadata implements MetadataInterface +{ + /** + * @var string + */ + private $title; + + /** + * @var string + */ + private $description; + + /** + * @var array + */ + private $keywords; + + /** + * @var MetadataExtensionInterface + */ + private $extensionAttributes; + + /** + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @param MetadataExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $title = null, + string $description = null, + array $keywords = null, + ?MetadataExtensionInterface $extensionAttributes = null + ) { + $this->title = $title; + $this->description = $description; + $this->keywords = $keywords; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getTitle(): ?string + { + return $this->title; + } + + /** + * @inheritdoc + */ + public function getKeywords(): ?array + { + return $this->keywords; + } + + /** + * @inheritdoc + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php new file mode 100644 index 0000000000000..673f8ff436ebe --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/ReadFile.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\ReadFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\Framework\Exception\ValidatorException; + +/** + * File segments reader + */ +class ReadFile implements ReadFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + private const PNG_MARKER_IMAGE_END = 'IEND'; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param DriverInterface $driver + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + DriverInterface $driver, + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->driver = $driver; + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * @inheritdoc + */ + public function execute(string $path): FileInterface + { + $resource = $this->driver->fileOpen($path, 'rb'); + $header = $this->readHeader($resource); + + if ($header != self::PNG_FILE_START) { + $this->driver->fileClose($resource); + throw new ValidatorException(__('Not a PNG image')); + } + + do { + $header = $this->readHeader($resource); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentHeader = unpack('Nsize/a4type', $header); + $data = $this->read($resource, $segmentHeader['size']); + $segments[] = $this->segmentFactory->create([ + 'name' => $segmentHeader['type'], + 'data' => $data + ]); + $cyclicRedundancyCheck = $this->read($resource, 4); + + if (pack('N', crc32($segmentHeader['type'] . $data)) != $cyclicRedundancyCheck) { + throw new LocalizedException(__('The image is corrupted')); + } + } while ($header + && $segmentHeader['type'] != self::PNG_MARKER_IMAGE_END + && !$this->driver->endOfFile($resource) + ); + + $this->driver->fileClose($resource); + + return $this->fileFactory->create([ + 'path' => $path, + 'segments' => $segments + ]); + } + + /** + * Read 8 bytes + * + * @param resource $resource + * @return string + * @throws FileSystemException + */ + private function readHeader($resource): string + { + return $this->read($resource, 8); + } + + /** + * Read wrapper + * + * @param resource $resource + * @param int $length + * @return string + * @throws FileSystemException + */ + private function read($resource, int $length): string + { + $data = ''; + + while (!$this->driver->endOfFile($resource) && strlen($data) < $length) { + $data .= $this->driver->fileRead($resource, $length - strlen($data)); + } + + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php new file mode 100644 index 0000000000000..c856d95475a40 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadIptc.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * IPTC Reader to read IPTC data for png image + */ +class ReadIptc implements ReadMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_CHUNK_MARKER_LENGTH = 4; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory + ) { + $this->metadataFactory = $metadataFactory; + } + + /** + * @inheritdoc + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isIptcSegment($segment)) { + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + return $this->getIptcData($segment); + } + } + + return $this->metadataFactory->create([ + 'title' => null, + 'description' => null, + 'keywords' => null + ]); + } + + /** + * Read iptc data from zTXt segment + * + * @param SegmentInterface $segment + */ + private function getIptcData(SegmentInterface $segment): MetadataInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker); + if ($descriptionStartPosition) { + $description = substr( + $binData, + $descriptionStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $descriptionStartPosition + 3, 1)) + ); + } + + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker); + if ($titleStartPosition) { + $title = substr( + $binData, + $titleStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $titleStartPosition + 3, 1)) + ); + } + + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker); + if ($keywordsStartPosition) { + $keywords = substr( + $binData, + $keywordsStartPosition + self::IPTC_CHUNK_MARKER_LENGTH, + ord(substr($binData, $keywordsStartPosition + 3, 1)) + ); + } + + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => !empty($keywords) ? explode(',', $keywords) : null + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php new file mode 100644 index 0000000000000..83ba554f7bf5d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/ReadXmp.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\GetXmpMetadata; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\ReadMetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * PNG XMP Reader + */ +class ReadXmp implements ReadMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var GetXmpMetadata + */ + private $getXmpMetadata; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param GetXmpMetadata $getXmpMetadata + */ + public function __construct(MetadataInterfaceFactory $metadataFactory, GetXmpMetadata $getXmpMetadata) + { + $this->metadataFactory = $metadataFactory; + $this->getXmpMetadata = $getXmpMetadata; + } + + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + */ + public function execute(FileInterface $file): MetadataInterface + { + foreach ($file->getSegments() as $segment) { + if ($this->isXmpSegment($segment)) { + return $this->getXmpMetadata->execute($this->getXmpData($segment)); + } + } + return $this->metadataFactory->create([ + 'title' => '', + 'description' => '', + 'keywords' => [] + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php new file mode 100644 index 0000000000000..d40dbc13d2962 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteIptc.php @@ -0,0 +1,214 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * IPTC Writer to write IPTC data for png image + */ +class WriteIptc implements WriteMetadataInterface +{ + private const IPTC_SEGMENT_NAME = 'zTXt'; + private const IPTC_SEGMENT_START = 'iptc'; + private const IPTC_DATA_START_POSITION = 17; + private const IPTC_SEGMENT_START_STRING = 'Raw profile type iptc'; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + } + + /** + * Write iptc metadata to zTXt segment + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngIptcSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isIptcSegment($segment)) { + $pngIptcSegments[$key] = $segment; + } + } + + if (!is_callable('gzcompress') && !is_callable('gzuncompress')) { + throw new LocalizedException( + __('zlib gzcompress() && zlib gzuncompress() must be enabled in php configuration') + ); + } + + if (empty($pngIptcSegments)) { + $segments[] = $this->createPngIptcSegment($metadata); + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + foreach ($pngIptcSegments as $key => $segment) { + $segments[$key] = $this->updateIptcSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Create new zTXt segment with metadata + * + * @param MetadataInterface $metadata + */ + private function createPngIptcSegment(MetadataInterface $metadata): SegmentInterface + { + $start = '8BIM' . str_repeat(pack('C', 4), 2) . str_repeat(pack("C", 0), 5) + . 'c' . pack('C', 28) . pack('C', 1); + $compression = 'Z' . pack('C', 0) . pack('C', 3) . pack('C', 27) . '%G' . pack('C', 28) . pack('C', 1); + $end = str_repeat(pack('C', 0), 2) . pack('C', 2) . pack('C', 0) . pack('C', 4) . pack('C', 28); + $binData = $start . $compression . $end; + + $description = $metadata->getDescription(); + if ($description !== null) { + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $binData .= $descriptionMarker . pack('C', strlen($description)) . $description . pack('C', 28); + } + + $title = $metadata->getTitle(); + if ($title !== null) { + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $binData .= $titleMarker . pack('C', strlen($title)) . $title . pack('C', 28); + } + + $keywords = $metadata->getKeywords(); + if ($keywords !== null) { + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywords = implode(',', $keywords); + $binData .= $keywordsMarker . pack('C', strlen($keywords)) . $keywords . pack('C', 28); + } + + $binData .= pack('C', 0); + $hexString = bin2hex($binData); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $compressedIptcData = gzcompress(PHP_EOL . 'iptc' . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => self::IPTC_SEGMENT_NAME, + 'data' => self::IPTC_SEGMENT_START_STRING . str_repeat(pack('C', 0), 2) . $compressedIptcData + ]); + } + + /** + * Update iptc data to zTXt segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + */ + private function updateIptcSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + $description = null; + $title = null; + $keywords = null; + + $iptSegmentStartPosition = strpos($segment->getData(), pack("C", 0) . pack("C", 0) . 'x'); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $uncompressedData = gzuncompress(substr($segment->getData(), $iptSegmentStartPosition + 2)); + + $data = explode(PHP_EOL, trim($uncompressedData)); + //remove header and size from hex string + $iptcData = implode(array_slice($data, 2)); + $binData = hex2bin($iptcData); + + if ($metadata->getDescription() !== null) { + $description = $metadata->getDescription(); + $descriptionMarker = pack("C", 2) . 'x' . pack("C", 0); + $descriptionStartPosition = strpos($binData, $descriptionMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($description)) . $description, + $descriptionStartPosition + ) . substr($binData, $descriptionStartPosition + 1 + ord(substr($binData, $descriptionStartPosition))); + } + + if ($metadata->getTitle() !== null) { + $title = $metadata->getTitle(); + $titleMarker = pack("C", 2) . 'i' . pack("C", 0); + $titleStartPosition = strpos($binData, $titleMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($title)) . $title, + $titleStartPosition + ) . substr($binData, $titleStartPosition + 1 + ord(substr($binData, $titleStartPosition))); + } + + if ($metadata->getKeywords() !== null) { + $keywords = implode(',', $metadata->getKeywords()); + $keywordsMarker = pack("C", 2) . pack("C", 25) . pack("C", 0); + $keywordsStartPosition = strpos($binData, $keywordsMarker) + 3; + $binData = substr_replace( + $binData, + pack("C", strlen($keywords)) . $keywords, + $keywordsStartPosition + ) . substr($binData, $keywordsStartPosition + 1 + ord(substr($binData, $keywordsStartPosition))); + } + $hexString = bin2hex($binData); + $iptcSegmentStart = substr($segment->getData(), 0, $iptSegmentStartPosition + 2); + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $segmentDataCompressed = gzcompress(PHP_EOL . $data[0] . PHP_EOL . strlen($binData) . PHP_EOL . $hexString); + + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => $iptcSegmentStart . $segmentDataCompressed + ]); + } + + /** + * Does segment contain IPTC data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isIptcSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::IPTC_SEGMENT_NAME + && strncmp( + substr($segment->getData(), self::IPTC_DATA_START_POSITION, 4), + self::IPTC_SEGMENT_START, + self::IPTC_DATA_START_POSITION + ) == 0; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php new file mode 100644 index 0000000000000..292a52322d621 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/Segment/WriteXmp.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png\Segment; + +use Magento\MediaGalleryMetadata\Model\AddXmpMetadata; +use Magento\MediaGalleryMetadata\Model\XmpTemplate; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\FileInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Model\WriteMetadataInterface; + +/** + * XMP Writer for png format + */ +class WriteXmp implements WriteMetadataInterface +{ + private const XMP_SEGMENT_NAME = 'iTXt'; + private const XMP_SEGMENT_START = "XML:com.adobe.xmp\x00"; + + /** + * @var SegmentInterfaceFactory + */ + private $segmentFactory; + + /** + * @var FileInterfaceFactory + */ + private $fileFactory; + + /** + * @var AddXmpMetadata + */ + private $addXmpMetadata; + + /** + * @var XmpTemplate + */ + private $xmpTemplate; + + /** + * @param FileInterfaceFactory $fileFactory + * @param SegmentInterfaceFactory $segmentFactory + * @param AddXmpMetadata $addXmpMetadata + * @param XmpTemplate $xmpTemplate + */ + public function __construct( + FileInterfaceFactory $fileFactory, + SegmentInterfaceFactory $segmentFactory, + AddXmpMetadata $addXmpMetadata, + XmpTemplate $xmpTemplate + ) { + $this->fileFactory = $fileFactory; + $this->segmentFactory = $segmentFactory; + $this->addXmpMetadata = $addXmpMetadata; + $this->xmpTemplate = $xmpTemplate; + } + + /** + * Add xmp metadata to the png file + * + * @param FileInterface $file + * @param MetadataInterface $metadata + * @return FileInterface + */ + public function execute(FileInterface $file, MetadataInterface $metadata): FileInterface + { + $segments = $file->getSegments(); + $pngXmpSegments = []; + foreach ($segments as $key => $segment) { + if ($this->isXmpSegment($segment)) { + $pngXmpSegments[$key] = $segment; + } + } + + if (empty($pngXmpSegments)) { + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $this->insertPngXmpSegment($segments, $this->createPngXmpSegment($metadata)) + ]); + } + + foreach ($pngXmpSegments as $key => $segment) { + $segments[$key] = $this->updateSegment($segment, $metadata); + } + + return $this->fileFactory->create([ + 'path' => $file->getPath(), + 'segments' => $segments + ]); + } + + /** + * Insert XMP segment to image png segments (at position 1) + * + * @param SegmentInterface[] $segments + * @param SegmentInterface $xmpSegment + * @return SegmentInterface[] + */ + private function insertPngXmpSegment(array $segments, SegmentInterface $xmpSegment): array + { + return array_merge(array_slice($segments, 0, 2), [$xmpSegment], array_slice($segments, 2)); + } + + /** + * Write new png segment metadata + * + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function createPngXmpSegment(MetadataInterface $metadata): SegmentInterface + { + $xmpData = $this->xmpTemplate->get(); + return $this->segmentFactory->create([ + 'name' => self::XMP_SEGMENT_NAME, + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($xmpData, $metadata) + ]); + } + + /** + * Add metadata to the png xmp segment + * + * @param SegmentInterface $segment + * @param MetadataInterface $metadata + * @return SegmentInterface + */ + private function updateSegment(SegmentInterface $segment, MetadataInterface $metadata): SegmentInterface + { + return $this->segmentFactory->create([ + 'name' => $segment->getName(), + 'data' => self::XMP_SEGMENT_START . $this->addXmpMetadata->execute($this->getXmpData($segment), $metadata) + ]); + } + + /** + * Does segment contain XMP data + * + * @param SegmentInterface $segment + * @return bool + */ + private function isXmpSegment(SegmentInterface $segment): bool + { + return $segment->getName() === self::XMP_SEGMENT_NAME + && strpos($segment->getData(), '<x:xmpmeta') !== -1; + } + + /** + * Get XMP xml + * + * @param SegmentInterface $segment + * @return string + */ + private function getXmpData(SegmentInterface $segment): string + { + return substr($segment->getData(), strpos($segment->getData(), '<x:xmpmeta')); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php new file mode 100644 index 0000000000000..c5db6644b3545 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Png/WriteFile.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model\Png; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadata\Model\SegmentNames; +use Magento\MediaGalleryMetadataApi\Model\FileInterface; +use Magento\MediaGalleryMetadataApi\Model\WriteFileInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * File segments reader + */ +class WriteFile implements WriteFileInterface +{ + private const PNG_FILE_START = "\x89PNG\x0d\x0a\x1a\x0a"; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var SegmentNames + */ + private $segmentNames; + + /** + * @param DriverInterface $driver + * @param SegmentNames $segmentNames + */ + public function __construct( + DriverInterface $driver, + SegmentNames $segmentNames + ) { + $this->driver = $driver; + $this->segmentNames = $segmentNames; + } + + /** + * Write PNG file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void + { + $resource = $this->driver->fileOpen($file->getPath(), 'wb'); + + $this->driver->fileWrite($resource, self::PNG_FILE_START); + $this->writeSegments($resource, $file->getSegments()); + $this->driver->fileClose($resource); + } + + /** + * Write PNG segments + * + * @param resource $resource + * @param SegmentInterface[] $segments + */ + private function writeSegments($resource, array $segments): void + { + foreach ($segments as $segment) { + $this->driver->fileWrite($resource, pack("N", strlen($segment->getData()))); + $this->driver->fileWrite($resource, pack("a4", $segment->getName())); + $this->driver->fileWrite($resource, $segment->getData()); + $this->driver->fileWrite($resource, pack("N", crc32($segment->getName() . $segment->getData()))); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/Segment.php b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php new file mode 100644 index 0000000000000..0e8a89767e40c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/Segment.php @@ -0,0 +1,79 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentInterface; + +/** + * Segment internal data transfer object + */ +class Segment implements SegmentInterface +{ + /** + * @var array + */ + private $name; + + /** + * @var string + */ + private $data; + + /** + * @var SegmentExtensionInterface + */ + private $extensionAttributes; + + /** + * @param string $name + * @param string $data + * @param SegmentExtensionInterface|null $extensionAttributes + */ + public function __construct( + string $name, + string $data, + ?SegmentExtensionInterface $extensionAttributes = null + ) { + $this->name = $name; + $this->data = $data; + $this->extensionAttributes = $extensionAttributes; + } + + /** + * @inheritdoc + */ + public function getName(): string + { + return $this->name; + } + + /** + * @inheritdoc + */ + public function getData(): string + { + return $this->data; + } + + /** + * @inheritdoc + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface + { + return $this->extensionAttributes; + } + + /** + * @inheritdoc + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void + { + $this->extensionAttributes = $extensionAttributes; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php new file mode 100644 index 0000000000000..62eea09453ae5 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/SegmentNames.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +/** + * Segment types to names mapper + */ +class SegmentNames +{ + private const SEGMENT_TYPE_TO_NAME = [ + 0xC0 => "SOF0", + 0xC1 => "SOF1", + 0xC2 => "SOF2", + 0xC3 => "SOF4", + 0xC5 => "SOF5", + 0xC6 => "SOF6", + 0xC7 => "SOF7", + 0xC8 => "JPG", + 0xC9 => "SOF9", + 0xCA => "SOF10", + 0xCB => "SOF11", + 0xCD => "SOF13", + 0xCE => "SOF14", + 0xCF => "SOF15", + 0xC4 => "DHT", + 0xCC => "DAC", + 0xD0 => "RST0", + 0xD1 => "RST1", + 0xD2 => "RST2", + 0xD3 => "RST3", + 0xD4 => "RST4", + 0xD5 => "RST5", + 0xD6 => "RST6", + 0xD7 => "RST7", + 0xD8 => "SOI", + 0xD9 => "EOI", + 0xDA => "SOS", + 0xDB => "DQT", + 0xDC => "DNL", + 0xDD => "DRI", + 0xDE => "DHP", + 0xDF => "EXP", + 0xE0 => "APP0", + 0xE1 => "APP1", + 0xE2 => "APP2", + 0xE3 => "APP3", + 0xE4 => "APP4", + 0xE5 => "APP5", + 0xE6 => "APP6", + 0xE7 => "APP7", + 0xE8 => "APP8", + 0xE9 => "APP9", + 0xEA => "APP10", + 0xEB => "APP11", + 0xEC => "APP12", + 0xED => "APP13", + 0xEE => "APP14", + 0xEF => "APP15", + 0xF0 => "JPG0", + 0xF1 => "JPG1", + 0xF2 => "JPG2", + 0xF3 => "JPG3", + 0xF4 => "JPG4", + 0xF5 => "JPG5", + 0xF6 => "JPG6", + 0xF7 => "JPG7", + 0xF8 => "JPG8", + 0xF9 => "JPG9", + 0xFA => "JPG10", + 0xFB => "JPG11", + 0xFC => "JPG12", + 0xFD => "JPG13", + 0xFE => "COM", + 0x01 => "TEM", + 0x02 => "RES", + ]; + + /** + * Get segment name by type + * + * @param int $type + * @return string + */ + public function getSegmentName(int $type): string + { + return self::SEGMENT_TYPE_TO_NAME[$type]; + } + + /** + * Get segment type by name + * + * @param string $name + * @return int + */ + public function getSegmentType(string $name): int + { + return array_search($name, self::SEGMENT_TYPE_TO_NAME); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php new file mode 100644 index 0000000000000..a7d07f66ba8aa --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Model/XmpTemplate.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Module\Dir; +use Magento\Framework\Module\Dir\Reader; + +/** + * XMP template provider + */ +class XmpTemplate +{ + private const XMP_TEMPLATE_FILENAME = 'default.xmp'; + + /** + * @var Reader + */ + private $moduleReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @param Reader $moduleReader + * @param DriverInterface $driver + */ + public function __construct(Reader $moduleReader, DriverInterface $driver) + { + $this->moduleReader = $moduleReader; + $this->driver = $driver; + } + + /** + * Get default XMP template + * + * @return string + * @throws FileSystemException + */ + public function get(): string + { + $etcDirectoryPath = $this->moduleReader->getModuleDir( + Dir::MODULE_ETC_DIR, + 'Magento_MediaGalleryMetadata' + ); + return $this->driver->fileGetContents( + $etcDirectoryPath . '/' . self::XMP_TEMPLATE_FILENAME + ); + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/README.md b/app/code/Magento/MediaGalleryMetadata/README.md new file mode 100644 index 0000000000000..ec74e527ddebb --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadata + +The purpose of this module is to provide an ability to extract the metadata from file and populating Media Asset entity fields when an image is uploaded to Magento and also provide an ability to update the metadata stored in an image file. diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php new file mode 100644 index 0000000000000..c284bf71e60af --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/AddMetadataTest.php @@ -0,0 +1,197 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * ExtractMetadata test + */ +class AddMetadataTest extends TestCase +{ + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->addMetadata = Bootstrap::getObjectManager()->get(AddMetadataInterface::class); + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataInterfaceFactory::class); + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param null|string $fileName + * @param null|string $title + * @param null|string $description + * @param null|array $keywords + * @throws LocalizedException + */ + public function testExecute( + ?string $fileName, + ?string $title, + ?string $description, + ?array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $metadata = $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]); + + $this->addMetadata->execute($modifiableFilePath, $metadata); + + $updatedMetadata = $this->extractMetadata->execute($modifiableFilePath); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + + $this->driver->deleteFile($modifiableFilePath); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'iptc_only.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'macos-photos.jpeg', + 'Updated Title', + null, + null + ], + [ + 'iptc_only.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_iptc.jpeg', + 'Updated Title', + null, + null + ], + [ + 'macos-preview.png', + 'Title of the magento image 2', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ], + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'empty_xmp_image.png', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ], + ], + [ + 'exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ], + [ + 'empty_exiftool.gif', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php new file mode 100644 index 0000000000000..982ccbb20fe2c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/ExtractMetadataTest.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for ExtractMetadata + */ +class ExtractMetadataTest extends TestCase +{ + /** + * @var ExtractMetadataComposite + */ + private $extractMetadata; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->extractMetadata = Bootstrap::getObjectManager()->get(ExtractMetadataInterface::class); + } + + /** + * Test for ExtractMetadata::execute + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testExecute( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../_files/' . $fileName); + $metadata = $this->extractMetadata->execute($path); + + $this->assertEquals($title, $metadata->getTitle()); + $this->assertEquals($description, $metadata->getDescription()); + $this->assertEquals($keywords, $metadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'macos-photos.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'macos-preview.png', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.jpeg', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'exiftool.gif', + 'Title of the magento image', + 'Description of the magento image', + [ + 'magento', + 'mediagallerymetadata' + ] + ], + [ + 'iptc_only.png', + 'Title of the magento image', + 'PNG format is awesome', + [ + 'png', + 'awesome' + ] + ], + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php new file mode 100644 index 0000000000000..4bba73e3ca2a9 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Gif/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Gif\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Gif\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer gif format + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteReadGif( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalGifMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalGifMetadata->getTitle()); + $this->assertEmpty($originalGifMetadata->getDescription()); + $this->assertEmpty($originalGifMetadata->getKeywords()); + $updatedGifFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedGifMetadata = $this->xmpReader->execute($updatedGifFile); + $this->assertEquals($title, $updatedGifMetadata->getTitle()); + $this->assertEquals($description, $updatedGifMetadata->getDescription()); + $this->assertEquals($keywords, $updatedGifMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_exiftool.gif', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php new file mode 100644 index 0000000000000..932b71df28430 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.jpeg', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php new file mode 100644 index 0000000000000..043e26f67853f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Jpeg/Segment/XmpTest.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Jpeg\Segment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp; +use Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for XMP reader and writer + */ +class XmpTest extends TestCase +{ + /** + * @var WriteXmp + */ + private $xmpWriter; + + /** + * @var ReadXmp + */ + private $xmpReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->xmpWriter = Bootstrap::getObjectManager()->get(WriteXmp::class); + $this->xmpReader = Bootstrap::getObjectManager()->get(ReadXmp::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for XMP reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $file = $this->fileReader->execute($path); + $originalMetadata = $this->xmpReader->execute($file); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + $updatedFile = $this->xmpWriter->execute( + $file, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $updatedMetadata = $this->xmpReader->execute($updatedFile); + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_xmp_image.jpeg', + 'Title of the magento image', + 'Description of the magento image 2', + [ + 'magento2', + 'community' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php new file mode 100644 index 0000000000000..d8bcfd7a94561 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/Test/Integration/Model/Png/Segment/IptcTest.php @@ -0,0 +1,134 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadata\Test\Integration\Model\Png\Segment; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc; +use Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc; +use Magento\MediaGalleryMetadata\Model\Png\ReadFile; +use Magento\MediaGalleryMetadata\Model\MetadataFactory; + +/** + * Test for IPTC reader and writer + */ +class IptcTest extends TestCase +{ + /** + * @var WriteIptc + */ + private $iptcWriter; + + /** + * @var ReadIptc + */ + private $iptcReader; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @var ReadFile + */ + private $fileReader; + + /** + * @var MetadataFactory + */ + private $metadataFactory; + + /** + * @var WriteInterface + */ + private $varDirectory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->varDirectory = Bootstrap::getObjectManager()->get(Filesystem::class) + ->getDirectoryWrite(DirectoryList::VAR_DIR); + $this->iptcWriter = Bootstrap::getObjectManager()->get(WriteIptc::class); + $this->iptcReader = Bootstrap::getObjectManager()->get(ReadIptc::class); + $this->fileReader = Bootstrap::getObjectManager()->get(ReadFile::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + $this->metadataFactory = Bootstrap::getObjectManager()->get(MetadataFactory::class); + } + + /** + * Test for IPTC reader and writer + * + * @dataProvider filesProvider + * @param string $fileName + * @param string $title + * @param string $description + * @param array $keywords + * @throws LocalizedException + */ + public function testWriteRead( + string $fileName, + string $title, + string $description, + array $keywords + ): void { + $path = realpath(__DIR__ . '/../../../../_files/' . $fileName); + $modifiableFilePath = $this->varDirectory->getAbsolutePath($fileName); + $this->driver->copy( + $path, + $modifiableFilePath + ); + $modifiableFilePath = $this->fileReader->execute($modifiableFilePath); + $originalMetadata = $this->iptcReader->execute($modifiableFilePath); + + $this->assertEmpty($originalMetadata->getTitle()); + $this->assertEmpty($originalMetadata->getDescription()); + $this->assertEmpty($originalMetadata->getKeywords()); + + $updatedFile = $this->iptcWriter->execute( + $modifiableFilePath, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + + $updatedMetadata = $this->iptcReader->execute($updatedFile); + + $this->assertEquals($title, $updatedMetadata->getTitle()); + $this->assertEquals($description, $updatedMetadata->getDescription()); + $this->assertEquals($keywords, $updatedMetadata->getKeywords()); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'empty_iptc.png', + 'Updated Title', + 'Updated Description', + [ + 'magento2', + 'mediagallery' + ] + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif new file mode 100644 index 0000000000000..14cc6026b5950 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_exiftool.gif differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg new file mode 100644 index 0000000000000..144a56dac2d3e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png new file mode 100644 index 0000000000000..129c49a1b7e64 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_iptc.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg new file mode 100644 index 0000000000000..cee7bff38a6c6 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png new file mode 100644 index 0000000000000..7e81891ebc0ee Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/empty_xmp_image.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif b/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif new file mode 100644 index 0000000000000..70574d70b609e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/exiftool.gif differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg new file mode 100644 index 0000000000000..5d7dba35fede7 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png new file mode 100644 index 0000000000000..9b4821c1c4e5d Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/iptc_only.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg new file mode 100644 index 0000000000000..3a07b6abe788e Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-photos.jpeg differ diff --git a/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png new file mode 100644 index 0000000000000..966520f0d0112 Binary files /dev/null and b/app/code/Magento/MediaGalleryMetadata/Test/_files/macos-preview.png differ diff --git a/app/code/Magento/MediaGalleryMetadata/composer.json b/app/code/Magento/MediaGalleryMetadata/composer.json new file mode 100644 index 0000000000000..c2ce66ce64c36 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-media-gallery-metadata", + "description": "Magento module responsible for images metadata processing", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-metadata-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryMetadata\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadata/etc/default.xmp b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp new file mode 100644 index 0000000000000..772b6af671ec6 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/default.xmp @@ -0,0 +1,24 @@ +<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?> +<x:xmpmeta xmlns:x="adobe:ns:meta/" x:xmptk="XMP Core 5.4.0"> + <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <rdf:Description rdf:about="" + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:exif="http://ns.adobe.com/exif/1.0/" + xmlns:photoshop="http://ns.adobe.com/photoshop/1.0/"> + <dc:subject> + <rdf:Seq> + <rdf:li>magento</rdf:li> + </rdf:Seq> + </dc:subject> + <dc:description><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:description> + <dc:title><rdf:Alt><rdf:li xml:lang="x-default">Magento</rdf:li></rdf:Alt></dc:title> + <dc:subject> + <rdf:Bag> + <rdf:li>magento</rdf:li> + <rdf:li>mediagallerymetadata</rdf:li> + </rdf:Bag> + </dc:subject> + </rdf:Description> + </rdf:RDF> +</x:xmpmeta> +<?xpacket end="w"?> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadata/etc/di.xml b/app/code/Magento/MediaGalleryMetadata/etc/di.xml new file mode 100644 index 0000000000000..d2f1f90510488 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/di.xml @@ -0,0 +1,127 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface" type="Magento\MediaGalleryMetadata\Model\Metadata"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface" type="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\FileInterface" type="Magento\MediaGalleryMetadata\Model\File"/> + <preference for="Magento\MediaGalleryMetadataApi\Model\SegmentInterface" type="Magento\MediaGalleryMetadata\Model\Segment"/> + <type name="Magento\MediaGalleryMetadataApi\Model\ExtractMetadataComposite"> + <arguments> + <argument name="extractors" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadataApi\Model\AddMetadataComposite"> + <arguments> + <argument name="writers" xsi:type="array"> + <item name="jpeg" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata</item> + <item name="png" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\AddMetadata</item> + <item name="gif" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\AddMetadata</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\ReadFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Png\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\Gif\WriteFile"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\XmpTemplate"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryMetadata\Model\AddIptcMetadata"> + <arguments> + <argument name="driver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\WriteIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\AddMetadata" type="Magento\MediaGalleryMetadata\Model\File\AddMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="fileWriter" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\WriteFile</argument> + <argument name="segmentWriters" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\WriteXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Gif\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Gif\Segment\ReadXmp</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Png\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Png\Segment\ReadIptc</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryMetadata\Model\Jpeg\ExtractMetadata" type="Magento\MediaGalleryMetadata\Model\File\ExtractMetadata"> + <arguments> + <argument name="fileReader" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\ReadFile</argument> + <argument name="segmentReaders" xsi:type="array"> + <item name="xmp" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadXmp</item> + <item name="iptc" xsi:type="object">Magento\MediaGalleryMetadata\Model\Jpeg\Segment\ReadIptc</item> + </argument> + </arguments> + </virtualType> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/etc/module.xml b/app/code/Magento/MediaGalleryMetadata/etc/module.xml new file mode 100644 index 0000000000000..776b05aecd284 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryMetadata"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadata/registration.php b/app/code/Magento/MediaGalleryMetadata/registration.php new file mode 100644 index 0000000000000..fcf6789d9321f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadata/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryMetadata', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php new file mode 100644 index 0000000000000..df645681e8971 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/AddMetadataInterface.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Add metadata to asset file + */ +interface AddMetadataInterface +{ + /** + * Add metadata to the asset file + * + * @param string $path + * @param MetadataInterface $metadata + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $metadata): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php new file mode 100644 index 0000000000000..63e943150f4a7 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/Data/MetadataInterface.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api\Data; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface; + +/** + * Media asset metadata data transfer object + */ +interface MetadataInterface extends ExtensibleDataInterface +{ + /** + * Get asset title + * + * @return null|string + */ + public function getTitle(): ?string; + + /** + * Get asset description + * + * @return null|string + */ + public function getDescription(): ?string; + + /** + * Get asset keywords + * + * @return null|array + */ + public function getKeywords(): ?array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null + */ + public function getExtensionAttributes(): ?MetadataExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Api\Data\MetadataExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?MetadataExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php new file mode 100644 index 0000000000000..2327406db8bef --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Api/ExtractMetadataInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Api; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Extract asset metadata + */ +interface ExtractMetadataInterface +{ + /** + * Extract metadata from the asset file + * + * @param string $path + * @return MetadataInterface + */ + public function execute(string $path): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php new file mode 100644 index 0000000000000..fc3f53313199d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/AddMetadataComposite.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer pool + */ +class AddMetadataComposite implements AddMetadataInterface +{ + /** + * @var AddMetadataInterface[] + */ + private $writers; + + /** + * @param AddMetadataInterface[] $writers + */ + public function __construct(array $writers) + { + $this->writers = $writers; + } + + /** + * Write metadata to the path + * + * @param string $path + * @param MetadataInterface $data + * @throws LocalizedException + */ + public function execute(string $path, MetadataInterface $data): void + { + foreach ($this->writers as $writer) { + if (!$writer instanceof AddMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($writer) . ' must implement ' . AddMetadataInterface::class) + ); + } + + $writer->execute($path, $data); + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php new file mode 100644 index 0000000000000..0d6e8aa345178 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ExtractMetadataComposite.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; + +/** + * Metadata extractor composite + */ +class ExtractMetadataComposite implements ExtractMetadataInterface +{ + /** + * @var ExtractMetadataInterface[] + */ + private $extractors; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @param MetadataInterfaceFactory $metadataFactory + * @param ExtractMetadataInterface[] $extractors + */ + public function __construct( + MetadataInterfaceFactory $metadataFactory, + array $extractors + ) { + $this->metadataFactory = $metadataFactory; + $this->extractors = $extractors; + } + + /** + * Extract metadata from file + * + * @param string $path + * @return MetadataInterface + * @throws LocalizedException + */ + public function execute(string $path): MetadataInterface + { + $title = null; + $description = null; + $keywords = []; + + foreach ($this->extractors as $extractor) { + if (!$extractor instanceof ExtractMetadataInterface) { + throw new \InvalidArgumentException( + __(get_class($extractor) . ' must implement ' . ExtractMetadataInterface::class) + ); + } + + $data = $extractor->execute($path); + $title = !empty($data->getTitle()) ? $data->getTitle() : $title; + $description = !empty($data->getDescription()) ? $data->getDescription() : $description; + + if (!empty($data->getKeywords())) { + foreach ($data->getKeywords() as $keyword) { + $keywords[] = $keyword; + } + } + } + return $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => empty($keywords) ? null : array_unique($keywords) + ]); + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php new file mode 100644 index 0000000000000..0cd01bbf57c64 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/FileInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface; + +/** + * File internal data transfer object + */ +interface FileInterface extends ExtensibleDataInterface +{ + /** + * Get file path + * + * @return string + */ + public function getPath(): string; + + /** + * Get metadata sections + * + * @return SegmentInterface[] + */ + public function getSegments(): array; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null + */ + public function getExtensionAttributes(): ?FileExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\FileExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?FileExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php new file mode 100644 index 0000000000000..e45a934f7b5ad --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadFileInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +/** + * File reader + */ +interface ReadFileInterface +{ + /** + * Create file object from the file + * + * @param string $path + * @return FileInterface + */ + public function execute(string $path): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php new file mode 100644 index 0000000000000..b6d97118f848b --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/ReadMetadataInterface.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata reader + */ +interface ReadMetadataInterface +{ + /** + * Read metadata from the file + * + * @param FileInterface $file + * @return MetadataInterface + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): MetadataInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php new file mode 100644 index 0000000000000..bf6cdc30306f8 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/SegmentInterface.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface; + +/** + * Segment internal data transfer object + */ +interface SegmentInterface extends ExtensibleDataInterface +{ + /** + * Get segment name + * + * @return string + */ + public function getName(): string; + + /** + * Get segment data + * + * @return string + */ + public function getData(): string; + + /** + * Get extension attributes + * + * @return \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null + */ + public function getExtensionAttributes(): ?SegmentExtensionInterface; + + /** + * Set extension attributes + * + * @param \Magento\MediaGalleryMetadataApi\Model\SegmentExtensionInterface|null $extensionAttributes + * @return void + */ + public function setExtensionAttributes(?SegmentExtensionInterface $extensionAttributes): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php new file mode 100644 index 0000000000000..fe7579989c40f --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteFileInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; + +/** + * File writer + */ +interface WriteFileInterface +{ + /** + * Write file to filesystem + * + * @param FileInterface $file + * @throws LocalizedException + * @throws FileSystemException + */ + public function execute(FileInterface $file): void; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php new file mode 100644 index 0000000000000..943879ebaec86 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/Model/WriteMetadataInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryMetadataApi\Model; + +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; + +/** + * Metadata writer + */ +interface WriteMetadataInterface +{ + /** + * Add metadata to the file + * + * @param FileInterface $file + * @param MetadataInterface $data + */ + public function execute(FileInterface $file, MetadataInterface $data): FileInterface; +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/README.md b/app/code/Magento/MediaGalleryMetadataApi/README.md new file mode 100644 index 0000000000000..82f86d2f61c6d --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/README.md @@ -0,0 +1,3 @@ +# Magento_MediaGalleryMetadataApi + +The Magento_MediaGalleryMetadataApi module is responsible for the media gallery metadata implementation API. diff --git a/app/code/Magento/MediaGalleryMetadataApi/composer.json b/app/code/Magento/MediaGalleryMetadataApi/composer.json new file mode 100644 index 0000000000000..f8673884b050c --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-metadata-api", + "description": "Magento module responsible for media gallery metadata implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryMetadataApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml b/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml new file mode 100644 index 0000000000000..77adbc6efff88 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryMetadataApi"/> +</config> diff --git a/app/code/Magento/MediaGalleryMetadataApi/registration.php b/app/code/Magento/MediaGalleryMetadataApi/registration.php new file mode 100644 index 0000000000000..90988681a5483 --- /dev/null +++ b/app/code/Magento/MediaGalleryMetadataApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGalleryMetadataApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php b/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php new file mode 100644 index 0000000000000..339aca84ec68f --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Console/Command/Synchronize.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Console\Command; + +use Magento\Framework\Console\Cli; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Synchronize files in media storage and media assets database records + */ +class Synchronize extends Command +{ + /** + * @var SynchronizeInterface + */ + private $synchronizeAssets; + + /** + * @param SynchronizeInterface $synchronizeAssets + */ + public function __construct( + SynchronizeInterface $synchronizeAssets + ) { + $this->synchronizeAssets = $synchronizeAssets; + parent::__construct(); + } + + /** + * @inheritdoc + */ + protected function configure() + { + $this->setName('media-gallery:sync'); + $this->setDescription( + 'Synchronize media storage and media assets in the database' + ); + } + + /** + * @inheritdoc + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Synchronizing assets information from media storage to database...'); + + $this->synchronizeAssets->execute(); + + $output->writeln('Completed assets synchronization.'); + + return Cli::RETURN_SUCCESS; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/LICENSE.txt b/app/code/Magento/MediaGallerySynchronization/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Consume.php b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php new file mode 100644 index 0000000000000..79c0c9a1a803b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Consume.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; + +/** + * Media gallery image synchronization queue consumer. + */ +class Consume +{ + /** + * @var SynchronizeInterface + */ + private $synchronize; + + /** + * @param SynchronizeInterface $synchronize + */ + public function __construct(SynchronizeInterface $synchronize) + { + $this->synchronize = $synchronize; + } + + /** + * Run media files synchronization. + */ + public function execute() : void + { + $this->synchronize->execute(); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php new file mode 100644 index 0000000000000..87d477507b680 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/CreateAssetFromFile.php @@ -0,0 +1,148 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGallerySynchronization\Model\Filesystem\SplFileInfoFactory; +use Magento\MediaGallerySynchronization\Model\GetContentHash; + +/** + * Create media asset object based on the file information + */ +class CreateAssetFromFile +{ + /** + * Date format + */ + private const DATE_FORMAT = 'Y-m-d H:i:s'; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var TimezoneInterface; + */ + private $date; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var GetContentHash + */ + private $getContentHash; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @var SplFileInfoFactory + */ + private $splFileInfoFactory; + + /** + * @param Filesystem $filesystem + * @param File $driver + * @param TimezoneInterface $date + * @param AssetInterfaceFactory $assetFactory + * @param GetContentHash $getContentHash + * @param ExtractMetadataInterface $extractMetadata + * @param SplFileInfoFactory $splFileInfoFactory + */ + public function __construct( + Filesystem $filesystem, + File $driver, + TimezoneInterface $date, + AssetInterfaceFactory $assetFactory, + GetContentHash $getContentHash, + ExtractMetadataInterface $extractMetadata, + SplFileInfoFactory $splFileInfoFactory + ) { + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->date = $date; + $this->assetFactory = $assetFactory; + $this->getContentHash = $getContentHash; + $this->extractMetadata = $extractMetadata; + $this->splFileInfoFactory = $splFileInfoFactory; + } + + /** + * Create and format media asset object + * + * @param string $path + * @return AssetInterface + * @throws FileSystemException + */ + public function execute(string $path): AssetInterface + { + $absolutePath = $this->getMediaDirectory()->getAbsolutePath($path); + $file = $this->splFileInfoFactory->create($absolutePath); + [$width, $height] = getimagesize($absolutePath); + + $metadata = $this->extractMetadata->execute($absolutePath); + + return $this->assetFactory->create( + [ + 'id' => null, + 'path' => $path, + 'title' => $metadata->getTitle() ?: $file->getBasename('.' . $file->getExtension()), + 'description' => $metadata->getDescription(), + 'createdAt' => $this->date->date($file->getCTime())->format(self::DATE_FORMAT), + 'updatedAt' => $this->date->date($file->getMTime())->format(self::DATE_FORMAT), + 'width' => $width, + 'height' => $height, + 'hash' => $this->getHash($path), + 'size' => $file->getSize(), + 'contentType' => 'image/' . $file->getExtension(), + 'source' => 'Local' + ] + ); + } + + /** + * Get hash image content. + * + * @param string $path + * @return string + * @throws FileSystemException + */ + private function getHash(string $path): string + { + return $this->getContentHash->execute($this->getMediaDirectory()->readFile($path)); + } + + /** + * Retrieve media directory instance with read access + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php b/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php new file mode 100644 index 0000000000000..efc79d3c32423 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/FetchBatches.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\DB\Select; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\FlagManager; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; +use Psr\Log\LoggerInterface; + +/** + * Select data from database by provided batch size + */ +class FetchBatches implements FetchBatchesInterface +{ + private const LAST_EXECUTION_TIME_CODE = 'media_content_last_execution'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var int + */ + private $pageSize; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var FlagManager + */ + private $flagManager; + + /** + * @param FlagManager $flagManager + * @param ResourceConnection $resourceConnection + * @param LoggerInterface $logger + * @param int $pageSize + */ + public function __construct( + FlagManager $flagManager, + ResourceConnection $resourceConnection, + LoggerInterface $logger, + int $pageSize + ) { + $this->flagManager = $flagManager; + $this->resourceConnection = $resourceConnection; + $this->logger = $logger; + $this->pageSize = $pageSize; + } + + /** + * Get data from table by batches, based on limit offset value. + * + * @param string $tableName + * @param array $columns + * @param string|null $dateColumnName + */ + public function execute(string $tableName, array $columns, ?string $dateColumnName = null): \Traversable + { + try { + $connection = $this->resourceConnection->getConnection(); + $tableName = $this->resourceConnection->getTableName($tableName); + $totalPages = $this->getTotalPages($tableName); + + for ($page = 0; $page < $totalPages; $page++) { + $offset = $page * $this->pageSize; + $select = $connection->select() + ->from($this->resourceConnection->getTableName($tableName), $columns) + ->limit($this->pageSize, $offset); + if (!empty($dateColumnName)) { + $select = $this->addLastExecutionCondition($select, $dateColumnName); + } + yield $connection->fetchAll($select); + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + throw new LocalizedException( + __( + 'Could not fetch data from %tableName', + [ + 'tableName' => $tableName + ] + ) + ); + } + } + + /** + * Get where condition if last execution time set + * + * @param Select $select + * @param string $dateColumnName + * @return Select + */ + private function addLastExecutionCondition(Select $select, string $dateColumnName): Select + { + $lastExecutionTime = $this->flagManager->getFlagData(self::LAST_EXECUTION_TIME_CODE); + if (!empty($lastExecutionTime)) { + return $select->where($dateColumnName . ' > ?', $lastExecutionTime); + } + return $select; + } + + /** + * Return number of total pages by page size + * + * @param string $tableName + * @return float + */ + private function getTotalPages(string $tableName): float + { + $connection = $this->resourceConnection->getConnection(); + $total = $connection->fetchOne($connection->select()->from($tableName, 'COUNT(*)')); + return ceil($total / $this->pageSize); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php b/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php new file mode 100644 index 0000000000000..0643673ae30ab --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/FetchMediaStorageFileBatches.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Driver\File; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; +use Psr\Log\LoggerInterface; + +/** + * Fetch files from media storage in batches + */ +class FetchMediaStorageFileBatches +{ + /** + * @var GetAssetsIterator + */ + private $getAssetsIterator; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var File + */ + private $driver; + + /** + * @var string + */ + private $fileExtensions; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var int + */ + private $batchSize; + + /** + * @param LoggerInterface $log + * @param IsPathExcludedInterface $isPathExcluded + * @param Filesystem $filesystem + * @param GetAssetsIterator $assetsIterator + * @param File $driver + * @param int $batchSize + * @param array $fileExtensions + */ + public function __construct( + LoggerInterface $log, + IsPathExcludedInterface $isPathExcluded, + Filesystem $filesystem, + GetAssetsIterator $assetsIterator, + File $driver, + int $batchSize, + array $fileExtensions + ) { + $this->log = $log; + $this->isPathExcluded = $isPathExcluded; + $this->getAssetsIterator = $assetsIterator; + $this->filesystem = $filesystem; + $this->driver = $driver; + $this->batchSize = $batchSize; + $this->fileExtensions = $fileExtensions; + } + + /** + * Return files from files system by provided size of batch + */ + public function execute(): \Traversable + { + $i = 0; + $batch = []; + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + + /** @var \SplFileInfo $file */ + foreach ($this->getAssetsIterator->execute($mediaDirectory->getAbsolutePath()) as $file) { + $relativePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA) + ->getRelativePath($file->getPathName()); + if (!$this->isApplicable($relativePath)) { + continue; + } + + $batch[] = $relativePath; + if (++$i == $this->batchSize) { + yield $batch; + $i = 0; + $batch = []; + } + } + if (count($batch) > 0) { + yield $batch; + } + } + + /** + * Can synchronization be applied to asset with provided path + * + * @param string $path + * @return bool + */ + private function isApplicable(string $path): bool + { + try { + return $path + && !$this->isPathExcluded->execute($path) + && preg_match('#\.(' . implode("|", $this->fileExtensions) . ')$# i', $path); + } catch (\Exception $exception) { + $this->log->critical($exception); + return false; + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php new file mode 100644 index 0000000000000..1fbfae640a732 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Filesystem/SplFileInfoFactory.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model\Filesystem; + +/** + * Creates a new file based on the file name parameter. + */ +class SplFileInfoFactory +{ + /** + * Creates SplFileInfo from filename + * + * @param string $fileName + * @return \SplFileInfo + */ + public function create(string $fileName) : \SplFileInfo + { + return new \SplFileInfo($fileName); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php new file mode 100644 index 0000000000000..5e825d57c5ce7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetFromPath.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\ValidatorException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronization\Model\Filesystem\SplFileInfoFactory; + +/** + * Create media asset object based on the file information + */ +class GetAssetFromPath +{ + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var CreateAssetFromFile + */ + private $createAssetFromFile; + + /** + * @var SplFileInfoFactory + */ + private $splFileInfoFactory; + + /** + * @param AssetInterfaceFactory $assetFactory + * @param GetAssetsByPathsInterface $getMediaGalleryAssetByPath + * @param CreateAssetFromFile $createAssetFromFile + * @param SplFileInfoFactory $splFileInfoFactory + */ + public function __construct( + AssetInterfaceFactory $assetFactory, + GetAssetsByPathsInterface $getMediaGalleryAssetByPath, + CreateAssetFromFile $createAssetFromFile, + SplFileInfoFactory $splFileInfoFactory + ) { + $this->assetFactory = $assetFactory; + $this->getAssetsByPaths = $getMediaGalleryAssetByPath; + $this->createAssetFromFile = $createAssetFromFile; + $this->splFileInfoFactory= $splFileInfoFactory; + } + + /** + * Create media asset object based on the file information + * + * @param string $path + * @return AssetInterface + * @throws LocalizedException + * @throws ValidatorException + */ + public function execute(string $path): AssetInterface + { + $asset = $this->getAsset($path); + $assetFromFile = $this->createAssetFromFile->execute($path); + + if (!$asset) { + return $assetFromFile; + } + + return $this->assetFactory->create( + [ + 'id' => $asset->getId(), + 'path' => $path, + 'title' => $asset->getTitle(), + 'description' => $asset->getDescription() ?? $assetFromFile->getDescription(), + 'width' => $assetFromFile->getWidth(), + 'height' => $assetFromFile->getHeight(), + 'hash' => $assetFromFile->getHash(), + 'size' => $assetFromFile->getSize(), + 'contentType' => $asset->getContentType(), + 'source' => $asset->getSource() + ] + ); + } + + /** + * Returns asset if asset already exist by provided path + * + * @param string $path + * @return AssetInterface|null + * @throws ValidatorException + * @throws LocalizedException + */ + private function getAsset(string $path): ?AssetInterface + { + $asset = $this->getAssetsByPaths->execute([$path]); + return !empty($asset) ? $asset[0] : null; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php new file mode 100644 index 0000000000000..6c0592c49f09c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetAssetsIterator.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +/** + * Retrieve media storage assets iterator + */ +class GetAssetsIterator +{ + /** + * Get media storage assets iterator for provided path + * + * @param string $path + * @return \RecursiveIteratorIterator + */ + public function execute(string $path): \RecursiveIteratorIterator + { + return new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $path, + \FilesystemIterator::SKIP_DOTS | + \FilesystemIterator::UNIX_PATHS | + \RecursiveDirectoryIterator::FOLLOW_SYMLINKS + ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php b/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php new file mode 100644 index 0000000000000..703fd56c4f0b8 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/GetContentHash.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +/** + * Get hashed value of image content. + */ +class GetContentHash +{ + /** + * Return the hash value of the given filepath. + * + * @param string $content + * @return string + */ + public function execute(string $content): string + { + return sha1($content); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php b/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php new file mode 100644 index 0000000000000..361137ad27686 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ImportImageFileKeywords.php @@ -0,0 +1,152 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; +use Magento\MediaGalleryMetadataApi\Api\ExtractMetadataInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; + +/** + * import image keywords from file metadata + */ +class ImportImageFileKeywords implements ImportFilesInterface +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var File + */ + private $driver; + + /** + * @var KeywordInterfaceFactory + */ + private $keywordFactory; + + /** + * @var AssetKeywordsInterfaceFactory + */ + private $assetKeywordsFactory; + + /** + * @var ExtractMetadataInterface + */ + private $extractMetadata; + + /** + * @var SaveAssetsKeywordsInterface + */ + private $saveAssetKeywords; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @param File $driver + * @param Filesystem $filesystem + * @param KeywordInterfaceFactory $keywordFactory + * @param ExtractMetadataInterface $extractMetadata + * @param SaveAssetsKeywordsInterface $saveAssetKeywords + * @param AssetKeywordsInterfaceFactory $assetKeywordsFactory + * @param GetAssetsByPathsInterface $getAssetsByPaths + */ + public function __construct( + File $driver, + Filesystem $filesystem, + KeywordInterfaceFactory $keywordFactory, + ExtractMetadataInterface $extractMetadata, + SaveAssetsKeywordsInterface $saveAssetKeywords, + AssetKeywordsInterfaceFactory $assetKeywordsFactory, + GetAssetsByPathsInterface $getAssetsByPaths + ) { + $this->driver = $driver; + $this->filesystem = $filesystem; + $this->keywordFactory = $keywordFactory; + $this->extractMetadata = $extractMetadata; + $this->saveAssetKeywords = $saveAssetKeywords; + $this->assetKeywordsFactory = $assetKeywordsFactory; + $this->getAssetsByPaths = $getAssetsByPaths; + } + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $keywords = []; + + foreach ($paths as $path) { + $metadataKeywords = $this->getMetadataKeywords($path); + if ($metadataKeywords !== null) { + $keywords[$path] = $metadataKeywords; + } + } + + $assets = $this->getAssetsByPaths->execute(array_keys($keywords)); + + $assetKeywords = []; + + foreach ($assets as $asset) { + $assetKeywords[] = $this->assetKeywordsFactory->create([ + 'assetId' => $asset->getId(), + 'keywords' => $keywords[$asset->getPath()] + ]); + } + + $this->saveAssetKeywords->execute($assetKeywords); + } + + /** + * Get keywords from file metadata + * + * @param string $path + * @return KeywordInterface[]|null + */ + private function getMetadataKeywords(string $path): ?array + { + $metadataKeywords = $this->extractMetadata->execute($this->getMediaDirectory()->getAbsolutePath($path)) + ->getKeywords(); + if ($metadataKeywords === null) { + return null; + } + + $keywords = []; + + foreach ($metadataKeywords as $keyword) { + $keywords[] = $this->keywordFactory->create( + [ + 'keyword' => $keyword + ] + ); + } + + return $keywords; + } + + /** + * Retrieve media directory instance with read access + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php b/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php new file mode 100644 index 0000000000000..3cac99f816d12 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ImportMediaAsset.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; + +/** + * Import image file to the media gallery asset table + */ +class ImportMediaAsset implements ImportFilesInterface +{ + /** + * @var SaveAssetsInterface + */ + private $saveAssets; + + /** + * @var GetAssetFromPath + */ + private $getAssetFromPath; + + /** + * @param SaveAssetsInterface $saveAssets + * @param GetAssetFromPath $getAssetFromPath + */ + public function __construct( + SaveAssetsInterface $saveAssets, + GetAssetFromPath $getAssetFromPath + ) { + $this->saveAssets = $saveAssets; + $this->getAssetFromPath = $getAssetFromPath; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + $assets = []; + + foreach ($paths as $path) { + $assets[] = $this->getAssetFromPath->execute($path); + } + + $this->saveAssets->execute($assets); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Publish.php b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php new file mode 100644 index 0000000000000..386798d68d9df --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Publish.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\MessageQueue\PublisherInterface; + +/** + * Publish media gallery synchronization queue. + */ +class Publish +{ + /** + * Media gallery synchronization queue topic name. + */ + private const TOPIC_MEDIA_GALLERY_SYNCHRONIZATION = 'media.gallery.synchronization'; + + /** + * @var PublisherInterface + */ + private $publisher; + + /** + * @param PublisherInterface $publisher + */ + public function __construct(PublisherInterface $publisher) + { + $this->publisher = $publisher; + } + + /** + * Publish media content synchronization message to the message queue. + */ + public function execute() : void + { + $this->publisher->publish( + self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION, + [self::TOPIC_MEDIA_GALLERY_SYNCHRONIZATION] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php b/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php new file mode 100644 index 0000000000000..d70547a7528e0 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/ResolveNonExistedAssets.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface; +use Psr\Log\LoggerInterface; + +/** + * Delete assets which not exist physically + */ +class ResolveNonExistedAssets +{ + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + private const MEDIA_GALLERY_ASSET_PATH = 'path'; + + /** + * @var ResourceConnection + */ + private $resourceConnection; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Read + */ + private $mediaDirectory; + + /** + * @var FetchBatchesInterface + */ + private $selectBatches; + + /** + * @param Filesystem $filesystem + * @param ResourceConnection $resourceConnection + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param LoggerInterface $logger + * @param FetchBatchesInterface $selectBatches + */ + public function __construct( + Filesystem $filesystem, + ResourceConnection $resourceConnection, + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + LoggerInterface $logger, + FetchBatchesInterface $selectBatches + ) { + $this->filesystem = $filesystem; + $this->resourceConnection = $resourceConnection; + $this->deleteAssetsByPaths = $deleteAssetsByPaths; + $this->logger = $logger; + $this->selectBatches = $selectBatches; + } + + /** + * Delete assets which not existed + * + * @return void + */ + public function execute(): void + { + $columns = [self::MEDIA_GALLERY_ASSET_PATH]; + try { + foreach ($this->selectBatches->execute(self::TABLE_MEDIA_GALLERY_ASSET, $columns, null) as $batch) { + foreach ($batch as $item) { + if (!$this->getMediaDirectory()->isExist($item[self::MEDIA_GALLERY_ASSET_PATH])) { + $this->deleteAssetsByPaths->execute([$item[self::MEDIA_GALLERY_ASSET_PATH]]); + } + } + } + } catch (\Exception $exception) { + $this->logger->critical($exception); + } + } + + /** + * Retrieve media directory instance with read permissions + * + * @return Read + */ + private function getMediaDirectory(): Read + { + if (!$this->mediaDirectory) { + $this->mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } + return $this->mediaDirectory; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php b/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php new file mode 100644 index 0000000000000..4396ea6a77736 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/Synchronize.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface; +use Magento\MediaGallerySynchronizationApi\Model\SynchronizerPool; +use Psr\Log\LoggerInterface; + +/** + * Synchronize media storage and media assets database records + */ +class Synchronize implements SynchronizeInterface +{ + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var SynchronizerPool + */ + private $synchronizerPool; + + /** + * @var FetchMediaStorageFileBatches + */ + private $batchGenerator; + + /** + * @var ResolveNonExistedAssets + */ + private $resolveNonExistedAssets; + + /** + * @param ResolveNonExistedAssets $resolveNonExistedAssets + * @param LoggerInterface $log + * @param SynchronizerPool $synchronizerPool + * @param FetchMediaStorageFileBatches $batchGenerator + */ + public function __construct( + ResolveNonExistedAssets $resolveNonExistedAssets, + LoggerInterface $log, + SynchronizerPool $synchronizerPool, + FetchMediaStorageFileBatches $batchGenerator + ) { + $this->resolveNonExistedAssets = $resolveNonExistedAssets; + $this->log = $log; + $this->synchronizerPool = $synchronizerPool; + $this->batchGenerator = $batchGenerator; + } + + /** + * @inheritdoc + */ + public function execute(): void + { + $failed = []; + + foreach ($this->synchronizerPool->get() as $name => $synchronizer) { + if (!$synchronizer instanceof SynchronizeFilesInterface) { + $failed[] = $name; + continue; + } + foreach ($this->batchGenerator->execute() as $batch) { + try { + $synchronizer->execute($batch); + } catch (\Exception $exception) { + $this->log->critical($exception); + $failed[] = $name; + } + } + } + + $this->resolveNonExistedAssets->execute(); + if (!empty($failed)) { + throw new LocalizedException( + __( + 'Failed to execute the following synchronizers: %synchronizers', + [ + 'synchronizers' => implode(', ', $failed) + ] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php new file mode 100644 index 0000000000000..81e9629f703f3 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Model/SynchronizeFiles.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Model; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\ReadInterface; +use Magento\Framework\Filesystem\Driver\File; +use Magento\Framework\Stdlib\DateTime\DateTime; +use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface; +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; +use Magento\MediaGallerySynchronization\Model\Filesystem\SplFileInfoFactory; +use Psr\Log\LoggerInterface; + +/** + * Synchronize files in media storage and media assets database records + */ +class SynchronizeFiles implements SynchronizeFilesInterface +{ + /** + * Date format + */ + private const DATE_FORMAT = 'Y-m-d H:i:s'; + + /** + * @var LoggerInterface + */ + private $log; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var GetAssetsByPathsInterface + */ + private $getAssetsByPaths; + + /** + * @var File + */ + private $driver; + + /** + * @var SplFileInfoFactory + */ + private $splFileInfoFactory; + + /** + * @var ImportFilesInterface + */ + private $importFiles; + + /** + * @var DateTime + */ + private $date; + + /** + * @param File $driver + * @param Filesystem $filesystem + * @param DateTime $date + * @param LoggerInterface $log + * @param SplFileInfoFactory $splFileInfoFactory + * @param GetAssetsByPathsInterface $getAssetsByPaths + * @param ImportFilesInterface $importFiles + */ + public function __construct( + File $driver, + Filesystem $filesystem, + DateTime $date, + LoggerInterface $log, + SplFileInfoFactory $splFileInfoFactory, + GetAssetsByPathsInterface $getAssetsByPaths, + ImportFilesInterface $importFiles + ) { + $this->driver = $driver; + $this->filesystem = $filesystem; + $this->date = $date; + $this->log = $log; + $this->splFileInfoFactory = $splFileInfoFactory; + $this->getAssetsByPaths = $getAssetsByPaths; + $this->importFiles = $importFiles; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + try { + $this->importFiles->execute($this->getPathsToUpdate($paths)); + } catch (LocalizedException $localizedException) { + throw $localizedException; + } catch (\Exception $exception) { + $this->log->critical($exception); + throw new LocalizedException( + __( + 'Could not import media assets for files: %files', + [ + 'files' => implode(', ', $paths) + ] + ) + ); + } + } + + /** + * Return existing assets from files + * + * @param string[] $paths + * @return array + * @throws LocalizedException + */ + private function getPathsToUpdate(array $paths): array + { + $assetPaths = []; + + foreach ($paths as $path) { + $assetPath = $this->getAssetPath($path); + $assetPaths[$assetPath] = $assetPath; + } + + $assets = $this->getAssetsByPaths->execute($assetPaths); + + foreach ($assets as $asset) { + if ($asset->getUpdatedAt() === $this->getFileModificationTime($asset->getPath())) { + unset($assetPaths[$asset->getPath()]); + } + } + + return $assetPaths; + } + + /** + * Retrieve formatted file modification time + * + * @param string $path + * @return string + */ + private function getFileModificationTime(string $path): string + { + return $this->date->gmtDate( + self::DATE_FORMAT, + $this->splFileInfoFactory->create($this->getMediaDirectory()->getAbsolutePath($path))->getMTime() + ); + } + + /** + * Get correct path for media asset + * + * @param string $path + * @return string + */ + private function getAssetPath(string $path): string + { + return $this->driver->getParentDirectory($path) === '.' ? '/' . $path : $path; + } + + /** + * Retrieve media directory instance + * + * @return ReadInterface + */ + private function getMediaDirectory(): ReadInterface + { + return $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php b/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php new file mode 100644 index 0000000000000..9583c91184d1a --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Plugin/MediaGallerySyncTrigger.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Plugin; + +use Magento\Framework\App\Config\Value; +use Magento\MediaGallerySynchronization\Model\Publish; + +/** + * Plugin to synchronize media storage and media assets database records when media gallery enabled in configuration. + */ +class MediaGallerySyncTrigger +{ + private const MEDIA_GALLERY_CONFIG_VALUE = 'system/media_gallery/enabled'; + private const MEDIA_GALLERY_ENABLED_VALUE = 1; + + /** + * @var Publish + */ + private $publish; + + /** + * @param Publish $publish + */ + public function __construct(Publish $publish) + { + $this->publish = $publish; + } + + /** + * Update media gallery grid table when configuration is saved and media gallery enabled. + * + * @param Value $config + * @param Value $result + * @return Value + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterSave(Value $config, Value $result): Value + { + if ($result->getPath() === self::MEDIA_GALLERY_CONFIG_VALUE + && $result->isValueChanged() + && (int) $result->getValue() === self::MEDIA_GALLERY_ENABLED_VALUE + ) { + $this->publish->execute(); + } + + return $result; + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/README.md b/app/code/Magento/MediaGallerySynchronization/README.md new file mode 100644 index 0000000000000..4947c18986f3b --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/README.md @@ -0,0 +1,14 @@ +# Magento_MediaGallerySynchronization module + +The Magento_MediaGallerySynchronization module represents implementation of synchronization between data and objects contains +media asset information. + +## Extensibility + +Extension developers can interact with the Magento_MediaGallerySynchronization module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallerySynchronization module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php new file mode 100644 index 0000000000000..a9c428fed7bca --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/Test/Integration/Model/GetContentHashTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronization\Test\Integration\Model; + +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\MediaGallerySynchronization\Model\GetContentHash; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetContentHash. + */ +class GetContentHashTest extends TestCase +{ + /** + * @var GetContentHash + */ + private $getContentHash; + + /** + * @var DriverInterface + */ + private $driver; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->getContentHash = Bootstrap::getObjectManager()->get(GetContentHash::class); + $this->driver = Bootstrap::getObjectManager()->get(DriverInterface::class); + } + + /** + * Test for GetContentHash::execute + * + * @dataProvider filesProvider + * @param string $firstFile + * @param string $secondFile + * @param bool $isEqual + * @throws FileSystemException + */ + public function testExecute( + string $firstFile, + string $secondFile, + bool $isEqual + ): void { + $firstHash = $this->getContentHash->execute($this->getImageContent($firstFile)); + $secondHash = $this->getContentHash->execute($this->getImageContent($secondFile)); + $isEqual ? $this->assertEquals($firstHash, $secondHash) : $this->assertNotEquals($firstHash, $secondHash); + } + + /** + * Data provider for testExecute + * + * @return array[] + */ + public function filesProvider(): array + { + return [ + [ + 'magento.jpg', + 'magento_2.jpg', + true + ], + [ + 'magento.jpg', + 'magento_3.png', + false + ] + ]; + } + + /** + * Get image file content. + * + * @param string $filename + * @return string + * @throws FileSystemException + */ + private function getImageContent(string $filename): string + { + return $this->driver->fileGetContents($this->getImageFilePath($filename)); + } + + /** + * Return image file path + * + * @param string $filename + * @return string + */ + private function getImageFilePath(string $filename): string + { + return dirname(__DIR__, 1) + . DIRECTORY_SEPARATOR + . implode( + DIRECTORY_SEPARATOR, + [ + '_files', + $filename + ] + ); + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento.jpg differ diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg new file mode 100644 index 0000000000000..c377daf8fb0b3 Binary files /dev/null and b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_2.jpg differ diff --git a/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png new file mode 100644 index 0000000000000..366b1b8b9c3f7 Binary files /dev/null and b/app/code/Magento/MediaGallerySynchronization/Test/Integration/_files/magento_3.png differ diff --git a/app/code/Magento/MediaGallerySynchronization/composer.json b/app/code/Magento/MediaGallerySynchronization/composer.json new file mode 100644 index 0000000000000..e1d4962366978 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/composer.json @@ -0,0 +1,25 @@ +{ + "name": "magento/module-media-gallery-synchronization", + "description": "Magento module provides implementation of the media gallery data synchronization.", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/framework-message-queue": "*", + "magento/module-media-gallery-metadata-api": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronization\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronization/etc/communication.xml b/app/code/Magento/MediaGallerySynchronization/etc/communication.xml new file mode 100644 index 0000000000000..ba5ae5fc9f9bc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/communication.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:Communication/etc/communication.xsd"> + <topic name="media.gallery.synchronization" is_synchronous="false" request="string[]"> + <handler name="media.gallery.synchronization.handler" + type="Magento\MediaGallerySynchronization\Model\Consume" method="execute"/> + </topic> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/di.xml b/app/code/Magento/MediaGallerySynchronization/etc/di.xml new file mode 100644 index 0000000000000..47a4360575b2e --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/di.xml @@ -0,0 +1,53 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGallerySynchronizationApi\Api\SynchronizeInterface" type="Magento\MediaGallerySynchronization\Model\Synchronize"/> + <preference for="Magento\MediaGallerySynchronizationApi\Model\FetchBatchesInterface" type="Magento\MediaGallerySynchronization\Model\FetchBatches"/> + <preference for="Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface" type="Magento\MediaGallerySynchronization\Model\SynchronizeFiles"/> + <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <arguments> + <argument name="importers" xsi:type="array"> + <item name="0" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportMediaAsset</item> + <item name="1" xsi:type="object">Magento\MediaGallerySynchronization\Model\ImportImageFileKeywords</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\SynchronizerPool"> + <arguments> + <argument name="synchronizers" xsi:type="array"> + <item name="media_gallery_asset_synchronizer" xsi:type="object">Magento\MediaGallerySynchronization\Model\SynchronizeFiles</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronization\Model\FetchMediaStorageFileBatches"> + <arguments> + <argument name="batchSize" xsi:type="number">100</argument> + <argument name="fileExtensions" xsi:type="array"> + <item name="jpg" xsi:type="string">jpg</item> + <item name="jpeg" xsi:type="string">jpeg</item> + <item name="gif" xsi:type="string">gif</item> + <item name="png" xsi:type="string">png</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronization\Model\FetchBatches"> + <arguments> + <argument name="pageSize" xsi:type="number">100</argument> + </arguments> + </type> + <type name="Magento\Framework\Console\CommandListInterface"> + <arguments> + <argument name="commands" xsi:type="array"> + <item name="mediaGallerySynchronization" xsi:type="object">Magento\MediaGallerySynchronization\Console\Command\Synchronize</item> + </argument> + </arguments> + </type> + <type name="Magento\Framework\App\Config\Value"> + <plugin name="admin_system_config_adobe_stock_save_plugin" type="Magento\MediaGallerySynchronization\Plugin\MediaGallerySyncTrigger"/> + </type> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/module.xml b/app/code/Magento/MediaGallerySynchronization/etc/module.xml new file mode 100644 index 0000000000000..496f6aa0233a5 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGallerySynchronization" /> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml new file mode 100644 index 0000000000000..4471d68fd8c47 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/queue_consumer.xml @@ -0,0 +1,11 @@ +<?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-message-queue:etc/consumer.xsd"> + <consumer name="media.gallery.synchronization" queue="media.gallery.synchronization" + connection="db" handler="Magento\MediaGallerySynchronization\Model\Consume::execute"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml new file mode 100644 index 0000000000000..1a7cb04847c4a --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/queue_publisher.xml @@ -0,0 +1,12 @@ +<!-- +/** + * 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-message-queue:etc/publisher.xsd"> + <publisher topic="media.gallery.synchronization"> + <connection name="db" exchange="magento-db" disabled="false" /> + </publisher> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml b/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml new file mode 100644 index 0000000000000..81baefbfc53dc --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/etc/queue_topology.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/topology.xsd"> + <exchange name="magento-db" type="topic" connection="db"> + <binding id="MediaGallerySynchronization" topic="media.gallery.synchronization" + destinationType="queue" destination="media.gallery.synchronization"/> + </exchange> +</config> diff --git a/app/code/Magento/MediaGallerySynchronization/registration.php b/app/code/Magento/MediaGallerySynchronization/registration.php new file mode 100644 index 0000000000000..9e5f42b14c985 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronization/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGallerySynchronization', + __DIR__ +); diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php new file mode 100644 index 0000000000000..de5b00f99e059 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeFilesInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Api; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Synchronize assets from the provided files information to database + */ +interface SynchronizeFilesInterface +{ + /** + * Create media gallery assets based on files information and save them to database + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php new file mode 100644 index 0000000000000..0b49780bd7590 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Api/SynchronizeInterface.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Api; + +/** + * Synchronize assets from the media storage to database + */ +interface SynchronizeInterface +{ + /** + * Synchronize assets from the media storage to database + * + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function execute(): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php new file mode 100644 index 0000000000000..42cd8265d5087 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/FetchBatchesInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +/** + * Fetch data from database in batches + */ +interface FetchBatchesInterface +{ + /** + * Fetch the columns from the database table in batches + * $modificationDateColumn contains the entities which were changed since last execution + * to avoid fetching items that have been previously synchronized + * + * @param string $tableName + * @param array $columns + * @param string|null $modificationDateColumn + * @return \Traversable + */ + public function execute(string $tableName, array $columns, ?string $modificationDateColumn): \Traversable; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php new file mode 100644 index 0000000000000..8e5df842d8a55 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesComposite.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +/** + * File save pool + */ +class ImportFilesComposite implements ImportFilesInterface +{ + /** + * @var ImportFilesInterface[] + */ + private $importers; + + /** + * @param ImportFilesInterface[] $importers + */ + public function __construct(array $importers) + { + ksort($importers); + $this->importers = $importers; + } + + /** + * @inheritdoc + */ + public function execute(array $paths): void + { + foreach ($this->importers as $importer) { + $importer->execute($paths); + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php new file mode 100644 index 0000000000000..40e5947d3a11d --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/ImportFilesInterface.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +use Magento\Framework\Exception\LocalizedException; + +/** + * Save media files data + */ +interface ImportFilesInterface +{ + /** + * Save media files data + * + * @param string[] $paths + * @throws LocalizedException + */ + public function execute(array $paths): void; +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php b/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php new file mode 100644 index 0000000000000..1294a4f7679f1 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/Model/SynchronizerPool.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallerySynchronizationApi\Model; + +use Magento\MediaGallerySynchronizationApi\Api\SynchronizeFilesInterface; + +/** + * A pool of Media storage to database synchronizers + * @see SynchronizeFilesInterface + */ +class SynchronizerPool +{ + /** + * Media storage to database synchronizers + * + * @var SynchronizeFilesInterface[] + */ + private $synchronizers; + + /** + * @param SynchronizeFilesInterface[] $synchronizers + */ + public function __construct(array $synchronizers = []) + { + foreach ($synchronizers as $name => $synchronizer) { + if (!$synchronizer instanceof SynchronizeFilesInterface) { + throw new \InvalidArgumentException(sprintf( + 'Synchronizer %s must implement %s.', + $name, + SynchronizeFilesInterface::class + )); + } + } + $this->synchronizers = $synchronizers; + } + + /** + * Get all synchronizers from the pool + * + * @return SynchronizeFilesInterface[] + */ + public function get(): array + { + return $this->synchronizers; + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/README.md b/app/code/Magento/MediaGallerySynchronizationApi/README.md new file mode 100644 index 0000000000000..1a12883413920 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGallerySynchronizationApi module + +The Magento_MediaGallerySynchronizationApi module is responsible for the media gallery data synchronization implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaGallerySynchronizationApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGallerySynchronizationApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGallerySynchronizationApi/composer.json b/app/code/Magento/MediaGallerySynchronizationApi/composer.json new file mode 100644 index 0000000000000..427bd2bd4aca7 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-synchronization-api", + "description": "Magento module responsible for the media gallery synchronization implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGallerySynchronizationApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml b/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml new file mode 100644 index 0000000000000..5cf3b424539bd --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/etc/di.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGallerySynchronizationApi\Model\ImportFilesInterface" type="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"/> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml b/app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml new file mode 100644 index 0000000000000..48736124400c9 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGallerySynchronizationApi" /> +</config> diff --git a/app/code/Magento/MediaGallerySynchronizationApi/registration.php b/app/code/Magento/MediaGallerySynchronizationApi/registration.php new file mode 100644 index 0000000000000..542a46d02dd33 --- /dev/null +++ b/app/code/Magento/MediaGallerySynchronizationApi/registration.php @@ -0,0 +1,14 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register( + ComponentRegistrar::MODULE, + 'Magento_MediaGallerySynchronizationApi', + __DIR__ +); diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php new file mode 100644 index 0000000000000..df13250eacb5f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Asset/Search.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Asset; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the asset options for multiselect filter + */ +class Search extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var SearchAssetsInterface + */ + private $searchAssets; + + /** + * @param SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var Images + */ + private $images; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var Storage + */ + private $storage; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param FilterBuilder $filterBuilder + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param SearchAssetsInterface $searchAssets + * @param Context $context + * @param LoggerInterface $logger + * @param Images $images + * @param Storage $storage + */ + public function __construct( + FilterBuilder $filterBuilder, + SearchCriteriaBuilder $searchCriteriaBuilder, + FilterGroupBuilder $filterGroupBuilder, + SearchAssetsInterface $searchAssets, + Context $context, + LoggerInterface $logger, + Images $images, + Storage $storage + ) { + parent::__construct($context); + + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->logger = $logger; + $this->searchAssets = $searchAssets; + $this->images = $images; + $this->storage = $storage; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $searchKey = $this->getRequest()->getParam('searchKey'); + $limit = $this->getRequest()->getParam('limit'); + $pageNum = $this->getRequest()->getParam('page'); + $responseContent = []; + + if (!$searchKey) { + return $resultJson->setData([ + 'options' => [], + 'total' => 0 + ]); + } + + try { + $titleFilter = $this->filterBuilder->setField('title') + ->setConditionType('fulltext') + ->setValue($searchKey) + ->create(); + $searchCriteria = $this->searchCriteriaBuilder + ->setFilterGroups([$this->filterGroupBuilder->setFilters([$titleFilter])->create()]) + ->setPageSize($limit) + ->setCurrentPage($pageNum < 2 ? 0 : $pageNum) + ->create(); + + $assets = $this->searchAssets->execute($searchCriteria); + + if (!empty($assets)) { + foreach ($assets as $asset) { + $responseContent['options'][] = [ + 'value' => (string) $asset->getId(), + 'label' => $asset->getTitle(), + 'path' => $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $asset->getPath()) + ]; + $responseContent['total'] = count($responseContent['options']); + } + } + + $responseCode = self::HTTP_OK; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php new file mode 100644 index 0000000000000..3d4af88e4ad67 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Create.php @@ -0,0 +1,108 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\CreateDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller to create the folders + */ +class Create extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var CreateDirectoriesByPathsInterface + */ + private $createDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param CreateDirectoriesByPathsInterface $createDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + CreateDirectoriesByPathsInterface $createDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->createDirectoriesByPaths = $createDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Create folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $paths = $this->getRequest()->getParam('paths'); + + if (!$paths) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder paths parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->createDirectoriesByPaths->execute($paths); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully created the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to create folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php new file mode 100644 index 0000000000000..56f12c5139d65 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/Delete.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\DeleteAssetsByPathsInterface; +use Magento\MediaGalleryApi\Api\DeleteDirectoriesByPathsInterface; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the folders + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var DeleteAssetsByPathsInterface + */ + private $deleteAssetsByPaths; + + /** + * @var DeleteDirectoriesByPathsInterface + */ + private $deleteDirectoriesByPaths; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param DeleteAssetsByPathsInterface $deleteAssetsByPaths + * @param DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteAssetsByPathsInterface $deleteAssetsByPaths, + DeleteDirectoriesByPathsInterface $deleteDirectoriesByPaths, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteAssetsByPaths = $deleteAssetsByPaths; + $this->deleteDirectoriesByPaths = $deleteDirectoriesByPaths; + $this->logger = $logger; + } + + /** + * Delete folder by provided path. + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $path = $this->getRequest()->getParam('path'); + + if (!$path) { + $responseContent = [ + 'success' => false, + 'message' => __('Folder path parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->deleteDirectoriesByPaths->execute([$path]); + $this->deleteAssetsByPaths->execute([$path]); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully removed the folder.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to remove folder.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php new file mode 100644 index 0000000000000..d4885cae055dd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Directories/GetTree.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Directories; + +use Magento\Backend\App\Action; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\MediaGalleryUi\Model\Directories\GetFolderTree; +use Psr\Log\LoggerInterface; + +/** + * Returns all available directories + */ +class GetTree extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var GetFolderTree + */ + private $getFolderTree; + + /** + * Constructor + * + * @param Action\Context $context + * @param LoggerInterface $logger + * @param GetFolderTree $getFolderTree + */ + public function __construct( + Action\Context $context, + LoggerInterface $logger, + GetFolderTree $getFolderTree + ) { + parent::__construct($context); + $this->logger = $logger; + $this->getFolderTree = $getFolderTree; + } + /** + * @inheritdoc + */ + public function execute() + { + try { + $responseContent[] = $this->getFolderTree->execute(); + $responseCode = self::HTTP_OK; + } catch (\Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('Retrieving directories list failed.'), + ]; + } + + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php new file mode 100644 index 0000000000000..a5d1cee7abf41 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Delete.php @@ -0,0 +1,143 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryUi\Model\DeleteImage; +use Psr\Log\LoggerInterface; + +/** + * Controller deleting the media gallery content + */ +class Delete extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var DeleteImage + */ + private $deleteImage; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Delete constructor. + * + * @param Context $context + * @param DeleteImage $deleteImage + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param Storage $imagesStorage + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + DeleteImage $deleteImage, + GetAssetsByIdsInterface $getAssetsByIds, + Storage $imagesStorage, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->deleteImage = $deleteImage; + $this->getAssetsByIds = $getAssetsByIds; + $this->imagesStorage = $imagesStorage; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $imageIds = $this->getRequest()->getParam('ids'); + + if (empty($imageIds) || !is_array($imageIds)) { + $responseContent = [ + 'success' => false, + 'message' => __('Image Ids are required and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $assets = $this->getAssetsByIds->execute($imageIds); + $this->deleteImage->execute($assets); + $responseCode = self::HTTP_OK; + if (count($imageIds) === 1) { + $message = __( + 'The asset "%title" has been successfully deleted.', + [ + 'title' => current($assets)->getTitle() + ] + ); + } else { + $message = __( + '%count assets have been successfully deleted.', + [ + 'count' => count($imageIds) + ] + ); + } + $responseContent = [ + 'success' => true, + 'message' => $message, + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to delete image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php new file mode 100644 index 0000000000000..d959a070148ed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Details.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\GetDetailsByAssetId; +use Psr\Log\LoggerInterface; + +/** + * Controller getting the media gallery image details + */ +class Details extends Action implements HttpGetActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var GetDetailsByAssetId + */ + private $getDetailsByAssetId; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * Details constructor. + * + * @param Context $context + * @param GetDetailsByAssetId $getDetailsByAssetId + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + GetDetailsByAssetId $getDetailsByAssetId, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->logger = $logger; + $this->getDetailsByAssetId = $getDetailsByAssetId; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $ids = $this->getRequest()->getParam('ids'); + + if (empty($ids) || !is_array($ids)) { + $responseContent = [ + 'success' => false, + 'message' => __('Assets Ids is required, and must be of type array.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $details = $this->getDetailsByAssetId->execute($ids); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'imageDetails' => $details + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to get image details.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php new file mode 100644 index 0000000000000..f41c489607b15 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/SaveDetails.php @@ -0,0 +1,128 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterfaceFactory; +use Magento\MediaGalleryUi\Model\UpdateAsset; +use Psr\Log\LoggerInterface; + +class SaveDetails extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_INTERNAL_ERROR = 500; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var UpdateAsset + */ + private $updateAsset; + + /** + * @var MetadataInterfaceFactory + */ + private $metadataFactory; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param MetadataInterfaceFactory $metadataFactory + * @param UpdateAsset $updateAsset + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + MetadataInterfaceFactory $metadataFactory, + UpdateAsset $updateAsset, + LoggerInterface $logger + ) { + parent::__construct($context); + + $this->metadataFactory = $metadataFactory; + $this->updateAsset = $updateAsset; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $assetId = (int) $this->getRequest()->getParam('id'); + $title = $this->getRequest()->getParam('title'); + $description = $this->getRequest()->getParam('description'); + $keywords = (array) $this->getRequest()->getParam('keywords'); + + if ($assetId === 0) { + $responseContent = [ + 'success' => false, + 'message' => __('Image ID is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->updateAsset->execute( + $assetId, + $this->metadataFactory->create([ + 'title' => $title, + 'description' => $description, + 'keywords' => $keywords + ]) + ); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('You have successfully saved the image "%image"', ['image' => $title]), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_BAD_REQUEST; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_INTERNAL_ERROR; + $responseContent = [ + 'success' => false, + 'message' => __('An error occurred on attempt to save image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php new file mode 100644 index 0000000000000..e965d94b33f0c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Image/Upload.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Image; + +use Exception; +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Json; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryUi\Model\UploadImage; +use Psr\Log\LoggerInterface; + +/** + * Controller responsible to upload the media gallery content + */ +class Upload extends Action implements HttpPostActionInterface +{ + private const HTTP_OK = 200; + private const HTTP_BAD_REQUEST = 400; + + /** + * @see _isAllowed() + */ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Context $context + * @param UploadImage $upload + * @param LoggerInterface $logger + */ + public function __construct( + Context $context, + UploadImage $upload, + LoggerInterface $logger + ) { + parent::__construct($context); + $this->uploadImage = $upload; + $this->logger = $logger; + } + + /** + * @inheritDoc + */ + public function execute() + { + /** @var Json $resultJson */ + $resultJson = $this->resultFactory->create(ResultFactory::TYPE_JSON); + $targetFolder = $this->getRequest()->getParam('target_folder'); + $type = $this->getRequest()->getParam('type'); + + if (!$targetFolder) { + $responseContent = [ + 'success' => false, + 'message' => __('The target_folder parameter is required.'), + ]; + $resultJson->setHttpResponseCode(self::HTTP_BAD_REQUEST); + $resultJson->setData($responseContent); + + return $resultJson; + } + + try { + $this->uploadImage->execute($targetFolder, $type); + + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => true, + 'message' => __('The image was uploaded successfully.'), + ]; + } catch (LocalizedException $exception) { + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => $exception->getMessage(), + ]; + } catch (Exception $exception) { + $this->logger->critical($exception); + $responseCode = self::HTTP_OK; + $responseContent = [ + 'success' => false, + 'message' => __('Could not upload image.'), + ]; + } + + $resultJson->setHttpResponseCode($responseCode); + $resultJson->setData($responseContent); + + return $resultJson; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php new file mode 100644 index 0000000000000..e97d93d86bb0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Index/Index.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Index; + +use Magento\Backend\App\Action; +use Magento\Backend\App\Action\Context; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\View\Result\Layout; +use Magento\Framework\View\Result\LayoutFactory; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * @var LayoutFactory + */ + private $layoutFactory; + + /** + * Index constructor. + * + * @param Context $context + * @param LayoutFactory $layoutFactory + */ + public function __construct( + Context $context, + LayoutFactory $layoutFactory + ) { + parent::__construct($context); + $this->layoutFactory = $layoutFactory; + } + + /** + * Get the media gallery layout + * + * @return Layout + */ + public function execute(): Layout + { + return $this->layoutFactory->create(); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php new file mode 100644 index 0000000000000..3660374243d16 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Controller/Adminhtml/Media/Index.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Controller\Adminhtml\Media; + +use Magento\Backend\App\Action; +use Magento\Backend\Model\View\Result\Page; +use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\Controller\ResultInterface; + +/** + * Controller serving the media gallery content + */ +class Index extends Action implements HttpGetActionInterface +{ + public const ADMIN_RESOURCE = 'Magento_Cms::media_gallery'; + + /** + * Get the media gallery layout + * + * @return ResultInterface + */ + public function execute(): ResultInterface + { + /** @var Page $resultPage */ + $resultPage = $this->resultFactory->create(ResultFactory::TYPE_PAGE); + $resultPage->setActiveMenu('Magento_MediaGalleryUi::media_gallery') + ->addBreadcrumb(__('Media'), __('Media Gallery')); + $resultPage->getConfig()->getTitle()->prepend(__('Manage Gallery')); + + return $resultPage; + } +} diff --git a/app/code/Magento/MediaGalleryUi/LICENSE.txt b/app/code/Magento/MediaGalleryUi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php new file mode 100644 index 0000000000000..7c3eccfea521f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/CreatedAt.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset created at date time + */ +class CreatedAt implements AssetDetailsProviderInterface +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @param TimezoneInterface $dateTime + */ + public function __construct( + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + } + + /** + * Provide asset created at date time + * + * @param AssetInterface $asset + * @return array + * @throws \Exception + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Created'), + 'value' => $this->formatDate($asset->getCreatedAt()) + ]; + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + * @throws \Exception + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php new file mode 100644 index 0000000000000..b2b0f389f6b9a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Height.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset height + */ +class Height implements AssetDetailsProviderInterface +{ + /** + * Provide asset height + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Height'), + 'value' => sprintf('%spx', $asset->getHeight()) + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php new file mode 100644 index 0000000000000..55841cc5abd3f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Size.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset file size + */ +class Size implements AssetDetailsProviderInterface +{ + /** + * Provide asset file size + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Size'), + 'value' => $this->formatImageSize($asset->getSize()) + ]; + } + + /** + * Format image size + * + * @param int $imageSize + * + * @return string + */ + private function formatImageSize(int $imageSize): string + { + if ($imageSize === 0) { + return ''; + } + + return sprintf('%sKb', $imageSize / 1000); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php new file mode 100644 index 0000000000000..5b47616398ef7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Type.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset type + */ +class Type implements AssetDetailsProviderInterface +{ + /** + * @var array + */ + private $types; + + /**= + * @param array $types + */ + public function __construct(array $types = []) + { + $this->types = $types; + } + + /** + * Provide asset type + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Type'), + 'value' => $this->getImageTypeByContentType($asset->getContentType()), + ]; + } + + /** + * Return image type by content type + * + * @param string $contentType + * @return string + */ + private function getImageTypeByContentType(string $contentType): string + { + $type = current(explode('/', $contentType)); + + return isset($this->types[$type]) ? $this->types[$type] : 'Asset'; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php new file mode 100644 index 0000000000000..2f50bd9a72208 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UpdatedAt.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Stdlib\DateTime\TimezoneInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset updated at date time + */ +class UpdatedAt implements AssetDetailsProviderInterface +{ + /** + * @var TimezoneInterface + */ + private $dateTime; + + /** + * @param TimezoneInterface $dateTime + */ + public function __construct( + TimezoneInterface $dateTime + ) { + $this->dateTime = $dateTime; + } + + /** + * Provide asset updated at date time + * + * @param AssetInterface $asset + * @return array + * @throws \Exception + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Modified'), + 'value' => $this->formatDate($asset->getUpdatedAt()) + ]; + } + + /** + * Format date to standard format + * + * @param string $date + * @return string + * @throws \Exception + */ + private function formatDate(string $date): string + { + return $this->dateTime->formatDate($date, \IntlDateFormatter::SHORT, true); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php new file mode 100644 index 0000000000000..ca3883d5c937c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/UsedIn.php @@ -0,0 +1,113 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide information on which content asset is used in + */ +class UsedIn implements AssetDetailsProviderInterface +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContent; + + /** + * @var array + */ + private $contentTypes; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @param GetContentByAssetIdsInterface $getContent + * @param UrlInterface $url + * @param array $contentTypes + */ + public function __construct( + GetContentByAssetIdsInterface $getContent, + UrlInterface $url, + array $contentTypes = [] + ) { + $this->getContent = $getContent; + $this->url = $url; + $this->contentTypes = $contentTypes; + } + + /** + * Provide information on which content asset is used in + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Used In'), + 'value' => $this->getUsedIn($asset->getId()) + ]; + } + + /** + * Retrieve assets used in the Content + * + * @param int $assetId + * @return array + * @throws IntegrationException + */ + private function getUsedIn(int $assetId): array + { + $details = []; + + foreach ($this->getUsedInCounts($assetId) as $type => $number) { + $details[$type] = $this->contentTypes[$type] ?? ['name' => $type, 'link' => null]; + $details[$type]['number'] = $number; + $details[$type]['link'] = $details[$type]['link'] ? $this->url->getUrl($details[$type]['link']) : null; + } + + return array_values($details); + } + + /** + * Get used in counts per type + * + * @param int $assetId + * @return int[] + * @throws IntegrationException + */ + private function getUsedInCounts(int $assetId): array + { + $usedIn = []; + $entityIds = []; + + $contentIdentities = $this->getContent->execute([$assetId]); + + foreach ($contentIdentities as $contentIdentity) { + $entityId = $contentIdentity->getEntityId(); + $type = $contentIdentity->getEntityType(); + + if (!isset($entityIds[$type])) { + $usedIn[$type] = 1; + } elseif ($entityIds[$type]['entity_id'] !== $entityId) { + ++$usedIn[$type]; + } + $entityIds[$type]['entity_id'] = $entityId; + } + return $usedIn; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php new file mode 100644 index 0000000000000..64e9cf8ad1a8f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProvider/Width.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\AssetDetailsProvider; + +use Magento\Framework\Exception\IntegrationException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryUi\Model\AssetDetailsProviderInterface; + +/** + * Provide asset width + */ +class Width implements AssetDetailsProviderInterface +{ + /** + * Provide asset width + * + * @param AssetInterface $asset + * @return array + * @throws IntegrationException + */ + public function execute(AssetInterface $asset): array + { + return [ + 'title' => __('Width'), + 'value' => sprintf('%spx', $asset->getWidth()) + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php new file mode 100644 index 0000000000000..92375adfdd4f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +interface AssetDetailsProviderInterface +{ + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array; +} diff --git a/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php new file mode 100644 index 0000000000000..207f35bb99d6a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/AssetDetailsProviderPool.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\MediaGalleryApi\Api\Data\AssetInterface; + +/** + * Provides asset detail for view details section + */ +class AssetDetailsProviderPool +{ + /** + * @var AssetDetailsProviderInterface[] + */ + private $detailsProviders; + + /** + * @param AssetDetailsProviderInterface[] $detailsProviders + */ + public function __construct(array $detailsProviders = []) + { + $this->detailsProviders = $detailsProviders; + } + + /** + * Get a piece of asset details + * + * @param AssetInterface $asset + * @return array + */ + public function execute(AssetInterface $asset): array + { + $details = []; + foreach ($this->detailsProviders as $detailsProvider) { + if ($detailsProvider instanceof AssetDetailsProviderInterface) { + $details[] = $detailsProvider->execute($asset); + } + } + return $details; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Config.php b/app/code/Magento/MediaGalleryUi/Model/Config.php new file mode 100644 index 0000000000000..a9391d76428ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Config.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaGalleryUiApi\Api\ConfigInterface; + +/** + * Class responsible to provide access to system configuration related to the Media Gallery + */ +class Config implements ConfigInterface +{ + /** + * Path to enable/disable media gallery in the system settings. + */ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Config constructor. + * + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct(ScopeConfigInterface $scopeConfig) + { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check if masonry grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag(self::XML_PATH_ENABLED); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php new file mode 100644 index 0000000000000..2f4793c28ad47 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/DeleteImage.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; + +/** + * Delete image from a storage + */ +class DeleteImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * DeleteImage constructor. + * + * @param Storage $imagesStorage + * @param Filesystem $filesystem + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem, + IsPathExcludedInterface $isPathExcluded + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Delete asset image physically from file storage and from data storage. + * + * @param AssetInterface[] $assets + * @throws LocalizedException + */ + public function execute(array $assets): void + { + $failedAssets = []; + foreach ($assets as $asset) { + if ($this->isPathExcluded->execute($asset->getPath())) { + $failedAssets[] = $asset->getPath(); + } + + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + $absolutePath = $mediaDirectory->getAbsolutePath($asset->getPath()); + $this->imagesStorage->deleteFile($absolutePath); + } + if (!empty($failedAssets)) { + throw new LocalizedException( + __( + 'Could not delete "%image": destination directory is restricted.', + ['image' => implode(",", $failedAssets)] + ) + ); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php new file mode 100644 index 0000000000000..f0998a3e120f2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Directories/GetFolderTree.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Directories; + +use Magento\Framework\Exception\ValidatorException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; + +/** + * Build folder tree structure by path + */ +class GetFolderTree +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $path; + + /** + * @var IsPathExcludedInterface + */ + private $isPathExcluded; + + /** + * Constructor + * + * @param Filesystem $filesystem + * @param string $path + * @param IsPathExcludedInterface $isPathExcluded + */ + public function __construct( + Filesystem $filesystem, + string $path, + IsPathExcludedInterface $isPathExcluded + ) { + $this->filesystem = $filesystem; + $this->path = $path; + $this->isPathExcluded = $isPathExcluded; + } + + /** + * Return directory folder structure in array + * + * @param bool $skipRoot + * @return array + * @throws ValidatorException + */ + public function execute(bool $skipRoot = true): array + { + return $this->buildFolderTree($this->getDirectories(), $skipRoot); + } + + /** + * Build directory tree array in format for jstree strandart + * + * @return array + * @throws ValidatorException + */ + private function getDirectories(): array + { + $directories = []; + + /** @var Read $directory */ + $directory = $this->filesystem->getDirectoryRead($this->path); + + if (!$directory->isDirectory()) { + return $directories; + } + + foreach ($directory->readRecursively() as $path) { + if (!$directory->isDirectory($path) || $this->isPathExcluded->execute($path)) { + continue; + } + + $pathArray = explode('/', $path); + $directories[] = [ + 'data' => count($pathArray) > 0 ? end($pathArray) : $path, + 'attr' => ['id' => $path], + 'metadata' => [ + 'path' => $path + ], + 'path_array' => $pathArray + ]; + } + return $directories; + } + + /** + * Build folder tree structure by provided directories path + * + * @param array $directories + * @param bool $skipRoot + * @return array + */ + private function buildFolderTree(array $directories, bool $skipRoot): array + { + $tree = [ + 'name' => 'root', + 'path' => '/', + 'children' => [] + ]; + foreach ($directories as $idx => &$node) { + $node['children'] = []; + $result = $this->findParent($node, $tree); + $parent = & $result['treeNode']; + + $parent['children'][] =& $directories[$idx]; + } + return $skipRoot ? $tree['children'] : $tree; + } + + /** + * Find parent directory + * + * @param array $node + * @param array $treeNode + * @param int $level + * @return array + */ + private function findParent(array &$node, array &$treeNode, int $level = 0): array + { + $nodePathLength = count($node['path_array']); + $treeNodeParentLevel = $nodePathLength - 1; + + $result = ['treeNode' => &$treeNode]; + + if ($nodePathLength <= 1 || $level > $treeNodeParentLevel) { + return $result; + } + + foreach ($treeNode['children'] as &$tnode) { + if ($node['path_array'][$level] === $tnode['path_array'][$level]) { + return $this->findParent($node, $tnode, $level + 1); + } + } + return $result; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php new file mode 100644 index 0000000000000..b870082ea2aa1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/GetDetailsByAssetId.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Exception; +use Magento\Backend\Model\UrlInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; + +/** + * Load Media Asset from database by id add all related data to it + */ +class GetDetailsByAssetId +{ + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsById; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var SourceIconProvider + */ + private $sourceIconProvider; + + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @var AssetDetailsProviderPool + */ + private $detailsProviderPool; + + /** + * @param AssetDetailsProviderPool $detailsProviderPool + * @param GetAssetsByIdsInterface $getAssetById + * @param StoreManagerInterface $storeManager + * @param SourceIconProvider $sourceIconProvider + * @param GetAssetsKeywordsInterface $getAssetKeywords + */ + public function __construct( + AssetDetailsProviderPool $detailsProviderPool, + GetAssetsByIdsInterface $getAssetById, + StoreManagerInterface $storeManager, + SourceIconProvider $sourceIconProvider, + GetAssetsKeywordsInterface $getAssetKeywords + ) { + $this->detailsProviderPool = $detailsProviderPool; + $this->getAssetsById = $getAssetById; + $this->storeManager = $storeManager; + $this->sourceIconProvider = $sourceIconProvider; + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * Get image details by assets Ids + * + * @param array $assetIds + * @throws LocalizedException + * @throws Exception + * @return array + */ + public function execute(array $assetIds): array + { + $assets = $this->getAssetsById->execute($assetIds); + + $details = []; + foreach ($assets as $asset) { + $details[$asset->getId()] = [ + 'image_url' => $this->getUrl($asset->getPath()), + 'title' => $asset->getTitle(), + 'path' => $asset->getPath(), + 'description' => $asset->getDescription(), + 'id' => $asset->getId(), + 'details' => $this->detailsProviderPool->execute($asset), + 'size' => $asset->getSize(), + 'tags' => $this->getKeywords($asset), + 'source' => $asset->getSource() ? + $this->sourceIconProvider->getSourceIconUrl($asset->getSource()) : + null, + 'content_type' => strtoupper(str_replace('image/', '', $asset->getContentType())), + ]; + } + return $details; + } + + /** + * Key asset keywords + * + * @param AssetInterface $asset + * @return string[] + */ + private function getKeywords(AssetInterface $asset): array + { + $assetKeywords = $this->getAssetKeywords->execute([$asset->getId()]); + + if (empty($assetKeywords)) { + return []; + } + + $keywords = current($assetKeywords)->getKeywords(); + + return array_map( + function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, + $keywords + ); + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * + * @return string + * + * @throws LocalizedException + */ + private function getUrl(string $path): string + { + /** @var Store $store */ + $store = $this->storeManager->getStore(); + + return $store->getBaseUrl(UrlInterface::URL_TYPE_MEDIA) . $path; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php new file mode 100644 index 0000000000000..88401465d56b7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/Listing/DataProvider.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\Listing; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\ReportingInterface; +use Magento\Framework\Api\Search\SearchCriteriaBuilder; +use Magento\Framework\Api\Search\SearchResultInterface; +use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory; +use Magento\Framework\View\Element\UiComponent\DataProvider\DataProvider as UiComponentDataProvider; +use Magento\MediaGalleryUi\Ui\Component\Listing\Provider; + +/** + * Media gallery UI data provider. Try catch added for displaying errors in grid + */ +class DataProvider extends UiComponentDataProvider +{ + /** + * @var CollectionProcessorInterface + */ + private $collectionProcessor; + + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param string $name + * @param string $primaryFieldName + * @param string $requestFieldName + * @param ReportingInterface $reporting + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param RequestInterface $request + * @param FilterBuilder $filterBuilder + * @param CollectionProcessorInterface $collectionProcessor + * @param CollectionFactory $collectionFactory + * @param array $meta + * @param array $data + * @SuppressWarnings(PHPMD.ExcessiveParameterList) + */ + public function __construct( + $name, + $primaryFieldName, + $requestFieldName, + ReportingInterface $reporting, + SearchCriteriaBuilder $searchCriteriaBuilder, + RequestInterface $request, + FilterBuilder $filterBuilder, + CollectionProcessorInterface $collectionProcessor, + CollectionFactory $collectionFactory, + array $meta = [], + array $data = [] + ) { + parent::__construct( + $name, + $primaryFieldName, + $requestFieldName, + $reporting, + $searchCriteriaBuilder, + $request, + $filterBuilder, + $meta, + $data + ); + $this->collectionFactory = $collectionFactory; + $this->collectionProcessor = $collectionProcessor; + } + + /** + * @inheritdoc + */ + public function getData(): array + { + try { + return $this->searchResultToOutput($this->getSearchResult()); + } catch (\Exception $exception) { + return [ + 'items' => [], + 'totalRecords' => 0, + 'errorMessage' => $exception->getMessage() + ]; + } + } + + /** + * @inheritDoc + */ + public function getSearchResult(): SearchResultInterface + { + /** @var Provider $collection */ + $collection = $this->collectionFactory->getReport($this->getSearchCriteria()->getRequestName()); + $this->collectionProcessor->process($this->getSearchCriteria(), $collection); + + return $collection; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php new file mode 100644 index 0000000000000..785c3078cdbe5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/ContentField.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; + +/** + * Class responsible to filter a content field + */ +class ContentField implements CustomFilterInterface +{ + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentStatus; + + /** + * ContentField constructor. + * + * @param GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + */ + public function __construct( + GetAssetIdsByContentFieldInterface $getAssetIdsByContentStatus + ) { + $this->getAssetIdsByContentStatus = $getAssetIdsByContentStatus; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $collection->addFieldToFilter( + 'main_table.id', + ['in' => $this->getAssetIdsByContentStatus->execute($filter->getField(), $filter->getValue())] + ); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php new file mode 100644 index 0000000000000..36e9375525f8d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Directory.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\Data\Collection\AbstractDb; + +class Directory implements CustomFilterInterface +{ + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = str_replace('%', '', $filter->getValue()); + $collection->getSelect()->where('path REGEXP ? ', '^' . $value . '/[^\/]*$'); + + return true; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php new file mode 100644 index 0000000000000..d43b3ac2ca451 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Duplicated.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\DB\Select; + +/** + * Custom filter to filter collection by duplicated hash values + */ +class Duplicated implements CustomFilterInterface +{ + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + if ($filter->getValue()) { + $collection->getSelect()->where('main_table.hash IN (?)', $this->getDuplicatedIds()); + } + return true; + } + /** + * Return sql part of duplicated values. + */ + private function getDuplicatedIds(): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select() + ->from($this->connection->getTableName('media_gallery_asset'), ['hash']) + ->group('hash') + ->having('COUNT(*) > 1') + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php new file mode 100644 index 0000000000000..6027a7daf7442 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Entity.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +/** + * Custom filter to filter collection by entity type + */ +class Entity implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @var string + */ + private $entityType; + + /** + * @param ResourceConnection $resource + * @param string $entityType + */ + public function __construct(ResourceConnection $resource, string $entityType) + { + $this->connection = $resource; + $this->entityType = $entityType; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $ids = $filter->getValue(); + if (is_array($ids)) { + $collection->addFieldToFilter( + [self::TABLE_ALIAS . '.id'], + [ + ['in' => $this->getSelectByEntityIds($ids)] + ] + ); + } + return true; + } + + /** + * Return asset ids by entity type + * + * @param array $ids + * @return array + */ + private function getSelectByEntityIds(array $ids): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type = ?', + $this->entityType + )->where( + 'entity_id IN (?)', + $ids + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php new file mode 100644 index 0000000000000..1b5e2282ff3dc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/EntityType.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +/** + * Custom filter to filter collection by entity type + */ +class EntityType implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_MEDIA_CONTENT_ASSET = 'media_content_asset'; + private const TABLE_MEDIA_GALLERY_ASSET = 'media_gallery_asset'; + private const NOT_USED = 'not_used'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + if (is_array($value)) { + $conditions = []; + + if (in_array(self::NOT_USED, $value)) { + unset($value[array_search(self::NOT_USED, $value)]); + $conditions[] = ['in' => $this->getNotUsedEntityIds()]; + } + + if (!empty($value)) { + $conditions[] = ['in' => $this->getEntityTypesIds($value)]; + } + + $collection->addFieldToFilter( + self::TABLE_ALIAS . '.id', + $conditions + ); + } + return true; + } + + /** + * Return asset ids by entity type + * + * @param array $value + * @return array + */ + private function getEntityTypesIds(array $value): array + { + $connection = $this->connection->getConnection(); + return $connection->fetchAssoc( + $connection->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + )->where( + 'entity_type IN (?)', + $value + ) + ); + } + + /** + * Return asset ids that not exists in asset_content_table + */ + private function getNotUsedEntityIds(): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + ['media_gallery_asset' => $this->connection->getTableName(self::TABLE_MEDIA_GALLERY_ASSET)], + ['id'] + )->where( + 'media_gallery_asset.id not in ?', + $this->connection->getConnection()->select()->from( + ['asset_content_table' => $this->connection->getTableName(self::TABLE_MEDIA_CONTENT_ASSET)], + ['asset_id'] + ) + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php new file mode 100644 index 0000000000000..1c8baa58d90ea --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/SearchCriteria/CollectionProcessor/FilterProcessor/Keyword.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor; + +use Magento\Framework\Api\Filter; +use Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor\CustomFilterInterface; +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Data\Collection\AbstractDb; + +class Keyword implements CustomFilterInterface +{ + private const TABLE_ALIAS = 'main_table'; + private const TABLE_KEYWORDS = 'media_gallery_asset_keyword'; + private const TABLE_ASSET_KEYWORD = 'media_gallery_keyword'; + + /** + * @var ResourceConnection + */ + private $connection; + + /** + * @param ResourceConnection $resource + */ + public function __construct(ResourceConnection $resource) + { + $this->connection = $resource; + } + + /** + * @inheritDoc + */ + public function apply(Filter $filter, AbstractDb $collection): bool + { + $value = $filter->getValue(); + + $collection->addFieldToFilter( + [self::TABLE_ALIAS . '.title', self::TABLE_ALIAS . '.id'], + [ + ['like' => sprintf('%%%s%%', $value)], + ['in' => $this->getAssetIdsByKeyword($value)] + ] + ); + + return true; + } + + /** + * Return asset ids by keyword + * + * @param string $value + * @return array + */ + private function getAssetIdsByKeyword(string $value): array + { + $connection = $this->connection->getConnection(); + + return $connection->fetchAssoc( + $connection->select()->from( + $connection->select() + ->from( + ['asset_keywords_table' => $this->connection->getTableName(self::TABLE_ASSET_KEYWORD)], + ['id'] + )->where( + 'keyword = ?', + $value + )->joinInner( + ['keywords_table' => $this->connection->getTableName(self::TABLE_KEYWORDS)], + 'keywords_table.keyword_id = asset_keywords_table.id', + ['asset_id'] + ), + ['asset_id'] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php new file mode 100644 index 0000000000000..ff82b990d2a01 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Framework\Exception\LocalizedException; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetInterfaceFactory; +use Magento\MediaGalleryApi\Api\GetAssetsByIdsInterface; +use Magento\MediaGalleryApi\Api\SaveAssetsInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Magento\MediaGalleryUi\Model\UpdateAsset\UpdateKeywords; +use Magento\MediaGalleryUi\Model\UpdateAsset\SaveMetadataToFile; + +class UpdateAsset +{ + /** + * @var AssetInterfaceFactory + */ + private $assetFactory; + + /** + * @var GetAssetsByIdsInterface + */ + private $getAssetsByIds; + + /** + * @var SaveAssetsInterface + */ + private $saveAssets; + + /** + * @var SaveMetadataToFile + */ + private $processMetadata; + + /** + * @var UpdateKeywords + */ + private $processKeywords; + + /** + * @param AssetInterfaceFactory $assetFactory + * @param GetAssetsByIdsInterface $getAssetsByIds + * @param SaveAssetsInterface $saveAssets + * @param UpdateKeywords $processKeywords + * @param SaveMetadataToFile $processMetadata + */ + public function __construct( + AssetInterfaceFactory $assetFactory, + GetAssetsByIdsInterface $getAssetsByIds, + SaveAssetsInterface $saveAssets, + UpdateKeywords $processKeywords, + SaveMetadataToFile $processMetadata + ) { + $this->assetFactory = $assetFactory; + $this->getAssetsByIds = $getAssetsByIds; + $this->saveAssets = $saveAssets; + $this->processKeywords = $processKeywords; + $this->processMetadata = $processMetadata; + } + + /** + * Save asset details + * + * @param int $id + * @param MetadataInterface $data + */ + public function execute(int $id, MetadataInterface $data): void + { + $asset = $this->getAsset($id); + + $updatedAsset = $this->assetFactory->create( + [ + 'path' => $asset->getPath(), + 'contentType' => $asset->getContentType(), + 'width' => $asset->getWidth(), + 'height' => $asset->getHeight(), + 'size' => $asset->getSize(), + 'id' => $asset->getId(), + 'title' => $data->getTitle() ?? $asset->getTitle(), + 'description' => $data->getDescription() ?? $asset->getDescription(), + 'source' => $asset->getSource(), + 'hash' => $asset->getHash(), + 'created_at' => $asset->getCreatedAt(), + 'updated_at' => $asset->getUpdatedAt() + ] + ); + + $this->saveAssets->execute([$updatedAsset]); + $this->processMetadata->execute($asset->getPath(), $data); + + $keywords = $data->getKeywords(); + if (isset($keywords)) { + $this->processKeywords->execute($id, $keywords); + } + } + + /** + * Load asset by id + * + * @param int $id + * @return AssetInterface + * @throws LocalizedException + */ + private function getAsset(int $id): AssetInterface + { + $assets = $this->getAssetsByIds->execute([$id]); + if (empty($assets)) { + throw new LocalizedException(__('Could not retrieve the asset.')); + } + return current($assets); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php new file mode 100644 index 0000000000000..3ebe04374f81e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/SaveMetadataToFile.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\MediaGalleryMetadataApi\Api\AddMetadataInterface; +use Magento\MediaGalleryMetadataApi\Api\Data\MetadataInterface; +use Psr\Log\LoggerInterface; + +class SaveMetadataToFile +{ + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var AddMetadataInterface + */ + private $addMetadata; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @param Filesystem $filesystem + * @param AddMetadataInterface $addMetadata + * @param LoggerInterface $logger + */ + public function __construct( + Filesystem $filesystem, + AddMetadataInterface $addMetadata, + LoggerInterface $logger + ) { + $this->filesystem = $filesystem; + $this->addMetadata = $addMetadata; + $this->logger = $logger; + } + + /** + * Save updated metadata + * + * @param string $path + * @param MetadataInterface $data + */ + public function execute(string $path, MetadataInterface $data): void + { + $absolutePath = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path); + + try { + $this->addMetadata->execute($absolutePath, $data); + } catch (LocalizedException $e) { + $this->logger->critical($e); + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php new file mode 100644 index 0000000000000..2a359d5a14025 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UpdateAsset/UpdateKeywords.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model\UpdateAsset; + +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; +use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; + +class UpdateKeywords +{ + /** + * @var AssetKeywordsInterfaceFactory + */ + private $assetKeywordsFactory; + + /** + * @var KeywordInterfaceFactory + */ + private $keywordFactory; + + /** + * @var SaveAssetsKeywordsInterface + */ + private $saveAssetKeywords; + + /** + * @param AssetKeywordsInterfaceFactory $assetKeywordsFactory + * @param KeywordInterfaceFactory $keywordFactory + * @param SaveAssetsKeywordsInterface $saveAssetKeywords + */ + public function __construct( + AssetKeywordsInterfaceFactory $assetKeywordsFactory, + KeywordInterfaceFactory $keywordFactory, + SaveAssetsKeywordsInterface $saveAssetKeywords + ) { + $this->assetKeywordsFactory = $assetKeywordsFactory; + $this->keywordFactory = $keywordFactory; + $this->saveAssetKeywords = $saveAssetKeywords; + } + + /** + * Save asset keywords + * + * @param int $assetId + * @param string[] $keywords + */ + public function execute(int $assetId, array $keywords): void + { + $this->saveAssetKeywords->execute([ + $this->assetKeywordsFactory->create([ + 'assetId' => $assetId, + 'keywords' => $this->createKeywords($keywords) + ]) + ]); + } + + /** + * Create keyword objects from strings + * + * @param string[] $keywords + * @return KeywordInterface[] + */ + private function createKeywords(array $keywords): array + { + $keywordObjects = []; + foreach ($keywords as $keyword) { + $keywordObjects[] = $this->keywordFactory->create( + [ + 'keyword' => $keyword + ] + ); + } + return $keywordObjects; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Model/UploadImage.php b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php new file mode 100644 index 0000000000000..c918548bea553 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Model/UploadImage.php @@ -0,0 +1,58 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; + +/** + * Uploads an image to storage + */ +class UploadImage +{ + /** + * @var Storage + */ + private $imagesStorage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Storage $imagesStorage + * @param Filesystem $filesystem + */ + public function __construct( + Storage $imagesStorage, + Filesystem $filesystem + ) { + $this->imagesStorage = $imagesStorage; + $this->filesystem = $filesystem; + } + + /** + * Uploads the image and returns file object + * + * @param string $targetFolder + * @param string $type + * @throws LocalizedException + */ + public function execute(string $targetFolder, string $type): void + { + $mediaDirectory = $this->filesystem->getDirectoryRead(DirectoryList::MEDIA); + if (!$mediaDirectory->isDirectory($targetFolder)) { + throw new LocalizedException(__('Directory %1 does not exist in media directory.', $targetFolder)); + } + + $this->imagesStorage->uploadFile($mediaDirectory->getAbsolutePath($targetFolder), $type); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php new file mode 100644 index 0000000000000..7988ac2d9e635 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Plugin/CreateThumbnails.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Plugin; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite; + +/** + * Create resizes files that were synced + */ +class CreateThumbnails +{ + /** + * @var Storage + */ + private $storage; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @param Filesystem $filesystem + * @param Storage $storage + */ + public function __construct(Filesystem $filesystem, Storage $storage) + { + $this->storage = $storage; + $this->filesystem = $filesystem; + } + + /** + * Create thumbnails for synced files. + * + * @param ImportFilesComposite $subject + * @param string[] $paths + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeExecute(ImportFilesComposite $subject, array $paths): array + { + foreach ($paths as $path) { + $this->storage->resizeFile( + $this->filesystem->getDirectoryRead(DirectoryList::MEDIA)->getAbsolutePath($path) + ); + } + + return [$paths]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/README.md b/app/code/Magento/MediaGalleryUi/README.md new file mode 100644 index 0000000000000..6fbad656b23a8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUi module + +The Magento_MediaGalleryUi module is responsible for the media gallery user interface (UI) implementation. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryUi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..c056727aa8fe8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminAssertImageInStandaloneMediaGalleryActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAssertImageInStandaloneMediaGalleryActionGroup"> + <annotations> + <description>Validates that the provided image is present and correct in the standalone media gallery.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + + <seeElement selector="{{AdminEnhancedMediaGalleryActionsSection.imageSrc(imageName)}}" + stepKey="checkFirstImageAfterSearch"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..d47eb491f9b5d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup"> + <annotations> + <description>Adds image to target element from View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.addImage}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml new file mode 100644 index 0000000000000..9a550805a7dec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup"> + <annotations> + <description>Applies duplicated images filter to the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.duplicatedFilterCheckbox}}" stepKey="clickShowDuplicates"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..9d7d725cf49de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryApplyFiltersActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.applyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml new file mode 100644 index 0000000000000..aeee921f92e58 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup"> + <annotations> + <description>Assert media gallery grid filters</description> + </annotations> + <arguments> + <argument name="resultValue" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFiltersToCheckAppliedFilter"/> + <see selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilter(resultValue)}}" userInput="{{resultValue}}" stepKey="verifyAppliedFilter"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml new file mode 100644 index 0000000000000..7f4db971702ca --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup"> + <annotations> + <description>Asserts images has been deleted in mass action.</description> + </annotations> + + <see userInput='Assets have been successfully deleted' stepKey="verifyDeleteImages"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml new file mode 100644 index 0000000000000..efcf40cd2b644 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup"> + <annotations> + <description>Asserts that massaction mode can be enabled and disabled, verify massaction view after switch to massaction mode</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + <see selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" userInput="(1 Selected)" stepKey="verifySelectedCount"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml new file mode 100644 index 0000000000000..a691f65387e8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup"> + <annotations> + <description>Asserts that massaction mode is terminated</description> + </annotations> + + + <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMassAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml new file mode 100644 index 0000000000000..783e71719c659 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup"> + <annotations> + <description>Assert that grid have no active filter</description> + </annotations> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryFiltersSection.activeFilterPlaceholder}}" stepKey="assertThereIsNoActiveFilters"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml new file mode 100644 index 0000000000000..b53e76e06cfb5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryAssertWarningMessageActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup"> + <annotations> + <description>Assert image delete action popup contains warnin message</description> + </annotations> + <arguments> + <argument name="messageText" type="string"/> + </arguments> + + <see userInput="{{messageText}}" stepKey="assertWarningMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml new file mode 100644 index 0000000000000..478ca2b3b5be9 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup"> + <annotations> + <description>Apply filters in Category grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridApplyFilters}}" stepKey="applyFilters"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..00608504fd7a6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery category filters by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.categoryGridFiltersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml new file mode 100644 index 0000000000000..600e1cd747943 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup"> + <annotations> + <description>Click delete images button.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteSelected}}" stepKey="clickDeleteImages"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickEntityUsedInActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickEntityUsedInActionGroup.xml new file mode 100644 index 0000000000000..ec54d83bd808a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryClickEntityUsedInActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryClickEntityUsedInActionGroup"> + <annotations> + <description>Clicks one Used In section entity</description> + </annotations> + <arguments> + <argument name="entityName" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.usedInLink(entityName)}}" stepKey="openContextMenu"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml new file mode 100644 index 0000000000000..3754eb319da44 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryCloseViewDetailsActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup"> + <annotations> + <description>Closes View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.cancel}}" stepKey="clickCancel"/> + <wait time="1" stepKey="waitForElementRender"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml new file mode 100644 index 0000000000000..90546eca8dc0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup"> + <annotations> + <description>Click confirm on confirmation popup images delete action.</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeletingProcces"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml new file mode 100644 index 0000000000000..95f3080049db7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDeleteGridViewActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryDeleteGridViewActionGroup"> + <annotations> + <description>Delete grid view bookmarks by name</description> + </annotations> + <arguments> + <argument name="viewToDelete" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(viewToDelete)}}{{AdminEnhancedMediaGalleryActionsSection.editViewButtonPartial}}" stepKey="clickEditButton"/> + <seeElement selector="{{AdminEnhancedMediaGalleryActionsSection.deleteViewButton}}" stepKey="seeDeleteButton"/> + <click selector="{{AdminEnhancedMediaGalleryActionsSection.deleteViewButton}}" stepKey="clickDeleteButton"/> + <waitForPageLoad stepKey="waitForDeletion" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml new file mode 100644 index 0000000000000..f404ffbe7c4f0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryDisableMassactionModeActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup"> + <annotations> + <description>Disable massaction mode by clicking on cancel button</description> + </annotations> + + + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.cancelMassActionMode}}" stepKey="cancelMassAction"/> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryMassActionSection.totalSelected}}" stepKey="verifyTeminateMAssAction"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..84712e8e3f3ae --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEditImageDetailsActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryEditImageDetailsActionGroup"> + <annotations> + <description>Opens Edit image details panel panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.edit}}" stepKey="edit"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml new file mode 100644 index 0000000000000..5e5c89637c6a1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryEnableMassActionModeActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup"> + <annotations> + <description>Activate massaction mode by click on Delete Selected..</description> + </annotations> + + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteImages}}" stepKey="waitForMassActionButton"/> + <click selector="{{AdminEnhancedMediaGalleryMassActionSection.deleteImages}}" stepKey="clickOnMassActionButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml new file mode 100644 index 0000000000000..d2ac1c78b2582 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryExpandFilterActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryExpandFilterActionGroup"> + <annotations> + <description>Expand media gallery filter by clicking on button</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.filtersButton}}" stepKey="expandFilter"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..b3733ceb4c4a0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDeleteActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Media Gallery</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.delete}}" stepKey="deleteImage"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml new file mode 100644 index 0000000000000..001aa010dbdd4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup"> + <annotations> + <description>Delete image from the View Details panel</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.delete}}" stepKey="deleteImage"/> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="waitForConfirmation"/> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml new file mode 100644 index 0000000000000..931da0ee06fef --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsEditActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDetailsEditActionGroup"> + <annotations> + <description>Edit image from the View Details panel</description> + </annotations> + <click selector="{{AdminEnhancedMediaGalleryViewDetailsSection.edit}}" stepKey="editImage"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml new file mode 100644 index 0000000000000..0da3de9501c13 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryImageDetailsSaveActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup"> + <annotations> + <description>Save image details from the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.title}}" userInput="{{image.title}}" stepKey="setTitle" /> + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.description}}" userInput="{{image.description}}" stepKey="setDescription" /> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.save}}" stepKey="saveDetails"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml new file mode 100644 index 0000000000000..57096124c0370 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySaveCustomViewActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySaveCustomViewActionGroup"> + <annotations> + <description>Save custom view media gallery</description> + </annotations> + <arguments> + <argument name="viewName" type="string" defaultValue="Test View"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.saveViewAs}}" stepKey="saveView"/> + <fillField selector="{{AdminGridDefaultViewControls.viewName}}" userInput="{{viewName}}" stepKey="inputViewName"/> + <pressKey selector="{{AdminGridDefaultViewControls.viewName}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::ENTER]" stepKey="pressEnterKey"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml new file mode 100644 index 0000000000000..4244724599fed --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup"> + <annotations> + <description>Apply custom bookmarks view to the media gallery grid</description> + </annotations> + <arguments> + <argument name="selectView" type="string"/> + </arguments> + + <click selector="{{AdminDataGridHeaderSection.bookmarkToggle}}" stepKey="openViewBookmarks"/> + <click selector="{{AdminGridDefaultViewControls.viewByName(selectView)}}" stepKey="clickOnViewButton"/> + <waitForPageLoad stepKey="waitForGridLoad" time="10"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml new file mode 100644 index 0000000000000..6532fb869d2cc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectImageForMassActionActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup"> + <annotations> + <description>Select images in grid by clicking on mass action checkbox</description> + </annotations> + <arguments> + <argument name="imageName" type="string" defaultValue="magento"/> + </arguments> + + <checkOption selector="{{AdminEnhancedMediaGalleryMassActionSection.massActionCheckbox(imageName)}}" stepKey="selectImageInGridToDelte"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml new file mode 100644 index 0000000000000..9be288b064742 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectSourceFilterActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectSourceFilterActionGroup"> + <annotations> + <description>Select source filter by provided option</description> + </annotations> + <arguments> + <argument type="string" name="filterValue"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.sourceFilterValue(filterValue)}}" stepKey="openContextMenu"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..72d01e1871513 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGallerySelectUsedInFilterActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup"> + <annotations> + <description>Set search options filter</description> + </annotations> + <arguments> + <argument type="string" name="filterName"/> + <argument type="string" name="optionName"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilter(filterName)}}" stepKey="openFilter"/> + <fillField selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterInput(filterName)}}" userInput="{{optionName}}" stepKey="enterOptionName" /> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterOption(filterName, optionName)}}" stepKey="selectOption"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone(filterName)}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml new file mode 100644 index 0000000000000..053a1185b3fda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryUploadImageActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryUploadImageActionGroup"> + <annotations> + <description>Uploads the provided Image to Media Gallery. + If you use this action group, you MUST add steps to delete the image in the "after" steps.</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <attachFile selector="{{AdminEnhancedMediaGalleryActionsSection.upload}}" userInput="{{image.value}}" stepKey="uploadImage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml new file mode 100644 index 0000000000000..eb2fc79567d08 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup"> + <annotations> + <description>Verifies image description on the View Details panel</description> + </annotations> + <arguments> + <argument name="description"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.description}}" stepKey="grabDescription"/> + <assertStringContainsString stepKey="verifyDescription"> + <actualResult type="variable">grabDescription</actualResult> + <expectedResult type="string">{{description}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..1ebaa0581e33e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup"> + <annotations> + <description>Verifies image information on the View Details panel</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.type}}" stepKey="grabType"/> + <assertStringContainsString stepKey="verifyType"> + <actualResult type="variable">grabType</actualResult> + <expectedResult type="string">Image</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml new file mode 100644 index 0000000000000..4c38b7dbc8c3e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup"> + <annotations> + <description>Verifies image filename on the View Details panel</description> + </annotations> + <arguments> + <argument name="filename" type="string"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.filename}}" stepKey="grabFilename"/> + <assertStringContainsString stepKey="verifyFilename"> + <actualResult type="variable">grabFilename</actualResult> + <expectedResult type="string">{{filename}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml new file mode 100644 index 0000000000000..2fc4f7ea25fd0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup"> + <annotations> + <description>Verifies image keywords on the View Details panel</description> + </annotations> + <arguments> + <argument name="keywords"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.keywords}}" stepKey="grabKeywords"/> + <assertStringContainsString stepKey="verifyKeywords"> + <actualResult type="variable">grabKeywords</actualResult> + <expectedResult type="string">{{keywords}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml new file mode 100644 index 0000000000000..08dac976332ee --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryVerifyImageTitleActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup"> + <annotations> + <description>Verifies image title on the View Details panel</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryViewDetailsSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{title}}</expectedResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml new file mode 100644 index 0000000000000..b5c0bbac69bec --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminEnhancedMediaGalleryViewImageDetailsActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminEnhancedMediaGalleryViewImageDetails"> + <annotations> + <description>Opens View Details panel for the first image in the media gallery grid</description> + </annotations> + + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.openContextMenu}}" stepKey="openContextMenu"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.viewDetails}}" stepKey="viewDetails"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml new file mode 100644 index 0000000000000..6ddb6311c1a7e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplySelectFilterActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryApplySelectFilterActionGroup"> + <annotations> + <description>Applies select filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="filterLabel" type="string"/> + <argument name="optionLabel" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilter(filterLabel)}}" stepKey="openSelectFilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.selectFilterOption(filterLabel, optionLabel)}}" stepKey="selectFilterOption"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml new file mode 100644 index 0000000000000..a930f65b71040 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryApplyUsedInFilterActionGroup.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryApplyUsedInFilterActionGroup"> + <annotations> + <description>Applies Show Images Used In filter to the media gallery grid</description> + </annotations> + <arguments> + <argument name="entityType" type="string"/> + </arguments> + + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInSelectDropdown}}" stepKey="openUsedInfilter"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.usedInEntityType(entityType)}}" stepKey="selectEntityType"/> + <click selector="{{AdminEnhancedMediaGalleryFiltersSection.searchOptionsFilterDone('Show Images Used In')}}" stepKey="clickDone"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml new file mode 100644 index 0000000000000..42d723f0811d3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup"> + <annotations> + <description>Asserts category name in category grid page</description> + </annotations> + <arguments> + <argument name="categoryName" type="string"/> + </arguments> + + <seeElement selector="{{AdminMediaGalleryCatalogUiCategoryGridSection.name('1', categoryName)}}" stepKey="assertNameColumn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml new file mode 100644 index 0000000000000..d0d9817da6d34 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderDoesNotExistActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertFolderDoesNotExistActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="5" stepKey="waitForFolderTreeReloads"/> + <dontSeeElement selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="folderDoesNotExist"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml new file mode 100644 index 0000000000000..7d71c764bc8de --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertFolderNameActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertFolderNameActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <waitForElementVisible selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="waitForFolder"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml new file mode 100644 index 0000000000000..6785558c8ef54 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageInGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertImageInGridActionGroup"> + <annotations> + <description>Asserts that image exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml new file mode 100644 index 0000000000000..cc4de51357de0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup"> + <annotations> + <description>Asserts that image does not exists in media gallery grid</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <dontSeeElement selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(title)}}" stepKey="waitForImageToBeVisible"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.xml new file mode 100644 index 0000000000000..28dcc1c553a5a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickAddSelectedActionGroup.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="AdminMediaGalleryClickAddSelectedActionGroup"> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="waitForAddSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.addSelected}}" stepKey="ClickAddSelected"/> + <wait time="5" stepKey="waitForImageToBeAdded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml new file mode 100644 index 0000000000000..ee2ff887488a4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickImageInGridActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryClickImageInGridActionGroup"> + <annotations> + <description>Select image on enhanced media gallery</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="waitForImageToBeVisible"/> + <click selector="{{AdminEnhancedMediaGalleryImageActionsSection.imageInGrid(imageName)}}" stepKey="clickOnImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml new file mode 100644 index 0000000000000..3e555c25e0a98 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryClickOkButtonTinyMce4ActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup"> + <annotations> + <description>Click ok button on upload image tinyMce4 popup.</description> + </annotations> + + <waitForElementVisible selector="{{MediaGallerySection.OkBtn}}" stepKey="waitForOkBtn"/> + <click selector="{{MediaGallerySection.OkBtn}}" stepKey="clickOkBtn"/> + <waitForPageLoad stepKey="wait"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml new file mode 100644 index 0000000000000..f3ccc8ef7be04 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryCreateNewFolderActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryCreateNewFolderActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <fillField selector="{{AdminMediaGalleryFolderSection.folderNameField}}" userInput="{{name}}" stepKey="setFolderName" /> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmCreateButton}}" stepKey="clickCreateButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml new file mode 100644 index 0000000000000..964b33dd38d55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEditAssetAddKeywordActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryEditAssetAddKeywordActionGroup"> + <annotations> + <description>Set Keywords on the Edit Details panel</description> + </annotations> + <arguments> + <argument name="keyword"/> + </arguments> + + <fillField selector="{{AdminEnhancedMediaGalleryEditDetailsSection.newKeyword}}" userInput="{{keyword}}" stepKey="enterKeyword"/> + <click selector="{{AdminEnhancedMediaGalleryEditDetailsSection.addNewKeyword}}" stepKey="addKeyword"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml new file mode 100644 index 0000000000000..d842535940253 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryEnhancedEnableActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryEnhancedEnableActionGroup"> + <arguments> + <argument name="enabled" type="string" defaultValue="{{MediaGalleryConfigDataDisabled.value}}"/> + </arguments> + <amOnPage url="{{AdminMediaGalleryConfigSystemPage.url}}" stepKey="navigateToSystemConfigurationPage" /> + <waitForPageLoad stepKey="waitForPageLoad"/> + <scrollTo selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="scrollToEnhancedMediaGalleryFieldset"/> + <conditionalClick stepKey="expandEnhancedMediaGalleryTab" selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" dependentSelector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" visible="false" /> + <waitForElementVisible selector="{{AdminConfigSystemSection.enhancedMediaGalleryFieldset}}" stepKey="waitForFieldset" /> + <selectOption userInput="{{enabled}}" selector="{{AdminConfigSystemSection.enhancedMediaGalleryEnabledField}}" stepKey="enableOrDisableMediaGallery"/> + <click selector="{{AdminConfigSystemSection.saveConfig}}" stepKey="saveConfiguration"/> + <waitForPageLoad stepKey="waitForConfigurationToSave"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml new file mode 100644 index 0000000000000..f7e8f551e681f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderDeleteActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryFolderDeleteActionGroup"> + <wait time="2" stepKey="waitBeforeDeleteButtonWillBeActive"/> + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderDeleteModalHeader}}" stepKey="waitBeforeModalAppears"/> + <click selector="{{AdminMediaGalleryFolderSection.folderConfirmDeleteButton}}" stepKey="clickConfirmDeleteButton"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml new file mode 100644 index 0000000000000..b8ed1d4f1cd25 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryFolderSelectActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryFolderSelectActionGroup"> + <arguments> + <argument name="name" type="string" defaultValue="{{AdminMediaGalleryFolderData.name}}"/> + </arguments> + <wait time="2" stepKey="waitBeforeClickOnFolder"/> + <click selector="//div[contains(@class, 'media-directory-container')]//a[contains(text(), '{{name}}')]" stepKey="selectFolder"/> + <waitForLoadingMaskToDisappear stepKey="waitForFolderContents"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml new file mode 100644 index 0000000000000..e6cbbfbc1f48d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryImageDeleteActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryImageDeleteActionGroup"> + <annotations> + <description>Delete image from the Enhanced Media Gallery using header delete button</description> + </annotations> + <waitForElementVisible selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="waitForDeleteSelectedButton"/> + <click selector="{{AdminMediaGalleryHeaderButtonsSection.deleteSelected}}" stepKey="ClickDeleteSelectedButton"/> + <waitForLoadingMaskToDisappear stepKey="waitForDeleteModal"/> + <click selector="{{AdminEnhancedMediaGalleryDeleteModalSection.confirmDelete}}" stepKey="confirmDelete"/> + <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml new file mode 100644 index 0000000000000..165522892f271 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminMediaGalleryOpenNewFolderFormActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMediaGalleryOpenNewFolderFormActionGroup"> + <click selector="{{AdminMediaGalleryFolderSection.folderNewCreateButton}}" stepKey="clickCreateNewFolderButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderNewModalHeader}}" stepKey="waitForModalOpen"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml new file mode 100644 index 0000000000000..6f38bd7c7d738 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from image uploader on category page</description> + </annotations> + + <conditionalClick stepKey="clickExpandContent" selector="{{AdminCategoryContentSection.sectionHeader}}" dependentSelector="{{AdminCategoryContentSection.selectFromGalleryButton}}" visible="false" /> + <waitForElementVisible selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="waitForSelectFromGallery" /> + <click selector="{{AdminCategoryContentSection.selectFromGalleryButton}}" stepKey="clickSelectFromGallery" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml new file mode 100644 index 0000000000000..0b2540de5288e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromPageNoEditorActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenMediaGalleryFromPageNoEditorActionGroup"> + <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="waitForInsertImageButton" /> + <click selector="{{CmsWYSIWYGSection.InsertImageBtn}}" stepKey="clickInsertImage" /> + <!-- wait for initial media gallery load, where the gallery chrome loads (and triggers loading modal) --> + <waitForPageLoad stepKey="waitForMediaGalleryInitialLoad"/> + <!-- wait for second media gallery load, where the gallery images load (and triggers loading modal once more) --> + <waitForPageLoad stepKey="waitForMediaGallerySecondaryLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml new file mode 100644 index 0000000000000..3143b4ff24fb4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenMediaGalleryFromTinyMce4IconActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenMediaGalleryTinyMce4ActionGroup"> + <annotations> + <description>Opens Enhanced MediaGallery from category page by tyniMce4 image icon</description> + </annotations> + + <click selector="{{AdminCategoryContentSection.sectionHeader}}" stepKey="clickExpandContent"/> + <waitForElementVisible selector="{{TinyMCESection.TinyMCE4}}" stepKey="waitForTinyMCE4" /> + <click selector="{{TinyMCESection.InsertImageIcon}}" stepKey="clickInsertImageIcon" /> + <waitForPageLoad stepKey="waitForPageLoad" /> + <click selector="{{MediaGallerySection.Browse}}" stepKey="clickBrowse"/> + <waitForPageLoad stepKey="waitForPopup"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..1ef908f34918e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AdminOpenStandaloneMediaGalleryActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOpenStandaloneMediaGalleryActionGroup"> + <amOnPage url="{{AdminStandaloneMediaGalleryPage.url}}" stepKey="amOnStandaloneMediaGalleryPage"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml new file mode 100644 index 0000000000000..e9558ac87df3b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertAdminEnhancedMediaGalleryImageDeletedActionGroup.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup"> + <annotations> + <description>Assert that an image was deleted from Enhanced Media Gallery.</description> + </annotations> + <arguments> + <argument name="title"/> + </arguments> + <see userInput='The asset "{{title}}" has been successfully deleted' stepKey="verifyDeleteImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml new file mode 100644 index 0000000000000..ff11f1a5c7058 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAddedToPageContentActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertImageAddedToPageContentActionGroup"> + <annotations> + <description>Validates that the an image was added to the content.</description> + </annotations> + <arguments> + <argument name="imageName" type="string"/> + </arguments> + <grabValueFrom selector="{{CmsNewPagePageContentSection.content}}" stepKey="grabTextFromContent"/> + <assertStringContainsString stepKey="assertContentContainsAddedImage"> + <expectedResult type="string">{{imageName}}</expectedResult> + <actualResult type="variable">grabTextFromContent</actualResult> + </assertStringContainsString> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml new file mode 100644 index 0000000000000..e17be216335fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/AssertImageAttributesOnEnhancedMediaGalleryActionGroup.xml @@ -0,0 +1,36 @@ +<?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="AssertImageAttributesOnEnhancedMediaGalleryActionGroup"> + <annotations> + <description>Assets image information on the Media Gallery grid</description> + </annotations> + <arguments> + <argument name="image"/> + </arguments> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.title}}" stepKey="grabImageTitle"/> + <assertStringContainsString stepKey="verifyImageTitle"> + <actualResult type="variable">grabImageTitle</actualResult> + <expectedResult type="string">{{image.fileName}}</expectedResult> + </assertStringContainsString> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.contentType}}" stepKey="grabContentType"/> + <assertStringContainsStringIgnoringCase stepKey="verifyContentType"> + <actualResult type="variable">grabContentType</actualResult> + <expectedResult type="string">{{image.extension}}</expectedResult> + </assertStringContainsStringIgnoringCase> + + <grabTextFrom selector="{{AdminEnhancedMediaGalleryImageDescriptionSection.dimensions}}" stepKey="grabDimensions"/> + <assertNotEmpty stepKey="verifyDimensions"> + <actualResult type="variable">grabDimensions</actualResult> + </assertNotEmpty> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml new file mode 100644 index 0000000000000..1d568fb6a1da4 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="SearchStandaloneMediaGalleryAdminDataGridByKeywordActionGroup" extends="SearchAdminDataGridByKeywordActionGroup"> + <annotations> + <description>EXTENDS: SearchAdminDataGridByKeywordActionGroup. Fills 'Search by keyword' on an Standalone Media Gallery Admin Grid page. Clicks on Submit Search.</description> + </annotations> + <arguments> + <argument name="keyword" type="string" defaultValue=""/> + </arguments> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml new file mode 100644 index 0000000000000..1ec5e7d802a61 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/ActionGroup/StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup"> + <arguments> + <argument name="categoryEntity" defaultValue="SimpleSubCategory"/> + <argument name="imageName" type="string"/> + </arguments> + <annotations> + <description>Navigates to the category page on the storefront and asserts that the image is present in description.</description> + </annotations> + + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="openHomePage"/> + <waitForPageLoad stepKey="waitForStorefrontPageLoad"/> + <click selector="{{StorefrontHeaderSection.NavigationCategoryByName(categoryEntity.name)}}" stepKey="toCategory"/> + <waitForPageLoad stepKey="waitForCategoryPage"/> + <seeElement selector="{{StorefrontCategoryMainSection.imageSource(imageName)}}" stepKey="seeImage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml new file mode 100644 index 0000000000000..dbc298798ee8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminEnhancedMediaGalleryImageData.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="UpdatedImageDetails" type="image"> + <data key="title">renamed title</data> + <data key="description">test description</data> + <data key="file">magento.jpg</data> + <data key="fileName">renamed title</data> + <data key="extension">jpg</data> + <data key="keyword">newkeyword</data> + </entity> + <entity name="ImageUploadPng" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">png.png</data> + <data key="file">png.png</data> + <data key="fileName">png</data> + <data key="extension">png</data> + </entity> + <entity name="ImageUploadGif" type="uploadImage"> + <data key="title" unique="suffix">Image1</data> + <data key="file_type">Upload File</data> + <data key="value">gif.gif</data> + <data key="file">gif.gif</data> + <data key="fileName">gif</data> + <data key="extension">gif</data> + </entity> + <entity name="ImageMetadata" type="image"> + <data key="title">Title of the magento image</data> + <data key="description">Description of the magento image</data> + <data key="file">magento3.jpg</data> + <data key="fileName">Title of the magento image</data> + <data key="extension">jpg</data> + <data key="keywords">magento, mediagallerymetadata</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml new file mode 100644 index 0000000000000..e4149acdf58d1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdminMediaGalleryFolderData.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminMediaGalleryFolderData"> + <data key="name" unique="suffix">folder</data> + </entity> + <entity name="AdminMediaGalleryFolderInvalidData"> + <data key="name">,.?/:;'[{]}|~`!@#$%^*()_=+</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml new file mode 100644 index 0000000000000..e8f394a006104 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Data/AdobeStockConfigData.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="MediaGalleryConfigDataEnabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">1</data> + </entity> + <entity name="MediaGalleryConfigDataDisabled"> + <data key="path">system/media_gallery/enabled</data> + <data key="value">0</data> + </entity> +</entities> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminMediaGalleryConfigSystemPage.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminMediaGalleryConfigSystemPage.xml new file mode 100644 index 0000000000000..429e5da4129d3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminMediaGalleryConfigSystemPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminMediaGalleryConfigSystemPage" url="admin/system_config/edit/section/system" area="admin" module="Magento_Config"> + <section name="AdminConfigSystemSection"/> + </page> +</pages> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml new file mode 100644 index 0000000000000..f7ed27171db40 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Page/AdminStandaloneMediaGalleryPage.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<pages xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/PageObject.xsd"> + <page name="AdminStandaloneMediaGalleryPage" url="/media_gallery/media" area="admin" module="Magento_MediaGalleryUi"/> +</pages> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml new file mode 100644 index 0000000000000..b7900f6664c62 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminConfigSystemSection.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminConfigSystemSection"> + <element name="enhancedMediaGalleryFieldset" type="block" selector="#system_media_gallery-head"/> + <element name="enhancedMediaGalleryEnabledField" type="select" selector="[data-ui-id='select-groups-media-gallery-fields-enabled-value']"/> + <element name="saveConfig" type="button" selector="#save"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml new file mode 100644 index 0000000000000..7f9a5aefdf69c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryActionsSection.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryActionsSection"> + <element name="editViewButtonPartial" type="button" selector="/following-sibling::div/button[@class='action-edit']"/> + <element name="deleteViewButton" type="button" selector="//div[@data-bind='afterRender: \$data.setToolbarNode']//input/following-sibling::div/button[@class='action-delete']"/> + <element name="upload" type="input" selector="#image-uploader-input"/> + <element name="cancel" type="button" selector="[data-ui-id='cancel-button']"/> + <element name="createFolder" type="button" selector="[data-ui-id='create-folder-button']"/> + <element name="deleteFolder" type="button" selector="[data-ui-id='delete-folder-button']"/> + <element name="imageSrc" type="text" selector="//div[@class='masonry-image-column' and contains(@data-repeat-index, '0')]//img[contains(@src,'{{src}}')]" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.xml new file mode 100644 index 0000000000000..b4071295bacf3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryDeleteModalSection.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="AdminEnhancedMediaGalleryDeleteModalSection"> + <element name="confirmDelete" type="button" selector=".media-gallery-delete-image-action .action-accept"/> + <element name="cancelDelete" type="button" selector=".media-gallery-delete-image-action .action-dismiss"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.xml new file mode 100644 index 0000000000000..b8e2f698ccfe8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryEditDetailsSection.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="AdminEnhancedMediaGalleryEditDetailsSection"> + <element name="title" type="input" selector="#title"/> + <element name="fileName" type="text" selector="#path"/> + <element name="description" type="textarea" selector="#description"/> + <element name="newKeyword" type="input" selector="[data-ui-id='keyword']"/> + <element name="addNewKeyword" type="input" selector="[data-ui-id='add-keyword']"/> + <element name="cancel" type="button" selector="#image-details-action-cancel"/> + <element name="save" type="button" selector="#image-details-action-save"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml new file mode 100644 index 0000000000000..32b109f1e0483 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryFiltersSection.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryFiltersSection"> + <element name="filtersButton" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-expand']"/> + <element name="categoryGridFiltersButton" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-expand']"/> + <element name="sourceFilterValue" type="select" parameterized="true" selector="//div[@class='media-gallery-container']//select[@name='source']//option[@value='{{option}}']"/> + <element name="applyFilters" type="button" selector="//div[@class='media-gallery-container']//button[@data-action='grid-filter-apply']"/> + <element name="categoryGridApplyFilters" type="button" selector="//div[@class='media-gallery-category-container']//button[@data-action='grid-filter-apply']"/> + <element name="activeFilter" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']//span[contains( ., '{{filter}}')]" parameterized="true"/> + <element name="activeFilterPlaceholder" type="text" selector="//div[@class='media-gallery-container']//div[@class='admin__current-filters-list-wrap']"/> + <element name="usedInSelectDropdown" type="text" selector="//label[@class='admin__form-field-label']/span[text()='Show Images Used In']/parent::*/parent::div/div//div[@class='admin__action-multiselect-text' and text()='Select...']"/> + <element name="usedInEntityType" type="text" selector="//label[@class='admin__action-multiselect-label']/span[text()='{{entityType}}']" parameterized="true"/> + <element name="usedInDoneButton" type="button" selector="//div[@class='admin__action-multiselect-actions-wrap']/button/span[text()='Done']"/> + <element name="selectFilter" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select" parameterized="true"/> + <element name="selectFilterOption" type="button" selector="//label[@class='admin__form-field-label']/span[text()='{{filterLabel}}']/parent::*/parent::div/div[@class='admin__form-field-control']/select/option[@data-title='{{optionLabel}}']" parameterized="true"/> + <element name="searchOptionsFilter" type="select" selector="//div[label/span[contains(text(), '{{filterName}}')]]//div[@class='action-select admin__action-multiselect']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterInput" type="input" selector="//div[label/span[contains(text(), '{{filterName}}')]]//input[@data-role='advanced-select-text']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterOption" type="text" selector="//div[label/span[contains(text(), '{{filterName}}')]]//label[@class='admin__action-multiselect-label']/span[text()='{{optionName}}']" parameterized="true" timeout="30"/> + <element name="searchOptionsFilterDone" type="button" selector="//div[label/span[contains(text(), '{{filterName}}')]]//button[@data-action='close-advanced-select']" parameterized="true"/> + <element name="duplicatedFilterCheckbox" type="button" selector="//input[@name='duplicated']"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml new file mode 100644 index 0000000000000..3f13a57697e6f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageActionsSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryImageActionsSection"> + <element name="openContextMenu" type="button" selector=".three-dots"/> + <element name="viewDetails" type="button" selector="[data-ui-id='action-image-details']"/> + <element name="delete" type="button" selector="[data-ui-id='action-delete']"/> + <element name="edit" type="button" selector="[data-ui-id='action-edit']"/> + <element name="imageInGrid" type="button" selector="//li[@data-ui-id='title'and text()='{{imageTitle}}']/parent::*/parent::*/parent::div//img[@class='media-gallery-image-column']" parameterized="true"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.xml new file mode 100644 index 0000000000000..32cd99bfe6b11 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryImageDescriptionSection.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="AdminEnhancedMediaGalleryImageDescriptionSection"> + <element name="title" type="text" selector=".masonry-image-description .name"/> + <element name="contentType" type="text" selector=".masonry-image-description .type"/> + <element name="dimensions" type="text" selector=".masonry-image-description .dimensions" /> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml new file mode 100644 index 0000000000000..07f2dc23530e1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryMassActionSection.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryMassActionSection"> + <element name="massActionCheckbox" type="button" selector="//input[@type='checkbox'][@data-ui-id ='{{imageName}}']" parameterized="true"/> + <element name="totalSelected" type="text" selector=".mediagallery-massaction-items-count > .selected_count_text"/> + <element name="cancelMassActionMode" type="button" selector="#cancel_massaction"/> + <element name="deleteImages" type="button" selector="#delete_massaction"/> + <element name="deleteSelected" type="button" selector="#delete_selected_massaction"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml new file mode 100644 index 0000000000000..048739ed3f81d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminEnhancedMediaGalleryViewDetailsSection.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminEnhancedMediaGalleryViewDetailsSection"> + <element name="title" type="text" selector=".image-title"/> + <element name="contentType" type="text" selector="[data-ui-id='content-type']"/> + <element name="type" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Type')]/following-sibling::div"/> + <element name="height" type="text" selector="//div[@class='attribute']/span[contains(text(), 'Height')]/following-sibling::div"/> + <element name="description" type="text" selector=".image-details-section.description p"/> + <element name="keywords" type="text" selector="//div[@class='tags-list']"/> + <element name="filename" type="text" selector=".image-details-section.filename p"/> + <element name="edit" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'edit')]"/> + <element name="delete" type="button" selector="//div[@class='media-gallery-image-details-modal']//button[contains(@class, 'delete')]"/> + <element name="confirmDelete" type="button" selector=".action-accept"/> + <element name="addImage" type="button" selector=".add-image-action"/> + <element name="cancel" type="button" selector="#image-details-action-cancel"/> + <element name="usedInLink" type="button" parameterized="true" selector="//div[@class='attribute']/span[contains(text(), 'Used In')]/following-sibling::div/a[contains(text(), '{{entityName}}')]"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml new file mode 100644 index 0000000000000..4c9e6bf362194 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryFolderSection.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminMediaGalleryFolderSection"> + <element name="folderNewModalHeader" type="block" selector="//h1[contains(text(), 'New Folder Name')]"/> + <element name="folderDeleteModalHeader" type="block" selector="//h1[contains(text(), 'Are you sure you want to delete this folder?')]"/> + <element name="folderNewCreateButton" type="button" selector="#create_folder"/> + <element name="folderDeleteButton" type="button" selector="#delete_folder"/> + <element name="folderConfirmDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'OK')]"/> + <element name="folderCancelDeleteButton" type="button" selector="//footer//button/span[contains(text(), 'Cancel')]"/> + <element name="folderNameField" type="button" selector="[name=folder_name]"/> + <element name="folderConfirmCreateButton" type="button" selector="//button/span[contains(text(),'Confirm')]"/> + <element name="folderNameValidationMessage" type="block" selector="label.mage-error"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.xml new file mode 100644 index 0000000000000..9271c0ff61618 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Section/AdminMediaGalleryHeaderButtonsSection.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="AdminMediaGalleryHeaderButtonsSection"> + <element name="addSelected" type="button" selector=".media-gallery-add-selected"/> + <element name="deleteSelected" type="button" selector="#delete_selected"/> + </section> +</sections> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml new file mode 100644 index 0000000000000..4749fc4a885b0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Suite/MediaGalleryUiSuite.xml @@ -0,0 +1,29 @@ +<?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="MediaGalleryUiSuite"> + <before> + <actionGroup ref="AdminDisableWYSIWYGActionGroup" stepKey="disableWYSIWYG" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="enableEnhancedMediaGallery"> + <argument name="enabled" value="1"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </before> + <after> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminMediaGalleryEnhancedEnableActionGroup" stepKey="disableEnhancedMediaGallery"/> + <actionGroup ref="AdminEnableWYSIWYGActionGroup" stepKey="enableWYSIWYG" /> + </after> + <include> + <group name="media_gallery_ui"/> + </include> + </suite> +</suites> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml new file mode 100644 index 0000000000000..94831b039b53a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDeleteImagesInBulkTest.xml @@ -0,0 +1,50 @@ +<?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="AdminEnhancedMediaGalleryDeleteImagesInBulkTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1488"/> + <title value="User deletes images with less clicks"/> + <stories value="[Story #42] User deletes images in bulk"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User deletes images with less clicks"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToVerifyMode"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeDetailsActionGroup" stepKey="assertMassActionModeAvailable"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryDisableMassactionModeActionGroup" stepKey="disableMassActionMode"/> + + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertImagesDeletedInBulkActionGroup" stepKey="assertImagesDeleted"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertMassActionModeNotActiveActionGroup" stepKey="assertMassectionModeDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml new file mode 100644 index 0000000000000..52f3a8079e962 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryDuplicatedImagesTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryDuplicatedImagesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1500"/> + <title value="User can filter duplicated images"/> + <stories value="[Story 59] User finds image duplicates"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="User can filter duplicated images"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyDuplicatedFilterActionGroup" stepKey="SelectDuplicatedFilter"/> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertFirstImageInGrid"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertSecondImageInGrid"> + <argument name="title" value="ImageUpload_1.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml new file mode 100644 index 0000000000000..f026b87f7ec88 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryUploadImageWithMetadataTest.xml @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryUploadImageWithMetadataTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="Magento extracts image meta data from file"/> + <stories value="Story 53 - Magento extracts image meta data from file"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="Magento extracts image meta data from file"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteJpegImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadPngImage"> + <argument name="image" value="ImageUploadPng"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewPngImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyPngImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyPngImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyPngImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deletePngImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadGifImage"> + <argument name="image" value="ImageUploadGif"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewGifImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyGifImageDescription"> + <argument name="description" value="ImageMetadata.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyGifImageKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageTitleActionGroup" stepKey="verifyGifImageTitle"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteGifImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml new file mode 100644 index 0000000000000..bd7e4fcf7a9a2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyAssetFilterTest.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryVerifyAssetFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1292"/> + <title value="User sees entities where asset is used in"/> + <stories value="Story 58: User sees entities where asset is used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4951024"/> + <description value="User sees entities where asset is used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenCategoryGridPageActionGroup" stepKey="openCategoryGridPage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectUsedInFilterActionGroup" stepKey="setUsedInFilter"> + <argument name="filterName" value="Asset"/> + <argument name="optionName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCategoryGridApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertCategoryNameInCategoryGridActionGroup" stepKey="assertCategoryInGrid"> + <argument name="categoryName" value="$$category.name$$"/> + </actionGroup> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml new file mode 100644 index 0000000000000..4719b98c78dbe --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminEnhancedMediaGalleryVerifyNotUsedOptionFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1489"/> + <title value="User filters images that are not used in the content"/> + <stories value="Story 52: User filters images that are not used in the content"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images that are not used in the content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterImage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Not used anywhere"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml new file mode 100644 index 0000000000000..d54399bdeb2b2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminEnhancedMediaGalleryVerifyUsedInFilterTest.xml @@ -0,0 +1,83 @@ +<?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="AdminEnhancedMediaGalleryVerifyUsedInFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1567"/> + <title value="User filters images by the area they used in"/> + <stories value="User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4930844"/> + <description value="User filters images by the area they used in"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadFirstIMage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadSecondImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToFilterIMage"/> + + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplyUsedInFilterActionGroup" stepKey="applyUsedInCategoryFilter"> + <argument name="entityType" value="Categories"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryAssertImageNotExistsInTheGridActionGroup" stepKey="assertImageNotExistsInGrid"> + <argument name="title" value="UpdatedImageDetails.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml new file mode 100644 index 0000000000000..cb7adf3307865 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageFromTwoComponentsTest.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryAddCategoryImageFromTwoComponentsTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1168"/> + <title value="User add category image via wysiwyg and image uploader button"/> + <stories value="Story [54]: User inserts image rendition to the content with text area + Insert image button" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4523889"/> + <description value="User add category image via wysiwyg and image uploader button"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectSecondImageToDelete"> + <argument name="imageName" value="{{UpdatedImageDetails.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload_1"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryCloseViewDetailsActionGroup" stepKey="closeViewDetails"/> + + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"> + <argument name="name" value="wysiwyg"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedCategoryImage"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="reSaveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertContentImageIsVisible"> + <argument name="imageName" value="{{ImageUpload3.fileName}}"/> + </actionGroup> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertCategoryImageIsVisible"> + <argument name="imageName" value="{{ImageUpload_1.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml new file mode 100644 index 0000000000000..30f1412a5b08d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddCategoryImageTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryAddCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1073"/> + <title value="User add category image via wysiwyg"/> + <stories value="User add category image via wysiwyg"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4484351"/> + <description value="User add category image via wysiwyg"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="StoreFrontMediaGalleryAssertImageInCategoryDescriptionActionGroup" stepKey="assertImageInCategoryDescriptionField"> + <argument name="imageName" value="{{ImageUpload3.fileName}}" /> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml new file mode 100644 index 0000000000000..94307fa510a50 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryAddFromImageDetailsTest.xml @@ -0,0 +1,41 @@ +<?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="AdminMediaGalleryAddFromImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1229"/> + <stories value="[Story #38] User views basic image attributes in Media Gallery"/> + <title value="Adding image from the Image Details"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4569982"/> + <description value="Adding image from the Image Details"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </before> + <after> + <actionGroup ref="AdminOpenCreateNewCMSPageActionGroup" stepKey="openNewPage"/> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryAddImageFromImageDetailsActionGroup" stepKey="addImageFromViewDetails"/> + <actionGroup ref="AssertImageAddedToPageContentActionGroup" stepKey="assertImageAddedToContent"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..6e6f5240e84be --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryCreateDeleteFolderTest.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="AdminMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1046; https://github.com/magento/adobe-stock-integration/issues/1047"/> + <stories value="Creating, deleting new folder functionality in Media Gallery"/> + <title value="Creating, deleting new folder functionality in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4456547; https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4457075"/> + <description value="Creating, deleting new folder functionality in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml new file mode 100644 index 0000000000000..980d6b7c85c20 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageContextMenuTest.xml @@ -0,0 +1,33 @@ +<?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="AdminMediaGalleryDeleteImageContextMenuTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/710"/> + <title value="Uploading and deleting an image using context menu"/> + <stories value="[Story #52] User accesses Media Gallery from the main navigation"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4753539"/> + <description value="Uploading and deleting an image using context menu"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml new file mode 100644 index 0000000000000..ad364e7709a33 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageFileTest.xml @@ -0,0 +1,41 @@ +<?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="AdminMediaGalleryDeleteImageFileTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1094"/> + <title value="Deleting new image file functionality in Enhanced Media Gallery"/> + <stories value="Deleting new image file functionality in Enhanced Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4756652"/> + <description value="Deleting new image file functionality in Enhanced Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageUpload.fileName}}"/> + </actionGroup> + + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="verifyImageIsDeleted"> + <argument name="title" value="ImageUpload.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml new file mode 100644 index 0000000000000..6ae8ed7047434 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDeleteImageWithWarningPopupTest.xml @@ -0,0 +1,56 @@ +<?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="AdminMediaGalleryDeleteImageWithWarningPopupTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1511"/> + <title value="User sees warning when deleting image if it's used on storefront"/> + <stories value="User sees warning when deleting image if it's used on storefront"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4843896"/> + <description value="User sees warning when deleting image if it's used on storefront"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + </after> + + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadCategoryImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectCategoryImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedContentImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwygToAssertMessage"/> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertWarningMessageActionGroup" stepKey="assertMessageImageUsedIn"> + <argument name="messageText" value="The selected assets are used in the content of the following entities: Categories(1)"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImage"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml new file mode 100644 index 0000000000000..963a0b954e45b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryDisabledContentFilterTest.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryDisabledContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by disabled content"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by disabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <actionGroup ref="AdminEnableCategoryActionGroup" stepKey="disableCategory"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Disabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..960443998d010 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEditImageDetailsTest.xml @@ -0,0 +1,48 @@ +<?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="AdminMediaGalleryEditImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.xml new file mode 100644 index 0000000000000..c2b167912dda7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryEnabledContentFilterTest.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="AdminMediaGalleryEnabledContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by enabled content"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970565"/> + <description value="User filter asset by enabled content"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Content Status"/> + <argument name="optionLabel" value="Enabled"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml new file mode 100644 index 0000000000000..4369a61708a83 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryFilterImagesBySourceTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryFilterImagesBySourceTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1393"/> + <title value="User filters images by source filter"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4760144"/> + <description value="User filters images by source filter"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteContentImage"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadContentImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml new file mode 100644 index 0000000000000..b8ce1f76ad4c8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGallerySaveFiltersStateTest.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGallerySaveFiltersStateTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1397"/> + <title value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <stories value="User is able to use bookmarks controls in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4763040"/> + <description value="User is able to use bookmarks controls for filter views in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + </after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGalleryPage"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectSourceFilterActionGroup" stepKey="applyLocalFilter"> + <argument name="filterValue" value="Local"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminEnhancedMediaGallerySaveCustomViewActionGroup" stepKey="saveCustomView"/> + <actionGroup ref="AdminEnhancedMediaGalleryAssertActiveFiltersActionGroup" stepKey="assertFilterApplied"> + <argument name="resultValue" value="Uploaded Locally"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGallerySelectCustomBookmarksViewActionGroup" stepKey="selectDefaultView"> + <argument name="selectView" value="Default View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryAssertNoActiveFiltersAppliedActionGroup" stepKey="assertNoActiveFilters"/> + <actionGroup ref="AdminEnhancedMediaGalleryDeleteGridViewActionGroup" stepKey="deleteView"> + <argument name="viewToDelete" value="Test View"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.xml new file mode 100644 index 0000000000000..eceda879e5597 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewCategoryFilterTest.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="AdminMediaGalleryStoreViewCategoryFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by category store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by category store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <magentoCLI command="config:set cms/wysiwyg/enabled enabled" stepKey="enableWYSIWYG"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <magentoCLI command="config:set cms/wysiwyg/enabled disabled" stepKey="disableWYSIWYG"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryTinyMce4ActionGroup" stepKey="openMediaGalleryFromWysiwyg"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <actionGroup ref="AdminMediaGalleryClickOkButtonTinyMce4ActionGroup" stepKey="clickOkButton"/> + <actionGroup ref="AdminSaveCategoryActionGroup" stepKey="saveCategory"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml new file mode 100644 index 0000000000000..86cae11267eaa --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryStoreViewContentFilterTest.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryStoreViewContentFilterTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1464"/> + <title value="User filter asset by content store view"/> + <stories value="Story 57: User filters images by the area they used in"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/4970870"/> + <description value="User filter asset by content store view"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="_defaultCmsPage" stepKey="createCMSPage" /> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <deleteData createDataKey="createCMSPage" stepKey="deleteCmsPage"/> + </after> + <actionGroup ref="NavigateToCreatedCMSPageActionGroup" stepKey="navigateToCreatedCMSPage"> + <argument name="CMSPage" value="$$createCMSPage$$"/> + </actionGroup> + <actionGroup ref="AdminOpenMediaGalleryFromPageNoEditorActionGroup" stepKey="openMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickImageInGridActionGroup" stepKey="selectImageInGrid"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminMediaGalleryClickAddSelectedActionGroup" stepKey="clickAddSelectedImage"/> + <scrollToTopOfPage stepKey="scrollToTop"/> + <click selector="{{CmsNewPagePageActionsSection.saveAndContinueEdit}}" stepKey="clickSavePage"/> + <waitForPageLoad stepKey="waitForPageSave"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="AdminEnhancedMediaGalleryExpandFilterActionGroup" stepKey="expandFilters"/> + <actionGroup ref="AdminMediaGalleryApplySelectFilterActionGroup" stepKey="selectFilterOption"> + <argument name="filterLabel" value="Store View"/> + <argument name="optionLabel" value="Main Website/Main Website Store/Default Store View"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryApplyFiltersActionGroup" stepKey="applyFilters"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEnableMassActionModeActionGroup" stepKey="enableMassActionToDeleteImages"/> + <actionGroup ref="AdminEnhancedMediaGallerySelectImageForMassActionActionGroup" stepKey="selectFirstImageToDelete"> + <argument name="imageName" value="{{ImageMetadata.title}}"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryClickDeleteImagesButtonActionGroup" stepKey="clikDeleteSelectedButton"/> + <actionGroup ref="AdminEnhancedMediaGalleryConfirmDeleteImagesActionGroup" stepKey="deleteImages"/> + + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml new file mode 100644 index 0000000000000..ca7a71258fead --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryUploadCategoryImageTest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryUploadCategoryImageTest"> + <annotations> + <features value="AdminMediaGalleryImagePanel"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1435"/> + <stories value="User uploads image outside of the Media Gallery"/> + <title value="User uploads image outside of the Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/943908/scenarios/4836631"/> + <description value="User uploads image outside of the Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="category"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + <actionGroup ref="ResetAdminDataGridToDefaultViewActionGroup" stepKey="resetAdminDataGridToDefaultView"/> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewContentImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteCategoryImage"/> + <deleteData createDataKey="category" stepKey="deleteCategory"/> + </after> + <actionGroup ref="AdminOpenCategoryPageActionGroup" stepKey="openCategoryPage"/> + <actionGroup ref="AdminCategoriesOpenCategoryActionGroup" stepKey="openCategory"> + <argument name="category" value="$$category$$"/> + </actionGroup> + <actionGroup ref="AddCategoryImageActionGroup" stepKey="addCategoryImage"/> + <actionGroup ref="AdminSaveCategoryFormActionGroup" stepKey="saveCategoryForm"/> + <actionGroup ref="AdminOpenMediaGalleryFromCategoryImageUploaderActionGroup" stepKey="openMediaGalleryFromImageUploader"/> + <actionGroup ref="AdminMediaGalleryAssertImageInGridActionGroup" stepKey="assertImageInGrid"> + <argument name="title" value="ProductImage.filename"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml new file mode 100644 index 0000000000000..01a26cce1b6fb --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryVerifyImageGridAttributesTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryVerifyImageGridAttributesTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/708"/> + <title value="Verify image grid attributes"/> + <stories value="[Story #41] User views limited image information from the image grid in Media Gallery" /> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/3839218"/> + <description value="User views basic image attributes in Media Gallery grid"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="assertImageAttributes"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml new file mode 100644 index 0000000000000..00fc07eb6c1af --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsDeleteImageTest.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryViewDetailsDeleteImageTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/461"/> + <title value="Deleting an image from view details panel"/> + <stories value="[Story #42] User deletes images"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4516773"/> + <description value="Deleting an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageMetadata"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + <actionGroup ref="AssertAdminEnhancedMediaGalleryImageDeletedActionGroup" stepKey="assertImageDeleted"> + <argument name="title" value="ImageMetadata.title"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..92909bcf06795 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,46 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..c9447d5cc8a52 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminMediaGalleryViewDetailsTest.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="AdminMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1054245/scenarios/4653671"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml new file mode 100644 index 0000000000000..164ab523d508a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryCreateDeleteFolderTest.xml @@ -0,0 +1,56 @@ +<?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="AdminStandaloneMediaGalleryCreateDeleteFolderTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1119; https://github.com/magento/adobe-stock-integration/issues/1120"/> + <stories value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <title value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503041; https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503101"/> + <description value="Creating, deleting new folder functionality in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + + <actionGroup ref="AdminMediaGalleryOpenNewFolderFormActionGroup" stepKey="openNewFolderForm"/> + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolderWithNotValidName"> + <argument name="name" value="{{AdminMediaGalleryFolderInvalidData.name}}"/> + </actionGroup> + + <grabTextFrom selector="{{AdminMediaGalleryFolderSection.folderNameValidationMessage}}" stepKey="grabValidationMessage"/> + <assertStringContainsString stepKey="assertFirst"> + <actualResult type="variable">grabValidationMessage</actualResult> + <expectedResult type="string">Please use only letters (a-z or A-Z) or numbers (0-9) in this field. No spaces or other characters are allowed.</expectedResult> + </assertStringContainsString> + + <actionGroup ref="AdminMediaGalleryCreateNewFolderActionGroup" stepKey="createNewFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertNewFolderCreated"/> + + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openMediaGalleryForPage"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="deleteFolderButtonIsNotDisabled"/> + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="unselectFolder"/> + <seeElement selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}, :disabled" stepKey="deleteFolderButtonIsDisabledAgain"/> + + <actionGroup ref="AdminMediaGalleryFolderSelectActionGroup" stepKey="selectFolderForDelete"/> + + <click selector="{{AdminMediaGalleryFolderSection.folderDeleteButton}}" stepKey="clickDeleteButton"/> + <waitForElementVisible selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="waitBeforeModalLoads"/> + <click selector="{{AdminMediaGalleryFolderSection.folderCancelDeleteButton}}" stepKey="cancelDeleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderNameActionGroup" stepKey="assertFolderWasNotDeleted"/> + + <actionGroup ref="AdminMediaGalleryFolderDeleteActionGroup" stepKey="deleteFolder"/> + <actionGroup ref="AdminMediaGalleryAssertFolderDoesNotExistActionGroup" stepKey="assertFolderWasDeleted"/> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml new file mode 100644 index 0000000000000..ede3a452e4ca5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryEditImageDetailsTest.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryEditImageDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/724"/> + <title value="User edits image meta data in standalone media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="User edits image meta data in Standalone Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryEditImageDetailsActionGroup" stepKey="editImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml new file mode 100644 index 0000000000000..2cf6bf5dfe623 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsEditTest.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminStandaloneMediaGalleryViewDetailsEditTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/1581"/> + <title value="Editing an image from standalone view details panel"/> + <stories value="[Story #44] User edits image meta data in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1320712/scenarios/3961351"/> + <description value="Editing an image from standalone view details panel"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload3"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsEditActionGroup" stepKey="editImage"/> + <actionGroup ref="AdminMediaGalleryEditAssetAddKeywordActionGroup" stepKey="setKeywords"> + <argument name="keyword" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsSaveActionGroup" stepKey="saveImage"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AssertImageAttributesOnEnhancedMediaGalleryActionGroup" stepKey="verifyUpdateImageOnTheGrid"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="UpdatedImageDetails"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDescriptionActionGroup" stepKey="verifyImageDescription"> + <argument name="description" value="UpdatedImageDetails.description"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyAddedKeywords"> + <argument name="keywords" value="UpdatedImageDetails.keyword"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageKeywordsActionGroup" stepKey="verifyMetadataKeywords"> + <argument name="keywords" value="ImageMetadata.keywords"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.xml new file mode 100644 index 0000000000000..bb7071497ce24 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Mftf/Test/AdminStandaloneMediaGalleryViewDetailsTest.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="AdminStandaloneMediaGalleryViewDetailsTest"> + <annotations> + <features value="MediaGallery"/> + <useCaseId value="https://github.com/magento/adobe-stock-integration/issues/428"/> + <title value="View image details in standalone media gallery"/> + <stories value="[Story # 38] User views basic image attributes in Media Gallery"/> + <testCaseId value="https://studio.cucumber.io/projects/131313/test-plan/folders/1337102/scenarios/4503223"/> + <description value="User views basic image attributes in Media Gallery"/> + <severity value="CRITICAL"/> + <group value="media_gallery_ui"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminOpenStandaloneMediaGalleryActionGroup" stepKey="openStandaloneMediaGallery"/> + </before> + <after> + <actionGroup ref="AdminEnhancedMediaGalleryImageDetailsDeleteActionGroup" stepKey="deleteImage"/> + </after> + + <actionGroup ref="AdminEnhancedMediaGalleryUploadImageActionGroup" stepKey="uploadImage"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryViewImageDetails" stepKey="viewImageDetails"/> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageDetailsActionGroup" stepKey="verifyImageDetails"> + <argument name="image" value="ImageUpload"/> + </actionGroup> + <actionGroup ref="AdminEnhancedMediaGalleryVerifyImageFilenameActionGroup" stepKey="verifyFilename"> + <argument name="filename" value="{{ImageUpload.file}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php new file mode 100644 index 0000000000000..3d4e523d0d6b1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/ConfigTest.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\Config; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Config data test. + */ +class ConfigTest extends TestCase +{ + private const XML_PATH_ENABLED = 'system/media_gallery/enabled'; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Config + */ + private $config; + + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfigMock; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->config = $this->objectManager->getObject( + Config::class, + [ + 'scopeConfig' => $this->scopeConfigMock + ] + ); + } + + /** + * Get Magento media gallery enabled test. + */ + public function testIsEnabled(): void + { + $this->scopeConfigMock->expects($this->once()) + ->method('isSetFlag') + ->with(self::XML_PATH_ENABLED) + ->willReturn(true); + $this->assertEquals(true, $this->config->isEnabled()); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php new file mode 100644 index 0000000000000..fc8a0756a7b55 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Test/Unit/Model/UploadImageTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Test\Unit\Model; + +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\Read; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use Magento\MediaGalleryUi\Model\UploadImage; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Provides test for upload image functionality + */ +class UploadImageTest extends TestCase +{ + /** + * @var Storage|MockObject + */ + private $imagesStorageMock; + + /** + * @var Filesystem|MockObject + */ + private $fileSystemMock; + + /** + * @var Read|MockObject + */ + private $mediaDirectoryMock; + + /** + * @var UploadImage + */ + private $uploadImage; + + /** + * Prepare test objects. + */ + protected function setUp(): void + { + $this->imagesStorageMock = $this->createMock(Storage::class); + $this->fileSystemMock = $this->createMock(Filesystem::class); + $this->mediaDirectoryMock = $this->createMock(Read::class); + + $this->uploadImage = (new ObjectManager($this))->getObject( + UploadImage::class, + [ + 'imagesStorage' => $this->imagesStorageMock, + 'filesystem' => $this->fileSystemMock, + ] + ); + } + + /** + * Test successful image file upload. + * + * @param string $targetFolder + * @param string|null $type + * @param string $absolutePath + * + * @dataProvider executeDataProvider + */ + public function testExecute(string $targetFolder, string $type = null, string $absolutePath): void + { + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(true); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('getAbsolutePath') + ->with($targetFolder) + ->willReturn($absolutePath); + + $uploadResult = ['path' => 'media/catalog', 'file' => 'test-image.jpeg']; + $this->imagesStorageMock->expects($this->once()) + ->method('uploadFile') + ->with($absolutePath, $type) + ->willReturn($uploadResult); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Test upload image method with logical exception when the folder is not a folder. + */ + public function testExecuteWithException(): void + { + $targetFolder = 'not-a-folder'; + $type = 'image'; + $this->fileSystemMock->expects($this->once()) + ->method('getDirectoryRead') + ->with(DirectoryList::MEDIA) + ->willReturn($this->mediaDirectoryMock); + + $this->mediaDirectoryMock->expects($this->once()) + ->method('isDirectory') + ->with($targetFolder) + ->willReturn(false); + + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage('Directory not-a-folder does not exist in media directory.'); + + $this->uploadImage->execute($targetFolder, $type); + } + + /** + * Provides test case data. + * + * @return array + */ + public function executeDataProvider(): array + { + return [ + [ + 'targetFolder' => 'media/catalog', + 'type' => 'image', + 'absolutePath' => 'root/pub/media/catalog/test-image.jpeg' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg new file mode 100644 index 0000000000000..5244f8dc420e1 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/subdir/test_img2.jpeg differ diff --git a/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg new file mode 100644 index 0000000000000..5244f8dc420e1 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/Test/Unit/_files/test_img1.jpeg differ diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php new file mode 100644 index 0000000000000..4047a4fcb98d8 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/DirectoriesTree.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; + +/** + * Directories tree component + */ +class DirectoriesTree extends Container +{ + /** + * @var UrlInterface + */ + private $url; + + /** + * Constructor + * + * @param ContextInterface $context + * @param UrlInterface $url + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UrlInterface $url, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->url = $url; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'getDirectoryTreeUrl' => $this->url->getUrl("media_gallery/directories/gettree"), + 'deleteDirectoryUrl' => $this->url->getUrl("media_gallery/directories/delete"), + 'createDirectoryUrl' => $this->url->getUrl("media_gallery/directories/create") + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php new file mode 100644 index 0000000000000..ad5e27381dee2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploader.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +use Magento\Framework\File\Size; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Ui\Component\Container; + +/** + * Image Uploader component + */ +class ImageUploader extends Container +{ + private const ACCEPT_FILE_TYPES = '/(\.|\/)(gif|jpe?g|png)$/i'; + private const ALLOWED_EXTENSIONS = 'jpg jpeg png gif'; + + /** + * @var UrlInterface + */ + private $url; + + /** + * @var Size + */ + private $size; + + /** + * @param Size $size + * @param ContextInterface $context + * @param UrlInterface $url + * @param array $components + * @param array $data + */ + public function __construct( + Size $size, + ContextInterface $context, + UrlInterface $url, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $components, $data); + $this->size = $size; + $this->url = $url; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'imageUploadUrl' => $this->url->getUrl('media_gallery/image/upload', ['type' => 'image']), + 'acceptFileTypes' => self::ACCEPT_FILE_TYPES, + 'allowedExtensions' => self::ALLOWED_EXTENSIONS, + 'maxFileSize' => $this->size->getMaxFileSize() + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php new file mode 100644 index 0000000000000..1fc5a80960a69 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/ImageUploaderStandAlone.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component; + +/** + * Image Uploader component + */ +class ImageUploaderStandAlone extends ImageUploader +{ + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array) $this->getData('config'), + [ + 'actionsPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_columns.thumbnail_url', + 'directoriesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing' . + '.media_gallery_directories', + 'messagesPath' => 'standalone_media_gallery_listing.standalone_media_gallery_listing.messages' + ] + ) + ); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Source/Options.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Source/Options.php new file mode 100644 index 0000000000000..b56848aa3515c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Source/Options.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Source; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Image source filter options + */ +class Options implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + [ + 'value' => 'Local', + 'label' => __('Uploaded Locally'), + ], + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php new file mode 100644 index 0000000000000..e425c9488b5c2 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/SourceIconProvider.php @@ -0,0 +1,106 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Asset\Repository as AssetRepository; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\Store; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Source icon url provider + */ +class SourceIconProvider extends Column +{ + /** + * @var array + */ + private $sourceIcons; + + /** + * @var AssetRepository + */ + private $assetRepository; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param AssetRepository $assetRepository + * @param ScopeConfigInterface $scopeConfig + * @param array $components + * @param array $data + * @param array $sourceIcons + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + AssetRepository $assetRepository, + ScopeConfigInterface $scopeConfig, + array $components = [], + array $data = [], + array $sourceIcons = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->assetRepository = $assetRepository; + $this->scopeConfig = $scopeConfig; + $this->sourceIcons = $sourceIcons; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items']) && is_iterable($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as &$item) { + $item[$this->getData('name')] = $item[$this->getData('name')] + ? $this->getSourceIconUrl($item[$this->getData('name')]) + : null; + } + } + + return $dataSource; + } + + /** + * Construct source icon url based on the source code matching + * + * @param string $sourceName + * + * @return string|null + */ + private function getSourceIconUrl(string $sourceName): ?string + { + return isset($this->sourceIcons[$sourceName]) + ? $this->assetRepository->getUrlWithParams( + $this->sourceIcons[$sourceName], + ['_secure' => $this->isSecure()] + ) + : null; + } + + /** + * Check if store use secure connection + * + * @return bool + */ + private function isSecure(): bool + { + return $this->scopeConfig->isSetFlag(Store::XML_PATH_SECURE_IN_ADMINHTML); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php new file mode 100644 index 0000000000000..481f8ab861f0f --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Columns/Url.php @@ -0,0 +1,119 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Columns; + +use Magento\Backend\Model\UrlInterface; +use Magento\Cms\Helper\Wysiwyg\Images; +use Magento\Cms\Model\Wysiwyg\Images\Storage; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Ui\Component\Listing\Columns\Column; + +/** + * Overlay column + */ +class Url extends Column +{ + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * UrlInterface $urlInterface + */ + private $urlInterface; + + /** + * @var Images + */ + private $images; + + /** + * @var Storage + */ + private $storage; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param StoreManagerInterface $storeManager + * @param UrlInterface $urlInterface + * @param Images $images + * @param Storage $storage + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + StoreManagerInterface $storeManager, + UrlInterface $urlInterface, + Images $images, + Storage $storage, + array $components = [], + array $data = [] + ) { + parent::__construct($context, $uiComponentFactory, $components, $data); + $this->storeManager = $storeManager; + $this->urlInterface = $urlInterface; + $this->images = $images; + $this->storage = $storage; + } + + /** + * Prepare Data Source + * + * @param array $dataSource + * @return array + * @throws NoSuchEntityException + */ + public function prepareDataSource(array $dataSource): array + { + if (isset($dataSource['data']['items'])) { + foreach ($dataSource['data']['items'] as & $item) { + $item['encoded_id'] = $this->images->idEncode($item['path']); + $item[$this->getData('name')] = $this->getUrl($item[$this->getData('name')]); + } + } + + return $dataSource; + } + + /** + * @inheritdoc + */ + public function prepare(): void + { + parent::prepare(); + $this->setData( + 'config', + array_replace_recursive( + (array)$this->getData('config'), + [ + 'onInsertUrl' => $this->urlInterface->getUrl('cms/wysiwyg_images/oninsert'), + 'storeId' => $this->storeManager->getStore()->getId() + ] + ) + ); + } + + /** + * Get URL for the provided media asset path + * + * @param string $path + * @return string + * @throws NoSuchEntityException + */ + private function getUrl(string $path): string + { + return $this->storage->getThumbnailUrl($this->images->getStorageRoot() . $path); + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php new file mode 100644 index 0000000000000..273cf9e37554b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Asset.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Data\OptionSourceInterface; +use Magento\Framework\View\Element\UiComponent\ContextInterface; +use Magento\Framework\View\Element\UiComponentFactory; +use Magento\MediaContentApi\Api\GetContentByAssetIdsInterface; +use Magento\Ui\Component\Filters\FilterModifier; +use Magento\Ui\Component\Filters\Type\Select; + +/** + * Asset filter + */ +class Asset extends Select +{ + /** + * @var GetContentByAssetIdsInterface + */ + private $getContentIdentities; + + /** + * @param ContextInterface $context + * @param UiComponentFactory $uiComponentFactory + * @param FilterBuilder $filterBuilder + * @param FilterModifier $filterModifier + * @param OptionSourceInterface $optionsProvider + * @param GetContentByAssetIdsInterface $getContentIdentities + * @param array $components + * @param array $data + */ + public function __construct( + ContextInterface $context, + UiComponentFactory $uiComponentFactory, + FilterBuilder $filterBuilder, + FilterModifier $filterModifier, + OptionSourceInterface $optionsProvider = null, + GetContentByAssetIdsInterface $getContentIdentities, + array $components = [], + array $data = [] + ) { + $this->uiComponentFactory = $uiComponentFactory; + $this->filterBuilder = $filterBuilder; + parent::__construct( + $context, + $uiComponentFactory, + $filterBuilder, + $filterModifier, + $optionsProvider, + $components, + $data + ); + $this->getContentIdentities = $getContentIdentities; + } + + /** + * Apply filter + * + * @return void + */ + public function applyFilter() + { + if (isset($this->filterData[$this->getName()])) { + $ids = is_array($this->filterData[$this->getName()]) + ? $this->filterData[$this->getName()] + : [$this->filterData[$this->getName()]]; + $filter = $this->filterBuilder->setConditionType('in') + ->setField($this->_data['config']['identityColumn']) + ->setValue($this->getEntityIdsByAsset($ids)) + ->create(); + + $this->getContext()->getDataProvider()->addFilter($filter); + } + } + + /** + * Return entity ids by assets ids. + * + * @param array $ids + */ + private function getEntityIdsByAsset(array $ids): string + { + if (!empty($ids)) { + $categoryIds = []; + $data = $this->getContentIdentities->execute($ids); + foreach ($data as $identity) { + if ($identity->getEntityType() === $this->_data['config']['entityType']) { + $categoryIds[] = $identity->getEntityId(); + } + } + return implode(',', $categoryIds); + } + return ''; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php new file mode 100644 index 0000000000000..31c658a6c4208 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Status.php @@ -0,0 +1,27 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Status filter options + */ +class Status implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + ['value' => '1', 'label' => __('Enabled')], + ['value' => '0', 'label' => __('Disabled')] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php new file mode 100644 index 0000000000000..f60124a6cf933 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/Store.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Store\Ui\Component\Listing\Column\Store\Options as StoreOptions; + +/** + * Store Options for content field + */ +class Store extends StoreOptions +{ + /** + * All Store Views value + */ + private const ALL_STORE_VIEWS = '0'; + + /** + * Get options + * + * @return array + */ + public function toOptionArray() + { + if ($this->options !== null) { + return $this->options; + } + + $this->currentOptions['All Store Views']['label'] = __('All Store Views'); + $this->currentOptions['All Store Views']['value'] = self::ALL_STORE_VIEWS; + + $this->generateCurrentOptions(); + + $this->options = array_values($this->currentOptions); + + return $this->options; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php new file mode 100644 index 0000000000000..e638fb7e86625 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Filters/Options/UsedIn.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options; + +use Magento\Framework\Data\OptionSourceInterface; + +/** + * Used in filter options + */ +class UsedIn implements OptionSourceInterface +{ + /** + * @inheritdoc + */ + public function toOptionArray(): array + { + return [ + 'cms_page' => [ + 'value' => 'cms_page', + 'label' => 'Pages' + ], + 'catalog_category' => [ + 'value' => 'catalog_category', + 'label' => 'Categories' + ], + 'cms_block' => [ + 'value' => 'cms_block', + 'label' => 'Blocks' + ], + 'catalog_product' => [ + 'value' => 'catalog_product', + 'label' => 'Products' + ], + 'not_used' => [ + 'value' => 'not_used', + 'label' => 'Not used anywhere' + ] + ]; + } +} diff --git a/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php new file mode 100644 index 0000000000000..160097967165d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/Ui/Component/Listing/Provider.php @@ -0,0 +1,88 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\MediaGalleryUi\Ui\Component\Listing; + +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface as FetchStrategy; +use Magento\Framework\Data\Collection\EntityFactoryInterface as EntityFactory; +use Magento\Framework\Event\ManagerInterface as EventManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult; +use Magento\MediaGalleryApi\Api\Data\AssetInterface; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface; +use Magento\MediaGalleryApi\Api\Data\KeywordInterface; +use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; +use Psr\Log\LoggerInterface as Logger; + +class Provider extends SearchResult +{ + /** + * @var GetAssetsKeywordsInterface + */ + private $getAssetKeywords; + + /** + * @param EntityFactory $entityFactory + * @param Logger $logger + * @param FetchStrategy $fetchStrategy + * @param EventManager $eventManager + * @param GetAssetsKeywordsInterface $getAssetKeywords + * @param string $mainTable + * @param null|string $resourceModel + * @param null|string $identifierName + * @param null|string $connectionName + * @throws LocalizedException + */ + public function __construct( + EntityFactory $entityFactory, + Logger $logger, + FetchStrategy $fetchStrategy, + EventManager $eventManager, + GetAssetsKeywordsInterface $getAssetKeywords, + $mainTable = 'media_gallery_asset', + $resourceModel = null, + $identifierName = null, + $connectionName = null + ) { + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $mainTable, + $resourceModel, + $identifierName, + $connectionName + ); + $this->getAssetKeywords = $getAssetKeywords; + } + + /** + * @inheritdoc + */ + public function getData() + { + $data = parent::getData(); + $keywords = []; + foreach ($this->_items as $asset) { + $keywords[$asset->getId()] = array_map(function (AssetKeywordsInterface $assetKeywords) { + return array_map(function (KeywordInterface $keyword) { + return $keyword->getKeyword(); + }, $assetKeywords->getKeywords()); + }, $this->getAssetKeywords->execute([$asset->getId()])); + } + + /** @var AssetInterface $asset */ + foreach ($data as $key => $asset) { + $data[$key]['thumbnail_url'] = $asset['path']; + $data[$key]['content_type'] = strtoupper(str_replace('image/', '', $asset['content_type'])); + $data[$key]['preview_url'] = $asset['path']; + $data[$key]['keywords'] = isset($keywords[$asset['id']]) ? implode(",", $keywords[$asset['id']]) : ''; + $data[$key]['source'] = empty($asset['source']) ? __('Local') : $asset['source']; + } + return $data; + } +} diff --git a/app/code/Magento/MediaGalleryUi/composer.json b/app/code/Magento/MediaGalleryUi/composer.json new file mode 100644 index 0000000000000..f4701306eb369 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/composer.json @@ -0,0 +1,30 @@ +{ + "name": "magento/module-media-gallery-ui", + "description": "Magento module responsible for the media gallery UI implementation", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-backend": "*", + "magento/module-ui": "*", + "magento/module-store": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-api": "*", + "magento/module-cms": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml new file mode 100644 index 0000000000000..552c5364f3500 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/di.xml @@ -0,0 +1,44 @@ +<?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\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField"> + <arguments> + <argument name="getAssetIdsByContentStatus" xsi:type="object">Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface</argument> + </arguments> + </type> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\FilterProcessor"> + <arguments> + <argument name="customFilters" xsi:type="array"> + <item name="path" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Directory</item> + <item name="fulltext" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Keyword</item> + <item name="entity_type" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\EntityType</item> + <item name="duplicated" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\Duplicated</item> + <item name="content_status" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + <item name="store_id" xsi:type="object">Magento\MediaGalleryUi\Model\SearchCriteria\CollectionProcessor\FilterProcessor\ContentField</item> + </argument> + </arguments> + </virtualType> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\SortingProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\PaginationProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor\JoinProcessor" /> + <virtualType name="Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor" type="Magento\Framework\Api\SearchCriteria\CollectionProcessor"> + <arguments> + <argument name="processors" xsi:type="array"> + <item name="filters" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\FilterProcessor</item> + <item name="sorting" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\SortingProcessor</item> + <item name="pagination" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\PaginationProcessor</item> + <item name="joins" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor\JoinProcessor</item> + </argument> + </arguments> + </virtualType> + <type name="Magento\MediaGalleryUi\Model\Listing\DataProvider"> + <arguments> + <argument name="collectionProcessor" xsi:type="object">Magento\MediaGalleryUi\Model\Api\SearchCriteria\CollectionProcessor</argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml new file mode 100644 index 0000000000000..92839aa75ac8b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/menu.xml @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Backend:etc/menu.xsd"> + <menu> + <add id="Magento_MediaGalleryUi::media" title="Media" translate="title" module="Magento_MediaGalleryUi" sortOrder="15" parent="Magento_Backend::content" resource="Magento_Cms::media_gallery" dependsOnConfig="system/media_gallery/enabled"/> + <add id="Magento_MediaGalleryUi::media_gallery" title="Media Gallery" translate="title" module="Magento_MediaGalleryUi" sortOrder="0" parent="Magento_MediaGalleryUi::media" action="media_gallery/media/index" resource="Magento_Cms::media_gallery"/> + </menu> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml new file mode 100644 index 0000000000000..11a555e16e957 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/routes.xml @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="admin"> + <route id="media_gallery" frontName="media_gallery"> + <module name="Magento_MediaGalleryUi" before="Magento_Backend" /> + </route> + </router> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml new file mode 100644 index 0000000000000..77544b42e899a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/adminhtml/system.xml @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd"> + <system> + <section id="system"> + <group id="media_gallery" translate="label" type="text" sortOrder="1000" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enhanced Media Gallery</label> + <field id="enabled" translate="label" type="select" sortOrder="10" showInDefault="1" showInWebsite="0" showInStore="0"> + <label>Enabled</label> + <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> + <config_path>system/media_gallery/enabled</config_path> + </field> + </group> + </section> + </system> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/config.xml b/app/code/Magento/MediaGalleryUi/etc/config.xml new file mode 100644 index 0000000000000..fe8e73c406e59 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/config.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd"> + <default> + <system> + <media_gallery> + <enabled>0</enabled> + </media_gallery> + </system> + </default> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/di.xml b/app/code/Magento/MediaGalleryUi/etc/di.xml new file mode 100644 index 0000000000000..56ccf7c1aa727 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/di.xml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <preference for="Magento\MediaGalleryUiApi\Api\ConfigInterface" type="Magento\MediaGalleryUi\Model\Config"/> + <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> + <arguments> + <argument name="collections" xsi:type="array"> + <item name="media_gallery_listing_data_source" xsi:type="string">Magento\MediaGalleryUi\Ui\Component\Listing\Provider</item> + </argument> + </arguments> + </type> + <virtualType name="mediaGallerySearchResult" type="Magento\Framework\View\Element\UiComponent\DataProvider\SearchResult"> + <arguments> + <argument name="mainTable" xsi:type="string">media_gallery_asset_grid</argument> + <argument name="resourceModel" xsi:type="string">Magento\MediaGalleryUi\Model\ResourceModel\Grid\Asset</argument> + </arguments> + </virtualType> + <type name="Magento\Cms\Model\Wysiwyg\Images\Storage"> + <arguments> + <argument name="resizeParameters" xsi:type="array"> + <item name="height" xsi:type="number">200</item> + <item name="width" xsi:type="number">200</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Model\Directories\GetFolderTree"> + <arguments> + <argument name="path" xsi:type="string">media</argument> + </arguments> + </type> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProvider\Type"> + <arguments> + <argument name="types" xsi:type="array"> + <item name="image" xsi:type="string">Image</item> + </argument> + </arguments> + </type> + <type name="Magento\MediaGallerySynchronizationApi\Model\ImportFilesComposite"> + <plugin name="createMediaGalleryThumbnails" type="Magento\MediaGalleryUi\Plugin\CreateThumbnails"/> + </type> + <type name="Magento\MediaGalleryUi\Model\AssetDetailsProviderPool"> + <arguments> + <argument name="detailsProviders" xsi:type="array"> + <item name="10" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Type</item> + <item name="20" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\CreatedAt</item> + <item name="30" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\UpdatedAt</item> + <item name="40" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Width</item> + <item name="50" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Height</item> + <item name="60" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\Size</item> + <item name="70" xsi:type="object">Magento\MediaGalleryUi\Model\AssetDetailsProvider\UsedIn</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/MediaGalleryUi/etc/module.xml b/app/code/Magento/MediaGalleryUi/etc/module.xml new file mode 100644 index 0000000000000..0deede3e6aad0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/etc/module.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryUi"> + <sequence> + <module name="Magento_Cms" /> + </sequence> + </module> +</config> diff --git a/app/code/Magento/MediaGalleryUi/i18n/en_US.csv b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv new file mode 100644 index 0000000000000..1882665ce8033 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/i18n/en_US.csv @@ -0,0 +1,8 @@ +"Enhanced Media Gallery","Enhanced Media Gallery" +Enabled,Enabled +All,All +Directory,Directory +"Uploaded Date","Uploaded Date" +"Modification Date","Modification Date" +Overlay,Overlay +"Thumbnail Image","Thumbnail Image" diff --git a/app/code/Magento/MediaGalleryUi/registration.php b/app/code/Magento/MediaGalleryUi/registration.php new file mode 100644 index 0000000000000..e1d321c5a8ff3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUi', __DIR__); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml new file mode 100644 index 0000000000000..f41c0f91b2249 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_index_index.xml @@ -0,0 +1,32 @@ +<?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 name="media.gallery.container" + class="Magento\Backend\Block\Template" + template="Magento_MediaGalleryUi::container.phtml" + aclResource="Magento_Cms::media_gallery"> + <container name="gallery.actions" htmlTag="div" htmlClass="page-main-actions"> + <block name="page.actions.toolbar" template="Magento_Backend::pageactions.phtml"/> + </container> + <uiComponent name="media_gallery_listing"/> + <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details.phtml"> + <arguments> + <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + </arguments> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </block> + </container> +</layout> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml new file mode 100644 index 0000000000000..7750f22b39ce7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/layout/media_gallery_media_index.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer htmlTag="div" htmlClass="media-gallery-container" name="content"> + <uiComponent name="standalone_media_gallery_listing"/> + <block name="image.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_details_standalone.phtml"> + <arguments> + <argument name="imageDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + </arguments> + </block> + <block name="image.edit.details" class="Magento\Backend\Block\Template" template="Magento_MediaGalleryUi::image_edit_details_standalone.phtml"> + <arguments> + <argument name="imageEditDetailsUrl" xsi:type="url" path="media_gallery/image/details"/> + <argument name="saveDetailsUrl" xsi:type="url" path="media_gallery/image/saveDetails"/> + </arguments> + </block> + </referenceContainer> + </body> +</page> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml new file mode 100644 index 0000000000000..5b905ea97d64a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/container.phtml @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength + +?> + +<div class="media-gallery-container"> + <?= $block->getChildHtml(); ?> +</div> + +<script type="text/x-magento-init"> + { + ".media-gallery-container": { + "Magento_Ui/js/core/app": { + "components": { + "media_gallery_container": { + "component": "Magento_MediaGalleryUi/js/container", + "containerSelector": ".media-gallery-container", + "masonryComponentPath": "media_gallery_listing.media_gallery_listing.media_gallery_columns" + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml new file mode 100644 index 0000000000000..ba2033478afa1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details.phtml @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; +use Magento\Framework\Escaper; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var Escaper $escaper */ + +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-image-actions" + data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "media_gallery_listing.media_gallery_listing.messages" + } + } + } + }, + "#media-gallery-image-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", + "handler": "deleteImageAction", + "name": "delete", + "classes": "action-default scalable delete action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Add Image')); ?>", + "handler": "addImage", + "name": "add-image", + "classes": "scalable action-primary add-image-action" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml new file mode 100644 index 0000000000000..9fc0e749ac888 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_details_standalone.phtml @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Image Details')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-image-actions" + data-bind="scope: 'mediaGalleryImageActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-details-messages" data-bind="scope: 'mediaGalleryImageDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <div id="media-gallery-image-details" data-bind="scope: 'mediaGalleryImageDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-details", + "imageDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageDetailsUrl')); ?>", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + } + }, + "#media-gallery-image-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-image-details-modal", + "modalWindowSelector": ".media-gallery-image-details", + "mediaGalleryImageDetailsName": "mediaGalleryImageDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Edit Details')); ?>", + "handler": "editImageAction", + "name": "edit", + "classes": "action-default scalable edit action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Delete Image')); ?>", + "handler": "deleteImageAction", + "name": "delete", + "classes": "action-default scalable delete action-quaternary" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml new file mode 100644 index 0000000000000..c2b7e66cc89bd --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details.phtml @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-edit-image-actions" + data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + }, + "#media-gallery-image-edit-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-edit-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + } + } + } +</script> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml new file mode 100644 index 0000000000000..ec48ed8bb9053 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/templates/image_edit_details_standalone.phtml @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Backend\Block\Template; + +// phpcs:disable Magento2.Files.LineLength, Generic.Files.LineLength +/** @var Template $block */ +/** @var \Magento\Framework\Escaper $escaper */ +?> + +<div class="media-gallery-edit-image-details-modal" + data-bind="mageInit: { + 'Magento_Ui/js/modal/modal': { + type: 'slide', + buttons: [], + modalClass: 'media-gallery-edit-image-details', + title: '<?= $escaper->escapeHtmlAttr(__('Edit Image')); ?>' + } + }"> + <div class="page-main-actions"> + <div class="page-actions"> + <div class="page-actions-inner"> + <div class="page-action-buttons" id="media-gallery-edit-image-actions" + data-bind="scope: 'mediaGalleryImageEditActions'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </div> + </div> + </div> + <div id="media-gallery-image-edit-details-messages" data-bind="scope: 'mediaGalleryEditDetailsMessages'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + <form data-bind="mageInit:{'validation':{}}" id="image-edit-details-form" method="post" enctype="multipart/form-data"> + <div id="media-gallery-image-edit-details" data-bind="scope: 'mediaGalleryEditDetails'"> + <!-- ko template: getTemplate() --><!-- /ko --> + </div> + </form> +</div> + +<script type="text/x-magento-init"> + { + "#media-gallery-image-edit-details": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetails": { + "component": "Magento_MediaGalleryUi/js/image/image-edit", + "imageEditDetailsUrl": "<?= $escaper->escapeJs($block->getData('imageEditDetailsUrl')); ?>", + "saveDetailsUrl": "<?= $escaper->escapeJs($block->getData('saveDetailsUrl')); ?>", + "mediaGridMessages": "standalone_media_gallery_listing.standalone_media_gallery_listing.messages" + } + } + }, + "Magento_MediaGalleryUi/js/validation/validate-image-title": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-description": {}, + "Magento_MediaGalleryUi/js/validation/validate-image-keyword": {} + }, + "#media-gallery-image-edit-details-messages": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryEditDetailsMessages": { + "component": "Magento_MediaGalleryUi/js/grid/messages" + } + } + } + }, + "#media-gallery-edit-image-actions": { + "Magento_Ui/js/core/app": { + "components": { + "mediaGalleryImageEditActions": { + "component": "Magento_MediaGalleryUi/js/image/image-actions", + "modalSelector": ".media-gallery-edit-image-details-modal", + "modalWindowSelector": ".media-gallery-edit-image-details", + "mediaGalleryEditDetailsName": "mediaGalleryEditDetails", + "imageModelName" : "standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url", + "actionsList": [ + { + "title": "<?= $escaper->escapeJs(__('Cancel')); ?>", + "handler": "closeModal", + "name": "cancel", + "classes": "action-default scalable cancel action-quaternary" + }, + { + "title": "<?= $escaper->escapeJs(__('Save')); ?>", + "handler": "saveImageDetailsAction", + "name": "save", + "classes": "action-default scalable save action-quaternary" + } + ] + } + } + } + } + } +</script> + + diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml new file mode 100644 index 0000000000000..86c8590bb4860 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_block_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">cms_block</item> + <item name="identityColumn" xsi:type="string">block_id</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml new file mode 100644 index 0000000000000..58881a8c9de6c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/cms_page_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">cms_page</item> + <item name="identityColumn" xsi:type="string">page_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml new file mode 100644 index 0000000000000..49206043725f9 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/media_gallery_listing.xml @@ -0,0 +1,401 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <buttons> + <button name="add_selected"> + <param name="on_click" xsi:type="string">return false;</param> + <param name="sort_order" xsi:type="number">110</param> + <class>action-primary no-display media-gallery-add-selected</class> + <label translate="true">Add Selected</label> + </button> + <button name="cancel"> + <param name="on_click" xsi:type="string">MediabrowserUtility.closeDialog();</param> + <param name="sort_order" xsi:type="number">1</param> + <class>cancel action-quaternary</class> + <label translate="true">Cancel</label> + </button> + <button name="upload_image"> + <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> + <class>action-add scalable media-gallery-actions-buttons</class> + <param name="sort_order" xsi:type="number">20</param> + <label translate="true">Upload Image</label> + </button> + <button name="delete_folder"> + <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> + <param name="disabled" xsi:type="string">disabled</param> + <param name="sort_order" xsi:type="number">30</param> + <class>action-default scalable media-gallery-actions-buttons</class> + <label translate="true">Delete Folder</label> + </button> + <button name="create_folder"> + <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> + <param name="sort_order" xsi:type="number">10</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Create Folder</label> + </button> + <button name="delete_massaction"> + <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> + <param name="sort_order" xsi:type="number">50</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Images...</label> + </button> + </buttons> + <spinner>media_gallery_columns</spinner> + <deps> + <dep>media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + </settings> + <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="source" provider="${ $.parentName }" sortOrder="60"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Source\Options"/> + <label translate="true">Source</label> + <dataScope>source</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + media_gallery_listing.media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <container name="media_gallery_massactions" + displayArea="sorting" + sortOrder="10" + component="Magento_MediaGalleryUi/js/grid/massaction/massactions" + template="Magento_MediaGalleryUi/grid/massactions/count" > + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + template="Magento_MediaGalleryUi/grid/directories/directoryTree" + component="Magento_MediaGalleryUi/js/directory/directoryTree"/> + <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">media_gallery_listing.media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">media_gallery_listing.media_gallery_listing.messages</item> + <item name="imageModelname" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">media_gallery_listing.media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploader" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + media_gallery_listing.media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml new file mode 100644 index 0000000000000..2b7d9fde3b9ff --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/product_listing.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <listingToolbar name="listing_top"> + <filters name="listing_filters"> + <filterSelect + name="asset_id" + provider="${ $.parentName }" + sortOrder="10" + class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Asset" + component="Magento_Ui/js/form/element/ui-select" + template="Magento_MediaGalleryUi/grid/filters/elements/ui-select"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="entityType" xsi:type="string">catalog_product</item> + <item name="identityColumn" xsi:type="string">entity_id</item> + <item name="filterOptions" xsi:type="boolean">true</item> + <item name="searchOptions" xsi:type="boolean">true</item> + <item name="filterPlaceholder" xsi:type="string" translate="true">Asset Title</item> + <item name="emptyOptionsHtml" xsi:type="string" translate="true">Start typing to find assets</item> + <item name="searchUrl" xsi:type="url" path="media_gallery/asset/search" /> + <item name="levelsVisibility" xsi:type="number">1</item> + </item> + </argument> + <settings> + <caption translate="true">– Please Select assets –</caption> + <label translate="true">Asset</label> + <dataScope>asset_id</dataScope> + </settings> + </filterSelect> + </filters> + </listingToolbar> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml new file mode 100644 index 0000000000000..655178c104492 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/ui_component/standalone_media_gallery_listing.xml @@ -0,0 +1,388 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<listing xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Ui:etc/ui_configuration.xsd"> + <argument name="data" xsi:type="array"> + <item name="js_config" xsi:type="array"> + <item name="provider" xsi:type="string"> + standalone_media_gallery_listing.media_gallery_listing_data_source + </item> + </item> + </argument> + <settings> + <spinner>media_gallery_columns</spinner> + <deps> + <dep>standalone_media_gallery_listing.media_gallery_listing_data_source</dep> + </deps> + <buttons> + <button name="delete_folder"> + <param name="on_click" xsi:type="string">jQuery('#delete_folder').trigger('delete_folder');</param> + <param name="disabled" xsi:type="string">disabled</param> + <param name="sort_order" xsi:type="number">20</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Folder</label> + </button> + <button name="create_folder"> + <param name="on_click" xsi:type="string">jQuery('#create_folder').trigger('create_folder');</param> + <param name="sort_order" xsi:type="number">30</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Create Folder</label> + </button> + <button name="delete_massaction"> + <param name="on_click" xsi:type="string">jQuery(window).trigger('massAction.MediaGallery')</param> + <param name="sort_order" xsi:type="number">50</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Delete Images...</label> + </button> + <button name="upload_image"> + <param name="on_click" xsi:type="string">jQuery('#image-uploader-input').click();</param> + <class>action-default scalable add media-gallery-actions-buttons</class> + <label translate="true">Upload Image</label> + </button> + </buttons> + </settings> + <dataSource name="media_gallery_listing_data_source" component="Magento_Ui/js/grid/provider"> + <settings> + <storageConfig> + <param name="indexField" xsi:type="string">id</param> + </storageConfig> + <updateUrl path="mui/index/render"/> + </settings> + <aclResource>Magento_Cms::media_gallery</aclResource> + <dataProvider class="Magento\MediaGalleryUi\Model\Listing\DataProvider" name="media_gallery_listing_data_source"> + <settings> + <requestFieldName>id</requestFieldName> + <primaryFieldName>id</primaryFieldName> + </settings> + </dataProvider> + </dataSource> + <container name="messages" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/messages"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="messageDelay" xsi:type="number">10</item> + </item> + </argument> + </container> + <listingToolbar name="listing_top" template="Magento_MediaGalleryUi/grid/toolbar"> + <bookmark name="bookmarks"/> + <filterSearch name="fulltext" /> + <filters name="listing_filters"> + <filterInput name="path" provider="${ $.parentName }" sortOrder="2000"> + <settings> + <visible>false</visible> + <dataScope>path</dataScope> + <label translate="true">Directory</label> + </settings> + </filterInput> + <filterRange name="created_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="10"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Uploaded Date</label> + <dataScope>created_at</dataScope> + </settings> + </filterRange> + <filterRange name="updated_at" + class="Magento\Ui\Component\Filters\Type\Date" + provider="${ $.parentName }" + template="ui/grid/filters/elements/group" sortOrder="20"> + <settings> + <rangeType>date</rangeType> + <label translate="true">Modification Date</label> + <dataScope>updated_at</dataScope> + </settings> + </filterRange> + <filterSelect name="entity_type" provider="${ $.parentName }" sortOrder="210" component="Magento_Ui/js/form/element/ui-select" template="ui/grid/filters/elements/ui-select"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\UsedIn"/> + <label translate="true">Show Images Used In</label> + <dataScope>entity_type</dataScope> + </settings> + </filterSelect> + <filterSelect name="source" provider="${ $.parentName }" sortOrder="60"> + <settings> + <caption translate="true">All</caption> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Source\Options"/> + <label translate="true">Source</label> + <dataScope>source</dataScope> + </settings> + </filterSelect> + <filterSelect name="content_status" provider="${ $.parentName }" sortOrder="220"> + <settings> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Status"/> + <label translate="true">Content Status</label> + <caption>All</caption> + <dataScope>content_status</dataScope> + </settings> + </filterSelect> + <filterSelect name="store_id" provider="${ $.parentName }" sortOrder="200"> + <settings> + <captionValue>0</captionValue> + <options class="Magento\MediaGalleryUi\Ui\Component\Listing\Filters\Options\Store"/> + <label translate="true">Store View</label> + <dataScope>store_id</dataScope> + <imports> + <link name="visible">componentType = column, index = ${ $.index }:visible</link> + </imports> + </settings> + </filterSelect> + <filterInput + name="duplicated" + provider="${ $.parentName }" + sortOrder="300" + template="Magento_MediaGalleryUi/grid/filter/checkbox" + component="Magento_Ui/js/form/element/single-checkbox"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="description" xsi:type="string" translate="true">Show duplicates</item> + <item name="valueMap" xsi:type="array"> + <item name="true" xsi:type="string">Yes</item> + </item> + </item> + </argument> + <settings> + <dataScope>duplicated</dataScope> + <label translate="true">Show duplicates</label> + </settings> + </filterInput> + </filters> + <paging name="listing_paging"> + <settings> + <options> + <option name="32" xsi:type="array"> + <item name="value" xsi:type="number">32</item> + <item name="label" xsi:type="string">32</item> + </option> + <option name="48" xsi:type="array"> + <item name="value" xsi:type="number">48</item> + <item name="label" xsi:type="string">48</item> + </option> + <option name="64" xsi:type="array"> + <item name="value" xsi:type="number">64</item> + <item name="label" xsi:type="string">64</item> + </option> + </options> + <pageSize>32</pageSize> + </settings> + </paging> + <container + name="sorting" + provider="standalone_media_gallery_listing.media_gallery_listing_data_source" + displayArea="sorting" + sortOrder="20" + component="Magento_MediaGalleryUi/js/grid/sortBy"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="deps" xsi:type="array"> + <item name="0" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns + </item> + </item> + </item> + </argument> + </container> + <container name="media_gallery_massactions" + displayArea="sorting" + sortOrder="10" + component="Magento_MediaGalleryUi/js/grid/massaction/massactions" + template="Magento_MediaGalleryUi/grid/massactions/count" > + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="checkboxComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.massaction_checkbox</item> + <item name="imageModelName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_columns.thumbnail_url</item> + <item name="mediaGalleryProvider" xsi:type="string">standalone_media_gallery_listing.media_gallery_listing_data_source</item> + </item> + </argument> + </container> + </listingToolbar> + <container name="media_gallery_directories" + class="Magento\MediaGalleryUi\Ui\Component\DirectoriesTree" + template="Magento_MediaGalleryUi/grid/directories/directoryTree" + component="Magento_MediaGalleryUi/js/directory/directoryTree"/> + <columns name="media_gallery_columns" component="Magento_MediaGalleryUi/js/grid/masonry"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="containerId" xsi:type="string">media-gallery-masonry-grid</item> + </item> + </argument> + <column name="source" component="Magento_Ui/js/grid/columns/overlay" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\SourceIconProvider"> + <settings> + <label translate="true">Source</label> + <visible>false</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="thumbnail_url" component="Magento_MediaGalleryUi/js/grid/columns/image" class="Magento\MediaGalleryUi\Ui\Component\Listing\Columns\Url"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="fields" xsi:type="array"> + <item name="url" xsi:type="string">thumbnail_url</item> + </item> + <item name="url" xsi:type="string">thumbnail_url</item> + <item name="deleteImageUrl" xsi:type="url" path="media_gallery/image/delete"/> + <item name="massactionComponentName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.media_gallery_massactions</item> + <item name="messagesName" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.messages</item> + <item name="mediaGalleryDirectoryComponent" xsi:type="string">standalone_media_gallery_listing.standalone_media_gallery_listing.media_gallery_directories</item> + </item> + </argument> + <settings> + <label translate="true">Thumbnail Image</label> + <visible>true</visible> + <sortable>false</sortable> + </settings> + </column> + <column name="newest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Newest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="oldest_first"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">created_at</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Oldest first</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="created_at"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Uploaded Date</label> + <dataType>date</dataType> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="path"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_desc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Descending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="directory_asc"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">path</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Directory: Ascending</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="title"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="excluded" xsi:type="boolean">true</item> + </item> + </item> + </argument> + <settings> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_az"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: A to Z</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + <column name="name_za"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sort_by" xsi:type="array"> + <item name="field" xsi:type="string">title</item> + <item name="direction" xsi:type="string">desc</item> + </item> + </item> + </argument> + <settings> + <label translate="true">Name: Z to A</label> + <visible>false</visible> + <sortable>true</sortable> + </settings> + </column> + </columns> + <container name="media_gallery_image_uploader" + class="Magento\MediaGalleryUi\Ui\Component\ImageUploaderStandAlone" + template="Magento_MediaGalleryUi/image-uploader" + component="Magento_MediaGalleryUi/js/image-uploader"> + <argument name="data" xsi:type="array"> + <item name="config" xsi:type="array"> + <item name="sortByName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.sorting + </item> + <item name="listingPagingName" xsi:type="string"> + standalone_media_gallery_listing.standalone_media_gallery_listing.listing_top.listing_paging + </item> + </item> + </argument> + </container> +</listing> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less new file mode 100644 index 0000000000000..fc8bd49126d8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/css/source/_module.less @@ -0,0 +1,484 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +// +// Variables +// _____________________________________________ + +@color-folders-background: #a6a6a6; +@color-folders-background-selected: #cdecf6; +@color-folders-border: #7185f5; +@color-masonry-overlay: #d9631c; +@color-masonry-grey: #9e9e9e; +@color-masonry-white: #e1e1e1; +@color-masonry-steelblue: #4682b4; +@color-media-gallery-buttons-background: #e3e3e3; +@color-media-gallery-buttons-border: #adadad; +@color-media-gallery-buttons-text: #514943; +@color-media-gallery-checkbox-background: #eee; + +& when (@media-common = true) { + + .media-gallery-delete-image-action, + .delete-folder-confirmation-popup { + + .modal-content { + word-wrap: anywhere; + } + } + + .media-gallery-asset-ui-select-filter, + .edit-image-details { + + .admin__action-multiselect-crumb { + max-width: 70%; + overflow: hidden; + text-overflow: ellipsis + } + + .admin__action-multiselect-label > span { + display: block; + margin-top: -2px; + max-height: 18px; + max-width: 70%; + overflow: hidden; + padding-left: 23px; + position: absolute; + text-overflow: ellipsis; + } + } + + .media-gallery-asset-ui-select-filter, + .edit-image-details { + .admin__action-multiselect-item-path { + float: right; + max-height: 70px; + max-width: 70px; + } + + .admin__action-multiselect-label { + display: inline-block; + width: 100%; + } + } + + .page-actions-buttons > button.no-display { + display: none; + } + + .page-actions-buttons > button.media-gallery-actions-buttons, + .page-actions .page-actions-buttons > button.media-gallery-actions-buttons:focus, + .page-actions-buttons > button.media-gallery-actions-buttons:hover { + background-color: @color-media-gallery-buttons-background; + border-color: @color-media-gallery-buttons-border; + color: @color-media-gallery-buttons-text; + } + + .mediagallery-massaction-checkbox { + background-color: @color-media-gallery-checkbox-background; + border-radius: 4px; + height: 40px; + input[type='checkbox'] { + margin-left: 10px; + margin-top: 11px; + } + margin-left: 15px; + margin-top: 10px; + position: absolute; + width: 40px; + z-index: 10; + } + + .mediagallery-massaction-items-count { + display: inline-block; + margin-left: -15px; + padding-right: 20px; + } + + .media-gallery-container { + + .masonry-image-grid .no-data-message-container, + .masonry-image-grid .error-message-container { + left: 50%; + margin-right: -50%; + position: sticky; + top: 50%; + } + + .admin__action-dropdown-wrap._active .admin__action-dropdown-text::after { + margin-right: 6px; + } + + .admin__data-grid-action-bookmarks .admin__action-dropdown-menu { + left: auto; + right: 0; + } + + .page-main-actions { + .page-actions { + .media-gallery-add-selected { + order: unset; + } + } + + & > .page-actions { + & > button.no-display { + display: none; + } + } + } + .jstree-default .jstree-hovered { + background: @color-folders-background; + border-color: @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .jstree-default .jstree-leaf a .jstree-icon { + background-position: -52px -16px; + } + + + .jstree-default a .jstree-icon { + background-position: -52px -16px; + } + + .jstree-default .jstree-no-dots .jstree-open > a > ins { + background-position: -52px -38px; + height: 20px; + width: 29px; + } + + .jstree a > ins { + float: left; + height: 22px; + margin-top: -3px; + width: 20px; + } + + .jstree-default .jstree-no-dots .jstree-leaf > ins { + background-image: none; + } + + .jstree-default ins { + background-image: url("@{baseDir}Magento_MediaGalleryUi/images/d.png"); + } + + .jstree a { + height: 30px; + margin: 1px; + padding-left: 6px; + padding-top: 6px; + width: 100%; + } + + .jstree-default .jstree-clicked { + background: @color-folders-background-selected; + border: .14em solid @color-folders-border; + border-radius: 6px; + padding-top: 6px; + } + + .masonry-image-overlay { + background-color: @color-masonry-overlay; + float: right; + font-size: 11px; + margin-left: 120px; + margin-top: 170px; + padding: .3rem; + pointer-events: none; + position: relative; + } + + .media-gallery-image-details { + float: left; + list-style: none; + margin-bottom: 0; + position: absolute; + width: 89%; + + .name { + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + display: -webkit-box; + font-size: 15px; + font-weight: bold; + line-height: 20px; + max-height: 50px; + overflow: hidden; + padding-bottom: 2px; + text-overflow: ellipsis; + white-space: pre-line; + word-wrap: anywhere; + word-wrap: break-word; + @media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { + white-space: nowrap; + } + } + + .type { + display: inline-block; + font-size: 12px; + padding-bottom: 5px; + } + + .dimensions { + display: inline-block; + } + + .source { + display: inline-block; + } + } + + .media-gallery-image-actions { + float: right; + position: absolute; + right: 0; + width: 10%; + + .action-select-wrap { + cursor: pointer; + } + + .three-dots { + &:before { + content: url("@{baseDir}Magento_MediaGalleryUi/images/3-dots.png"); + cursor: pointer; + } + } + } + + .media-gallery-image { + height: 200px; + margin: 0 auto; + position: relative; + text-align: center; + width: 200px; + } + + .masonry-image-description { + background-color: @color-white; + min-height: 90px; + padding-top: 10px; + position: relative; + } + + .masonry-image-column { + background-color: @color-masonry-white; + width: 200px; + } + + .media-directory-container { + float: left; + padding-right: 40px; + } + + .media-gallery-image-block { + cursor: pointer; + height: 200px; + margin: 0 auto; + position: relative; + + &.selected { + border: 5px solid @color-masonry-steelblue; + } + } + + .media-gallery-image { + img { + bottom: 0; + height: auto; + left: 0; + margin: auto; + max-height: 100%; + max-width: 100%; + padding: 5px; + position: absolute; + right: 0; + top: 0; + width: auto; + } + + .action-menu { + bottom: 0; + float: right; + left: auto; + top: auto; + z-index: 100; + } + } + + .media-gallery-source-icon { + margin-bottom: -6px; + width: 29px; + } + + .masonry-image-grid { + align-items: first baseline; + display: grid; + grid-template-columns: repeat(auto-fill, 210px); + justify-content: end; + margin: 10px 0; + position: relative; + } + + .admin__data-grid-filters .admin__form-field { + .action-select-wrap { + .action-menu { + width: 110%; + } + .admin__action-multiselect-search-label { + right: 1.5rem; + } + } + + .action-close { + padding: 0; + &:before { + font-size: 6px; + } + } + } + } + + .media-gallery-image-details-modal, + .media-gallery-edit-image-details-modal { + + .admin__action-multiselect-crumb { + .action-close { + padding: 0; + + &:before { + font-size: .5em; + } + } + } + + .edit-image-details { + padding: 50px; + } + + .path-display { + margin-top: 8px; + } + + .page-action-buttons { + float: right; + } + + .image-type { + .media-gallery-source-icon { + margin-bottom: -6px; + width: 29px; + } + + .type { + color: @color-very-dark-gray; + } + } + + .image-details { + .lib-vendor-prefix-display(); + + .image-details-image { + img { + max-height: 650px; + } + } + + .image-details-sidebar { + .lib-vendor-prefix-flex-grow(1); + margin-top: 0; + padding-left: 40px; + + .image-details-section { + margin-bottom: 40px; + max-width: 400px; + min-width: 290px; + word-wrap: anywhere; + .lib-clearfix(); + } + + h3.image-title { + font-weight: bold; + line-height: 1.5; + } + + .attributes { + .attribute { + &:not(:last-child) { + margin-bottom: 20px; + padding-bottom: 20px; + } + + & > * { + float: left; + margin-left: -1px; + width: 50%; + } + + .value { + display: inline; + float: right; + margin-left: 1px; + } + + .title { + color: @color-very-dark-gray; + } + } + } + + .tags { + .tags-list { + margin-bottom: 10px; + + .show-more-item { + display: none; + } + + &.show-all-tags { + margin-bottom: 0; + + .show-more-item { + display: inline; + } + + & + .show-more-link-container { + display: none; + } + } + } + } + } + } + } + .masonry-image-sortby { + display: inline-block; + } + + .masonry-results-number { + display: inline-block; + margin-right: 1.4rem; + } +} + +.media-width(@extremum, @break) when (@extremum = 'max') and (@break = @screen__m) { + .media-gallery-image-details-modal { + .image-details { + display: block; + + .image-details-sidebar { + margin-top: 20px; + padding-left: 0; + } + + .image-details-image img { + max-height: 450px; + } + } + } +} diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png new file mode 100644 index 0000000000000..601ba415f2446 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/3-dots.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png new file mode 100644 index 0000000000000..db5cda9c5512b Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/Astock.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png new file mode 100644 index 0000000000000..6516e915624c3 Binary files /dev/null and b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/images/d.png differ diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js new file mode 100644 index 0000000000000..51d124ca319e6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImageWithDetailConfirmation.js @@ -0,0 +1,74 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'Magento_MediaGalleryUi/js/action/deleteImages', + 'mage/translate' +], function ($, _, getDetails, deleteImages, $t) { + 'use strict'; + + return { + + /** + * Get information about image use + * + * @param {Array} recordsIds + * @param {String} imageDetailsUrl + * @param {String} deleteImageUrl + */ + deleteImageAction: function (recordsIds, imageDetailsUrl, deleteImageUrl) { + var confirmationContent = $t('%1 Are you sure you want to delete "%2" image(s)?') + .replace('%2', Object.keys(recordsIds).length), + deferred = $.Deferred(); + + getDetails(imageDetailsUrl, recordsIds) + .then(function (imageDetails) { + confirmationContent = confirmationContent.replace( + '%1', + this.getRecordRelatedContentMessage(imageDetails) + ); + }.bind(this)).fail(function () { + confirmationContent = confirmationContent.replace('%1', ''); + }).always(function () { + deleteImages(recordsIds, deleteImageUrl, confirmationContent).then(function (status) { + deferred.resolve(status); + }).fail(function (error) { + deferred.reject(error); + }); + }); + + return deferred.promise(); + }, + + /** + * Get information about image use + * + * @param {Object|String} images + * @return {String} + */ + getRecordRelatedContentMessage: function (images) { + var usedInMessage = $t('The selected assets are used in the content of the following entities: '), + usedIn = []; + + $.each(images, function (key, image) { + $.each(image.details, function (sectionIndex, section) { + if (section.title === 'Used In' && _.isObject(section) && !_.isEmpty(section.value)) { + $.each(section.value, function (entityTypeIndex, entityTypeData) { + usedIn.push(entityTypeData.name + '(' + entityTypeData.number + ')'); + }); + } + }); + }); + + if (_.isEmpty(usedIn)) { + return ''; + } + + return usedInMessage + usedIn.join(', ') + '.'; + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js new file mode 100644 index 0000000000000..c8ddeaf3d3929 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/deleteImages.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'mage/url', + 'Magento_MediaGalleryUi/js/grid/messages', + 'Magento_Ui/js/modal/confirm', + 'mage/translate' +], function ($, _, urlBuilder, messages, confirmation, $t) { + 'use strict'; + + return function (ids, deleteUrl, confirmationContent) { + var deferred = $.Deferred(), + title = $t('Delete assets'), + cancelText = $t('Cancel'), + deleteImageText = $t('Delete'); + + /** + * Send deletion request with redords ids + * + * @param {Array} recordIds + * @param {String} serviceUrl + */ + function sendRequest(recordIds, serviceUrl) { + + $.ajax({ + type: 'POST', + url: serviceUrl, + dataType: 'json', + showLoader: true, + data: { + 'form_key': window.FORM_KEY, + 'ids': recordIds + }, + context: this, + + /** + * Success handler for deleting image + * + * @param {Object} response + */ + success: function (response) { + var message = !_.isUndefined(response.message) ? response.message : null; + + if (!response.success) { + message = message || $t('There was an error on attempt to delete the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + + deferred.reject(message); + } + + message = message || $t('You have successfully removed the images.'); + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: true, + message: message, + code: 'success' + }); + deferred.resolve(message); + }, + + /** + * Error handler for deleting image + * + * @param {Object} response + */ + error: function (response) { + var message; + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('There was an error on attempt to delete the image.'); + } else { + message = response.responseJSON.message; + } + + $(window).trigger('fileDeleted.enhancedMediaGallery', { + reload: false, + message: message, + code: 'error' + }); + deferred.reject(message); + } + }); + } + + confirmation({ + title: title, + modalClass: 'media-gallery-delete-image-action', + content: confirmationContent, + buttons: [ + { + text: cancelText, + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + deferred.resolve({ + status: 'canceled' + }); + } + }, + { + text: deleteImageText, + class: 'action-primary action-accept', + + /** + * Delete Image and close modal + */ + click: function () { + sendRequest(ids, deleteUrl); + this.closeModal(); + } + } + ] + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js new file mode 100644 index 0000000000000..ec750afff29bf --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/getDetails.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (imageDetailsUrl, imageIds) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'GET', + url: imageDetailsUrl, + dataType: 'json', + showLoader: true, + data: { + 'ids': imageIds + }, + context: this, + + /** + * Resolve with image details if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.imageDetails); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not retrieve image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js new file mode 100644 index 0000000000000..4d1120badeca0 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/action/saveDetails.js @@ -0,0 +1,56 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (saveImageDetailsUrl, data) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: saveImageDetailsUrl, + dataType: 'json', + showLoader: true, + data: data, + + /** + * Resolve with image details if success, reject with response message otherwise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not save image details.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js new file mode 100644 index 0000000000000..f6dd277fb85f5 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/container.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement', + 'jquery' +], function (Element, $) { + 'use strict'; + + return Element.extend({ + defaults: { + containerSelector: '.media-gallery-container', + masonryComponentPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns', + modules: { + masonry: '${ $.masonryComponentPath }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super(); + + $(this.containerSelector).applyBindings(); + + return this; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js new file mode 100644 index 0000000000000..cc4d759069c67 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/createDirectory.js @@ -0,0 +1,61 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (createFolderUrl, paths) { + var deferred = $.Deferred(), + message, + data = { + paths: paths + }; + + $.ajax({ + type: 'POST', + url: createFolderUrl, + dataType: 'json', + showLoader: true, + data: data, + context: this, + + /** + * Resolve if success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not create the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js new file mode 100644 index 0000000000000..06277481e1142 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/actions/deleteDirectory.js @@ -0,0 +1,60 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/translate' +], function ($, $t) { + 'use strict'; + + return function (deleteFolderUrl, path) { + var deferred = $.Deferred(), + message; + + $.ajax({ + type: 'POST', + url: deleteFolderUrl, + dataType: 'json', + showLoader: true, + data: { + path: path + }, + context: this, + + /** + * Resolve if delete folder success, reject with response message othervise + * + * @param {Object} response + */ + success: function (response) { + if (response.success) { + deferred.resolve(response.message); + + return; + } + + deferred.reject(response.message); + }, + + /** + * Extract the message and reject + * + * @param {Object} response + */ + error: function (response) { + + if (typeof response.responseJSON === 'undefined' || + typeof response.responseJSON.message === 'undefined' + ) { + message = $t('Could not delete the directory.'); + } else { + message = response.responseJSON.message; + } + deferred.reject(message); + } + }); + + return deferred.promise(); + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js new file mode 100644 index 0000000000000..d7f756d8bbd90 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directories.js @@ -0,0 +1,186 @@ +/** + * Copyright © Magento, Inc. All rights reserved.g + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_Ui/js/modal/confirm', + 'Magento_Ui/js/modal/alert', + 'underscore', + 'Magento_Ui/js/modal/prompt', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'Magento_MediaGalleryUi/js/directory/actions/deleteDirectory', + 'mage/translate', + 'validation' +], function ($, Component, confirm, uiAlert, _, prompt, createDirectory, deleteDirectory, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + directoryTreeSelector: '#media-gallery-directory-tree', + deleteButtonSelector: '#delete_folder', + createFolderButtonSelector: '#create_folder', + messageDelay: 5, + messagesName: 'media_gallery_listing.media_gallery_listing.messages', + modules: { + directoryTree: '${ $.parentName }.media_gallery_directories', + messages: '${ $.messagesName }' + } + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['selectedFolder']); + this.initEvents(); + + return this; + }, + + /** + * Initialize directories events + */ + initEvents: function () { + $(this.deleteButtonSelector).on('delete_folder', function () { + this.getConfirmationPopupDeleteFolder(); + }.bind(this)); + + $(this.createFolderButtonSelector).on('create_folder', function () { + this.getPrompt({ + title: $t('New Folder Name:'), + content: '', + actions: { + /** + * Confirm action + */ + confirm: function (folderName) { + createDirectory( + this.directoryTree().createDirectoryUrl, + [this.getNewFolderPath(folderName)] + ).then(function () { + this.directoryTree().reloadJsTree().then(function () { + $(this.directoryTree().directoryTreeSelector).on('loaded.jstree', function () { + this.directoryTree().locateNode(this.getNewFolderPath(folderName)); + }.bind(this)); + }.bind(this)); + + }.bind(this)).fail(function (error) { + uiAlert({ + content: error + }); + }); + }.bind(this) + }, + buttons: [{ + text: $t('Cancel'), + class: 'action-secondary action-dismiss', + + /** + * Close modal + */ + click: function () { + this.closeModal(); + } + }, { + text: $t('Confirm'), + class: 'action-primary action-accept' + }] + }); + }.bind(this)); + }, + + /** + * Return configured path for folder creation. + * + * @param {String} folderName + * @returns {String} + */ + getNewFolderPath: function (folderName) { + var selectedFolder = _.isUndefined(this.selectedFolder()) || + _.isNull(this.selectedFolder()) ? '/' : this.selectedFolder(), + folderToCreate = selectedFolder !== '/' ? selectedFolder + '/' + folderName : folderName; + + return folderToCreate; + }, + + /** + * Return configured prompt with input field + */ + getPrompt: function (data) { + prompt({ + title: $t(data.title), + content: $t(data.content), + modalClass: 'media-gallery-folder-prompt', + validation: true, + validationRules: ['required-entry', 'validate-alphanum'], + attributesField: { + name: 'folder_name', + 'data-validate': '{required:true, validate-alphanum}', + maxlength: '128' + }, + attributesForm: { + novalidate: 'novalidate', + action: '' + }, + context: this, + actions: data.actions, + buttons: data.buttons + }); + }, + + /** + * Confirmation popup for delete folder action. + */ + getConfirmationPopupDeleteFolder: function () { + confirm({ + title: $t('Are you sure you want to delete this folder?'), + modalClass: 'delete-folder-confirmation-popup', + content: $t('The following folder is going to be deleted: %1') + .replace('%1', this.selectedFolder()), + actions: { + + /** + * Delete folder on button click + */ + confirm: function () { + deleteDirectory( + this.directoryTree().deleteDirectoryUrl, + this.selectedFolder() + ).then(function () { + this.directoryTree().removeNode(); + this.directoryTree().selectStorageRoot(); + $(window).trigger('folderDeleted.enhancedMediaGallery'); + }.bind(this)).fail(function (error) { + uiAlert({ + content: error + }); + }); + }.bind(this) + } + }); + }, + + /** + * Set inactive all nodes, adds disable state to Delete Folder Button + */ + setInActive: function () { + this.selectedFolder(null); + $(this.deleteButtonSelector).attr('disabled', true).addClass('disabled'); + }, + + /** + * Set active node, remove disable state from Delete Forlder button + * + * @param {String} folderId + */ + setActive: function (folderId) { + this.selectedFolder(folderId); + $(this.deleteButtonSelector).removeAttr('disabled').removeClass('disabled'); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js new file mode 100644 index 0000000000000..decc337e1b83c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/directory/directoryTree.js @@ -0,0 +1,477 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global Base64 */ +define([ + 'jquery', + 'uiComponent', + 'uiLayout', + 'underscore', + 'Magento_MediaGalleryUi/js/directory/actions/createDirectory', + 'jquery/jstree/jquery.jstree', + 'Magento_Ui/js/lib/view/utils/async' +], function ($, Component, layout, _, createDirectory) { + 'use strict'; + + return Component.extend({ + defaults: { + filterChipsProvider: 'componentType = filters, ns = ${ $.ns }', + directoryTreeSelector: '#media-gallery-directory-tree', + getDirectoryTreeUrl: 'media_gallery/directories/gettree', + jsTreeReloaded: null, + modules: { + directories: '${ $.name }_directories', + filterChips: '${ $.filterChipsProvider }' + }, + listens: { + '${ $.provider }:params.filters.path': 'clearFiltersHandle' + }, + viewConfig: [{ + component: 'Magento_MediaGalleryUi/js/directory/directories', + name: '${ $.name }_directories' + }] + }, + + /** + * Initializes media gallery directories component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe(['activeNode']).initView(); + + $.async( + this.directoryTreeSelector, + this, + function () { + this.renderDirectoryTree().then(function () { + this.initEvents(); + }.bind(this)); + }.bind(this)); + + return this; + }, + + /** + * Render directory tree component. + */ + renderDirectoryTree: function () { + + return this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isFolderCreated) { + if (isFolderCreated) { + this.getJsonTree().then(function (newData) { + this.createTree(newData); + }.bind(this)); + } else { + this.createTree(data); + } + }.bind(this)); + }.bind(this)); + }, + + /** + * Set jstree reloaded + * + * @param {Boolean} value + */ + setJsTreeReloaded: function (value) { + this.jsTreeReloaded = value; + }, + + /** + * Create folder by provided current_tree_path param + * + * @param {Array} directories + */ + createFolderIfNotExists: function (directories) { + var isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + currentTreePath = isMediaBrowser ? window.MediabrowserUtility.pathId : null, + deferred = $.Deferred(), + decodedPath, + pathArray; + + if (currentTreePath) { + decodedPath = Base64.idDecode(currentTreePath); + + if (!this.isDirectoryExist(directories[0], decodedPath)) { + pathArray = this.convertPathToPathsArray(decodedPath); + + $.each(pathArray, function (i, val) { + if (this.isDirectoryExist(directories[0], val)) { + pathArray.splice(i, 1); + } + }.bind(this)); + + createDirectory( + this.createDirectoryUrl, + pathArray + ).then(function () { + deferred.resolve(true); + }); + } else { + deferred.resolve(false); + } + } else { + deferred.resolve(false); + } + + return deferred.promise(); + }, + + /** + * Verify if directory exists in array + * + * @param {Array} directories + * @param {String} directoryId + */ + isDirectoryExist: function (directories, directoryId) { + var found = false; + + /** + * Recursive search in array + * + * @param {Array} data + * @param {String} id + */ + function recurse(data, id) { + var i; + + for (i = 0; i < data.length; i++) { + if (data[i].attr.id === id) { + found = data[i]; + break; + } else if (data[i].children && data[i].children.length) { + recurse(data[i].children, id); + } + } + } + + recurse(directories, directoryId); + + return found; + }, + + /** + * Convert path string to path array e.g 'path1/path2' -> ['path1', 'path1/path2'] + * + * @param {String} path + */ + convertPathToPathsArray: function (path) { + var pathsArray = [], + pathString = '', + paths = path.split('/'); + + $.each(paths, function (i, val) { + pathString += i >= 1 ? val : val + '/'; + pathsArray.push(i >= 1 ? pathString : val); + }); + + return pathsArray; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Wait for condition then call provided callback + */ + waitForCondition: function (condition, callback) { + if (condition()) { + setTimeout(function () { + this.waitForCondition(condition, callback); + }.bind(this), 100); + } else { + callback(); + } + }, + + /** + * Remove ability to multiple select on nodes + */ + overrideMultiselectBehavior: function () { + $.jstree.defaults.ui['select_range_modifier'] = false; + $.jstree.defaults.ui['select_multiple_modifier'] = false; + }, + + /** + * Handle jstree events + */ + initEvents: function () { + this.firejsTreeEvents(); + this.overrideMultiselectBehavior(); + + $(window).on('reload.MediaGallery', function () { + this.getJsonTree().then(function (data) { + this.createFolderIfNotExists(data).then(function (isCreated) { + if (isCreated) { + this.renderDirectoryTree().then(function () { + this.setJsTreeReloaded(true); + this.firejsTreeEvents(); + }.bind(this)); + } else { + this.checkChipFiltersState(); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + /** + * Fire event for jstree component + */ + firejsTreeEvents: function () { + $(this.directoryTreeSelector).on('select_node.jstree', function (element, data) { + var path = $(data.rslt.obj).data('path'); + + this.setActiveNodeFilter(path); + this.setJsTreeReloaded(false); + }.bind(this)); + + $(this.directoryTreeSelector).on('loaded.jstree', function () { + this.checkChipFiltersState(); + }.bind(this)); + + }, + + /** + * Verify directory filter on init event, select folder per directory filter state + */ + checkChipFiltersState: function () { + var currentFilterPath = this.filterChips().filters.path, + isMediaBrowser = !_.isUndefined(window.MediabrowserUtility), + currentTreePath; + + currentTreePath = this.isFiltersApplied(currentFilterPath) || !isMediaBrowser ? currentFilterPath : + Base64.idDecode(window.MediabrowserUtility.pathId); + + if (this.folderExistsInTree(currentTreePath)) { + this.locateNode(currentTreePath); + } else { + this.selectStorageRoot(); + } + }, + + /** + * Verify if directory exists in folder tree + * + * @param {String} path + */ + folderExistsInTree: function (path) { + if (!_.isUndefined(path)) { + return $('#' + path.replace(/\//g, '\\/')).length === 1; + } + + return false; + }, + + /** + * Check if need to select directory by filters state + * + * @param {String} currentFilterPath + */ + isFiltersApplied: function (currentFilterPath) { + return !_.isUndefined(currentFilterPath) && currentFilterPath !== '' && + currentFilterPath !== 'wysiwyg' && currentFilterPath !== 'catalog/category'; + }, + + /** + * Locate and higlight node in jstree by path id. + * + * @param {String} path + */ + locateNode: function (path) { + var selectedId = $(this.directoryTreeSelector).jstree('get_selected').attr('id'); + + if (path === selectedId) { + return; + } + path = path.replace(/\//g, '\\/'); + $(this.directoryTreeSelector).jstree('open_node', '#' + path); + $(this.directoryTreeSelector).jstree('select_node', '#' + path, true); + + }, + + /** + * Listener to clear filters event + */ + clearFiltersHandle: function () { + if (_.isUndefined(this.filterChips().filters.path)) { + $(this.directoryTreeSelector).jstree('deselect_all'); + this.activeNode(null); + this.directories().setInActive(); + } + }, + + /** + * Set active node filter, or deselect if the same node clicked + * + * @param {String} nodePath + */ + setActiveNodeFilter: function (nodePath) { + + if (this.activeNode() === nodePath && !this.jsTreeReloaded) { + this.selectStorageRoot(); + } else { + this.selectFolder(nodePath); + } + }, + + /** + * Remove folders selection -> select storage root + */ + selectStorageRoot: function () { + var filters = {}, + applied = this.filterChips().get('applied'); + + $(this.directoryTreeSelector).jstree('deselect_all'); + + filters = $.extend(true, filters, applied); + delete filters.path; + this.filterChips().set('applied', filters); + this.activeNode(null); + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setInActive(); + }.bind(this) + ); + + }, + + /** + * Set selected folder + * + * @param {String} path + */ + selectFolder: function (path) { + this.activeNode(path); + + this.waitForCondition( + function () { + return _.isUndefined(this.directories()); + }.bind(this), + function () { + this.directories().setActive(path); + }.bind(this) + ); + + this.applyFilter(path); + }, + + /** + * Remove active node from directory tree, and select next + */ + removeNode: function () { + $(this.directoryTreeSelector).jstree('remove'); + }, + + /** + * Apply folder filter by path + * + * @param {String} path + */ + applyFilter: function (path) { + var filters = {}, + applied = this.filterChips().get('applied'); + + filters = $.extend(true, filters, applied); + filters.path = path; + this.filterChips().set('applied', filters); + + }, + + /** + * Reload jstree and update jstree events + */ + reloadJsTree: function () { + var deferred = $.Deferred(); + + this.getJsonTree().then(function (data) { + this.createTree(data); + this.setJsTreeReloaded(true); + this.initEvents(); + deferred.resolve(); + }.bind(this)); + + return deferred.promise(); + }, + + /** + * Get json data for jstree + */ + getJsonTree: function () { + var deferred = $.Deferred(); + + $.ajax({ + url: this.getDirectoryTreeUrl, + type: 'GET', + dataType: 'json', + + /** + * Success handler for request + * + * @param {Object} data + */ + success: function (data) { + deferred.resolve(data); + }, + + /** + * Error handler for request + * + * @param {Object} jqXHR + * @param {String} textStatus + */ + error: function (jqXHR, textStatus) { + deferred.reject(); + throw textStatus; + } + }); + + return deferred.promise(); + }, + + /** + * Initialize directory tree + * + * @param {Array} data + */ + createTree: function (data) { + $(this.directoryTreeSelector).jstree({ + plugins: ['json_data', 'themes', 'ui', 'crrm', 'types', 'hotkeys'], + vcheckbox: { + 'two_state': true, + 'real_checkboxes': true + }, + 'json_data': { + data: data + }, + hotkeys: { + space: this._changeState, + 'return': this._changeState + }, + types: { + 'types': { + 'disabled': { + 'check_node': true, + 'uncheck_node': true + } + } + } + }); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js new file mode 100644 index 0000000000000..bf852d0ddae68 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image.js @@ -0,0 +1,289 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'Magento_Ui/js/grid/columns/column', + 'uiLayout', + 'underscore' +], function ($, Column, layout, _) { + 'use strict'; + + return Column.extend({ + defaults: { + bodyTmpl: 'Magento_MediaGalleryUi/grid/columns/image', + deleteImageUrl: 'media_gallery/image/delete', + addSelectedBtnSelector: '#add_selected', + deleteSelectedBtnSelector: '#delete_selected', + selected: null, + fields: { + id: 'id', + url: 'url', + alt: 'name' + }, + modules: { + actions: '${ $.name }_actions', + provider: '${ $.provider }', + messages: '${ $.messagesName }', + massaction: '${ $.massactionComponentName }' + }, + imports: { + activeDirectory: '${ $.mediaGalleryDirectoryComponent }:activeNode' + }, + listens: { + activeDirectory: 'selectDirectoryHandle', + '${ $.massactionComponentName }:massActionMode': 'updateSelected' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/columns/image/actions', + name: '${ $.name }_actions', + imageModelName: '${ $.name }' + } + ] + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + this.initView(); + $(window).on('fileDeleted.enhancedMediaGallery', this.reloadMediaGrid.bind(this)); + $(window).on('reload.MediaGallery', this.reloadGrid.bind(this)); + + return this; + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'selected' + ]); + + return this; + }, + + /** + * Is massaction mode active. + */ + isMassActionMode: function () { + return this.massaction().massActionMode(); + }, + + /** + * Returns url to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getUrl: function (record) { + return record[this.fields.url]; + }, + + /** + * Returns id to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {Number} + */ + getId: function (record) { + return record[this.fields.id]; + }, + + /** + * Update selected items per massaction mode. + */ + updateSelected: function () { + this.selected({}); + this.hideAddSelectedAndDeleteButon(); + }, + + /** + * Returns name to given record. + * + * @param {Object} record - Data to be preprocessed. + * @returns {String} + */ + getImageAlt: function (record) { + return record[this.fields.alt]; + }, + + /** + * Check if the record is currently selected + * + * @param {Object} record - Data to be preprocessed. + * @returns {Boolean} + */ + isSelected: function (record) { + if (_.isNull(this.selected())) { + return false; + } + + if (this.massaction().massActionMode()) { + return this.selected()[record.id]; + } + + return this.getId(this.selected()) === this.getId(record); + }, + + /** + * Click on image + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnImage: function (record, collapsibleOpened) { + if (!collapsibleOpened) { + this.select(record); + } + }, + + /** + * Click on three-dots + * + * @param {Object} record + * @param {Boolean} collapsibleOpened + */ + clickOnThreeDots: function (record, collapsibleOpened) { + if (!this.isSelected(record) || collapsibleOpened) { + this.select(record); + } + }, + + /** + * Handle checkbox click. + */ + checkboxClick: function (record) { + var items = this.selected(); + + if (this.selected()[record.id]) { + delete items[record.id]; + this.selected(items); + } else { + items[record.id] = record.id; + this.selected(items); + } + + return true; + }, + + /** + * Set the record as selected + */ + select: function (record) { + if (this.massaction().massActionMode()) { + return this.checkboxClick(record); + } + + this.isSelected(record) ? this.selected(null) : this.selected(record); + this.toggleAddSelectedButton(); + + return true; + }, + + /** + * Deselect the record + */ + deselectImage: function () { + this.selected(null); + this.toggleAddSelectedButton(); + }, + + /** + * Get the selected record + * @returns {Object} + */ + getSelected: function () { + return this.selected(); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Toggle add selected button + */ + toggleAddSelectedButton: function () { + if (this.selected() === null) { + this.hideAddSelectedAndDeleteButon(); + } else { + $(this.addSelectedBtnSelector).removeClass('no-display'); + $(this.deleteSelectedBtnSelector).removeClass('no-display'); + } + }, + + /** + * Hide add selected and Delete button + */ + hideAddSelectedAndDeleteButon: function () { + $(this.addSelectedBtnSelector).addClass('no-display'); + $(this.deleteSelectedBtnSelector).addClass('no-display'); + }, + + /** + * @param {jQuery.event} e + * @param {Object} data + */ + reloadMediaGrid: function (e, data) { + if (data.reload) { + this.reloadGrid(); + } + + if (data.message && data.code) { + this.addMessage(data.code, data.message); + } + this.hideAddSelectedAndDeleteButon(); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + }, + + /** + * Add message + * + * @param {String} code + * @param {String} message + */ + addMessage: function (code, message) { + this.messages().add(code, message); + this.messages().scheduleCleanup(); + }, + + /** + * Listener to select directory event + * + * @param {String} path + */ + selectDirectoryHandle: function (path) { + if (this.selected() && + this.selected().directory !== path && + !this.massaction().massActionMode()) { + this.deselectImage(); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js new file mode 100644 index 0000000000000..38743c8d83d3b --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/actions.js @@ -0,0 +1,109 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'mage/translate' +], function ($, _, Component, deleteImageWithDetailConfirmation, image, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/grid/columns/image/actions', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + actionsList: [ + { + name: 'image-details', + title: $t('View Details'), + handler: 'viewImageDetails' + }, + { + name: 'edit', + title: $t('Edit'), + handler: 'editImageDetails' + }, + { + name: 'delete', + title: $t('Delete'), + handler: 'deleteImageAction' + } + ], + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + this.initEvents(); + + return this; + }, + + /** + * Initialize image action events + */ + initEvents: function () { + $(this.imageModel().addSelectedBtnSelector).click(function () { + image.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + }.bind(this)); + $(this.imageModel().deleteSelectedBtnSelector).click(function () { + this.deleteImageAction(this.imageModel().selected()); + }.bind(this)); + + }, + + /** + * Delete image action + * + * @param {Object} record + */ + deleteImageAction: function (record) { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction([record.id], imageDetailsUrl, deleteImageUrl); + }, + + /** + * View image details + * + * @param {Object} record + */ + viewImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryImageDetails().showImageDetailsById(recordId); + }, + + /** + * Edit image details + * + * @param {Object} record + */ + editImageDetails: function (record) { + var recordId = this.imageModel().getId(record); + + this.mediaGalleryEditDetails().showEditDetailsPanel(recordId); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js new file mode 100644 index 0000000000000..f72a05b6d2709 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/columns/image/insertImageAction.js @@ -0,0 +1,131 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* global FORM_KEY, tinyMceEditors */ +define([ + 'jquery', + 'wysiwygAdapter', + 'underscore', + 'mage/translate' +], function ($, wysiwyg, _, $t) { + 'use strict'; + + return { + + /** + * Insert provided image in wysiwyg if enabled, or widget + * + * @param {Object} record + * @param {Object} config + * @returns {Boolean} + */ + insertImage: function (record, config) { + var targetElement; + + if (record === null) { + return false; + } + targetElement = this.getTargetElement(window.MediabrowserUtility.targetElementId); + + if (!targetElement.length) { + window.MediabrowserUtility.closeDialog(); + throw $t('Target element not found for content update'); + } + + $.ajax({ + url: config.onInsertUrl, + data: { + filename: record['encoded_id'], + 'store_id': config.storeId, + 'as_is': targetElement.is('textarea') ? 1 : 0, + 'force_static_path': targetElement.data('force_static_path') ? 1 : 0, + 'form_key': FORM_KEY + }, + context: this, + showLoader: true + }).done($.proxy(function (data) { + if (targetElement.is('textarea')) { + this.insertAtCursor(targetElement.get(0), data); + targetElement.focus(); + $(targetElement).change(); + } else { + targetElement.val(data) + .data('size', record.size) + .data('mime-type', record['content_type']) + .trigger('change'); + } + }, this)); + window.MediabrowserUtility.closeDialog(); + targetElement.focus(); + }, + + /** + * Insert image to target instance. + * + * @param {Object} element + * @param {*} value + */ + insertAtCursor: function (element, value) { + var sel, startPos, endPos, scrollTop; + + if ('selection' in document) { + //For browsers like Internet Explorer + element.focus(); + sel = document.selection.createRange(); + sel.text = value; + element.focus(); + } else if (element.selectionStart || element.selectionStart == '0') { //eslint-disable-line eqeqeq + //For browsers like Firefox and Webkit based + startPos = element.selectionStart; + endPos = element.selectionEnd; + scrollTop = element.scrollTop; + element.value = element.value.substring(0, startPos) + value + + element.value.substring(startPos, endPos) + element.value.substring(endPos, element.value.length); + element.focus(); + element.selectionStart = startPos + value.length; + element.selectionEnd = startPos + value.length + element.value.substring(startPos, endPos).length; + element.scrollTop = scrollTop; + } else { + element.value += value; + element.focus(); + } + }, + + /** + * Return opener Window object if it exists, not closed and editor is active + * + * @param {String} targetElementId + * return {Object|null} + */ + getMediaBrowserOpener: function (targetElementId) { + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId) && !_.isUndefined(tinyMceEditors) && + !tinyMceEditors.get(targetElementId).getMediaBrowserOpener().closed + ) { + return tinyMceEditors.get(targetElementId).getMediaBrowserOpener(); + } + + return null; + }, + + /** + * Get target element + * + * @param {String} targetElementId + * @returns {*|n.fn.init|jQuery|HTMLElement} + */ + getTargetElement: function (targetElementId) { + var opener; + + if (!_.isUndefined(wysiwyg) && wysiwyg.get(targetElementId)) { + opener = this.getMediaBrowserOpener(targetElementId) || window; + targetElementId = tinyMceEditors.get(targetElementId).getMediaBrowserTargetElementId(); + + return $(opener.document.getElementById(targetElementId)); + } + + return $('#' + targetElementId); + } + }; +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js new file mode 100644 index 0000000000000..659fcc0cdcfda --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/masonry.js @@ -0,0 +1,49 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'Magento_Ui/js/grid/masonry', + 'jquery' +], function (Masonry, $) { + 'use strict'; + + return Masonry.extend({ + defaults: { + modules: { + provider: '${ $.provider }' + } + }, + + /** + * Init component + * + * @return {Object} + */ + initialize: function () { + this._super(); + this.initEvents(); + + return this; + }, + + /** + * Initialize events + */ + initEvents: function () { + $(window).on('folderDeleted.enhancedMediaGallery', this.reloadGrid.bind(this)); + }, + + /** + * Reload grid + */ + reloadGrid: function () { + var provider = this.provider(), + dataStorage = provider.storage(); + + dataStorage.clearRequests(); + provider.reload(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js new file mode 100644 index 0000000000000..ddc5af0ab6296 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactionView.js @@ -0,0 +1,110 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'mage/translate', + 'text!Magento_MediaGalleryUi/template/grid/massactions/massactionButtons.html' +], function ($, Component, $t, massactionButtons) { + 'use strict'; + + return Component.extend({ + defaults: { + gridSelector: '[data-id="media-gallery-masonry-grid"]', + standAloneTitle: 'Manage Gallery', + slidePanelTitle: 'Media Gallery', + defaultTitle: null, + are: null, + standAloneArea: 'standalone', + slidepanelArea: 'slidepanel', + massactionButtonsSelector: '.massaction-buttons', + buttonsSelectorStandalone: '.page-actions-buttons', + buttonsSelectorSlidePanel: '.page-actions.floating-header', + buttons: '.page-main-actions :button', + massactionModeTitle: $t('Select Images to Delete') + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + + return this; + }, + + /** + * Switch massaction view state per active mode. + */ + switchView: function () { + this.changePageTitle(); + this.switchButtons(); + }, + + /** + * Hide or show buttons per active mode. + */ + switchButtons: function () { + if (this.massActionMode()) { + this.activateMassactionButtonView(); + } else { + this.revertButtonsToDefaultView(); + } + }, + + /** + * Sets buttons to default regular -mode view. + */ + revertButtonsToDefaultView: function () { + $(this.buttons).removeClass('no-display'); + $(this.massactionButtonsSelector).remove(); + }, + + /** + * Activate mass action buttons view + */ + activateMassactionButtonView: function () { + var buttonsContainer; + + $(this.buttons).addClass('no-display'); + + buttonsContainer = this.area === this.standAloneArea ? + this.buttonsSelectorStandalone : + this.buttonsSelectorSlidePanel; + + $(buttonsContainer).append(massactionButtons); + $(this.massactionButtonsSelector).applyBindings(); + }, + + /** + * Change page title per active mode. + */ + changePageTitle: function () { + var title = $('h1:contains(' + this.standAloneTitle + ')'), + titleSelector; + + if (title.length === 1) { + titleSelector = title; + this.area = this.standAloneArea; + } else { + titleSelector = $('h1:contains(' + this.slidePanelTitle + ')'); + this.area = this.slidepanelArea; + } + + if (this.massActionMode()) { + this.defaultTitle = titleSelector.text(); + titleSelector.text(this.massactionModeTitle); + } else { + titleSelector = $('h1:contains(' + this.massactionModeTitle + ')'); + titleSelector.text(this.defaultTitle); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js new file mode 100644 index 0000000000000..4f09854005f23 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/massaction/massactions.js @@ -0,0 +1,153 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'uiLayout', + 'underscore', + 'Magento_Ui/js/modal/alert', + 'mage/translate' +], function ($, Component, DeleteImages, Layout, _, uiAlert, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + deleteButtonSelector: '#delete_selected_massaction', + deleteImagesSelector: '#delete_massaction', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + modules: { + massactionView: '${ $.name }_view', + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }' + }, + viewConfig: [ + { + component: 'Magento_MediaGalleryUi/js/grid/massaction/massactionView', + name: '${ $.name }_view' + } + ], + imports: { + imageItems: '${ $.mediaGalleryProvider }:data.items' + }, + listens: { + imageItems: 'checkButtonVisibility' + }, + exports: { + massActionMode: '${ $.name }_view:massActionMode' + } + }, + + /** + * Initializes media gallery massaction component. + * + * @returns {Sticky} Chainable. + */ + initialize: function () { + this._super().observe([ + 'massActionMode' + ]); + this.initView(); + this.initEvents(); + + return this; + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + Layout(this.viewConfig); + + return this; + }, + + /** + * Initilize massactions events for media gallery grid. + */ + initEvents: function () { + $(window).on('massAction.MediaGallery', function () { + if (this.massActionMode()) { + return; + } + this.imageModel().selected(null); + this.massActionMode(true); + this.switchMode(); + }.bind(this)); + + $(window).on('terminateMassAction.MediaGallery', function () { + if (!this.massActionMode()) { + return; + } + + this.massActionMode(false); + this.switchMode(); + this.imageModel().updateSelected(); + }.bind(this)); + }, + + /** + * Return total selected items. + */ + getSelectedCount: function () { + if (this.massActionMode() && !_.isNull(this.imageModel().selected())) { + return Object.keys(this.imageModel().selected()).length; + } + + return 0; + }, + + /** + * If images records less than one, disable "delete images" button + */ + checkButtonVisibility: function () { + if (this.imageItems.length < 1) { + $(this.deleteImagesSelector).addClass('disabled'); + } else { + $(this.deleteImagesSelector).removeClass('disabled'); + } + }, + + /** + * Switch massaction per current event. + */ + switchMode: function () { + this.massactionView().switchView(); + this.handleDeleteAction(); + }, + + /** + * Change Default behavior of delete image to bulk deletion. + */ + handleDeleteAction: function () { + if (this.massActionMode()) { + $(this.deleteButtonSelector).on('massDelete.MediaGallery', function () { + if (this.getSelectedCount() < 1) { + uiAlert({ + content: $t('You need to select at least one image') + }); + + } else { + DeleteImages.deleteImageAction( + this.imageModel().selected(), + this.mediaGalleryImageDetails().imageDetailsUrl, + this.imageModel().deleteImageUrl + ).then(function (response) { + if (response.status === 'canceled') { + return; + } + this.imageModel().selected({}); + this.massActionMode(false); + this.switchMode(); + }.bind(this)); + } + }.bind(this)); + } + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js new file mode 100644 index 0000000000000..7116784f41a0d --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/messages.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiElement' +], function (Element) { + 'use strict'; + + return Element.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/grid/messages', + messageDelay: 5, + messages: [] + }, + + /** + * Init observable variables + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'messages' + ]); + + return this; + }, + + /** + * Get messages + * + * @returns {Array} + */ + get: function () { + return this.messages(); + }, + + /** + * Add message + * + * @param {String} type + * @param {String} message + */ + add: function (type, message) { + this.messages.push({ + code: type, + message: message + }); + }, + + /** + * Clear messages + */ + clear: function () { + this.messages.removeAll(); + }, + + /** + * Schedule message cleanup + * + * @param {Number} delay + */ + scheduleCleanup: function (delay) { + // eslint-disable-next-line no-unused-vars + var timerId; + + delay = delay || this.messageDelay; + + timerId = setTimeout(function () { + clearTimeout(timerId); + this.clear(); + }.bind(this), Number(delay) * 1000); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js new file mode 100644 index 0000000000000..15f62d6a7efd1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/grid/sortBy.js @@ -0,0 +1,77 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'Magento_Ui/js/grid/sortBy' +], function (Element) { + 'use strict'; + + return Element.extend({ + defaults: { + columnIndexMap: {} + }, + + /** + * Prepared sort order options + */ + preparedOptions: function (columns) { + var index = 0, + sortBy; + + if (columns && columns.length > 0) { + columns.map(function (column) { + if (column.sortable === true) { + sortBy = column['sort_by'] || {}; + + if (sortBy.excluded) { + return; + } + + this.options.push({ + value: column.index, + label: column.label, + sortByField: sortBy.field, + sortDirection: sortBy.direction + }); + + this.columnIndexMap[column.index] = index++; + + this.isVisible(true); + } else { + this.isVisible(false); + } + }.bind(this)); + } + }, + + /** + * Apply changes + */ + applyChanges: function () { + var column = this.getColumn(this.selectedOption()); + + this.applied({ + field: column.sortByField || this.selectedOption(), + direction: column.sortDirection || this.sorting + }); + }, + + /** + * Get column by index + * + * @param {String} optionIndex + * @returns {Object} + */ + getColumn: function (optionIndex) { + return this.options[this.columnIndexMap[optionIndex]]; + }, + + /** + * Select default option + */ + selectDefaultOption: function () { + this.selectedOption(this.options[0].value); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js new file mode 100644 index 0000000000000..58fff640f9db3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image-uploader.js @@ -0,0 +1,244 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'jquery', + 'underscore', + 'Magento_Ui/js/lib/validation/validator', + 'mage/translate', + 'jquery/file-uploader' +], function (Component, $, _, validator, $t) { + 'use strict'; + + return Component.extend({ + defaults: { + imageUploadInputSelector: '#image-uploader-form', + directoriesPath: 'media_gallery_listing.media_gallery_listing.media_gallery_directories', + actionsPath: 'media_gallery_listing.media_gallery_listing.media_gallery_columns.thumbnail_url', + messagesPath: 'media_gallery_listing.media_gallery_listing.messages', + imageUploadUrl: '', + acceptFileTypes: '', + allowedExtensions: '', + maxFileSize: '', + maxFileNameLength: 90, + loader: false, + modules: { + directories: '${ $.directoriesPath }', + actions: '${ $.actionsPath }', + mediaGridMessages: '${ $.messagesPath }', + sortBy: '${ $.sortByName }', + listingPaging: '${ $.listingPagingName }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super().observe( + [ + 'loader', + 'count' + ] + ); + + return this; + }, + + /** + * Initializes file upload library + */ + initializeFileUpload: function () { + $(this.imageUploadInputSelector).fileupload({ + url: this.imageUploadUrl, + acceptFileTypes: this.acceptFileTypes, + allowedExtensions: this.allowedExtensions, + maxFileSize: this.maxFileSize, + + /** + * Extending the form data + * + * @param {Object} form + * @returns {Array} + */ + formData: function (form) { + return form.serializeArray().concat( + [{ + name: 'isAjax', + value: true + }, + { + name: 'form_key', + value: window.FORM_KEY + }, + { + name: 'target_folder', + value: this.getTargetFolder() + }] + ); + }.bind(this), + + add: function (e, data) { + if (!this.isSizeExceeded(data.files[0]).passed) { + this.addValidationErrorMessage( + $t('Cannot upload "%1". File exceeds maximum file size limit.') + .replace('%1', data.files[0].name) + ); + + return; + } else if (!this.isFileNameLengthExceeded(data.files[0]).passed) { + this.addValidationErrorMessage( + $t('Cannot upload "%1". Filename is too long, must be 90 characters or less.') + .replace('%1', data.files[0].name) + ); + + return; + } + + this.showLoader(); + this.count(1); + data.submit(); + }.bind(this), + + stop: function () { + this.openNewestImages(); + this.mediaGridMessages().scheduleCleanup(); + }.bind(this), + + start: function () { + this.mediaGridMessages().clear(); + }.bind(this), + + done: function (e, data) { + var response = data.jqXHR.responseJSON; + + if (!response) { + this.showErrorMessage(data, $t('Could not upload the asset.')); + + return; + } + + if (!response.success) { + this.showErrorMessage(data, response.message); + + return; + } + this.showSuccessMessage(data); + this.hideLoader(); + this.actions().reloadGrid(); + }.bind(this) + }); + }, + + /** + * Add error message after validation error. + * + * @param {String} message + */ + addValidationErrorMessage: function (message) { + this.mediaGridMessages().add('error', message); + + this.count() < 2 || this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Checks if size of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isSizeExceeded: function (file) { + return validator('validate-max-size', file.size, this.maxFileSize); + }, + + /** + * Checks if name length of provided file exceeds + * defined in configuration size limits. + * + * @param {Object} file - File to be checked. + * @returns {Boolean} + */ + isFileNameLengthExceeded: function (file) { + return validator('max_text_length', file.name, this.maxFileNameLength); + }, + + /** + * Go to recently uploaded images if at least one uploaded successfully + */ + openNewestImages: function () { + this.mediaGridMessages().get().each(function (message) { + if (message.code === 'success') { + this.actions().deselectImage(); + this.sortBy().selectDefaultOption(); + this.listingPaging().goFirst(); + + return false; + } + }.bind(this)); + }, + + /** + * Show error meassages with file name. + * + * @param {Object} data + * @param {String} message + */ + showErrorMessage: function (data, message) { + data.files.each(function (file) { + this.mediaGridMessages().add( + 'error', + file.name + ': ' + $t(message) + ); + }.bind(this)); + + this.hideLoader(); + }, + + /** + * Show success message, and files counts + */ + showSuccessMessage: function () { + this.mediaGridMessages().messages.remove(function (item) { + return item.code === 'success'; + }); + this.mediaGridMessages().add('success', $t('Assets have been successfully uploaded!')); + this.count(this.count() + 1); + + }, + + /** + * Gets Media Gallery selected folder + * + * @returns {String} + */ + getTargetFolder: function () { + + if (_.isUndefined(this.directories().activeNode()) || + _.isNull(this.directories().activeNode())) { + return '/'; + } + + return this.directories().activeNode(); + }, + + /** + * Shows spinner loader + */ + showLoader: function () { + this.loader(true); + }, + + /** + * Hides spinner loader + */ + hideLoader: function () { + this.loader(false); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js new file mode 100644 index 0000000000000..c7ca95bed863c --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-actions.js @@ -0,0 +1,130 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiElement', + 'Magento_MediaGalleryUi/js/action/deleteImageWithDetailConfirmation', + 'Magento_MediaGalleryUi/js/grid/columns/image/insertImageAction', + 'Magento_MediaGalleryUi/js/action/saveDetails', + 'mage/validation' +], function ($, _, Element, deleteImageWithDetailConfirmation, addSelected, saveDetails) { + 'use strict'; + + return Element.extend({ + defaults: { + modalSelector: '', + modalWindowSelector: '', + mediaGalleryImageDetailsName: 'mediaGalleryImageDetails', + mediaGalleryEditDetailsName: 'mediaGalleryEditDetails', + template: 'Magento_MediaGalleryUi/image/actions', + modules: { + imageModel: '${ $.imageModelName }', + mediaGalleryImageDetails: '${ $.mediaGalleryImageDetailsName }', + mediaGalleryEditDetails: '${ $.mediaGalleryEditDetailsName }' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super(); + $(window).on('fileDeleted.enhancedMediaGallery', this.closeViewDetailsModal.bind(this)); + + return this; + }, + + /** + * Close the images details modal + */ + closeModal: function () { + var modalElement = $(this.modalSelector), + modalWindow = $(this.modalWindowSelector); + + if (!modalWindow.hasClass('_show') || !modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Opens the image edit panel + */ + editImageAction: function () { + var record = this.imageModel().getSelected().id; + + this.mediaGalleryEditDetails().showEditDetailsPanel(record); + }, + + /** + * Delete image action + */ + deleteImageAction: function () { + var imageDetailsUrl = this.mediaGalleryImageDetails().imageDetailsUrl, + deleteImageUrl = this.imageModel().deleteImageUrl; + + deleteImageWithDetailConfirmation.deleteImageAction( + [this.imageModel().getSelected().id], + imageDetailsUrl, + deleteImageUrl + ); + }, + + /** + * Save image details action + */ + saveImageDetailsAction: function () { + var saveDetailsUrl = this.mediaGalleryEditDetails().saveDetailsUrl, + modalElement = $(this.modalSelector), + form = modalElement.find('#image-edit-details-form'), + imageId = this.imageModel().getSelected().id, + keywords = this.mediaGalleryEditDetails().selectedKeywords(), + imageDetails = this.mediaGalleryImageDetails(); + + if (form.validation('isValid')) { + saveDetails( + saveDetailsUrl, + [form.serialize(), $.param({ + 'keywords': keywords + })].join('&') + ).then(function () { + this.closeModal(); + this.imageModel().reloadGrid(); + imageDetails.removeCached(imageId); + + if (imageDetails.isActive()) { + imageDetails.showImageDetailsById(imageId); + } + }.bind(this)); + } + }, + + /** + * Add Image + */ + addImage: function () { + addSelected.insertImage( + this.imageModel().getSelected(), + { + onInsertUrl: this.imageModel().onInsertUrl, + storeId: this.imageModel().storeId + } + ); + this.closeModal(); + }, + + /** + * Close view details modal after confirm deleting image + */ + closeViewDetailsModal: function () { + this.closeModal(); + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js new file mode 100644 index 0000000000000..db42f155501c3 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-details.js @@ -0,0 +1,184 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'Magento_MediaGalleryUi/js/action/getDetails' +], function ($, _, Component, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-details', + modalSelector: '', + modalWindowSelector: '', + imageDetailsUrl: '/media_gallery/image/details', + images: [], + tagListLimit: 7, + showAllTags: false, + image: null, + modules: { + mediaGridMessages: '${ $.mediaGridMessages }' + } + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'showAllTags' + ]); + + return this; + }, + + /** + * Show image details by ID + * + * @param {String} imageId + */ + showImageDetailsById: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openImageDetailsModal(); + }, + + /** + * Open image details popup + */ + openImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.showAllTags(false); + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Get tag text + * + * @param {String} tagText + * @param {Number} tagIndex + * @return {String} + */ + getTagText: function (tagText, tagIndex) { + return tagText + (this.image().tags.length - 1 === tagIndex ? '' : ','); + }, + + /** + * Show all image tags + */ + showMoreImageTags: function () { + this.showAllTags(true); + }, + + /** + * Is value an object + * + * @param {*} value + * @returns {Boolean} + */ + isArray: function (value) { + return _.isArray(value); + }, + + /** + * Is value not empty + * + * @param {*} value + * @returns {Boolean} + */ + notEmpty: function (value) { + return value.length > 0; + }, + + /** + * Get name and number text for used in link + * + * @param {Object} item + * @returns {String} + */ + getUsedInText: function (item) { + return item.name + '(' + item.number + ')'; + }, + + /** + * Get filter url + * + * @param {String} link + */ + getFilterUrl: function (link) { + return link + '?filters[asset_id]=[' + this.image().id + ']'; + }, + + /** + * Check if details modal is active + * @return {Boolean} + */ + isActive: function () { + return $(this.modalWindowSelector).hasClass('_show'); + }, + + /** + * Remove image details + * + * @param {String} id + */ + removeCached: function (id) { + delete this.images[id]; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js new file mode 100644 index 0000000000000..c31bc848bdc70 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/image/image-edit.js @@ -0,0 +1,228 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'uiComponent', + 'uiLayout', + 'Magento_Ui/js/lib/key-codes', + 'Magento_MediaGalleryUi/js/action/getDetails', + 'mage/validation' +], function ($, _, Component, layout, keyCodes, getDetails) { + 'use strict'; + + return Component.extend({ + defaults: { + template: 'Magento_MediaGalleryUi/image/image-edit', + modalSelector: '.media-gallery-edit-image-details-modal', + imageEditDetailsUrl: '/media_gallery/image/details', + saveDetailsUrl: '/media_gallery/image/saveDetails', + images: [], + image: null, + keywordOptions: [], + selectedKeywords: [], + newKeyword: '', + newKeywordSelector: '#keyword', + modules: { + mediaGridMessages: '${ $.mediaGridMessages }', + keywordsSelect: '${ $.name }_keywords' + }, + viewConfig: [ + { + component: 'Magento_Ui/js/form/element/ui-select', + name: '${ $.name }_keywords', + template: 'ui/grid/filters/elements/ui-select', + disableLabel: true + } + ], + exports: { + keywordOptions: '${ $.name }_keywords:options' + }, + links: { + selectedKeywords: '${ $.name }_keywords:value' + } + }, + + /** + * Initialize the component + * + * @returns {Object} + */ + initialize: function () { + this._super().initView(); + + return this; + }, + + /** + * Add a new keyword to select + */ + addKeyword: function () { + var options = this.keywordOptions(), + selected = this.selectedKeywords(), + newKeywordField = $(this.newKeywordSelector); + + newKeywordField.validation(); + + if (!newKeywordField.validation('isValid') || this.newKeyword() === '') { + return; + } + + options.push(this.getOptionForKeyword(this.newKeyword())); + selected.push(this.newKeyword()); + this.newKeyword(''); + + this.keywordOptions(options); + this.selectedKeywords(selected); + }, + + /** + * Create an option object based on keyword string + * + * @param {String} keyword + * @returns {Object} + */ + getOptionForKeyword: function (keyword) { + return { + 'is_active': 1, + level: 1, + value: keyword, + label: keyword + }; + }, + + /** + * Convert array of keywords to options format + * + * @param {Array} tags + */ + setKeywordOptions: function (tags) { + var options = []; + + tags.forEach(function (tag) { + options.push(this.getOptionForKeyword(tag)); + }.bind(this)); + + this.keywordOptions(options); + this.selectedKeywords(tags); + }, + + /** + * Initialize child components + * + * @returns {Object} + */ + initView: function () { + layout(this.viewConfig); + + return this; + }, + + /** + * Init observable variables + * + * @return {Object} + */ + initObservable: function () { + this._super() + .observe([ + 'image', + 'keywordOptions', + 'selectedKeywords', + 'newKeyword' + ]); + + return this; + }, + + /** + * Get image details by ID + * + * @param {String} imageId + */ + showEditDetailsPanel: function (imageId) { + if (_.isUndefined(this.images[imageId])) { + getDetails(this.imageEditDetailsUrl, [imageId]).then(function (imageDetails) { + this.images[imageId] = imageDetails[imageId]; + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }.bind(this)).fail(function (error) { + this.addMediaGridMessage('error', error); + }.bind(this)); + + return; + } + + if (this.image() && this.image().id === imageId) { + this.openEditImageDetailsModal(); + + return; + } + + this.image(this.images[imageId]); + this.openEditImageDetailsModal(); + }, + + /** + * Open edit image details popup + */ + openEditImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + this.setKeywordOptions(this.image().tags); + this.newKeyword(''); + + modalElement.modal('openModal'); + }, + + /** + * Close image details popup + */ + closeImageDetailsModal: function () { + var modalElement = $(this.modalSelector); + + if (!modalElement.length || _.isUndefined(modalElement.modal)) { + return; + } + + modalElement.modal('closeModal'); + }, + + /** + * Add media grid message + * + * @param {String} code + * @param {String} message + */ + addMediaGridMessage: function (code, message) { + this.mediaGridMessages().add(code, message); + this.mediaGridMessages().scheduleCleanup(); + }, + + /** + * Handle Enter key event to save image details + * + * @param {Object} data + * @param {jQuery.Event} event + * @returns {Boolean} + */ + handleEnterKey: function (data, event) { + var modalElement = $(this.modalSelector), + key = keyCodes[event.keyCode]; + + if (key === 'enterKey') { + event.preventDefault(); + modalElement.find('.page-action-buttons button.save').click(); + } + + return true; + } + }); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js new file mode 100644 index 0000000000000..127f1676015f1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-description.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-description', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\n\ ]+$|^$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), ' + + 'dots (.), commas(,), underscores (_), dashes (-), and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js new file mode 100644 index 0000000000000..47fa5b19781bc --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-keyword.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($, validate, $t) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-keyword', function (value) { + return /^[a-zA-Z0-9\-\_\.\,]+$|^$/i.test(value); + + }, $t('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_) and dashes(-) on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js new file mode 100644 index 0000000000000..1429be64b7d12 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/js/validation/validate-image-title.js @@ -0,0 +1,19 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'jquery/validate', + 'mage/translate' +], function ($) { + 'use strict'; + + $.validator.addMethod( + 'validate-image-title', function (value) { + return /^[a-zA-Z0-9\-\_\.\,\ ]+$/i.test(value); + + }, $.mage.__('Please use only letters (a-z or A-Z), numbers (0-9), dots (.), commas(,), ' + + 'underscores (_), dashes(-) and spaces on this field.')); +}); diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html new file mode 100644 index 0000000000000..3b88c58201be7 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image.html @@ -0,0 +1,45 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-wrap" collapsible> + <div class="mediagallery-massaction-checkbox" if="isMassActionMode()"> + <input type="checkbox" attr="{ 'data-ui-id': $row().title }" visible="isMassActionMode()" ko-checked="isSelected($row())" click="function () { return select($row()); }"/> + </div> + <div class="media-gallery-image"> + <div data-row="file" + class="masonry-image-block media-gallery-image-block" + attr="'data-id': $col.getId($row())" + css="{ selected: isSelected($row()) }" + click="function(){ clickOnImage($row(), $collapsible.opened()) }" + > + <img attr="src: $col.getUrl($row()), alt: $col.getImageAlt($row())" + class="media-gallery-image-column" + data-role="thumbnail"/> + </div> + <ul class="action-menu" css="_active: $collapsible.opened"> + <scope args="actions"> + <render args="template"/> + </scope> + </ul> + </div> + <div class="masonry-image-description"> + <ul class="media-gallery-image-details"> + <li class="name" data-ui-id="title" text="$row().title"></li> + <li class="source"> + <img if="$row().source" class="media-gallery-source-icon" attr="{ src: $row().source }"/> + </li> + <li class="type" data-ui-id="content-type" text="$row().content_type"></li> • + <li class="dimensions" data-ui-id="dimensions" text="$row().width + 'x' + $row().height"></li> + </ul> + <div class="media-gallery-image-actions"> + <div class="action-select-wrap"> + <span class="three-dots" ifnot="isMassActionMode()" + toggleCollapsible + click="function () { clickOnThreeDots($row(), $collapsible.opened()); }"></span> + </div> + </div> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html new file mode 100644 index 0000000000000..042e119b9f40e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/columns/image/actions.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<each args="{ data: actionsList, as: 'action' }"> + <li> + <a class="action-menu-item" href="" text="action.title" + click="$parent[action.handler].bind($parent, $row())" + attr="{'data-action': 'item-' + action.name}"> + </a> + </li> +</each> \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html new file mode 100644 index 0000000000000..da835952e2f23 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/directories/directoryTree.html @@ -0,0 +1,10 @@ +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<div class="media-directory-container"> + <div id="media-gallery-directory-tree"></div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html new file mode 100644 index 0000000000000..d1840fdb3dc8e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filter/checkbox.html @@ -0,0 +1,24 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__field admin__media-gallery-image-checkbox" visible="visible" css="$data.additionalClasses"> + <div class="admin__field-control"> + <label class="admin__form-field-label" if="$data.label" attr="for: uid"> + <span translate="label" attr="'data-config-scope': $data.scopeLabel" /> + </label> + </div> + <div class="admin__field admin__field-option"> + <input type="checkbox" + class="admin__control-checkbox" + ko-checked="$data.checked" + disable="disabled" + ko-value="value" + hasFocus="focused" + attr="id: uid, name: inputName"/> + + <label class="admin__field-label" text="description" attr="for: uid"/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html new file mode 100644 index 0000000000000..cce859f331d9a --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/filters/elements/ui-select.html @@ -0,0 +1,133 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<ifnot args="disableLabel"> + <label class="admin__form-field-label" attr="{for: uid}"> + <span translate="label"></span> + </label> +</ifnot> +<div class="admin__action-multiselect-wrap action-select-wrap media-gallery-asset-ui-select-filter" + tabindex="0" attr="{id: uid}" css="{_active: listVisible,'admin__action-multiselect-tree': isTree()}" + event="{focusin: onFocusIn,focusout: onFocusOut,keydown: keydownSwitcher}" outerClick="outerClick.bind($data)"> + <ifnot args="chipsEnabled"> + <div class="action-select admin__action-multiselect" + data-role="advanced-select" + css="{_active: listVisible}" + click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" data-role="selected-option" + ifnot="validationLoading" css="{warning: warn().length}" text="setCaption()"> + </div> + <button if="isRemoveSelectedIcon && hasData() || !validationLoading" class="action-close" + type="button" data-action="remove-selected-item" tabindex="-1" click="clear"> + <span class="action-close-text" translate="'Close'"></span> + </button> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="validationLoading" + if="validationLoading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + </div> + </ifnot> + <if args="chipsEnabled"> + <div class="action-select admin__action-multiselect" data-role="advanced-select" + css="{_active: listVisible}" click="function(data, event) {toggleListVisible(data, event)}"> + <div class="admin__action-multiselect-text" visible="!hasData()" + translate="selectedPlaceholders.defaultPlaceholder"> + </div> + <each args="{ data: getSelected(), as: 'option'}"> + <span class="admin__action-multiselect-crumb"> + <span text="label"> + </span> + <button class="action-close" type="button" data-action="remove-selected-item" + tabindex="-1" click="$parent.removeSelected.bind($parent, value)"> + <span class="action-close-text" translate="'Close'"></span> + </button> + </span> + </each> + </div> + </if> + <div class="action-menu" css="{ _active: listVisible}"> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loading" if="loading"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> + <if args="filterOptions"> + <div class="admin__action-multiselect-search-wrap"> + <input class="admin__control-text admin__action-multiselect-search" data-role="advanced-select-text" + type="text" event="{keydown: filterOptionsKeydown}" attr="{id: uid+2, placeholder: filterPlaceholder}" + ko-focused="filterOptionsFocus" ko-value="filterInputValue" data-bind="valueUpdate:'afterkeydown'"> + <label class="admin__action-multiselect-search-label" + data-action="advanced-select-search" + attr="{for: uid+2}"> + </label> + <div if="itemsQuantity" + text="itemsQuantity" + class="admin__action-multiselect-search-count"> + </div> + </div> + <div ifnot="options().length" + class="admin__action-multiselect-empty-area"> + <ul text="emptyOptionsHtml"/> + </div> + </if> + <ul class="admin__action-multiselect-menu-inner _root" + event="{mousemove: function(data, event){onMousemove($data, $index(), event)}, + scroll: function(data, event){onScrollDown(data, event)}}"> + <each args="{ data: options, as: 'option'}"> + <li class="admin__action-multiselect-menu-inner-item _root" + css="{ _parent: $data.optgroup }" + data-role="option-group"> + <div class="action-menu-item" + css="{ + _selected: $parent.isSelectedValue(option), + _hover: $parent.isHovered(option, $element), + _expended: $parent.getLevelVisibility($data) && $parent.showLevels($data), + _unclickable: $parent.isLabelDecoration($data), + _last: $parent.addLastElement($data), + '_with-checkbox': $parent.showCheckbox + }" + click="function(data, event){ + $parent.toggleOptionSelected($data, $index(), event); + }" + data-bind="clickBubble:false"> + <if args="$data.optgroup && $parent.showOpenLevelsActionIcon"> + <div class="admin__action-multiselect-dropdown" + click="function(event){ $parent.showLevels($data); $parent.openChildLevel($data, $element, event);}" + data-bind="clickBubble:false"> + </div> + </if> + <if args="$parent.showCheckbox"> + <input class="admin__control-checkbox" type="checkbox" + tabindex="-1" attr="{ 'checked': $parent.isSelected(option.value) }"> + </if> + <label class="admin__action-multiselect-label"> + <span text="option.label"></span> + <img if="$parent.getPath(option)" + class="admin__action-multiselect-item-path" + attr="{ src: option.path }"/> + </label> + </div> + <if args="$data.optgroup"> + <render args="{name: $parent.optgroupTmpl, data: {root: $parent, current: $data}}" ></render> + </if> + </li> + </each> + </ul> + <if args="$data.closeBtn"> + <div class="admin__action-multiselect-actions-wrap"> + <button class="action-default" + data-action="close-advanced-select" + type="button" + click="outerClick"> + <span translate="closeBtnLabel"></span> + </button> + </div> + </if> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html new file mode 100644 index 0000000000000..5bbdafebe4095 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/count.html @@ -0,0 +1,9 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div visible="massActionMode()" class="mediagallery-massaction-items-count"> + <div class="selected_count_text">(<b><text args="getSelectedCount()"/> Selected</b>) </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/massactionButtons.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/massactionButtons.html new file mode 100644 index 0000000000000..a4294434c82bf --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/massactions/massactionButtons.html @@ -0,0 +1,13 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<button id="cancel_massaction" type="button" onclick="jQuery(window).trigger('terminateMassAction.MediaGallery')" class="massaction-buttons cancel"> + <span data-bind="i18n: 'Cancel'"/> +</button> +<button id="delete_selected_massaction" onclick="jQuery('#delete_selected_massaction').trigger('massDelete.MediaGallery')" type="button" class="primary massaction-buttons cancel"> + <span data-bind="i18n: 'Delete Selected'"/> +</button> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html new file mode 100644 index 0000000000000..1ec084e223e98 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/messages.html @@ -0,0 +1,15 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<ul class="messages"> + <div class="messages" outereach="messages"> + <div attr="class: 'message message-'+code"> + <div data-ui-id="messages-message-error"> + <span text="message"></span> + </div> + </div> + </div> +</ul> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html new file mode 100644 index 0000000000000..fb7334a7b0d06 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/grid/toolbar.html @@ -0,0 +1,32 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="admin__data-grid-header" data-role="masonry-main-toolbar" afterRender="$data.setToolbarNode"> + <div class="admin__data-grid-header-row"> + <div class="admin__data-grid-actions-wrap" each="getRegion('dataGridActions')" render=""/> + <each args="getRegion('dataGridFilters')" render=""/> + </div> + <div class="admin__data-grid-header-row row row-gutter"> + <div class="col-xs-2" if="hasChild('listing_massaction')" ko-scope="requestChild('listing_massaction')" render=""/> + <div css=" + 'col-xs-10': hasChild('listing_massaction'), + 'col-xs-12': !hasChild('listing_massaction')"> + <div class="row"> + <div class="col-xs-4"> + <div class="masonry-results-number" ko-scope="requestChild('listing_paging')"> + <render args="totalTmpl"/> + </div> + <each args="getRegion('sorting')" render=""/> + </div> + <div class="col-xs-8" ko-scope="requestChild('listing_paging')"> + <div render=""/> + </div> + </div> + </div> + </div> +</div> + +<render args="stickyTmpl" if="$data.sticky"/> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html new file mode 100644 index 0000000000000..6d5580b1aad6e --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image-uploader.html @@ -0,0 +1,17 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="media-gallery-image-uploader-container"> + <form id="image-uploader-form" class="no-display" method="POST" enctype="multipart/form-data"> + <input afterRender="initializeFileUpload" id="image-uploader-input" type="file" name="image" + multiple="multiple"/> + </form> + <div data-role="spinner" class="admin__data-grid-loading-mask" visible="loader"> + <div class="spinner"> + <span repeat="8"/> + </div> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html new file mode 100644 index 0000000000000..8ecaf0bd2a019 --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/actions.html @@ -0,0 +1,12 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<each args="{ data: actionsList, as: 'action' }"> + <button type="button" click="$parent[action.handler].bind($parent)" + attr="{class: action.classes, id: 'image-details-action-' + action.name, title: $t(action.title)}"> + <span translate="action.title"></span> + </button> +</each> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html new file mode 100644 index 0000000000000..15b94f823c2ba --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-details.html @@ -0,0 +1,64 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="image-details" if="image"> + <div class="image-details-image"> + <img attr="src: image().image_url"/> + </div> + <div class="image-details-sidebar"> + <div class="image-details-section"> + <h3 class="image-title" text="image().title"></h3> + <div class="image-type"> + <span class="source"><img if="image().source" class="media-gallery-source-icon" attr="{ src: image().source }" /></span> + <span class="type" data-ui-id="content-type" text="image().content_type"></span> + </div> + </div> + <div class="filename image-details-section"> + <h3 translate="'Filename'"></h3> + <p text="image().path"></p> + </div> + <div class="general-details image-details-section" if="image().details"> + <h3 translate="'Details'"></h3> + <div class="attributes"> + <each args="image().details"> + <div class="attribute" if="value"> + <span if="$parent.notEmpty(value)" class="title" translate="title"></span> + <ifnot args="$parent.isArray(value)"> + <div class="value" text="value"></div> + </ifnot> + <if args="$parent.isArray(value)"> + <each args="{ data: value, as: 'item'}"> + <div class="value"> + <a attr="href: $parents[1].getFilterUrl(item.link)" + text="$parents[1].getUsedInText(item)"></a></br> + </div> + </each> + </if> + </div> + </each> + </div> + </div> + <div class="description image-details-section" if="image().description"> + <h3 translate="'Description'"></h3> + <p text="image().description"></p> + </div> + <div class="tags image-details-section" if="image().tags.length"> + <h3 translate="'Tags'"></h3> + <div class="tags-list" css="{'show-all-tags': showAllTags}"> + <each args="data: image().tags, as: '$tag'"> + <span class="tag-item" text="$parent.getTagText($tag, $index())" + css="{'show-more-item': ($index() + 1) > $parent.tagListLimit}"></span> + </each> + </div> + <div class="show-more-link-container"> + <a href="#" class="show-more-link" if="image().tags.length > tagListLimit" + translate="'Show More'" click="showMoreImageTags"></a> + </div> + </div> + + <each args="getRegion('additional_image_details')" render=""/> + </div> +</div> diff --git a/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html new file mode 100644 index 0000000000000..e8448e1a64aef --- /dev/null +++ b/app/code/Magento/MediaGalleryUi/view/adminhtml/web/template/image/image-edit.html @@ -0,0 +1,74 @@ +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<div class="edit-image-details" if="image"> + <fieldset class="admin__fieldset"> + <input type="hidden" ko-value="image().id" data-ui-id="id" name="id"/> + <div class="admin__field _required"> + <label for="title" class="admin__field-label"> + <span translate="'Title'"></span> + </label> + <div class="admin__field-control"> + <input type="text" id="title" data-ui-id="title" name="title" placeholder="Title" + class="admin__control-text required-entry minimum-length-1 maximum-length-128" + ko-value="image().title" event="{keypress: handleEnterKey}" + data-validate="{'required':true,'validate-image-title':true, 'validate-length':true}"/> + </div> + </div> + <div class="admin__field"> + <label for="path" class="admin__field-label"> + <span translate="'Filename'"></span> + </label> + <div class="admin__field-control path-display"> + <span data-ui-id="path" id="path" text="image().path"></span> + </div> + </div> + <div class="admin__field"> + <label for="description" class="admin__field-label"> + <span translate="'Description'"></span> + </label> + <div class="admin__field-control"> + <textarea id="description" + data-ui-id="description" + name="description" + class="admin__control-textarea minimum-length-0 maximum-length-500" + rows="7" cols="80" + ko-value="image().description" + data-validate="{'validate-image-description':true, 'validate-length':true}"></textarea> + </div> + </div> + <div class="admin__field"> + <label class="admin__field-label"> + <span translate="'Tags'"></span> + </label> + <div class="admin__field-control"> + <div class="admin__field"> + <scope args="keywordsSelect"> + <render args="template"/> + </scope> + </div> + <div class="admin__field"> + <div class="admin__field-control admin__field-option admin__control-grouped"> + <div class="admin__field admin__field-group-additional"> + <div class="admin__field-control"> + <input type="text" id="keyword" data-ui-id="keyword" name="keyword" placeholder="New Keyword" + class="admin__control-text minimum-length-0 maximum-length-128" ko-value="newKeyword" + data-validate="{'validate-image-keyword': true, 'validate-length': true}"/> + </div> + </div> + <div class="admin__field admin__field-group-additional admin__field-small"> + <div class="admin__field-control"> + <button type="button" data-ui-id="add-keyword" class="action-basic" click="addKeyword"> + <span translate="'Add New Tag'"></span> + </button> + </div> + </div> + </div> + </div> + </div> + </div> + </fieldset> +</div> diff --git a/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php new file mode 100644 index 0000000000000..a516ac927fd2d --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/Api/ConfigInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\MediaGalleryUiApi\Api; + +/** + * Class responsible to provide API access to system configuration related to the Media Gallery + */ +interface ConfigInterface +{ + /** + * Check if grid UI is enabled for Magento media gallery + * + * @return bool + */ + public function isEnabled(): bool; +} diff --git a/app/code/Magento/MediaGalleryUiApi/LICENSE.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE.txt new file mode 100644 index 0000000000000..49525fd99da9c --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/LICENSE.txt @@ -0,0 +1,48 @@ + +Open Software License ("OSL") v. 3.0 + +This Open Software License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Open Software License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, with the proviso that copies of Original Work or Derivative Works that You distribute or communicate shall be licensed under this Open Software License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including 'fair use' or 'fair dealing'). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright (C) 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Open Software License" or "OSL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. \ No newline at end of file diff --git a/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt b/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt new file mode 100644 index 0000000000000..f39d641b18a19 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/LICENSE_AFL.txt @@ -0,0 +1,48 @@ + +Academic Free License ("AFL") v. 3.0 + +This Academic Free License (the "License") applies to any original work of authorship (the "Original Work") whose owner (the "Licensor") has placed the following licensing notice adjacent to the copyright notice for the Original Work: + +Licensed under the Academic Free License version 3.0 + + 1. Grant of Copyright License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, for the duration of the copyright, to do the following: + + 1. to reproduce the Original Work in copies, either alone or as part of a collective work; + + 2. to translate, adapt, alter, transform, modify, or arrange the Original Work, thereby creating derivative works ("Derivative Works") based upon the Original Work; + + 3. to distribute or communicate copies of the Original Work and Derivative Works to the public, under any license of your choice that does not contradict the terms and conditions, including Licensor's reserved rights and remedies, in this Academic Free License; + + 4. to perform the Original Work publicly; and + + 5. to display the Original Work publicly. + + 2. Grant of Patent License. Licensor grants You a worldwide, royalty-free, non-exclusive, sublicensable license, under patent claims owned or controlled by the Licensor that are embodied in the Original Work as furnished by the Licensor, for the duration of the patents, to make, use, sell, offer for sale, have made, and import the Original Work and Derivative Works. + + 3. Grant of Source Code License. The term "Source Code" means the preferred form of the Original Work for making modifications to it and all available documentation describing how to modify the Original Work. Licensor agrees to provide a machine-readable copy of the Source Code of the Original Work along with each copy of the Original Work that Licensor distributes. Licensor reserves the right to satisfy this obligation by placing a machine-readable copy of the Source Code in an information repository reasonably calculated to permit inexpensive and convenient access by You for as long as Licensor continues to distribute the Original Work. + + 4. Exclusions From License Grant. Neither the names of Licensor, nor the names of any contributors to the Original Work, nor any of their trademarks or service marks, may be used to endorse or promote products derived from this Original Work without express prior permission of the Licensor. Except as expressly stated herein, nothing in this License grants any license to Licensor's trademarks, copyrights, patents, trade secrets or any other intellectual property. No patent license is granted to make, use, sell, offer for sale, have made, or import embodiments of any patent claims other than the licensed claims defined in Section 2. No license is granted to the trademarks of Licensor even if such marks are included in the Original Work. Nothing in this License shall be interpreted to prohibit Licensor from licensing under terms different from this License any Original Work that Licensor otherwise would have a right to license. + + 5. External Deployment. The term "External Deployment" means the use, distribution, or communication of the Original Work or Derivative Works in any way such that the Original Work or Derivative Works may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Work or a Derivative Work as a distribution under section 1(c). + + 6. Attribution Rights. You must retain, in the Source Code of any Derivative Works that You create, all copyright, patent, or trademark notices from the Source Code of the Original Work, as well as any notices of licensing and any descriptive text identified therein as an "Attribution Notice." You must cause the Source Code for any Derivative Works that You create to carry a prominent Attribution Notice reasonably calculated to inform recipients that You have modified the Original Work. + + 7. Warranty of Provenance and Disclaimer of Warranty. Licensor warrants that the copyright in and to the Original Work and the patent rights granted herein by Licensor are owned by the Licensor or are sublicensed to You under the terms of this License with the permission of the contributor(s) of those copyrights and patent rights. Except as expressly stated in the immediately preceding sentence, the Original Work is provided under this License on an "AS IS" BASIS and WITHOUT WARRANTY, either express or implied, including, without limitation, the warranties of non-infringement, merchantability or fitness for a particular purpose. THE ENTIRE RISK AS TO THE QUALITY OF THE ORIGINAL WORK IS WITH YOU. This DISCLAIMER OF WARRANTY constitutes an essential part of this License. No license to the Original Work is granted by this License except under this disclaimer. + + 8. Limitation of Liability. Under no circumstances and under no legal theory, whether in tort (including negligence), contract, or otherwise, shall the Licensor be liable to anyone for any indirect, special, incidental, or consequential damages of any character arising as a result of this License or the use of the Original Work including, without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses. This limitation of liability shall not apply to the extent applicable law prohibits such limitation. + + 9. Acceptance and Termination. If, at any time, You expressly assented to this License, that assent indicates your clear and irrevocable acceptance of this License and all of its terms and conditions. If You distribute or communicate copies of the Original Work or a Derivative Work, You must make a reasonable effort under the circumstances to obtain the express assent of recipients to the terms of this License. This License conditions your rights to undertake the activities listed in Section 1, including your right to create Derivative Works based upon the Original Work, and doing so without honoring these terms and conditions is prohibited by copyright law and international treaty. Nothing in this License is intended to affect copyright exceptions and limitations (including "fair use" or "fair dealing"). This License shall terminate immediately and You may no longer exercise any of the rights granted to You by this License upon your failure to honor the conditions in Section 1(c). + + 10. Termination for Patent Action. This License shall terminate automatically and You may no longer exercise any of the rights granted to You by this License as of the date You commence an action, including a cross-claim or counterclaim, against Licensor or any licensee alleging that the Original Work infringes a patent. This termination provision shall not apply for an action alleging patent infringement by combinations of the Original Work with other software or hardware. + + 11. Jurisdiction, Venue and Governing Law. Any action or suit relating to this License may be brought only in the courts of a jurisdiction wherein the Licensor resides or in which Licensor conducts its primary business, and under the laws of that jurisdiction excluding its conflict-of-law provisions. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any use of the Original Work outside the scope of this License or after its termination shall be subject to the requirements and penalties of copyright or patent law in the appropriate jurisdiction. This section shall survive the termination of this License. + + 12. Attorneys' Fees. In any action to enforce the terms of this License or seeking damages relating thereto, the prevailing party shall be entitled to recover its costs and expenses, including, without limitation, reasonable attorneys' fees and costs incurred in connection with such action, including any appeal of such action. This section shall survive the termination of this License. + + 13. Miscellaneous. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. + + 14. Definition of "You" in This License. "You" throughout this License, whether in upper or lower case, means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with you. For purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + 15. Right to Use. You may use the Original Work in all ways not otherwise restricted or conditioned by this License or by law, and Licensor promises not to interfere with or be responsible for such uses by You. + + 16. Modification of This License. This License is Copyright © 2005 Lawrence Rosen. Permission is granted to copy, distribute, or communicate this License without modification. Nothing in this License permits You to modify this License as applied to the Original Work or to Derivative Works. However, You may modify the text of this License and copy, distribute or communicate your modified version (the "Modified License") and apply it to other original works of authorship subject to the following conditions: (i) You may not indicate in any way that your Modified License is the "Academic Free License" or "AFL" and you may not use those names in the name of your Modified License; (ii) You must replace the notice specified in the first paragraph above with the notice "Licensed under <insert your license name here>" or with a notice of your own that is not confusingly similar to the notice in this License; and (iii) You may not claim that your original works are open source software unless your Modified License has been approved by Open Source Initiative (OSI) and You comply with its license review and certification process. diff --git a/app/code/Magento/MediaGalleryUiApi/README.md b/app/code/Magento/MediaGalleryUiApi/README.md new file mode 100644 index 0000000000000..005a445c68b2a --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/README.md @@ -0,0 +1,13 @@ +# Magento_MediaGalleryUiApi module + +The Magento_MediaGalleryUiApi module is responsible for the media gallery user interface (UI) implementation API. + +## Extensibility + +Extension developers can interact with the Magento_MediaGalleryUiApi module. For more information about the Magento extension mechanism, see [Magento plug-ins](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/plugins.html). + +[The Magento dependency injection mechanism](https://devdocs.magento.com/guides/v2.3/extension-dev-guide/depend-inj.html) enables you to override the functionality of the Magento_MediaGalleryUiApi module. + +## Additional information + +For information about significant changes in patch releases, see [2.3.x Release information](https://devdocs.magento.com/guides/v2.3/release-notes/bk-release-notes.html). diff --git a/app/code/Magento/MediaGalleryUiApi/composer.json b/app/code/Magento/MediaGalleryUiApi/composer.json new file mode 100644 index 0000000000000..f8d5ef11058c1 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-media-gallery-ui-api", + "description": "Magento module responsible for the media gallery UI implementation API", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\MediaGalleryUiApi\\": "" + } + } +} diff --git a/app/code/Magento/MediaGalleryUiApi/etc/module.xml b/app/code/Magento/MediaGalleryUiApi/etc/module.xml new file mode 100644 index 0000000000000..cf62515ff92b6 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_MediaGalleryUiApi" /> +</config> diff --git a/app/code/Magento/MediaGalleryUiApi/registration.php b/app/code/Magento/MediaGalleryUiApi/registration.php new file mode 100644 index 0000000000000..b3ee130a1c510 --- /dev/null +++ b/app/code/Magento/MediaGalleryUiApi/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_MediaGalleryUiApi', __DIR__); diff --git a/app/code/Magento/MediaStorage/Model/File/Uploader.php b/app/code/Magento/MediaStorage/Model/File/Uploader.php index 3f3cefe1d6330..173211dfac011 100644 --- a/app/code/Magento/MediaStorage/Model/File/Uploader.php +++ b/app/code/Magento/MediaStorage/Model/File/Uploader.php @@ -6,6 +6,10 @@ namespace Magento\MediaStorage\Model\File; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Validation\ValidationException; +use Magento\MediaStorage\Model\File\Validator\Image; + /** * Core file uploader model * @@ -40,6 +44,11 @@ class Uploader extends \Magento\Framework\File\Uploader */ protected $_validator; + /** + * @var Image + */ + private $imageValidator; + /** * @param string $fileId * @param \Magento\MediaStorage\Helper\File\Storage\Database $coreFileStorageDb @@ -130,4 +139,33 @@ public function validateFile() $this->_validateFile(); return $this->_file; } + + /** + * @inheritDoc + * @since 100.4.0 + */ + protected function _validateFile() + { + parent::_validateFile(); + + if (!$this->getImageValidator()->isValid($this->_file['tmp_name'])) { + throw new ValidationException(__('File validation failed.')); + } + } + + /** + * Return image validator class. + * + * Child classes __construct() don't call parent, so we have to retrieve class instance with private function. + * + * @return Image + */ + private function getImageValidator(): Image + { + if (!$this->imageValidator) { + $this->imageValidator = ObjectManager::getInstance()->get(Image::class); + } + + return $this->imageValidator; + } } diff --git a/app/code/Magento/MediaStorage/Model/File/Validator/Image.php b/app/code/Magento/MediaStorage/Model/File/Validator/Image.php new file mode 100644 index 0000000000000..a8bce7cfee20b --- /dev/null +++ b/app/code/Magento/MediaStorage/Model/File/Validator/Image.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaStorage\Model\File\Validator; + +use Magento\Framework\File\Mime; +use Magento\Framework\Filesystem\Driver\File; + +/** + * Image validator + */ +class Image extends \Zend_Validate_Abstract +{ + /** + * @var array + */ + private $imageMimeTypes = [ + 'png' => 'image/png', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpg' => 'image/jpeg', + 'gif' => 'image/gif', + 'bmp' => 'image/bmp', + 'ico' => 'image/vnd.microsoft.icon', + ]; + + /** + * @var Mime + */ + private $fileMime; + + /** + * @var File + */ + private $file; + + /** + * @param Mime $fileMime + * @param File $file + */ + public function __construct( + Mime $fileMime, + File $file + ) { + $this->fileMime = $fileMime; + $this->file = $file; + } + + /** + * @inheritDoc + */ + public function isValid($filePath): bool + { + $fileMimeType = $this->fileMime->getMimeType($filePath); + $isValid = true; + + if (in_array($fileMimeType, $this->imageMimeTypes)) { + try { + //phpcs:ignore Magento2.Functions.DiscouragedFunction + $image = imagecreatefromstring($this->file->fileGetContents($filePath)); + + $isValid = $image ? true : false; + } catch (\Exception $e) { + $isValid = false; + } + } + + return $isValid; + } +} diff --git a/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml b/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml index fd437161dfbb0..aaf03b33514c1 100644 --- a/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml +++ b/app/code/Magento/MediaStorage/view/adminhtml/templates/system/config/system/storage/media/synchronize.phtml @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<?php /* @var $block \Magento\MediaStorage\Block\System\Config\System\Storage\Media\Synchronize */ ?> +/** + * @var $block \Magento\MediaStorage\Block\System\Config\System\Storage\Media\Synchronize + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> -<script> +<?php +$syncStorageParams = $block->getSyncStorageParams(); +$stateRunning = /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_RUNNING; +$stateFinished = /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_FINISHED; +$stateNotified = /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_NOTIFIED; +$scriptString = <<<script require([ 'jquery', 'prototype', @@ -31,12 +39,14 @@ require([ $('system_media_storage_configuration_media_database').value ); - <?php $syncStorageParams = $block->getSyncStorageParams() ?> - addAllowedStorage(<?= $block->escapeJs($syncStorageParams['storage_type']) ?>, '<?= $block->escapeJs($syncStorageParams['connection_name']) ?>'); + addAllowedStorage({$block->escapeJs($syncStorageParams['storage_type'])}, + '{$block->escapeJs($syncStorageParams['connection_name'])}'); defaultValues = []; - defaultValues['system_media_storage_configuration_media_storage'] = $('system_media_storage_configuration_media_storage').value; - defaultValues['system_media_storage_configuration_media_database'] = $('system_media_storage_configuration_media_database').value; + defaultValues['system_media_storage_configuration_media_storage'] = + $('system_media_storage_configuration_media_storage').value; + defaultValues['system_media_storage_configuration_media_database'] = + $('system_media_storage_configuration_media_database').value; function addAllowedStorage(storageType, connection) @@ -90,7 +100,7 @@ require([ } var checkStatus = function() { - u = new Ajax.PeriodicalUpdater('', '<?= $block->escapeUrl($block->getAjaxStatusUpdateUrl()) ?>', { + u = new Ajax.PeriodicalUpdater('', '{$block->escapeJs($block->getAjaxStatusUpdateUrl())}', { method: 'get', frequency: 5, loaderArea: false, @@ -100,7 +110,7 @@ require([ try { response = JSON.parse(transport.responseText); - if (response.state == '<?= /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_RUNNING ?>' + if (response.state == '{$stateRunning}' && response.message ) { if ($('sync_span').hasClassName('no-display')) { @@ -112,12 +122,12 @@ require([ enableStorageSelection(); $('sync_span').addClassName('no-display'); - if (response.state == '<?= /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_FINISHED ?>') { + if (response.state == '{$stateFinished}') { addAllowedStorage( $('system_media_storage_configuration_media_storage').value, $('system_media_storage_configuration_media_database').value ); - } else if (response.state == '<?= /* @noEscape */ (int)\Magento\MediaStorage\Model\File\Storage\Flag::STATE_NOTIFIED ?>') { + } else if (response.state == '{$stateNotified}') { if (response.has_errors) { enableSyncButton(); } else { @@ -152,7 +162,7 @@ require([ connection: $('system_media_storage_configuration_media_database').value }; - new Ajax.Request('<?= $block->escapeUrl($block->getAjaxSyncUrl()) ?>', { + new Ajax.Request('{$block->escapeJs($block->getAjaxSyncUrl())}', { parameters: params, loaderArea: false, asynchronous: true @@ -172,11 +182,14 @@ require([ return allowedStorages.include(storage); }, 'Synchronization is required.'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= $block->getButtonHtml() ?> <span class="sync-indicator no-display" id="sync_span"> - <img alt="Synchronize" style="margin:0 5px" src="<?= $block->escapeUrl($block->getViewFileUrl('images/process_spinner.gif')) ?>"/> + <img alt="Synchronize" src="<?= $block->escapeUrl($block->getViewFileUrl('images/process_spinner.gif')) ?>"/> <span id="sync_message_span"></span> </span> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("margin:0 5px", '#sync_span img') ?> <input type="hidden" id="synchronize-validation-input" class="required-synchronize no-display"/> diff --git a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php index fc2207dcd7c86..8ea6290a2a430 100644 --- a/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php +++ b/app/code/Magento/MessageQueue/Console/StartConsumerCommand.php @@ -79,20 +79,15 @@ protected function execute(InputInterface $input, OutputInterface $output) $singleThread = $input->getOption(self::OPTION_SINGLE_THREAD); - if ($singleThread && $this->lockManager->isLocked(md5($consumerName))) { //phpcs:ignore + if ($singleThread && !$this->lockManager->lock(md5($consumerName),0)) { //phpcs:ignore $output->writeln('<error>Consumer with the same name is running</error>'); return \Magento\Framework\Console\Cli::RETURN_FAILURE; } - if ($singleThread) { - $this->lockManager->lock(md5($consumerName)); //phpcs:ignore - } - $this->appState->setAreaCode($areaCode ?? 'global'); $consumer = $this->consumerFactory->get($consumerName, $batchSize); $consumer->process($numberOfMessages); - if ($singleThread) { $this->lockManager->unlock(md5($consumerName)); //phpcs:ignore } @@ -163,7 +158,7 @@ protected function configure() To specify the preferred area: <comment>%command.full_name% someConsumer --area-code='adminhtml'</comment> - + To do not run multiple copies of one consumer simultaneously: <comment>%command.full_name% someConsumer --single-thread'</comment> diff --git a/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php new file mode 100644 index 0000000000000..c097f461e621b --- /dev/null +++ b/app/code/Magento/MessageQueue/Model/CheckIsAvailableMessagesInQueue.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MessageQueue\Model; + +use Magento\Framework\MessageQueue\QueueRepository; + +/** + * Class CheckIsAvailableMessagesInQueue for checking messages available in queue + */ +class CheckIsAvailableMessagesInQueue +{ + /** + * @var QueueRepository + */ + private $queueRepository; + + /** + * Initialize dependencies. + * + * @param QueueRepository $queueRepository + */ + public function __construct(QueueRepository $queueRepository) + { + $this->queueRepository = $queueRepository; + } + + /** + * Checks if there is available messages in the queue + * + * @param string $connectionName connection name + * @param string $queueName queue name + * @return bool + * @throws \LogicException if queue is not available + */ + public function execute($connectionName, $queueName) + { + $queue = $this->queueRepository->get($connectionName, $queueName); + $message = $queue->dequeue(); + if ($message) { + $queue->reject($message); + return true; + } + return false; + } +} diff --git a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php index 056cf4fc57a2e..fd61f96b300d6 100644 --- a/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php +++ b/app/code/Magento/MessageQueue/Model/Cron/ConsumersRunner.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Process\PhpExecutableFinder; use Magento\Framework\Lock\LockManagerInterface; +use Magento\MessageQueue\Model\CheckIsAvailableMessagesInQueue; /** * Class for running consumers processes by cron @@ -65,6 +66,11 @@ class ConsumersRunner */ private $lockManager; + /** + * @var CheckIsAvailableMessagesInQueue + */ + private $checkIsAvailableMessages; + /** * @param PhpExecutableFinder $phpExecutableFinder The executable finder specifically designed * for the PHP executable @@ -74,6 +80,7 @@ class ConsumersRunner * @param LockManagerInterface $lockManager The lock manager * @param ConnectionTypeResolver $mqConnectionTypeResolver Consumer connection resolver * @param LoggerInterface $logger Logger + * @param CheckIsAvailableMessagesInQueue $checkIsAvailableMessages */ public function __construct( PhpExecutableFinder $phpExecutableFinder, @@ -82,7 +89,8 @@ public function __construct( ShellInterface $shellBackground, LockManagerInterface $lockManager, ConnectionTypeResolver $mqConnectionTypeResolver = null, - LoggerInterface $logger = null + LoggerInterface $logger = null, + CheckIsAvailableMessagesInQueue $checkIsAvailableMessages = null ) { $this->phpExecutableFinder = $phpExecutableFinder; $this->consumerConfig = $consumerConfig; @@ -93,6 +101,8 @@ public function __construct( ?: ObjectManager::getInstance()->get(ConnectionTypeResolver::class); $this->logger = $logger ?: ObjectManager::getInstance()->get(LoggerInterface::class); + $this->checkIsAvailableMessages = $checkIsAvailableMessages + ?: ObjectManager::getInstance()->get(CheckIsAvailableMessagesInQueue::class); } /** @@ -166,6 +176,30 @@ private function canBeRun(ConsumerConfigItemInterface $consumerConfig, array $al return false; } + $globalOnlySpawnWhenMessageAvailable = (bool)$this->deploymentConfig->get( + 'queue/only_spawn_when_message_available', + true + ); + if ($consumerConfig->getOnlySpawnWhenMessageAvailable() === true + || ($consumerConfig->getOnlySpawnWhenMessageAvailable() === null && $globalOnlySpawnWhenMessageAvailable)) { + try { + return $this->checkIsAvailableMessages->execute( + $connectionName, + $consumerConfig->getQueue() + ); + } catch (\LogicException $e) { + $this->logger->info( + sprintf( + 'Consumer "%s" skipped as its related queue "%s" is not available. %s', + $consumerName, + $consumerConfig->getQueue(), + $e->getMessage() + ) + ); + return false; + } + } + return true; } } diff --git a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php index b73fcc278f970..1aa805f0e323b 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Console/StartConsumerCommandTest.php @@ -103,7 +103,6 @@ public function testExecute( $pidFilePath, $singleThread, $lockExpects, - $isLockedExpects, $isLocked, $unlockExpects, $runProcessExpects, @@ -144,14 +143,11 @@ public function testExecute( ->method('get')->with($consumerName, $batchSize)->willReturn($consumer); $consumer->expects($this->exactly($runProcessExpects))->method('process')->with($numberOfMessages); - $this->lockManagerMock->expects($this->exactly($isLockedExpects)) - ->method('isLocked') - ->with(md5($consumerName)) //phpcs:ignore - ->willReturn($isLocked); - $this->lockManagerMock->expects($this->exactly($lockExpects)) ->method('lock') - ->with(md5($consumerName)); //phpcs:ignore + ->with(md5($consumerName))//phpcs:ignore + ->willReturn($isLocked); + $this->lockManagerMock->expects($this->exactly($unlockExpects)) ->method('unlock') ->with(md5($consumerName)); //phpcs:ignore @@ -172,8 +168,7 @@ public function executeDataProvider() 'pidFilePath' => null, 'singleThread' => false, 'lockExpects' => 0, - 'isLockedExpects' => 0, - 'isLocked' => false, + 'isLocked' => true, 'unlockExpects' => 0, 'runProcessExpects' => 1, 'expectedReturn' => Cli::RETURN_SUCCESS, @@ -182,8 +177,7 @@ public function executeDataProvider() 'pidFilePath' => '/var/consumer.pid', 'singleThread' => true, 'lockExpects' => 1, - 'isLockedExpects' => 1, - 'isLocked' => false, + 'isLocked' => true, 'unlockExpects' => 1, 'runProcessExpects' => 1, 'expectedReturn' => Cli::RETURN_SUCCESS, @@ -191,9 +185,8 @@ public function executeDataProvider() [ 'pidFilePath' => '/var/consumer.pid', 'singleThread' => true, - 'lockExpects' => 0, - 'isLockedExpects' => 1, - 'isLocked' => true, + 'lockExpects' => 1, + 'isLocked' => false, 'unlockExpects' => 0, 'runProcessExpects' => 0, 'expectedReturn' => Cli::RETURN_FAILURE, diff --git a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php index fcc4816082919..b907661e14b6b 100644 --- a/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php +++ b/app/code/Magento/MessageQueue/Test/Unit/Model/Cron/ConsumersRunnerTest.php @@ -14,6 +14,7 @@ use Magento\Framework\MessageQueue\Consumer\ConfigInterface as ConsumerConfigInterface; use Magento\Framework\ShellInterface; use Magento\MessageQueue\Model\Cron\ConsumersRunner; +use Magento\MessageQueue\Model\CheckIsAvailableMessagesInQueue; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Process\PhpExecutableFinder; @@ -48,10 +49,15 @@ class ConsumersRunnerTest extends TestCase */ private $phpExecutableFinderMock; + /** + * @var CheckIsAvailableMessagesInQueue|MockObject + */ + private $checkIsAvailableMessagesMock; + /** * @var ConnectionTypeResolver */ - private $connectionTypeResover; + private $connectionTypeResolver; /** * @var ConsumersRunner @@ -77,10 +83,11 @@ protected function setUp(): void $this->deploymentConfigMock = $this->getMockBuilder(DeploymentConfig::class) ->disableOriginalConstructor() ->getMock(); - $this->connectionTypeResover = $this->getMockBuilder(ConnectionTypeResolver::class) + $this->checkIsAvailableMessagesMock = $this->createMock(CheckIsAvailableMessagesInQueue::class); + $this->connectionTypeResolver = $this->getMockBuilder(ConnectionTypeResolver::class) ->disableOriginalConstructor() ->getMock(); - $this->connectionTypeResover->method('getConnectionType')->willReturn('something'); + $this->connectionTypeResolver->method('getConnectionType')->willReturn('something'); $this->consumersRunner = new ConsumersRunner( $this->phpExecutableFinderMock, @@ -88,7 +95,9 @@ protected function setUp(): void $this->deploymentConfigMock, $this->shellBackgroundMock, $this->lockManagerMock, - $this->connectionTypeResover + $this->connectionTypeResolver, + null, + $this->checkIsAvailableMessagesMock ); } @@ -137,22 +146,21 @@ public function testRun( ) { $consumerName = 'consumerName'; - $this->deploymentConfigMock->expects($this->exactly(3)) + $this->deploymentConfigMock ->method('get') ->willReturnMap( [ ['cron_consumers_runner/cron_run', true, true], ['cron_consumers_runner/max_messages', 10000, $maxMessages], ['cron_consumers_runner/consumers', [], $allowedConsumers], + ['queue/only_spawn_when_message_available', null, 0], ] ); /** @var ConsumerConfigInterface|MockObject $firstCunsumer */ $consumer = $this->getMockBuilder(ConsumerConfigItemInterface::class) ->getMockForAbstractClass(); - $consumer->expects($this->any()) - ->method('getName') - ->willReturn($consumerName); + $consumer->method('getName')->willReturn($consumerName); $this->phpExecutableFinderMock->expects($this->once()) ->method('find') @@ -262,4 +270,125 @@ public function runDataProvider() ], ]; } + + /** + * @param boolean $onlySpawnWhenMessageAvailable + * @param boolean $isMassagesAvailableInTheQueue + * @param int $shellBackgroundExpects + * @param boolean $globalOnlySpawnWhenMessageAvailable + * @param int $getOnlySpawnWhenMessageAvailableCallCount + * @param int $isMassagesAvailableInTheQueueCallCount + * @dataProvider runBasedOnOnlySpawnWhenMessageAvailableConsumerConfigurationDataProvider + */ + public function testRunBasedOnOnlySpawnWhenMessageAvailableConsumerConfiguration( + $onlySpawnWhenMessageAvailable, + $isMassagesAvailableInTheQueue, + $shellBackgroundExpects, + $globalOnlySpawnWhenMessageAvailable, + $getOnlySpawnWhenMessageAvailableCallCount, + $isMassagesAvailableInTheQueueCallCount + ) { + $consumerName = 'consumerName'; + $connectionName = 'connectionName'; + $queueName = 'queueName'; + $this->deploymentConfigMock->expects($this->exactly(4)) + ->method('get') + ->willReturnMap( + [ + ['cron_consumers_runner/cron_run', true, true], + ['cron_consumers_runner/max_messages', 10000, 1000], + ['cron_consumers_runner/consumers', [], []], + ['queue/only_spawn_when_message_available', true, $globalOnlySpawnWhenMessageAvailable], + ] + ); + + /** @var ConsumerConfigInterface|MockObject $firstCunsumer */ + $consumer = $this->getMockBuilder(ConsumerConfigItemInterface::class) + ->getMockForAbstractClass(); + $consumer->method('getName')->willReturn($consumerName); + $consumer->expects($this->once()) + ->method('getConnection') + ->willReturn($connectionName); + $consumer->method('getQueue')->willReturn($queueName); + $consumer->expects($this->exactly($getOnlySpawnWhenMessageAvailableCallCount)) + ->method('getOnlySpawnWhenMessageAvailable') + ->willReturn($onlySpawnWhenMessageAvailable); + $this->consumerConfigMock->expects($this->once()) + ->method('getConsumers') + ->willReturn([$consumer]); + + $this->phpExecutableFinderMock->expects($this->once()) + ->method('find') + ->willReturn(''); + + $this->lockManagerMock->expects($this->once()) + ->method('isLocked') + ->willReturn(false); + + $this->checkIsAvailableMessagesMock->expects($this->exactly($isMassagesAvailableInTheQueueCallCount)) + ->method('execute') + ->willReturn($isMassagesAvailableInTheQueue); + + $this->shellBackgroundMock->expects($this->exactly($shellBackgroundExpects)) + ->method('execute'); + + $this->consumersRunner->run(); + } + + /** + * @return array + */ + public function runBasedOnOnlySpawnWhenMessageAvailableConsumerConfigurationDataProvider() + { + return [ + [ + 'onlySpawnWhenMessageAvailable' => true, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 1, + 'isMassagesAvailableInTheQueueCallCount' => 1 + ], + [ + 'onlySpawnWhenMessageAvailable' => true, + 'isMassagesAvailableInTheQueue' => false, + 'shellBackgroundExpects' => 0, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 1, + 'isMassagesAvailableInTheQueueCallCount' => 1 + ], + [ + 'onlySpawnWhenMessageAvailable' => false, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 0 + ], + [ + 'onlySpawnWhenMessageAvailable' => null, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => true, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 1 + ], + [ + 'onlySpawnWhenMessageAvailable' => null, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => false, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 0 + ], + [ + 'onlySpawnWhenMessageAvailable' => false, + 'isMassagesAvailableInTheQueue' => true, + 'shellBackgroundExpects' => 1, + 'globalOnlySpawnWhenMessageAvailable' => true, + 'getOnlySpawnWhenMessageAvailableCallCount' => 2, + 'isMassagesAvailableInTheQueueCallCount' => 0 + ], + ]; + } } diff --git a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml index 2bfb1239cba60..1a27bf5aa56a2 100644 --- a/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml +++ b/app/code/Magento/Msrp/Test/Mftf/Test/StorefrontProductWithMapAssignedConfigProductIsCorrectTest.xml @@ -132,7 +132,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Clear cache--> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Go to store front and check msrp for products--> <amOnPage url="{{StorefrontProductPage.url($$createConfigProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToConfigProductPage"/> diff --git a/app/code/Magento/Msrp/etc/adminhtml/system.xml b/app/code/Magento/Msrp/etc/adminhtml/system.xml index 8f6c3750c3835..3b9d07be2f7c7 100644 --- a/app/code/Magento/Msrp/etc/adminhtml/system.xml +++ b/app/code/Magento/Msrp/etc/adminhtml/system.xml @@ -14,7 +14,7 @@ <label>Enable MAP</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment> - <![CDATA[<strong style="color:red">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.]]> + <![CDATA[<strong class="colorRed">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.]]> </comment> </field> <field id="display_price_type" translate="label" type="select" sortOrder="30" showInDefault="1" showInWebsite="1" canRestore="1"> diff --git a/app/code/Magento/Msrp/i18n/en_US.csv b/app/code/Magento/Msrp/i18n/en_US.csv index d47d72b2bdc9a..9ed2d2fb86597 100644 --- a/app/code/Magento/Msrp/i18n/en_US.csv +++ b/app/code/Magento/Msrp/i18n/en_US.csv @@ -13,7 +13,7 @@ Price,Price "Add to Cart","Add to Cart" "Minimum Advertised Price","Minimum Advertised Price" "Enable MAP","Enable MAP" -"<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.","<strong style=""color:red"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront." +"<strong class=""colorRed"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront.","<strong class=""colorRed"">Warning!</strong> Enabling MAP by default will hide all product prices on Storefront." "Display Actual Price","Display Actual Price" "Default Popup Text Message","Default Popup Text Message" "Default ""What's This"" Text Message","Default ""What's This"" Text Message" diff --git a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml index b062e911876c3..4e011df66974c 100644 --- a/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml +++ b/app/code/Magento/Msrp/view/base/templates/product/price/msrp.phtml @@ -8,6 +8,7 @@ * Template for displaying product price at product view page, gift registry and wish-list * * @var $block \Magento\Msrp\Pricing\Render\PriceBox + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php @@ -32,18 +33,20 @@ $msrpPrice = $block->renderAmount( $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElementIdPrefix() : 'product-price-'; ?> -<?php if ($amount) : ?> +<?php if ($amount): ?> <span class="old-price map-old-price"><?= /* @noEscape */ $msrpPrice ?></span> <span class="map-fallback-price normal-price"><?= /* @noEscape */ $msrpPrice ?></span> <?php endif; ?> -<?php if ($priceType->isShowPriceOnGesture()) : ?> +<?php if ($priceType->isShowPriceOnGesture()): ?> <?php $addToCartUrl = ''; if ($product->isSaleable()) { /** @var Magento\Catalog\Block\Product\AbstractProduct $addToCartUrlGenerator */ - $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton(\Magento\Catalog\Block\Product\AbstractProduct::class); + $addToCartUrlGenerator = $block->getLayout()->getBlockSingleton( + \Magento\Catalog\Block\Product\AbstractProduct::class + ); // phpcs:disable $addToCartUrl = $addToCartUrlGenerator->getAddToCartUrl( $product, @@ -86,29 +89,40 @@ $priceElementIdPrefix = $block->getPriceElementIdPrefix() ? $block->getPriceElem ); } ?> - <span id="<?= $block->escapeHtmlAttr($block->getPriceId() ? $block->getPriceId() : $priceElementId) ?>" style="display:none"></span> - <a href="javascript:void(0);" + <?php $priceId = $block->escapeHtmlAttr($block->getPriceId() ? $block->getPriceId() : $priceElementId); ?> + <span id="s_<?= /* @noEscape*/ $priceId ?>"></span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none", 'span#s_' . $priceId) ?> + <a href="#" id="<?= /* @noEscape */ ($popupId) ?>" class="action map-show-info" - <?php //phpcs:disable ?> - data-mage-init='{"addToCart":<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($data) ?>}'> - <?php //phpcs:enable ?> + data-mage-init='{"addToCart":<?= /* @noEscape */ $block->jsonEncode($data) ?>}'> <?= $block->escapeHtml(__('Click for price')) ?> </a> -<?php else : ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ ($popupId) + ) ?> +<?php else: ?> <span class="msrp-message"> <?= $block->escapeHtml($priceType->getMsrpPriceMessage()) ?> </span> <?php endif; ?> -<?php if ($block->getZone() == \Magento\Framework\Pricing\Render::ZONE_ITEM_VIEW) : ?> +<?php if ($block->getZone() == \Magento\Framework\Pricing\Render::ZONE_ITEM_VIEW): ?> <?php $helpLinkId = 'msrp-help-' . $productId . $block->getRandomString(20); ?> - <a href="javascript:void(0);" + <a href="#" id="<?= /* @noEscape */ $helpLinkId ?>" class="action map-show-info" data-mage-init='{"addToCart":{"origin": "info", "helpLinkId": "#<?= /* @noEscape */ $helpLinkId ?>", - "productName": "<?= $block->escapeJs($block->escapeHtml($product->getName())) ?>", - "closeButtonId": "#map-popup-close"}}'><span><?= $block->escapeHtml(__("What's this?")) ?></span> + "productName": "<?= $block->escapeJs($product->getName()) ?>", + "closeButtonId": "#map-popup-close"}}'> + <span><?= $block->escapeHtml(__("What's this?")) ?></span> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ $helpLinkId + ) ?> <?php endif; ?> diff --git a/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml b/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml index dfb66e4cc47b2..d2a0982586eed 100644 --- a/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml +++ b/app/code/Magento/Msrp/view/frontend/templates/render/item/price_msrp_item.phtml @@ -10,8 +10,10 @@ * Template for displaying product price at product view page, gift registry and wishlist * * @var $block \Magento\Catalog\Block\Product\Price + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> + <?php //phpcs:disable /** @var $pricingHelper \Magento\Framework\Pricing\Helper\Data */ @@ -26,31 +28,47 @@ $_msrpPrice = ''; ?> <div class="price-box msrp"> - <?php if ($_product->getMsrp()) : ?> + <?php if ($_product->getMsrp()): ?> <?php $_msrpPrice = $pricingHelper->currency($_product->getMsrp(), true, false) ?> <span class="old-price"><?= /* @noEscape */ $_msrpPrice ?></span> <?php endif; ?> - <?php if ($_catalogHelper->isShowPriceOnGesture($_product)) : ?> + <?php if ($_catalogHelper->isShowPriceOnGesture($_product)): ?> <?php $priceElementId = 'product-price-' . $_id . $block->getIdSuffix(); ?> - <span id="<?= /* @noEscape */ $priceElementId ?>" style="display: none"></span> + <span id="<?= /* @noEscape */ $priceElementId ?>"/> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none", '#'. $priceElementId) ?> + <?php $popupId = 'msrp-popup-' . $_id . $block->getRandomString(20); ?> - <a href="javascript:void(0);" + <a href="#" id="<?= /* @noEscape */ ($popupId) ?>" data-mage-init='{"addToCart":{"popupId": "#<?= /* @noEscape */ ($popupId) ?>", - "productName": "<?= /* @noEscape */ $block->escapeJs($block->escapeHtml($_product->getName())) ?>", + "productName": "<?= /* @noEscape */ $block->escapeJs($_product->getName()) ?>", "realPrice": <?= /* @noEscape */ $block->getRealPriceJs($_product) ?>, "msrpPrice": "<?= /* @noEscape */ $_msrpPrice ?>", "priceElementId":"<?= /* @noEscape */ $priceElementId ?>", "popupCartButtonId": "#map-popup-button", - "cartForm": "#wishlist-view-form"}}'><?= $block->escapeHtml(__('Click for price')) ?> + "cartForm": "#wishlist-view-form"}}'> + <?= $block->escapeHtml(__('Click for price')) ?> </a> - <?php else : ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ ($popupId) + ) ?> + <?php else: ?> <span class="msrp-message"> <?= $block->escapeHtml($_catalogHelper->getMsrpPriceMessage($_product)) ?> </span> <?php endif; ?> <?php $helpLinkId = 'msrp-help-' . $_id . $block->getRandomString(20); ?> - <a href="javascript:void(0);" id="<?= /* @noEscape */ ($helpLinkId) ?>" data-mage-init='{"addToCart":{"helpLinkId": "#<?= /* @noEscape */ ($helpLinkId) ?>", "productName": "<?= /* @noEscape */$block->escapeJs($block->escapeHtml($_product->getName())) ?>"}}' class="link tip"> + <a href="#" id="<?= /* @noEscape */ ($helpLinkId) ?>" + data-mage-init='{"addToCart":{"helpLinkId": "#<?= /* @noEscape */ ($helpLinkId) ?>", + "productName": "<?= /* @noEscape */$block->escapeJs($_product->getName()) ?>"}}' + class="link tip"> <?= $block->escapeHtml(__("What's this?")) ?> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#' . /* @noEscape */ ($helpLinkId) + ) ?> </div> diff --git a/app/code/Magento/Multishipping/Block/Checkout/Overview.php b/app/code/Magento/Multishipping/Block/Checkout/Overview.php index d17da90c58bef..1ea2dc2618778 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Overview.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Overview.php @@ -8,6 +8,8 @@ use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Quote\Model\Quote\Address; +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\App\ObjectManager; /** * Multishipping checkout overview information @@ -15,6 +17,7 @@ * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Overview extends \Magento\Sales\Block\Items\AbstractItems { @@ -56,6 +59,7 @@ class Overview extends \Magento\Sales\Block\Items\AbstractItems * @param \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector * @param \Magento\Quote\Model\Quote\TotalsReader $totalsReader * @param array $data + * @param CheckoutHelper|null $checkoutHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -64,11 +68,14 @@ public function __construct( PriceCurrencyInterface $priceCurrency, \Magento\Quote\Model\Quote\TotalsCollector $totalsCollector, \Magento\Quote\Model\Quote\TotalsReader $totalsReader, - array $data = [] + array $data = [], + ?CheckoutHelper $checkoutHelper = null ) { $this->_taxHelper = $taxHelper; $this->_multishipping = $multishipping; $this->priceCurrency = $priceCurrency; + $data['taxHelper'] = $this->_taxHelper; + $data['checkoutHelper'] = $checkoutHelper ?? ObjectManager::getInstance()->get(CheckoutHelper::class); parent::__construct($context, $data); $this->_isScopePrivate = true; $this->totalsCollector = $totalsCollector; @@ -393,7 +400,7 @@ public function getQuote() * Get billin address totals * * @return mixed - * @deprecated + * @deprecated 100.2.3 * typo in method name, see getBillingAddressTotals() */ public function getBillinAddressTotals() @@ -405,6 +412,7 @@ public function getBillinAddressTotals() * Get billing address totals * * @return mixed + * @since 100.2.3 */ public function getBillingAddressTotals() { diff --git a/app/code/Magento/Multishipping/Block/Checkout/Results.php b/app/code/Magento/Multishipping/Block/Checkout/Results.php index 35c050d5ff8c1..40cbce1990d00 100644 --- a/app/code/Magento/Multishipping/Block/Checkout/Results.php +++ b/app/code/Magento/Multishipping/Block/Checkout/Results.php @@ -21,6 +21,7 @@ * Multi-shipping checkout results information * * @api + * @since 100.2.1 */ class Results extends Success { @@ -66,6 +67,7 @@ public function __construct( * Returns shipping addresses from quote. * * @return array + * @since 100.2.1 */ public function getQuoteShippingAddresses(): array { @@ -76,6 +78,7 @@ public function getQuoteShippingAddresses(): array * Returns all failed addresses from quote. * * @return array + * @since 100.2.1 */ public function getFailedAddresses(): array { @@ -91,6 +94,7 @@ public function getFailedAddresses(): array * * @param int $orderId * @return OrderAddress|null + * @since 100.2.1 */ public function getOrderShippingAddress(int $orderId) { @@ -101,6 +105,7 @@ public function getOrderShippingAddress(int $orderId) * Retrieve quote billing address. * * @return QuoteAddress + * @since 100.2.1 */ public function getQuoteBillingAddress(): QuoteAddress { @@ -112,6 +117,7 @@ public function getQuoteBillingAddress(): QuoteAddress * * @param OrderAddress $address * @return string + * @since 100.2.1 */ public function formatOrderShippingAddress(OrderAddress $address): string { @@ -123,6 +129,7 @@ public function formatOrderShippingAddress(OrderAddress $address): string * * @param QuoteAddress $address * @return string + * @since 100.2.1 */ public function formatQuoteShippingAddress(QuoteAddress $address): string { @@ -134,6 +141,7 @@ public function formatQuoteShippingAddress(QuoteAddress $address): string * * @param QuoteAddress $address * @return bool + * @since 100.2.1 */ public function isShippingAddress(QuoteAddress $address): bool { @@ -158,6 +166,7 @@ private function getAddressOneline(array $address): string * * @param QuoteAddress $address * @return string + * @since 100.2.1 */ public function getAddressError(QuoteAddress $address): string { @@ -171,6 +180,7 @@ public function getAddressError(QuoteAddress $address): string * * @throws LocalizedException * @return Success + * @since 100.2.1 */ protected function _prepareLayout(): Success { diff --git a/app/code/Magento/Multishipping/Controller/Checkout/RemoveItem.php b/app/code/Magento/Multishipping/Controller/Checkout/RemoveItem.php index 1bb333faaf2e4..d6d8ec9de0d58 100644 --- a/app/code/Magento/Multishipping/Controller/Checkout/RemoveItem.php +++ b/app/code/Magento/Multishipping/Controller/Checkout/RemoveItem.php @@ -6,7 +6,14 @@ */ namespace Magento\Multishipping\Controller\Checkout; -class RemoveItem extends \Magento\Multishipping\Controller\Checkout +use Magento\Framework\App\Action\HttpPostActionInterface; + +/** + * Class RemoveItem + * + * Removes multishipping items + */ +class RemoveItem extends \Magento\Multishipping\Controller\Checkout implements HttpPostActionInterface { /** * Multishipping checkout remove item action diff --git a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php index 5d384a5373d5e..85726a8dab0b5 100644 --- a/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php +++ b/app/code/Magento/Multishipping/Model/Checkout/Type/Multishipping/PlaceOrderInterface.php @@ -13,6 +13,7 @@ * Place orders during multishipping checkout flow. * * @api + * @since 100.2.1 */ interface PlaceOrderInterface { @@ -21,6 +22,7 @@ interface PlaceOrderInterface * * @param OrderInterface[] $orderList * @return array + * @since 100.2.1 */ public function place(array $orderList): array; } diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml index 7bb26525b173f..46f1daad053d5 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontProcessMultishippingCheckoutWhenCartPageIsOpenedInAnotherTabTest.xml @@ -101,7 +101,7 @@ <seeElement selector="{{StorefrontCustomerOrdersGridSection.orderView({$grabSecondOrderId})}}" stepKey="seeSecondOrder"/> <waitForPageLoad stepKey="waitForOrderPageLoad"/> <!-- Go to Admin > Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <actionGroup ref="SearchAdminDataGridByKeywordActionGroup" stepKey="searchFirstOrder"> <argument name="keyword" value="$grabFirstOrderId"/> </actionGroup> diff --git a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml index e65747f4d63d0..494259e0ead9d 100644 --- a/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml +++ b/app/code/Magento/Multishipping/Test/Mftf/Test/StorefrontVerifySecureURLRedirectMultishippingTest.xml @@ -41,11 +41,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <deleteData createDataKey="product" stepKey="deleteProduct"/> <deleteData createDataKey="category" stepKey="deleteCategory"/> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml index 418efa5033263..a37ff04a8dc2a 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/addresses.phtml @@ -4,8 +4,8 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Files.LineLength - +// phpcs:disable Generic.Files.LineLength +// phpcs:disable Magento2.Templates.ThisInTemplate.FoundHelper ?> <?php /** @@ -16,7 +16,7 @@ ?> <form id="checkout_multishipping_form" data-mage-init='{ - "multiShipping":{}, + "multiShipping": {"itemsQty": <?= /* @noEscape */ (int)$block->getCheckout()->getQuote()->getItemsSummaryQty() ?>}, "cartUpdate": { "validationURL": "<?= $block->escapeUrl($block->getUrl('multishipping/checkout/checkItems')) ?>", "eventName": "updateMulticartItemQty" @@ -43,8 +43,8 @@ </tr> </thead> <tbody> - <?php foreach ($block->getItems() as $_index => $_item) : ?> - <?php if ($_item->getQuoteItem()) : ?> + <?php foreach ($block->getItems() as $_index => $_item): ?> + <?php if ($_item->getQuoteItem()): ?> <tr> <td class="col product" data-th="<?= $block->escapeHtml(__('Product')) ?>"> <?= $block->getItemHtml($_item->getQuoteItem()) ?> @@ -69,11 +69,11 @@ </div> </td> <td class="col address" data-th="<?= $block->escapeHtml(__('Send To')) ?>"> - <?php if ($_item->getProduct()->getIsVirtual()) : ?> + <?php if ($_item->getProduct()->getIsVirtual()): ?> <div class="applicable"> <?= $block->escapeHtml(__('A shipping selection is not applicable.')) ?> </div> - <?php else : ?> + <?php else: ?> <div class="field address"> <label for="ship_<?= $block->escapeHtml($_index) ?>_<?= $block->escapeHtml($_item->getQuoteItemId()) ?>_address" class="label"> @@ -86,8 +86,12 @@ <?php endif; ?> </td> <td class="col actions" data-th="<?= $block->escapeHtml(__('Actions')) ?>"> - <a href="<?= $block->escapeUrl($block->getItemDeleteUrl($_item)) ?>" + <a href="#" title="<?= $block->escapeHtml(__('Remove Item')) ?>" + data-post='<?= /* @noEscape */ + $this->helper(\Magento\Framework\Data\Helper\PostHelper::class) + ->getPostData($block->getItemDeleteUrl($_item)) + ?>' class="action delete" data-multiship-item-remove=""> <span><?= $block->escapeHtml(__('Remove item')) ?></span> @@ -106,7 +110,7 @@ class="action primary continue<?= $block->isContinueDisabled() ? ' disabled' : '' ?>" data-role="can-continue" data-flag="1" - <?php if ($block->isContinueDisabled()) : ?> + <?php if ($block->isContinueDisabled()): ?> disabled="disabled" <?php endif; ?>> <span><?= $block->escapeHtml(__('Go to Shipping Information')) ?></span> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml index 761c1f1a78423..c9ee0a8b12ce3 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/billing.phtml @@ -8,20 +8,24 @@ * Multishipping checkout billing information * * @var $block \Magento\Multishipping\Block\Checkout\Billing + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="checkout-loader" data-role="checkout-loader" class="loading-mask" data-mage-init='{"billingLoader": {}}'> <div class="loader"> <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')); ?>" - alt="<?= $block->escapeHtml(__('Loading...')); ?>" - style="position: absolute;"> + alt="<?= $block->escapeHtml(__('Loading...')); ?>"> </div> </div> -<script> - window.checkoutConfig = <?= /* @noEscape */ $block->getCheckoutData()->getSerializedCheckoutConfigs(); ?>; +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag('position: absolute;', 'div#checkout-loader .loader img') ?> +<?php $checkoutConfig = /* @noEscape */ $block->getCheckoutData()->getSerializedCheckoutConfigs(); +$scriptString = <<<script + window.checkoutConfig = {$checkoutConfig}; window.isCustomerLoggedIn = window.checkoutConfig.isCustomerLoggedIn; window.customerData = window.checkoutConfig.customerData; -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <div id="checkout" data-bind="scope:'checkoutMessages'"> <!-- ko template: getTemplate() --><!-- /ko --> <script type="text/x-magento-init"> @@ -72,7 +76,7 @@ $methodsCount = count($methods); $methodsForms = $block->hasFormTemplates() ? $block->getFormTemplates(): []; - foreach ($methods as $_method) : + foreach ($methods as $_method): $code = $_method->getCode(); $checked = $block->getSelectedMethodCode() === $code; @@ -82,7 +86,7 @@ ?> <div data-bind="scope: 'payment_method_<?= $block->escapeHtml($code);?>'"> <dt class="item-title"> - <?php if ($methodsCount > 1) : ?> + <?php if ($methodsCount > 1): ?> <input type="radio" id="p_method_<?= $block->escapeHtml($code); ?>" value="<?= $block->escapeHtml($code); ?>" @@ -93,11 +97,11 @@ checked: isChecked, click: selectPaymentMethod, visible: isRadioButtonVisible()" - <?php if ($checked) : ?> + <?php if ($checked): ?> checked="checked" <?php endif; ?> class="radio"/> - <?php else : ?> + <?php else: ?> <input type="radio" id="p_method_<?= $block->escapeHtml($code); ?>" value="<?= $block->escapeHtml($code); ?>" @@ -112,7 +116,7 @@ <?= $block->escapeHtml($_method->getTitle()) ?> </label> </dt> - <?php if ($html = $block->getChildHtml('payment.method.' . $code)) : ?> + <?php if ($html = $block->getChildHtml('payment.method.' . $code)): ?> <dd class="item-content <?= $checked ? '' : 'no-display'; ?>"> <?= /* @noEscape */ $html; ?> </dd> @@ -142,12 +146,14 @@ </div> </div> </form> -<script> +<?php $quoteBaseGrandTotal = (float)$block->getQuoteBaseGrandTotal(); +$scriptString = <<<script + require(['jquery', 'mage/mage'], function(jQuery) { var addtocartForm = jQuery('#multishipping-billing-form'); addtocartForm.mage('payment', { - checkoutPrice: <?= (float)$block->getQuoteBaseGrandTotal() ?> + checkoutPrice: {$quoteBaseGrandTotal} }); addtocartForm.mage('validation', { @@ -160,9 +166,13 @@ } }); }); -</script> -<script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + +<?php $scriptString = <<<script + //<![CDATA[ require( [ @@ -171,21 +181,27 @@ 'domReady!' ], function(quote, $) { quote.billingAddress({ - city: '<?= /* @noEscape */ $block->getAddress()->getCity() ?>', - company: '<?= /* @noEscape */ $block->getAddress()->getCompany(); ?>', - countryId: '<?= /* @noEscape */ $block->getAddress()->getCountryId(); ?>', - customerAddressId: '<?= /* @noEscape */ $block->getAddress()->getCustomerAddressId(); ?>', - customerId: '<?= /* @noEscape */ $block->getAddress()->getCustomerId(); ?>', - fax: '<?= /* @noEscape */ $block->getAddress()->getFax(); ?>', - firstname: '<?= /* @noEscape */ $block->getAddress()->getFirstname(); ?>', - lastname: '<?= /* @noEscape */ $block->getAddress()->getLastname(); ?>', - postcode: '<?= /* @noEscape */ $block->getAddress()->getPostcode(); ?>', - regionId: '<?= /* @noEscape */ $block->getAddress()->getRegionId(); ?>', - regionCode: '<?= /* @noEscape */ $block->getAddress()->getRegionCode() ?>', - region: '<?= /* @noEscape */ $block->getAddress()->getRegion(); ?>', - street: <?= /* @noEscape */ json_encode($block->getAddress()->getStreet()); ?>, - telephone: '<?= /* @noEscape */ $block->getAddress()->getTelephone(); ?>' + +script; +$scriptString .= "city: '" . /* @noEscape */ $block->getAddress()->getCity() . "'," . PHP_EOL; +$scriptString .= "company: '" . /* @noEscape */ $block->getAddress()->getCompany() . "'," . PHP_EOL; +$scriptString .= "countryId: '" . /* @noEscape */ $block->getAddress()->getCountryId() . "'," . PHP_EOL; +$scriptString .= "customerAddressId: '" . /* @noEscape */ $block->getAddress()->getCustomerAddressId() . "'," . PHP_EOL; +$scriptString .= "customerId: '" . /* @noEscape */ $block->getAddress()->getCustomerId() . "'," . PHP_EOL; +$scriptString .= "fax: '" . /* @noEscape */ $block->getAddress()->getFax() . "'," . PHP_EOL; +$scriptString .= "firstname: '" . /* @noEscape */ $block->getAddress()->getFirstname() . "'," . PHP_EOL; +$scriptString .= "lastname: '" . /* @noEscape */ $block->getAddress()->getLastname() . "'," . PHP_EOL; +$scriptString .= "postcode: '" . /* @noEscape */ $block->getAddress()->getPostcode() . "'," . PHP_EOL; +$scriptString .= "regionId: '" . /* @noEscape */ $block->getAddress()->getRegionId() . "'," . PHP_EOL; +$scriptString .= "regionCode: '" . /* @noEscape */ $block->getAddress()->getRegionCode() . "'," . PHP_EOL; +$scriptString .= "region: '" . /* @noEscape */ $block->getAddress()->getRegion() . "'," . PHP_EOL; +$scriptString .= "street: " . /* @noEscape */ json_encode($block->getAddress()->getStreet()) . "," . PHP_EOL; +$scriptString .= "telephone: '" . /* @noEscape */ $block->getAddress()->getTelephone() . "'" . PHP_EOL; +$scriptString .= <<<script }); }); //]]> -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml index 5fff0d72e8000..3b72679bfc34e 100644 --- a/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml +++ b/app/code/Magento/Multishipping/view/frontend/templates/checkout/overview.phtml @@ -4,12 +4,18 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** @var \Magento\Multishipping\Block\Checkout\Overview $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> + +<?php +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> <?php $errors = $block->getCheckoutData()->getAddressErrors(); ?> -<?php foreach ($errors as $addressId => $error) : ?> +<?php foreach ($errors as $addressId => $error): ?> <div class="message message-error error"> <?= $block->escapeHtml($error); ?> <?= $block->escapeHtml(__('Please see')); ?> @@ -59,8 +65,8 @@ </div> <div class="block block-shipping"> <div class="block-title"><strong><?= $block->escapeHtml(__('Shipping Information')); ?></strong></div> - <?php $mergedCells = ($this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?> - <?php foreach ($block->getShippingAddresses() as $index => $address) : ?> + <?php $mergedCells = ($taxHelper->displayCartBothPrices() ? 2 : 1); ?> + <?php foreach ($block->getShippingAddresses() as $index => $address): ?> <div class="block-content"> <a name="<?= $block->escapeHtml($block->getCheckoutData() ->getAddressAnchorName($address->getId())); ?>"></a> @@ -72,7 +78,7 @@ </span> </strong> </div> - <?php if ($error = $block->getCheckoutData()->getAddressError($address)) : ?> + <?php if ($error = $block->getCheckoutData()->getAddressError($address)): ?> <div class="error-description"><?= $block->escapeHtml($error); ?></div> <?php endif;?> <div class="box box-shipping-address"> @@ -93,17 +99,16 @@ <a href="<?= $block->escapeUrl($block->getEditShippingUrl()); ?>" class="action edit"><span><?= $block->escapeHtml(__('Change')); ?></span></a> </strong> - <?php if ($_rate = $block->getShippingAddressRate($address)) : ?> + <?php if ($_rate = $block->getShippingAddressRate($address)): ?> <div class="box-content"> <?= $block->escapeHtml($_rate->getCarrierTitle()) ?> (<?= $block->escapeHtml($_rate->getMethodTitle()) ?>) <?php $exclTax = $block->getShippingPriceExclTax($address); $inclTax = $block->getShippingPriceInclTax($address); - $displayBothPrices = $this->helper(Magento\Tax\Helper\Data::class) - ->displayShippingBothPrices() && $inclTax !== $exclTax; + $displayBothPrices = $taxHelper->displayShippingBothPrices() && $inclTax !== $exclTax; ?> - <?php if ($displayBothPrices) : ?> + <?php if ($displayBothPrices): ?> <span class="price-including-tax" data-label="<?= $block->escapeHtml(__('Incl. Tax')); ?>"> <?= /* @noEscape */ $inclTax ?> @@ -112,7 +117,7 @@ data-label="<?= $block->escapeHtml(__('Excl. Tax')); ?>"> <?= /* @noEscape */ $exclTax; ?> </span> - <?php else : ?> + <?php else: ?> <?= /* @noEscape */ $inclTax ?> <?php endif; ?> </div> @@ -138,7 +143,7 @@ </tr> </thead> <tbody> - <?php foreach ($block->getShippingAddressItems($address) as $item) : ?> + <?php foreach ($block->getShippingAddressItems($address) as $item): ?> <?= /* @noEscape */ $block->getRowItemHtml($item) ?> <?php endforeach; ?> </tbody> @@ -155,13 +160,13 @@ <?php endforeach; ?> </div> - <?php if ($block->getQuote()->hasVirtualItems()) : ?> + <?php if ($block->getQuote()->hasVirtualItems()): ?> <div class="block block-other"> <?php $billingAddress = $block->getQuote()->getBillingAddress(); ?> <a name="<?= $block->escapeHtml($block->getCheckoutData() ->getAddressAnchorName($billingAddress->getId())); ?>"></a> <div class="block-title"><strong><?= $block->escapeHtml(__('Other items in your order')); ?></strong></div> - <?php if ($error = $block->getCheckoutData()->getAddressError($billingAddress)) :?> + <?php if ($error = $block->getCheckoutData()->getAddressError($billingAddress)): ?> <div class="error-description"><?= $block->escapeHtml($error); ?></div> <?php endif;?> <div class="block-content"> @@ -170,7 +175,7 @@ <a href="<?= $block->escapeUrl($block->getVirtualProductEditUrl()); ?>" class="action edit"><span><?= $block->escapeHtml(__('Edit Items')); ?></span></a> </strong> - <?php $mergedCells = ($this->helper(Magento\Tax\Helper\Data::class)->displayCartBothPrices() ? 2 : 1); ?> + <?php $mergedCells = ($taxHelper->displayCartBothPrices() ? 2 : 1); ?> <div class="order-review-wrapper table-wrapper"> <table class="items data table table-order-review" id="virtual-overview-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Items')); ?></caption> @@ -183,7 +188,7 @@ </tr> </thead> <tbody> - <?php foreach ($block->getVirtualItems() as $_item) : ?> + <?php foreach ($block->getVirtualItems() as $_item): ?> <?= /* @noEscape */ $block->getRowItemHtml($_item) ?> <?php endforeach; ?> </tbody> @@ -203,8 +208,7 @@ <div class="grand totals"> <strong class="mark"><?= $block->escapeHtml(__('Grand Total:')); ?></strong> <strong class="amount"> - <?= /* @noEscape */ $this->helper(Magento\Checkout\Helper\Data::class) - ->formatPrice($block->getTotal()); ?> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()); ?> </strong> </div> <div class="actions-toolbar" id="review-buttons-container"> @@ -221,10 +225,10 @@ </div> <span id="review-please-wait" class="please-wait load indicator" - style="display: none;" data-text="<?= $block->escapeHtml(__('Submitting order information...')); ?>"> <span><?= $block->escapeHtml(__('Submitting order information...')); ?></span> </span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'span#review-please-wait') ?> </div> </div> </form> diff --git a/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js b/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js index 537abb3aa2071..8af1c1ed06fc1 100644 --- a/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js +++ b/app/code/Magento/Multishipping/view/frontend/web/js/multi-shipping.js @@ -5,12 +5,14 @@ define([ 'jquery', + 'Magento_Customer/js/customer-data', 'jquery-ui-modules/widget' -], function ($) { +], function ($, customerData) { 'use strict'; $.widget('mage.multiShipping', { options: { + itemsQty: 0, addNewAddressBtn: 'button[data-role="add-new-address"]', // Add a new multishipping address. addNewAddressFlag: '#add_new_address_flag', // Hidden input field with value 0 or 1. canContinueBtn: 'button[data-role="can-continue"]', // Continue (update quantity or go to shipping). @@ -22,10 +24,24 @@ define([ * @private */ _create: function () { + this._prepareCartData(); $(this.options.addNewAddressBtn).on('click', $.proxy(this._addNewAddress, this)); $(this.options.canContinueBtn).on('click', $.proxy(this._canContinue, this)); }, + /** + * Takes cart items qty from current cart data and compare it with current items qty + * Reloads cart data if cart items qty is wrong + * @private + */ + _prepareCartData: function () { + var cartData = customerData.get('cart'); + + if (cartData()['summary_count'] !== this.options.itemsQty) { + customerData.reload(['cart'], false); + } + }, + /** * Add a new address. Set the hidden input field and submit the form. Then enter a new shipping address. * @private diff --git a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php index dd6f51a5342f2..69512775f4e93 100644 --- a/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php +++ b/app/code/Magento/Newsletter/Block/Adminhtml/Queue/Preview.php @@ -26,8 +26,8 @@ class Preview extends \Magento\Newsletter\Block\Adminhtml\Template\Preview /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Newsletter\Model\TemplateFactory $templateFactory - * @param \Magento\Newsletter\Model\QueueFactory $queueFactory * @param \Magento\Newsletter\Model\SubscriberFactory $subscriberFactory + * @param \Magento\Newsletter\Model\QueueFactory $queueFactory * @param array $data */ public function __construct( @@ -42,6 +42,8 @@ public function __construct( } /** + * Return template. + * * @param \Magento\Newsletter\Model\Template $template * @param string $id * @return $this @@ -50,9 +52,11 @@ protected function loadTemplate(\Magento\Newsletter\Model\Template $template, $i { /** @var \Magento\Newsletter\Model\Queue $queue */ $queue = $this->_queueFactory->create()->load($id); + $template->setId($queue->getTemplateId()); $template->setTemplateType($queue->getNewsletterType()); $template->setTemplateText($queue->getNewsletterText()); $template->setTemplateStyles($queue->getNewsletterStyles()); + return $this; } } diff --git a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php index 8fc729ea34078..dc4d50c22b162 100644 --- a/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.php +++ b/app/code/Magento/Newsletter/Controller/Adminhtml/Template/Save.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\App\TemplateTypesInterface; use Magento\Framework\Exception\LocalizedException; +/** + * An action that saves a template. + */ class Save extends \Magento\Newsletter\Controller\Adminhtml\Template implements HttpPostActionInterface { /** @@ -32,9 +34,7 @@ public function execute() } try { - $template->addData( - $request->getParams() - )->setTemplateSubject( + $template->setTemplateSubject( $request->getParam('subject') )->setTemplateCode( $request->getParam('code') diff --git a/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php b/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php index aa3a2bcfe0f59..0f20a8379d04b 100644 --- a/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php +++ b/app/code/Magento/Newsletter/Model/Queue/TransportBuilder.php @@ -185,7 +185,7 @@ public function setReplyTo($email, $name = null) * @throws MailException * @see setFromByScope() * - * @deprecated This function sets the from address but does not provide + * @deprecated 100.3.3 This function sets the from address but does not provide * a way of setting the correct from addresses based on the scope. */ public function setFrom($from) diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php index 33c539fbba84f..2914a25ba7214 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Queue/Collection.php @@ -213,6 +213,7 @@ public function addSubscriberFilter($subscriberId) * * @param int $customerId * @return $this + * @since 100.4.0 */ public function addCustomerFilter(int $customerId): Collection { diff --git a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php index 2519dd3a6fea8..fb5a4fd915734 100644 --- a/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/ResourceModel/Subscriber.php @@ -116,6 +116,7 @@ public function setMessagesScope($scope) * @param string $email * @param int $websiteId * @return array + * @since 100.4.0 * @throws LocalizedException */ public function loadBySubscriberEmail(string $email, int $websiteId): array @@ -141,6 +142,7 @@ public function loadBySubscriberEmail(string $email, int $websiteId): array * @param int $customerId * @param int $websiteId * @return array + * @since 100.4.0 */ public function loadByCustomerId(int $customerId, int $websiteId): array { @@ -200,7 +202,7 @@ public function received(SubscriberModel $subscriber, \Magento\Newsletter\Model\ * * @param string $subscriberEmail * @return array - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadBySubscriberEmail */ public function loadByEmail($subscriberEmail) @@ -214,7 +216,7 @@ public function loadByEmail($subscriberEmail) * * @param CustomerInterface $customer * @return array - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadByCustomerId */ public function loadByCustomerData(CustomerInterface $customer) diff --git a/app/code/Magento/Newsletter/Model/Subscriber.php b/app/code/Magento/Newsletter/Model/Subscriber.php index 5c573f47aa0bf..c2d80f9000792 100644 --- a/app/code/Magento/Newsletter/Model/Subscriber.php +++ b/app/code/Magento/Newsletter/Model/Subscriber.php @@ -382,6 +382,7 @@ public function isSubscribed() * @param string $email * @param int $websiteId * @return $this + * @since 100.4.0 */ public function loadBySubscriberEmail(string $email, int $websiteId): Subscriber { @@ -400,6 +401,7 @@ public function loadBySubscriberEmail(string $email, int $websiteId): Subscriber * @param int $customerId * @param int $websiteId * @return $this + * @since 100.4.0 */ public function loadByCustomer(int $customerId, int $websiteId): Subscriber { @@ -588,6 +590,7 @@ public function getSubscriberFullName() * Set date of last changed status * * @return $this + * @since 100.2.1 */ public function beforeSave() { @@ -603,7 +606,7 @@ public function beforeSave() * * @param string $subscriberEmail * @return $this - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadBySubscriberEmail */ public function loadByEmail($subscriberEmail) @@ -619,7 +622,7 @@ public function loadByEmail($subscriberEmail) * * @param int $customerId * @return $this - * @deprecated The subscription should be loaded by website id + * @deprecated 100.4.0 The subscription should be loaded by website id * @see loadByCustomer */ public function loadByCustomerId($customerId) @@ -644,7 +647,7 @@ public function loadByCustomerId($customerId) * * @param string $email * @return int - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribe */ public function subscribe($email) @@ -661,7 +664,7 @@ public function subscribe($email) * * @param int $customerId * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribeCustomer */ public function subscribeCustomerById($customerId) @@ -674,7 +677,7 @@ public function subscribeCustomerById($customerId) * * @param int $customerId * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::unsubscribeCustomer */ public function unsubscribeCustomerById($customerId) @@ -687,7 +690,7 @@ public function unsubscribeCustomerById($customerId) * * @param int $customerId * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribeCustomer */ public function updateSubscription($customerId) @@ -703,7 +706,7 @@ public function updateSubscription($customerId) * @param int $customerId * @param bool $subscribe indicates whether the customer should be subscribed or unsubscribed * @return $this - * @deprecated The subscription should be updated by store id + * @deprecated 100.4.0 The subscription should be updated by store id * @see \Magento\Newsletter\Model\SubscriptionManager::subscribeCustomer */ protected function _updateCustomerSubscription($customerId, $subscribe) diff --git a/app/code/Magento/Newsletter/Model/Template.php b/app/code/Magento/Newsletter/Model/Template.php index 88fbfb152d14f..58afd45b26f44 100644 --- a/app/code/Magento/Newsletter/Model/Template.php +++ b/app/code/Magento/Newsletter/Model/Template.php @@ -40,7 +40,7 @@ class Template extends \Magento\Email\Model\AbstractTemplate /** * Mail object * - * @deprecated Unused property + * @deprecated 100.3.0 Unused property * */ protected $_mail; diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup.xml new file mode 100644 index 0000000000000..ed60c1509e453 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup"> + <click selector="{{AdminNewsletterSubscriberGridSection.rowCheckbox('1')}}" stepKey="clickOnFirstCheckbox"/> + <selectOption selector="{{AdminNewsletterSubscriberGridSection.actionsDropdown}}" userInput="Delete" stepKey="selectDelete"/> + <click selector="{{AdminNewsletterSubscriberGridSection.submit}}" stepKey="clickSubmitBtn"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <click selector="{{AdminNewsletterSubscriberGridSection.okButton}}" stepKey="clickOkButton"/> + <waitForPageLoad stepKey="waitForResultsLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingFindNewsletterSubscribersInGridActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingFindNewsletterSubscribersInGridActionGroup.xml new file mode 100644 index 0000000000000..9f9231397a1f8 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AdminMarketingFindNewsletterSubscribersInGridActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminMarketingFindNewsletterSubscribersInGridActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + + <click stepKey="resetFilter" selector="{{AdminNewsletterSubscriberGridSection.resetFilter}}"/> + <waitForPageLoad stepKey="waitForPageLoading"/> + <fillField stepKey="fillEmailField" selector="{{AdminNewsletterSubscriberGridSection.emailField}}" userInput="{{email}}"/> + <click stepKey="clickSearchButton" selector="{{AdminNewsletterSubscriberGridSection.searchButton}}"/> + <waitForPageLoad stepKey="waitForResultsLoading"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup.xml new file mode 100644 index 0000000000000..e114a4e640fa2 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <dontSee selector="{{AdminNewsletterSubscriberGridSection.email('1')}}" userInput="{{email}}" stepKey="dontSeeSubscriber"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml index d6b0adff53a86..8037baa6b199c 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewAccountNewsletterUncheckedActionGroup.xml @@ -15,10 +15,11 @@ <arguments> <argument name="Customer"/> <argument name="Store"/> + <argument name="StoreGroup"/> </arguments> <amOnPage stepKey="amOnStorefrontPage" url="{{Store.code}}"/> <see stepKey="seeDescriptionNewsletter" userInput="You aren't subscribed to our newsletter." selector="{{CustomerMyAccountPage.DescriptionNewsletter}}"/> - <see stepKey="seeThankYouMessage" userInput="Thank you for registering with NewStore."/> + <see stepKey="seeThankYouMessage" userInput="Thank you for registering with {{StoreGroup.name}}."/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml index 0aee2cb9b2e3c..482ecec583552 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewSubscriberActionGroup.xml @@ -8,7 +8,8 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> - <actionGroup name="StorefrontCreateNewSubscriberActionGroup"> + <actionGroup name="StorefrontCreateNewSubscriberActionGroup" deprecated="Use StorefrontCreateNewsletterSubscriberActionGroup"> + <!-- Deprecated Due to inconsistency with the best practices --> <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> <submitForm selector="{{BasicFrontendNewsletterFormSection.subscribeForm}}" parameterArray="['email' => '{{_defaultNewsletter.senderEmail}}']" diff --git a/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml new file mode 100644 index 0000000000000..44104f3adf0d9 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/ActionGroup/StorefrontCreateNewsletterSubscriberActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontCreateNewsletterSubscriberActionGroup"> + <arguments> + <argument name="email" type="string"/> + </arguments> + <fillField stepKey="fillEmailField" selector="{{BasicFrontendNewsletterFormSection.newsletterEmail}}" userInput="{{email}}"/> + <click selector="{{BasicFrontendNewsletterFormSection.subscribeButton}}" stepKey="submitForm"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml index 3332041817150..26512a28c9f3d 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/AdminNewsletterSubscriberGridSection.xml @@ -12,5 +12,12 @@ <element name="type" type="text" selector="//table[contains(@class, 'data-grid')]/tbody/tr[{{row}}][@data-role='row']/td[@data-column='type']" parameterized="true"/> <element name="firstName" type="text" selector="//table[contains(@class, 'data-grid')]/tbody/tr[{{row}}][@data-role='row']/td[@data-column='firstname']" parameterized="true"/> <element name="lastName" type="text" selector="//table[contains(@class, 'data-grid')]/tbody/tr[{{row}}][@data-role='row']/td[@data-column='lastname']" parameterized="true"/> + <element name="resetFilter" type="button" selector=".action-default.scalable.action-reset.action-tertiary"/> + <element name="emailField" type="input" selector=".col-email #subscriberGrid_filter_email"/> + <element name="searchButton" type="button" selector="//*[@class='admin__filter-actions']//*[text()='Search']"/> + <element name="rowCheckbox" type="checkbox" selector="table.data-grid tbody > tr:nth-of-type({{row}}) td.data-grid-checkbox-cell input" parameterized="true"/> + <element name="actionsDropdown" type="select" selector=".admin__grid-massaction-form #subscriberGrid_massaction-select"/> + <element name="submit" type="button" selector="//*[@class='admin__grid-massaction-form']//*[text()='Submit']"/> + <element name="okButton" type="button" selector="//footer[@class='modal-footer']/button[contains(@class, 'action-accept')]" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml index 8475fb4d55b9e..f4c685e730be3 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Section/NewsletterTemplateSection/BasicFrontendNewsletterFormSection.xml @@ -8,8 +8,8 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="BasicFrontendNewsletterFormSection"> - <element name="newsletterEmail" type="input" selector="#newsletter"/> - <element name="subscribeButton" type="button" selector=".subscribe" timeout="30"/> + <element name="newsletterEmail" type="input" selector=".control #newsletter"/> + <element name="subscribeButton" type="button" selector=".actions .action.subscribe.primary" timeout="30"/> <element name="subscribeForm" type="input" selector="#newsletter-validate-detail" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml index a763f43d9e4d1..e255f14a83661 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminAddImageToWYSIWYGNewsletterTest.xml @@ -35,6 +35,9 @@ <waitForPageLoad stepKey="waitForPageLoad" /> <actionGroup ref="ClickBrowseBtnOnUploadPopupActionGroup" stepKey="clickBrowserBtn"/> <actionGroup ref="VerifyMediaGalleryStorageActionsActionGroup" stepKey="VerifyMediaGalleryStorageBtn"/> + <actionGroup ref="NavigateToMediaFolderActionGroup" stepKey="navigateToFolder"> + <argument name="FolderName" value="Storage Root"/> + </actionGroup> <actionGroup ref="CreateImageFolderActionGroup" stepKey="CreateImageFolder"> <argument name="ImageFolder" value="ImageFolder"/> </actionGroup> @@ -57,6 +60,10 @@ <seeElementInDOM selector="{{StorefrontNewsletterSection.imageSource(ImageUpload3.fileName)}}" stepKey="assertMediaSource"/> <closeTab stepKey="closeTab"/> <after> + <actionGroup ref="NavigateToMediaGalleryActionGroup" stepKey="navigateToMediaGallery"/> + <actionGroup ref="DeleteFolderActionGroup" stepKey="DeleteCreatedFolder"> + <argument name="ImageFolder" value="ImageFolder"/> + </actionGroup> <actionGroup ref="DisabledWYSIWYGActionGroup" stepKey="disableWYSIWYG"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml new file mode 100644 index 0000000000000..c472d262a34c8 --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/AdminMarketingDeleteNewsletterSubscriberTest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminMarketingDeleteNewsletterSubscriberTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Subscribers Deleting"/> + <title value="Admin deletes newsletter subscribers"/> + <description value="Admin should be able delete newsletter subscribers"/> + <severity value="CRITICAL"/> + <group value="newsletter"/> + </annotations> + <before> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCreatedCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginAsCustomer"> + <argument name="Customer" value="$$createCustomer$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerNavigateToNewsletterPageActionGroup" stepKey="navigateToNewsletterPage"/> + <actionGroup ref="StorefrontCustomerUpdateGeneralSubscriptionActionGroup" stepKey="subscribeToNewsletter"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToNewsletterSubscribersPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminMenuMarketingCommunicationsNewsletterSubscribers.dataUiId}}"/> + </actionGroup> + <actionGroup ref="AdminMarketingFindNewsletterSubscribersInGridActionGroup" stepKey="findSubscriber"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <actionGroup ref="AdminMarketingDeleteNewsletterSubscriberFromGridActionGroup" stepKey="deleteSubscriber"/> + <actionGroup ref="AssertMessageInAdminPanelActionGroup" stepKey="assertSuccessMessage"> + <argument name="message" value="Total of 1 record(s) were deleted."/> + </actionGroup> + <actionGroup ref="AdminMarketingFindNewsletterSubscribersInGridActionGroup" stepKey="findDeletedSubscriber"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + <actionGroup ref="AssertAdminDeletedNewsletterSubscriberIsNotInGridActionGroup" stepKey="dontSeeSubscriber"> + <argument name="email" value="{{Simple_US_Customer.email}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.xml new file mode 100644 index 0000000000000..6c62434f1620e --- /dev/null +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest.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="StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest"> + <annotations> + <features value="Newsletter"/> + <stories value="Disabled Guest Newsletter Subscription"/> + <title value="Newsletter Subscription for guest is disabled and cannot be performed"/> + <description value="Guest cannot subscribe to Newsletter if it is disallowed in configurations"/> + <severity value="AVERAGE"/> + <group value="newsletter"/> + <group value="configuration"/> + <testCaseId value="MC-35728"/> + </annotations> + <before> + <magentoCLI stepKey="disableGuestSubscription" command="config:set newsletter/subscription/allow_guest_subscribe 0"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + </before> + <after> + <magentoCLI stepKey="allowGuestSubscription" command="config:set newsletter/subscription/allow_guest_subscribe 1"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + </after> + + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="openStorefrontPage"/> + <actionGroup ref="StorefrontCreateNewsletterSubscriberActionGroup" stepKey="createSubscription"> + <argument name="email" value="{{_defaultNewsletter.senderEmail}}"/> + </actionGroup> + <actionGroup ref="StorefrontAssertErrorMessageActionGroup" stepKey="assertMessage"> + <argument name="message" value="Sorry, but the administrator denied subscription for guests. Please register."/> + <argument name="messageType" value="error"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml index c38725f263525..8ae592a17d620 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/StorefrontVerifySecureURLRedirectNewsletterTest.xml @@ -28,11 +28,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml index cffce8da1d710..dbce742aa0eef 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest.xml @@ -8,7 +8,8 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest"> + <test name="VerifyRegistredLinkDisplayedForGuestSubscriptionNoTest" deprecated="Use StorefrontNewsletterGuestSubscriptionWithDisallowedOptionTest"> + <!-- Deprecated Due to inconsistency with the best practices --> <annotations> <features value="Newsletter"/> <stories value="Configure guest newsletter subscription to 'No'"/> @@ -22,6 +23,11 @@ <magentoCLI command="config:set newsletter/subscription/allow_guest_subscribe 0" stepKey="setConfigGuestSubscriptionDisable"/> </before> - <actionGroup ref="StorefrontCreateNewSubscriberActionGroup" stepKey="createSubscriber"/> + <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnStorefrontPage"/> + <submitForm selector="{{BasicFrontendNewsletterFormSection.subscribeForm}}" + parameterArray="['email' => '{{_defaultNewsletter.senderEmail}}']" + button="{{BasicFrontendNewsletterFormSection.subscribeButton}}" stepKey="submitForm"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + <waitForElementVisible stepKey="waitForErrorAppears" selector="{{StorefrontMessagesSection.error}}"/> </test> </tests> diff --git a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml index a568fb1799ac2..63b2741e7bd15 100644 --- a/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml +++ b/app/code/Magento/Newsletter/Test/Mftf/Test/VerifySubscribedNewsletterDisplayedTest.xml @@ -16,39 +16,46 @@ <title value="Newsletter subscription when user is registered on 2 stores"/> <description value="Newsletter subscription when user is registered on 2 stores"/> <severity value="MAJOR"/> - <testCaseId value="MAGETWO-93836"/> + <testCaseId value="MC-25840"/> </annotations> <before> <!--Log in to Magento as admin.--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AdminCreateWebsiteActionGroup" stepKey="createWebsite"> - <argument name="newWebsiteName" value="Second"/> - <argument name="websiteCode" value="Base2"/> + <argument name="newWebsiteName" value="{{customWebsite.name}}"/> + <argument name="websiteCode" value="{{customWebsite.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateNewStoreGroupActionGroup" stepKey="createNewStore"> - <argument name="website" value="Second"/> - <argument name="storeGroupName" value="NewStore"/> - <argument name="storeGroupCode" value="Base12"/> + <argument name="website" value="{{customWebsite.name}}"/> + <argument name="storeGroupName" value="{{customStoreGroup.name}}"/> + <argument name="storeGroupCode" value="{{customStoreGroup.code}}"/> </actionGroup> - <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> - <argument name="StoreGroup" value="staticStoreGroup"/> - <argument name="customStore" value="staticStore"/> + <argument name="StoreGroup" value="customStoreGroup"/> + <argument name="customStore" value="customStore"/> </actionGroup> - <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="EnableWebUrlOptionsActionGroup" stepKey="addStoreCodeToUrls"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> + <after> <!--Delete created data and set Default Configuration--> <actionGroup ref="ResetWebUrlOptionsActionGroup" stepKey="resetUrlOption"/> <actionGroup ref="AdminDeleteWebsiteActionGroup" stepKey="deleteWebsite"> - <argument name="websiteName" value="Second"/> + <argument name="websiteName" value="{{customWebsite.name}}"/> + </actionGroup> + <actionGroup ref="AdminClearFiltersActionGroup" stepKey="clearFilters"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> @@ -62,7 +69,8 @@ <!--Create new Account with the same email address. (unchecked Sign Up for Newsletter checkbox)--> <actionGroup ref="StorefrontCreateNewAccountNewsletterUncheckedActionGroup" stepKey="createNewAccountNewsletterUnchecked"> <argument name="Customer" value="CustomerEntityOne"/> - <argument name="Store" value="staticStore"/> + <argument name="Store" value="customStore"/> + <argument name="StoreGroup" value="customStoreGroup"/> </actionGroup> </test> </tests> diff --git a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php index 199eeac377759..d3b6495df680f 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Block/Adminhtml/Queue/PreviewTest.php @@ -100,7 +100,23 @@ protected function setUp(): void ->willReturn($backendSession); $templateFactory = $this->createPartialMock(TemplateFactory::class, ['create']); - $this->templateMock = $this->createMock(Template::class); + $this->templateMock = $this->getMockBuilder(Template::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'isPlain', + 'setId', + ] + ) + ->addMethods( + [ + 'setTemplateType', + 'setTemplateText', + 'setTemplateStyles', + ] + ) + ->getMock(); + $templateFactory->expects($this->once()) ->method('create') ->willReturn($this->templateMock); @@ -112,7 +128,22 @@ protected function setUp(): void ->willReturn($this->subscriberMock); $queueFactory = $this->createPartialMock(QueueFactory::class, ['create']); - $this->queueMock = $this->createPartialMock(Queue::class, ['load']); + $this->queueMock = $this->getMockBuilder(Queue::class) + ->disableOriginalConstructor() + ->setMethods( + [ + 'load', + ] + ) + ->addMethods( + [ + 'getTemplateId', + 'getNewsletterType', + 'getNewsletterText', + 'getNewsletterStyles', + ] + ) + ->getMock(); $queueFactory->expects($this->any()) ->method('create') ->willReturn($this->queueMock); @@ -130,7 +161,7 @@ protected function setUp(): void 'context' => $context, 'templateFactory' => $templateFactory, 'subscriberFactory' => $subscriberFactory, - 'queueFactory' => $queueFactory + 'queueFactory' => $queueFactory, ] ); } @@ -148,17 +179,29 @@ public function testToHtmlEmpty() public function testToHtmlWithId() { + $templateId = 1; + $newsletterType = 2; + $newsletterText = 'newsletter text'; + $newsletterStyle = 'style'; $this->requestMock->expects($this->any())->method('getParam')->willReturnMap( [ ['id', null, 1], - ['store_id', null, 0] + ['store_id', null, 0], ] ); $this->queueMock->expects($this->once()) ->method('load')->willReturnSelf(); + $this->queueMock->expects($this->once())->method('getTemplateId')->willReturn($templateId); + $this->queueMock->expects($this->once())->method('getNewsletterType')->willReturn($newsletterType); + $this->queueMock->expects($this->once())->method('getNewsletterText')->willReturn($newsletterText); + $this->queueMock->expects($this->once())->method('getNewsletterStyles')->willReturn($newsletterStyle); $this->templateMock->expects($this->any()) ->method('isPlain') ->willReturn(true); + $this->templateMock->expects($this->once())->method('setId')->willReturn($templateId); + $this->templateMock->expects($this->once())->method('setTemplateType')->willReturn($newsletterType); + $this->templateMock->expects($this->once())->method('setTemplateText')->willReturn($newsletterText); + $this->templateMock->expects($this->once())->method('setTemplateStyles')->willReturn($newsletterStyle); /** @var Store $store */ $this->storeManagerMock->expects($this->once()) ->method('getDefaultStoreView') diff --git a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php index 912c6a1df8729..4e1f18a26a95a 100644 --- a/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php +++ b/app/code/Magento/Newsletter/Test/Unit/Model/SubscriptionManagerTest.php @@ -136,11 +136,8 @@ public function testSubscribe( ->with(Subscriber::XML_PATH_CONFIRMATION_FLAG, ScopeInterface::SCOPE_STORE, $storeId) ->willReturn($isConfirmNeed); - $this->assertEquals( - $subscriber, - $this->subscriptionManager->subscribe($email, $storeId) - ); - $this->assertEquals($subscriber->getData(), $expectedData); + $this->assertEquals($subscriber, $this->subscriptionManager->subscribe($email, $storeId)); + $this->assertEquals($expectedData, $subscriber->getData()); } /** @@ -308,7 +305,7 @@ public function testSubscribeCustomer( $subscriber, $this->subscriptionManager->subscribeCustomer($customerId, $storeId) ); - $this->assertEquals($subscriber->getData(), $expectedData); + $this->assertEquals($expectedData, $subscriber->getData()); } /** @@ -553,7 +550,7 @@ public function testUnsubscribeCustomer( $subscriber, $this->subscriptionManager->unsubscribeCustomer($customerId, $storeId) ); - $this->assertEquals($subscriber->getData(), $expectedData); + $this->assertEquals($expectedData, $subscriber->getData()); } /** diff --git a/app/code/Magento/Newsletter/composer.json b/app/code/Magento/Newsletter/composer.json index cc0d717a1958d..790370c328644 100644 --- a/app/code/Magento/Newsletter/composer.json +++ b/app/code/Magento/Newsletter/composer.json @@ -14,7 +14,8 @@ "magento/module-email": "*", "magento/module-require-js": "*", "magento/module-store": "*", - "magento/module-widget": "*" + "magento/module-widget": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ 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 20ff63a60a263..62b368b8911f8 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/iframeswitcher.phtml @@ -5,30 +5,31 @@ */ /** @var \Magento\Backend\Block\Page $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="preview" class="cms-revision-preview"> <div class="toolbar"> - <?php if (!$block->isSingleStoreMode()) :?> - <div class="store-switcher"> - <?= $block->getChildHtml('store_switcher') ?> - </div> + <?php if (!$block->isSingleStoreMode()):?> + <div class="store-switcher"> + <?= $block->getChildHtml('store_switcher') ?> + </div> <?php endif;?> </div> <iframe - name="preview_iframe" - id="preview_iframe" - class="preview_iframe" - frameborder="0" - title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" - width="100%" - sandbox="allow-forms allow-pointer-lock" + name="preview_iframe" + id="preview_iframe" + class="preview_iframe" + frameborder="0" + title="<?= $block->escapeHtmlAttr(__('Preview')) ?>" + width="100%" + sandbox="allow-forms allow-pointer-lock" > </iframe> <?= $block->getChildHtml('preview_form') ?> </div> -<script> +<?php $scriptString = <<<script require(['jquery', 'loadingPopup', 'prototype'], function(jQuery){ //<![CDATA[ @@ -61,4 +62,6 @@ jQuery("#preview_iframe").load(function() { //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml index d5a24fad2ac91..896b8ce773c2d 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/preview/store.phtml @@ -3,9 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($websites = $block->getWebsites()) : ?> +<?php if ($websites = $block->getWebsites()): ?> <div class="field field-store-switcher"> <label class="label" for="store_switcher"><?= $block->escapeHtml(__('Choose Store View:')) ?></label> <div class="control"> @@ -13,22 +15,25 @@ id="store_switcher" class="admin__control-select" name="store_switcher"> - <?php foreach ($websites as $website) : ?> + <?php foreach ($websites as $website): ?> <?php $showWebsite = false; ?> - <?php foreach ($website->getGroups() as $group) : ?> + <?php foreach ($website->getGroups() as $group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStores($group) as $store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStores($group) as $store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> <optgroup label="<?= $block->escapeHtmlAttr($website->getName()) ?>"></optgroup> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> <optgroup label="   <?= $block->escapeHtmlAttr($group->getName()) ?>"> <?php endif; ?> - <option value="<?= $block->escapeHtmlAttr($store->getId()) ?>"<?php if ($block->getStoreId() == $store->getId()) : ?> selected="selected"<?php endif; ?>>    <?= $block->escapeHtml($store->getName()) ?></option> + <option value="<?= $block->escapeHtmlAttr($store->getId()) ?>" + <?php if ($block->getStoreId() == $store->getId()): ?> selected="selected"<?php endif; ?>> +     <?= $block->escapeHtml($store->getName()) ?> + </option> <?php endforeach; ?> - <?php if ($showGroup) : ?> + <?php if ($showGroup): ?> </optgroup> <?php endif; ?> <?php endforeach; ?> @@ -37,7 +42,7 @@ </div> <?= $block->getHintHtml() ?> </div> -<script> + <?php $scriptString= <<<script require(['prototype'], function(){ //<![CDATA[ @@ -48,5 +53,7 @@ Event.observe($('store_switcher'), 'change', function(event) { //]]> }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml index b697be4cf753a..8148af3221922 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/problem/list.phtml @@ -3,21 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getChildHtml('grid') ?> -<?php if ($block->getShowButtons()) : ?> +<?php if ($block->getShowButtons()): ?> <div class="form-buttons"> <?= $block->getUnsubscribeButtonHtml() ?> <?= $block->getDeleteButtonHtml() ?> </div> <?php endif ?> -<script> +<?php $scriptString = <<<script require(["prototype", "mage/adminhtml/events"], function(){ problemController = { checkCheckboxes:function (controlCheckbox) { - var elements = $$('input.problemCheckbox'); + var elements = \$$('input.problemCheckbox'); if (elements && elements.length) { elements.each(function (obj) { obj.checked = controlCheckbox.checked; @@ -35,7 +37,7 @@ require(["prototype", "mage/adminhtml/events"], function(){ }, unsubscribe:function () { - var elements = $$('input.problemCheckbox'); + var elements = \$$('input.problemCheckbox'); var serializedElements = Form.serializeElements(elements, true); serializedElements._unsubscribe = '1'; serializedElements.form_key = FORM_KEY; @@ -48,7 +50,7 @@ require(["prototype", "mage/adminhtml/events"], function(){ }, deleteSelected:function () { - var elements = $$('input.problemCheckbox'); + var elements = \$$('input.problemCheckbox'); var serializedElements = Form.serializeElements(elements, true); serializedElements._delete = '1'; serializedElements.form_key = FORM_KEY; @@ -65,4 +67,6 @@ require(["prototype", "mage/adminhtml/events"], function(){ //--> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml index 3d52cc0dee777..eb2e3f2b399f2 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/queue/edit.phtml @@ -5,16 +5,16 @@ */ /* @var $block \Magento\Newsletter\Block\Adminhtml\Queue\Edit */ - +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> <?= $block->getPreviewButtonHtml() ?> - <?php if (!$block->getIsPreview()) : ?> + <?php if (!$block->getIsPreview()): ?> <?= $block->getResetButtonHtml() ?> <?= $block->getSaveButtonHtml() ?> <?php endif ?> - <?php if ($block->getCanResume()) : ?> + <?php if ($block->getCanResume()): ?> <?= $block->getResumeButtonHtml() ?> <?php endif ?> </div> @@ -23,16 +23,18 @@ <?= $block->getBlockHtml('formkey') ?> <?= $block->getChildHtml('form') ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_queue_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_queue_preview_form" + target="_blank"> <?= $block->getBlockHtml('formkey') ?> <div class="no-display"> - <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->getIsTextType() ? 1 : 2 ?>" /> + <input type="hidden" id="preview_type" name="type" + value="<?= /* @noEscape */ $block->getIsTextType() ? 1 : 2 ?>" /> <input type="hidden" id="preview_text" name="text" value="" /> <input type="hidden" id="preview_styles" name="styles" value="" /> <input type="hidden" id="preview_id" name="id" value="" /> </div> </form> -<script> +<?php $scriptString= <<<script require([ 'jquery', 'wysiwygAdapter', @@ -71,4 +73,6 @@ queueControl = { //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml index 13bd5d5118be0..b69a89fc296dc 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/subscriber/list.phtml @@ -5,20 +5,29 @@ */ /** @var \Magento\Newsletter\Block\Adminhtml\Subscriber $block */ - +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?= $block->getChildHtml('grid') ?> -<?php if (count($block->getQueueAsOptions())>0 && $block->getShowQueueAdd()) : ?> +<?php if (count($block->getQueueAsOptions())>0 && $block->getShowQueueAdd()): ?> <div class="form-buttons"> <select id="queueList" name="queue"> - <?php foreach ($block->getQueueAsOptions() as $_queue) : ?> - <option value="<?= $block->escapeHtmlAttr($_queue['value']) ?>"><?= $block->escapeHtml($_queue['label']) ?> #<?= $block->escapeHtml($_queue['value']) ?></option> + <?php foreach ($block->getQueueAsOptions() as $_queue): ?> + <option value="<?= $block->escapeHtmlAttr($_queue['value']) ?>"> + <?= $block->escapeHtml($_queue['label']) ?> #<?= $block->escapeHtml($_queue['value']) ?> + </option> <?php endforeach; ?> </select> - <button type="button" class="scalable" onclick="subscriberController.addToQueue();"><span><span><span><?= $block->escapeHtml(__('Add to Queue')) ?></span></span></span></button> + <button type="button" class="scalable" id="addToQueue"> + <span><span><span><?= $block->escapeHtml(__('Add to Queue')) ?></span></span></span> + </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'subscriberController.addToQueue();', + 'button#addToQueue' + ) ?> </div> <?php endif ?> -<script> +<?php $scriptString= <<<script require(["prototype", "mage/adminhtml/events"], function(){ subscriberController = { checkCheckboxes: function(controlCheckbox) { @@ -53,4 +62,6 @@ require(["prototype", "mage/adminhtml/events"], function(){ varienGlobalEvents.attachEventHandler('gridRowClick', subscriberController.rowClick.bind(subscriberController)); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml index abc56070b6892..29555130de1ae 100644 --- a/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml +++ b/app/code/Magento/Newsletter/view/adminhtml/templates/template/edit.phtml @@ -7,26 +7,29 @@ use Magento\Framework\App\TemplateTypesInterface; /* @var $block \Magento\Newsletter\Block\Adminhtml\Template\Edit */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" id="newsletter_template_edit_form"> <?= $block->getBlockHtml('formkey') ?> <div class="no-display"> <input type="hidden" id="change_flag_element" name="_change_type_flag" value="" /> - <input type="hidden" id="save_as_flag" name="_save_as_flag" value="<?= $block->escapeHtmlAttr($block->getSaveAsFlag()) ?>" /> + <input type="hidden" id="save_as_flag" name="_save_as_flag" + value="<?= $block->escapeHtmlAttr($block->getSaveAsFlag()) ?>" /> </div> <?= /* @noEscape */ $block->getForm() ?> </form> -<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_template_preview_form" target="_blank"> +<form action="<?= $block->escapeUrl($block->getPreviewUrl()) ?>" method="post" id="newsletter_template_preview_form" + target="_blank"> <div class="no-display"> - <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>" /> + <input type="hidden" id="preview_type" name="type" value="<?= /* @noEscape */ $block->isTextType() ? 1 : 2 ?>"/> <input type="hidden" id="preview_text" name="text" value="" /> <input type="hidden" id="preview_styles" name="styles" value="" /> <input type="hidden" id="preview_id" name="id" value="" /> <input type="hidden" name="form_key" value="<?= $block->escapeHtmlAttr($block->getFormKey()) ?>" > </div> </form> -<script> +<?php $scriptString = <<<script require([ 'jquery', 'wysiwygAdapter', @@ -91,7 +94,7 @@ require([ var self = this; confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure that you want to strip all tags?'))) ?>", + content: "{$block->escapeJs(__('Are you sure that you want to strip all tags?'))}", actions: { confirm: function () { if (wysiwyg.activeEditor()) { @@ -140,10 +143,10 @@ require([ $('change_flag_element').value = '1'; } - if ($F('code').blank() || $F('code') == templateControl.templateName) { + if (\$F('code').blank() || \$F('code') == templateControl.templateName) { prompt({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Please enter a new template name.'))) ?>', - value: templateControl.templateName + '<?= $block->escapeJs(__(' Copy')) ?>', + content: '{$block->escapeJs(__('Please enter a new template name.'))}', + value: templateControl.templateName + '{$block->escapeJs(__(' Copy'))}', actions: { confirm: function (value) { $('code').value = value; @@ -174,9 +177,9 @@ require([ preview: function () { if (this.typeChange) { - $('preview_type').value = <?= $block->escapeJs(TemplateTypesInterface::TYPE_TEXT) ?>; + $('preview_type').value = {$block->escapeJs(TemplateTypesInterface::TYPE_TEXT)}; } else { - $('preview_type').value = <?= $block->escapeJs($block->getTemplateType()) ?>; + $('preview_type').value = {$block->escapeJs($block->getTemplateType())}; } if (wysiwyg.activeEditor()) { @@ -200,10 +203,10 @@ require([ deleteTemplate: function () { confirm({ - content: "<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to delete this template?'))) ?>", + content: "{$block->escapeJs(__('Are you sure you want to delete this template?'))}", actions: { confirm: function () { - window.location.href = '<?= $block->escapeUrl($block->getDeleteUrl()) ?>'; + window.location.href = '{$block->escapeJs($block->getDeleteUrl())}'; } } }); @@ -211,8 +214,10 @@ require([ }; templateControl.init(); - templateControl.templateName = "<?= $block->escapeJs($block->getJsTemplateName()) ?>"; + templateControl.templateName = "{$block->escapeJs($block->getJsTemplateName())}"; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml index 429482e5795bf..768c97ef316f7 100644 --- a/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml +++ b/app/code/Magento/Newsletter/view/frontend/templates/subscribe.phtml @@ -40,3 +40,12 @@ </form> </div> </div> +<script type="text/x-magento-init"> + { + "*": { + "Magento_Customer/js/block-submit-on-send": { + "formId": "newsletter-validate-detail" + } + } + } +</script> diff --git a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php index 464142df5b996..fe30570aba50d 100644 --- a/app/code/Magento/OfflinePayments/Model/Purchaseorder.php +++ b/app/code/Magento/OfflinePayments/Model/Purchaseorder.php @@ -62,6 +62,7 @@ public function assignData(\Magento\Framework\DataObject $data) * @return $this * @throws LocalizedException * @api + * @since 100.2.3 */ public function validate() { diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml index a251c609ea324..01ed26d5e57a6 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/banktransfer.phtml @@ -6,16 +6,21 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Banktransfer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display:none;"> + <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <li> <div class="<?= /* @noEscape */ $methodCode ?>-instructions-content checkout-agreement-item-content"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> </li> </ul> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'ul#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml index 8e8730640a8a7..c1b07f08d4ce3 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/cashondelivery.phtml @@ -6,16 +6,21 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Cashondelivery + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display:none;"> + <ul class="form-list checkout-agreements" id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <li> <div class="<?= /* @noEscape */ $methodCode ?>-instructions-content checkout-agreement-item-content"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> </li> </ul> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'ul#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml index db1d7c87ada0e..789a3921b2c21 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/checkmo.phtml @@ -6,13 +6,15 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Checkmo + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none"> - <?php if ($block->getMethod()->getPayableTo()) : ?> - <label class="label"><span><?= $block->escapeHtml(__('Make Check payable to:')) ?></span></label> <?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?> +<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" > + <?php if ($block->getMethod()->getPayableTo()): ?> + <label class="label"><span><?= $block->escapeHtml(__('Make Check payable to:')) ?></span></label> + <?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?> <?php endif; ?> - <?php if ($block->getMethod()->getMailingAddress()) : ?> + <?php if ($block->getMethod()->getMailingAddress()): ?> <div class="admin__field"> <label class="admin__field-label"><span><?= $block->escapeHtml(__('Send Check to:')) ?></span></label> <div class="admin__field-control checkmo-mailing-address"> @@ -21,3 +23,7 @@ </div> <?php endif; ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeJs($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml index c115765697fc5..a1e3da2713811 100644 --- a/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/adminhtml/templates/form/purchaseorder.phtml @@ -6,15 +6,23 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Purchaseorder + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display: none"> +<fieldset class="admin__fieldset payment-method" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> <div class="admin__field _required"> - <label for="po_number" class="admin__field-label"><span><?= $block->escapeHtml(__('Purchase Order Number')) ?></span></label> + <label for="po_number" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Purchase Order Number')) ?></span> + </label> <div class="admin__field-control"> <input type="text" id="po_number" name="payment[po_number]" - title="<?= $block->escapeHtml(__("Purchase Order Number")) ?>" class="required-entry admin__control-text" + title="<?= $block->escapeHtml(__("Purchase Order Number")) ?>" + class="required-entry admin__control-text" value="<?= /* @noEscape */ $block->getInfoData('po_number') ?>"/> </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeJs($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml index 568ef7c3f69f2..97288194342ba 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/banktransfer.phtml @@ -6,12 +6,18 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Banktransfer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement checkout-agreement-item-content" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none;"> + <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement checkout-agreement-item-content" + id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml index 2943f59be4ab3..160c1d27052f0 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/cashondelivery.phtml @@ -6,12 +6,18 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Cashondelivery + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $instructions = $block->getInstructions(); ?> -<?php if ($instructions) : ?> +<?php if ($instructions): ?> <?php $methodCode = $block->escapeHtml($block->getMethodCode());?> - <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none;"> + <div class="items <?= /* @noEscape */ $methodCode ?> instructions agreement" + id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <?= /* @noEscape */ nl2br($block->escapeHtml($instructions)) ?> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#payment_form_' . /* @noEscape */ $methodCode + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml index 36f58fc155a18..3b381bbf72f4f 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/checkmo.phtml @@ -6,15 +6,16 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Checkmo + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getMethod()->getMailingAddress() || $block->getMethod()->getPayableTo()) : ?> - <dl class="items check payable" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none;"> - <?php if ($block->getMethod()->getPayableTo()) : ?> +<?php if ($block->getMethod()->getMailingAddress() || $block->getMethod()->getPayableTo()): ?> + <dl class="items check payable" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> + <?php if ($block->getMethod()->getPayableTo()): ?> <dt class="title"><?= $block->escapeHtml(__('Make Check payable to:')) ?></dt> <dd class="content"><?= $block->escapeHtml($block->getMethod()->getPayableTo()) ?></dd> <?php endif; ?> - <?php if ($block->getMethod()->getMailingAddress()) : ?> + <?php if ($block->getMethod()->getMailingAddress()): ?> <dt class="title"><?= $block->escapeHtml(__('Send Check to:')) ?></dt> <dd class="content"> <address class="checkmo mailing address"> @@ -23,4 +24,8 @@ </dd> <?php endif; ?> </dl> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'dl#payment_form_' . $block->escapeJs($block->getMethodCode()) + ) ?> <?php endif; ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml index 52b7df9fb9187..35ef5d9db8616 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/form/purchaseorder.phtml @@ -6,16 +6,23 @@ /** * @var $block \Magento\OfflinePayments\Block\Form\Purchaseorder + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $methodCode = $block->escapeHtml($block->getMethodCode()); ?> -<fieldset class="fieldset items <?= /* @noEscape */ $methodCode ?>" id="payment_form_<?= /* @noEscape */ $methodCode ?>" style="display: none"> +<fieldset class="fieldset items <?= /* @noEscape */ $methodCode ?>" + id="payment_form_<?= /* @noEscape */ $methodCode ?>"> <div class="field number required"> <label for="po_number" class="label"><span><?= $block->escapeHtml(__('Purchase Order Number')) ?></span></label> <div class="control"> - <input type="text" id="po_number" name="payment[po_number]" title="<?= $block->escapeHtml(__('Purchase Order Number')) ?>" + <input type="text" id="po_number" name="payment[po_number]" + title="<?= $block->escapeHtml(__('Purchase Order Number')) ?>" class="input-text required-entry" value="<?= $block->escapeHtml($block->getInfoData('po_number')) ?>" /> </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $methodCode +) ?> diff --git a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml index b96918243a7a7..730976a15be5d 100644 --- a/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml +++ b/app/code/Magento/OfflinePayments/view/frontend/templates/multishipping/checkmo_form.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script require([ 'uiLayout', 'jquery' @@ -25,4 +28,6 @@ $('body').trigger('contentUpdated'); }) }) -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php index e015f7b54637d..bd75a1ffe698c 100644 --- a/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php +++ b/app/code/Magento/OfflineShipping/Model/ResourceModel/Carrier/Tablerate/LocationDirectory.php @@ -155,7 +155,7 @@ protected function loadRegions() * @param int $countryId * @param string $regionCode * @return string - * @deprecated + * @deprecated 100.3.1 */ public function getRegionId($countryId, $regionCode) { diff --git a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php index bd36b899ce89b..062e428ef68b1 100644 --- a/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php +++ b/app/code/Magento/OfflineShipping/Test/Unit/Block/Adminhtml/Form/Field/ImportTest.php @@ -18,6 +18,7 @@ use Magento\OfflineShipping\Block\Adminhtml\Form\Field\Import; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; class ImportTest extends TestCase { @@ -38,13 +39,16 @@ protected function setUp(): void ->onlyMethods(['addSuffixToName']) ->disableOriginalConstructor() ->getMock(); + $randomMock = $this->getMockBuilder(Random::class)->disableOriginalConstructor()->getMock(); + $randomMock->method('getRandomString')->willReturn('123456abcdefg'); $testData = ['name' => 'test_name', 'html_id' => 'test_html_id']; $testHelper = new ObjectManager($this); $this->_object = $testHelper->getObject( Import::class, [ 'data' => $testData, - '_escaper' => $testHelper->getObject(Escaper::class) + '_escaper' => $testHelper->getObject(Escaper::class), + 'random' => $randomMock ] ); $this->_object->setForm($this->_formMock); @@ -87,9 +91,9 @@ public function testGetElementHtml() '<input id="time_condition" type="hidden" name="test_name" value="', $testString ); - $this->assertStringEndsWith( + $this->assertStringContainsString( '<input id="test_name_prefixtest_html_idtest_name_suffix" ' . - 'name="test_name" data-ui-id="form-element-test_name" value="" type="file"/>', + 'name="test_name" data-ui-id="form-element-test_name" value="" type="file"', $testString ); } diff --git a/app/code/Magento/PageCache/Model/Config.php b/app/code/Magento/PageCache/Model/Config.php index 10ae41be21d4d..bf144cc46637e 100644 --- a/app/code/Magento/PageCache/Model/Config.php +++ b/app/code/Magento/PageCache/Model/Config.php @@ -121,7 +121,7 @@ public function __construct( */ public function getType() { - return $this->_scopeConfig->getValue(self::XML_PAGECACHE_TYPE); + return (int)$this->_scopeConfig->getValue(self::XML_PAGECACHE_TYPE); } /** diff --git a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php index 762f393f2a1b9..1b64f3b635c03 100644 --- a/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php +++ b/app/code/Magento/PageCache/Model/Layout/LayoutPlugin.php @@ -8,10 +8,12 @@ namespace Magento\PageCache\Model\Layout; use Magento\Framework\App\MaintenanceMode; +use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResponseInterface; use Magento\Framework\DataObject\IdentityInterface; use Magento\Framework\View\Layout; use Magento\PageCache\Model\Config; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; /** * Append cacheable pages response headers. @@ -28,6 +30,11 @@ class LayoutPlugin */ private $response; + /** + * @var PageCacheTagsPreprocessorInterface + */ + private $pageCacheTagsPreprocessor; + /** * @var MaintenanceMode */ @@ -37,15 +44,19 @@ class LayoutPlugin * @param ResponseInterface $response * @param Config $config * @param MaintenanceMode $maintenanceMode + * @param PageCacheTagsPreprocessorInterface|null $pageCacheTagsPreprocessor */ public function __construct( ResponseInterface $response, Config $config, - MaintenanceMode $maintenanceMode + MaintenanceMode $maintenanceMode, + ?PageCacheTagsPreprocessorInterface $pageCacheTagsPreprocessor = null ) { $this->response = $response; $this->config = $config; $this->maintenanceMode = $maintenanceMode; + $this->pageCacheTagsPreprocessor = $pageCacheTagsPreprocessor + ?? ObjectManager::getInstance()->get(PageCacheTagsPreprocessorInterface::class); } /** @@ -74,10 +85,11 @@ public function afterGetOutput(Layout $subject, $result) { if ($subject->isCacheable() && $this->config->isEnabled()) { $tags = [[]]; + $isVarnish = $this->config->getType() === Config::VARNISH; + foreach ($subject->getAllBlocks() as $block) { if ($block instanceof IdentityInterface) { $isEsiBlock = $block->getTtl() > 0; - $isVarnish = $this->config->getType() == Config::VARNISH; if ($isVarnish && $isEsiBlock) { continue; } @@ -85,6 +97,7 @@ public function afterGetOutput(Layout $subject, $result) } } $tags = array_unique(array_merge(...$tags)); + $tags = $this->pageCacheTagsPreprocessor->process($tags); $this->response->setHeader('X-Magento-Tags', implode(',', $tags)); } diff --git a/app/code/Magento/PageCache/Model/PageCacheTagsPreprocessorComposite.php b/app/code/Magento/PageCache/Model/PageCacheTagsPreprocessorComposite.php new file mode 100644 index 0000000000000..caaf3b378571c --- /dev/null +++ b/app/code/Magento/PageCache/Model/PageCacheTagsPreprocessorComposite.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Model; + +use InvalidArgumentException; +use Magento\Framework\App\RequestInterface; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; + +/** + * Composite page cache preprocessors + */ +class PageCacheTagsPreprocessorComposite implements PageCacheTagsPreprocessorInterface +{ + /** + * @var PageCacheTagsPreprocessorInterface[][] + */ + private $preprocessors; + /** + * @var RequestInterface + */ + private $request; + + /** + * @param RequestInterface $request + * @param PageCacheTagsPreprocessorInterface[][] $preprocessors + */ + public function __construct( + RequestInterface $request, + array $preprocessors = [] + ) { + foreach ($preprocessors as $group) { + foreach ($group as $preprocessor) { + if (!$preprocessor instanceof PageCacheTagsPreprocessorInterface) { + throw new InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + PageCacheTagsPreprocessorInterface::class, + get_class($preprocessor) + ) + ); + } + } + } + $this->preprocessors = $preprocessors; + $this->request = $request; + } + + /** + * @inheritDoc + */ + public function process(array $tags): array + { + $forwardInfo = $this->request->getBeforeForwardInfo(); + $actionName = $forwardInfo + ? implode('_', [$forwardInfo['route_name'], $forwardInfo['controller_name'], $forwardInfo['action_name']]) + : $this->request->getFullActionName(); + if (isset($this->preprocessors[$actionName])) { + foreach ($this->preprocessors[$actionName] as $preprocessor) { + $tags = $preprocessor->process($tags); + } + } + return $tags; + } +} diff --git a/app/code/Magento/PageCache/Model/Spi/PageCacheTagsPreprocessorInterface.php b/app/code/Magento/PageCache/Model/Spi/PageCacheTagsPreprocessorInterface.php new file mode 100644 index 0000000000000..19f9eedf7546d --- /dev/null +++ b/app/code/Magento/PageCache/Model/Spi/PageCacheTagsPreprocessorInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PageCache\Model\Spi; + +/** + * Interface for page tags preprocessors + */ +interface PageCacheTagsPreprocessorInterface +{ + /** + * Change page tags and returned the modified tags + * + * @param array $tags + * @return array + */ + public function process(array $tags): array; +} diff --git a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php index da6a71a0c2655..14b72e75d9473 100644 --- a/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php +++ b/app/code/Magento/PageCache/Observer/SwitchPageCacheOnMaintenance/PageCacheState.php @@ -17,7 +17,7 @@ * * Page Cache State Observer * - * @deprecated Originally used by now removed observer SwitchPageCacheOnMaintenance + * @deprecated 100.4.0 Originally used by now removed observer SwitchPageCacheOnMaintenance */ class PageCacheState { diff --git a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml index d2c738398aae1..1c280acd63a7b 100644 --- a/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml +++ b/app/code/Magento/PageCache/Test/Mftf/Test/AdminFrontendAreaSessionMustNotAffectAdminAreaTest.xml @@ -39,7 +39,9 @@ <requiredEntity createDataKey="createCategoryA"/> </createData> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="clearCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="full_page"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> <resetCookie userInput="PHPSESSID" stepKey="resetSessionCookie"/> @@ -61,8 +63,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> <!-- 2. Navigate Go to "Catalog"->"Products" --> - <amOnPage url="{{ProductCatalogPage.url}}" stepKey="onCatalogProductPage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminProductCatalogPageOpenActionGroup" stepKey="onCatalogProductPage"/> <!-- 3. Open separate tab with Storefront --> <openNewTab stepKey="openNewTab"/> diff --git a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php index a7f4a1e844264..2cb52dee43e40 100644 --- a/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php +++ b/app/code/Magento/PageCache/Test/Unit/Model/Layout/LayoutPluginTest.php @@ -15,6 +15,7 @@ use Magento\Framework\View\Layout; use Magento\PageCache\Model\Config; use Magento\PageCache\Model\Layout\LayoutPlugin; +use Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface; use Magento\PageCache\Test\Unit\Block\Controller\StubBlock; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -58,6 +59,8 @@ protected function setUp(): void $this->responseMock = $this->createMock(Http::class); $this->configMock = $this->createMock(Config::class); $this->maintenanceModeMock = $this->createMock(MaintenanceMode::class); + $preprocessor = $this->createMock(PageCacheTagsPreprocessorInterface::class); + $preprocessor->method('process')->willReturnArgument(0); $this->model = (new ObjectManagerHelper($this))->getObject( LayoutPlugin::class, @@ -65,6 +68,7 @@ protected function setUp(): void 'response' => $this->responseMock, 'config' => $this->configMock, 'maintenanceMode' => $this->maintenanceModeMock, + 'pageCacheTagsPreprocessor' => $preprocessor ] ); } diff --git a/app/code/Magento/PageCache/etc/di.xml b/app/code/Magento/PageCache/etc/di.xml index 9bc86b6f1e3f9..f70a561342763 100644 --- a/app/code/Magento/PageCache/etc/di.xml +++ b/app/code/Magento/PageCache/etc/di.xml @@ -37,9 +37,6 @@ <argument name="layoutCacheKey" xsi:type="object">Magento\Framework\View\Layout\LayoutCacheKeyInterface</argument> </arguments> </type> - <type name="Magento\Framework\App\FrontControllerInterface"> - <plugin name="page_cache_from_key_from_cookie" type="Magento\PageCache\Plugin\RegisterFormKeyFromCookie" /> - </type> <type name="Magento\Framework\App\Cache\RuntimeStaleCacheStateModifier"> <arguments> <argument name="cacheTypes" xsi:type="array"> @@ -49,4 +46,5 @@ </type> <preference for="Magento\PageCache\Model\VclGeneratorInterface" type="Magento\PageCache\Model\Varnish\VclGenerator"/> <preference for="Magento\PageCache\Model\VclTemplateLocatorInterface" type="Magento\PageCache\Model\Varnish\VclTemplateLocator"/> + <preference for="Magento\PageCache\Model\Spi\PageCacheTagsPreprocessorInterface" type="Magento\PageCache\Model\PageCacheTagsPreprocessorComposite"/> </config> diff --git a/app/code/Magento/PageCache/etc/frontend/di.xml b/app/code/Magento/PageCache/etc/frontend/di.xml index a396a46ae7346..1aaa331da7025 100644 --- a/app/code/Magento/PageCache/etc/frontend/di.xml +++ b/app/code/Magento/PageCache/etc/frontend/di.xml @@ -9,6 +9,7 @@ <type name="Magento\Framework\App\FrontControllerInterface"> <plugin name="front-controller-builtin-cache" type="Magento\PageCache\Model\App\FrontController\BuiltinPlugin"/> <plugin name="front-controller-varnish-cache" type="Magento\PageCache\Model\App\FrontController\VarnishPlugin"/> + <plugin name="page_cache_form_key_from_cookie" type="Magento\PageCache\Plugin\RegisterFormKeyFromCookie" /> </type> <type name="Magento\Framework\Controller\ResultInterface"> <plugin name="result-builtin-cache" type="Magento\PageCache\Model\Controller\Result\BuiltinPlugin"/> diff --git a/app/code/Magento/PageCache/etc/graphql/di.xml b/app/code/Magento/PageCache/etc/graphql/di.xml new file mode 100644 index 0000000000000..93714465f4d72 --- /dev/null +++ b/app/code/Magento/PageCache/etc/graphql/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\App\FrontControllerInterface"> + <plugin name="page_cache_form_key_from_cookie" type="Magento\PageCache\Plugin\RegisterFormKeyFromCookie" /> + </type> +</config> diff --git a/app/code/Magento/PageCache/etc/varnish6.vcl b/app/code/Magento/PageCache/etc/varnish6.vcl index eef5e99862538..b23bec4c45fb8 100644 --- a/app/code/Magento/PageCache/etc/varnish6.vcl +++ b/app/code/Magento/PageCache/etc/varnish6.vcl @@ -23,6 +23,10 @@ acl purge { } sub vcl_recv { + if (req.restarts > 0) { + set req.hash_always_miss = true; + } + if (req.method == "PURGE") { if (client.ip !~ purge) { return (synth(405, "Method not allowed")); diff --git a/app/code/Magento/PageCache/view/adminhtml/templates/page_cache_validation.phtml b/app/code/Magento/PageCache/view/adminhtml/templates/page_cache_validation.phtml index 57bb5be87e138..b83e0a172574b 100644 --- a/app/code/Magento/PageCache/view/adminhtml/templates/page_cache_validation.phtml +++ b/app/code/Magento/PageCache/view/adminhtml/templates/page_cache_validation.phtml @@ -4,9 +4,12 @@ * See COPYING.txt for license details. */ -/** @var \Magento\PageCache\Block\System\Config\Form\Field\Export $block */ +/** + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<script> + +<?php $scriptString = <<<script require(['jquery'], function($){ //<![CDATA[ @@ -31,4 +34,6 @@ require(['jquery'], function($){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Payment/Block/Transparent/Iframe.php b/app/code/Magento/Payment/Block/Transparent/Iframe.php index 672db1b065b74..6999a722dbeda 100644 --- a/app/code/Magento/Payment/Block/Transparent/Iframe.php +++ b/app/code/Magento/Payment/Block/Transparent/Iframe.php @@ -5,6 +5,9 @@ */ namespace Magento\Payment\Block\Transparent; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Json\Helper\Data as JsonHelper; + /** * Iframe block for register specific params in layout * @@ -28,13 +31,16 @@ class Iframe extends \Magento\Framework\View\Element\Template * @param \Magento\Framework\View\Element\Template\Context $context * @param \Magento\Framework\Registry $registry * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, \Magento\Framework\Registry $registry, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->coreRegistry = $registry; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Payment/Block/Transparent/Redirect.php b/app/code/Magento/Payment/Block/Transparent/Redirect.php index 1be6dec4cc1d8..b62e86e0f831c 100644 --- a/app/code/Magento/Payment/Block/Transparent/Redirect.php +++ b/app/code/Magento/Payment/Block/Transparent/Redirect.php @@ -13,6 +13,7 @@ * Redirect block for register specific params in layout * * @api + * @since 100.3.5 */ class Redirect extends Template { @@ -44,6 +45,7 @@ public function __construct( * Returns url for redirect. * * @return string + * @since 100.3.5 */ public function getRedirectUrl(): string { @@ -53,10 +55,22 @@ public function getRedirectUrl(): string /** * Returns params to be redirected. * + * Encodes invalid UTF-8 values to UTF-8 to prevent character escape error. + * Some payment methods like PayPal, send data in merchant defined language encoding + * which can be different from the system character encoding (UTF-8). + * * @return array + * @since 100.3.5 */ public function getPostParams(): array { - return (array)$this->_request->getPostValue(); + $params = []; + foreach ($this->_request->getPostValue() as $name => $value) { + if (!empty($value) && mb_detect_encoding($value, 'UTF-8', true) === false) { + $value = utf8_encode($value); + } + $params[$name] = $value; + } + return $params; } } diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php index 2072615a39b92..8a9f08e83005e 100644 --- a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapper.php @@ -17,6 +17,7 @@ * In that case, this implementation can be extended via di.xml and configured with appropriate mappers. * * @api + * @since 100.2.2 */ class ErrorMessageMapper implements ErrorMessageMapperInterface { @@ -35,6 +36,7 @@ public function __construct(DataInterface $messageMapping) /** * @inheritdoc + * @since 100.2.2 */ public function getMessage(string $code) { diff --git a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php index f09f49b7f8100..fc8c69902f373 100644 --- a/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php +++ b/app/code/Magento/Payment/Gateway/ErrorMapper/ErrorMessageMapperInterface.php @@ -13,6 +13,7 @@ * Interface to provide customization for payment validation errors. * * @api + * @since 100.2.2 */ interface ErrorMessageMapperInterface { @@ -22,6 +23,7 @@ interface ErrorMessageMapperInterface * * @param string $code * @return Phrase|null + * @since 100.2.2 */ public function getMessage(string $code); } diff --git a/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php b/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php index c1ad947e49c5b..9ed30b1c56cf4 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php +++ b/app/code/Magento/Payment/Gateway/Validator/ResultInterface.php @@ -33,6 +33,7 @@ public function getFailsDescription(); * Returns list of error codes. * * @return string[] + * @since 100.3.0 */ public function getErrorCodes(); } diff --git a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php index f96c08a9605a8..8c8d13300849e 100644 --- a/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php +++ b/app/code/Magento/Payment/Gateway/Validator/ValidatorComposite.php @@ -14,6 +14,7 @@ * Compiles a result using the results of multiple validators * * @api + * @since 100.0.2 */ class ValidatorComposite extends AbstractValidator { diff --git a/app/code/Magento/Payment/Model/Method/Adapter.php b/app/code/Magento/Payment/Model/Method/Adapter.php index dba48efaae837..1a9831c47f5b1 100644 --- a/app/code/Magento/Payment/Model/Method/Adapter.php +++ b/app/code/Magento/Payment/Model/Method/Adapter.php @@ -668,6 +668,7 @@ public function getConfigPaymentAction() /** * @inheritdoc + * @since 100.4.0 */ public function canSale(): bool { @@ -676,6 +677,7 @@ public function canSale(): bool /** * @inheritdoc + * @since 100.4.0 */ public function sale(InfoInterface $payment, float $amount) { diff --git a/app/code/Magento/Payment/Model/Method/ConfigInterface.php b/app/code/Magento/Payment/Model/Method/ConfigInterface.php index 06afde4657f26..7c74736cf2ef1 100644 --- a/app/code/Magento/Payment/Model/Method/ConfigInterface.php +++ b/app/code/Magento/Payment/Model/Method/ConfigInterface.php @@ -8,7 +8,7 @@ /** * Interface for payment methods config * - * @deprecated This interface has no semantic meaning and all it implementations has no joint points. + * @deprecated 100.3.0 This interface has no semantic meaning and all it implementations has no joint points. */ interface ConfigInterface extends \Magento\Payment\Gateway\ConfigInterface { diff --git a/app/code/Magento/Payment/Model/MethodList.php b/app/code/Magento/Payment/Model/MethodList.php index 746306cbd0bbf..0700f25fcbee5 100644 --- a/app/code/Magento/Payment/Model/MethodList.php +++ b/app/code/Magento/Payment/Model/MethodList.php @@ -19,7 +19,7 @@ class MethodList { /** * @var \Magento\Payment\Helper\Data - * @deprecated 100.1.3 Do not use this property in case of inheritance. + * @deprecated 100.1.0 Do not use this property in case of inheritance. */ protected $paymentHelper; diff --git a/app/code/Magento/Payment/Model/SaleOperationInterface.php b/app/code/Magento/Payment/Model/SaleOperationInterface.php index 384913a75ae26..da7dc5dfcb390 100644 --- a/app/code/Magento/Payment/Model/SaleOperationInterface.php +++ b/app/code/Magento/Payment/Model/SaleOperationInterface.php @@ -11,6 +11,7 @@ * Responsible for support of `sale` payment operation via Magento payment provider gateway. * * @api + * @since 100.4.0 */ interface SaleOperationInterface { @@ -18,6 +19,7 @@ interface SaleOperationInterface * Checks `sale` payment operation availability. * * @return bool + * @since 100.4.0 */ public function canSale(): bool; @@ -27,6 +29,7 @@ public function canSale(): bool; * @param InfoInterface $payment * @param float $amount * @return void + * @since 100.4.0 */ public function sale(InfoInterface $payment, float $amount); } diff --git a/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php new file mode 100644 index 0000000000000..1cd1230a14634 --- /dev/null +++ b/app/code/Magento/Payment/Test/Unit/Block/Transparent/RedirectTest.php @@ -0,0 +1,102 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Payment\Test\Unit\Block\Transparent; + +use Magento\Payment\Block\Transparent\Redirect; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\MockObject\MockObject; + +class RedirectTest extends TestCase +{ + /** + * @var \Magento\Framework\View\Element\Context|MockObject + */ + private $context; + /** + * @var \Magento\Framework\UrlInterface|MockObject + */ + private $url; + /** + * @var Redirect + */ + private $model; + /** + * @var \Magento\Framework\App\RequestInterface|MockObject + */ + private $request; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->context = $this->createMock(\Magento\Framework\View\Element\Template\Context::class); + $this->request = $this->createMock(\Magento\Framework\App\Request\Http::class); + $this->context->method('getRequest') + ->willReturn($this->request); + $this->url = $this->createMock(\Magento\Framework\UrlInterface::class); + $this->model = new Redirect( + $this->context, + $this->url + ); + } + + /** + * @param array $postData + * @param array $expected + * @dataProvider getPostParamsDataProvider + */ + public function testGetPostParams(array $postData, array $expected): void + { + $this->request->method('getPostValue') + ->willReturn($postData); + $this->assertEquals($expected, $this->model->getPostParams()); + } + + /** + * @return array + */ + public function getPostParamsDataProvider(): array + { + return [ + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ], + [ + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => mb_convert_encoding('Ãtienne', 'ISO-8859-1'), + 'BILLTOFIRSTNAME' => mb_convert_encoding('Ãillin', 'ISO-8859-1'), + ], + [ + 'BILLTOEMAIL' => 'john.doe@magento.lo', + 'BILLTOSTREET' => '3640 Holdrege Ave', + 'BILLTOZIP' => '90016', + 'BILLTOLASTNAME' => 'Ãtienne', + 'BILLTOFIRSTNAME' => 'Ãillin', + ] + ] + ]; + } +} diff --git a/app/code/Magento/Payment/composer.json b/app/code/Magento/Payment/composer.json index 6ee0baec247f3..72246c5698f80 100644 --- a/app/code/Magento/Payment/composer.json +++ b/app/code/Magento/Payment/composer.json @@ -12,7 +12,8 @@ "magento/module-directory": "*", "magento/module-quote": "*", "magento/module-sales": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml index 678bde815d370..2ff4df6e4885a 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/form/cc.phtml @@ -6,14 +6,14 @@ /** * @var \Magento\Payment\Block\Adminhtml\Transparent\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); $ccExpMonth = $block->getInfoData('cc_exp_month'); $ccExpYear = $block->getInfoData('cc_exp_year'); ?> -<fieldset class="admin__fieldset payment-method" id="payment_form_<?= /* @noEscape */ $code ?>" - style="display:none"> +<fieldset class="admin__fieldset payment-method" id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="field-type admin__field _required"> <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_type"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -22,8 +22,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <select id="<?= /* @noEscape */ $code ?>_cc_type" name="payment[cc_type]" class="required-entry validate-cc-type-select admin__control-select"> <option value=""></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> - <option value="<?= $block->escapeHtml($typeCode) ?>" <?php if ($typeCode == $ccType) : ?>selected="selected"<?php endif ?>> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> + <option value="<?= $block->escapeHtml($typeCode) ?>" + <?php if ($typeCode == $ccType): ?>selected="selected"<?php endif ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach ?> @@ -36,7 +37,8 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </label> <div class="admin__field-control"> <input type="text" id="<?= /* @noEscape */ $code ?>_cc_number" name="payment[cc_number]" - title="<?= $block->escapeHtml(__('Credit Card Number')) ?>" class="admin__control-text validate-cc-number" + title="<?= $block->escapeHtml(__('Credit Card Number')) ?>" + class="admin__control-text validate-cc-number" value="<?= /* @noEscape */ $block->getInfoData('cc_number') ?>"/> </div> </div> @@ -47,18 +49,18 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="admin__field-control"> <select id="<?= /* @noEscape */ $code ?>_expiration" name="payment[cc_exp_month]" class="admin__control-select admin__control-select-month validate-cc-exp required-entry"> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= $block->escapeHtml($k) ?>" - <?php if ($k == $ccExpMonth) : ?>selected="selected"<?php endif ?>> + <?php if ($k == $ccExpMonth): ?>selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach; ?> </select> <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="admin__control-select admin__control-select-year required-entry"> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?>selected="selected"<?php endif ?>> + <?php if ($k == $ccExpYear): ?>selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -66,7 +68,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="field-number required admin__field _required"> <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_cc_cid"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -80,3 +82,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </div> <?php endif; ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml index 36b8c978c339f..60fbeed2c542f 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/form.phtml @@ -5,6 +5,8 @@ */ /** @var \Magento\Payment\Block\Transparent\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); $ccExpYear = $block->getInfoData('cc_exp_year'); @@ -17,8 +19,11 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); allowtransparency="true" frameborder="0" name="iframeTransparent" - style="display: none; width: 100%; background-color: transparent;" src="<?= $block->escapeUrl($block->getViewFileUrl('blank.html')) ?>"></iframe> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none; width: 100%; background-color: transparent;", + 'iframe#' . /* @noEscape */ $code . '-transparent-iframe' +) ?> <fieldset id="payment_form_<?= /* @noEscape */ $code ?>" class="admin__fieldset" @@ -31,9 +36,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); "orderSaveUrl":"<?= $block->escapeUrl($block->getOrderUrl()) ?>", "cgiUrl":"<?= $block->escapeUrl($block->getCgiUrl()) ?>", "expireYearLength":"<?= $block->escapeHtml($block->getMethodConfigData('cc_year_length')) ?>", - "nativeAction":"<?= $block->escapeUrl($block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()])) ?>" - }, "validation":[]}' - style="display: none;"> + "nativeAction":"<?= $block->escapeUrl( + $block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" + }, "validation":[]}'> <div class="admin__field _required"> <label for="<?= /* @noEscape */ $code ?>_cc_type" class="admin__field-label"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -46,9 +52,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-validate='{required:true, "validate-cc-type-select":"#<?= /* @noEscape */ $code ?>_cc_number"}' class="admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option - value="<?= $block->escapeHtml($typeCode) ?>"<?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif ?>> + value="<?= $block->escapeHtml($typeCode) ?>" + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach ?> @@ -86,10 +93,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-container="<?= /* @noEscape */ $code ?>-cc-month" class="admin__control-select admin__control-select-month" data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr"}'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -98,17 +105,17 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="admin__control-select admin__control-select-year" data-container="<?= /* @noEscape */ $code ?>-cc-year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif ?>> + <?php if ($k == $ccExpYear): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> </select> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="admin__field _required field-cvv" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="admin__field-label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -120,19 +127,24 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); class="admin__control-text cvv" id="<?= /* @noEscape */ $code ?>_cc_cid" name="payment[cc_cid]" value="" - data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}' + data-validate='{"required-number":true, "validate-cc-cvn":"#<?=/* @noEscape */ $code?>_cc_type"}' autocomplete="off"/> </div> </div> <?php endif; ?> <?= $block->getChildHtml() ?> </fieldset> - -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> +<?php $scriptString = <<<script /** * Disable card server validation in admin */ require(["Magento_Sales/order/create/form"], function () { - order.addExcludedPaymentMethod('<?= /* @noEscape */ $code ?>'); + order.addExcludedPaymentMethod('{$code}'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml index ece7106e91236..77d881257f10a 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/iframe.phtml @@ -6,22 +6,39 @@ /** * @var \Magento\Payment\Block\Transparent\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $params = $block->getParams(); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <html> <head> -<script> -<?php if (isset($params['redirect'])) : ?> - window.location="<?= $block->escapeUrl($params['redirect']) ?>"; -<?php elseif (isset($params['redirect_parent'])) : ?> - window.top.location="<?= $block->escapeUrl($params['redirect_parent']) ?>"; -<?php elseif (isset($params['error_msg'])) : ?> - window.top.alert(<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($params['error_msg']) ?>); -<?php elseif (isset($params['order_success'])) : ?> - window.top.location = "<?= $block->escapeUrl($params['order_success']) ?>"; -<?php else : ?> + <?php $scriptString = '' ?> +<?php if (isset($params['redirect'])): ?> + <?php $scriptString .= <<<script + window.location="{$block->escapeJs($params['redirect'])}"; +script; + ?> +<?php elseif (isset($params['redirect_parent'])): ?> + <?php $scriptString .= <<<script + window.top.location="{$block->escapeJs($params['redirect_parent'])}"; +script; + ?> +<?php elseif (isset($params['error_msg'])): ?> + <?php $encodedErrorMsg = /* @noEscape */ $jsonHelper->jsonEncode($params['error_msg']); + $scriptString .= <<<script + window.top.alert({$encodedErrorMsg}); +script; + ?> +<?php elseif (isset($params['order_success'])): ?> + <?php $scriptString .= <<<script + window.top.location = "{$block->escapeJs($params['order_success'])}"; +script; + ?> +<?php else: ?> + <?php $scriptString .= <<<script var require = window.top.require; require(['jquery'], function($) { $('#edit_form').trigger('processStop'); @@ -34,8 +51,10 @@ $params = $block->getParams(); $('#edit_form').trigger('realOrder'); }); +script; + ?> <?php endif; ?> -</script> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </head> <body> </body> diff --git a/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml b/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml index fb06f1a4dbf33..5997648ed5582 100644 --- a/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml +++ b/app/code/Magento/Payment/view/adminhtml/templates/transparent/info.phtml @@ -6,9 +6,14 @@ /** * @var \Magento\Payment\Block\Transparent\Info $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Payment\Block\Transparent\Info */ ?> -<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none" class="fieldset items redirect"> +<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="fieldset items redirect"> <div><?= $block->escapeHtml(__('We\'ll ask for your payment details before you place an order.')) ?></div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml b/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml index 5f61a3ee1d400..7ddc89aac4f6c 100644 --- a/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/form/cc.phtml @@ -6,6 +6,7 @@ /** * @var \Magento\Payment\Block\Transparent\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); @@ -13,7 +14,7 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); $ccExpYear = $block->getInfoData('cc_exp_year'); ?> <fieldset class="fieldset payment items ccard <?= /* @noEscape */ $code ?>" - id="payment_form_<?= /* @noEscape */ $code ?>" style="display: none;"> + id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="field type required"> <label for="<?= /* @noEscape */ $code ?>_cc_type" class="label"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -29,9 +30,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); }' class="select"> <option value=""><?= $block->escapeHtml(__('--Please Select--')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option value="<?= $block->escapeHtml($typeCode) ?>" - <?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif; ?>> + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach; ?> @@ -60,11 +61,14 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="fields group group-2"> <div class="field no-label month"> <div class="control"> - <select id="<?= /* @noEscape */ $code ?>_expiration" name="payment[cc_exp_month]" class="select month" - data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr"}'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <select id="<?= /* @noEscape */ $code ?>_expiration" + name="payment[cc_exp_month]" + class="select month" + data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code + ?>_expiration_yr"}'> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach; ?> @@ -75,9 +79,9 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="control"> <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="select year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?> - "<?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif; ?>> + "<?php if ($k == $ccExpYear): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach; ?> @@ -87,7 +91,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); </div> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="field cvv required" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -95,7 +99,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <div class="control"> <input type="number" title="<?= $block->escapeHtml(__('Card Verification Number')) ?>" class="input-text cvv" id="<?= /* @noEscape */ $code ?>_cc_cid" name="payment[cc_cid]" value="" - data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}' /> + data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}'/> <?php $content = '<img src=\"' . $block->getViewFileUrl('Magento_Checkout::cvv.png') . '\" alt=\"' . $block->escapeHtml(__('Card Verification Number Visual Reference')) . '\" title=\"' . $block->escapeHtml(__('Card Verification Number Visual Reference')) . '\" />'; ?> @@ -110,3 +114,7 @@ $ccExpYear = $block->getInfoData('cc_exp_year'); <?php endif; ?> <?= $block->getChildHtml() ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml index 290c8384537fb..b8c2c083a7e98 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/form.phtml @@ -5,6 +5,8 @@ */ /** @var \Magento\Payment\Block\Transparent\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $code = $block->escapeHtml($block->getMethodCode()); $ccExpMonth = $block->getInfoData('cc_exp_month'); $ccExpYear = $block->getInfoData('cc_exp_year'); @@ -17,8 +19,12 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che <!-- IFRAME for request to Payment Gateway --> <iframe width="0" height="0" id="<?= /* @noEscape */ $code ?>-transparent-iframe" data-container="<?= /* @noEscape */ $code ?>-transparent-iframe" allowtransparency="true" - frameborder="0" name="iframeTransparent" style="display:none;width:100%;background-color:transparent" + frameborder="0" name="iframeTransparent" src="<?= $block->escapeUrl($block->getViewFileUrl('blank.html')) ?>"></iframe> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none; width: 100%; background-color: transparent;", + 'iframe#' . /* @noEscape */ $code . '-transparent-iframe' +) ?> <form class="form" id="co-transparent-form" action="#" method="post" data-mage-init='{ "transparent":{ "controller":"<?= $block->escapeHtml($block->getRequest()->getControllerName()) ?>", @@ -27,7 +33,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che "cgiUrl":"<?= $block->escapeUrl($block->getCgiUrl()) ?>", "dateDelim":"<?= $block->escapeHtml($block->getDateDelim()) ?>", "cardFieldsMap":<?= $block->escapeHtml($block->getCardFieldsMap()) ?>, - "nativeAction":"<?= $block->escapeUrl($block->getUrl('checkout/onepage/saveOrder', ['_secure' => $block->getRequest()->isSecure()])) ?>" + "nativeAction":"<?= $block->escapeUrl( + $block->getUrl('checkout/onepage/saveOrder', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" }, "validation":[]}'> <fieldset class="fieldset ccard <?= /* @noEscape */ $code ?>" id="payment_form_<?= /* @noEscape */ $code ?>"> <legend class="legend"> @@ -45,9 +53,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che "validate-cc-type-select":"#<?= /* @noEscape */ $code ?>_cc_number" }'> <option value=""><?= $block->escapeHtml(__('--Please Select--')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option value="<?= $block->escapeHtml($typeCode) ?>" - <?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif; ?>> + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($typeName) ?></option> <?php endforeach ?> </select> @@ -83,9 +91,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr" }'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -97,9 +105,9 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="year" data-container="<?= /* @noEscape */ $code ?>-cc-year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpYear): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -109,7 +117,7 @@ $content = '<img src=\"' . $block->escapeUrl($block->getViewFileUrl('Magento_Che </div> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="field required cvv" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml index d45f014de08a6..233d932e5f642 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/iframe.phtml @@ -5,14 +5,20 @@ */ /** @var \Magento\Payment\Block\Transparent\Iframe $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $params = $block->getParams(); ?> <html> <head> - <script> - <?php if (isset($params['redirect'])) : ?> - window.location="<?= $block->escapeUrl($params['redirect']) ?>"; - <?php elseif (isset($params['redirect_parent'])) : ?> + <?php $scriptString = '' ?> + <?php if (isset($params['redirect'])): ?> + <?php $scriptString .= <<<script + window.location="{$block->escapeJs($params['redirect'])}"; +script; + ?> + <?php elseif (isset($params['redirect_parent'])): ?> + <?php $scriptString .= <<<script var require = window.parent.require; require( [ @@ -21,10 +27,15 @@ $params = $block->getParams(); function($) { var parent = window.parent; $(parent).trigger('clearTimeout'); - parent.location="<?= $block->escapeUrl($params['redirect_parent']) ?>"; + parent.location="{$block->escapeJs($params['redirect_parent'])}"; } ); - <?php elseif (isset($params['error_msg'])) : ?> +script; + ?> + <?php elseif (isset($params['error_msg'])): ?> + <?php + $encodedMsg = /* @noEscape */ json_encode($params['error_msg']); + $scriptString .= <<<script var require = window.parent.require; require( [ @@ -33,16 +44,19 @@ $params = $block->getParams(); 'mage/translate', 'Magento_Checkout/js/model/full-screen-loader' ], - function($, globalMessageList, $t, fullScreenLoader) { + function($, globalMessageList, \$t, fullScreenLoader) { var parent = window.parent; $(parent).trigger('clearTimeout'); fullScreenLoader.stopLoader(); globalMessageList.addErrorMessage({ - message: $t(<?= /* @noEscape */ json_encode($params['error_msg'])?>) + message: \$t({$encodedMsg}) }); } ); - <?php elseif (isset($params['multishipping'])) : ?> +script; + ?> + <?php elseif (isset($params['multishipping'])): ?> + <?php $scriptString .= <<<script var require = window.parent.require; require( [ @@ -54,9 +68,15 @@ $params = $block->getParams(); $(parent.document).find('#multishipping-billing-form').submit(); } ); - <?php elseif (isset($params['order_success'])) : ?> - window.parent.location = "<?= $block->escapeUrl($params['order_success']) ?>"; - <?php else : ?> +script; + ?> + <?php elseif (isset($params['order_success'])): ?> + <?php $scriptString .= <<<script + window.parent.location = "{$block->escapeJs($params['order_success'])}"; +script; + ?> + <?php else: ?> + <?php $scriptString .= <<<script var require = window.parent.require; require( [ @@ -85,8 +105,10 @@ $params = $block->getParams(); ); } ); +script; + ?> <?php endif; ?> - </script> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </head> <body></body> </html> diff --git a/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml b/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml index 084e1e0ebf329..49c35e844c39a 100644 --- a/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml +++ b/app/code/Magento/Payment/view/frontend/templates/transparent/info.phtml @@ -6,11 +6,16 @@ /** * @var \Magento\Payment\Block\Transparent\Info $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Payment\Block\Transparent\Info */ ?> -<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none" class="fieldset items redirect"> +<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="fieldset items redirect"> <div> <?= $block->escapeHtml(__('We\'ll ask for your payment details before you place an order.')) ?> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php b/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php index 0cbd82798a2c1..82f2b6ab577e0 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/Order/View.php @@ -19,6 +19,7 @@ /** * Adminhtml sales order view. * @api + * @since 100.2.2 */ class View extends OrderView { @@ -59,6 +60,7 @@ public function __construct( * * @return void * @throws LocalizedException + * @since 100.2.2 */ protected function _construct() { @@ -97,6 +99,7 @@ private function getPaymentAuthorizationUrl(): string * @param Order $order * @return bool * @throws LocalizedException + * @since 100.2.2 */ public function canAuthorize(Order $order): bool { diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php index 340c34fc2635c..6f29e607df58d 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Country.php @@ -9,6 +9,7 @@ */ namespace Magento\Paypal\Block\Adminhtml\System\Config\Field; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Paypal\Model\Config\StructurePlugin; class Country extends \Magento\Config\Block\System\Config\Form\Field @@ -51,15 +52,17 @@ class Country extends \Magento\Config\Block\System\Config\Form\Field * @param \Magento\Framework\View\Helper\Js $jsHelper * @param \Magento\Directory\Helper\Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureHtmlRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Backend\Model\Url $url, \Magento\Framework\View\Helper\Js $jsHelper, \Magento\Directory\Helper\Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null ) { - parent::__construct($context, $data); + parent::__construct($context, $data, $secureHtmlRenderer); $this->_url = $url; $this->_jsHelper = $jsHelper; $this->directoryHelper = $directoryHelper; @@ -132,7 +135,7 @@ protected function _getElementHtml(\Magento\Framework\Data\Form\Element\Abstract } return parent::_getElementHtml($element) . $this->_jsHelper->getScript( - 'require([\'prototype\'], function(){document.observe("dom:loaded", function() {' . $jsString . '});});' + 'require([\'prototype\'], function() { document.observe("dom:loaded", function() {' . $jsString . '}); });' ); } } diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php index 88a33f19de2f4..76fa0856fd23c 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Enable/BmlApi.php @@ -7,7 +7,7 @@ /** * Class Bml - * @deprecated + * @deprecated 100.3.1 * "Enable PayPal Credit" setting was removed. Please @see "Disable Funding Options" */ class BmlApi extends AbstractEnable diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php index 656d9049b5a40..bf8c563d6ce1d 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Field/Hidden.php @@ -9,8 +9,29 @@ */ namespace Magento\Paypal\Block\Adminhtml\System\Config\Field; +use Magento\Backend\Block\Template\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + class Hidden extends \Magento\Config\Block\System\Config\Form\Field { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct(Context $context, array $data = [], ?SecureHtmlRenderer $secureRenderer = null) + { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; + } + /** * Decorate field row html to be invisible * @@ -20,6 +41,10 @@ class Hidden extends \Magento\Config\Block\System\Config\Form\Field */ protected function _decorateRowHtml(\Magento\Framework\Data\Form\Element\AbstractElement $element, $html) { - return '<tr id="row_' . $element->getHtmlId() . '" style="display: none;">' . $html . '</tr>'; + return '<tr id="row_' . $element->getHtmlId() . '" >' . $html . '</tr>' . + /* @noEscape */ $this->secureRenderer->renderStyleAsTag( + "display: none;", + 'tr#row_' . $element->getHtmlId() + ); } } diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php index 944b30f5792ae..567322fd89372 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Hint.php @@ -17,7 +17,7 @@ class Hint extends Template implements RendererInterface { /** * @var string - * @deprecated 100.1.2 + * @deprecated 100.1.0 */ protected $_template = 'Magento_Paypal::system/config/fieldset/hint.phtml'; diff --git a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php index b3a575cc8ea9f..d34646a4138eb 100644 --- a/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php +++ b/app/code/Magento/Paypal/Block/Adminhtml/System/Config/Fieldset/Payment.php @@ -5,6 +5,9 @@ */ namespace Magento\Paypal\Block\Adminhtml\System\Config\Fieldset; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Fieldset renderer for PayPal solution */ @@ -15,22 +18,31 @@ class Payment extends \Magento\Config\Block\System\Config\Form\Fieldset */ protected $_backendConfig; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Context $context * @param \Magento\Backend\Model\Auth\Session $authSession * @param \Magento\Framework\View\Helper\Js $jsHelper * @param \Magento\Config\Model\Config $backendConfig * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Context $context, \Magento\Backend\Model\Auth\Session $authSession, \Magento\Framework\View\Helper\Js $jsHelper, \Magento\Config\Model\Config $backendConfig, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_backendConfig = $backendConfig; - parent::__construct($context, $authSession, $jsHelper, $data); + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $authSession, $jsHelper, $data, $secureRenderer); + $this->secureRenderer = $secureRenderer; } /** @@ -90,19 +102,20 @@ protected function _getHeaderTitleHtml($element) ' class="button action-configure' . (empty($groupConfig['paypal_ec_separate']) ? '' : ' paypal-ec-separate') . $disabledClassString . - '" id="' . - $htmlId . - '-head" onclick="paypalToggleSolution.call(this, \'' . - $htmlId . - "', '" . - $this->getUrl( - 'adminhtml/*/state' - ) . '\'); return false;"><span class="state-closed">' . __( + '" id="' . $htmlId . '-head" >' . + '<span class="state-closed">' . __( 'Configure' ) . '</span><span class="state-opened">' . __( 'Close' ) . '</span></button>'; + $html .= /* @noEscape */ $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "paypalToggleSolution.call(this, '" . $htmlId . "', '" . $this->getUrl('adminhtml/*/state') . + "');event.preventDefault();", + 'button#' . $htmlId . '-head' + ); + if (!empty($groupConfig['more_url'])) { $html .= '<a class="link-more" href="' . $groupConfig['more_url'] . '" target="_blank">' . __( 'Learn More' @@ -151,6 +164,8 @@ protected function _isCollapseState($element) } /** + * Return extra Js. + * * @param \Magento\Framework\Data\Form\Element\AbstractElement $element * @return string * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -162,7 +177,7 @@ protected function _getExtraJs($element) var doScroll = false; Fieldset.toggleCollapse(id, url); if ($(this).hasClassName(\"open\")) { - $$(\".with-button button.button\").each(function(anotherButton) { + \$$(\".with-button button.button\").each(function(anotherButton) { if (anotherButton != this && $(anotherButton).hasClassName(\"open\")) { $(anotherButton).click(); doScroll = true; diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Component.php b/app/code/Magento/Paypal/Block/Express/InContext/Component.php index bb5c17a18fe95..d1adf058e3b4f 100644 --- a/app/code/Magento/Paypal/Block/Express/InContext/Component.php +++ b/app/code/Magento/Paypal/Block/Express/InContext/Component.php @@ -5,14 +5,16 @@ */ namespace Magento\Paypal\Block\Express\InContext; +use Magento\Framework\App\ObjectManager; use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; use Magento\Framework\View\Element\Template; use Magento\Framework\Locale\ResolverInterface; use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** - * Class Component + * Paypal Express InContext Component. * * @api * @since 100.1.0 @@ -32,15 +34,20 @@ class Component extends Template private $config; /** - * @inheritdoc + * @param Context $context * @param ResolverInterface $localeResolver + * @param ConfigFactory $configFactory + * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( Context $context, ResolverInterface $localeResolver, ConfigFactory $configFactory, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); $this->localeResolver = $localeResolver; $this->config = $configFactory->create(); @@ -62,6 +69,8 @@ protected function _toHtml() } /** + * Check if is in Context. + * * @return bool */ private function isInContext() @@ -70,6 +79,8 @@ private function isInContext() } /** + * Return environment. + * * @return string * @since 100.1.0 */ @@ -79,6 +90,8 @@ public function getEnvironment() } /** + * Return locale. + * * @return string * @since 100.1.0 */ @@ -88,6 +101,8 @@ public function getLocale() } /** + * Return merchant id. + * * @return string * @since 100.1.0 */ @@ -97,6 +112,8 @@ public function getMerchantId() } /** + * Check if button is in context. + * * @return bool * @since 100.1.0 */ diff --git a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php index 8d1e04c1397fc..6b4071120b511 100644 --- a/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php +++ b/app/code/Magento/Paypal/Block/Express/InContext/Minicart/Button.php @@ -16,7 +16,7 @@ /** * Class Button - * @deprecated @see \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton + * @deprecated 100.3.1 @see \Magento\Paypal\Block\Express\InContext\Minicart\SmartButton */ class Button extends Template implements ShortcutInterface { diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php index c469338d03961..375a2639ab073 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/Cancel.php @@ -31,6 +31,7 @@ public function execute() ->unsLastSuccessQuoteId() ->unsLastOrderId() ->unsLastRealOrderId(); + $this->_getSession()->unsQuoteId(); // clean quote from session that was set in OnAuthorization $this->messageManager->addSuccessMessage( __('Express Checkout and Order have been canceled.') ); diff --git a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php index 055af4162d5f3..29d4a5bd1f25c 100644 --- a/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php +++ b/app/code/Magento/Paypal/Controller/Express/AbstractExpress/PlaceOrder.php @@ -11,7 +11,7 @@ use Magento\Paypal\Model\Api\ProcessableException as ApiProcessableException; /** - * Class PlaceOrder + * Creates order on backend and prepares session to show appropriate next step in flow * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PlaceOrder extends \Magento\Paypal\Controller\Express\AbstractExpress @@ -127,6 +127,7 @@ public function execute() return; } $this->_initToken(false); // no need in token anymore + $this->_getSession()->unsQuoteId(); // clean quote from session that was set in OnAuthorization $this->_redirect('checkout/onepage/success'); return; } catch (ApiProcessableException $e) { diff --git a/app/code/Magento/Paypal/Model/AbstractConfig.php b/app/code/Magento/Paypal/Model/AbstractConfig.php index cf7e8009dab65..c8ad56f242a63 100644 --- a/app/code/Magento/Paypal/Model/AbstractConfig.php +++ b/app/code/Magento/Paypal/Model/AbstractConfig.php @@ -58,7 +58,7 @@ abstract class AbstractConfig implements ConfigInterface /** * @var string */ - private static $bnCode = 'Magento_Cart_%s'; + private static $bnCode = 'Magento_2_%s'; /** * @var \Magento\Framework\App\Config\ScopeConfigInterface @@ -229,7 +229,7 @@ public function shouldUseUnilateralPayments() /** * Check whether WPP API credentials are available for this method * - * @deprecated + * @deprecated 100.3.1 * @return bool */ public function isWppApiAvailabe() diff --git a/app/code/Magento/Paypal/Model/Api/Nvp.php b/app/code/Magento/Paypal/Model/Api/Nvp.php index 9e4f7693f4bfb..b35f783482e06 100644 --- a/app/code/Magento/Paypal/Model/Api/Nvp.php +++ b/app/code/Magento/Paypal/Model/Api/Nvp.php @@ -1423,7 +1423,7 @@ protected function _validateResponse($method, $response) */ protected function _deformatNVP($nvpstr) { - $intial = 0; + $initial = 0; $nvpArray = []; $nvpstr = strpos($nvpstr, "\r\n\r\n") !== false ? substr($nvpstr, strpos($nvpstr, "\r\n\r\n") + 4) : $nvpstr; @@ -1435,7 +1435,7 @@ protected function _deformatNVP($nvpstr) $valuepos = strpos($nvpstr, '&') ? strpos($nvpstr, '&') : strlen($nvpstr); /*getting the Key and Value values and storing in a Associative Array*/ - $keyval = substr($nvpstr, $intial, $keypos); + $keyval = substr($nvpstr, $initial, $keypos); $valval = substr($nvpstr, $keypos + 1, $valuepos - $keypos - 1); //decoding the response $nvpArray[urldecode($keyval)] = urldecode($valval); @@ -1465,7 +1465,7 @@ protected function _exportLineItems(array &$request, $i = 0) * * @param array $data * @return void - * @deprecated 100.2.2 typo in method name + * @deprecated 100.2.4 typo in method name * @see _exportAddresses */ protected function _exportAddressses($data) diff --git a/app/code/Magento/Paypal/Model/SmartButtonConfig.php b/app/code/Magento/Paypal/Model/SmartButtonConfig.php index 88d68511ae5fe..8adff75df205b 100644 --- a/app/code/Magento/Paypal/Model/SmartButtonConfig.php +++ b/app/code/Magento/Paypal/Model/SmartButtonConfig.php @@ -11,7 +11,7 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\Locale\ResolverInterface; use Magento\Store\Model\ScopeInterface; -use Magento\Paypal\Model\Config as PayPalConfig; +use Magento\Store\Model\StoreManagerInterface; /** * Provides configuration values for PayPal in-context checkout @@ -50,6 +50,11 @@ class SmartButtonConfig */ private $unsupportedPaymentMethods; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Base url for Paypal SDK */ @@ -59,6 +64,7 @@ class SmartButtonConfig * @param ResolverInterface $localeResolver * @param ConfigFactory $configFactory * @param ScopeConfigInterface $scopeConfig + * @param StoreManagerInterface $storeManager * @param array $defaultStyles * @param array $disallowedFundingMap * @param array $unsupportedPaymentMethods @@ -67,6 +73,7 @@ public function __construct( ResolverInterface $localeResolver, ConfigFactory $configFactory, ScopeConfigInterface $scopeConfig, + StoreManagerInterface $storeManager, $defaultStyles = [], $disallowedFundingMap = [], $unsupportedPaymentMethods = [] @@ -75,6 +82,7 @@ public function __construct( $this->config = $configFactory->create(); $this->config->setMethod(Config::METHOD_EXPRESS); $this->scopeConfig = $scopeConfig; + $this->storeManager = $storeManager; $this->defaultStyles = $defaultStyles; $this->disallowedFundingMap = $disallowedFundingMap; $this->unsupportedPaymentMethods = $unsupportedPaymentMethods; @@ -123,6 +131,7 @@ private function generatePaypalSdkUrl(string $page): string 'merchant-id' => $this->config->getValue('merchant_id'), 'locale' => $this->localeResolver->getLocale(), 'intent' => $this->getIntent(), + 'currency' => $this->storeManager->getStore()->getBaseCurrencyCode(), ]; if ($disallowedFunding) { $params['disable-funding'] = $disallowedFunding; diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml index 5619aa27860ce..a2c7b7d82a349 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup.xml @@ -9,7 +9,6 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup"> <arguments> - <argument name="payerName" defaultValue="MPI" type="string"/> <argument name="credentials" defaultValue="_CREDS"/> </arguments> <!--Check in-context--> @@ -17,6 +16,8 @@ <waitForPageLoad stepKey="waitForPageLoad"/> <seeCurrentUrlMatches regex="~\//www.sandbox.paypal.com/~" stepKey="seeCurrentUrlMatchesConfigPath1"/> <conditionalClick selector="{{PayPalPaymentSection.notYouLink}}" dependentSelector="{{PayPalPaymentSection.notYouLink}}" visible="true" stepKey="selectNotYouSection"/> + <conditionalClick selector="{{PayPalPaymentSection.existingAccountLoginBtn}}" dependentSelector="{{PayPalPaymentSection.existingAccountLoginBtn}}" visible="true" stepKey="skipAccountCreationAndLogin"/> + <waitForPageLoad stepKey="waitForLoginPageLoad"/> <waitForElement selector="{{PayPalPaymentSection.email}}" stepKey="waitForLoginForm" /> <fillField selector="{{PayPalPaymentSection.email}}" userInput="{{credentials.magento/paypal_sandbox_login_email}}" stepKey="fillEmail"/> <click selector="{{PayPalPaymentSection.nextButton}}" stepKey="clickNext"/> @@ -25,6 +26,5 @@ <fillField selector="{{PayPalPaymentSection.password}}" userInput="{{credentials.magento/paypal_sandbox_login_password}}" stepKey="fillPassword"/> <click selector="{{PayPalPaymentSection.loginBtn}}" stepKey="login"/> <waitForPageLoad stepKey="wait"/> - <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.reviewUserInfo}}" stepKey="seePayerName"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml index f627b9158f868..aa682cb7a3bb3 100644 --- a/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml +++ b/app/code/Magento/Paypal/Test/Mftf/ActionGroup/StorefrontLoginToPayPalPaymentFromCartActionGroup.xml @@ -8,7 +8,10 @@ <actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="StorefrontLoginToPayPalPaymentFromCartAccountActionGroup" extends="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup"> + <arguments> + <argument name="payerName" defaultValue="MPI" type="string"/> + </arguments> <seeElement selector="{{PayPalCheckoutAsGuestSection.CreditDebitBtn}}" stepKey="assertCheckoutAsGuest" before="waitForLoginForm"/> - <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.userName}}" stepKey="seePayerName"/> + <see userInput="{{payerName}}" selector="{{PayPalPaymentSection.userName}}" stepKey="seePayerName" after="assertCheckoutAsGuest"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml index 361016c40539c..e53c1bbc1ec29 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Section/PayPalExpressCheckoutConfigSection/PayPalPaymentSection.xml @@ -10,6 +10,7 @@ <section name="PayPalPaymentSection"> <element name="guestCheckout" type="input" selector="#guest"/> <element name="loginSection" type="input" selector=" #main>#login"/> + <element name="existingAccountLoginBtn" type="input" selector="#loginSection a"/> <element name="email" type="input" selector="//input[contains(@name, 'email') and not(contains(@style, 'display:none'))]"/> <element name="password" type="input" selector="//input[contains(@name, 'password') and not(contains(@style, 'display:none'))]"/> <element name="loginBtn" type="input" selector="button#btnLogin"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml b/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml index b52fc05ca5a11..44ec500722e58 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Suite/InContextPaypalSuite.xml @@ -13,7 +13,9 @@ <!--Config PayPal Express Checkout--> <actionGroup ref="ConfigPayPalExpressCheckoutActionGroup" stepKey="ConfigPayPalExpressCheckout"/> <!-- Configure PayPal Express Checkout --> - <magentoCLI command="cache:clean" arguments="config full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> </before> <after> <!-- Cleanup Paypal configurations --> @@ -21,7 +23,9 @@ <magentoCLI command="config:set {{StorefrontPaypalDisableInContextCheckoutConfigData.path}} {{StorefrontPaypalDisableInContextCheckoutConfigData.value}}" stepKey="disableInContextPayPal"/> <magentoCLI command="config:set {{StorefrontPaypalDisableConfigData.path}} {{StorefrontPaypalDisableConfigData.value}}" stepKey="disablePaypal"/> <createData entity="SamplePaypalConfig" stepKey="setDefaultPaypalConfig"/> - <magentoCLI command="cache:clean" arguments="config full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="config full_page"/> + </actionGroup> </after> <include> <group name="paypalExpress"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml index cae67f411200c..e21655763e7a3 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontCheckPayPalSmartButtonWithPayPalLabelOnCheckoutPageTest.xml @@ -36,7 +36,9 @@ <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.path}} {{StorefrontPaypalCheckoutPageButtonVerticalLayoutConfigData.value}}" stepKey="setLayoutForPayPalSmartButton"/> <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonPillShapeConfigData.path}} {{StorefrontPaypalCheckoutPageButtonPillShapeConfigData.value}}" stepKey="setShapeForPayPalSmartButton"/> <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.path}} {{StorefrontPaypalCheckoutPageButtonBlueColorConfigData.value}}" stepKey="setColorForPayPalSmartButton"/> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="cleanFullPageCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanFullPageCache"> + <argument name="tags" value="full_page"/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.path}} {{StorefrontPaypalCheckoutPageDisableCustomizeButtonConfigData.value}}" stepKey="disableCustomizeButton"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml index 2cc94caf4c1b1..d27ac4c4a92f5 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInCheckoutPageTest.xml @@ -17,9 +17,6 @@ <severity value="CRITICAL"/> <testCaseId value="MC-13690"/> <group value="paypalExpress"/> - <skip> - <issueId value="MC-33951"/> - </skip> </annotations> <before> <!-- Login --> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml index 41578eed67625..53f9f8adf4d44 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonInProductPageTest.xml @@ -79,9 +79,8 @@ <actionGroup ref="SwitchToPayPalGroupBtnActionGroup" stepKey="clickPayPalBtn"/> <!--Login to Paypal in-context--> - <actionGroup ref="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup" stepKey="LoginToPayPal"> - <argument name="payerName" value="{{Payer.firstName}}"/> - </actionGroup> + <actionGroup ref="StorefrontLoginToPayPalPaymentAccountTwoStepActionGroup" stepKey="LoginToPayPal"/> + <!--Transfer Cart Line and Shipping Method assertion--> <actionGroup ref="PayPalAssertTransferLineAndShippingMethodNotExistActionGroup" stepKey="assertPayPalSettings"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml index 69ec26a8ea806..f949235e98025 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithAUDCurrencyTest.xml @@ -22,7 +22,12 @@ </skip> </annotations> <before> - + <!--Set price scope global--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> + <!--Remove Currency options for Website--> + <remove keyForRemoval="setCurrencyBaseEURWebsites"/> + <remove keyForRemoval="setAllowedCurrencyWebsitesForEURandUSD"/> + <remove keyForRemoval="setCurrencyDefaultEURWebsites"/> <!--Enable Advanced Setting--> <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> <!--Set merchant country--> @@ -31,10 +36,6 @@ <magentoCLI command="config:set {{SetCurrencyAUDBaseConfig.path}} {{SetCurrencyAUDBaseConfig.value}}" stepKey="setCurrencyBaseEUR"/> <magentoCLI command="config:set {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForAUD.value}}" stepKey="setAllowedCurrencyEURandUSD"/> <magentoCLI command="config:set {{SetDefaultCurrencyAUDConfig.path}} {{SetDefaultCurrencyAUDConfig.value}}" stepKey="setCurrencyDefaultEUR"/> - <!--Set Currency options for Website--> - <magentoCLI command="config:set --scope={{SetCurrencyUSDBaseConfig.scope}} --scope-code={{SetCurrencyUSDBaseConfig.scope_code}} {{SetCurrencyUSDBaseConfig.path}} {{SetCurrencyUSDBaseConfig.value}}" stepKey="setCurrencyBaseEURWebsites"/> - <magentoCLI command="config:set --scope={{SetAllowedCurrenciesConfigForUSD.scope}} --scope-code={{SetAllowedCurrenciesConfigForUSD.scope_code}} {{SetAllowedCurrenciesConfigForUSD.path}} {{SetAllowedCurrenciesConfigForUSD.value}},{{SetAllowedCurrenciesConfigForAUD.value}}" stepKey="setAllowedCurrencyWebsitesForEURandUSD"/> - <magentoCLI command="config:set --scope={{SetDefaultCurrencyAUDConfig.scope}} --scope-code={{SetDefaultCurrencyAUDConfig.scope_code}} {{SetDefaultCurrencyAUDConfig.path}} {{SetDefaultCurrencyAUDConfig.value}}" stepKey="setCurrencyDefaultEURWebsites"/> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml index 5077544ea0b39..bd756e4df176d 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithEuroCurrencyTest.xml @@ -22,6 +22,12 @@ </skip> </annotations> <before> + <!--Set price scope global--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> + <!--Remove Currency options for Website--> + <remove keyForRemoval="setCurrencyBaseEURWebsites"/> + <remove keyForRemoval="setAllowedCurrencyWebsitesForEURandUSD"/> + <remove keyForRemoval="setCurrencyDefaultEURWebsites"/> <!--Set merchant country--> <magentoCLI command="config:set {{MerchantUnitedKingdom.path}} {{MerchantUnitedKingdom.value}}" stepKey="setMerchantCountryUK"/> <!--Enable Advanced Setting--> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml index 22997b7005f91..3fd5f44d5a4b6 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontPaypalSmartButtonWithFranceMerchantCountryTest.xml @@ -17,16 +17,16 @@ <severity value="MAJOR"/> <testCaseId value="MC-33274"/> <group value="paypalExpress"/> - <skip> - <issueId value="MC-33951"/> - </skip> </annotations> <before> + <!--Set price scope global--> + <magentoCLI command="config:set {{CatalogPriceScopeGlobalConfigData.path}} {{CatalogPriceScopeGlobalConfigData.value}}" stepKey="setCatalogPriceScopeWebsite"/> <!--Set merchant country--> <magentoCLI command="config:set {{MerchantFrance.path}} {{MerchantFrance.value}}" stepKey="setMerchantCountryUK"/> <!--Enable Advanced Setting--> <magentoCLI command="config:set {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalEnableSkipOrderReviewStepConfigData.value}}" stepKey="enableSkipOrderReview"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> </before> <after> <magentoCLI command="config:set {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.path}} {{StorefrontPaypalDisableSkipOrderReviewStepConfigData.value}}" stepKey="disableSkipOrderReview"/> @@ -34,6 +34,11 @@ <magentoCLI command="config:set {{MerchantUnitedStates.path}} {{MerchantUnitedStates.value}}" stepKey="setMerchantCountryDefault"/> <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> </after> + <!-- Switch to USD-US Dollar--> + <actionGroup ref="StorefrontSwitchCurrencyActionGroup" after="waitForProductPagePageLoad" stepKey="switchCurrency"> + <argument name="currency" value="USD"/> + </actionGroup> + <!-- click on PayPal payment radio button --> <waitForElement selector="{{CheckoutPaymentSection.paymentSectionTitle}}" after="guestCheckoutFillingShippingSection" stepKey="waitForPlaceOrderButton"/> <click selector="{{CheckoutPaymentSection.PayPalPaymentRadio}}" stepKey="guestSelectCheckMoneyOrderPayment"/> @@ -52,5 +57,10 @@ <actionGroup ref="StorefrontPaypalSwitchBackToMagentoFromCheckoutPageActionGroup" after="LoginToPayPal" stepKey="submitPayment"/> <waitForElement after="submitPayment" selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="waitForOrderNumber"/> + <see selector="{{AdminOrderDetailsInformationSection.orderInformationTable}}" userInput="USD / EUR rate" stepKey="seeEURandUSDRate"/> + <assertEquals stepKey="assertSelectedCategories"> + <actualResult type="variable">grabRate</actualResult> + <expectedResult type="array">[USD / EUR rate:]</expectedResult> + </assertEquals> </test> </tests> diff --git a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml index cf0e4b3d0b370..f17fa203af9f5 100644 --- a/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml +++ b/app/code/Magento/Paypal/Test/Mftf/Test/StorefrontVerifySecureURLRedirectPaypalTest.xml @@ -28,11 +28,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php index eebe7c2201689..c06bb6d847225 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/CountryTest.php @@ -12,12 +12,13 @@ use Magento\Framework\Data\Form\Element\AbstractElement; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Helper\Js; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use Magento\Paypal\Block\Adminhtml\System\Config\Field\Country; use Magento\Paypal\Model\Config\StructurePlugin; -use PHPUnit\Framework\Constraint\LogicalAnd; use PHPUnit\Framework\Constraint\StringContains; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Directory\Helper\Data as DirectoryHelper; class CountryTest extends TestCase { @@ -46,6 +47,11 @@ class CountryTest extends TestCase */ protected $_url; + /** + * @var DirectoryHelper + */ + private $helper; + protected function setUp(): void { $helper = new ObjectManager($this); @@ -70,9 +76,29 @@ protected function setUp(): void $this->_request = $this->getMockForAbstractClass(RequestInterface::class); $this->_jsHelper = $this->createMock(Js::class); $this->_url = $this->createMock(Url::class); + $this->helper = $this->createMock(DirectoryHelper::class); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); $this->_model = $helper->getObject( Country::class, - ['request' => $this->_request, 'jsHelper' => $this->_jsHelper, 'url' => $this->_url] + [ + 'request' => $this->_request, + 'jsHelper' => $this->_jsHelper, + 'url' => $this->_url, + 'directoryHelper' => $this->helper, + 'secureHtmlRenderer' => $secureRendererMock + ] ); } @@ -104,15 +130,7 @@ public function testRender($requestCountry, $requestDefaultCountry, $canUseDefau '$("' . $this->_element->getHtmlId() . '").observe("change", function () {' ), ]; - if ($canUseDefault && ($requestCountry == 'US') && $requestDefaultCountry) { - $constraints[] = new StringContains( - '$("' . $this->_element->getHtmlId() . '_inherit").observe("click", function () {' - ); - } - $this->_jsHelper->expects($this->once()) - ->method('getScript') - ->with(new LogicalAnd($constraints)); - $this->_url->expects($this->once()) + $this->_url->expects($this->at(0)) ->method('getUrl') ->with( '*/*/*', @@ -123,6 +141,27 @@ public function testRender($requestCountry, $requestDefaultCountry, $canUseDefau StructurePlugin::REQUEST_PARAM_COUNTRY => '__country__' ] ); + if ($canUseDefault && ($requestCountry == 'US') && $requestDefaultCountry) { + $this->helper->method('getDefaultCountry')->willReturn($requestDefaultCountry); + $constraints[] = new StringContains( + '$("' . $this->_element->getHtmlId() . '_inherit").observe("click", function () {' + ); + $this->_url->expects($this->at(1)) + ->method('getUrl') + ->with( + '*/*/*', + [ + 'section' => 'section', + 'website' => 'website', + 'store' => 'store', + StructurePlugin::REQUEST_PARAM_COUNTRY => '__country__', + Country::REQUEST_PARAM_DEFAULT_COUNTRY => '__default__' + ] + ); + } + $this->_jsHelper->expects($this->once()) + ->method('getScript') + ->with(self::logicalAnd(...$constraints)); $this->_model->render($this->_element); } diff --git a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php index 42578f5a53e39..a7b08499df7cd 100644 --- a/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Block/Adminhtml/System/Config/Field/Enable/AbstractEnableTest.php @@ -7,12 +7,15 @@ namespace Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable; +use Magento\Framework\Data\Form\Element\CollectionFactory; +use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; use Magento\Framework\Data\Form; use Magento\Framework\Data\Form\Element\AbstractElement; -use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; -use Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub; +use Magento\Framework\View\Helper\SecureHtmlRenderer; use PHPUnit\Framework\MockObject\MockObject; +use Magento\Paypal\Test\Unit\Block\Adminhtml\System\Config\Field\Enable\AbstractEnable\Stub; use PHPUnit\Framework\TestCase; /** @@ -34,6 +37,22 @@ class AbstractEnableTest extends TestCase */ protected $elementMock; + /** + * Create mock objects. + * + * @param string[] $classes + * @return MockObject[] + */ + private function createMocks(array $classes): array + { + $mocks = []; + foreach ($classes as $class) { + $mocks[] = $this->getMockBuilder($class)->disableOriginalConstructor()->getMock(); + } + + return $mocks; + } + /** * Set up * @@ -43,14 +62,24 @@ protected function setUp(): void { $objectManager = new ObjectManager($this); + $randomMock = $this->getMockBuilder(Random::class)->disableOriginalConstructor()->getMock(); + $randomMock->method('getRandomString')->willReturn('12345abcdef'); + $mockArguments = $this->createMocks([ + \Magento\Framework\Data\Form\Element\Factory::class, + CollectionFactory::class, + Escaper::class + ]); + $mockArguments[] = []; + $mockArguments[] = $this->createMock(SecureHtmlRenderer::class); + $mockArguments[] = $randomMock; $this->elementMock = $this->getMockBuilder(AbstractElement::class) ->setMethods( [ 'getHtmlId', 'getTooltip', - 'getForm', + 'getForm' ] - )->disableOriginalConstructor() + )->setConstructorArgs($mockArguments) ->getMockForAbstractClass(); $objectManager = new ObjectManager($this); 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 index af9908a5ec20c..8ac4c5268094d 100644 --- 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 @@ -14,6 +14,7 @@ use Magento\User\Model\User; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class GroupTest extends TestCase { @@ -79,9 +80,23 @@ protected function setUp(): void ->method('__call') ->with('getUser') ->willReturn($this->_user); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); + $this->_model = $helper->getObject( \Magento\Paypal\Block\Adminhtml\System\Config\Fieldset\Group::class, - ['authSession' => $this->_authSession] + ['authSession' => $this->_authSession, 'secureRenderer' => $secureRendererMock] ); $this->_model->setGroup($this->_group); } diff --git a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php index 44151dc81a34b..e532e50a51209 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/AbstractConfigTest.php @@ -15,6 +15,9 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Paypal\Model\AbstractConfig + */ class AbstractConfigTest extends TestCase { @@ -353,7 +356,7 @@ public function testGetBuildNotationCode() $productMetadata ); - self::assertEquals('Magento_Cart_SomeEdition', $this->config->getBuildNotationCode()); + self::assertEquals('Magento_2_SomeEdition', $this->config->getBuildNotationCode()); } /** diff --git a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php index fe7619e4166ba..f7ee15efa3ab9 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/SmartButtonConfigTest.php @@ -12,6 +12,8 @@ use Magento\Paypal\Model\Config; use Magento\Paypal\Model\ConfigFactory; use Magento\Paypal\Model\SmartButtonConfig; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -56,10 +58,22 @@ protected function setUp(): void ->setMethods(['create']) ->getMock(); $configFactoryMock->expects($this->once())->method('create')->willReturn($this->configMock); + + /** @var Store|MockObject $storeMock */ + $storeMock = $this->createMock(Store::class); + $storeMock->method('getBaseCurrencyCode') + ->willReturn('USD'); + + /** @var StoreManagerInterface|MockObject $storeManagerMock */ + $storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $storeManagerMock->method('getStore') + ->willReturn($storeMock); + $this->model = new SmartButtonConfig( $this->localeResolverMock, $configFactoryMock, $scopeConfigMock, + $storeManagerMock, $this->getDefaultStyles(), $this->getDisallowedFundingMap(), $this->getUnsupportedPaymentMethods() diff --git a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php index 6089b8b20b1ac..a7bd43e53085f 100644 --- a/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php +++ b/app/code/Magento/Paypal/Test/Unit/Model/_files/expected_config.php @@ -46,6 +46,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'es_MX', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['credit', 'venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -84,6 +85,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_BR', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -121,6 +123,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_US', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -158,6 +161,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_US', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['credit','venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] @@ -196,6 +200,7 @@ function generateExpectedPaypalSdkUrl(array $params) : String 'merchant-id' => 'merchant', 'locale' => 'en_BR', 'intent' => 'authorize', + 'currency' => 'USD', 'disable-funding' => implode( ',', ['card','venmo', 'bancontact', 'eps', 'giropay', 'ideal', 'mybank', 'p24', 'sofort'] diff --git a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml index fe3ed6ce5e00e..2c5d669ccd9e9 100644 --- a/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml +++ b/app/code/Magento/Paypal/etc/adminhtml/system/express_checkout.xml @@ -623,7 +623,7 @@ <label>Header Image URL</label> <config_path>paypal/style/paypal_hdrimg</config_path> <tooltip> - <![CDATA[The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style="color:red">https</strong> is highly encouraged.]]> + <![CDATA[The image at the top left of the checkout page. Max size is 750x90-pixel. <strong class="colorRed">https</strong> is highly encouraged.]]> </tooltip> <attribute type="shared">1</attribute> </field> diff --git a/app/code/Magento/Paypal/etc/csp_whitelist.xml b/app/code/Magento/Paypal/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..932664bde9e09 --- /dev/null +++ b/app/code/Magento/Paypal/etc/csp_whitelist.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="img-src"> + <values> + <value id="paypal_analytics" type="host">t.paypal.com</value> + <value id="www_paypal" type="host">www.paypal.com</value> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="paypal_fpdbs" type="host">fpdbs.paypal.com</value> + <value id="paypal_fpdbs_sandbox" type="host">fpdbs.sandbox.paypal.com</value> + </values> + </policy> + <policy id="script-src"> + <values> + <value id="www_paypal" type="host">www.paypal.com</value> + <value id="www_sandbox_paypal" type="host">www.sandbox.paypal.com</value> + <value id="paypal_objects" type="host">www.paypalobjects.com</value> + <value id="paypal_analytics" type="host">t.paypal.com</value> + </values> + </policy> + <policy id="frame-src"> + <values> + <value id="www_paypal" type="host">www.paypal.com</value> + <value id="www_sandbox_paypal" type="host">www.sandbox.paypal.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/Paypal/i18n/en_US.csv b/app/code/Magento/Paypal/i18n/en_US.csv index 852bf39c57966..8db6285dc157e 100644 --- a/app/code/Magento/Paypal/i18n/en_US.csv +++ b/app/code/Magento/Paypal/i18n/en_US.csv @@ -585,9 +585,9 @@ Schedule,Schedule " "Header Image URL","Header Image URL" " - The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style=""color:red"">https</strong> is highly encouraged. + The image at the top left of the checkout page. Max size is 750x90-pixel. <strong class=""colorRed"">https</strong> is highly encouraged. "," - The image at the top left of the checkout page. Max size is 750x90-pixel. <strong style=""color:red"">https</strong> is highly encouraged. + The image at the top left of the checkout page. Max size is 750x90-pixel. <strong class=""colorRed"">https</strong> is highly encouraged. " "Header Background Color","Header Background Color" " diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml index 19cebe863b7ef..7413c29fdd59e 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/billing/agreement/form.phtml @@ -4,10 +4,13 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Paypal\Block\Adminhtml\Billing\Agreement\View\Form $block */ +/** + * @var \Magento\Paypal\Block\Adminhtml\Billing\Agreement\View\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?php $code = $block->escapeHtml($block->getMethodCode()) ?> -<fieldset class="form-list" id="payment_form_<?= /* @noEscape */ $code ?>" style="display:none;"> +<fieldset class="form-list" id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="admin__field _required"> <label for="<?= /* @noEscape */ $code ?>_ba_agreement_id" class="admin__field-label"> <span><?= $block->escapeHtml(__('Billing Agreement')) ?></span> @@ -17,7 +20,7 @@ name="payment[<?= $block->escapeHtml($block->getTransportBAId()) ?>]" class="required-entry admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getBillingAgreements() as $id => $referenceId) : ?> + <?php foreach ($block->getBillingAgreements() as $id => $referenceId): ?> <option value="<?= $block->escapeHtml($id) ?>"> <?= $block->escapeHtml($referenceId) ?> </option> @@ -26,3 +29,7 @@ </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml index a4e7b6974c737..b37bd261ce1a5 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/payment/form/billing/agreement.phtml @@ -4,11 +4,13 @@ * See COPYING.txt for license details. */ -/* @var $block \Magento\Paypal\Block\Payment\Form\Billing\Agreement */ +/** + * @var $block \Magento\Paypal\Block\Payment\Form\Billing\Agreement + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?php $code = $block->escapeHtml($block->getMethodCode()) ?> -<fieldset class="admin__fieldset payment-method form-list" - id="payment_form_<?= /* @noEscape */ $code ?>" style="display:none;"> +<fieldset class="admin__fieldset payment-method form-list" id="payment_form_<?= /* @noEscape */ $code ?>"> <div class="admin__field _required"> <label class="admin__field-label" for="<?= /* @noEscape */ $code ?>_ba_agreement_id"> @@ -19,7 +21,7 @@ name="payment[<?= $block->escapeHtml($block->getTransportName()) ?>]" class="required-entry admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getBillingAgreements() as $id => $referenceId) : ?> + <?php foreach ($block->getBillingAgreements() as $id => $referenceId): ?> <option value="<?= $block->escapeHtml($id) ?>" <?= ($id == $block->getInfoData($block->getTransportName())) ? ' selected="selected"' : ''; @@ -31,3 +33,7 @@ </div> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml index f906a08425aa4..0268ab4f4c482 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/api_wizard.phtml @@ -7,11 +7,12 @@ /** * @see \Magento\Paypal\Block\Adminhtml\System\Config\ApiWizard * @var \Magento\Paypal\Block\Adminhtml\System\Config\ApiWizard $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="pp-buttons-container"> - <div dir="ltr" style="text-align: left;" trbidi="on"> - <script> + <div id="paypal_api_config" dir="ltr" trbidi="on"> + <?php $scriptString = <<<script (function(d, s, id){ var js, ref = d.getElementsByTagName(s)[0]; if (!d.getElementById(id)){ @@ -19,7 +20,9 @@ js.src = "https://www.paypal.com/webapps/merchantboarding/js/lib/lightbox/partner.js"; ref.parentNode.insertBefore(js, ref); } }(document, "script", "paypal-js")); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <a class="action-default" data-paypal-button="true" @@ -32,3 +35,4 @@ target="PPFrame"><?= $block->escapeHtml($block->getSandboxButtonLabel()) ?></a> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("text-align: left;", 'div#paypal_api_config') ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml index 72b7ac86ee056..040be8e3f4fa6 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/bml_api_wizard.phtml @@ -7,16 +7,21 @@ /** * @see \Magento\Paypal\Block\Adminhtml\System\Config\BmlApiWizard * @var \Magento\Paypal\Block\Adminhtml\System\Config\BmlApiWizard $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="pp-buttons-container"> - <button onclick="javascript:window.open( - '<?= $block->escapeUrl($block->getButtonUrl()) ?>', - 'bmlapiwizard', - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + - 'left=100, top=100, width=550, height=550' - ); return false;" - class="scalable" type="button" id="<?= $block->escapeHtml($block->getHtmlId()) ?>"> + <button class="scalable" type="button" id="<?= $block->escapeHtml($block->getHtmlId()) ?>"> <span><span><span><?= $block->escapeHtml($block->getButtonLabel()) ?></span></span></span> </button> </div> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.open( + '" . $block->escapeUrl($block->getButtonUrl()) . "', + 'bmlapiwizard', + 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + + 'left=100, top=100, width=550, height=550' + );event.preventDefault()", + 'button#' . $block->escapeHtml($block->getHtmlId()) +) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml index ee97d60aa72f8..cd52dd06f9bc1 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/system/config/rules.phtml @@ -6,14 +6,18 @@ /** * @var \Magento\Paypal\Block\Adminhtml\System\Config\ResolutionRules $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ - ?> -<script> + +<?php $scriptString = <<<script require([ "Magento_Paypal/js/solutions", "domReady!" ], function (Solutions) { - var solutions = new Solutions({config: {solutions: <?= /* @noEscape */ $block->getJson() ?>}}); - }); -</script> +script; + +$scriptString .= 'var solutions = new Solutions({config: {solutions: ' . /* @noEscape */ $block->getJson() . '}}); + });'; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml index f4318b40fef1c..98e59f3a066c3 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/form.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Paypal\Block\Adminhtml\Payflowpro\CcForm $block */ +/** + * @var \Magento\Paypal\Block\Adminhtml\Payflowpro\CcForm $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $code = $block->escapeHtml($block->getMethodCode()); $ccType = $block->getInfoData('cc_type'); $ccExpYear = $block->getInfoData('cc_exp_year'); @@ -17,8 +20,11 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); allowtransparency="true" frameborder="0" name="iframeTransparent" - style="display: none; width: 100%; background-color: transparent;" src="<?= $block->escapeUrl($block->getViewFileUrl('blank.html')) ?>"></iframe> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none; width: 100%; background-color: transparent;", + "iframe#" . /* @noEscape */ $code . "-transparent-iframe" +) ?> <fieldset id="payment_form_<?= /* @noEscape */ $code ?>" class="admin__fieldset" @@ -31,9 +37,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); "orderSaveUrl":"<?= $block->escapeUrl($block->getOrderUrl()) ?>", "cgiUrl":"<?= $block->escapeUrl($block->getCgiUrl()) ?>", "expireYearLength":"<?= $block->escapeHtml($block->getMethodConfigData('cc_year_length')) ?>", - "nativeAction":"<?= $block->escapeUrl($block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()])) ?>" - }, "validation":[]}' - style="display: none;"> + "nativeAction":"<?= $block->escapeUrl( + $block->getUrl('*/*/save', ['_secure' => $block->getRequest()->isSecure()]) + ) ?>" + }, "validation":[]}'> <div class="admin__field _required"> <label for="<?= /* @noEscape */ $code ?>_cc_type" class="admin__field-label"> <span><?= $block->escapeHtml(__('Credit Card Type')) ?></span> @@ -46,9 +53,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-validate='{required:true, "validate-cc-type-select":"#<?= /* @noEscape */ $code ?>_cc_number"}' class="admin__control-select"> <option value=""><?= $block->escapeHtml(__('Please Select')) ?></option> - <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName) : ?> + <?php foreach ($block->getCcAvailableTypes() as $typeCode => $typeName): ?> <option - value="<?= $block->escapeHtml($typeCode) ?>"<?php if ($typeCode == $ccType) : ?> selected="selected"<?php endif ?>> + value="<?= $block->escapeHtml($typeCode) ?>" + <?php if ($typeCode == $ccType): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($typeName) ?> </option> <?php endforeach ?> @@ -86,10 +94,10 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); data-container="<?= /* @noEscape */ $code ?>-cc-month" class="admin__control-select admin__control-select-month" data-validate='{required:true, "validate-cc-exp":"#<?= /* @noEscape */ $code ?>_expiration_yr"}'> - <?php foreach ($block->getCcMonths() as $k => $v) : ?> + <?php foreach ($block->getCcMonths() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpMonth) : ?> selected="selected"<?php endif; ?>> + <?php if ($k == $ccExpMonth): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> @@ -98,17 +106,17 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); <select id="<?= /* @noEscape */ $code ?>_expiration_yr" name="payment[cc_exp_year]" class="admin__control-select admin__control-select-year" data-container="<?= /* @noEscape */ $code ?>-cc-year" data-validate='{required:true}'> - <?php foreach ($block->getCcYears() as $k => $v) : ?> + <?php foreach ($block->getCcYears() as $k => $v): ?> <option value="<?= /* @noEscape */ $k ? $block->escapeHtml($k) : '' ?>" - <?php if ($k == $ccExpYear) : ?> selected="selected"<?php endif ?>> + <?php if ($k == $ccExpYear): ?> selected="selected"<?php endif ?>> <?= $block->escapeHtml($v) ?> </option> <?php endforeach ?> </select> </div> </div> - <?php if ($block->hasVerification()) : ?> + <?php if ($block->hasVerification()): ?> <div class="admin__field _required field-cvv" id="<?= /* @noEscape */ $code ?>_cc_type_cvv_div"> <label for="<?= /* @noEscape */ $code ?>_cc_cid" class="admin__field-label"> <span><?= $block->escapeHtml(__('Card Verification Number')) ?></span> @@ -120,13 +128,13 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); class="admin__control-text cvv" id="<?= /* @noEscape */ $code ?>_cc_cid" name="payment[cc_cid]" value="" - data-validate='{"required-number":true, "validate-cc-cvn":"#<?= /* @noEscape */ $code ?>_cc_type"}' + data-validate='{"required-number":true, "validate-cc-cvn":"#<?=/* @noEscape */ $code?>_cc_type"}' autocomplete="off"/> </div> </div> <?php endif; ?> - <?php if ($block->isVaultEnabled()) : ?> + <?php if ($block->isVaultEnabled()): ?> <div class="admin__field admin__field-option field-tooltip-content"> <input type="checkbox" id="<?= /* @noEscape */ $code ?>_vault" @@ -142,12 +150,18 @@ $ccExpMonth = $block->getInfoData('cc_exp_month'); <?= $block->getChildHtml() ?> </fieldset> - -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none;", + "fieldset#payment_form_" . /* @noEscape */ $code +) ?> +<?php $codeNoEscaped = /* @noEscape */ $code; +$scriptString = <<<script /** * Disable card server validation in admin */ require(["Magento_Sales/order/create/form"], function () { - order.addExcludedPaymentMethod('<?= /* @noEscape */ $code ?>'); + order.addExcludedPaymentMethod('{$codeNoEscaped}'); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml index 4edb109d6a4b9..8808bd08985f5 100644 --- a/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml +++ b/app/code/Magento/Paypal/view/adminhtml/templates/transparent/iframe.phtml @@ -6,22 +6,28 @@ /** * @var \Magento\Payment\Block\Transparent\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $params = $block->getParams(); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <html> <head> -<script> -<?php if (isset($params['redirect'])) : ?> - window.location="<?= $block->escapeUrl($params['redirect']) ?>"; -<?php elseif (isset($params['redirect_parent'])) : ?> - window.top.location="<?= $block->escapeUrl($params['redirect_parent']) ?>"; -<?php elseif (isset($params['error_msg'])) : ?> - window.top.alert(<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($params['error_msg']) ?>); -<?php elseif (isset($params['order_success'])) : ?> - window.top.location = "<?= $block->escapeUrl($params['order_success']) ?>"; -<?php else : ?> +<?php +$scriptString = ''; +if (isset($params['redirect'])): + $scriptString .= 'window.location="' . $block->escapeJs($params['redirect']) . '";' . PHP_EOL; +elseif (isset($params['redirect_parent'])): + $scriptString .= 'window.top.location="' . $block->escapeJs($params['redirect_parent']) . '";' . PHP_EOL; +elseif (isset($params['error_msg'])): + $scriptString .= 'window.top.alert(' . /* @noEscape */ $jsonHelper->jsonEncode($params['error_msg']) . ');' . + PHP_EOL; +elseif (isset($params['order_success'])): + $scriptString .= 'window.top.location = "' . $block->escapeJs($params['order_success']) . '";' . PHP_EOL; +else: + $scriptString .= <<<script var require = window.top.require; require(['jquery'], function($) { var cc_number = $("input[name='payment[cc_number]']").val(); @@ -33,8 +39,10 @@ $params = $block->getParams(); $('#edit_form').trigger('realOrder'); }); -<?php endif; ?> -</script> +script; +endif; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </head> <body> </body> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml index 8e222ca7eb04d..69c7c8179850a 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/review.phtml @@ -6,12 +6,13 @@ /** * @var \Magento\Paypal\Block\Express\Review $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="paypal-review view"> <div class="block block-order-details-view"> <div class="block-content"> - <?php if ($block->getShippingAddress()) : ?> + <?php if ($block->getShippingAddress()): ?> <div class="box box-order-shipping-method"> <strong class="box-title"> <span><?= $block->escapeHtml(__('Shipping Method')) ?></span> @@ -20,17 +21,20 @@ <form method="post" id="shipping-method-form" action="<?= $block->escapeUrl($block->getShippingMethodSubmitUrl()) ?>" class="form"> - <?php if ($block->canEditShippingMethod()) : ?> - <?php if ($groups = $block->getShippingRateGroups()) : ?> + <?php if ($block->canEditShippingMethod()): ?> + <?php if ($groups = $block->getShippingRateGroups()): ?> <?php $currentRate = $block->getCurrentShippingRate(); ?> <div class="field shipping required"> <select name="shipping_method" id="shipping-method" class="select"> - <?php if (!$currentRate) : ?> - <option value=""><?= $block->escapeHtml(__('Please select a shipping method...')); ?></option> + <?php if (!$currentRate): ?> + <option value=""> + <?= $block->escapeHtml(__('Please select a shipping method...')); ?> + </option> <?php endif; ?> - <?php foreach ($groups as $code => $rates) : ?> - <optgroup label="<?= $block->escapeHtml($block->getCarrierName($code)); ?>"> - <?php foreach ($rates as $rate) : ?> + <?php foreach ($groups as $code => $rates): ?> + <optgroup label="<?= $block->escapeHtml($block->getCarrierName($code)); + ?>"> + <?php foreach ($rates as $rate): ?> <option value="<?= $block->escapeHtml( $block->renderShippingRateValue($rate) @@ -39,7 +43,8 @@ <?= ($currentRate === $rate) ? ' selected="selected"' : ''; ?>> - <?= /* @noEscape */ $block->renderShippingRateOption($rate); ?> + <?= /* @noEscape */ $block->renderShippingRateOption($rate); + ?> </option> <?php endforeach; ?> </optgroup> @@ -56,14 +61,14 @@ </button> </div> </div> - <?php else : ?> + <?php else: ?> <p> <?= $block->escapeHtml(__( 'Sorry, no quotes are available for this order right now.' )); ?> </p> <?php endif; ?> - <?php else : ?> + <?php else: ?> <p> <?= /* @noEscape */ $block->renderShippingRateOption( $block->getCurrentShippingRate() @@ -85,7 +90,7 @@ );?> </address> </div> - <?php if ($block->getCanEditShippingAddress()) : ?> + <?php if ($block->getCanEditShippingAddress()): ?> <div class="box-actions"> <a href="<?= $block->escapeUrl($block->getEditUrl()) ?>" class="action edit"> <span><?= $block->escapeHtml(__('Edit')) ?></span> @@ -102,7 +107,7 @@ <img src="https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-medium.png" alt="<?= $block->escapeHtml(__('Buy now with PayPal')) ?>"/> </div> - <?php if ($block->getEditUrl()) : ?> + <?php if ($block->getEditUrl()): ?> <div class="box-actions"> <a href="<?= $block->escapeUrl($block->getEditUrl()) ?>" class="action edit"> <span><?= $block->escapeHtml(__('Edit Payment Information')) ?></span> @@ -137,10 +142,11 @@ <span><?= $block->escapeHtml(__('Place Order')) ?></span> </button> </div> - <span class="please-wait load indicator" id="review-please-wait" style="display: none;" + <span class="please-wait load indicator" id="review-please-wait" data-text="<?= $block->escapeHtml(__('Submitting order information...')) ?>"> <span><?= $block->escapeHtml(__('Submitting order information...')) ?></span> </span> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'span#review-please-wait')?> </div> </form> </div> diff --git a/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml b/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml index 839d278ed227c..826628c5cbc63 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/express/review/shipping/method.phtml @@ -4,22 +4,25 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Paypal\Block\Express\Review */ +/** + * @var $block \Magento\Paypal\Block\Express\Review + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div id="shipping-method-container"> - <?php if ($block->getCanEditShippingMethod() || !$block->getCurrentShippingRate()) : ?> - <?php if ($groups = $block->getShippingRateGroups()) : ?> + <?php if ($block->getCanEditShippingMethod() || !$block->getCurrentShippingRate()): ?> + <?php if ($groups = $block->getShippingRateGroups()): ?> <?php $currentRate = $block->getCurrentShippingRate(); ?> <select name="shipping_method" id="shipping_method" class="required-entry"> - <?php if (!$currentRate) : ?> + <?php if (!$currentRate): ?> <option value=""> <?= $block->escapeHtml(__('Please select a shipping method...')) ?> </option> <?php endif; ?> - <?php foreach ($groups as $code => $rates) : ?> - <optgroup label="<?= $block->escapeHtml($block->getCarrierName($code)) ?>" - style="font-style:normal;"> - <?php foreach ($rates as $rate) : ?> + <?php foreach ($groups as $code => $rates): ?> + <optgroup id="group_<?= /* @noEscape */ $code ?>" + label="<?= $block->escapeHtml($block->getCarrierName($code)) ?>"> + <?php foreach ($rates as $rate): ?> <option value="<?= $block->escapeHtml($block->renderShippingRateValue($rate)) ?>" <?= ($currentRate === $rate) ? ' selected="selected"' : '' ?>> @@ -27,16 +30,20 @@ </option> <?php endforeach; ?> </optgroup> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'font-style:normal;', + 'optgroup#group_' . /* @noEscape */ $code + ) ?> <?php endforeach; ?> </select> - <?php else : ?> + <?php else: ?> <p> <strong> <?= $block->escapeHtml(__('Sorry, no quotes are available for this order right now.')) ?> </strong> </p> <?php endif; ?> - <?php else : ?> + <?php else: ?> <p> <strong> <?= /* @noEscape */ $block->renderShippingRateOption($block->getCurrentShippingRate()) ?> @@ -44,6 +51,10 @@ </p> <?php endif; ?> </div> -<div style="display: none" id="shipping_method_update"> +<div id="shipping_method_update"> <p><?= $block->escapeHtml(__('Please update order data to get shipping methods and rates')) ?></p> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#shipping_method_update' +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml b/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml index ec6f7b4ad985e..036ebb49d4eff 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/hss/form.phtml @@ -8,6 +8,7 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Payflow\Link\Iframe */ ?> @@ -20,8 +21,10 @@ <input type="hidden" name="SECURETOKENID" value="<?= $block->escapeHtml($block->getSecureTokenId()) ?>"/> <input type="hidden" name="MODE" value="<?= /* @noEscape */ $block->isTestMode() ? 'TEST' : 'LIVE' ?>"/> </form> -<script> +<?php $scriptString = <<<script document.getElementById('token_form').submit(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml index c2339f85b7ca5..d8bdf9b183b68 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/hss/info.phtml @@ -6,11 +6,15 @@ /** * @var \Magento\Paypal\Block\Payment\Info $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" - style="display:none" class="hss items"> +<div id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="hss items"> <?= $block->escapeHtml(__( 'You will be required to enter your payment details after you place an order.' )); ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml b/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml index f0f672492270a..63249f9a52455 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/partner/logo.phtml @@ -7,6 +7,7 @@ /** * @var \Magento\Paypal\Block\Logo $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Logo */ ?> @@ -14,11 +15,6 @@ <div class="block paypal acceptance"> <div class="block-content"> <a href="#" title="<?= $block->escapeHtml(__('Additional Options')) ?>" - onclick="javascript:window.open( - '<?= $block->escapeUrl($block->getAboutPaypalPageUrl()) ?>', - 'paypal', - 'width=600,height=350,left=0,top=0,location=no,status=yes,scrollbars=yes,resizable=yes' - ); return false;" class="action paypal additional"> <img src="<?= $block->escapeUrl($block->getLogoImageUrl()) ?>" alt="<?= $block->escapeHtml(__('Additional Options')) ?>" @@ -26,3 +22,12 @@ </a> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.open( + '" . $block->escapeJs($block->getAboutPaypalPageUrl()) . "', + 'paypal', + 'width=600,height=350,left=0,top=0,location=no,status=yes,scrollbars=yes,resizable=yes' + ); event.preventDefault();", + 'div.block.paypal.acceptance div.block-content a.action.paypal.additional' +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml index e643acac297e9..4491b8c09603e 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/form.phtml @@ -6,6 +6,7 @@ /** * @var \Magento\Paypal\Block\Payflow\Advanced\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <html> @@ -17,8 +18,10 @@ <input type="hidden" name="SECURETOKENID" value="<?= $block->escapeHtml($block->getSecureTokenId()) ?>"/> <input type="hidden" name="MODE" value="<?= /* @noEscape */ $block->isTestMode() ? 'TEST' : 'LIVE' ?>"/> </form> -<script> +<?php $scriptString = <<<script document.getElementById('token_form').submit(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml index d5944a6f22f5f..8e11186d43e6c 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowadvanced/info.phtml @@ -6,11 +6,16 @@ /** * @var \Magento\Paypal\Block\Payflow\Advanced\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" style="display:none" +<fieldset id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" class="fieldset payflowadvanced items redirect"> <div> <?= $block->escapeHtml(__('You will be required to enter your payment details after you place an order.')) ?> </div> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml index cef3e2f0565ba..839ded13ae680 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/form.phtml @@ -8,6 +8,7 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Payflow\Link\Iframe */ ?> @@ -20,8 +21,10 @@ <input type="hidden" name="SECURETOKENID" value="<?= $block->escapeHtml($block->getSecureTokenId()) ?>"/> <input type="hidden" name="MODE" value="<?= /* @noEscape */ $block->isTestMode() ? 'TEST' : 'LIVE' ?>"/> </form> -<script> +<?php $scriptString = <<<script document.getElementById('token_form').submit(); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml index cbd4a8ba715e7..3d17b24f53e61 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/info.phtml @@ -6,9 +6,13 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div class="payflowlink items" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>" - style="display:none"> +<div class="payflowlink items" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> <?= $block->escapeHtml(__('You will be required to enter your payment details after you place an order.')) ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml index 75cc2a09e9444..35b678a8853b1 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payflowlink/redirect.phtml @@ -8,13 +8,15 @@ /** * @var \Magento\Paypal\Block\Payflow\Link\Iframe $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <html> <head> </head> <body> -<script> +<?php $scriptString= <<<script + (function() { 'use strict'; @@ -29,13 +31,13 @@ } } - var cartUrl = '<?= $block->escapeUrl($block->getUrl('checkout/cart')) ?>', - successUrl = '<?= $block->escapeUrl($block->getUrl('checkout/onepage/success')) ?>', - goToSuccessPage = '<?= $block->escapeUrl($block->getGotoSuccessPage()) ?>', + var cartUrl = '{$block->escapeJs($block->getUrl('checkout/cart'))}', + successUrl = '{$block->escapeJs($block->getUrl('checkout/onepage/success'))}', + goToSuccessPage = '{$block->escapeJs($block->getGotoSuccessPage())}', require = window.top.require, windowContext = window, errorMessage = { - message: '<?= $block->escapeHtml($block->getErrorMsg()) ?>' + message: '{$block->escapeJs($block->getErrorMsg())}' }; if(typeof(require) == "undefined") { @@ -49,8 +51,10 @@ }) } - })(); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </body> </html> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml b/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml index 75ee08111bd7a..85f627ad5509b 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payment/form/billing/agreement.phtml @@ -6,10 +6,11 @@ /** * @var \Magento\Paypal\Block\Payment\Form\Billing\Agreement $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $code = $block->escapeHtml($block->getMethodCode()); ?> -<div class="field items required" id="payment_form_<?= /* @noEscape */ $code ?>" style="display:none;"> +<div class="field items required" id="payment_form_<?= /* @noEscape */ $code ?>"> <label for="<?= /* @noEscape */ $code ?>_ba_agreement_id" class="label"> <span><?= $block->escapeHtml(__('Billing Agreement')) ?></span> </label> @@ -17,7 +18,7 @@ $code = $block->escapeHtml($block->getMethodCode()); <select id="<?= /* @noEscape */ $code ?>_ba_agreement_id" name="payment[<?= $block->escapeHtml($block->getTransportName()) ?>]" class="select"> <option value=""><?= $block->escapeHtml(__('-- Please Select Billing Agreement--')) ?></option> - <?php foreach ($block->getBillingAgreements() as $id => $referenceId) : ?> + <?php foreach ($block->getBillingAgreements() as $id => $referenceId): ?> <option value="<?= $block->escapeHtml($id) ?>"> <?= $block->escapeHtml($referenceId) ?> </option> @@ -25,3 +26,4 @@ $code = $block->escapeHtml($block->getMethodCode()); </select> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'div#payment_form_' . /* @noEscape */ $code) ?> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml b/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml index d9fb5fb43bcc7..0b95e3788f91c 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payment/mark.phtml @@ -7,6 +7,7 @@ /** * Note: This mark is a requirement of PayPal * @var \Magento\Paypal\Block\Express\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Paypal\Block\Express\Form */ $url = $block->escapeUrl($block->getPaymentAcceptanceMarkHref()); @@ -14,18 +15,21 @@ $url = $block->escapeUrl($block->getPaymentAcceptanceMarkHref()); <!-- PayPal Logo --> <img src="<?= $block->escapeUrl($block->getPaymentAcceptanceMarkSrc()) ?>" alt="<?= $block->escapeHtml(__('Acceptance Mark')) ?>" class="paypal icon"/> -<a href="<?= /* @noEscape */ $url ?>" - onclick="javascript:window.open( - '<?= /* @noEscape */ $url ?>', - 'olcwhatispaypal', - 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + - 'left=0, top=0, width=400, height=350' - ); return false;" - class="action paypal about"> +<a href="<?= /* @noEscape */ $url ?>" class="action paypal about"> <?php if ($block->getPaymentWhatIs()) { echo $block->escapeHtml(__($block->getPaymentWhatIs())); -} else { + } else { echo $block->escapeHtml(__('What is PayPal?')); -} ?> + } ?> </a> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.open( + '" . /* @noEscape */ $block->escapeJs($block->getPaymentAcceptanceMarkHref()) . "', + 'olcwhatispaypal', + 'toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=yes, resizable=yes, ,' + + 'left=0, top=0, width=400, height=350' + ); event.preventDefault();", + 'a.action.paypal.about' +) ?> <!-- PayPal Logo --> diff --git a/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml b/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml index 683153b12db7a..a123f9b9ed7dc 100644 --- a/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml +++ b/app/code/Magento/Paypal/view/frontend/templates/payment/redirect.phtml @@ -6,15 +6,15 @@ /** * @var \Magento\PayPal\Block\Express\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\PayPal\Block\Express\Form */ $code = $block->escapeHtml($block->getBillingAgreementCode()); ?> -<fieldset class="fieldset paypal items redirect" style="display:none;" - id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> +<fieldset class="fieldset paypal items redirect" id="payment_form_<?= $block->escapeHtml($block->getMethodCode()) ?>"> <div><?= $block->escapeHtml($block->getRedirectMessage()) ?></div> <?php ?> - <?php if ($code) : ?> + <?php if ($code): ?> <input type="checkbox" id="<?= /* @noEscape */ $code ?>" value="1" class="checkbox" name="payment[<?= /* @noEscape */ $code ?>]"> <label for="<?= /* @noEscape */ $code ?>" class="label"> @@ -24,3 +24,7 @@ $code = $block->escapeHtml($block->getBillingAgreementCode()); </label> <?php endif; ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . $block->escapeHtml($block->getMethodCode()) +) ?> diff --git a/app/code/Magento/PaypalGraphQl/Model/PayflowProCcVaultAdditionalDataProvider.php b/app/code/Magento/PaypalGraphQl/Model/PayflowProCcVaultAdditionalDataProvider.php new file mode 100644 index 0000000000000..781cd8d0a9095 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/PayflowProCcVaultAdditionalDataProvider.php @@ -0,0 +1,47 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model; + +use Magento\Framework\Stdlib\ArrayManager; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderInterface; + +/** + * Get payment additional data for Payflow pro cc vault payment + */ +class PayflowProCcVaultAdditionalDataProvider implements AdditionalDataProviderInterface +{ + const CC_VAULT_CODE = 'payflowpro_cc_vault'; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @param ArrayManager $arrayManager + */ + public function __construct( + ArrayManager $arrayManager + ) { + $this->arrayManager = $arrayManager; + } + + /** + * Returns additional data + * + * @param array $args + * @return array + */ + public function getData(array $args): array + { + if (isset($args[self::CC_VAULT_CODE])) { + return $args[self::CC_VAULT_CODE]; + } + return []; + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowPro/SetPaymentMethodOnCart.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowPro/SetPaymentMethodOnCart.php new file mode 100644 index 0000000000000..7ca4d41cfd33a --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowPro/SetPaymentMethodOnCart.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowPro; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Paypal\Model\Config; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderPool; +use Magento\Sales\Model\Order\Payment\Repository as PaymentRepository; +use Magento\PaypalGraphQl\Observer\PayflowProSetCcData; + +/** + * Set additionalInformation on payment for PayflowPro method + */ +class SetPaymentMethodOnCart +{ + /** + * @var PaymentRepository + */ + private $paymentRepository; + + /** + * @var AdditionalDataProviderPool + */ + private $additionalDataProviderPool; + + /** + * @param PaymentRepository $paymentRepository + * @param AdditionalDataProviderPool $additionalDataProviderPool + */ + public function __construct( + PaymentRepository $paymentRepository, + AdditionalDataProviderPool $additionalDataProviderPool + ) { + $this->paymentRepository = $paymentRepository; + $this->additionalDataProviderPool = $additionalDataProviderPool; + } + + /** + * Set redirect URL paths on payment additionalInformation + * + * @param \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject + * @param mixed $result + * @param Quote $cart + * @param array $paymentData + * @return void + * @throws GraphQlInputException + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function afterExecute( + \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject, + $result, + Quote $cart, + array $paymentData + ): void { + $paymentData = $this->additionalDataProviderPool->getData(Config::METHOD_PAYFLOWPRO, $paymentData); + $cartCustomerId = (int)$cart->getCustomerId(); + if ($cartCustomerId === 0 && + array_key_exists(PayflowProSetCcData::IS_ACTIVE_PAYMENT_TOKEN_ENABLER, $paymentData)) { + $payment = $cart->getPayment(); + $payment->unsAdditionalInformation(PayflowProSetCcData::IS_ACTIVE_PAYMENT_TOKEN_ENABLER); + $payment->save(); + } + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowProCcVault/SetPaymentMethodOnCart.php b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowProCcVault/SetPaymentMethodOnCart.php new file mode 100644 index 0000000000000..46bad75f0ed19 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Model/Plugin/Cart/PayflowProCcVault/SetPaymentMethodOnCart.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowProCcVault; + +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\Payment\AdditionalDataProviderPool; +use Magento\Sales\Model\Order\Payment\Repository as PaymentRepository; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Api\PaymentTokenManagementInterface; + +/** + * Set additionalInformation on payment for PayflowPro vault method + */ +class SetPaymentMethodOnCart +{ + const CC_VAULT_CODE = 'payflowpro_cc_vault'; + + /** + * @var PaymentRepository + */ + private $paymentRepository; + + /** + * @var AdditionalDataProviderPool + */ + private $additionalDataProviderPool; + + /** + * PaymentTokenManagementInterface $paymentTokenManagement + */ + private $paymentTokenManagement; + + /** + * @param PaymentRepository $paymentRepository + * @param AdditionalDataProviderPool $additionalDataProviderPool + * @param PaymentTokenManagementInterface $paymentTokenManagement + */ + public function __construct( + PaymentRepository $paymentRepository, + AdditionalDataProviderPool $additionalDataProviderPool, + PaymentTokenManagementInterface $paymentTokenManagement + ) { + $this->paymentRepository = $paymentRepository; + $this->additionalDataProviderPool = $additionalDataProviderPool; + $this->paymentTokenManagement = $paymentTokenManagement; + } + + /** + * Set public hash and customer id on payment additionalInformation + * + * @param \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject + * @param mixed $result + * @param Quote $cart + * @param array $additionalData + * @return void + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws \Magento\Framework\Exception\LocalizedException + */ + public function afterExecute( + \Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart $subject, + $result, + Quote $cart, + array $additionalData + ): void { + $additionalData = $this->additionalDataProviderPool->getData(self::CC_VAULT_CODE, $additionalData); + $customerId = (int) $cart->getCustomer()->getId(); + $payment = $cart->getPayment(); + if (!is_array($additionalData) + || !isset($additionalData[PaymentTokenInterface::PUBLIC_HASH]) + || $customerId === 0 + ) { + return; + } + $tokenPublicHash = $additionalData[PaymentTokenInterface::PUBLIC_HASH]; + if ($tokenPublicHash === null) { + return; + } + $paymentToken = $this->paymentTokenManagement->getByPublicHash($tokenPublicHash, $customerId); + if ($paymentToken === null) { + return; + } + $payment->setAdditionalInformation( + [ + PaymentTokenInterface::CUSTOMER_ID => $customerId, + PaymentTokenInterface::PUBLIC_HASH => $tokenPublicHash + ] + ); + $payment->save(); + } +} diff --git a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php index ce44511c60f3e..b3ddced97aca6 100644 --- a/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php +++ b/app/code/Magento/PaypalGraphQl/Model/Resolver/PayflowProResponse.php @@ -126,9 +126,9 @@ public function resolve( $this->parameters->fromString(urldecode($paypalPayload)); $data = $this->parameters->toArray(); try { - $do = $this->dataObjectFactory->create(['data' => array_change_key_case($data, CASE_LOWER)]); - $this->responseValidator->validate($do, $this->transparent); - $this->transaction->savePaymentInQuote($do, $cart->getId()); + $response = $this->transaction->getResponseObject($data); + $this->responseValidator->validate($response, $this->transparent); + $this->transaction->savePaymentInQuote($response, $cart->getId()); } catch (LocalizedException $exception) { $parameters['error'] = true; $parameters['error_msg'] = $exception->getMessage(); diff --git a/app/code/Magento/PaypalGraphQl/Observer/PayflowProSetCcData.php b/app/code/Magento/PaypalGraphQl/Observer/PayflowProSetCcData.php new file mode 100644 index 0000000000000..55310b1744107 --- /dev/null +++ b/app/code/Magento/PaypalGraphQl/Observer/PayflowProSetCcData.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Observer; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Payment\Observer\AbstractDataAssignObserver; +use Magento\Quote\Api\Data\PaymentInterface; +use Magento\Quote\Model\Quote\Payment; + +/** + * Class PayflowProSetCcData set CcData to quote payment + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class PayflowProSetCcData extends AbstractDataAssignObserver +{ + const XML_PATH_PAYMENT_PAYFLOWPRO_CC_VAULT_ACTIVE = "payment/payflowpro_cc_vault/active"; + const IS_ACTIVE_PAYMENT_TOKEN_ENABLER = "is_active_payment_token_enabler"; + + /** + * Core store config + * + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Set CcData + * + * @param Observer $observer + * + * @throws GraphQlInputException + */ + public function execute(Observer $observer) + { + $dataObject = $this->readDataArgument($observer); + $additionalData = $dataObject->getData(PaymentInterface::KEY_ADDITIONAL_DATA); + /** + * @var Payment $paymentModel + */ + $paymentModel = $this->readPaymentModelArgument($observer); + $customerId = (int)$paymentModel->getQuote()->getCustomer()->getId(); + + if (!isset($additionalData['cc_details'])) { + return; + } + + if ($this->isPayflowProVaultEnable() && $customerId !== 0) { + if (isset($additionalData[self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER])) { + $paymentModel->setData( + self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER, + $additionalData[self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER] + ); + } + } else { + $paymentModel->setData(self::IS_ACTIVE_PAYMENT_TOKEN_ENABLER, false); + } + + $ccData = $additionalData['cc_details']; + $paymentModel->setCcType($ccData['cc_type']); + $paymentModel->setCcExpYear($ccData['cc_exp_year']); + $paymentModel->setCcExpMonth($ccData['cc_exp_month']); + $paymentModel->setCcLast4($ccData['cc_last_4']); + } + + /** + * Check if payflowpro vault is enable + * + * @return bool + */ + private function isPayflowProVaultEnable() + { + return (bool)$this->scopeConfig->getValue(self::XML_PATH_PAYMENT_PAYFLOWPRO_CC_VAULT_ACTIVE); + } +} diff --git a/app/code/Magento/PaypalGraphQl/composer.json b/app/code/Magento/PaypalGraphQl/composer.json index 8d012be3492dd..285217da64d72 100644 --- a/app/code/Magento/PaypalGraphQl/composer.json +++ b/app/code/Magento/PaypalGraphQl/composer.json @@ -13,10 +13,12 @@ "magento/module-quote-graph-ql": "*", "magento/module-sales": "*", "magento/module-payment": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-vault": "*" }, "suggest": { - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "*", + "magento/module-store-graph-ql": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml index cd5d6e2062bb9..f5f22050fe50a 100644 --- a/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/PaypalGraphQl/etc/graphql/di.xml @@ -11,6 +11,8 @@ </type> <type name="Magento\QuoteGraphQl\Model\Cart\SetPaymentMethodOnCart"> <plugin name="hosted_pro_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Cart\HostedPro\SetPaymentMethodOnCart"/> + <plugin name="payflowpro_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowPro\SetPaymentMethodOnCart"/> + <plugin name="payflowpro_cc_vault_payment_method" type="Magento\PaypalGraphQl\Model\Plugin\Cart\PayflowProCcVault\SetPaymentMethodOnCart"/> </type> <type name="Magento\Paypal\Model\Payflowlink"> <plugin name="payflow_link_update_redirect_urls" type="Magento\PaypalGraphQl\Model\Plugin\Payflowlink"/> @@ -50,6 +52,15 @@ <item name="payflow_advanced" xsi:type="object">Magento\PaypalGraphQl\Model\PayflowLinkAdditionalDataProvider</item> <item name="payflowpro" xsi:type="object">\Magento\PaypalGraphQl\Model\PayflowProAdditionalDataProvider</item> <item name="hosted_pro" xsi:type="object">\Magento\PaypalGraphQl\Model\HostedProAdditionalDataProvider</item> + <item name="payflowpro_cc_vault" xsi:type="object">\Magento\PaypalGraphQl\Model\PayflowProCcVaultAdditionalDataProvider</item> + </argument> + </arguments> + </type> + + <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> + <arguments> + <argument name="extendedConfigData" xsi:type="array"> + <item name="payment_payflowpro_cc_vault_active" xsi:type="string">payment/payflowpro_cc_vault/active</item> </argument> </arguments> </type> diff --git a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml index 41154e5ae06e6..0d2be95d77c92 100644 --- a/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml +++ b/app/code/Magento/PaypalGraphQl/etc/graphql/events.xml @@ -12,4 +12,7 @@ <event name="payment_method_assign_data_payflow_advanced"> <observer name="payflow_advanced_data_assigner" instance="Magento\PaypalGraphQl\Observer\PayflowLinkSetAdditionalData"/> </event> + <event name="payment_method_assign_data_payflowpro"> + <observer name="payflowpro_cc_data_assigner" instance="Magento\PaypalGraphQl\Observer\PayflowProSetCcData" /> + </event> </config> diff --git a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls index b8f14eec70d18..cdc8ee6fda2f3 100644 --- a/app/code/Magento/PaypalGraphQl/etc/schema.graphqls +++ b/app/code/Magento/PaypalGraphQl/etc/schema.graphqls @@ -37,7 +37,7 @@ type PayflowLinkToken @doc(description:"Contains information used to generate Pa paypal_url: String @doc(description:"PayPal URL used for requesting Payflow form") } -type HostedProUrl @doc(desription:"Contains secure URL used for Payments Pro Hosted Solution payment method.") { +type HostedProUrl @doc(description:"Contains secure URL used for Payments Pro Hosted Solution payment method.") { secure_form_url: String @doc(description:"Secure Url generated by PayPal") } @@ -51,6 +51,7 @@ input PaymentMethodInput { payflow_link: PayflowLinkInput @doc(description:"Required input for PayPal Payflow Link and Payments Advanced payments") payflowpro: PayflowProInput @doc(description: "Required input type for PayPal Payflow Pro and Payment Pro payments") hosted_pro: HostedProInput @doc(description:"Required input for PayPal Hosted pro payments") + payflowpro_cc_vault: VaultTokenInput @doc(description:"Required input type for PayPal Payflow Pro vault payments") } input HostedProInput @doc(description:"A set of relative URLs that PayPal will use in response to various actions during the authorization process. Magento prepends the base URL to this value to create a full URL. For example, if the full URL is https://www.example.com/path/to/page.html, the relative URL is path/to/page.html. Use this input for Payments Pro Hosted Solution payment method.") { @@ -102,6 +103,7 @@ input PayflowProTokenInput @doc(description:"Input required to fetch payment tok input PayflowProInput @doc(description:"Required input for Payflow Pro and Payments Pro payment methods.") { cc_details: CreditCardDetailsInput! @doc(description: "Required input for credit card related information") + is_active_payment_token_enabler: Boolean @doc(description:"States whether details about the customer's credit/debit card should be tokenized for later usage. Required only if Vault is enabled for PayPal Payflow Pro payment integration.") } input CreditCardDetailsInput @doc(description:"Required fields for Payflow Pro and Payments Pro credit card payments") { @@ -141,3 +143,11 @@ input PayflowProResponseInput @doc(description:"Input required to complete payme type PayflowProResponseOutput { cart: Cart! } + +type StoreConfig { + payment_payflowpro_cc_vault_active: String @doc(description: "Payflow Pro vault status.") +} + +input VaultTokenInput @doc(description:"Required input for payment methods with Vault support.") { + public_hash: String! @doc(description: "The public hash of the payment token") +} diff --git a/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php b/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php index 52a2912c4b170..f0b05cb7850cc 100644 --- a/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php +++ b/app/code/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserver.php @@ -11,6 +11,9 @@ /** * Persistent Session Observer + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class SynchronizePersistentOnLoginObserver implements ObserverInterface { @@ -63,6 +66,8 @@ public function __construct( } /** + * Synchronize persistent session data with logged in customer + * * @param Observer $observer * @return void * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -96,8 +101,9 @@ public function execute(Observer $observer) if (!$sessionModel->getId()) { /** @var \Magento\Persistent\Model\Session $sessionModel */ $sessionModel = $this->_sessionFactory->create(); - $sessionModel->setCustomerId($customer->getId())->save(); + $sessionModel->setCustomerId($customer->getId()); } + $sessionModel->save(); $this->_persistentSession->setSession($sessionModel); } diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml index 7c4e6948386f3..f094c4f07475d 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/ShippingQuotePersistedForGuestTest.xml @@ -66,7 +66,7 @@ <selectOption selector="{{CheckoutCartSummarySection.stateProvince}}" userInput="{{US_Address_CA.state}}" stepKey="selectCaliforniaRegion"/> <fillField selector="{{CheckoutCartSummarySection.postcode}}" userInput="{{US_Address_CA.postcode}}" stepKey="inputPostCode"/> <!--Step 6: Go to Homepage--> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToHomePageAfterChangingShippingAndTaxSection"/> <!--Step 7: Go to shopping cart and check "Estimate Shipping and Tax" fields values are saved--> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" after="goToHomePageAfterChangingShippingAndTaxSection" stepKey="goToShoppingCartAfterChangingShippingAndTaxSection"/> <conditionalClick selector="{{CheckoutCartSummarySection.estimateShippingAndTax}}" dependentSelector="{{CheckoutCartSummarySection.country}}" visible="false" stepKey="expandEstimateShippingAndTaxAfterChanging" /> diff --git a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml index dd24c6ae4279d..80ca7a2eb90c7 100644 --- a/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml +++ b/app/code/Magento/Persistent/Test/Mftf/Test/StorefrontVerifyShoppingCartPersistenceUnderLongTermCookieTest.xml @@ -49,7 +49,7 @@ </after> <!-- 1. Go to storefront and click the Create an Account link--> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnHomePage"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnHomePage"/> <click selector="{{StorefrontPanelHeaderSection.createAnAccountLink}}" stepKey="clickCreateAnAccountLink" /> <actionGroup ref="StorefrontAssertPersistentRegistrationPageFields" stepKey="assertPersistentRegistrationPageFields"/> diff --git a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php index 5c4a3eb624d3c..0c183084edca2 100644 --- a/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php +++ b/app/code/Magento/Persistent/Test/Unit/Model/QuoteManagerTest.php @@ -228,13 +228,9 @@ public function testSetGuest() ->method('removePersistentCookie')->willReturn($this->sessionMock); $this->quoteMock->expects($this->once())->method('isVirtual')->willReturn(false); $this->quoteMock->expects($this->once())->method('getItemsQty')->willReturn(1); - $extensionAttributes = $this->createPartialMock( - CartExtensionInterface::class, - [ - 'setShippingAssignments', - 'getShippingAssignments' - ] - ); + $extensionAttributes = $this->getMockBuilder(CartExtensionInterface::class) + ->addMethods(['getShippingAssignments', 'setShippingAssignments']) + ->getMockForAbstractClass(); $shippingAssignment = $this->createMock(ShippingAssignmentInterface::class); $extensionAttributes->expects($this->once()) ->method('setShippingAssignments') diff --git a/app/code/Magento/ProductAlert/Model/Email.php b/app/code/Magento/ProductAlert/Model/Email.php index 3351166aa6a12..379ae29ef4649 100644 --- a/app/code/Magento/ProductAlert/Model/Email.php +++ b/app/code/Magento/ProductAlert/Model/Email.php @@ -1,9 +1,10 @@ <?php - /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\ProductAlert\Model; use Magento\Catalog\Model\Product; @@ -40,7 +41,7 @@ * @api * @since 100.0.2 * @method int getStoreId() - * @method $this setStoreId() + * @method $this setStoreId(int $storeId) */ class Email extends AbstractModel { @@ -206,7 +207,7 @@ public function getType() * * @return $this */ - public function setWebsite(\Magento\Store\Model\Website $website) + public function setWebsite(Website $website) { $this->_website = $website; return $this; @@ -275,7 +276,7 @@ public function clean() * * @return $this */ - public function addPriceProduct(\Magento\Catalog\Model\Product $product) + public function addPriceProduct(Product $product) { $this->_priceProducts[$product->getId()] = $product; return $this; @@ -288,7 +289,7 @@ public function addPriceProduct(\Magento\Catalog\Model\Product $product) * * @return $this */ - public function addStockProduct(\Magento\Catalog\Model\Product $product) + public function addStockProduct(Product $product) { $this->_stockProducts[$product->getId()] = $product; return $this; @@ -342,7 +343,7 @@ public function send() return false; } - $storeId = $this->getStoreId() ?: (int) $this->_customer->getStoreId(); + $storeId = (int) $this->getStoreId() ?: (int) $this->_customer->getStoreId(); $store = $this->getStore($storeId); $this->_appEmulation->startEnvironmentEmulation($storeId); @@ -378,12 +379,13 @@ public function send() 'customerName' => $customerName, 'alertGrid' => $alertGrid, ] - )->setFrom( + )->setFromByScope( $this->_scopeConfig->getValue( self::XML_PATH_EMAIL_IDENTITY, ScopeInterface::SCOPE_STORE, $storeId - ) + ), + $storeId )->addTo( $this->_customer->getEmail(), $customerName diff --git a/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml index 3032f5208dd59..cc2c933812352 100644 --- a/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml +++ b/app/code/Magento/ProductVideo/Test/Mftf/Test/YoutubeVideoWindowOnProductPageTest.xml @@ -37,8 +37,7 @@ </before> <!--Open simple product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="wait1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetProductGrid"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProductGridBySku"> <argument name="product" value="$$createProduct$$"/> @@ -51,8 +50,7 @@ <actionGroup ref="AssertProductVideoAdminProductPageActionGroup" stepKey="assertProductVideoAdminProductPage" after="addProductVideo"/> <!-- Save the product --> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveFirstProduct"/> - <waitForPageLoad stepKey="waitForFirstProductSaved"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveFirstProduct"/> <!-- Assert product video in storefront product page --> <amOnPage url="$$createProduct.name$$.html" stepKey="goToStorefrontCategoryPage"/> diff --git a/app/code/Magento/ProductVideo/etc/csp_whitelist.xml b/app/code/Magento/ProductVideo/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..ca4536057104d --- /dev/null +++ b/app/code/Magento/ProductVideo/etc/csp_whitelist.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="youtube_cdn" type="host">s.ytimg.com</value> + <value id="google_video" type="host">www.googleapis.com</value> + <value id="vimeo" type="host">vimeo.com</value> + <value id="www_vimeo" type="host">www.vimeo.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="vimeo_cdn" type="host">*.vimeocdn.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml index 1548770d4032f..b729eadf122c5 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/helper/gallery.phtml @@ -4,12 +4,15 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Files.LineLength -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ +/** + * @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $elementNameEscaped = $block->escapeHtmlAttr($block->getElement()->getName()) . '[images]'; $formNameEscaped = $block->escapeHtmlAttr($block->getFormName()); + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div class="row"> @@ -28,16 +31,17 @@ $formNameEscaped = $block->escapeHtmlAttr($block->getFormName()); <?php /** @var $block \Magento\Catalog\Block\Adminhtml\Product\Helper\Form\Gallery\Content */ $element = $block->getElement(); -$elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; +$elementToggleCode = $element->getToggleCode() ? $element->getToggleCode(): + 'toggleValueElements(this, this.parentNode.parentNode.parentNode)'; ?> <div id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>" class="gallery" data-mage-init='{"openVideoModal":{}}' data-parent-component="<?= $block->escapeHtml($block->getData('config/parentComponent')) ?>" data-images="<?= $block->escapeHtmlAttr($block->getImagesJson()) ?>" - data-types='<?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getImageTypes()) ?>' + data-types='<?= /* @noEscape */ $jsonHelper->jsonEncode($block->getImageTypes()) ?>' > - <?php if (!$block->getElement()->getReadonly()) : ?> + <?php if (!$block->getElement()->getReadonly()): ?> <div class="image image-placeholder"> <?= $block->getUploaderHtml(); ?> <div class="product-image-wrapper"> @@ -48,15 +52,17 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> <?= $block->getChildHtml('additional_buttons') ?> <?php endif; ?> - <?php foreach ($block->getImageTypes() as $typeData) : ?> + <?php foreach ($block->getImageTypes() as $typeData): ?> <input name="<?= $block->escapeHtmlAttr($typeData['name']) ?>" data-form-part="<?= /* @noEscape */ $formNameEscaped ?>" class="image-<?= $block->escapeHtmlAttr($typeData['code']) ?>" type="hidden" value="<?= $block->escapeHtmlAttr($typeData['value']) ?>"/> <?php endforeach; ?> - <script id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-template" data-template="image" type="text/x-magento-template"> - <div class="image item <% if (data.disabled == 1) { %>hidden-for-front<% } %> <% if (data.video_url) { %>video-item<% } %>" + <script id="<?= $block->escapeHtmlAttr($block->getHtmlId()) ?>-template" data-template="image" + type="text/x-magento-template"> + <div class="image item <% if (data.disabled == 1) { %>hidden-for-front<% } %> + <% if (data.video_url) { %>video-item<% } %>" data-role="image"> <input type="hidden" name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][position]" @@ -164,8 +170,9 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> <ul class="item-roles" data-role="roles-labels"> - <?php foreach ($block->getImageTypes() as $typeData) : ?> - <li data-role-code="<?= $block->escapeHtmlAttr($typeData['code']) ?>" class="item-role item-role-<?= $block->escapeHtmlAttr($typeData['code']) ?>"> + <?php foreach ($block->getImageTypes() as $typeData): ?> + <li data-role-code="<?= $block->escapeHtmlAttr($typeData['code']) ?>" + class="item-role item-role-<?= $block->escapeHtmlAttr($typeData['code']) ?>"> <?= $block->escapeHtml($typeData['label']) ?> </li> <?php endforeach; ?> @@ -195,7 +202,8 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <textarea data-role="image-description" rows="3" class="admin__control-textarea" - name="<?= /* @noEscape */ $elementNameEscaped ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> + name="<?= /* @noEscape */ $elementNameEscaped + ?>[<%- data.file_id %>][label]"><%- data.label %></textarea> </div> </div> @@ -206,7 +214,7 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <div class="admin__field-control"> <ul class="multiselect-alt"> <?php - foreach ($block->getMediaAttributes() as $attribute) : + foreach ($block->getMediaAttributes() as $attribute): ?> <li class="item"> <label> @@ -235,7 +243,8 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to <label class="admin__field-label"> <span><?= $block->escapeHtml(__('Image Resolution')) ?></span> </label> - <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) ?>"></div> + <div class="admin__field-value" data-message="<?= $block->escapeHtmlAttr(__('{width}^{height} px')) + ?>"></div> </div> <div class="admin__field field-image-hide"> @@ -259,7 +268,7 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </fieldset> </div> </script> - <div id="<?= /* @noEscape */ $block->getNewVideoBlockName() ?>" style="display:none"> + <div id="new_video_<?= /* @noEscape */ $block->getNewVideoBlockName() ?>"> <?= $block->getFormHtml() ?> <div id="video-player-preview-location" class="video-player-sidebar"> <div class="video-player-container"></div> @@ -277,9 +286,11 @@ $elementToggleCode = $element->getToggleCode() ? $element->getToggleCode() : 'to </div> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#new_video_' . /* @noEscape */ $block->getNewVideoBlockName() + ) ?> <?= $block->getChildHtml('new-video') ?> </div> -<script> - jQuery('body').trigger('contentUpdated'); -</script> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "jQuery('body').trigger('contentUpdated');", false) ?> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml index e1dcab9e8b2d4..8c40c174c9787 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/base_image.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="row"> <div class="add-video-button-container"> @@ -11,10 +13,14 @@ title="<?= $block->escapeHtmlAttr($addVideoTitle) ?>" type="button" class="action-secondary" - onclick="jQuery('#new-video').modal('openModal'); jQuery('#new_video_form')[0].reset();" data-ui-id="widget-button-1"> <span><?= $block->escapeHtml(__('Add Video')) ?></span> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "jQuery('#new-video').modal('openModal'); jQuery('#new_video_form')[0].reset();", + 'button#add_video_button' + ) ?> </div> </div> <div id="<?= $block->escapeHtmlAttr($htmlId) ?>-container" @@ -62,7 +68,7 @@ <span class="action-manage-images" data-activate-tab="image-management"> <span><?= $block->escapeHtml($imageManagementText) ?></span> </span> -<script> +<?php $scriptString = <<<script require([ 'jquery' ],function($){ @@ -74,4 +80,6 @@ $('#product_info_tabs_image-management').trigger('click'); }); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml index 7de3042b56ab5..bf46bd1411e84 100644 --- a/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml +++ b/app/code/Magento/ProductVideo/view/adminhtml/templates/product/edit/slideout/form.phtml @@ -3,9 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/* @var Magento\ProductVideo\Block\Adminhtml\Product\Edit\NewVideo $block */ +/** + * @var Magento\ProductVideo\Block\Adminhtml\Product\Edit\NewVideo $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none" +<div id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" data-modal-info='<?= /* @noEscape */ $block->getWidgetOptions() ?>' > <?= $block->getFormHtml() ?> @@ -25,3 +28,7 @@ </div> </div> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#' . $block->escapeJs($block->getNameInLayout()) +) ?> diff --git a/app/code/Magento/Quote/Model/BillingAddressManagement.php b/app/code/Magento/Quote/Model/BillingAddressManagement.php index bc055e71c662e..6f8a44dff464c 100644 --- a/app/code/Magento/Quote/Model/BillingAddressManagement.php +++ b/app/code/Magento/Quote/Model/BillingAddressManagement.php @@ -103,7 +103,7 @@ public function get($cartId) * Get shipping address assignment * * @return \Magento\Quote\Model\ShippingAddressAssignment - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getShippingAddressAssignment() { diff --git a/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php b/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php new file mode 100644 index 0000000000000..2c5c3536d6682 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/AddProductsToCart.php @@ -0,0 +1,225 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder; +use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput; +use Magento\Quote\Model\MaskedQuoteIdToQuoteIdInterface; +use Magento\Quote\Model\Quote; +use Magento\Framework\Message\MessageInterface; + +/** + * Unified approach to add products to the Shopping Cart. + * Client code must validate, that customer is eligible to call service with provided {cartId} and {cartItems} + */ +class AddProductsToCart +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_INSUFFICIENT_STOCK = 'INSUFFICIENT_STOCK'; + private const ERROR_NOT_SALABLE = 'NOT_SALABLE'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * List of error messages and codes. + */ + private const MESSAGE_CODES = [ + 'Could not find a product with SKU' => self::ERROR_PRODUCT_NOT_FOUND, + 'The required options you selected are not available' => self::ERROR_NOT_SALABLE, + 'Product that you are trying to add is not available.' => self::ERROR_NOT_SALABLE, + 'This product is out of stock' => self::ERROR_INSUFFICIENT_STOCK, + 'There are no source items' => self::ERROR_NOT_SALABLE, + 'The fewest you may purchase is' => self::ERROR_INSUFFICIENT_STOCK, + 'The most you may purchase is' => self::ERROR_INSUFFICIENT_STOCK, + 'The requested qty is not available' => self::ERROR_INSUFFICIENT_STOCK, + ]; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var array + */ + private $errors = []; + + /** + * @var CartRepositoryInterface + */ + private $cartRepository; + + /** + * @var MaskedQuoteIdToQuoteIdInterface + */ + private $maskedQuoteIdToQuoteId; + + /** + * @var BuyRequestBuilder + */ + private $requestBuilder; + + /** + * @param ProductRepositoryInterface $productRepository + * @param CartRepositoryInterface $cartRepository + * @param MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId + * @param BuyRequestBuilder $requestBuilder + */ + public function __construct( + ProductRepositoryInterface $productRepository, + CartRepositoryInterface $cartRepository, + MaskedQuoteIdToQuoteIdInterface $maskedQuoteIdToQuoteId, + BuyRequestBuilder $requestBuilder + ) { + $this->productRepository = $productRepository; + $this->cartRepository = $cartRepository; + $this->maskedQuoteIdToQuoteId = $maskedQuoteIdToQuoteId; + $this->requestBuilder = $requestBuilder; + } + + /** + * Add cart items to the cart + * + * @param string $maskedCartId + * @param Data\CartItem[] $cartItems + * @return AddProductsToCartOutput + * @throws NoSuchEntityException Could not find a Cart with provided $maskedCartId + */ + public function execute(string $maskedCartId, array $cartItems): AddProductsToCartOutput + { + $cartId = $this->maskedQuoteIdToQuoteId->execute($maskedCartId); + $cart = $this->cartRepository->get($cartId); + + foreach ($cartItems as $cartItemPosition => $cartItem) { + $this->addItemToCart($cart, $cartItem, $cartItemPosition); + } + + if ($cart->getData('has_error')) { + $errors = $cart->getErrors(); + + /** @var MessageInterface $error */ + foreach ($errors as $error) { + $this->addError($error->getText()); + } + } + + if (count($this->errors) !== 0) { + /* Revert changes introduced by add to cart processes in case of an error */ + $cart->getItemsCollection()->clear(); + } + + return $this->prepareErrorOutput($cart); + } + + /** + * Adds a particular item to the shopping cart + * + * @param CartInterface|Quote $cart + * @param Data\CartItem $cartItem + * @param int $cartItemPosition + */ + private function addItemToCart(CartInterface $cart, Data\CartItem $cartItem, int $cartItemPosition): void + { + $sku = $cartItem->getSku(); + + if ($cartItem->getQuantity() <= 0) { + $this->addError(__('The product quantity should be greater than 0')->render()); + + return; + } + + try { + $product = $this->productRepository->get($sku, false, null, true); + } catch (NoSuchEntityException $e) { + $this->addError( + __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(), + $cartItemPosition + ); + + return; + } + + try { + $result = $cart->addProduct($product, $this->requestBuilder->build($cartItem)); + $this->cartRepository->save($cart); + } catch (\Throwable $e) { + $this->addError( + __($e->getMessage())->render(), + $cartItemPosition + ); + $cart->setHasError(false); + + return; + } + + if (is_string($result)) { + $errors = array_unique(explode("\n", $result)); + foreach ($errors as $error) { + $this->addError(__($error)->render(), $cartItemPosition); + } + } + } + + /** + * Add order line item error + * + * @param string $message + * @param int $cartItemPosition + * @return void + */ + private function addError(string $message, int $cartItemPosition = 0): void + { + $this->errors[] = new Data\Error( + $message, + $this->getErrorCode($message), + $cartItemPosition + ); + } + + /** + * Get message error code. + * + * TODO: introduce a separate class for getting error code from a message + * + * @param string $message + * @return string + */ + private function getErrorCode(string $message): string + { + foreach (self::MESSAGE_CODES as $codeMessage => $code) { + if (false !== stripos($message, $codeMessage)) { + return $code; + } + } + + /* If no code was matched, return the default one */ + return self::ERROR_UNDEFINED; + } + + /** + * Creates a new output from existing errors + * + * @param CartInterface $cart + * @return AddProductsToCartOutput + */ + private function prepareErrorOutput(CartInterface $cart): AddProductsToCartOutput + { + $output = new AddProductsToCartOutput($cart, $this->errors); + $this->errors = []; + $cart->setHasError(false); + + return $output; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php new file mode 100644 index 0000000000000..13b19e4f79c9a --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestBuilder.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\BuyRequest; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Build buy request for adding products to cart + */ +class BuyRequestBuilder +{ + /** + * @var BuyRequestDataProviderInterface[] + */ + private $providers; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @param DataObjectFactory $dataObjectFactory + * @param array $providers + */ + public function __construct( + DataObjectFactory $dataObjectFactory, + array $providers = [] + ) { + $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; + } + + /** + * Build buy request for adding product to cart + * + * @see \Magento\Quote\Model\Quote::addProduct + * @param CartItem $cartItem + * @return DataObject + */ + public function build(CartItem $cartItem): DataObject + { + $requestData = [ + ['qty' => $cartItem->getQuantity()] + ]; + + /** @var BuyRequestDataProviderInterface $provider */ + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($cartItem); + } + + return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + } +} diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..b9c41b18ee163 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/BuyRequestDataProviderInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\BuyRequest; + +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Provides data for buy request for different types of products + */ +interface BuyRequestDataProviderInterface +{ + /** + * Provide buy request data from add to cart item request + * + * @param CartItem $cartItem + * @return array + */ + public function execute(CartItem $cartItem): array; +} diff --git a/app/code/Magento/Quote/Model/Cart/BuyRequest/CustomizableOptionDataProvider.php b/app/code/Magento/Quote/Model/Cart/BuyRequest/CustomizableOptionDataProvider.php new file mode 100644 index 0000000000000..90f2cbbc5f9e3 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/BuyRequest/CustomizableOptionDataProvider.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Extract buy request elements require for custom options + */ +class CustomizableOptionDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'custom-option'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $customizableOptionsData = []; + + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $optionId, $optionValue] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $optionValue; + } + } + + foreach ($cartItem->getEnteredOptions() as $option) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($option->getUid())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [$optionType, $optionId] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $option->getValue(); + } + } + + return ['options' => $this->flattenOptionValues($customizableOptionsData)]; + } + + /** + * Flatten option values for non-multiselect customizable options + * + * @param array $customizableOptionsData + * @return array + */ + private function flattenOptionValues(array $customizableOptionsData): array + { + foreach ($customizableOptionsData as $optionId => $optionValue) { + if (count($optionValue) === 1) { + $customizableOptionsData[$optionId] = $optionValue[0]; + } + } + + return $customizableOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 3) { + throw new LocalizedException( + __('Wrong format of the entered option data') + ); + } + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/AddProductsToCartOutput.php b/app/code/Magento/Quote/Model/Cart/Data/AddProductsToCartOutput.php new file mode 100644 index 0000000000000..c12c02c0449f6 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/AddProductsToCartOutput.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +use Magento\Quote\Api\Data\CartInterface; + +/** + * DTO represents output for \Magento\Quote\Model\Cart\AddProductsToCart + */ +class AddProductsToCartOutput +{ + /** + * @var CartInterface + */ + private $cart; + + /** + * @var Error[] + */ + private $errors; + + /** + * @param CartInterface $cart + * @param Error[] $errors + */ + public function __construct(CartInterface $cart, array $errors) + { + $this->cart = $cart; + $this->errors = $errors; + } + + /** + * Get Shopping Cart + * + * @return CartInterface + */ + public function getCart(): CartInterface + { + return $this->cart; + } + + /** + * Get errors happened during adding item to the cart + * + * @return Error[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/CartItem.php b/app/code/Magento/Quote/Model/Cart/Data/CartItem.php new file mode 100644 index 0000000000000..9836247c56694 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/CartItem.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO represents Cart Item data + */ +class CartItem +{ + /** + * @var string + */ + private $sku; + + /** + * @var float + */ + private $quantity; + + /** + * @var string + */ + private $parentSku; + + /** + * @var SelectedOption[] + */ + private $selectedOptions; + + /** + * @var EnteredOption[] + */ + private $enteredOptions; + + /** + * @param string $sku + * @param float $quantity + * @param string|null $parentSku + * @param array|null $selectedOptions + * @param array|null $enteredOptions + */ + public function __construct( + string $sku, + float $quantity, + string $parentSku = null, + array $selectedOptions = null, + array $enteredOptions = null + ) { + $this->sku = $sku; + $this->quantity = $quantity; + $this->parentSku = $parentSku; + $this->selectedOptions = $selectedOptions; + $this->enteredOptions = $enteredOptions; + } + + /** + * Returns cart item SKU + * + * @return string + */ + public function getSku(): string + { + return $this->sku; + } + + /** + * Returns cart item quantity + * + * @return float + */ + public function getQuantity(): float + { + return $this->quantity; + } + + /** + * Returns parent SKU + * + * @return string|null + */ + public function getParentSku(): ?string + { + return $this->parentSku; + } + + /** + * Returns selected options + * + * @return SelectedOption[]|null + */ + public function getSelectedOptions(): ?array + { + return $this->selectedOptions; + } + + /** + * Returns entered options + * + * @return EnteredOption[]|null + */ + public function getEnteredOptions(): ?array + { + return $this->enteredOptions; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/CartItemFactory.php b/app/code/Magento/Quote/Model/Cart/Data/CartItemFactory.php new file mode 100644 index 0000000000000..823f03b28229c --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/CartItemFactory.php @@ -0,0 +1,74 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +use Magento\Framework\Exception\InputException; + +/** + * Creates CartItem DTO + */ +class CartItemFactory +{ + /** + * Creates CartItem DTO + * + * @param array $data + * @return CartItem + * @throws InputException + */ + public function create(array $data): CartItem + { + if (!isset($data['sku'], $data['quantity'])) { + throw new InputException(__('Required fields are not present: sku, quantity')); + } + return new CartItem( + $data['sku'], + $data['quantity'], + $data['parent_sku'] ?? null, + isset($data['selected_options']) ? $this->createSelectedOptions($data['selected_options']) : [], + isset($data['entered_options']) ? $this->createEnteredOptions($data['entered_options']) : [] + ); + } + + /** + * Creates array of Entered Options + * + * @param array $options + * @return EnteredOption[] + */ + private function createEnteredOptions(array $options): array + { + return \array_map( + function (array $option) { + if (!isset($option['uid'], $option['value'])) { + throw new InputException( + __('Required fields are not present EnteredOption.uid, EnteredOption.value') + ); + } + return new EnteredOption($option['uid'], $option['value']); + }, + $options + ); + } + + /** + * Creates array of Selected Options + * + * @param string[] $options + * @return SelectedOption[] + */ + private function createSelectedOptions(array $options): array + { + return \array_map( + function ($option) { + return new SelectedOption($option); + }, + $options + ); + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/EnteredOption.php b/app/code/Magento/Quote/Model/Cart/Data/EnteredOption.php new file mode 100644 index 0000000000000..ba55051d33805 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/EnteredOption.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO for quote item entered option + */ +class EnteredOption +{ + /** + * @var string + */ + private $uid; + + /** + * @var string + */ + private $value; + + /** + * @param string $uid + * @param string $value + */ + public function __construct(string $uid, string $value) + { + $this->uid = $uid; + $this->value = $value; + } + + /** + * Returns entered option ID + * + * @return string + */ + public function getUid(): string + { + return $this->uid; + } + + /** + * Returns entered option value + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/Error.php b/app/code/Magento/Quote/Model/Cart/Data/Error.php new file mode 100644 index 0000000000000..42b14b06d94aa --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/Error.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO represents error item + */ +class Error +{ + /** + * @var string + */ + private $message; + + /** + * @var string + */ + private $code; + + /** + * @var int + */ + private $cartItemPosition; + + /** + * @param string $message + * @param string $code + * @param int $cartItemPosition + */ + public function __construct(string $message, string $code, int $cartItemPosition) + { + $this->message = $message; + $this->code = $code; + $this->cartItemPosition = $cartItemPosition; + } + + /** + * Get error message + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Get error code + * + * @return string + */ + public function getCode(): string + { + return $this->code; + } + + /** + * Get cart item position + * + * @return int + */ + public function getCartItemPosition(): int + { + return $this->cartItemPosition; + } +} diff --git a/app/code/Magento/Quote/Model/Cart/Data/SelectedOption.php b/app/code/Magento/Quote/Model/Cart/Data/SelectedOption.php new file mode 100644 index 0000000000000..70edd93cd8ef8 --- /dev/null +++ b/app/code/Magento/Quote/Model/Cart/Data/SelectedOption.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Cart\Data; + +/** + * DTO for quote item selected option + */ +class SelectedOption +{ + /** + * @var string + */ + private $id; + + /** + * @param string $id + */ + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * Get selected option ID + * + * @return string + */ + public function getId(): string + { + return $this->id; + } +} diff --git a/app/code/Magento/Quote/Model/CustomerManagement.php b/app/code/Magento/Quote/Model/CustomerManagement.php index 86725bd6211c7..3607cf7f9be63 100644 --- a/app/code/Magento/Quote/Model/CustomerManagement.php +++ b/app/code/Magento/Quote/Model/CustomerManagement.php @@ -3,14 +3,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\Quote\Model; -use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; use Magento\Customer\Api\AccountManagementInterface as AccountManagement; use Magento\Customer\Api\AddressRepositoryInterface as CustomerAddressRepository; -use Magento\Quote\Model\Quote as QuoteEntity; +use Magento\Customer\Api\CustomerRepositoryInterface as CustomerRepository; +use Magento\Customer\Model\AddressFactory; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Validator\Exception as ValidatorException; +use Magento\Framework\Validator\Factory as ValidatorFactory; +use Magento\Quote\Model\Quote as QuoteEntity; /** * Class Customer @@ -33,12 +37,12 @@ class CustomerManagement protected $accountManagement; /** - * @var \Magento\Framework\Validator\Factory + * @var ValidatorFactory */ private $validatorFactory; /** - * @var \Magento\Customer\Model\AddressFactory + * @var AddressFactory */ private $addressFactory; @@ -47,23 +51,23 @@ class CustomerManagement * @param CustomerRepository $customerRepository * @param CustomerAddressRepository $customerAddressRepository * @param AccountManagement $accountManagement - * @param \Magento\Framework\Validator\Factory|null $validatorFactory - * @param \Magento\Customer\Model\AddressFactory|null $addressFactory + * @param ValidatorFactory|null $validatorFactory + * @param AddressFactory|null $addressFactory */ public function __construct( CustomerRepository $customerRepository, CustomerAddressRepository $customerAddressRepository, AccountManagement $accountManagement, - \Magento\Framework\Validator\Factory $validatorFactory = null, - \Magento\Customer\Model\AddressFactory $addressFactory = null + ValidatorFactory $validatorFactory = null, + AddressFactory $addressFactory = null ) { $this->customerRepository = $customerRepository; $this->customerAddressRepository = $customerAddressRepository; $this->accountManagement = $accountManagement; $this->validatorFactory = $validatorFactory ?: ObjectManager::getInstance() - ->get(\Magento\Framework\Validator\Factory::class); + ->get(ValidatorFactory::class); $this->addressFactory = $addressFactory ?: ObjectManager::getInstance() - ->get(\Magento\Customer\Model\AddressFactory::class); + ->get(AddressFactory::class); } /** @@ -82,6 +86,7 @@ public function populateCustomerInfo(QuoteEntity $quote) $quote->getPasswordHash() ); $quote->setCustomer($customer); + $this->fillCustomerAddressId($quote); } if (!$quote->getBillingAddress()->getId() && $customer->getDefaultBilling()) { $quote->getBillingAddress()->importCustomerAddressData( @@ -100,11 +105,36 @@ public function populateCustomerInfo(QuoteEntity $quote) } } + /** + * Filling 'CustomerAddressId' in quote for a newly created customer. + * + * @param QuoteEntity $quote + * @return void + */ + private function fillCustomerAddressId(QuoteEntity $quote): void + { + $customer = $quote->getCustomer(); + + $customer->getDefaultBilling() ? + $quote->getBillingAddress()->setCustomerAddressId($customer->getDefaultBilling()) : + $quote->getBillingAddress()->setCustomerAddressId(0); + + if ($customer->getDefaultShipping() || $customer->getDefaultBilling()) { + if ($quote->getShippingAddress()->getSameAsBilling()) { + $quote->getShippingAddress()->setCustomerAddressId($customer->getDefaultBilling()); + } else { + $quote->getShippingAddress()->setCustomerAddressId($customer->getDefaultShipping()); + } + } else { + $quote->getShippingAddress()->setCustomerAddressId(0); + } + } + /** * Validate Quote Addresses * * @param Quote $quote - * @throws \Magento\Framework\Validator\Exception + * @throws ValidatorException * @return void */ public function validateAddresses(QuoteEntity $quote) @@ -126,7 +156,7 @@ public function validateAddresses(QuoteEntity $quote) $addressModel = $this->addressFactory->create(); $addressModel->updateData($address); if (!$validator->isValid($addressModel)) { - throw new \Magento\Framework\Validator\Exception( + throw new ValidatorException( null, null, $validator->getMessages() diff --git a/app/code/Magento/Quote/Model/GuestCart/GuestCartResolver.php b/app/code/Magento/Quote/Model/GuestCart/GuestCartResolver.php new file mode 100644 index 0000000000000..45d2e60d103c1 --- /dev/null +++ b/app/code/Magento/Quote/Model/GuestCart/GuestCartResolver.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\GuestCart; + +use Magento\Quote\Api\GuestCartManagementInterface; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\Quote\Model\Quote; + +/** + * Return empty cart for guest + */ +class GuestCartResolver +{ + /** + * @var GuestCartManagementInterface + */ + private $guestCartManagement; + + /** + * @var QuoteIdMaskFactory + */ + private $quoteIdMaskFactory; + + /** + * @var QuoteIdMaskResourceModel + */ + private $quoteIdMaskResourceModel; + + /** + * @var \Magento\Quote\Api\GuestCartRepositoryInterface + */ + private $guestCartRepository; + + /** + * @param GuestCartManagementInterface $guestCartManagement + * @param QuoteIdMaskFactory $quoteIdMaskFactory + * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel + * @param \Magento\Quote\Api\GuestCartRepositoryInterface $guestCartRepository + */ + public function __construct( + GuestCartManagementInterface $guestCartManagement, + QuoteIdMaskFactory $quoteIdMaskFactory, + QuoteIdMaskResourceModel $quoteIdMaskResourceModel, + \Magento\Quote\Api\GuestCartRepositoryInterface $guestCartRepository + ) { + $this->guestCartManagement = $guestCartManagement; + $this->quoteIdMaskFactory = $quoteIdMaskFactory; + $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; + $this->guestCartRepository = $guestCartRepository; + } + + /** + * Create empty cart for guest + * + * @param string|null $predefinedMaskedQuoteId + * @return Quote + * @throws \Magento\Framework\Exception\AlreadyExistsException + * @throws \Magento\Framework\Exception\CouldNotSaveException + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + public function resolve(string $predefinedMaskedQuoteId = null): Quote + { + $maskedQuoteId = $this->guestCartManagement->createEmptyCart(); + + if ($predefinedMaskedQuoteId !== null) { + $quoteIdMask = $this->quoteIdMaskFactory->create(); + $this->quoteIdMaskResourceModel->load($quoteIdMask, $maskedQuoteId, 'masked_id'); + + $quoteIdMask->setMaskedId($predefinedMaskedQuoteId); + $this->quoteIdMaskResourceModel->save($quoteIdMask); + $maskedQuoteId = $predefinedMaskedQuoteId; + } + + return $this->guestCartRepository->get($maskedQuoteId); + } +} diff --git a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php index 152d575e059c8..5cdcca5349c1b 100644 --- a/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php +++ b/app/code/Magento/Quote/Model/MaskedQuoteIdToQuoteIdInterface.php @@ -12,6 +12,7 @@ /** * Converts masked quote id to the quote id (entity id) * @api + * @since 101.1.0 */ interface MaskedQuoteIdToQuoteIdInterface { @@ -19,6 +20,7 @@ interface MaskedQuoteIdToQuoteIdInterface * @param string $maskedQuoteId * @return int * @throws NoSuchEntityException + * @since 101.1.0 */ public function execute(string $maskedQuoteId): int; } diff --git a/app/code/Magento/Quote/Model/Quote.php b/app/code/Magento/Quote/Model/Quote.php index c7d40c82bbcc2..d2e900138cd06 100644 --- a/app/code/Magento/Quote/Model/Quote.php +++ b/app/code/Magento/Quote/Model/Quote.php @@ -10,6 +10,7 @@ use Magento\Directory\Model\AllowedCountries; use Magento\Framework\Api\AttributeValueFactory; use Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Model\AbstractExtensibleModel; use Magento\Quote\Api\Data\PaymentInterface; use Magento\Quote\Model\Quote\Address; @@ -873,7 +874,7 @@ public function beforeSave() * Loading quote data by customer * * @param \Magento\Customer\Model\Customer|int $customer - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return $this */ public function loadByCustomer($customer) @@ -1104,7 +1105,22 @@ public function getCustomerTaxClassId() //if (!$this->getData('customer_group_id') && !$this->getData('customer_tax_class_id')) { $groupId = $this->getCustomerGroupId(); if ($groupId !== null) { - $taxClassId = $this->groupRepository->getById($this->getCustomerGroupId())->getTaxClassId(); + $taxClassId = null; + try { + $taxClassId = $this->groupRepository->getById($this->getCustomerGroupId())->getTaxClassId(); + } catch (NoSuchEntityException $e) { + /** + * A customer MAY create a quote and AFTER that customer group MAY be deleted. + * That breaks a quote because it still refers no a non-existent customer group. + * In such a case we should load a new customer group id from the current customer + * object and use it to retrieve tax class and update quote. + */ + $groupId = $this->getCustomer()->getGroupId(); + $this->setCustomerGroupId($groupId); + if ($groupId !== null) { + $taxClassId = $this->groupRepository->getById($groupId)->getTaxClassId(); + } + } $this->setCustomerTaxClassId($taxClassId); } diff --git a/app/code/Magento/Quote/Model/Quote/Address.php b/app/code/Magento/Quote/Model/Quote/Address.php index 39148f990b714..5476915d9d649 100644 --- a/app/code/Magento/Quote/Model/Quote/Address.php +++ b/app/code/Magento/Quote/Model/Quote/Address.php @@ -238,7 +238,7 @@ class Address extends AbstractAddress implements /** * @var RateFactory - * @since 100.2.0 + * @since 101.0.0 */ protected $_addressRateFactory; @@ -1019,6 +1019,13 @@ public function collectShippingRates() */ public function requestShippingRates(AbstractItem $item = null) { + $storeId = $this->getQuote()->getStoreId() ?: $this->storeManager->getStore()->getId(); + $taxInclude = $this->_scopeConfig->getValue( + 'tax/calculation/price_includes_tax', + ScopeInterface::SCOPE_STORE, + $storeId + ); + /** @var $request RateRequest */ $request = $this->_rateRequestFactory->create(); $request->setAllItems($item ? [$item] : $this->getAllItems()); @@ -1028,9 +1035,11 @@ public function requestShippingRates(AbstractItem $item = null) $request->setDestStreet($this->getStreetFull()); $request->setDestCity($this->getCity()); $request->setDestPostcode($this->getPostcode()); - $request->setPackageValue($item ? $item->getBaseRowTotal() : $this->getBaseSubtotal()); + $baseSubtotal = $taxInclude ? $this->getBaseSubtotalTotalInclTax() : $this->getBaseSubtotal(); + $request->setPackageValue($item ? $item->getBaseRowTotal() : $baseSubtotal); + $baseSubtotalWithDiscount = $baseSubtotal + $this->getBaseDiscountAmount(); $packageWithDiscount = $item ? $item->getBaseRowTotal() - - $item->getBaseDiscountAmount() : $this->getBaseSubtotalWithDiscount(); + $item->getBaseDiscountAmount() : $baseSubtotalWithDiscount; $request->setPackageValueWithDiscount($packageWithDiscount); $request->setPackageWeight($item ? $item->getRowWeight() : $this->getWeight()); $request->setPackageQty($item ? $item->getQty() : $this->getItemQty()); @@ -1038,8 +1047,7 @@ public function requestShippingRates(AbstractItem $item = null) /** * Need for shipping methods that use insurance based on price of physical products */ - $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $this->getBaseSubtotal() - - $this->getBaseVirtualAmount(); + $packagePhysicalValue = $item ? $item->getBaseRowTotal() : $baseSubtotal - $this->getBaseVirtualAmount(); $request->setPackagePhysicalValue($packagePhysicalValue); $request->setFreeMethodWeight($item ? 0 : $this->getFreeMethodWeight()); @@ -1047,12 +1055,10 @@ public function requestShippingRates(AbstractItem $item = null) /** * Store and website identifiers specified from StoreManager */ + $request->setStoreId($storeId); if ($this->getQuote()->getStoreId()) { - $storeId = $this->getQuote()->getStoreId(); - $request->setStoreId($storeId); $request->setWebsiteId($this->storeManager->getStore($storeId)->getWebsiteId()); } else { - $request->setStoreId($this->storeManager->getStore()->getId()); $request->setWebsiteId($this->storeManager->getWebsite()->getId()); } $request->setFreeShipping($this->getFreeShipping()); diff --git a/app/code/Magento/Quote/Model/Quote/Address/Item.php b/app/code/Magento/Quote/Model/Quote/Address/Item.php index ade4f9270b68f..bbf74d5a28935 100644 --- a/app/code/Magento/Quote/Model/Quote/Address/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Address/Item.php @@ -199,6 +199,7 @@ public function importQuoteItem(\Magento\Quote\Model\Quote\Item $quoteItem) /** * @inheritdoc + * @since 101.1.1 */ public function getOptionByCode($code) { diff --git a/app/code/Magento/Quote/Model/Quote/Item.php b/app/code/Magento/Quote/Model/Quote/Item.php index 2e4a9c7ded683..22554380ca61e 100644 --- a/app/code/Magento/Quote/Model/Quote/Item.php +++ b/app/code/Magento/Quote/Model/Quote/Item.php @@ -173,7 +173,7 @@ class Item extends \Magento\Quote\Model\Quote\Item\AbstractItem implements \Mage /** * @var \Magento\CatalogInventory\Api\StockRegistryInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $stockRegistry; diff --git a/app/code/Magento/Quote/Model/Quote/Item/Processor.php b/app/code/Magento/Quote/Model/Quote/Item/Processor.php index ef4b853862681..c6bef1cc80bfb 100644 --- a/app/code/Magento/Quote/Model/Quote/Item/Processor.php +++ b/app/code/Magento/Quote/Model/Quote/Item/Processor.php @@ -97,7 +97,9 @@ public function prepare(Item $item, DataObject $request, Product $candidate): vo $item->addQty($candidate->getCartQty()); $customPrice = $request->getCustomPrice(); - $item->setPrice($candidate->getFinalPrice()); + if (!$item->getParentItem() || $item->getParentItem()->isChildrenCalculated()) { + $item->setPrice($candidate->getFinalPrice()); + } if (!empty($customPrice)) { $item->setCustomPrice($customPrice); $item->setOriginalCustomPrice($customPrice); diff --git a/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php b/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php index 38bfcbf1d30ca..78aa31d7d9527 100644 --- a/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php +++ b/app/code/Magento/Quote/Model/Quote/Validator/MinimumOrderAmount/ValidationMessage.php @@ -19,7 +19,7 @@ class ValidationMessage /** * @var \Magento\Framework\Locale\CurrencyInterface - * @deprecated since 101.0.0 + * @deprecated 101.0.3 since 101.0.0 */ private $currency; diff --git a/app/code/Magento/Quote/Model/QuoteAddressValidator.php b/app/code/Magento/Quote/Model/QuoteAddressValidator.php index e7750f5879de5..f0bc12f7b3a36 100644 --- a/app/code/Magento/Quote/Model/QuoteAddressValidator.php +++ b/app/code/Magento/Quote/Model/QuoteAddressValidator.php @@ -31,7 +31,7 @@ class QuoteAddressValidator protected $customerRepository; /** - * @deprecated This class is not a part of HTML presentation layer and should not use sessions. + * @deprecated 101.1.1 This class is not a part of HTML presentation layer and should not use sessions. */ protected $customerSession; diff --git a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php index 4d2a8ce877d8c..2a73a648889fb 100644 --- a/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php +++ b/app/code/Magento/Quote/Model/QuoteIdToMaskedQuoteIdInterface.php @@ -12,6 +12,7 @@ /** * Converts quote id to the masked quote id * @api + * @since 101.1.0 */ interface QuoteIdToMaskedQuoteIdInterface { @@ -19,6 +20,7 @@ interface QuoteIdToMaskedQuoteIdInterface * @param int $quoteId * @return string * @throws NoSuchEntityException + * @since 101.1.0 */ public function execute(int $quoteId): string; } diff --git a/app/code/Magento/Quote/Model/QuoteRepository.php b/app/code/Magento/Quote/Model/QuoteRepository.php index ccfd3df5fafa3..0dd2b00a596ea 100644 --- a/app/code/Magento/Quote/Model/QuoteRepository.php +++ b/app/code/Magento/Quote/Model/QuoteRepository.php @@ -43,7 +43,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteFactory - * @deprecated + * @deprecated 101.1.2 */ protected $quoteFactory; @@ -54,7 +54,7 @@ class QuoteRepository implements CartRepositoryInterface /** * @var QuoteCollection - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $quoteCollection; @@ -261,7 +261,7 @@ public function getList(SearchCriteriaInterface $searchCriteria) * @param FilterGroup $filterGroup The filter group. * @param QuoteCollection $collection The quote collection. * @return void - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @throws InputException The specified filter group or quote collection does not exist. */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, QuoteCollection $collection) diff --git a/app/code/Magento/Quote/Model/ResourceModel/Quote.php b/app/code/Magento/Quote/Model/ResourceModel/Quote.php index 48945dacd1738..e6350dd5aeb2b 100644 --- a/app/code/Magento/Quote/Model/ResourceModel/Quote.php +++ b/app/code/Magento/Quote/Model/ResourceModel/Quote.php @@ -230,7 +230,8 @@ public function subtractProductFromQuotes($product) 'items_qty' => new \Zend_Db_Expr( $connection->quoteIdentifier('q.items_qty') . ' - ' . $connection->quoteIdentifier('qi.qty') ), - 'items_count' => new \Zend_Db_Expr($ifSql) + 'items_count' => new \Zend_Db_Expr($ifSql), + 'updated_at' => 'q.updated_at', ] )->join( ['qi' => $this->getTable('quote_item')], @@ -257,7 +258,7 @@ public function subtractProductFromQuotes($product) * * @param \Magento\Catalog\Model\Product $product * - * @deprecated 101.0.1 + * @deprecated 101.0.3 * @see \Magento\Quote\Model\ResourceModel\Quote::subtractProductFromQuotes * * @return $this @@ -277,21 +278,27 @@ public function markQuotesRecollect($productIds) { $tableQuote = $this->getTable('quote'); $tableItem = $this->getTable('quote_item'); - $subSelect = $this->getConnection()->select()->from( - $tableItem, - ['entity_id' => 'quote_id'] - )->where( - 'product_id IN ( ? )', - $productIds - )->group( - 'quote_id' - ); - - $select = $this->getConnection()->select()->join( - ['t2' => $subSelect], - 't1.entity_id = t2.entity_id', - ['trigger_recollect' => new \Zend_Db_Expr('1')] - ); + $subSelect = $this->getConnection() + ->select() + ->from( + $tableItem, + ['entity_id' => 'quote_id'] + )->where( + 'product_id IN ( ? )', + $productIds + )->group( + 'quote_id' + ); + $select = $this->getConnection() + ->select() + ->join( + ['t2' => $subSelect], + 't1.entity_id = t2.entity_id', + [ + 'trigger_recollect' => new \Zend_Db_Expr('1'), + 'updated_at' => 't1.updated_at', + ] + ); $updateQuery = $select->crossUpdateFromSelect(['t1' => $tableQuote]); $this->getConnection()->query($updateQuery); diff --git a/app/code/Magento/Quote/Model/ShippingMethodManagement.php b/app/code/Magento/Quote/Model/ShippingMethodManagement.php index d9fa37c0185a9..dab4fa98607a0 100644 --- a/app/code/Magento/Quote/Model/ShippingMethodManagement.php +++ b/app/code/Magento/Quote/Model/ShippingMethodManagement.php @@ -286,7 +286,7 @@ public function estimateByAddressId($cartId, $addressId) * @param ExtensibleDataInterface|null $address * @return ShippingMethodInterface[] An array of shipping methods. * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated 100.2.0 + * @deprecated 100.1.6 */ protected function getEstimatedRates( Quote $quote, @@ -366,7 +366,7 @@ private function extractAddressData($address) * Gets the data object processor * * @return DataObjectProcessor - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getDataObjectProcessor() { diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml new file mode 100755 index 0000000000000..a14be3b533fa8 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartData.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerCart" type="CustomerCart"> + <var key="customer_id" entityType="customer" entityKey="id"/> + </entity> + + <entity name="CustomerAddressInformation" type="CustomerAddressInformation"> + <var key="cart_id" entityKey="return" entityType="CustomerCart"/> + <requiredEntity type="shipping_address">ShippingAddressTX</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + <data key="shipping_method_code">flatrate</data> + <data key="shipping_carrier_code">flatrate</data> + </entity> + + <entity name="CustomerOrderPaymentMethod" type="CustomerPaymentInformation"> + <var key="cart_id" entityKey="return" entityType="CustomerCart"/> + <requiredEntity type="payment_method">PaymentMethodCheckMoneyOrder</requiredEntity> + <requiredEntity type="billing_address">BillingAddressTX</requiredEntity> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml new file mode 100644 index 0000000000000..3681245311188 --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Data/CustomerCartItemData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="CustomerCartItem" type="CustomerCartItem"> + <var key="quote_id" entityKey="return" entityType="CustomerCart"/> + <var key="sku" entityKey="sku" entityType="product"/> + <data key="qty">1</data> + </entity> +</entities> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml new file mode 100644 index 0000000000000..f5555394f8d4d --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + + <operation name="CreateCustomerCartItem" dataType="CustomerCartItem" type="create" auth="adminOauth" url="/V1/carts/mine/items" method="POST"> + <contentType>application/json</contentType> + <object key="cartItem" dataType="CustomerCartItem"> + <field key="quote_id" type="string">string</field> + <field key="sku" type="string">string</field> + <field key="qty">integer</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml new file mode 100644 index 0000000000000..f233954f2cdcf --- /dev/null +++ b/app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartMeta.xml @@ -0,0 +1,63 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd"> + <operation name="CreateCustomerCart" dataType="CustomerCart" type="create" + auth="adminOauth" url="/V1/carts/mine" method="POST" > + <contentType>application/json</contentType> + <field key="customer_id">string</field> + </operation> + + <operation name="AddAddressInfoToCustomerCart" dataType="CustomerAddressInformation" type="create" auth="adminOauth" url="/V1/carts/mine/shipping-information" method="POST"> + <contentType>application/json</contentType> + <field key="cart_id">string</field> + <object key="addressInformation" dataType="CustomerAddressInformation"> + <object key="shipping_address" dataType="shipping_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <object key="billing_address" dataType="billing_address"> + <field key="city">string</field> + <field key="region">string</field> + <field key="region_code">string</field> + <field key="region_id">integer</field> + <field key="country_id">string</field> + <array key="street"> + <value>string</value> + </array> + <field key="postcode">string</field> + <field key="firstname">string</field> + <field key="lastname">string</field> + <field key="email">string</field> + <field key="telephone">string</field> + </object> + <field key="shipping_method_code">string</field> + <field key="shipping_carrier_code">string</field> + </object> + </operation> + + <operation name="SendCustomerPaymentInformation" dataType="CustomerPaymentInformation" type="update" auth="adminOauth" url="/V1/carts/mine/payment-information" method="POST"> + <contentType>application/json</contentType> + <field key="cart_id">string</field> + <object key="paymentMethod" dataType="payment_method"> + <field key="method">string</field> + </object> + </operation> +</operations> diff --git a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml index 904a07d72035f..80af412439338 100644 --- a/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml +++ b/app/code/Magento/Quote/Test/Mftf/Test/StorefrontGuestCheckoutDisabledProductTest.xml @@ -75,7 +75,9 @@ </createData> <magentoCLI command="config:set customer/online_customers/section_data_lifetime 1" stepKey="setConfigForCartLifetime"/> - <magentoCLI command="cache:flush" stepKey="flushCache" /> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteProduct"/> @@ -85,7 +87,7 @@ <deleteData createDataKey="createConfigChildProduct1" stepKey="deleteConfigChildProduct1"/> <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> @@ -105,13 +107,12 @@ <openNewTab stepKey="openNewTab"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Find the first simple product that we just created using the product grid and go to its page--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct"> <argument name="product" value="$$createConfigChildProduct1$$"/> </actionGroup> <waitForPageLoad stepKey="waitForFiltersToBeApplied"/> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage"/> - <waitForPageLoad stepKey="waitForProductPageLoad"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage"/> <!-- Disabled child configurable product --> <click selector="{{AdminProductFormSection.enableProductAttributeLabel}}" stepKey="clickDisableProduct"/> <actionGroup ref="SaveProductFormActionGroup" stepKey="clickSaveProduct"/> @@ -137,12 +138,11 @@ <!-- Disabled via admin panel --> <openNewTab stepKey="openNewTab2"/> <!-- Find the first simple product that we just created using the product grid and go to its page --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage2"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage2"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="findCreatedProduct2"> <argument name="product" value="$$createSimpleProduct2$$"/> </actionGroup> - <click selector="{{AdminProductGridSection.firstRow}}" stepKey="clickOnProductPage2"/> - <waitForPageLoad stepKey="waitForProductPageLoad2"/> + <actionGroup ref="AdminProductGridSectionClickFirstRowActionGroup" stepKey="clickOnProductPage2"/> <!-- Disabled simple product from grid --> <actionGroup ref="ChangeStatusProductUsingProductGridActionGroup" stepKey="disabledProductFromGrid2"> <argument name="product" value="$$createSimpleProduct2$$"/> diff --git a/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php b/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php index 956598d17b4d6..26d6aea049915 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/CustomerManagementTest.php @@ -147,7 +147,7 @@ protected function setUp(): void public function testPopulateCustomerInfo() { - $this->quoteMock->expects($this->once()) + $this->quoteMock->expects($this->atLeastOnce()) ->method('getCustomer') ->willReturn($this->customerMock); $this->customerMock->expects($this->atLeastOnce()) diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php index 2f8a5a344503c..3cc586096d4a0 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Address/Total/SubtotalTest.php @@ -59,7 +59,7 @@ protected function setUp(): void $this->stockRegistry = $this->createPartialMock( StockRegistry::class, - ['getStockItem', '__wakeup'] + ['getStockItem'] ); $this->stockItemMock = $this->createPartialMock( \Magento\CatalogInventory\Model\Stock\Item::class, @@ -110,10 +110,11 @@ public function testCollect($price, $originalPrice, $itemHasParent, $expectedPri ] ); /** @var Address|MockObject $address */ - $address = $this->createPartialMock( - Address::class, - ['setTotalQty', 'getTotalQty', 'removeItem', 'getQuote'] - ); + $address = $this->getMockBuilder(Address::class) + ->disableOriginalConstructor() + ->onlyMethods(['removeItem', 'getQuote']) + ->addMethods(['setTotalQty', 'getTotalQty']) + ->getMock(); /** @var Product|MockObject $product */ $product = $this->createMock(Product::class); @@ -161,10 +162,10 @@ public function testCollect($price, $originalPrice, $itemHasParent, $expectedPri $shippingAssignmentMock->expects($this->exactly(2))->method('getShipping')->willReturn($shipping); $shippingAssignmentMock->expects($this->once())->method('getItems')->willReturn([$quoteItem]); - $total = $this->createPartialMock( - Total::class, - ['setBaseVirtualAmount', 'setVirtualAmount'] - ); + $total = $this->getMockBuilder(Total::class) + ->disableOriginalConstructor() + ->addMethods(['setVirtualAmount', 'setBaseVirtualAmount']) + ->getMock(); $total->expects($this->once())->method('setBaseVirtualAmount')->willReturnSelf(); $total->expects($this->once())->method('setVirtualAmount')->willReturnSelf(); @@ -185,7 +186,9 @@ public function testFetch() ]; $quoteMock = $this->createMock(Quote::class); - $totalMock = $this->createPartialMock(Total::class, ['getSubtotal']); + $totalMock = $this->getMockBuilder(Total::class) + ->addMethods(['getSubtotal']) + ->getMockForAbstractClass(); $totalMock->expects($this->once())->method('getSubtotal')->willReturn(100); $this->assertEquals($expectedResult, $this->subtotalModel->fetch($quoteMock, $totalMock)); @@ -229,13 +232,11 @@ public function testCollectWithInvalidItems() $address->expects($this->once()) ->method('removeItem') ->with($addressItemId); - $addressItem = $this->createPartialMock( - AddressItem::class, - [ - 'getId', - 'getQuoteItemId' - ] - ); + $addressItem = $this->getMockBuilder(AddressItem::class) + ->disableOriginalConstructor() + ->onlyMethods(['getId']) + ->addMethods(['getQuoteItemId']) + ->getMock(); $addressItem->setAddress($address); $addressItem->method('getId') ->willReturn($addressItemId); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php index a8fd794c08757..d4f6778a2ccb8 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/AddressTest.php @@ -352,10 +352,40 @@ public function testRequestShippingRates() $currentCurrencyCode = 'UAH'; + $this->quote->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + + $this->storeManager->expects($this->at(0)) + ->method('getStore') + ->with($storeId) + ->willReturn($this->store); + $this->store->expects($this->any()) + ->method('getWebsiteId') + ->willReturn($webSiteId); + + $this->scopeConfig->expects($this->exactly(1)) + ->method('getValue') + ->with( + 'tax/calculation/price_includes_tax', + ScopeInterface::SCOPE_STORE, + $storeId + ) + ->willReturn(1); + /** @var RateRequest */ $request = $this->getMockBuilder(RateRequest::class) ->disableOriginalConstructor() - ->setMethods(['setStoreId', 'setWebsiteId', 'setBaseCurrency', 'setPackageCurrency']) + ->setMethods( + [ + 'setStoreId', + 'setWebsiteId', + 'setBaseCurrency', + 'setPackageCurrency', + 'getBaseSubtotalTotalInclTax', + 'getBaseSubtotal' + ] + ) ->getMock(); /** @var Collection */ @@ -434,13 +464,6 @@ public function testRequestShippingRates() $this->storeManager->method('getStore') ->willReturn($this->store); - $this->storeManager->expects($this->once()) - ->method('getWebsite') - ->willReturn($this->website); - - $this->store->method('getId') - ->willReturn($storeId); - $this->store->method('getBaseCurrency') ->willReturn($baseCurrency); @@ -452,10 +475,6 @@ public function testRequestShippingRates() ->method('getCurrentCurrencyCode') ->willReturn($currentCurrencyCode); - $this->website->expects($this->once()) - ->method('getId') - ->willReturn($webSiteId); - $this->addressRateFactory->expects($this->once()) ->method('create') ->willReturn($rate); diff --git a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php index cbcb7dd0adc3c..3025a72410671 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/Quote/Item/ProcessorTest.php @@ -77,7 +77,16 @@ protected function setUp(): void $this->itemMock = $this->getMockBuilder(Item::class) ->addMethods(['setOriginalCustomPrice']) - ->onlyMethods(['getId', 'setOptions', 'setProduct', 'addQty', 'setCustomPrice', 'setData', 'setPrice']) + ->onlyMethods([ + 'getId', + 'setOptions', + 'setProduct', + 'addQty', + 'setCustomPrice', + 'setData', + 'setPrice', + 'getParentItem' + ]) ->disableOriginalConstructor() ->getMock(); $this->quoteItemFactoryMock->expects($this->any()) @@ -438,4 +447,41 @@ public function testPrepareWithResetCountAndNotStickAndSameItemId() $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock); } + + /** + * @param bool $isChildrenCalculated + * @dataProvider prepareChildProductDataProvider + */ + public function testPrepareChildProduct(bool $isChildrenCalculated): void + { + $finalPrice = 10; + $this->objectMock->method('getResetCount') + ->willReturn(false); + $this->productMock->method('getFinalPrice') + ->willReturn($finalPrice); + $this->itemMock->expects($isChildrenCalculated ? $this->once() : $this->never()) + ->method('setPrice') + ->with($finalPrice) + ->willReturnSelf(); + $parentItem = $this->createConfiguredMock( + \Magento\Quote\Model\Quote\Item::class, + [ + 'isChildrenCalculated' => $isChildrenCalculated + ] + ); + $this->itemMock->method('getParentItem') + ->willReturn($parentItem); + $this->processor->prepare($this->itemMock, $this->objectMock, $this->productMock); + } + + /** + * @return array + */ + public function prepareChildProductDataProvider(): array + { + return [ + [false], + [true] + ]; + } } diff --git a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php index d9b797c454d4e..422a6cbcb7bbe 100644 --- a/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php +++ b/app/code/Magento/Quote/Test/Unit/Model/QuoteTest.php @@ -30,7 +30,9 @@ use Magento\Framework\DataObject\Copy; use Magento\Framework\DataObject\Factory; use Magento\Framework\Event\Manager; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Model\Context; +use Magento\Framework\Phrase; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Quote; @@ -640,6 +642,48 @@ public function testGetCustomerTaxClassId() $this->assertEquals($taxClassId, $result); } + /** + * Test case when non-existent customer group is stored into the quote. + * In such a case we should get a NoSuchEntityException exception and try + * to get a valid customer group from the current customer object. + */ + public function testGetCustomerTaxClassIdForNonExistentCustomerGroup() + { + $customerId = 1; + $nonExistentGroupId = 100; + $groupId = 1; + $taxClassId = 1; + $groupMock = $this->getMockForAbstractClass(GroupInterface::class, [], '', false); + $this->groupRepositoryMock->expects($this->at(0)) + ->method('getById') + ->with($nonExistentGroupId) + ->willThrowException(new NoSuchEntityException(new Phrase('Entity Id does not exist'))); + $customerMock = $this->getMockForAbstractClass( + CustomerInterface::class, + [], + '', + false + ); + $customerMock->expects($this->once()) + ->method('getGroupId') + ->willReturn($groupId); + $this->customerRepositoryMock->expects($this->once()) + ->method('getById') + ->with($customerId) + ->willReturn($customerMock); + $this->groupRepositoryMock->expects($this->at(1)) + ->method('getById') + ->with($groupId) + ->willReturn($groupMock); + $groupMock->expects($this->once()) + ->method('getTaxClassId') + ->willReturn($taxClassId); + $this->quote->setData('customer_id', $customerId); + $this->quote->setData('customer_group_id', $nonExistentGroupId); + $result = $this->quote->getCustomerTaxClassId(); + $this->assertEquals($taxClassId, $result); + } + public function testGetAllAddresses() { $id = 1; diff --git a/app/code/Magento/Quote/etc/graphql/di.xml b/app/code/Magento/Quote/etc/graphql/di.xml new file mode 100644 index 0000000000000..0e688d42ecb32 --- /dev/null +++ b/app/code/Magento/Quote/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="customizable_option" xsi:type="object">Magento\Quote\Model\Cart\BuyRequest\CustomizableOptionDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteBundleOptions/Model/Cart/BuyRequest/BundleDataProvider.php b/app/code/Magento/QuoteBundleOptions/Model/Cart/BuyRequest/BundleDataProvider.php new file mode 100644 index 0000000000000..575784c86ace1 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/Model/Cart/BuyRequest/BundleDataProvider.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteBundleOptions\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * Data provider for bundle product buy requests + */ +class BundleDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'bundle'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $bundleOptionsData = []; + + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $optionId, $optionValueId, $optionQuantity] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + } + //for bundle options with custom quantity + foreach ($cartItem->getEnteredOptions() as $option) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($option->getUid())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $optionId, $optionValueId] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $optionQuantity = $option->getValue(); + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + } + + return $bundleOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 4) { + $errorMessage = __('Wrong format of the entered option data'); + throw new LocalizedException($errorMessage); + } + } +} diff --git a/app/code/Magento/QuoteBundleOptions/README.md b/app/code/Magento/QuoteBundleOptions/README.md new file mode 100644 index 0000000000000..3207eeaf2b683 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/README.md @@ -0,0 +1,3 @@ +# QuoteBundleOptions + +**QuoteBundleOptions** provides data provider for creating buy request for bundle products. diff --git a/app/code/Magento/QuoteBundleOptions/composer.json b/app/code/Magento/QuoteBundleOptions/composer.json new file mode 100644 index 0000000000000..a2651272018a8 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-quote-bundle-options", + "description": "Magento module provides data provider for creating buy request for bundle products", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\QuoteBundleOptions\\": "" + } + } +} diff --git a/app/code/Magento/QuoteBundleOptions/etc/graphql/di.xml b/app/code/Magento/QuoteBundleOptions/etc/graphql/di.xml new file mode 100644 index 0000000000000..e15493e092e3b --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="bundle" xsi:type="object">Magento\QuoteBundleOptions\Model\Cart\BuyRequest\BundleDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteBundleOptions/etc/module.xml b/app/code/Magento/QuoteBundleOptions/etc/module.xml new file mode 100644 index 0000000000000..4dc531b561115 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_QuoteBundleOptions"/> +</config> diff --git a/app/code/Magento/QuoteBundleOptions/registration.php b/app/code/Magento/QuoteBundleOptions/registration.php new file mode 100644 index 0000000000000..cf4c92fd929d9 --- /dev/null +++ b/app/code/Magento/QuoteBundleOptions/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_QuoteBundleOptions', __DIR__); diff --git a/app/code/Magento/QuoteConfigurableOptions/Model/Cart/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/QuoteConfigurableOptions/Model/Cart/BuyRequest/SuperAttributeDataProvider.php new file mode 100644 index 0000000000000..d58b574352bd8 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/Model/Cart/BuyRequest/SuperAttributeDataProvider.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteConfigurableOptions\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * DataProvider for building super attribute options in buy requests + */ +class SuperAttributeDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'configurable'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $configurableProductData = []; + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $attributeId, $valueIndex] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $configurableProductData[$attributeId] = $valueIndex; + } + } + + return ['super_attribute' => $configurableProductData]; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 3) { + throw new LocalizedException( + __('Wrong format of the entered option data') + ); + } + } +} diff --git a/app/code/Magento/QuoteConfigurableOptions/README.md b/app/code/Magento/QuoteConfigurableOptions/README.md new file mode 100644 index 0000000000000..db47e2c37c3ff --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/README.md @@ -0,0 +1,3 @@ +# QuoteConfigurableOptions + +**QuoteConfigurableOptions** provides data provider for creating buy request for configurable products. diff --git a/app/code/Magento/QuoteConfigurableOptions/composer.json b/app/code/Magento/QuoteConfigurableOptions/composer.json new file mode 100644 index 0000000000000..51d6933d5c6d6 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-quote-configurable-options", + "description": "Magento module provides data provider for creating buy request for configurable products", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\QuoteConfigurableOptions\\": "" + } + } +} diff --git a/app/code/Magento/QuoteConfigurableOptions/etc/graphql/di.xml b/app/code/Magento/QuoteConfigurableOptions/etc/graphql/di.xml new file mode 100644 index 0000000000000..c4fe6357a5689 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="super_attribute" xsi:type="object">Magento\QuoteConfigurableOptions\Model\Cart\BuyRequest\SuperAttributeDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteConfigurableOptions/etc/module.xml b/app/code/Magento/QuoteConfigurableOptions/etc/module.xml new file mode 100644 index 0000000000000..e32489c1b2109 --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_QuoteConfigurableOptions"/> +</config> diff --git a/app/code/Magento/QuoteConfigurableOptions/registration.php b/app/code/Magento/QuoteConfigurableOptions/registration.php new file mode 100644 index 0000000000000..0b55a18a81fce --- /dev/null +++ b/app/code/Magento/QuoteConfigurableOptions/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_QuoteConfigurableOptions', __DIR__); diff --git a/app/code/Magento/QuoteDownloadableLinks/Model/Cart/BuyRequest/DownloadableLinkDataProvider.php b/app/code/Magento/QuoteDownloadableLinks/Model/Cart/BuyRequest/DownloadableLinkDataProvider.php new file mode 100644 index 0000000000000..e412c7df573c7 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/Model/Cart/BuyRequest/DownloadableLinkDataProvider.php @@ -0,0 +1,77 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteDownloadableLinks\Model\Cart\BuyRequest; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Quote\Model\Cart\BuyRequest\BuyRequestDataProviderInterface; +use Magento\Quote\Model\Cart\Data\CartItem; + +/** + * DataProvider for building downloadable product links in buy requests + */ +class DownloadableLinkDataProvider implements BuyRequestDataProviderInterface +{ + private const OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + * + * @throws LocalizedException + */ + public function execute(CartItem $cartItem): array + { + $linksData = []; + + foreach ($cartItem->getSelectedOptions() as $optionData) { + // phpcs:ignore Magento2.Functions.DiscouragedFunction + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + $this->validateInput($optionData); + + [$optionType, $linkId] = $optionData; + if ($optionType == self::OPTION_TYPE) { + $linksData[] = $linkId; + } + } + + return ['links' => $linksData]; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + if ($optionData[0] !== self::OPTION_TYPE) { + return false; + } + + return true; + } + + /** + * Validates the provided options structure + * + * @param array $optionData + * @throws LocalizedException + */ + private function validateInput(array $optionData): void + { + if (count($optionData) !== 2) { + throw new LocalizedException( + __('Wrong format of the entered option data') + ); + } + } +} diff --git a/app/code/Magento/QuoteDownloadableLinks/README.md b/app/code/Magento/QuoteDownloadableLinks/README.md new file mode 100644 index 0000000000000..68efffcea6fb8 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/README.md @@ -0,0 +1,3 @@ +# QuoteDownloadableLinks + +**QuoteDownloadableLinks** provides data provider for creating buy request for links of downloadable products. diff --git a/app/code/Magento/QuoteDownloadableLinks/composer.json b/app/code/Magento/QuoteDownloadableLinks/composer.json new file mode 100644 index 0000000000000..ad120dea96263 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/composer.json @@ -0,0 +1,22 @@ +{ + "name": "magento/module-quote-downloadable-links", + "description": "Magento module provides data provider for creating buy request for links of downloadable products", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/framework": "*", + "magento/module-quote": "*" + }, + "type": "magento2-module", + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\QuoteDownloadableLinks\\": "" + } + } +} diff --git a/app/code/Magento/QuoteDownloadableLinks/etc/graphql/di.xml b/app/code/Magento/QuoteDownloadableLinks/etc/graphql/di.xml new file mode 100644 index 0000000000000..a932d199983a3 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/etc/graphql/di.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Quote\Model\Cart\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="downloadable" xsi:type="object">Magento\QuoteDownloadableLinks\Model\Cart\BuyRequest\DownloadableLinkDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/QuoteDownloadableLinks/etc/module.xml b/app/code/Magento/QuoteDownloadableLinks/etc/module.xml new file mode 100644 index 0000000000000..a0cc652ab9188 --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_QuoteDownloadableLinks"/> +</config> diff --git a/app/code/Magento/QuoteDownloadableLinks/registration.php b/app/code/Magento/QuoteDownloadableLinks/registration.php new file mode 100644 index 0000000000000..8b766e7fde06c --- /dev/null +++ b/app/code/Magento/QuoteDownloadableLinks/registration.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_QuoteDownloadableLinks', __DIR__); diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php index 4dbcfad31e84c..51303df345827 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/AssignShippingAddressToCart.php @@ -16,7 +16,7 @@ use Magento\Quote\Model\ShippingAddressManagementInterface; /** - * Assign shipping address to cart + * Assigning shipping address to cart */ class AssignShippingAddressToCart { @@ -49,7 +49,14 @@ public function execute( try { $this->shippingAddressManagement->assign($cart->getId(), $shippingAddress); } catch (NoSuchEntityException $e) { - throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + if ($cart->getIsVirtual()) { + throw new GraphQlNoSuchEntityException( + __('Shipping address is not allowed on cart: cart contains no items for shipment.'), + $e + ); + } else { + throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); + } } catch (InputException $e) { throw new GraphQlInputException(__($e->getMessage()), $e); } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php index f73daa715c1df..e959c19a7cbe4 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingAddressesOnCart.php @@ -51,7 +51,10 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s $shippingAddressInput = current($shippingAddressesInput) ?? []; $customerAddressId = $shippingAddressInput['customer_address_id'] ?? null; - if (!$customerAddressId && !isset($shippingAddressInput['address']['save_in_address_book'])) { + if (!$customerAddressId + && isset($shippingAddressInput['address']) + && !isset($shippingAddressInput['address']['save_in_address_book']) + ) { $shippingAddressInput['address']['save_in_address_book'] = true; } diff --git a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php index b2526bdc04e98..654a4bb558632 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Cart/SetShippingMethodsOnCart.php @@ -42,12 +42,12 @@ public function execute(ContextInterface $context, CartInterface $cart, array $s } $shippingMethodInput = current($shippingMethodsInput); - if (!isset($shippingMethodInput['carrier_code']) || empty($shippingMethodInput['carrier_code'])) { + if (empty($shippingMethodInput['carrier_code'])) { throw new GraphQlInputException(__('Required parameter "carrier_code" is missing.')); } $carrierCode = $shippingMethodInput['carrier_code']; - if (!isset($shippingMethodInput['method_code']) || empty($shippingMethodInput['method_code'])) { + if (empty($shippingMethodInput['method_code'])) { throw new GraphQlInputException(__('Required parameter "method_code" is missing.')); } $methodCode = $shippingMethodInput['method_code']; diff --git a/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php new file mode 100644 index 0000000000000..c2e94b215956e --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/CartItem/DataProvider/UpdateCartItems.php @@ -0,0 +1,157 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\CartItem\DataProvider; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\GiftMessage\Api\Data\MessageInterface; +use Magento\GiftMessage\Api\Data\MessageInterfaceFactory; +use Magento\GiftMessage\Api\ItemRepositoryInterface; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; +use Magento\Quote\Api\CartItemRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; + +/** + * Class contain update cart items methods + */ +class UpdateCartItems +{ + /** + * @var CartItemRepositoryInterface + */ + private $cartItemRepository; + + /** + * @var UpdateCartItem + */ + private $updateCartItem; + + /** + * @var ItemRepositoryInterface + */ + private $itemRepository; + + /** + * @var GiftMessageHelper + */ + private $giftMessageHelper; + + /** + * @var MessageInterfaceFactory + */ + private $giftMessageFactory; + + /** + * @param CartItemRepositoryInterface $cartItemRepository + * @param UpdateCartItem $updateCartItem + * @param ItemRepositoryInterface $itemRepository + * @param GiftMessageHelper $giftMessageHelper + * @param MessageInterfaceFactory $giftMessageFactory + */ + public function __construct( + CartItemRepositoryInterface $cartItemRepository, + UpdateCartItem $updateCartItem, + ItemRepositoryInterface $itemRepository, + GiftMessageHelper $giftMessageHelper, + MessageInterfaceFactory $giftMessageFactory + ) { + $this->cartItemRepository = $cartItemRepository; + $this->updateCartItem = $updateCartItem; + $this->itemRepository = $itemRepository; + $this->giftMessageHelper = $giftMessageHelper; + $this->giftMessageFactory = $giftMessageFactory; + } + + /** + * Process cart items + * + * @param Quote $cart + * @param array $items + * + * @throws GraphQlInputException + * @throws LocalizedException + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function processCartItems(Quote $cart, array $items): void + { + foreach ($items as $item) { + if (empty($item['cart_item_id'])) { + throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); + } + + $itemId = (int)$item['cart_item_id']; + $customizableOptions = $item['customizable_options'] ?? []; + $cartItem = $cart->getItemById($itemId); + + if ($cartItem && $cartItem->getParentItemId()) { + throw new GraphQlInputException(__('Child items may not be updated.')); + } + + if (count($customizableOptions) === 0 && !isset($item['quantity'])) { + throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); + } + + $quantity = (float)$item['quantity']; + + if ($quantity <= 0.0) { + $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); + } else { + $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); + } + + if (!empty($item['gift_message'])) { + try { + if (!$this->giftMessageHelper->isMessagesAllowed('items', $cartItem)) { + continue; + } + if (!$this->giftMessageHelper->isMessagesAllowed('item', $cartItem)) { + continue; + } + + /** @var MessageInterface $giftItemMessage */ + $giftItemMessage = $this->itemRepository->get($cart->getEntityId(), $itemId); + + if (empty($giftItemMessage)) { + /** @var MessageInterface $giftMessage */ + $giftMessage = $this->giftMessageFactory->create(); + $this->updateGiftMessageForItem($cart, $giftMessage, $item, $itemId); + continue; + } + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated.')); + } + + $this->updateGiftMessageForItem($cart, $giftItemMessage, $item, $itemId); + } + } + } + + /** + * Update Gift Message for Quote item + * + * @param Quote $cart + * @param MessageInterface $giftItemMessage + * @param array $item + * @param int $itemId + * + * @throws GraphQlInputException + */ + private function updateGiftMessageForItem(Quote $cart, MessageInterface $giftItemMessage, array $item, int $itemId) + { + try { + $giftItemMessage->setRecipient($item['gift_message']['to']); + $giftItemMessage->setSender($item['gift_message']['from']); + $giftItemMessage->setMessage($item['gift_message']['message']); + $this->itemRepository->save($cart->getEntityId(), $giftItemMessage, $itemId); + } catch (LocalizedException $exception) { + throw new GraphQlInputException(__('Gift Message cannot be updated')); + } + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php new file mode 100644 index 0000000000000..d5e554f096ec1 --- /dev/null +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/AddProductsToCart.php @@ -0,0 +1,93 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\QuoteGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Quote\Model\Cart\AddProductsToCart as AddProductsToCartService; +use Magento\Quote\Model\Cart\Data\AddProductsToCartOutput; +use Magento\Quote\Model\Cart\Data\CartItemFactory; +use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; +use Magento\Quote\Model\Cart\Data\Error; + +/** + * Resolver for addProductsToCart mutation + * + * @inheritdoc + */ +class AddProductsToCart implements ResolverInterface +{ + /** + * @var GetCartForUser + */ + private $getCartForUser; + + /** + * @var AddProductsToCartService + */ + private $addProductsToCartService; + + /** + * @param GetCartForUser $getCartForUser + * @param AddProductsToCartService $addProductsToCart + */ + public function __construct( + GetCartForUser $getCartForUser, + AddProductsToCartService $addProductsToCart + ) { + $this->getCartForUser = $getCartForUser; + $this->addProductsToCartService = $addProductsToCart; + } + + /** + * @inheritdoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (empty($args['cartId'])) { + throw new GraphQlInputException(__('Required parameter "cartId" is missing')); + } + if (empty($args['cartItems']) || !is_array($args['cartItems']) + ) { + throw new GraphQlInputException(__('Required parameter "cartItems" is missing')); + } + + $maskedCartId = $args['cartId']; + $cartItemsData = $args['cartItems']; + $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); + + // Shopping Cart validation + $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); + + $cartItems = []; + foreach ($cartItemsData as $cartItemData) { + $cartItems[] = (new CartItemFactory())->create($cartItemData); + } + + /** @var AddProductsToCartOutput $addProductsToCartOutput */ + $addProductsToCartOutput = $this->addProductsToCartService->execute($maskedCartId, $cartItems); + + return [ + 'cart' => [ + 'model' => $addProductsToCartOutput->getCart(), + ], + 'user_errors' => array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + 'path' => [$error->getCartItemPosition()] + ]; + }, + $addProductsToCartOutput->getErrors() + ) + ]; + } +} diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php index 0be95eccc39e5..e8aa8d612c670 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/CustomerCart.php @@ -7,17 +7,12 @@ namespace Magento\QuoteGraphQl\Model\Resolver; -use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\QuoteGraphQl\Model\Cart\CreateEmptyCartForCustomer; use Magento\GraphQl\Model\Query\ContextInterface; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; -use Magento\Quote\Api\CartManagementInterface; -use Magento\Quote\Model\QuoteIdMaskFactory; -use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; -use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResourceModel; +use Magento\Quote\Model\Cart\CustomerCartResolver; /** * Get cart for the customer @@ -25,48 +20,19 @@ class CustomerCart implements ResolverInterface { /** - * @var CreateEmptyCartForCustomer + * @var CustomerCartResolver */ - private $createEmptyCartForCustomer; + private $customerCartResolver; /** - * @var CartManagementInterface - */ - private $cartManagement; - - /** - * @var QuoteIdMaskFactory - */ - private $quoteIdMaskFactory; - - /** - * @var QuoteIdMaskResourceModel - */ - private $quoteIdMaskResourceModel; - /** - * @var QuoteIdToMaskedQuoteIdInterface - */ - private $quoteIdToMaskedQuoteId; - - /** - * @param CreateEmptyCartForCustomer $createEmptyCartForCustomer - * @param CartManagementInterface $cartManagement - * @param QuoteIdMaskFactory $quoteIdMaskFactory - * @param QuoteIdMaskResourceModel $quoteIdMaskResourceModel - * @param QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + * CustomerCart constructor. + * + * @param CustomerCartResolver $customerCartResolver */ public function __construct( - CreateEmptyCartForCustomer $createEmptyCartForCustomer, - CartManagementInterface $cartManagement, - QuoteIdMaskFactory $quoteIdMaskFactory, - QuoteIdMaskResourceModel $quoteIdMaskResourceModel, - QuoteIdToMaskedQuoteIdInterface $quoteIdToMaskedQuoteId + CustomerCartResolver $customerCartResolver ) { - $this->createEmptyCartForCustomer = $createEmptyCartForCustomer; - $this->cartManagement = $cartManagement; - $this->quoteIdMaskFactory = $quoteIdMaskFactory; - $this->quoteIdMaskResourceModel = $quoteIdMaskResourceModel; - $this->quoteIdToMaskedQuoteId = $quoteIdToMaskedQuoteId; + $this->customerCartResolver = $customerCartResolver; } /** @@ -76,22 +42,17 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value { $currentUserId = $context->getUserId(); - /** @var ContextInterface $context */ + /** + * @var ContextInterface $context + */ if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The request is allowed for logged in customer')); } - try { - $cart = $this->cartManagement->getCartForCustomer($currentUserId); - } catch (NoSuchEntityException $e) { - $this->createEmptyCartForCustomer->execute($currentUserId, null); - $cart = $this->cartManagement->getCartForCustomer($currentUserId); - } - $maskedId = $this->quoteIdToMaskedQuoteId->execute((int) $cart->getId()); - if (empty($maskedId)) { - $quoteIdMask = $this->quoteIdMaskFactory->create(); - $quoteIdMask->setQuoteId((int) $cart->getId()); - $this->quoteIdMaskResourceModel->save($quoteIdMask); + try { + $cart = $this->customerCartResolver->resolve($currentUserId); + } catch (\Exception $e) { + $cart = null; } return [ diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php index dd4ce8fe7f7a6..a2ac94a0f28cc 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/SetPaymentAndPlaceOrder.php @@ -22,7 +22,7 @@ /** * Resolver for setting payment method and placing order * - * @deprecated Should use setPaymentMethodOnCart and placeOrder mutations in single request. + * @deprecated 100.3.4 Should use setPaymentMethodOnCart and placeOrder mutations in single request. * @see \Magento\QuoteGraphQl\Model\Resolver\SetPaymentMethodOnCart * @see \Magento\QuoteGraphQl\Model\Resolver\PlaceOrder */ @@ -71,14 +71,15 @@ public function __construct( */ public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) { - if (!isset($args['input']['cart_id']) || empty($args['input']['cart_id'])) { + if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing')); } - $maskedCartId = $args['input']['cart_id']; - if (!isset($args['input']['payment_method']['code']) || empty($args['input']['payment_method']['code'])) { + if (empty($args['input']['payment_method']['code'])) { throw new GraphQlInputException(__('Required parameter "code" for "payment_method" is missing.')); } + + $maskedCartId = $args['input']['cart_id']; $paymentData = $args['input']['payment_method']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); diff --git a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php index fa90f08e4b553..005baaad0e1e5 100644 --- a/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php +++ b/app/code/Magento/QuoteGraphQl/Model/Resolver/UpdateCartItems.php @@ -14,53 +14,43 @@ use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; -use Magento\Quote\Api\CartItemRepositoryInterface; use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Quote\Model\Quote; use Magento\QuoteGraphQl\Model\Cart\GetCartForUser; -use Magento\QuoteGraphQl\Model\Cart\UpdateCartItem; +use Magento\QuoteGraphQl\Model\CartItem\DataProvider\UpdateCartItems as UpdateCartItemsProvider; /** * @inheritdoc */ class UpdateCartItems implements ResolverInterface { - /** - * @var UpdateCartItem - */ - private $updateCartItem; - /** * @var GetCartForUser */ private $getCartForUser; /** - * @var CartItemRepositoryInterface + * @var CartRepositoryInterface */ - private $cartItemRepository; + private $cartRepository; /** - * @var CartRepositoryInterface + * @var UpdateCartItemsProvider */ - private $cartRepository; + private $updateCartItems; /** - * @param GetCartForUser $getCartForUser - * @param CartItemRepositoryInterface $cartItemRepository - * @param UpdateCartItem $updateCartItem + * @param GetCartForUser $getCartForUser * @param CartRepositoryInterface $cartRepository + * @param UpdateCartItemsProvider $updateCartItems */ public function __construct( GetCartForUser $getCartForUser, - CartItemRepositoryInterface $cartItemRepository, - UpdateCartItem $updateCartItem, - CartRepositoryInterface $cartRepository + CartRepositoryInterface $cartRepository, + UpdateCartItemsProvider $updateCartItems ) { $this->getCartForUser = $getCartForUser; - $this->cartItemRepository = $cartItemRepository; - $this->updateCartItem = $updateCartItem; $this->cartRepository = $cartRepository; + $this->updateCartItems = $updateCartItems; } /** @@ -71,6 +61,7 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value if (empty($args['input']['cart_id'])) { throw new GraphQlInputException(__('Required parameter "cart_id" is missing.')); } + $maskedCartId = $args['input']['cart_id']; if (empty($args['input']['cart_items']) @@ -78,13 +69,13 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ) { throw new GraphQlInputException(__('Required parameter "cart_items" is missing.')); } - $cartItems = $args['input']['cart_items']; + $cartItems = $args['input']['cart_items']; $storeId = (int)$context->getExtensionAttributes()->getStore()->getId(); $cart = $this->getCartForUser->execute($maskedCartId, $context->getUserId(), $storeId); try { - $this->processCartItems($cart, $cartItems); + $this->updateCartItems->processCartItems($cart, $cartItems); $this->cartRepository->save($cart); } catch (NoSuchEntityException $e) { throw new GraphQlNoSuchEntityException(__($e->getMessage()), $e); @@ -98,39 +89,4 @@ public function resolve(Field $field, $context, ResolveInfo $info, array $value ], ]; } - - /** - * Process cart items - * - * @param Quote $cart - * @param array $items - * @throws GraphQlInputException - * @throws LocalizedException - */ - private function processCartItems(Quote $cart, array $items): void - { - foreach ($items as $item) { - if (empty($item['cart_item_id'])) { - throw new GraphQlInputException(__('Required parameter "cart_item_id" for "cart_items" is missing.')); - } - $itemId = (int)$item['cart_item_id']; - $customizableOptions = $item['customizable_options'] ?? []; - - $cartItem = $cart->getItemById($itemId); - if ($cartItem && $cartItem->getParentItemId()) { - throw new GraphQlInputException(__('Child items may not be updated.')); - } - - if (count($customizableOptions) === 0 && !isset($item['quantity'])) { - throw new GraphQlInputException(__('Required parameter "quantity" for "cart_items" is missing.')); - } - $quantity = (float)$item['quantity']; - - if ($quantity <= 0.0) { - $this->cartItemRepository->deleteById((int)$cart->getId(), $itemId); - } else { - $this->updateCartItem->execute($cart, $itemId, $quantity, $customizableOptions); - } - } - } } diff --git a/app/code/Magento/QuoteGraphQl/composer.json b/app/code/Magento/QuoteGraphQl/composer.json index 0652d39b5f426..25f089cf75a62 100644 --- a/app/code/Magento/QuoteGraphQl/composer.json +++ b/app/code/Magento/QuoteGraphQl/composer.json @@ -13,7 +13,8 @@ "magento/module-customer-graph-ql": "*", "magento/module-sales": "*", "magento/module-directory": "*", - "magento/module-graph-ql": "*" + "magento/module-graph-ql": "*", + "magento/module-gift-message": "*" }, "suggest": { "magento/module-graph-ql-cache": "*" diff --git a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls index 955ee1cc2429a..4e0e7ce5732be 100644 --- a/app/code/Magento/QuoteGraphQl/etc/schema.graphqls +++ b/app/code/Magento/QuoteGraphQl/etc/schema.graphqls @@ -22,6 +22,7 @@ type Mutation { setPaymentMethodAndPlaceOrder(input: SetPaymentMethodAndPlaceOrderInput): PlaceOrderOutput @deprecated(reason: "Should use setPaymentMethodOnCart and placeOrder mutations in single request.") @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\SetPaymentAndPlaceOrder") mergeCarts(source_cart_id: String!, destination_cart_id: String!): Cart! @doc(description:"Merges the source cart into the destination cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\MergeCarts") placeOrder(input: PlaceOrderInput): PlaceOrderOutput @resolver(class: "\\Magento\\QuoteGraphQl\\Model\\Resolver\\PlaceOrder") + addProductsToCart(cartId: String!, cartItems: [CartItemInput!]!): AddProductsToCartOutput @doc(description:"Add any type of product to the cart") @resolver(class: "Magento\\QuoteGraphQl\\Model\\Resolver\\AddProductsToCart") } input createEmptyCartInput { @@ -51,6 +52,9 @@ input VirtualProductCartItemInput { input CartItemInput { sku: String! quantity: Float! + parent_sku: String @doc(description: "For child products, the SKU of its parent product") + selected_options: [ID!] @doc(description: "The selected options for the base product, such as color or size") + entered_options: [EnteredOptionInput!] @doc(description: "An array of entered options for the base product, such as personalization text") } input CustomizableOptionInput { @@ -368,3 +372,21 @@ type Order { order_number: String! order_id: String @deprecated(reason: "The order_id field is deprecated, use order_number instead.") } + +type CartUserInputError @doc(description:"An error encountered while adding an item to the the cart.") { + message: String! @doc(description: "A localized error message") + code: CartUserInputErrorType! @doc(description: "Cart-specific error code") +} + +type AddProductsToCartOutput { + cart: Cart! @doc(description: "The cart after products have been added") + user_errors: [CartUserInputError!]! @doc(description: "An error encountered while adding an item to the cart.") +} + +enum CartUserInputErrorType { + PRODUCT_NOT_FOUND + NOT_SALABLE + INSUFFICIENT_STOCK + UNDEFINED +} + diff --git a/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php index dd42874b55795..257eb481e1923 100644 --- a/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php +++ b/app/code/Magento/Reports/Block/Adminhtml/Wishlist.php @@ -9,7 +9,7 @@ /** * Adminhtml wishlist report page content block * - * @deprecated + * @deprecated 100.3.3 * @author Magento Core Team <core@magentocommerce.com> */ class Wishlist extends \Magento\Backend\Block\Template diff --git a/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php b/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php index 12959f083d376..1e3eb12331bde 100644 --- a/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php +++ b/app/code/Magento/Reports/Block/Product/Widget/Viewed/Item.php @@ -8,7 +8,7 @@ /** * Reports Recently Viewed Products Widget * - * @deprecated + * @deprecated 100.3.3 * @author Magento Core Team <core@magentocommerce.com> */ class Item extends \Magento\Catalog\Block\Product\AbstractProduct implements \Magento\Widget\Block\BlockInterface diff --git a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php index 0a74c23fad991..44571550459c2 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Order/Collection.php @@ -818,7 +818,7 @@ public function addSumAvgTotals($storeId = 0) * @param string $baseSubtotalCanceled * @param string $baseDiscountCanceled * @return string - * @deprecated + * @deprecated 100.3.2 * @see getTotalsExpressionWithDiscountRefunded */ protected function getTotalsExpression( diff --git a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php index d194526858cde..ed3e4e8c4446d 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Product/Downloads/Collection.php @@ -100,6 +100,7 @@ public function addFieldToFilter($field, $condition = null) /** * @inheritDoc + * @since 100.3.2 */ public function getSelectCountSql() { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php index c1c6fb2eaed88..b69ea94aac9bb 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Quote/Collection.php @@ -58,6 +58,7 @@ public function __construct( * @param array $storeIds * @param bool $withAdmin * @return $this + * @since 100.3.1 */ public function addStoreFilter(array $storeIds, $withAdmin = true) { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php index f37bd6c6a7bd2..1ee47f3cd7bbb 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Review/Customer/Collection.php @@ -113,6 +113,7 @@ protected function _joinCustomers() * * Additional processing of 'customer_name' field is required, as it is a concat field, which can not be aliased. * @see _joinCustomers + * @since 100.2.2 */ public function addFieldToFilter($field, $condition = null) { diff --git a/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php index 6f7738a8273bb..69221af3322f0 100644 --- a/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Reports/Model/ResourceModel/Review/Product/Collection.php @@ -106,6 +106,7 @@ public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC) * @param array|null $condition * @param string $joinType * @return $this|\Magento\Catalog\Model\ResourceModel\Product\Collection + * @since 100.3.5 */ public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner') { diff --git a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml index e572febec5a5c..3e79eb044b5cb 100644 --- a/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml +++ b/app/code/Magento/Reports/Test/Mftf/Test/CancelOrdersInOrderSalesReportTest.xml @@ -49,8 +49,7 @@ <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="New Invoice" stepKey="seePageNameNewInvoicePage"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <click selector="{{AdminOrderDetailsMainActionsSection.ship}}" stepKey="clickShipAction"/> <seeInCurrentUrl url="{{AdminShipmentNewPage.url}}" stepKey="seeOrderShipmentUrl"/> <click selector="{{AdminShipmentMainActionsSection.submitShipment}}" stepKey="clickSubmitShipment"/> diff --git a/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml b/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml index 81453a5a17ad2..4f6e3c4a9a02b 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/grid.phtml @@ -5,19 +5,23 @@ */ ?> <?php -/** @var $block \Magento\Reports\Block\Adminhtml\Grid */ +/** + * @var $block \Magento\Reports\Block\Adminhtml\Grid + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getCollection()) : ?> - <?php if ($block->canDisplayContainer()) : ?> +<?php if ($block->getCollection()): ?> + <?php if ($block->canDisplayContainer()): ?> <div id="<?= $block->escapeHtmlAttr($block->getId()) ?>"> - <?php else : ?> + <?php else: ?> <?= $block->getLayout()->getMessagesBlock()->getGroupedHtml() ?> <?php endif; ?> - <?php if ($block->getStoreSwitcherVisibility() || $block->getDateFilterVisibility()) : ?> + <?php if ($block->getStoreSwitcherVisibility() || $block->getDateFilterVisibility()): ?> <div class="admin__data-grid-header admin__data-grid-toolbar"> <div class="admin__data-grid-header-row"> - <?php if ($block->getDateFilterVisibility()) : ?> - <div class="admin__filter-actions" data-role="filter-form" id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_range')) ?>"> + <?php if ($block->getDateFilterVisibility()): ?> + <div class="admin__filter-actions" data-role="filter-form" + id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_range')) ?>"> <span class="field-row"> <label for="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from')) ?>" class="admin__control-support-text"> @@ -28,7 +32,8 @@ id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from')) ?>" name="report_from" value="<?= $block->escapeHtmlAttr($block->getFilter('report_from')) ?>"> - <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from_advice')) ?>"></span> + <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_from_advice'))?>"> + </span> </span> <span class="field-row"> @@ -41,7 +46,8 @@ id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to')) ?>" name="report_to" value="<?= $block->escapeHtmlAttr($block->getFilter('report_to')) ?>"/> - <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to_advice')) ?>"></span> + <span id="<?= $block->escapeHtmlAttr($block->getSuffixId('period_date_to_advice')) ?>"> + </span> </span> <span class="field-row admin__control-filter"> @@ -49,34 +55,43 @@ class="admin__control-support-text"> <span><?= $block->escapeHtml(__('Show By')) ?>:</span> </label> - <select name="report_period" id="<?= $block->escapeHtmlAttr($block->getSuffixId('report_period')) ?>" class="admin__control-select"> - <?php foreach ($block->getPeriods() as $_value => $_label) : ?> - <option value="<?= $block->escapeHtmlAttr($_value) ?>" <?php if ($block->getFilter('report_period') == $_value) : ?> selected<?php endif; ?>><?= $block->escapeHtml($_label) ?></option> + <select name="report_period" + id="<?= $block->escapeHtmlAttr($block->getSuffixId('report_period')) ?>" + class="admin__control-select"> + <?php foreach ($block->getPeriods() as $_value => $_label): ?> + <option value="<?= $block->escapeHtmlAttr($_value) ?>" + <?php if ($block->getFilter('report_period') == $_value): + ?> selected<?php endif; ?>><?= $block->escapeHtml($_label) ?> + </option> <?php endforeach; ?> </select> <?= $block->getRefreshButtonHtml() ?> </span> - <script> + <?php $scriptString = <<<script + require([ "jquery", "mage/calendar" ], function($){ - $("#<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_range'))) ?>").dateRange({ - dateFormat:"<?= $block->escapeJs($block->escapeHtml($block->getDateFormat())) ?>", - buttonText:"<?= $block->escapeJs($block->escapeHtml(__('Select Date'))) ?>", + $("#{$block->escapeJs($block->getSuffixId('period_date_range'))}").dateRange({ + dateFormat:"{$block->escapeJs($block->getDateFormat())}", + buttonText:"{$block->escapeJs(__('Select Date'))}", from:{ - id:"<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>" + id:"{$block->escapeJs($block->getSuffixId('period_date_from'))}" }, to:{ - id:"<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>" + id:"{$block->escapeJs($block->getSuffixId('period_date_to'))}" } }); }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <?php endif; ?> - <?php if ($block->getChildBlock('grid.export')) : ?> + <?php if ($block->getChildBlock('grid.export')): ?> <?= $block->getChildHtml('grid.export') ?> <?php endif; ?> </div> @@ -88,8 +103,13 @@ </table> </div> </div> - <?php if ($block->canDisplayContainer()) : ?> - <script> + <?php if ($block->canDisplayContainer()): ?> + <?php $useAjax = ''; + if ($block->getUseAjax()): + $useAjax = $block->escapeJs($block->getUseAjax()); + endif; + $scriptString = <<<script + require([ "jquery", "validation", @@ -98,16 +118,24 @@ ], function(jQuery){ //<![CDATA[ - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?> = new varienGrid('<?= $block->escapeJs($block->escapeHtml($block->getId())) ?>', '<?= $block->escapeJs($block->escapeUrl($block->getGridUrl())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNamePage())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameSort())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameDir())) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getVarNameFilter())) ?>'); - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.useAjax = '<?php if ($block->getUseAjax()) : - echo $block->escapeJs($block->escapeHtml($block->getUseAjax())); - endif; ?>'; - <?php if ($block->getDateFilterVisibility()) : ?> - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.doFilterCallback = validateFilterDate; - var period_date_from = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>'); - var period_date_to = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>'); - period_date_from.adviceContainer = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from_advice'))) ?>'); - period_date_to.adviceContainer = $('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to_advice'))) ?>'); + {$block->escapeJs($block->getJsObjectName())} = new varienGrid('{$block->escapeJs($block->getId())}', + '{$block->escapeJs($block->getGridUrl())}', '{$block->escapeJs($block->getVarNamePage())}', + '{$block->escapeJs($block->getVarNameSort())}', '{$block->escapeJs($block->getVarNameDir())}', + '{$block->escapeJs($block->getVarNameFilter())}'); + {$block->escapeJs($block->getJsObjectName())}.useAjax = '{$useAjax}'; + +script; + ?> + <?php if ($block->getDateFilterVisibility()): ?> + <?php $scriptString .= <<<script + + {$block->escapeJs($block->getJsObjectName())}.doFilterCallback = validateFilterDate; + var period_date_from = $('{$block->escapeJs($block->getSuffixId('period_date_from'))}'); + var period_date_to = $('{$block->escapeJs($block->getSuffixId('period_date_to'))}'); + period_date_from.adviceContainer = + $('{$block->escapeJs($block->getSuffixId('period_date_from_advice'))}'); + period_date_to.adviceContainer = + $('{$block->escapeJs($block->getSuffixId('period_date_to_advice'))}'); var validateFilterDate = function() { if (period_date_from && period_date_to) { @@ -121,8 +149,13 @@ return true; } } + +script; + ?> <?php endif;?> - <?php if ($block->getStoreSwitcherVisibility()) : ?> + <?php if ($block->getStoreSwitcherVisibility()): ?> + <?php $scriptString .= <<<script + /* Overwrite function from switcher.phtml widget*/ switchStore = function(obj) { if (obj.options[obj.selectedIndex].getAttribute('website') == 'true') { @@ -136,9 +169,12 @@ if (obj.switchParams) { storeParam += obj.switchParams; } - var formParam = new Array('<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_from'))) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('period_date_to'))) ?>', '<?= $block->escapeJs($block->escapeHtml($block->getSuffixId('report_period'))) ?>'); + var formParam = new Array('{$block->escapeJs($block->getSuffixId('period_date_from'))}', + '{$block->escapeJs($block->getSuffixId('period_date_to'))}', + '{$block->escapeJs($block->getSuffixId('report_period'))}'); var paramURL = ''; - var switchURL = '<?= $block->escapeUrl($block->getAbsoluteGridUrl(['_current' => false])) ?>'.replace(/(store|group|website)\/\d+\//, ''); + var switchURL = '{$block->escapeJs($block->getAbsoluteGridUrl(['_current' => false]))}' + .replace(/(store|group|website)\/\d+\//, ''); for (var i = 0; i < formParam.length; i++) { if ($(formParam[i]).value && $(formParam[i]).name) { @@ -147,10 +183,18 @@ } setLocation(switchURL + storeParam + '?' + paramURL); } + +script; + ?> <?php endif; ?> + <?php $scriptString .= <<<script + //]]> }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml b/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml index 85145454428e2..1d3471a877387 100644 --- a/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml +++ b/app/code/Magento/Reports/view/adminhtml/templates/report/grid/container.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="reports-content"> @@ -11,7 +13,8 @@ <?= $block->getGridHtml() ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'mage/backend/validation', @@ -21,7 +24,7 @@ require([ //<![CDATA[ jQuery('#filter_form').mage('validation', {errorClass: 'mage-error'}); function filterFormSubmit() { - var filters = $$('#filter_form input', '#filter_form select'), + var filters = \$$('#filter_form input', '#filter_form select'), elements = []; for (var i in filters) { @@ -31,7 +34,7 @@ require([ } if (jQuery('#filter_form').valid()) { - setLocation('<?= $block->escapeJs($block->escapeUrl($block->getFilterUrl())) ?>filter/'+ + setLocation('{$block->escapeJs($block->getFilterUrl())}filter/'+ Base64.encode(Form.serializeElements(elements))+'/' ); } @@ -39,4 +42,7 @@ require([ //]]> window.filterFormSubmit = filterFormSubmit; }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Review/Block/Adminhtml/Edit/Tab/Reviews.php b/app/code/Magento/Review/Block/Adminhtml/Edit/Tab/Reviews.php index 15d41fad0a595..bf3c0e5b82ccb 100644 --- a/app/code/Magento/Review/Block/Adminhtml/Edit/Tab/Reviews.php +++ b/app/code/Magento/Review/Block/Adminhtml/Edit/Tab/Reviews.php @@ -13,6 +13,7 @@ * Review tab in adminhtml area. * * @api + * @since 100.4.0 */ class Reviews extends Grid { @@ -20,6 +21,7 @@ class Reviews extends Grid * Hide grid mass action elements. * * @return Reviews + * @since 100.4.0 */ protected function _prepareMassaction() { @@ -30,6 +32,7 @@ protected function _prepareMassaction() * Determine ajax url for grid refresh * * @return string + * @since 100.4.0 */ public function getGridUrl() { diff --git a/app/code/Magento/Review/Block/Customer/ListCustomer.php b/app/code/Magento/Review/Block/Customer/ListCustomer.php index eb67af5780ddb..282421401b674 100644 --- a/app/code/Magento/Review/Block/Customer/ListCustomer.php +++ b/app/code/Magento/Review/Block/Customer/ListCustomer.php @@ -7,12 +7,15 @@ use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\App\ObjectManager; +use Magento\Review\Helper\Data as ReviewHelper; /** * Customer Reviews list block * * @api * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ListCustomer extends \Magento\Customer\Block\Account\Dashboard { @@ -44,6 +47,7 @@ class ListCustomer extends \Magento\Customer\Block\Account\Dashboard * @param \Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory $collectionFactory * @param \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer * @param array $data + * @param ReviewHelper|null $reviewHelper */ public function __construct( \Magento\Framework\View\Element\Template\Context $context, @@ -53,9 +57,11 @@ public function __construct( AccountManagementInterface $customerAccountManagement, \Magento\Review\Model\ResourceModel\Review\Product\CollectionFactory $collectionFactory, \Magento\Customer\Helper\Session\CurrentCustomer $currentCustomer, - array $data = [] + array $data = [], + ?ReviewHelper $reviewHelper = null ) { $this->_collectionFactory = $collectionFactory; + $data['reviewHelper'] = $reviewHelper ?? ObjectManager::getInstance()->get(ReviewHelper::class); parent::__construct( $context, $customerSession, diff --git a/app/code/Magento/Review/Block/Customer/View.php b/app/code/Magento/Review/Block/Customer/View.php index da5aff1f4d2f8..bb322f17b6ce9 100644 --- a/app/code/Magento/Review/Block/Customer/View.php +++ b/app/code/Magento/Review/Block/Customer/View.php @@ -161,7 +161,7 @@ public function getRating() /** * Get rating summary * - * @deprecated + * @deprecated 100.3.3 * @return array */ public function getRatingSummary() diff --git a/app/code/Magento/Review/Block/View.php b/app/code/Magento/Review/Block/View.php index 82a5f37f9b6bf..fcfa11faa169d 100644 --- a/app/code/Magento/Review/Block/View.php +++ b/app/code/Magento/Review/Block/View.php @@ -119,7 +119,7 @@ public function getRating() /** * Retrieve rating summary for current product * - * @deprecated + * @deprecated 100.3.3 * @return string */ public function getRatingSummary() diff --git a/app/code/Magento/Review/Controller/Adminhtml/Rating.php b/app/code/Magento/Review/Controller/Adminhtml/Rating.php index 02649661154af..672c3ed327941 100644 --- a/app/code/Magento/Review/Controller/Adminhtml/Rating.php +++ b/app/code/Magento/Review/Controller/Adminhtml/Rating.php @@ -41,7 +41,7 @@ public function __construct( } /** - * @deprecated Misspelled method + * @deprecated 100.3.0 Misspelled method * @see initEntityId */ protected function initEnityId() diff --git a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php index ab264ef1b6179..1fb7e7df2461f 100644 --- a/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php +++ b/app/code/Magento/Review/Model/ResourceModel/Review/Product/Collection.php @@ -553,6 +553,7 @@ protected function _afterLoad() * Not add store ids to items * * @return $this + * @since 100.2.8 */ protected function prepareStoreId() { diff --git a/app/code/Magento/Review/Model/Review.php b/app/code/Magento/Review/Model/Review.php index 0c581f570ef0c..f2e0997ea8878 100644 --- a/app/code/Magento/Review/Model/Review.php +++ b/app/code/Magento/Review/Model/Review.php @@ -101,7 +101,7 @@ class Review extends \Magento\Framework\Model\AbstractModel implements IdentityI /** * Review model summary * - * @deprecated Summary factory injected as separate property + * @deprecated 100.3.3 Summary factory injected as separate property * @var \Magento\Review\Model\Review\Summary */ protected $_reviewSummary; @@ -216,7 +216,7 @@ public function aggregate() /** * Get entity summary * - * @deprecated + * @deprecated 100.3.3 * @param Product $product * @param int $storeId * @return void @@ -306,7 +306,7 @@ public function afterDeleteCommit() /** * Append review summary data object to product collection * - * @deprecated + * @deprecated 100.3.3 * @param ProductCollection $collection * @return $this * @throws \Magento\Framework\Exception\NoSuchEntityException diff --git a/app/code/Magento/Review/Model/Review/Config.php b/app/code/Magento/Review/Model/Review/Config.php new file mode 100644 index 0000000000000..a3082503b1391 --- /dev/null +++ b/app/code/Magento/Review/Model/Review/Config.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Review\Model\Review; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Provides reviews configuration + */ +class Config +{ + const XML_PATH_REVIEW_ACTIVE = 'catalog/review/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check whether the reviews are enabled or not + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_REVIEW_ACTIVE, + ScopeInterface::SCOPE_STORES + ); + } +} diff --git a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml index 8a2f441e5c4e8..4fc316b000c17 100644 --- a/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml +++ b/app/code/Magento/Review/Test/Mftf/Test/StorefrontVerifySecureURLRedirectReviewTest.xml @@ -28,11 +28,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Review/view/adminhtml/templates/add.phtml b/app/code/Magento/Review/view/adminhtml/templates/add.phtml index 83017eec57013..ec017fa36a33c 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/add.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/add.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> @@ -14,7 +16,8 @@ <div class="hidden" id="formContainer"> <?= $block->getFormHtml() ?> </div> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/mage", @@ -25,4 +28,7 @@ require([ $('#edit_form').mage('form').mage('validation'); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml index bf0cab4c621f5..8bacccef869e2 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/detailed.phtml @@ -4,26 +4,37 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Adminhtml\Rating\Detailed $block */ +/** + * @var \Magento\Review\Block\Adminhtml\Rating\Detailed $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getRating() && $block->getRating()->getSize()) : ?> - <?php foreach ($block->getRating() as $_rating) : ?> +<?php if ($block->getRating() && $block->getRating()->getSize()): ?> + <?php foreach ($block->getRating() as $_rating): ?> <div class="admin__field admin__field-rating"> <label class="admin__field-label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></label> <?php $_iterator = 1; ?> <?php $_options = ($_rating->getRatingOptions()) ? $_rating->getRatingOptions() : $_rating->getOptions() ?> <div class="admin__field-control" data-widget="ratingControl"> - <?php foreach (array_reverse($_options) as $_option) : ?> - <input type="radio" name="ratings[<?= $block->escapeHtmlAttr($_rating->getVoteId() ? $_rating->getVoteId() : $_rating->getId()) ?>]" id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" value="<?= $block->escapeHtmlAttr($_option->getId()) ?>" <?php if ($block->isSelected($_option, $_rating)) : ?>checked="checked"<?php endif; ?> /> - <label for="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>">★</label> + <?php foreach (array_reverse($_options) as $_option): ?> + <input type="radio" + name="ratings[<?= $block->escapeHtmlAttr($_rating->getVoteId() ? $_rating->getVoteId() : + $_rating->getId()) ?>]" + id="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) + ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>" + value="<?= $block->escapeHtmlAttr($_option->getId()) ?>" + <?php if ($block->isSelected($_option, $_rating)): ?>checked="checked"<?php endif; ?> /> + <label for="<?= $block->escapeHtmlAttr($_rating->getRatingCode()) + ?>_<?= $block->escapeHtmlAttr($_option->getValue()) ?>">★</label> <?php $_iterator++ ?> <?php endforeach; ?> </div> </div> <?php endforeach; ?> <input type="hidden" name="validate_rating" class="validate-rating" value="" /> -<script> + <?php $scriptString = <<<script + require([ "jquery", "mage/mage", @@ -33,7 +44,10 @@ require([ $('[data-widget=ratingControl]').ratingControl(); }); -</script> -<?php else : ?> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php else: ?> <?= $block->escapeHtml(__("Rating isn't Available")) ?> <?php endif; ?> diff --git a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml index 1f27db795f8c9..22ddf532b6926 100644 --- a/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml +++ b/app/code/Magento/Review/view/adminhtml/templates/rating/stars/summary.phtml @@ -4,12 +4,20 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Adminhtml\Rating\Summary $block */ +/** + * @var \Magento\Review\Block\Adminhtml\Rating\Summary $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getRatingSummary()->getCount()) : ?> +<?php if ($block->getRatingSummary()->getCount()): ?> <div class="rating-box"> - <div class="rating" style="width:<?= /* @noEscape */ ceil($block->getRatingSummary()->getSum() / ($block->getRatingSummary()->getCount())) ?>%;"></div> + <div class="rating"></div> </div> -<?php else : ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ceil($block->getRatingSummary()->getSum() / + ($block->getRatingSummary()->getCount())) . "%;", + 'div.rating-box div.rating' + ) ?> +<?php else: ?> <?= $block->escapeHtml(__("Rating isn't Available")) ?> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/list.phtml b/app/code/Magento/Review/view/frontend/templates/customer/list.phtml index 11ea987b74cec..6dd7aa575e9df 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/list.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/list.phtml @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Customer\ListCustomer $block */ +/** + * @var \Magento\Review\Block\Customer\ListCustomer $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Review\Helper\Data $reviewHelper */ +$reviewHelper = $block->getData('reviewHelper'); ?> -<?php if ($block->getReviews() && count($block->getReviews())) : ?> +<?php if ($block->getReviews() && count($block->getReviews())): ?> <div class="table-wrapper reviews"> <table class="data table table-reviews" id="my-reviews-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Product Reviews')) ?></caption> @@ -20,26 +26,39 @@ </tr> </thead> <tbody> - <?php foreach ($block->getReviews() as $review) : ?> + <?php foreach ($block->getReviews() as $review): ?> <tr> - <td data-th="<?= $block->escapeHtml(__('Created')) ?>" class="col date"><?= $block->escapeHtml($block->dateFormat($review->getReviewCreatedAt())) ?></td> + <td data-th="<?= $block->escapeHtml(__('Created')) ?>" + class="col date"><?= $block->escapeHtml($block->dateFormat($review->getReviewCreatedAt())) ?> + </td> <td data-th="<?= $block->escapeHtml(__('Product Name')) ?>" class="col item"> <strong class="product-name"> - <a href="<?= $block->escapeUrl($block->getProductUrl($review)) ?>"><?= $block->escapeHtml($review->getName()) ?></a> + <a href="<?= $block->escapeUrl($block->getProductUrl($review)) ?>"> + <?= $block->escapeHtml($review->getName()) ?> + </a> </strong> </td> <td data-th="<?= $block->escapeHtml(__('Rating')) ?>" class="col summary"> - <?php if ($review->getSum()) : ?> + <?php if ($review->getSum()): ?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> - <div class="rating-result" title="<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%"> - <span style="width:<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%;"><span><?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%</span></span> + <div class="rating-result" + title="<?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>%"> + <span> + <span> + <?= /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) ?>% + </span> + </span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ((int)$review->getSum() / (int)$review->getCount()) . "%;", + 'div.rating-summary div.rating-result>span:first-child' + ) ?> <?php endif; ?> </td> <td data-th="<?= $block->escapeHtmlAttr(__('Review')) ?>" class="col description"> - <?= $this->helper(\Magento\Review\Helper\Data::class)->getDetailHtml($review->getDetail()) ?> + <?= $reviewHelper->getDetailHtml($review->getDetail()) ?> </td> <td data-th="<?= $block->escapeHtmlAttr(__('Actions')) ?>" class="col actions"> <a href="<?= $block->escapeUrl($block->getReviewUrl($review)) ?>" class="action more"> @@ -51,12 +70,12 @@ </tbody> </table> </div> - <?php if ($block->getToolbarHtml()) : ?> + <?php if ($block->getToolbarHtml()): ?> <div class="toolbar products-reviews-toolbar bottom"> <?= $block->getToolbarHtml() ?> </div> <?php endif; ?> -<?php else : ?> +<?php else: ?> <div class="message info empty"><span><?= $block->escapeHtml(__('You have submitted no reviews.')) ?></span></div> <?php endif; ?> <div class="actions-toolbar"> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml b/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml index 5cd81a2f17cbc..cf7d53e818c36 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/recent.phtml @@ -4,26 +4,41 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Customer\Recent $block */ +/** + * @var \Magento\Review\Block\Customer\Recent $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getReviews() && count($block->getReviews())) : ?> +<?php if ($block->getReviews() && count($block->getReviews())): ?> <div class="block block-reviews-dashboard"> <div class="block-title"> <strong><?= $block->escapeHtml(__('My Recent Reviews')) ?></strong> - <a class="action view" href="<?= $block->escapeUrl($block->getAllReviewsUrl()) ?>"><span><?= $block->escapeHtml(__('View All')) ?></span></a> + <a class="action view" href="<?= $block->escapeUrl($block->getAllReviewsUrl()) ?>"> + <span><?= $block->escapeHtml(__('View All')) ?></span> + </a> </div> <div class="block-content"> <ol class="items"> - <?php foreach ($block->getReviews() as $_review) : ?> + <?php foreach ($block->getReviews() as $_review): ?> <li class="item"> - <strong class="product-name"><a href="<?= $block->escapeUrl($block->getReviewUrl($_review->getReviewId())) ?>"><?= $block->escapeHtml($_review->getName()) ?></a></strong> - <?php if ($_review->getSum()) : ?> + <strong class="product-name"> + <a href="<?= $block->escapeUrl($block->getReviewUrl($_review->getReviewId())) ?>"> + <?= $block->escapeHtml($_review->getName()) ?> + </a> + </strong> + <?php if ($_review->getSum()): ?> <?php $rating = $_review->getSum() / $_review->getCount() ?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating) ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating) ?>%"><span><?= $block->escapeHtml($rating) ?>%</span></span> + <span> + <span><?= $block->escapeHtml($rating) ?>%</span> + </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:". $block->escapeHtmlAttr($rating) . "%", + 'div.rating-result>span:first-child' + ) ?> </div> <?php endif; ?> </li> diff --git a/app/code/Magento/Review/view/frontend/templates/customer/view.phtml b/app/code/Magento/Review/view/frontend/templates/customer/view.phtml index f92282848b1b7..862a9a466414f 100644 --- a/app/code/Magento/Review/view/frontend/templates/customer/view.phtml +++ b/app/code/Magento/Review/view/frontend/templates/customer/view.phtml @@ -4,11 +4,14 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Customer\View $block */ +/** + * @var \Magento\Review\Block\Customer\View $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $product = $block->getProductData(); ?> -<?php if ($product->getId()) : ?> +<?php if ($product->getId()): ?> <div class="customer-review view"> <div class="product-details"> <div class="product-media"> @@ -19,7 +22,7 @@ $product = $block->getProductData(); </div> <div class="product-info"> <h2 class="product-name"><?= $block->escapeHtml($product->getName()) ?></h2> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <span class="rating-average-label"><?= $block->escapeHtml(__('Average Customer Rating:')) ?></span> <?= $block->getReviewsSummaryHtml($product) ?> <?php endif; ?> @@ -27,21 +30,27 @@ $product = $block->getProductData(); </div> <div class="review-details"> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <div class="title"> <strong><?= $block->escapeHtml(__('Your Review')) ?></strong> </div> <div class="customer-review-rating"> - <?php foreach ($block->getRating() as $_rating) : ?> - <?php if ($_rating->getPercent()) : ?> + <?php foreach ($block->getRating() as $_rating): ?> + <?php if ($_rating->getPercent()): ?> <?php $rating = ceil($_rating->getPercent()) ?> <div class="rating-summary item"> - <span class="rating-label"><span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span></span> + <span class="rating-label"> + <span><?= $block->escapeHtml($_rating->getRatingCode()) ?></span> + </span> <div class="rating-result" title="<?= /* @noEscape */ $rating ?>%"> - <span style="width:<?= /* @noEscape */ $rating ?>%"> + <span> <span><?= /* @noEscape */ $rating ?>%</span> </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ $rating . "%", + 'div.rating-result>span:first-child' + ) ?> </div> <?php endif; ?> <?php endforeach; ?> @@ -49,15 +58,20 @@ $product = $block->getProductData(); <?php endif; ?> <div class="review-title"><?= $block->escapeHtml($block->getReviewData()->getTitle()) ?></div> - <div class="review-content"><?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></div> + <div class="review-content"> + <?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?> + </div> <div class="review-date"> - <?= $block->escapeHtml(__('Submitted on %1', '<time class="date">' . $block->dateFormat($block->getReviewData()->getCreatedAt()) . '</time>'), ['time']) ?> + <?= $block->escapeHtml(__('Submitted on %1', '<time class="date">' . + $block->dateFormat($block->getReviewData()->getCreatedAt()) . '</time>'), ['time']) ?> </div> </div> </div> <div class="actions-toolbar"> <div class="secondary"> - <a class="action back" href="<?= $block->escapeUrl($block->getBackUrl()) ?>"><span><?= $block->escapeHtml(__('Back to My Reviews')) ?></span></a> + <a class="action back" href="<?= $block->escapeUrl($block->getBackUrl()) ?>"> + <span><?= $block->escapeHtml(__('Back to My Reviews')) ?></span> + </a> </div> </div> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/detailed.phtml b/app/code/Magento/Review/view/frontend/templates/detailed.phtml index 7b3b0e2dd6d02..1bd8138f9cdac 100644 --- a/app/code/Magento/Review/view/frontend/templates/detailed.phtml +++ b/app/code/Magento/Review/view/frontend/templates/detailed.phtml @@ -4,21 +4,28 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Rating\Entity\Detailed $block */ +/** + * @var \Magento\Review\Block\Rating\Entity\Detailed $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if (!empty($collection) && $collection->getSize()) : ?> +<?php if (!empty($collection) && $collection->getSize()): ?> <div class="table-wrapper"> <table class="data table ratings review summary"> <caption class="table-caption"><?= $block->escapeHtml(__('Ratings Review Summary')) ?></caption> <tbody> - <?php foreach ($collection as $_rating) : ?> - <?php if ($_rating->getSummary()) : ?> + <?php foreach ($collection as $_rating): ?> + <?php if ($_rating->getSummary()): ?> <tr> <th class="label" scope="row"><?= $block->escapeHtml(__($_rating->getRatingCode())) ?></th> <td class="value"> <div class="rating box"> - <div class="rating" style="width:<?= /* @noEscape */ ceil($_rating->getSummary()) ?>%;"></div> + <div class="rating"/> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ceil($_rating->getSummary()) . "%;", + 'div.rating.box div.rating' + ) ?> </td> </tr> <?php endif; ?> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml index b042b5e92cbac..93afe4a815f61 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary.phtml @@ -4,36 +4,49 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\Product\ReviewRenderer $block */ +/** + * @var \Magento\Review\Block\Product\ReviewRenderer $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->isReviewEnabled() && $block->getReviewsCount()) : ?> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()): ?> <?php $rating = $block->getRatingSummary(); ?> - <div class="product-reviews-summary<?= !$rating ? ' no-rating' : '' ?>" itemprop="aggregateRating" itemscope itemtype="http://schema.org/AggregateRating"> - <?php if ($rating) :?> + <div class="product-reviews-summary<?= !$rating ? ' no-rating' : '' ?>" itemprop="aggregateRating" itemscope + itemtype="http://schema.org/AggregateRating"> + <?php if ($rating):?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating); ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating); ?>%"> + <span> <span> - <span itemprop="ratingValue"><?= $block->escapeHtml($rating); ?></span>% of <span itemprop="bestRating">100</span> + <span itemprop="ratingValue"><?= $block->escapeHtml($rating); ?> + </span>% of <span itemprop="bestRating">100</span> </span> </span> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . $block->escapeHtmlAttr($rating) . "%", + 'div.rating-summary div.rating-result>span:first-child' + ) ?> <?php endif;?> <div class="reviews-actions"> <a class="action view" href="<?= $block->escapeUrl($url) ?>"> <span itemprop="reviewCount"><?= $block->escapeHtml($block->getReviewsCount()) ?></span>  - <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?></span> + <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : + $block->escapeHtml(__('Reviews')) ?> + </span> + </a> + <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> + <?= $block->escapeHtml(__('Add Your Review')) ?> </a> - <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"><?= $block->escapeHtml(__('Add Your Review')) ?></a> </div> </div> -<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()) : ?> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()): ?> <div class="product-reviews-summary empty"> <div class="reviews-actions"> <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> diff --git a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml index 7ea84c952eaaa..20d695195c920 100644 --- a/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml +++ b/app/code/Magento/Review/view/frontend/templates/helper/summary_short.phtml @@ -5,26 +5,38 @@ */ /** @var \Magento\Review\Block\Product\ReviewRenderer $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $url = $block->getReviewsUrl() . '#reviews'; $urlForm = $block->getReviewsUrl() . '#review-form'; ?> -<?php if ($block->isReviewEnabled() && $block->getReviewsCount()) : ?> +<?php if ($block->isReviewEnabled() && $block->getReviewsCount()): ?> <?php $rating = $block->getRatingSummary(); ?> <div class="product-reviews-summary short<?= !$rating ? ' no-rating' : '' ?>"> - <?php if ($rating) :?> + <?php if ($rating):?> <div class="rating-summary"> <span class="label"><span><?= $block->escapeHtml(__('Rating')) ?>:</span></span> - <div class="rating-result" title="<?= $block->escapeHtmlAttr($rating) ?>%"> - <span style="width:<?= $block->escapeHtmlAttr($rating) ?>%"><span><?= $block->escapeHtml($rating) ?>%</span></span> + <div class="rating-result" + id="rating-result_<?= /* @noEscape */ $block->getProduct()->getId() ?>" + title="<?= $block->escapeHtmlAttr($rating) ?>%"> + <span><span><?= $block->escapeHtml($rating) ?>%</span></span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'width:' . $block->escapeHtmlAttr($rating) . '%', + '#rating-result_' . $block->getProduct()->getId() . ' span' + ) ?> </div> <?php endif;?> <div class="reviews-actions"> - <a class="action view" href="<?= $block->escapeUrl($url) ?>"><?= $block->escapeHtml($block->getReviewsCount()) ?> <span><?= ($block->getReviewsCount() == 1) ? $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?></span></a> + <a class="action view" + href="<?= $block->escapeUrl($url) ?>"><?= $block->escapeHtml($block->getReviewsCount()) ?> +  <span><?= ($block->getReviewsCount() == 1) ? + $block->escapeHtml(__('Review')) : $block->escapeHtml(__('Reviews')) ?> + </span> + </a> </div> </div> -<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()) : ?> +<?php elseif ($block->isReviewEnabled() && $block->getDisplayIfEmpty()): ?> <div class="product-reviews-summary short empty"> <div class="reviews-actions"> <a class="action add" href="<?= $block->escapeUrl($urlForm) ?>"> diff --git a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml index d00c310069573..e631f5bc19580 100644 --- a/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml +++ b/app/code/Magento/Review/view/frontend/templates/product/view/list.phtml @@ -5,13 +5,14 @@ */ /** @var Magento\Review\Block\Product\View\ListView $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ $_items = $block->getReviewsCollection()->getItems(); $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; ?> -<?php if (count($_items)) : ?> +<?php if (count($_items)): ?> <div class="block review-list" id="customer-reviews"> - <?php if (!$block->getHideTitle()) : ?> + <?php if (!$block->getHideTitle()): ?> <div class="block-title"> <strong><?= $block->escapeHtml(__('Customer Reviews')) ?></strong> </div> @@ -21,21 +22,31 @@ $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; <?= $block->getChildHtml('toolbar') ?> </div> <ol class="items review-items"> - <?php foreach ($_items as $_review) : ?> + <?php foreach ($_items as $_review): ?> <li class="item review-item" itemscope itemprop="review" itemtype="http://schema.org/Review"> <div class="review-title" itemprop="name"><?= $block->escapeHtml($_review->getTitle()) ?></div> - <?php if (count($_review->getRatingVotes())) : ?> + <?php if (count($_review->getRatingVotes())): ?> <div class="review-ratings"> - <?php foreach ($_review->getRatingVotes() as $_vote) : ?> - <div class="rating-summary item" itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating"> - <span class="label rating-label"><span><?= $block->escapeHtml($_vote->getRatingCode()) ?></span></span> - <div class="rating-result" title="<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> + <?php foreach ($_review->getRatingVotes() as $_vote): ?> + <div class="rating-summary item" + itemprop="reviewRating" itemscope itemtype="http://schema.org/Rating"> + <span class="label rating-label"> + <span><?= $block->escapeHtml($_vote->getRatingCode()) ?></span> + </span> + <div class="rating-result" + id="review_<?= /* @noEscape */ $_review->getReviewId() + ?>_vote_<?= /* @noEscape */ $_vote->getVoteId() ?>" + title="<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> <meta itemprop="worstRating" content = "1"/> <meta itemprop="bestRating" content = "100"/> - <span style="width:<?= $block->escapeHtmlAttr($_vote->getPercent()) ?>%"> + <span> <span itemprop="ratingValue"><?= $block->escapeHtml($_vote->getPercent()) ?>%</span> </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'width:' . $_vote->getPercent() . '%', + 'div#review_' . $_review->getReviewId() . '_vote_' . $_vote->getVoteId() . ' span' + ) ?> </div> <?php endforeach; ?> </div> @@ -46,11 +57,18 @@ $format = $block->getDateFormat() ?: \IntlDateFormatter::SHORT; <div class="review-details"> <p class="review-author"> <span class="review-details-label"><?= $block->escapeHtml(__('Review by')) ?></span> - <strong class="review-details-value" itemprop="author"><?= $block->escapeHtml($_review->getNickname()) ?></strong> + <strong class="review-details-value" + itemprop="author"><?= $block->escapeHtml($_review->getNickname()) ?></strong> </p> <p class="review-date"> <span class="review-details-label"><?= $block->escapeHtml(__('Posted on')) ?></span> - <time class="review-details-value" itemprop="datePublished" datetime="<?= $block->escapeHtmlAttr($block->formatDate($_review->getCreatedAt(), $format)) ?>"><?= $block->escapeHtml($block->formatDate($_review->getCreatedAt(), $format)) ?></time> + <time class="review-details-value" + itemprop="datePublished" + datetime="<?= $block->escapeHtmlAttr($block->formatDate( + $_review->getCreatedAt(), + $format + )) ?>"><?= $block->escapeHtml($block->formatDate($_review->getCreatedAt(), $format)) ?> + </time> </p> </div> </li> diff --git a/app/code/Magento/Review/view/frontend/templates/review.phtml b/app/code/Magento/Review/view/frontend/templates/review.phtml index ea4b4bc42a1ed..04782080fc775 100644 --- a/app/code/Magento/Review/view/frontend/templates/review.phtml +++ b/app/code/Magento/Review/view/frontend/templates/review.phtml @@ -13,7 +13,7 @@ { "*": { "Magento_Review/js/process-reviews": { - "productReviewUrl": "<?= $block->escapeJs($block->escapeUrl($block->getProductReviewUrl())) ?>", + "productReviewUrl": "<?= $block->escapeJs($block->getProductReviewUrl()) ?>", "reviewsTabSelector": "#tab-label-reviews" } } diff --git a/app/code/Magento/Review/view/frontend/templates/view.phtml b/app/code/Magento/Review/view/frontend/templates/view.phtml index 1c3d1942dd2e7..b51353b7df685 100644 --- a/app/code/Magento/Review/view/frontend/templates/view.phtml +++ b/app/code/Magento/Review/view/frontend/templates/view.phtml @@ -4,44 +4,57 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Review\Block\View $block */ +/** + * @var \Magento\Review\Block\View $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->getProductData()->getId()) : ?> +<?php if ($block->getProductData()->getId()): ?> <div class="product-review"> <div class="page-title-wrapper"> <h1><?= $block->escapeHtml(__('Review Details')) ?></h1> </div> <div class="product-img-box"> <a href="<?= $block->escapeUrl($block->getProductData()->getProductUrl()) ?>"> - <?= $block->getImage($block->getProductData(), 'product_base_image', ['class' => 'product-image'])->toHtml() ?> + <?= $block->getImage($block->getProductData(), 'product_base_image', ['class' => 'product-image'])->toHtml() + ?> </a> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <p><?= $block->escapeHtml(__('Average Customer Rating')) ?>:</p> <?= $block->getReviewsSummaryHtml($block->getProductData()) ?> <?php endif; ?> </div> <div class="details"> <h3 class="product-name"><?= $block->escapeHtml($block->getProductData()->getName()) ?></h3> - <?php if ($block->getRating() && $block->getRating()->getSize()) : ?> + <?php if ($block->getRating() && $block->getRating()->getSize()): ?> <h4><?= $block->escapeHtml(__('Product Rating:')) ?></h4> <div class="table-wrapper"> <table class="data-table review-summary-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Product Rating')) ?></caption> - <?php foreach ($block->getRating() as $_rating) : ?> - <?php if ($_rating->getPercent()) : ?> + <?php foreach ($block->getRating() as $_rating): ?> + <?php if ($_rating->getPercent()): ?> <tr> <td class="label"><?= $block->escapeHtml(__($_rating->getRatingCode())) ?></td> <td class="value"> <div class="rating-box"> - <div class="rating" style="width:<?= /* @noEscape */ ceil($_rating->getPercent()) ?>%;"></div> - </div></td> + <div class="rating"/> + </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width:" . /* @noEscape */ ceil($_rating->getPercent()) . "%;", + 'div.rating-box div.rating' + ) ?> + </td> </tr> <?php endif; ?> <?php endforeach; ?> </table> </div> <?php endif; ?> - <p class="date"><?= $block->escapeHtml(__('Product Review (submitted on %1):', $block->dateFormat($block->getReviewData()->getCreatedAt()))) ?></p> + <p class="date"> + <?= $block->escapeHtml( + __('Product Review (submitted on %1):', $block->dateFormat($block->getReviewData()->getCreatedAt())) + ) ?> + </p> <p><?= /* @noEscape */ nl2br($block->escapeHtml($block->getReviewData()->getDetail())) ?></p> </div> <div class="actions"> diff --git a/app/code/Magento/ReviewGraphQl/Mapper/ReviewDataMapper.php b/app/code/Magento/ReviewGraphQl/Mapper/ReviewDataMapper.php new file mode 100644 index 0000000000000..6a06fbfc4102c --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Mapper/ReviewDataMapper.php @@ -0,0 +1,36 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Mapper; + +use Magento\Catalog\Model\Product; +use Magento\Review\Model\Review; + +/** + * Converts the review data from review object to an associative array + */ +class ReviewDataMapper +{ + /** + * Mapping the review data + * + * @param Review $review + * + * @return array + */ + public function map(Review $review): array + { + return [ + 'summary' => $review->getData('title'), + 'text' => $review->getData('detail'), + 'nickname' => $review->getData('nickname'), + 'created_at' => $review->getData('created_at'), + 'sku' => $review->getSku(), + 'model' => $review + ]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/AggregatedReviewsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/AggregatedReviewsDataProvider.php new file mode 100644 index 0000000000000..5412c670b4800 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/AggregatedReviewsDataProvider.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Review\Model\ResourceModel\Review\Collection as ReviewCollection; +use Magento\Review\Model\ResourceModel\Review\Product\Collection as ProductCollection; +use Magento\ReviewGraphQl\Mapper\ReviewDataMapper; + +/** + * Provides aggregated reviews result + * + * The following class prepares the GraphQl endpoints' result for Customer and Product reviews + */ +class AggregatedReviewsDataProvider +{ + /** + * @var ReviewDataMapper + */ + private $reviewDataMapper; + + /** + * @param ReviewDataMapper $reviewDataMapper + */ + public function __construct(ReviewDataMapper $reviewDataMapper) + { + $this->reviewDataMapper = $reviewDataMapper; + } + + /** + * Get reviews result + * + * @param ProductCollection|ReviewCollection $reviewsCollection + * + * @return array + */ + public function getData($reviewsCollection): array + { + if ($reviewsCollection->getPageSize()) { + $maxPages = ceil($reviewsCollection->getSize() / $reviewsCollection->getPageSize()); + } else { + $maxPages = 0; + } + + $currentPage = $reviewsCollection->getCurPage(); + if ($reviewsCollection->getCurPage() > $maxPages && $reviewsCollection->getSize() > 0) { + $currentPage = new GraphQlInputException( + __( + 'currentPage value %1 specified is greater than the number of pages available.', + [$maxPages] + ) + ); + } + + $items = []; + foreach ($reviewsCollection->getItems() as $item) { + $items[] = $this->reviewDataMapper->map($item); + } + + return [ + 'total_count' => $reviewsCollection->getSize(), + 'items' => $items, + 'page_info' => [ + 'page_size' => $reviewsCollection->getPageSize(), + 'current_page' => $currentPage, + 'total_pages' => $maxPages + ] + ]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php new file mode 100644 index 0000000000000..42adc8009c010 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/CustomerReviewsDataProvider.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Review\Model\ResourceModel\Review\Collection as ReviewsCollection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory as ReviewsCollectionFactory; +use Magento\Review\Model\Review; + +/** + * Provides customer reviews + */ +class CustomerReviewsDataProvider +{ + /** + * @var ReviewsCollectionFactory + */ + private $collectionFactory; + + /** + * @param ReviewsCollectionFactory $collectionFactory + */ + public function __construct( + ReviewsCollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * Get customer reviews + * + * @param int $customerId + * @param int $currentPage + * @param int $pageSize + * + * @return ReviewsCollection + */ + public function getData(int $customerId, int $currentPage, int $pageSize): ReviewsCollection + { + /** @var ReviewsCollection $reviewsCollection */ + $reviewsCollection = $this->collectionFactory->create(); + $reviewsCollection + ->addCustomerFilter($customerId) + ->setPageSize($pageSize) + ->setCurPage($currentPage) + ->setDateOrder(); + $reviewsCollection->getSelect()->join( + ['cpe' => $reviewsCollection->getTable('catalog_product_entity')], + 'cpe.entity_id = main_table.entity_pk_value', + ['sku'] + ); + $reviewsCollection->addRateVotes(); + + return $reviewsCollection; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/ProductReviewsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ProductReviewsDataProvider.php new file mode 100644 index 0000000000000..635605f9091ed --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ProductReviewsDataProvider.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory; +use Magento\Review\Model\Review; + +/** + * Provides product reviews + */ +class ProductReviewsDataProvider +{ + /** + * @var CollectionFactory + */ + private $collectionFactory; + + /** + * @param CollectionFactory $collectionFactory + */ + public function __construct( + CollectionFactory $collectionFactory + ) { + $this->collectionFactory = $collectionFactory; + } + + /** + * Get product reviews + * + * @param int $productId + * @param int $currentPage + * @param int $pageSize + * + * @return Collection + */ + public function getData(int $productId, int $currentPage, int $pageSize): Collection + { + /** @var Collection $reviewsCollection */ + $reviewsCollection = $this->collectionFactory->create() + ->addStatusFilter(Review::STATUS_APPROVED) + ->addEntityFilter(Review::ENTITY_PRODUCT_CODE, $productId) + ->setPageSize($pageSize) + ->setCurPage($currentPage) + ->setDateOrder(); + $reviewsCollection->getSelect()->join( + ['cpe' => $reviewsCollection->getTable('catalog_product_entity')], + 'cpe.entity_id = main_table.entity_pk_value', + ['sku'] + ); + $reviewsCollection->addRateVotes(); + + return $reviewsCollection; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/DataProvider/ReviewRatingsDataProvider.php b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ReviewRatingsDataProvider.php new file mode 100644 index 0000000000000..82e0f73b1c774 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/DataProvider/ReviewRatingsDataProvider.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\DataProvider; + +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\Collection as VoteCollection; +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\CollectionFactory as VoteCollectionFactory; + +/** + * Provides rating votes + */ +class ReviewRatingsDataProvider +{ + /** + * @var VoteCollectionFactory + */ + private $voteCollectionFactory; + + /** + * @param VoteCollectionFactory $voteCollectionFactory + */ + public function __construct(VoteCollectionFactory $voteCollectionFactory) + { + $this->voteCollectionFactory = $voteCollectionFactory; + } + + /** + * Providing rating votes + * + * @param int $reviewId + * + * @return array + */ + public function getData(int $reviewId): array + { + /** @var VoteCollection $ratingVotes */ + $ratingVotes = $this->voteCollectionFactory->create(); + $ratingVotes->setReviewFilter($reviewId); + $ratingVotes->addRatingInfo(); + + $data = []; + + foreach ($ratingVotes->getItems() as $ratingVote) { + $data[] = [ + 'name' => $ratingVote->getData('rating_code'), + 'value' => $ratingVote->getData('value') + ]; + } + + return $data; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/CreateProductReview.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/CreateProductReview.php new file mode 100644 index 0000000000000..9b0171c3b700a --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/CreateProductReview.php @@ -0,0 +1,118 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Helper\Data as ReviewHelper; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\ReviewGraphQl\Mapper\ReviewDataMapper; +use Magento\ReviewGraphQl\Model\Review\AddReviewToProduct; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Create product review resolver + */ +class CreateProductReview implements ResolverInterface +{ + /** + * @var ReviewHelper + */ + private $reviewHelper; + + /** + * @var AddReviewToProduct + */ + private $addReviewToProduct; + + /** + * @var ReviewDataMapper + */ + private $reviewDataMapper; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param AddReviewToProduct $addReviewToProduct + * @param ReviewDataMapper $reviewDataMapper + * @param ReviewHelper $reviewHelper + * @param ReviewsConfig $reviewsConfig + */ + public function __construct( + AddReviewToProduct $addReviewToProduct, + ReviewDataMapper $reviewDataMapper, + ReviewHelper $reviewHelper, + ReviewsConfig $reviewsConfig + ) { + + $this->addReviewToProduct = $addReviewToProduct; + $this->reviewDataMapper = $reviewDataMapper; + $this->reviewHelper = $reviewHelper; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolve product review ratings + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array[]|Value|mixed + * + * @throws GraphQlAuthorizationException + * @throws GraphQlNoSuchEntityException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + throw new GraphQlAuthorizationException(__('Creating product reviews are not currently available.')); + } + + $input = $args['input']; + $customerId = null; + + if (false !== $context->getExtensionAttributes()->getIsCustomer()) { + $customerId = (int) $context->getUserId(); + } + + if (!$customerId && !$this->reviewHelper->getIsGuestAllowToWrite()) { + throw new GraphQlAuthorizationException(__('Guest customers aren\'t allowed to add product reviews.')); + } + + $sku = $input['sku']; + $ratings = $input['ratings']; + $data = [ + 'nickname' => $input['nickname'], + 'title' => $input['summary'], + 'detail' => $input['text'], + ]; + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + $review = $this->addReviewToProduct->execute($data, $ratings, $sku, $customerId, (int) $store->getId()); + + return ['review' => $this->reviewDataMapper->map($review)]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php new file mode 100644 index 0000000000000..8c0bca63f8efc --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Customer/Reviews.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Customer; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\ReviewGraphQl\Model\DataProvider\AggregatedReviewsDataProvider; +use Magento\ReviewGraphQl\Model\DataProvider\CustomerReviewsDataProvider; + +/** + * Customer reviews resolver, used by GraphQL endpoints to retrieve customer's reviews + */ +class Reviews implements ResolverInterface +{ + /** + * @var CustomerReviewsDataProvider + */ + private $customerReviewsDataProvider; + + /** + * @var AggregatedReviewsDataProvider + */ + private $aggregatedReviewsDataProvider; + + /** + * @param CustomerReviewsDataProvider $customerReviewsDataProvider + * @param AggregatedReviewsDataProvider $aggregatedReviewsDataProvider + */ + public function __construct( + CustomerReviewsDataProvider $customerReviewsDataProvider, + AggregatedReviewsDataProvider $aggregatedReviewsDataProvider + ) { + $this->customerReviewsDataProvider = $customerReviewsDataProvider; + $this->aggregatedReviewsDataProvider = $aggregatedReviewsDataProvider; + } + + /** + * Resolves the customer reviews + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @throws GraphQlInputException + * @throws GraphQlAuthorizationException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + + $reviewsCollection = $this->customerReviewsDataProvider->getData( + (int) $context->getUserId(), + $args['currentPage'], + $args['pageSize'] + ); + + return $this->aggregatedReviewsDataProvider->getData($reviewsCollection); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/RatingSummary.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/RatingSummary.php new file mode 100644 index 0000000000000..eed5034c59daa --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/RatingSummary.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product; + +use Exception; +use Magento\Catalog\Model\Product; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\Review\Model\Review\SummaryFactory; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Average rating for the product + */ +class RatingSummary implements ResolverInterface +{ + /** + * @var SummaryFactory + */ + private $summaryFactory; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param SummaryFactory $summaryFactory + * @param ReviewsConfig $reviewsConfig + */ + public function __construct( + SummaryFactory $summaryFactory, + ReviewsConfig $reviewsConfig + ) { + $this->summaryFactory = $summaryFactory; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolves the product rating summary + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return float + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ): float { + if (false === $this->reviewsConfig->isEnabled()) { + return 0; + } + + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + /** @var Product $product */ + $product = $value['model']; + + try { + $summary = $this->summaryFactory->create()->setStoreId($store->getId())->load($product->getId()); + + return floatval($summary->getData('rating_summary')); + } catch (Exception $e) { + throw new GraphQlInputException(__('Couldn\'t get the product rating summary.')); + } + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/AverageRating.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/AverageRating.php new file mode 100644 index 0000000000000..2e0d428b47873 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/AverageRating.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product\Review; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\RatingFactory; +use Magento\Review\Model\Review; + +/** + * Review average rating resolver + */ +class AverageRating implements ResolverInterface +{ + /** + * @var RatingFactory + */ + private $ratingFactory; + + /** + * @param RatingFactory $ratingFactory + */ + public function __construct( + RatingFactory $ratingFactory + ) { + $this->ratingFactory = $ratingFactory; + } + + /** + * Resolves review average rating + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return float|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var Review $review */ + $review = $value['model']; + $summary = $this->ratingFactory->create()->getReviewSummary($review->getId()); + $averageRating = $summary->getSum() ?: 0; + + if ($averageRating > 0) { + $averageRating = (float) number_format( + (int) $summary->getSum() / (int) $summary->getCount(), + 2 + ); + } + + return $averageRating; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/RatingBreakdown.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/RatingBreakdown.php new file mode 100644 index 0000000000000..a51bd0420dda9 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Review/RatingBreakdown.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product\Review; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\Review; +use Magento\ReviewGraphQl\Model\DataProvider\ReviewRatingsDataProvider; + +/** + * Review rating resolver + */ +class RatingBreakdown implements ResolverInterface +{ + /** + * @var ReviewRatingsDataProvider + */ + private $reviewRatingsDataProvider; + + /** + * @param ReviewRatingsDataProvider $reviewRatingsDataProvider + */ + public function __construct( + ReviewRatingsDataProvider $reviewRatingsDataProvider + ) { + $this->reviewRatingsDataProvider = $reviewRatingsDataProvider; + } + + /** + * Resolves the rating breakdown + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var Review $review */ + $review = $value['model']; + + return $this->reviewRatingsDataProvider->getData((int) $review->getId()); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/ReviewCount.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/ReviewCount.php new file mode 100644 index 0000000000000..dfa62adf0266e --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/ReviewCount.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Catalog\Model\Product; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Review\Model\Review; +use Magento\Review\Model\Review\Config as ReviewsConfig; + +/** + * Product total review count + */ +class ReviewCount implements ResolverInterface +{ + /** + * @var Review + */ + private $review; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param Review $review + * @param ReviewsConfig $reviewsConfig + */ + public function __construct(Review $review, ReviewsConfig $reviewsConfig) + { + $this->review = $review; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolves the product total reviews + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return int|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + return 0; + } + + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + /** @var Product $product */ + $product = $value['model']; + + return (int) $this->review->getTotalReviews($product->getId(), true); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Reviews.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Reviews.php new file mode 100644 index 0000000000000..72eea5e6b3bd2 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/Product/Reviews.php @@ -0,0 +1,104 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver\Product; + +use Magento\Catalog\Model\Product; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\ReviewGraphQl\Model\DataProvider\AggregatedReviewsDataProvider; +use Magento\ReviewGraphQl\Model\DataProvider\ProductReviewsDataProvider; + +/** + * Product reviews resolver, used by GraphQL endpoints to retrieve product's reviews + */ +class Reviews implements ResolverInterface +{ + /** + * @var ProductReviewsDataProvider + */ + private $productReviewsDataProvider; + + /** + * @var AggregatedReviewsDataProvider + */ + private $aggregatedReviewsDataProvider; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param ProductReviewsDataProvider $productReviewsDataProvider + * @param AggregatedReviewsDataProvider $aggregatedReviewsDataProvider + * @param ReviewsConfig $reviewsConfig + */ + public function __construct( + ProductReviewsDataProvider $productReviewsDataProvider, + AggregatedReviewsDataProvider $aggregatedReviewsDataProvider, + ReviewsConfig $reviewsConfig + ) { + $this->productReviewsDataProvider = $productReviewsDataProvider; + $this->aggregatedReviewsDataProvider = $aggregatedReviewsDataProvider; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolves the product reviews + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + return ['items' => []]; + } + + if (!isset($value['model'])) { + throw new GraphQlInputException(__('Value must contain "model" property.')); + } + + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + + /** @var Product $product */ + $product = $value['model']; + $reviewsCollection = $this->productReviewsDataProvider->getData( + (int) $product->getId(), + $args['currentPage'], + $args['pageSize'] + ); + + return $this->aggregatedReviewsDataProvider->getData($reviewsCollection); + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingValueMetadata.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingValueMetadata.php new file mode 100644 index 0000000000000..e7e6574e7e7ae --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingValueMetadata.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; + +/** + * Product rating value resolver + */ +class ProductReviewRatingValueMetadata implements ResolverInterface +{ + /** + * Resolve product review rating values + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * @return array|Value|mixed + * + * @throws GraphQlInputException + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!isset($value['values'])) { + throw new GraphQlInputException(__('Value must contain "values" property.')); + } + + $ratingOptions = $value['values']; + $data = []; + + foreach ($ratingOptions as $item) { + $data[] = ['value' => $item->getData('value'), 'value_id' => base64_encode($item->getData('option_id'))]; + } + + return $data; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingsMetadata.php b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingsMetadata.php new file mode 100644 index 0000000000000..2cf536255baf7 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Resolver/ProductReviewRatingsMetadata.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ContextInterface; +use Magento\Framework\GraphQl\Query\Resolver\Value; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Review\Model\ResourceModel\Rating\Collection as RatingCollection; +use Magento\Review\Model\ResourceModel\Rating\CollectionFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\Review\Config as ReviewsConfig; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Resolve data review rating metadata + */ +class ProductReviewRatingsMetadata implements ResolverInterface +{ + /** + * @var CollectionFactory + */ + private $ratingCollectionFactory; + + /** + * @var ReviewsConfig + */ + private $reviewsConfig; + + /** + * @param CollectionFactory $ratingCollectionFactory + * @param ReviewsConfig $reviewsConfig + */ + public function __construct(CollectionFactory $ratingCollectionFactory, ReviewsConfig $reviewsConfig) + { + $this->ratingCollectionFactory = $ratingCollectionFactory; + $this->reviewsConfig = $reviewsConfig; + } + + /** + * Resolve product review ratings + * + * @param Field $field + * @param ContextInterface $context + * @param ResolveInfo $info + * @param array|null $value + * @param array|null $args + * + * @return array[]|Value|mixed + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $this->reviewsConfig->isEnabled()) { + return ['items' => []]; + } + + $items = []; + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + + /** @var RatingCollection $ratingCollection */ + $ratingCollection = $this->ratingCollectionFactory->create(); + $ratingCollection->addEntityFilter(Review::ENTITY_PRODUCT_CODE) + ->setStoreFilter($store->getId()) + ->setActiveFilter(true) + ->setPositionOrder() + ->addOptionToItems(); + + foreach ($ratingCollection->getItems() as $item) { + $items[] = [ + 'id' => base64_encode($item->getData('rating_id')), + 'name' => $item->getData('rating_code'), + 'values' => $item->getData('options') + ]; + } + + return ['items' => $items]; + } +} diff --git a/app/code/Magento/ReviewGraphQl/Model/Review/AddReviewToProduct.php b/app/code/Magento/ReviewGraphQl/Model/Review/AddReviewToProduct.php new file mode 100644 index 0000000000000..1b744e717a782 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/Model/Review/AddReviewToProduct.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ReviewGraphQl\Model\Review; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\GraphQl\Exception\GraphQlNoSuchEntityException; +use Magento\Review\Model\Rating; +use Magento\Review\Model\RatingFactory; +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\Collection as OptionVoteCollection; +use Magento\Review\Model\ResourceModel\Rating\Option\Vote\CollectionFactory as OptionVoteCollectionFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\ReviewFactory; + +/** + * Adding a review to specific product + */ +class AddReviewToProduct +{ + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var RatingFactory + */ + private $ratingFactory; + + /** + * @var ReviewFactory + */ + private $reviewFactory; + + /** + * @var OptionVoteCollectionFactory + */ + private $ratingOptionCollectionFactory; + + /** + * @param ProductRepositoryInterface $productRepository + * @param ReviewFactory $reviewFactory + * @param RatingFactory $ratingFactory + * @param OptionVoteCollectionFactory $ratingOptionCollectionFactory + */ + public function __construct( + ProductRepositoryInterface $productRepository, + ReviewFactory $reviewFactory, + RatingFactory $ratingFactory, + OptionVoteCollectionFactory $ratingOptionCollectionFactory + ) { + $this->productRepository = $productRepository; + $this->reviewFactory = $reviewFactory; + $this->ratingFactory = $ratingFactory; + $this->ratingOptionCollectionFactory = $ratingOptionCollectionFactory; + } + + /** + * Add review to product + * + * @param array $data + * @param array $ratings + * @param string $sku + * @param int|null $customerId + * @param int $storeId + * + * @return Review + * + * @throws GraphQlNoSuchEntityException + */ + public function execute(array $data, array $ratings, string $sku, ?int $customerId, int $storeId): Review + { + $review = $this->reviewFactory->create()->setData($data); + $review->unsetData('review_id'); + $productId = $this->getProductIdBySku($sku); + $review->setEntityId($review->getEntityIdByCode(Review::ENTITY_PRODUCT_CODE)) + ->setEntityPkValue($productId) + ->setStatusId(Review::STATUS_PENDING) + ->setCustomerId($customerId) + ->setStoreId($storeId) + ->setStores([$storeId]) + ->save(); + $this->addReviewRatingVotes($ratings, (int) $review->getId(), $customerId, $productId); + $review->aggregate(); + $votesCollection = $this->getReviewRatingVotes((int) $review->getId(), $storeId); + $review->setData('rating_votes', $votesCollection); + $review->setData('sku', $sku); + + return $review; + } + + /** + * Get Product ID + * + * @param string $sku + * + * @return int|null + * + * @throws GraphQlNoSuchEntityException + */ + private function getProductIdBySku(string $sku): ?int + { + try { + $product = $this->productRepository->get($sku, false, null, true); + + return (int) $product->getId(); + } catch (NoSuchEntityException $e) { + throw new GraphQlNoSuchEntityException(__('Could not find a product with SKU "%sku"', ['sku' => $sku])); + } + } + + /** + * Add review rating votes + * + * @param array $ratings + * @param int $reviewId + * @param int|null $customerId + * @param int $productId + * + * @return void + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + private function addReviewRatingVotes(array $ratings, int $reviewId, ?int $customerId, int $productId): void + { + foreach ($ratings as $option) { + $ratingId = $option['id']; + $optionId = $option['value_id']; + /** @var Rating $ratingModel */ + $ratingModel = $this->ratingFactory->create(); + $ratingModel->setRatingId(base64_decode($ratingId)) + ->setReviewId($reviewId) + ->setCustomerId($customerId) + ->addOptionVote(base64_decode($optionId), $productId); + } + } + + /** + * Get review rating votes + * + * @param int $reviewId + * @param int $storeId + * + * @return OptionVoteCollection + */ + private function getReviewRatingVotes(int $reviewId, int $storeId): OptionVoteCollection + { + /** @var OptionVoteCollection $votesCollection */ + $votesCollection = $this->ratingOptionCollectionFactory->create(); + $votesCollection->setReviewFilter($reviewId)->setStoreFilter($storeId)->addRatingInfo($storeId); + + return $votesCollection; + } +} diff --git a/app/code/Magento/ReviewGraphQl/README.md b/app/code/Magento/ReviewGraphQl/README.md new file mode 100644 index 0000000000000..bf9563b87c9b2 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/README.md @@ -0,0 +1,3 @@ +# ReviewGraphQl + +**ReviewGraphQl** provides endpoints for getting and creating the Product reviews by guest and logged in customers. diff --git a/app/code/Magento/ReviewGraphQl/composer.json b/app/code/Magento/ReviewGraphQl/composer.json new file mode 100644 index 0000000000000..819ddefd76213 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/composer.json @@ -0,0 +1,28 @@ +{ + "name": "magento/module-review-graph-ql", + "description": "N/A", + "type": "magento2-module", + "require": { + "php": "~7.3.0||~7.4.0", + "magento/module-catalog": "*", + "magento/module-review": "*", + "magento/module-store": "*", + "magento/framework": "*" + }, + "suggest": { + "magento/module-graph-ql": "*", + "magento/module-graph-ql-cache": "*" + }, + "license": [ + "OSL-3.0", + "AFL-3.0" + ], + "autoload": { + "files": [ + "registration.php" + ], + "psr-4": { + "Magento\\ReviewGraphQl\\": "" + } + } +} diff --git a/app/code/Magento/ReviewGraphQl/etc/module.xml b/app/code/Magento/ReviewGraphQl/etc/module.xml new file mode 100644 index 0000000000000..c098ee5094760 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/etc/module.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:Module/etc/module.xsd"> + <module name="Magento_ReviewGraphQl" > + <sequence> + <module name="Magento_GraphQl"/> + <module name="Magento_Review"/> + <module name="Magento_Store"/> + </sequence> + </module> +</config> diff --git a/app/code/Magento/ReviewGraphQl/etc/schema.graphqls b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls new file mode 100644 index 0000000000000..14b4fc60e8b09 --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/etc/schema.graphqls @@ -0,0 +1,78 @@ +# Copyright © Magento, Inc. All rights reserved. +# See COPYING.txt for license details. + +interface ProductInterface { + rating_summary: Float! @doc(description: "The average of all the ratings given to the product.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\RatingSummary") + review_count: Int! @doc(description: "The total count of all the reviews given to the product.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\ReviewCount") + reviews( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return."), + ): ProductReviews! @doc(description: "The list of products reviews.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\Reviews") +} + +type ProductReviews { + items: [ProductReview]! @doc(description: "An array of product reviews.") + page_info: SearchResultPageInfo! @doc(description: "Metadata for pagination rendering.") +} + +type ProductReview @doc(description: "Details of a product review") { + product: ProductInterface! @doc(description: "Contains details about the reviewed product") @resolver(class: "Magento\\CatalogGraphQl\\Model\\Resolver\\Product") + summary: String! @doc(description: "The summary (title) of the review") + text: String! @doc(description: "The review text.") + nickname: String! @doc(description: "The customer's nickname. Defaults to the customer name, if logged in") + created_at: String! @doc(description: "Date indicating when the review was created.") + average_rating: Float! @doc(description: "The average rating for product review.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\Review\\AverageRating") + ratings_breakdown: [ProductReviewRating!]! @doc(description: "An array of ratings by rating category, such as quality, price, and value") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Product\\Review\\RatingBreakdown") +} + +type ProductReviewRating { + name: String! @doc(description: "The label assigned to an aspect of a product that is being rated, such as quality or price") + value: String! @doc(description: "The rating value given by customer. By default, possible values range from 1 to 5.") +} + +type Query { + productReviewRatingsMetadata: ProductReviewRatingsMetadata! @doc(description: "Retrieves metadata required by clients to render the Reviews section.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\ProductReviewRatingsMetadata") +} + +type ProductReviewRatingsMetadata { + items: [ProductReviewRatingMetadata!]! @doc(description: "List of product reviews sorted by position") +} + +type ProductReviewRatingMetadata { + id: String! @doc(description: "Base64 encoded rating ID.") + name: String! @doc(description: "The label assigned to an aspect of a product that is being rated, such as quality or price") + values: [ProductReviewRatingValueMetadata!]! @doc(description: "List of product review ratings sorted by position.") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\ProductReviewRatingValueMetadata") +} + +type ProductReviewRatingValueMetadata { + value_id: String! @doc(description: "Base 64 encoded rating value id.") + value: String! @doc(description: "e.g Good, Perfect, 3, 4, 5") +} + +type Customer { + reviews( + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once."), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return."), + ): ProductReviews! @doc(description: "Contains the customer's product reviews") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\Customer\\Reviews") +} + +type Mutation { + createProductReview(input: CreateProductReviewInput!): CreateProductReviewOutput! @doc(description: "Creates a product review for the specified SKU") @resolver(class: "Magento\\ReviewGraphQl\\Model\\Resolver\\CreateProductReview") +} + +type CreateProductReviewOutput { + review: ProductReview! @doc(description: "Contains the completed product review") +} + +input CreateProductReviewInput { + sku: String! @doc(description: "The SKU of the reviewed product") + nickname: String! @doc(description: "The customer's nickname. Defaults to the customer name, if logged in") + summary: String! @doc(description: "The summary (title) of the review") + text: String! @doc(description: "The review text.") + ratings: [ProductReviewRatingInput!]! @doc(description: "Ratings details by category. e.g price: 5, quality: 4 etc") +} + +input ProductReviewRatingInput { + id: String! @doc(description: "Base64 encoded rating ID.") + value_id: String! @doc(description: "Base 64 encoded rating value id.") +} diff --git a/app/code/Magento/ReviewGraphQl/registration.php b/app/code/Magento/ReviewGraphQl/registration.php new file mode 100644 index 0000000000000..8fb6535902edf --- /dev/null +++ b/app/code/Magento/ReviewGraphQl/registration.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_ReviewGraphQl', __DIR__); diff --git a/app/code/Magento/Robots/Block/Data.php b/app/code/Magento/Robots/Block/Data.php index 460225d3ed71c..9a28f91de19d9 100644 --- a/app/code/Magento/Robots/Block/Data.php +++ b/app/code/Magento/Robots/Block/Data.php @@ -19,7 +19,7 @@ * Prepares base content for robots.txt and implements Page Cache functionality. * * @api - * @since 100.2.0 + * @since 100.1.0 */ class Data extends AbstractBlock implements IdentityInterface { @@ -60,7 +60,7 @@ public function __construct( * Retrieve base content for robots.txt file * * @return string - * @since 100.2.0 + * @since 100.1.0 */ protected function _toHtml() { @@ -71,7 +71,7 @@ protected function _toHtml() * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.0 */ public function getIdentities() { diff --git a/app/code/Magento/Robots/Model/Config/Value.php b/app/code/Magento/Robots/Model/Config/Value.php index 16a5a486e1078..ab955dadbe33d 100644 --- a/app/code/Magento/Robots/Model/Config/Value.php +++ b/app/code/Magento/Robots/Model/Config/Value.php @@ -23,7 +23,7 @@ * Required to implement Page Cache functionality. * * @api - * @since 100.2.0 + * @since 100.1.0 */ class Value extends ConfigValue implements IdentityInterface { @@ -35,7 +35,7 @@ class Value extends ConfigValue implements IdentityInterface /** * @inheritdoc * - * @since 100.2.0 + * @since 100.1.0 */ protected $_cacheTag = [self::CACHE_TAG]; @@ -86,7 +86,7 @@ public function __construct( * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.0 */ public function getIdentities() { diff --git a/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php new file mode 100644 index 0000000000000..a8a9f78df7f28 --- /dev/null +++ b/app/code/Magento/Rule/Test/Mftf/Helper/RuleHelper.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Rule\Test\Mftf\Helper; + +use Facebook\WebDriver\Remote\RemoteWebDriver as FacebookWebDriver; +use Facebook\WebDriver\WebDriverBy; +use Magento\FunctionalTestingFramework\Helper\Helper; +use Magento\FunctionalTestingFramework\Module\MagentoWebDriver; + +/** + * Class for MFTF helpers for CatalogRule module. + */ +class RuleHelper extends Helper +{ + /** + * Delete all Catalog Price Rules obe by one. + * + * @param string $emptyRow + * @param string $modalAceptButton + * @param string $deleteButton + * @param string $successMessageContainer + * @param string $successMessage + * + * @return void + */ + public function deleteAllRulesOneByOne( + string $firstNotEmptyRow, + string $modalAcceptButton, + string $deleteButton, + string $successMessageContainer, + string $successMessage + ): void { + try { + /** @var MagentoWebDriver $webDriver */ + $magentoWebDriver = $this->getModule('\Magento\FunctionalTestingFramework\Module\MagentoWebDriver'); + /** @var FacebookWebDriver $webDriver */ + $webDriver = $magentoWebDriver->webDriver; + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + while (!empty($rows)) { + $rows[0]->click(); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->click($deleteButton); + $magentoWebDriver->waitForPageLoad(30); + $magentoWebDriver->waitForElementVisible($modalAcceptButton, 10); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->click($modalAcceptButton); + $magentoWebDriver->waitForPageLoad(60); + $magentoWebDriver->waitForLoadingMaskToDisappear(); + $magentoWebDriver->waitForElementVisible($successMessageContainer, 10); + $magentoWebDriver->see($successMessage, $successMessageContainer); + $rows = $webDriver->findElements(WebDriverBy::cssSelector($firstNotEmptyRow)); + } + } catch (\Exception $e) { + $this->fail($e->getMessage()); + } + } +} diff --git a/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php b/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php index ac400206b8a2f..8f9ab43313968 100644 --- a/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php +++ b/app/code/Magento/Sales/Api/Data/OrderPaymentInterface.php @@ -1047,6 +1047,7 @@ public function setCcTransId($id); * * @param string[] $additionalInformation * @return $this + * @since 102.1.0 */ public function setAdditionalInformation($additionalInformation); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php index 87c15e474d11f..10d58e1f312c4 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Items/Column/Name.php @@ -5,7 +5,9 @@ */ namespace Magento\Sales\Block\Adminhtml\Items\Column; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Filter\TruncateFilter\Result; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Sales Order items name column renderer @@ -15,6 +17,28 @@ */ class Name extends \Magento\Sales\Block\Adminhtml\Items\Column\DefaultColumn { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry + * @param \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration + * @param \Magento\Framework\Registry $registry + * @param \Magento\Catalog\Model\Product\OptionFactory $optionFactory + * @param array $data + * @param CatalogHelper|null $catalogHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\CatalogInventory\Api\StockRegistryInterface $stockRegistry, + \Magento\CatalogInventory\Api\StockConfigurationInterface $stockConfiguration, + \Magento\Framework\Registry $registry, + \Magento\Catalog\Model\Product\OptionFactory $optionFactory, + array $data = [], + ?CatalogHelper $catalogHelper = null + ) { + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); + parent::__construct($context, $stockRegistry, $stockConfiguration, $registry, $optionFactory, $data); + } + /** * @var Result */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php b/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php index b9aff07cc96fd..e45405714956f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/AbstractOrder.php @@ -6,6 +6,9 @@ namespace Magento\Sales\Block\Adminhtml\Order; use Magento\Sales\Model\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Data as ShippingHelper; +use Magento\Tax\Helper\Data as TaxHelper; /** * Adminhtml order abstract block @@ -35,15 +38,21 @@ class AbstractOrder extends \Magento\Backend\Block\Widget * @param \Magento\Framework\Registry $registry * @param \Magento\Sales\Helper\Admin $adminHelper * @param array $data + * @param ShippingHelper|null $shippingHelper + * @param TaxHelper|null $taxHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, \Magento\Sales\Helper\Admin $adminHelper, - array $data = [] + array $data = [], + ?ShippingHelper $shippingHelper = null, + ?TaxHelper $taxHelper = null ) { $this->_adminHelper = $adminHelper; $this->_coreRegistry = $registry; + $data['shippingHelper'] = $shippingHelper ?? ObjectManager::getInstance()->get(ShippingHelper::class); + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php index b314ee24c3e27..6a1c16cd5d73a 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Giftmessage.php @@ -50,6 +50,7 @@ public function __construct( ) { $this->_messageHelper = $messageHelper; $this->_giftMessageSave = $giftMessageSave; + $data['giftMessageHelper'] = $messageHelper; parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php index 8a427a30a6c7a..8ec07f9765204 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Items/Grid.php @@ -8,9 +8,11 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; use Magento\CatalogInventory\Api\StockRegistryInterface; use Magento\CatalogInventory\Api\StockStateInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Pricing\PriceCurrencyInterface; use Magento\Framework\Session\SessionManagerInterface; use Magento\Quote\Model\Quote\Item; +use Magento\Catalog\Helper\Data as CatalogHelper; /** * Adminhtml sales order create items grid block @@ -85,6 +87,7 @@ class Grid extends \Magento\Sales\Block\Adminhtml\Order\Create\AbstractCreate * @param StockRegistryInterface $stockRegistry * @param StockStateInterface $stockState * @param array $data + * @param CatalogHelper|null $catalogHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -99,7 +102,8 @@ public function __construct( \Magento\GiftMessage\Helper\Message $messageHelper, StockRegistryInterface $stockRegistry, StockStateInterface $stockState, - array $data = [] + array $data = [], + ?CatalogHelper $catalogHelper = null ) { $this->_messageHelper = $messageHelper; $this->_wishlistFactory = $wishlistFactory; @@ -108,6 +112,7 @@ public function __construct( $this->_taxData = $taxData; $this->stockRegistry = $stockRegistry; $this->stockState = $stockState; + $data['catalogHelper'] = $catalogHelper ?? ObjectManager::getInstance()->get(CatalogHelper::class); parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $data); } @@ -428,7 +433,7 @@ protected function _getTierPriceInfo($prices) * @param Item $item * @return string * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function getCustomOptions(Item $item) { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php index 271b5b7afa1c9..8c1ef5f56dac2 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Search/Grid/Renderer/Product.php @@ -5,6 +5,10 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Search\Grid\Renderer; +use Magento\Backend\Block\Context; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Adminhtml sales create order product search grid product name column renderer * @@ -12,6 +16,22 @@ */ class Product extends \Magento\Backend\Block\Widget\Grid\Column\Renderer\Text { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + */ + public function __construct(Context $context, array $data = [], ?SecureHtmlRenderer $secureRenderer = null) + { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Render product name to add Configure link * @@ -28,10 +48,14 @@ public function render(\Magento\Framework\DataObject $row) $row->getId() ) : 'disabled="disabled"'; return sprintf( - '<a href="javascript:void(0)" class="action-configure %s" %s>%s</a>', + '<a href="#" id="search-grid-product-' . $row->getId() . '" class="action-configure %s" %s>%s</a>', $style, $prodAttributes, __('Configure') + ) . $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault()', + 'a#search-grid-product-' . $row->getId() ) . $rendered; } } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php index 1fd1c28c20727..0b926e8415e41 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/Method/Form.php @@ -52,6 +52,7 @@ public function __construct( array $data = [] ) { $this->_taxData = $taxData; + $data['taxHelper'] = $this->_taxData; parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php index a927b7177294a..77765b242001f 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Sidebar/Cart.php @@ -66,6 +66,7 @@ public function getItemCollection() /** * @inheritdoc + * @since 102.0.1 */ public function getItemPrice(Product $product) { @@ -150,6 +151,7 @@ private function getCartItemCustomPrice(Product $product): ?float /** * @inheritdoc + * @since 102.0.4 */ public function getItemCount() { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php index 207a4eca60213..165875955baa2 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Create/Totals/Tax.php @@ -5,6 +5,10 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Create\Totals; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Tax\Helper\Data as TaxHelper; + /** * Tax Total Row Renderer * @@ -13,6 +17,30 @@ */ class Tax extends \Magento\Sales\Block\Adminhtml\Order\Create\Totals\DefaultTotals { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Backend\Model\Session\Quote $sessionQuote + * @param \Magento\Sales\Model\AdminOrder\Create $orderCreate + * @param PriceCurrencyInterface $priceCurrency + * @param \Magento\Sales\Helper\Data $salesData + * @param \Magento\Sales\Model\Config $salesConfig + * @param array $data + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Backend\Model\Session\Quote $sessionQuote, + \Magento\Sales\Model\AdminOrder\Create $orderCreate, + PriceCurrencyInterface $priceCurrency, + \Magento\Sales\Helper\Data $salesData, + \Magento\Sales\Model\Config $salesConfig, + array $data = [], + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct($context, $sessionQuote, $orderCreate, $priceCurrency, $salesData, $salesConfig, $data); + } + /** * Template * diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php index a7649fecaf2bb..781fb3b7501b5 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Creditmemo/Create/Adjustments.php @@ -76,6 +76,7 @@ public function initTotals() * @param null|float $value * * @return string + * @since 102.1.0 */ public function formatValue($value) { diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php index 261f4b0cfd12a..51d2bfc6326ed 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Details.php @@ -5,12 +5,29 @@ */ namespace Magento\Sales\Block\Adminhtml\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\GiftMessage\Helper\Message as GiftMessageHelper; + /** - * Class Details - * @package Magento\Sales\Block\Adminhtml\Order + * Order Details */ class Details extends \Magento\Framework\View\Element\Template { + /** + * @param Template\Context $context + * @param array $data + * @param Message|null $giftMessageHelper + */ + public function __construct( + Template\Context $context, + array $data = [], + ?GiftMessageHelper $giftMessageHelper = null + ) { + $data['giftMessageHelper'] = $giftMessageHelper ?? ObjectManager::getInstance()->get(GiftMessageHelper::class); + parent::__construct($context, $data); + } + /** * @var string */ diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php index 3eb6cce37f567..3f41eb5ba7d8e 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Invoice/Create/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Invoice\Create; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; + /** * Adminhtml invoice create form * @@ -14,6 +17,24 @@ */ class Form extends \Magento\Sales\Block\Adminhtml\Order\AbstractOrder { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Sales\Helper\Admin $adminHelper + * @param array $data + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Sales\Helper\Admin $adminHelper, + array $data = [], + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct($context, $registry, $adminHelper, $data); + } + /** * Retrieve invoice order * diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php index c4ce48d162c2c..33e5250d27d26 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Totalbar.php @@ -8,7 +8,7 @@ /** * Adminhtml creditmemo bar * - * @deprecated + * @deprecated 101.0.6 * @api * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php index a9f7bf3516517..6cd2c53f894f8 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals.php @@ -42,13 +42,23 @@ protected function _initTotals() 'area' => 'footer', ] ); - $this->_totals['due'] = new \Magento\Framework\DataObject( + $code = 'due'; + $label = 'Total Due'; + $value = $this->getSource()->getTotalDue(); + $baseValue = $this->getSource()->getBaseTotalDue(); + if ($this->getSource()->getTotalCanceled() > 0 && $this->getSource()->getBaseTotalCanceled() > 0) { + $code = 'canceled'; + $label = 'Total Canceled'; + $value = $this->getSource()->getTotalCanceled(); + $baseValue = $this->getSource()->getBaseTotalCanceled(); + } + $this->_totals[$code] = new \Magento\Framework\DataObject( [ 'code' => 'due', 'strong' => true, - 'value' => $this->getSource()->getTotalDue(), - 'base_value' => $this->getSource()->getBaseTotalDue(), - 'label' => __('Total Due'), + 'value' => $value, + 'base_value' => $baseValue, + 'label' => __($label), 'area' => 'footer', ] ); diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php index 4b0969598fdcd..e923b006a0ac6 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/Totals/Tax.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Block\Adminhtml\Order\Totals; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; + /** * Adminhtml order tax totals block * @@ -50,6 +53,7 @@ class Tax extends \Magento\Tax\Block\Sales\Order\Tax * @param \Magento\Tax\Model\Sales\Order\TaxFactory $taxOrderFactory * @param \Magento\Sales\Helper\Admin $salesAdminHelper * @param array $data + * @param Random $randomHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -58,12 +62,15 @@ public function __construct( \Magento\Tax\Model\Calculation $taxCalculation, \Magento\Tax\Model\Sales\Order\TaxFactory $taxOrderFactory, \Magento\Sales\Helper\Admin $salesAdminHelper, - array $data = [] + array $data = [], + ?Random $randomHelper = null ) { $this->_taxHelper = $taxHelper; $this->_taxCalculation = $taxCalculation; $this->_taxOrderFactory = $taxOrderFactory; $this->_salesAdminHelper = $salesAdminHelper; + $data['taxHelper'] = $this->_taxHelper; + $data['randomHelper'] = $randomHelper ?? ObjectManager::getInstance()->get(Random::class); parent::__construct($context, $taxConfig, $data); } diff --git a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php index 598a3e226a879..22f61d3583faa 100644 --- a/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php +++ b/app/code/Magento/Sales/Block/Adminhtml/Order/View/Info.php @@ -305,7 +305,7 @@ public function getFormattedAddress(Address $address) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function getChildHtml($alias = '', $useCache = true) { diff --git a/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php b/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php index 0a1e87e5e0a27..bfb668a674095 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Creditmemo/Items.php @@ -71,6 +71,7 @@ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $r * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { @@ -96,6 +97,7 @@ public function getOrder() * For legacy custom email templates it can pass as an object. * * @return CreditmemoInterface|null + * @since 102.1.0 */ public function getCreditmemo() { diff --git a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php index cc2b197ab0eb2..7b5389a54e878 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Invoice/Items.php @@ -71,6 +71,7 @@ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $r * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { @@ -96,6 +97,7 @@ public function getOrder() * For legacy custom email templates it can pass as an object. * * @return InvoiceInterface|null + * @since 102.1.0 */ public function getInvoice() { diff --git a/app/code/Magento/Sales/Block/Order/Email/Items.php b/app/code/Magento/Sales/Block/Order/Email/Items.php index e11981285f04f..8a7256d1f1175 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items.php @@ -52,6 +52,7 @@ public function __construct( * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { diff --git a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php index 064405daf89a8..cbb79f188f231 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php +++ b/app/code/Magento/Sales/Block/Order/Email/Items/DefaultItems.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Block\Order\Email\Items; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\View\Element\Template; use Magento\Sales\Model\Order\Creditmemo\Item as CreditmemoItem; use Magento\Sales\Model\Order\Invoice\Item as InvoiceItem; use Magento\Sales\Model\Order\Item as OrderItem; @@ -16,7 +20,7 @@ * @author Magento Core Team <core@magentocommerce.com> * @since 100.0.2 */ -class DefaultItems extends \Magento\Framework\View\Element\Template +class DefaultItems extends Template { /** * Retrieve current order model instance @@ -92,6 +96,7 @@ public function getSku($item) * Return product additional information block * * @return \Magento\Framework\View\Element\AbstractBlock + * @throws LocalizedException */ public function getProductAdditionalInformationBlock() { @@ -103,10 +108,13 @@ public function getProductAdditionalInformationBlock() * * @param OrderItem|InvoiceItem|CreditmemoItem $item * @return string + * @throws LocalizedException */ public function getItemPrice($item) { $block = $this->getLayout()->getBlock('item_price'); + $item->setRowTotal((float) $item->getPrice() * (float) $this->getItem()->getQty()); + $item->setBaseRowTotal((float) $item->getBasePrice() * (float) $this->getItem()->getQty()); $block->setItem($item); return $block->toHtml(); } diff --git a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php index 1f9b353180fd9..db7fa6b03715a 100644 --- a/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php +++ b/app/code/Magento/Sales/Block/Order/Email/Shipment/Items.php @@ -71,6 +71,7 @@ protected function _prepareItem(\Magento\Framework\View\Element\AbstractBlock $r * For legacy custom email templates it can pass as an object. * * @return OrderInterface|null + * @since 102.1.0 */ public function getOrder() { @@ -96,6 +97,7 @@ public function getOrder() * For legacy custom email templates it can pass as an object. * * @return ShipmentInterface|null + * @since 102.1.0 */ public function getShipment() { diff --git a/app/code/Magento/Sales/Block/Order/History.php b/app/code/Magento/Sales/Block/Order/History.php index 09300424212fe..98b1ccfc5b2e8 100644 --- a/app/code/Magento/Sales/Block/Order/History.php +++ b/app/code/Magento/Sales/Block/Order/History.php @@ -158,7 +158,7 @@ public function getViewUrl($order) * * @param object $order * @return string - * @deprecated Action does not exist + * @deprecated 102.0.3 Action does not exist * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getTrackUrl($order) @@ -193,6 +193,7 @@ public function getBackUrl() * Get message for no orders. * * @return \Magento\Framework\Phrase + * @since 102.1.0 */ public function getEmptyOrdersMessage() { diff --git a/app/code/Magento/Sales/Block/Order/PrintShipment.php b/app/code/Magento/Sales/Block/Order/PrintShipment.php index 0006a38f0f1ce..039bf2c79e78b 100644 --- a/app/code/Magento/Sales/Block/Order/PrintShipment.php +++ b/app/code/Magento/Sales/Block/Order/PrintShipment.php @@ -88,7 +88,7 @@ public function getOrder() * Disable pager for printing page * * @return bool - * @since 100.2.0 + * @since 100.1.9 */ public function isPagerDisplayed() { @@ -99,7 +99,7 @@ public function isPagerDisplayed() * Get order items * * @return \Magento\Framework\DataObject[] - * @since 100.2.0 + * @since 100.1.9 */ public function getItems() { diff --git a/app/code/Magento/Sales/Block/Order/Recent.php b/app/code/Magento/Sales/Block/Order/Recent.php index 79119c1851347..934f1b5efdcdd 100644 --- a/app/code/Magento/Sales/Block/Order/Recent.php +++ b/app/code/Magento/Sales/Block/Order/Recent.php @@ -120,7 +120,7 @@ public function getViewUrl($order) * * @param object $order * @return string - * @deprecated Action does not exist + * @deprecated 102.0.3 Action does not exist * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function getTrackUrl($order) diff --git a/app/code/Magento/Sales/Block/Order/View.php b/app/code/Magento/Sales/Block/Order/View.php index 03d1340e0f690..eef13fd47bf94 100644 --- a/app/code/Magento/Sales/Block/Order/View.php +++ b/app/code/Magento/Sales/Block/Order/View.php @@ -29,7 +29,7 @@ class View extends \Magento\Framework\View\Element\Template /** * @var \Magento\Framework\App\Http\Context - * @since 100.2.0 + * @since 101.0.0 */ protected $httpContext; diff --git a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php index 062ad78e5001d..5eb485e262193 100644 --- a/app/code/Magento/Sales/Controller/AbstractController/Reorder.php +++ b/app/code/Magento/Sales/Controller/AbstractController/Reorder.php @@ -8,6 +8,7 @@ namespace Magento\Sales\Controller\AbstractController; +use Magento\Checkout\Model\Session as CheckoutSession; use Magento\Framework\App\Action; use Magento\Framework\App\Action\HttpPostActionInterface; use Magento\Framework\App\ObjectManager; @@ -35,6 +36,11 @@ abstract class Reorder extends Action\Action implements HttpPostActionInterface */ private $reorder; + /** + * @var CheckoutSession + */ + private $checkoutSession; + /** * Constructor * @@ -43,6 +49,7 @@ abstract class Reorder extends Action\Action implements HttpPostActionInterface * @param Registry $registry * @param ReorderHelper|null $reorderHelper * @param \Magento\Sales\Model\Reorder\Reorder|null $reorder + * @param CheckoutSession|null $checkoutSession * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -50,12 +57,14 @@ public function __construct( OrderLoaderInterface $orderLoader, Registry $registry, ReorderHelper $reorderHelper = null, - \Magento\Sales\Model\Reorder\Reorder $reorder = null + \Magento\Sales\Model\Reorder\Reorder $reorder = null, + CheckoutSession $checkoutSession = null ) { $this->orderLoader = $orderLoader; $this->_coreRegistry = $registry; parent::__construct($context); $this->reorder = $reorder ?: ObjectManager::getInstance()->get(\Magento\Sales\Model\Reorder\Reorder::class); + $this->checkoutSession = $checkoutSession ?: ObjectManager::getInstance()->get(CheckoutSession::class); } /** @@ -81,6 +90,10 @@ public function execute() return $resultRedirect->setPath('checkout/cart'); } + // Set quote id for guest session: \Magento\Quote\Api\CartRepositoryInterface::save doesn't set quote id + // to session for guest customer, as it does \Magento\Checkout\Model\Cart::save which is deprecated. + $this->checkoutSession->setQuoteId($reorderOutput->getCart()->getId()); + $errors = $reorderOutput->getErrors(); if (!empty($errors)) { $useNotice = $this->_objectManager->get(\Magento\Checkout\Model\Session::class)->getUseNotice(true); diff --git a/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php b/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php index c6b45f282debc..1f75008897102 100644 --- a/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php +++ b/app/code/Magento/Sales/Controller/Adminhtml/Order/AbstractMassAction.php @@ -14,7 +14,7 @@ /** * Class AbstractMassStatus - * @deprecated 100.2.0 + * @deprecated 101.0.0 * Never extend from this action. Implement mass-action logic in the "execute" method of your controller. */ abstract class AbstractMassAction extends \Magento\Backend\App\Action diff --git a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php index fc4e238d47c99..069b2783076d9 100644 --- a/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php +++ b/app/code/Magento/Sales/Controller/Download/DownloadCustomOption.php @@ -33,7 +33,7 @@ class DownloadCustomOption extends \Magento\Framework\App\Action\Action implemen /** * @var \Magento\Framework\Unserialize\Unserialize - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $unserialize; diff --git a/app/code/Magento/Sales/Model/AdminOrder/Create.php b/app/code/Magento/Sales/Model/AdminOrder/Create.php index 67a533ea88550..d5a94a4dd1fcf 100644 --- a/app/code/Magento/Sales/Model/AdminOrder/Create.php +++ b/app/code/Magento/Sales/Model/AdminOrder/Create.php @@ -663,6 +663,14 @@ public function initFromOrderItem(\Magento\Sales\Model\Order\Item $orderItem, $q if (is_numeric($qty)) { $buyRequest->setQty($qty); } + $productOptions = $orderItem->getProductOptions(); + if ($productOptions !== null && !empty($productOptions['options'])) { + $formattedOptions = []; + foreach ($productOptions['options'] as $option) { + $formattedOptions[$option['option_id']] = $option['option_value']; + } + $buyRequest->setData('options', $formattedOptions); + } $item = $this->getQuote()->addProduct($product, $buyRequest); if (is_string($item)) { return $item; @@ -1151,7 +1159,7 @@ public function updateQuoteItems($items) * @return array * @throws \Magento\Framework\Exception\LocalizedException * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additionalOptions) { @@ -1221,7 +1229,7 @@ protected function _parseOptions(\Magento\Quote\Model\Quote\Item $item, $additio * @param array $options * @return $this * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function _assignOptionsToItem(\Magento\Quote\Model\Quote\Item $item, $options) { @@ -1369,7 +1377,6 @@ protected function _setQuoteAddress(\Magento\Quote\Model\Quote\Address $address, $data = isset($data['region']) && is_array($data['region']) ? array_merge($data, $data['region']) : $data; $addressForm = $this->_metadataFormFactory->create( - AddressMetadataInterface::ENTITY_TYPE_ADDRESS, 'adminhtml_customer_address', $data, diff --git a/app/code/Magento/Sales/Model/Increment.php b/app/code/Magento/Sales/Model/Increment.php index 75ff1ee044a95..813b3dcc40a7a 100644 --- a/app/code/Magento/Sales/Model/Increment.php +++ b/app/code/Magento/Sales/Model/Increment.php @@ -9,7 +9,7 @@ /** * Class Increment - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class Increment { diff --git a/app/code/Magento/Sales/Model/Order.php b/app/code/Magento/Sales/Model/Order.php index 598b204a33097..0af42b0a99d09 100644 --- a/app/code/Magento/Sales/Model/Order.php +++ b/app/code/Magento/Sales/Model/Order.php @@ -193,7 +193,7 @@ class Order extends AbstractModel implements EntityInterface, OrderInterface /** * @var \Magento\Catalog\Api\ProductRepositoryInterface - * @deprecated 100.1.7 Remove unused dependency. + * @deprecated 100.1.0 Remove unused dependency. */ protected $productRepository; @@ -716,7 +716,7 @@ private function canCreditmemoForZeroTotal($totalRefunded) $hasDueAmount = $this->canInvoice() && ($checkAmtTotalPaid); //case when paid amount is refunded and order has creditmemo created $creditmemos = ($this->getCreditmemosCollection() === false) ? - true : (count($this->getCreditmemosCollection()) > 0); + true : ($this->_memoCollectionFactory->create()->setOrderFilter($this)->getTotalCount() > 0); $paidAmtIsRefunded = $this->getTotalRefunded() == $totalPaid && $creditmemos; if (($hasDueAmount || $paidAmtIsRefunded) || (!$checkAmtTotalPaid && @@ -1076,6 +1076,7 @@ public function setState($state) * Retrieve frontend label of order status * * @return string + * @since 102.0.1 */ public function getFrontendStatusLabel() { @@ -1115,7 +1116,7 @@ public function addStatusToHistory($status, $comment = '', $isCustomerNotified = * @param string $comment * @param bool|string $status * @return OrderStatusHistoryInterface - * @deprecated + * @deprecated 101.0.5 * @see addCommentToStatusHistory */ public function addStatusHistoryComment($comment, $status = false) @@ -1132,6 +1133,7 @@ public function addStatusHistoryComment($comment, $status = false) * @param bool|string $status * @param bool $isVisibleOnFront * @return OrderStatusHistoryInterface + * @since 101.0.5 */ public function addCommentToStatusHistory($comment, $status = false, $isVisibleOnFront = false) { @@ -1816,7 +1818,7 @@ public function getTotalDue() $total = $this->priceCurrency->round($total); return max($total, 0); } - + /** * Retrieve order total due value * diff --git a/app/code/Magento/Sales/Model/Order/Address.php b/app/code/Magento/Sales/Model/Order/Address.php index 9b8f4e79c23fa..0fd4555238ed5 100644 --- a/app/code/Magento/Sales/Model/Order/Address.php +++ b/app/code/Magento/Sales/Model/Order/Address.php @@ -732,6 +732,7 @@ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderAddressExten /** * @inheritdoc + * @since 102.0.3 */ public function beforeSave() { diff --git a/app/code/Magento/Sales/Model/Order/AddressRepository.php b/app/code/Magento/Sales/Model/Order/AddressRepository.php index deeeb16b7714c..1a700826dbc3f 100644 --- a/app/code/Magento/Sales/Model/Order/AddressRepository.php +++ b/app/code/Magento/Sales/Model/Order/AddressRepository.php @@ -240,7 +240,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Config.php b/app/code/Magento/Sales/Model/Order/Config.php index 92681f3ecf181..32b9298be2b5f 100644 --- a/app/code/Magento/Sales/Model/Order/Config.php +++ b/app/code/Magento/Sales/Model/Order/Config.php @@ -157,6 +157,7 @@ public function getStatusLabel($code) * * @param string|null $code * @return string|null + * @since 102.0.1 */ public function getStatusFrontendLabel(?string $code): ?string { @@ -307,7 +308,7 @@ protected function _getStatuses($visibility) * @param string $state * @param string $status * @return \Magento\Framework\Phrase|string - * @since 100.2.0 + * @since 101.0.0 */ public function getStateLabelByStateAndStatus($state, $status) { diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php index 1278d156ba869..80053210900c3 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoFactory.php @@ -32,7 +32,7 @@ class CreditmemoFactory /** * @var \Magento\Framework\Unserialize\Unserialize - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $unserialize; diff --git a/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php b/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php index ce4a0aa5b3e3a..269ee313c09df 100644 --- a/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php +++ b/app/code/Magento/Sales/Model/Order/CreditmemoRepository.php @@ -151,7 +151,7 @@ public function save(\Magento\Sales\Api\Data\CreditmemoInterface $entity) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/CustomerManagement.php b/app/code/Magento/Sales/Model/Order/CustomerManagement.php index ae3f940dbb2ba..50c7f88af546b 100644 --- a/app/code/Magento/Sales/Model/Order/CustomerManagement.php +++ b/app/code/Magento/Sales/Model/Order/CustomerManagement.php @@ -27,17 +27,17 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn protected $accountManagement; /** - * @deprecated + * @deprecated 101.0.4 */ protected $customerFactory; /** - * @deprecated + * @deprecated 101.0.4 */ protected $addressFactory; /** - * @deprecated + * @deprecated 101.0.4 */ protected $regionFactory; @@ -47,7 +47,7 @@ class CustomerManagement implements \Magento\Sales\Api\OrderCustomerManagementIn protected $orderRepository; /** - * @deprecated + * @deprecated 101.0.4 */ protected $objectCopyService; diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php index 05164d1b7b5f3..d0247294e75a1 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/InvoiceSender.php @@ -3,18 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Sender; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; use Magento\Payment\Helper\Data as PaymentHelper; use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address\Renderer; use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; use Magento\Sales\Model\Order\Email\Container\Template; use Magento\Sales\Model\Order\Email\Sender; use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\ResourceModel\Order\Invoice as InvoiceResource; -use Magento\Sales\Model\Order\Address\Renderer; -use Magento\Framework\Event\ManagerInterface; -use Magento\Framework\DataObject; /** * Sends order invoice email to the customer. @@ -106,6 +108,12 @@ public function send(Invoice $invoice, $forceSyncMode = false) $order = $invoice->getOrder(); $this->identityContainer->setStore($order->getStore()); + if ($this->checkIfPartialInvoice($order, $invoice)) { + $order->setBaseSubtotal((float) $invoice->getBaseSubtotal()); + $order->setBaseTaxAmount((float) $invoice->getBaseTaxAmount()); + $order->setBaseShippingAmount((float) $invoice->getBaseShippingAmount()); + } + $transport = [ 'order' => $order, 'order_id' => $order->getId(), @@ -165,4 +173,18 @@ protected function getPaymentHtml(Order $order) $this->identityContainer->getStore()->getStoreId() ); } + + /** + * Check if the order contains partial invoice + * + * @param Order $order + * @param Invoice $invoice + * @return bool + */ + private function checkIfPartialInvoice(Order $order, Invoice $invoice): bool + { + $totalQtyOrdered = (float) $order->getTotalQtyOrdered(); + $totalQtyInvoiced = (float) $invoice->getTotalQty(); + return $totalQtyOrdered !== $totalQtyInvoiced; + } } diff --git a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php index 4c8e1744ac0e0..49aef3de7fecb 100644 --- a/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php +++ b/app/code/Magento/Sales/Model/Order/Email/Sender/ShipmentSender.php @@ -19,7 +19,7 @@ /** * Sends order shipment email to the customer. * - * @deprecated since this class works only with the concrete model and no data interface + * @deprecated 102.1.0 since this class works only with the concrete model and no data interface * @see \Magento\Sales\Model\Order\Shipment\Sender\EmailSender * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ diff --git a/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php b/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php index acd0d0c67d8c0..ef7205b374415 100644 --- a/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php +++ b/app/code/Magento/Sales/Model/Order/Invoice/Total/Discount.php @@ -5,13 +5,20 @@ */ namespace Magento\Sales\Model\Order\Invoice\Total; +use Magento\Sales\Model\Order\Invoice; + +/** + * Discount invoice + */ class Discount extends AbstractTotal { /** - * @param \Magento\Sales\Model\Order\Invoice $invoice + * Collect invoice + * + * @param Invoice $invoice * @return $this */ - public function collect(\Magento\Sales\Model\Order\Invoice $invoice) + public function collect(Invoice $invoice) { $invoice->setDiscountAmount(0); $invoice->setBaseDiscountAmount(0); @@ -24,14 +31,7 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) * So basically if we have invoice with positive discount and it * was not canceled we don't add shipping discount to this one. */ - $addShippingDiscount = true; - foreach ($invoice->getOrder()->getInvoiceCollection() as $previousInvoice) { - if ($previousInvoice->getDiscountAmount()) { - $addShippingDiscount = false; - } - } - - if ($addShippingDiscount) { + if ($this->isShippingDiscount($invoice)) { $totalDiscountAmount = $totalDiscountAmount + $invoice->getOrder()->getShippingDiscountAmount(); $baseTotalDiscountAmount = $baseTotalDiscountAmount + $invoice->getOrder()->getBaseShippingDiscountAmount(); @@ -71,8 +71,29 @@ public function collect(\Magento\Sales\Model\Order\Invoice $invoice) $invoice->setDiscountAmount(-$totalDiscountAmount); $invoice->setBaseDiscountAmount(-$baseTotalDiscountAmount); - $invoice->setGrandTotal($invoice->getGrandTotal() - $totalDiscountAmount); - $invoice->setBaseGrandTotal($invoice->getBaseGrandTotal() - $baseTotalDiscountAmount); + $grandTotal = $invoice->getGrandTotal() - $totalDiscountAmount < 0.0001 + ? 0 : $invoice->getGrandTotal() - $totalDiscountAmount; + $baseGrandTotal = $invoice->getBaseGrandTotal() - $baseTotalDiscountAmount < 0.0001 + ? 0 : $invoice->getBaseGrandTotal() - $baseTotalDiscountAmount; + $invoice->setGrandTotal($grandTotal); + $invoice->setBaseGrandTotal($baseGrandTotal); return $this; } + + /** + * Checking if shipping discount was added in previous invoices. + * + * @param Invoice $invoice + * @return bool + */ + private function isShippingDiscount(Invoice $invoice): bool + { + $addShippingDiscount = true; + foreach ($invoice->getOrder()->getInvoiceCollection() as $previousInvoice) { + if ($previousInvoice->getDiscountAmount()) { + $addShippingDiscount = false; + } + } + return $addShippingDiscount; + } } diff --git a/app/code/Magento/Sales/Model/Order/InvoiceRepository.php b/app/code/Magento/Sales/Model/Order/InvoiceRepository.php index 2244a86260c2f..ac1a782367ab3 100644 --- a/app/code/Magento/Sales/Model/Order/InvoiceRepository.php +++ b/app/code/Magento/Sales/Model/Order/InvoiceRepository.php @@ -145,7 +145,7 @@ public function save(\Magento\Sales\Api\Data\InvoiceInterface $entity) /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Item.php b/app/code/Magento/Sales/Model/Order/Item.php index ba01090e5abff..bc55b2229770d 100644 --- a/app/code/Magento/Sales/Model/Order/Item.php +++ b/app/code/Magento/Sales/Model/Order/Item.php @@ -2409,7 +2409,7 @@ public function setExtensionAttributes(\Magento\Sales\Api\Data\OrderItemExtensio * Check if it is possible to process item after cancellation * * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isProcessingAvailable() { diff --git a/app/code/Magento/Sales/Model/Order/ItemRepository.php b/app/code/Magento/Sales/Model/Order/ItemRepository.php index 6e029ac468370..345fffc414fbc 100644 --- a/app/code/Magento/Sales/Model/Order/ItemRepository.php +++ b/app/code/Magento/Sales/Model/Order/ItemRepository.php @@ -167,10 +167,7 @@ public function deleteById($id) public function save(OrderItemInterface $entity) { if ($entity->getProductOption()) { - $request = $this->getBuyRequest($entity); - $productOptions = $entity->getProductOptions(); - $productOptions['info_buyRequest'] = $request->toArray(); - $entity->setProductOptions($productOptions); + $entity->setProductOptions($this->getItemProductOptions($entity)); } $this->metadata->getMapper()->save($entity); @@ -178,6 +175,23 @@ public function save(OrderItemInterface $entity) return $this->registry[$entity->getEntityId()]; } + /** + * Return product options + * + * @param OrderItemInterface $entity + * @return array + */ + private function getItemProductOptions(OrderItemInterface $entity): array + { + $request = $this->getBuyRequest($entity); + $productOptions = $entity->getProductOptions(); + $productOptions['info_buyRequest'] = $productOptions && !empty($productOptions['info_buyRequest']) + ? array_merge($productOptions['info_buyRequest'], $request->toArray()) + : $request->toArray(); + + return $productOptions; + } + /** * Set parent item. * diff --git a/app/code/Magento/Sales/Model/Order/Payment.php b/app/code/Magento/Sales/Model/Order/Payment.php index 3076c6dfd2ba7..6a2a77b52927a 100644 --- a/app/code/Magento/Sales/Model/Order/Payment.php +++ b/app/code/Magento/Sales/Model/Order/Payment.php @@ -1494,7 +1494,7 @@ protected function _getInvoiceForTransactionId($transactionId) /** * Get order state resolver instance. * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return OrderStateResolverInterface */ private function getOrderStateResolver() diff --git a/app/code/Magento/Sales/Model/Order/Payment/Info.php b/app/code/Magento/Sales/Model/Order/Payment/Info.php index 479d96b5842d9..c7641cb861b3e 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Info.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Info.php @@ -118,6 +118,7 @@ public function getMethodInstance() $instance = $this->paymentData->getMethodInstance(Substitution::CODE); } $instance->setInfoInstance($this); + $instance->setStore($this->getOrder()->getStoreId()); $this->setMethodInstance($instance); } return $this->getData('method_instance'); diff --git a/app/code/Magento/Sales/Model/Order/Payment/Repository.php b/app/code/Magento/Sales/Model/Order/Payment/Repository.php index 4353f6b1cc391..27686ffb46c9d 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Repository.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Repository.php @@ -131,7 +131,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php index 89731b5130605..d17f3b51f3934 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/AuthorizeCommand.php @@ -90,7 +90,7 @@ private function getNotificationMessage(OrderPaymentInterface $payment): ?string * @param string $status * @param string $state * @return void - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. */ protected function setOrderStateAndStatus(Order $order, $status, $state) { diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php index f57e1933a7e5a..79b329cd486e5 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/CaptureCommand.php @@ -74,7 +74,7 @@ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface * @param string $status * @param string $state * @return void - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. */ protected function setOrderStateAndStatus(Order $order, $status, $state) { diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php index 2a7e7145f6886..d6acd82613c0a 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/OrderCommand.php @@ -61,7 +61,7 @@ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface } /** - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. * * @param Order $order * @param string $status diff --git a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php index 2551092a64e9a..ff375c995a183 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php +++ b/app/code/Magento/Sales/Model/Order/Payment/State/RegisterCaptureNotificationCommand.php @@ -73,7 +73,7 @@ public function execute(OrderPaymentInterface $payment, $amount, OrderInterface /** * Sets the state and status of the order * - * @deprecated 100.2.0 Replaced by a StatusResolver class call. + * @deprecated 100.1.9 Replaced by a StatusResolver class call. * * @param Order $order * @param string $status diff --git a/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php b/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php index a602fe54363ed..30af4e07a42bf 100644 --- a/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php +++ b/app/code/Magento/Sales/Model/Order/Payment/Transaction/Repository.php @@ -233,7 +233,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php index f1430757939e7..cc67601f0ec51 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Creditmemo.php @@ -20,6 +20,11 @@ class Creditmemo extends AbstractPdf */ protected $_storeManager; + /** + * @var \Magento\Store\Model\App\Emulation + */ + private $appEmulation; + /** * @param \Magento\Payment\Helper\Data $paymentData * @param \Magento\Framework\Stdlib\StringUtils $string @@ -32,7 +37,7 @@ class Creditmemo extends AbstractPdf * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param \Magento\Store\Model\App\Emulation|null $appEmulation * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -50,11 +55,11 @@ public function __construct( \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + \Magento\Store\Model\App\Emulation $appEmulation, array $data = [] ) { $this->_storeManager = $storeManager; - $this->_localeResolver = $localeResolver; + $this->appEmulation = $appEmulation; parent::__construct( $paymentData, $string, @@ -150,7 +155,11 @@ public function getPdf($creditmemos = []) foreach ($creditmemos as $creditmemo) { if ($creditmemo->getStoreId()) { - $this->_localeResolver->emulate($creditmemo->getStoreId()); + $this->appEmulation->startEnvironmentEmulation( + $creditmemo->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $this->_storeManager->setCurrentStore($creditmemo->getStoreId()); } $page = $this->newPage(); @@ -185,7 +194,7 @@ public function getPdf($creditmemos = []) /* Add totals */ $this->insertTotals($page, $creditmemo); if ($creditmemo->getStoreId()) { - $this->_localeResolver->revert(); + $this->appEmulation->stopEnvironmentEmulation(); } } $this->_afterGetPdf(); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php index f294128a72f9f..d4ce16d1bbe8e 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Invoice.php @@ -19,9 +19,9 @@ class Invoice extends AbstractPdf protected $_storeManager; /** - * @var \Magento\Framework\Locale\ResolverInterface + * @var \Magento\Store\Model\App\Emulation */ - protected $_localeResolver; + private $appEmulation; /** * @param \Magento\Payment\Helper\Data $paymentData @@ -35,7 +35,7 @@ class Invoice extends AbstractPdf * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param \Magento\Store\Model\App\Emulation $appEmulation * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -52,11 +52,11 @@ public function __construct( \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + \Magento\Store\Model\App\Emulation $appEmulation, array $data = [] ) { $this->_storeManager = $storeManager; - $this->_localeResolver = $localeResolver; + $this->appEmulation = $appEmulation; parent::__construct( $paymentData, $string, @@ -127,7 +127,11 @@ public function getPdf($invoices = []) foreach ($invoices as $invoice) { if ($invoice->getStoreId()) { - $this->_localeResolver->emulate($invoice->getStoreId()); + $this->appEmulation->startEnvironmentEmulation( + $invoice->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $this->_storeManager->setCurrentStore($invoice->getStoreId()); } $page = $this->newPage(); @@ -162,7 +166,7 @@ public function getPdf($invoices = []) /* Add totals */ $this->insertTotals($page, $invoice); if ($invoice->getStoreId()) { - $this->_localeResolver->revert(); + $this->appEmulation->stopEnvironmentEmulation(); } } $this->_afterGetPdf(); diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php index 253dbd43fa580..6ddbce49829eb 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Items/Invoice/DefaultInvoice.php @@ -80,11 +80,9 @@ public function draw() $lines = []; // draw Product name - $lines[0] = [ - [ + $lines[0][] = [ 'text' => $this->string->split($this->prepareText((string)$item->getName()), 35, true, true), 'feed' => 35 - ] ]; // draw SKU diff --git a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php index 32a289c0f5fa8..92124b7fe8b72 100644 --- a/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php +++ b/app/code/Magento/Sales/Model/Order/Pdf/Shipment.php @@ -17,9 +17,9 @@ class Shipment extends AbstractPdf protected $_storeManager; /** - * @var \Magento\Framework\Locale\ResolverInterface + * @var \Magento\Store\Model\App\Emulation */ - protected $_localeResolver; + private $appEmulation; /** * @param \Magento\Payment\Helper\Data $paymentData @@ -33,7 +33,7 @@ class Shipment extends AbstractPdf * @param \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation * @param \Magento\Sales\Model\Order\Address\Renderer $addressRenderer * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Locale\ResolverInterface $localeResolver + * @param \Magento\Store\Model\App\Emulation $appEmulation * @param array $data * * @SuppressWarnings(PHPMD.ExcessiveParameterList) @@ -50,11 +50,11 @@ public function __construct( \Magento\Framework\Translate\Inline\StateInterface $inlineTranslation, \Magento\Sales\Model\Order\Address\Renderer $addressRenderer, \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Locale\ResolverInterface $localeResolver, + \Magento\Store\Model\App\Emulation $appEmulation, array $data = [] ) { $this->_storeManager = $storeManager; - $this->_localeResolver = $localeResolver; + $this->appEmulation = $appEmulation; parent::__construct( $paymentData, $string, @@ -118,7 +118,11 @@ public function getPdf($shipments = []) $this->_setFontBold($style, 10); foreach ($shipments as $shipment) { if ($shipment->getStoreId()) { - $this->_localeResolver->emulate($shipment->getStoreId()); + $this->appEmulation->startEnvironmentEmulation( + $shipment->getStoreId(), + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ); $this->_storeManager->setCurrentStore($shipment->getStoreId()); } $page = $this->newPage(); @@ -151,7 +155,7 @@ public function getPdf($shipments = []) $page = end($pdf->pages); } if ($shipment->getStoreId()) { - $this->_localeResolver->revert(); + $this->appEmulation->stopEnvironmentEmulation(); } } $this->_afterGetPdf(); diff --git a/app/code/Magento/Sales/Model/Order/ProductOption.php b/app/code/Magento/Sales/Model/Order/ProductOption.php index 9a4f847b135e7..3d0b5433d7a4f 100644 --- a/app/code/Magento/Sales/Model/Order/ProductOption.php +++ b/app/code/Magento/Sales/Model/Order/ProductOption.php @@ -17,6 +17,7 @@ * Adds product option to the order item according to product options processors pool. * * @api + * @since 102.0.1 */ class ProductOption { @@ -54,6 +55,7 @@ public function __construct( * Adds product option to the order item. * * @param OrderItemInterface $orderItem + * @since 102.0.1 */ public function add(OrderItemInterface $orderItem): void { diff --git a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php index dd70c6b5481df..e4f2ff0d57035 100644 --- a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php +++ b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityChecker.php @@ -16,7 +16,7 @@ * of the array $productAvailabilityChecks(constructor argument). A product type should be a key for the new element. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class OrderedProductAvailabilityChecker implements OrderedProductAvailabilityCheckerInterface { @@ -36,7 +36,7 @@ public function __construct(array $productAvailabilityChecks) /** * @inheritdoc - * @since 100.2.0 + * @since 101.0.0 */ public function isAvailable(Item $item) { diff --git a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php index 989bd482ed4e8..59f7dfc63b095 100644 --- a/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php +++ b/app/code/Magento/Sales/Model/Order/Reorder/OrderedProductAvailabilityCheckerInterface.php @@ -9,7 +9,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ interface OrderedProductAvailabilityCheckerInterface { @@ -19,7 +19,7 @@ interface OrderedProductAvailabilityCheckerInterface * * @param Item $item * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function isAvailable(Item $item); } diff --git a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php index 21b42abeb293d..3cd318ea67adb 100644 --- a/app/code/Magento/Sales/Model/Order/ShipmentFactory.php +++ b/app/code/Magento/Sales/Model/Order/ShipmentFactory.php @@ -12,6 +12,7 @@ * Factory class for @see \Magento\Sales\Api\Data\ShipmentInterface * * @api + * @since 100.0.2 */ class ShipmentFactory { diff --git a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php index 0b86bec895b75..ad73b22e94555 100644 --- a/app/code/Magento/Sales/Model/Order/ShipmentRepository.php +++ b/app/code/Magento/Sales/Model/Order/ShipmentRepository.php @@ -166,7 +166,7 @@ public function create() /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Sales/Model/OrderRepository.php b/app/code/Magento/Sales/Model/OrderRepository.php index f93de4c32d888..ecd4e7babb1e3 100644 --- a/app/code/Magento/Sales/Model/OrderRepository.php +++ b/app/code/Magento/Sales/Model/OrderRepository.php @@ -317,7 +317,7 @@ private function getShippingAssignmentBuilderDependency() * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup * @param \Magento\Sales\Api\Data\OrderSearchResultInterface $searchResult * @return void - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @throws \Magento\Framework\Exception\InputException */ protected function addFilterGroupToCollection( diff --git a/app/code/Magento/Sales/Model/Reorder/Reorder.php b/app/code/Magento/Sales/Model/Reorder/Reorder.php index a1a8d6e8c9928..c7636696382b4 100644 --- a/app/code/Magento/Sales/Model/Reorder/Reorder.php +++ b/app/code/Magento/Sales/Model/Reorder/Reorder.php @@ -7,7 +7,6 @@ namespace Magento\Sales\Model\Reorder; use Magento\Catalog\Api\Data\ProductInterface; -use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Framework\Exception\InputException; @@ -15,7 +14,8 @@ use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; use Magento\Quote\Model\Cart\CustomerCartResolver; -use Magento\Quote\Model\Quote as Quote; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\GuestCart\GuestCartResolver; use Magento\Sales\Api\Data\OrderItemInterface; use Magento\Sales\Helper\Reorder as ReorderHelper; use Magento\Sales\Model\Order\Item; @@ -72,11 +72,6 @@ class Reorder */ private $cartRepository; - /** - * @var ProductRepositoryInterface - */ - private $productRepository; - /** * @var Data\Error[] */ @@ -92,11 +87,16 @@ class Reorder */ private $productCollectionFactory; + /** + * @var GuestCartResolver + */ + private $guestCartResolver; + /** * @param OrderFactory $orderFactory * @param CustomerCartResolver $customerCartProvider + * @param GuestCartResolver $guestCartResolver * @param CartRepositoryInterface $cartRepository - * @param ProductRepositoryInterface $productRepository * @param ReorderHelper $reorderHelper * @param \Psr\Log\LoggerInterface $logger * @param ProductCollectionFactory $productCollectionFactory @@ -104,18 +104,18 @@ class Reorder public function __construct( OrderFactory $orderFactory, CustomerCartResolver $customerCartProvider, + GuestCartResolver $guestCartResolver, CartRepositoryInterface $cartRepository, - ProductRepositoryInterface $productRepository, ReorderHelper $reorderHelper, \Psr\Log\LoggerInterface $logger, ProductCollectionFactory $productCollectionFactory ) { $this->orderFactory = $orderFactory; $this->cartRepository = $cartRepository; - $this->productRepository = $productRepository; $this->reorderHelper = $reorderHelper; $this->logger = $logger; $this->customerCartProvider = $customerCartProvider; + $this->guestCartResolver = $guestCartResolver; $this->productCollectionFactory = $productCollectionFactory; } @@ -141,7 +141,9 @@ public function execute(string $orderNumber, string $storeId): Data\ReorderOutpu $customerId = (int)$order->getCustomerId(); $this->errors = []; - $cart = $this->customerCartProvider->resolve($customerId); + $cart = $customerId === 0 + ? $this->guestCartResolver->resolve() + : $this->customerCartProvider->resolve($customerId); if (!$this->reorderHelper->isAllowed($order->getStore())) { $this->addError((string)__('Reorders are not allowed.'), self::ERROR_REORDER_NOT_AVAILABLE); return $this->prepareOutput($cart); @@ -225,7 +227,8 @@ private function getOrderProducts(string $storeId, array $orderItemProductIds): ->addStoreFilter() ->addAttributeToSelect('*') ->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner') - ->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); + ->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner') + ->addOptionsToResult(); return $collection->getItems(); } diff --git a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php index 25c15449a9fb4..444fc589748ab 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php +++ b/app/code/Magento/Sales/Model/ResourceModel/AbstractGrid.php @@ -90,7 +90,7 @@ public function purge($value, $field = null) * * @param string $default * @return string - * @deprecated 100.2.0 this method is not used in abstract model but only in single child so + * @deprecated 101.0.0 this method is not used in abstract model but only in single child so * this deprecation is a part of cleaning abstract classes. * @see \Magento\Sales\Model\ResourceModel\Provider\UpdatedIdListProvider */ diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php index 8af6c03b44275..f2a28b613cfea 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Address/Collection.php @@ -6,12 +6,19 @@ namespace Magento\Sales\Model\ResourceModel\Order\Address; use Magento\Sales\Api\Data\OrderAddressSearchResultInterface; -use \Magento\Sales\Model\ResourceModel\Order\Collection\AbstractCollection; +use Magento\Sales\Model\ResourceModel\Order\Collection\AbstractCollection; +use Magento\Framework\Locale\ResolverInterface; +use Magento\Framework\Data\Collection\EntityFactoryInterface; +use Magento\Framework\Data\Collection\Db\FetchStrategyInterface; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\Model\ResourceModel\Db\AbstractDb; +use Magento\Framework\App\ObjectManager; +use Psr\Log\LoggerInterface; /** - * Flat sales order payment collection - * - * @author Magento Core Team <core@magentocommerce.com> + * Order addresses collection */ class Collection extends AbstractCollection implements OrderAddressSearchResultInterface { @@ -29,6 +36,44 @@ class Collection extends AbstractCollection implements OrderAddressSearchResultI */ protected $_eventObject = 'order_address_collection'; + /** + * @var ResolverInterface + */ + private $localeResolver; + + /** + * @param EntityFactoryInterface $entityFactory + * @param LoggerInterface $logger + * @param FetchStrategyInterface $fetchStrategy + * @param ManagerInterface $eventManager + * @param Snapshot $entitySnapshot + * @param AdapterInterface|null $connection + * @param AbstractDb|null $resource + * @param ResolverInterface|null $localeResolver + */ + public function __construct( + EntityFactoryInterface $entityFactory, + LoggerInterface $logger, + FetchStrategyInterface $fetchStrategy, + ManagerInterface $eventManager, + Snapshot $entitySnapshot, + AdapterInterface $connection = null, + AbstractDb $resource = null, + ResolverInterface $localeResolver = null + ) { + $this->localeResolver = $localeResolver ?: ObjectManager::getInstance() + ->get(ResolverInterface::class); + parent::__construct( + $entityFactory, + $logger, + $fetchStrategy, + $eventManager, + $entitySnapshot, + $connection, + $resource + ); + } + /** * Model initialization * @@ -42,6 +87,16 @@ protected function _construct() ); } + /** + * @inheritdoc + */ + protected function _initSelect() + { + parent::_initSelect(); + $this->joinRegions(); + return $this; + } + /** * Redeclare after load method for dispatch event * @@ -55,4 +110,31 @@ protected function _afterLoad() return $this; } + + /** + * Join region name table with current locale + * + * @return $this + */ + private function joinRegions() + { + $locale = $this->localeResolver->getLocale(); + $connection = $this->getConnection(); + + $defaultNameExpr = $connection->getIfNullSql( + $connection->quoteIdentifier('rct.default_name'), + $connection->quoteIdentifier('main_table.region') + ); + $expression = $connection->getIfNullSql($connection->quoteIdentifier('rnt.name'), $defaultNameExpr); + + $regionId = $connection->quoteIdentifier('main_table.region_id'); + $condition = $connection->quoteInto("rnt.locale=?", $locale); + $rctTable = $this->getTable('directory_country_region'); + $rntTable = $this->getTable('directory_country_region_name'); + + $this->getSelect() + ->joinLeft(['rct' => $rctTable], "rct.region_id={$regionId}", []) + ->joinLeft(['rnt' => $rntTable], "rnt.region_id={$regionId} AND {$condition}", ['region' => $expression]); + return $this; + } } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php index 0fec004a25fae..274132a7fea50 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Handler/Address.php @@ -9,9 +9,6 @@ use Magento\Sales\Model\Order; use Magento\Sales\Model\ResourceModel\Attribute; -/** - * Class Address - */ class Address { /** @@ -69,7 +66,7 @@ public function process(Order $order) $attributesForSave[] = 'billing_address_id'; } $shippingAddress = $order->getShippingAddress(); - if ($shippingAddress && $order->getShippigAddressId() != $shippingAddress->getId()) { + if ($shippingAddress && $order->getShippingAddressId() != $shippingAddress->getId()) { $order->setShippingAddressId($shippingAddress->getId()); $attributesForSave[] = 'shipping_address_id'; } diff --git a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php index 19d9b6f300eba..b1d2deb248ba1 100644 --- a/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php +++ b/app/code/Magento/Sales/Model/ResourceModel/Order/Rss/OrderStatus.php @@ -43,13 +43,13 @@ public function getAllCommentCollection($orderId) $commentSelects = []; foreach (['invoice', 'shipment', 'creditmemo'] as $entityTypeCode) { $mainTable = $resource->getTableName('sales_' . $entityTypeCode); - $slaveTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment'); + $commentTable = $resource->getTableName('sales_' . $entityTypeCode . '_comment'); $select = $read->select()->from( ['main' => $mainTable], ['entity_id' => 'order_id', 'entity_type_code' => new \Zend_Db_Expr("'{$entityTypeCode}'")] )->join( - ['slave' => $slaveTable], - 'main.entity_id = slave.parent_id', + ['comment' => $commentTable], + 'main.entity_id = comment.parent_id', $fields )->where( 'main.order_id = ?', diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml index dee2af6cd4053..48443512ee4c8 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AddConfigurableProductToOrderActionGroup.xml @@ -18,6 +18,8 @@ <argument name="option"/> </arguments> + <scrollToTopOfPage stepKey="scrollToTopOfThePage"/> + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductsButton"/> <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> <fillField selector="{{AdminOrderFormItemsSection.skuFilter}}" userInput="{{product.sku}}" stepKey="fillSkuFilterConfigurable"/> <click selector="{{AdminOrderFormItemsSection.search}}" stepKey="clickSearchConfigurable"/> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceClickSubmitActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceClickSubmitActionGroup.xml new file mode 100644 index 0000000000000..69d042591c198 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminInvoiceClickSubmitActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminInvoiceClickSubmitActionGroup"> + <annotations> + <description>Click submit invoice button for creating invoice.</description> + </annotations> + + <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <waitForPageLoad stepKey="waitForInvoiceToBeCreated"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersPageOpenActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersPageOpenActionGroup.xml new file mode 100644 index 0000000000000..2f08637cdcb53 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/AdminOrdersPageOpenActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminOrdersPageOpenActionGroup"> + <annotations> + <description>Goes to the Admin Orders page.</description> + </annotations> + <amOnPage url="{{AdminOrdersPage.url}}" stepKey="openOrdersGridPage"/> + <waitForPageLoad stepKey="waitForLoadingPage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml index 3d3efc705854d..6ec3cef59e22e 100644 --- a/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml +++ b/app/code/Magento/Sales/Test/Mftf/ActionGroup/NavigateToNewOrderPageNewCustomerSingleStoreActionGroup.xml @@ -18,6 +18,7 @@ <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> + <conditionalClick selector="{{AdminOrderStoreScopeTreeSection.storeOption(_defaultStore.name)}}" dependentSelector="{{AdminOrderStoreScopeTreeSection.storeOption(_defaultStore.name)}}" visible="true" stepKey="selectStoreViewIfAppears"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Create New Order" stepKey="seeNewOrderPageTitle"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml index 70f37352fb183..085eea12e2243 100644 --- a/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml +++ b/app/code/Magento/Sales/Test/Mftf/Section/StorefrontGuestOrderViewSection.xml @@ -11,5 +11,6 @@ <section name="StorefrontGuestOrderViewSection"> <element name="orderInformationTab" type="text" selector="//*[@class='nav item current']/strong[contains(text(), 'Order Information')]"/> <element name="printOrder" type="button" selector=".order-actions-toolbar .actions .print" timeout="30"/> + <element name="reorder" type="button" selector=".order-actions-toolbar .actions .order" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml index 11a9957fe0041..0eb8d71223276 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminAvailabilityCreditMemoWithNoPaymentTest.xml @@ -25,7 +25,9 @@ </createData> <!-- Enable *Free Shipping* --> <createData entity="FreeShippingMethodsSettingConfig" stepKey="freeShippingMethodsSettingConfig"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> @@ -37,7 +39,9 @@ <actionGroup ref="AdminDeleteCustomerActionGroup" stepKey="deleteCustomer"> <argument name="customerEmail" value="Simple_US_Customer.email"/> </actionGroup> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> +</actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logOut"/> </after> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml index c0ebbe450119e..4e750c2cc24b3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithCheckMoneyOrderPaymentMethodTest.xml @@ -94,8 +94,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleCustomer" stepKey="deleteSimpleCustomer"/> @@ -215,8 +219,7 @@ </actionGroup> <!-- Open Order Index Page --> <comment userInput="Open Order Index Page" stepKey="openOrderIndexPageComemnt"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter order using orderId --> <comment userInput="Filter order using orderId" stepKey="filterOrderUsingOrderIdComment"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml index 256417c0d0d10..62425cefb20db 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithProductQtyWithoutStockDecreaseTest.xml @@ -76,8 +76,7 @@ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStockStatus"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForPageLoad5"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <!-- Filter Order using orderId --> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> @@ -98,8 +97,7 @@ <seeInField selector="{{AdminProductFormSection.productStockStatus}}" userInput="In Stock" stepKey="seeProductStockStatusAfterCancelOrder"/> <!-- Open Order Index Page --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders1"/> - <waitForPageLoad stepKey="waitForPageLoad6"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders1"/> <!-- Filter Order using orderId --> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById1"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml index 477676085cf2e..0b873de34e279 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCancelTheCreatedOrderWithPurchaseOrderPaymentMethodTest.xml @@ -35,8 +35,12 @@ <createData entity="SimpleProduct2" stepKey="simpleProduct"> <field key="price">10.00</field> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set {{DisablePurchaseOrderConfigData.path}} {{DisablePurchaseOrderConfigData.value}}" stepKey="disablePurchaseOrderPayment"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml index b8612f7f795fb..33bc1a39ca11a 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCorrectnessInvoicedItemInBundleProductTest.xml @@ -58,7 +58,9 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!--Go to bundle product page--> <amOnPage url="{{StorefrontProductPage.url($$createCategory.name$$)}}" stepKey="navigateToBundleProductPage"/> @@ -77,7 +79,7 @@ <!--Go to order page submit invoice--> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="grabOrderNumber"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridById"> <argument name="orderId" value="$grabOrderNumber"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml index 2935a56a6c0a1..c4656e394d349 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoBankTransferPaymentTest.xml @@ -63,7 +63,7 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml index ab3a2cc647740..ff5dc0e36fdbd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoConfigurableProductTest.xml @@ -93,7 +93,7 @@ <deleteData createDataKey="createConfigChildProduct2" stepKey="deleteConfigChildProduct2"/> <deleteData createDataKey="createConfigProductAttribute" stepKey="deleteConfigProductAttribute"/> <deleteData createDataKey="createCategory" stepKey="deleteApiCategory"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <!-- Reindex invalidated indices after product attribute has been created/deleted --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml index d9ae276de31a0..eb3d4ad991915 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoPartialRefundTest.xml @@ -58,7 +58,7 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml index d888e6841e34d..4383820ba6bee 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithCashOnDeliveryTest.xml @@ -64,7 +64,8 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml index 7974d594eb99c..7818a1f3d9345 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithPurchaseOrderTest.xml @@ -67,7 +67,7 @@ <!-- Create Invoice --> <actionGroup ref="StartCreateInvoiceFromOrderPageActionGroup" stepKey="startInvoice"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForMessageAppears"/> <see selector="{{AdminMessagesSection.success}}" userInput="The invoice has been created." stepKey="seeInvoiceCreateSuccess"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.xml new file mode 100644 index 0000000000000..8b8789d488b9c --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest.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="AdminCreateCreditMemoWithZeroPriceCheckOrderStatusTest"> + <annotations> + <stories value="Github issue: #22762 Credit Memo with Zero Total: Order Status 'Complete' and not 'Closed'"/> + <title value="Create Credit Memo with zero total."/> + <description value="Assert order status after create CreditMemo with zero total."/> + <severity value="MAJOR"/> + <group value="sales"/> + <testCaseId value="MC-35848"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct_zero" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <createData entity="FreeShippingMethodDisableConfig" stepKey="disableFreeShipping"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderPage"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="AddSimpleProductToOrderActionGroup" stepKey="addSecondProduct"> + <argument name="product" value="$createProduct$"/> + <argument name="productQty" value="1"/> + </actionGroup> + <actionGroup ref="FillOrderCustomerInformationActionGroup" stepKey="fillCustomerInfo"> + <argument name="customer" value="$createCustomer$"/> + <argument name="address" value="US_Address_TX"/> + </actionGroup> + <actionGroup ref="OrderSelectFreeShippingActionGroup" stepKey="selectFlatRate"/> + <actionGroup ref="AdminSubmitOrderActionGroup" stepKey="submitOrder"/> + + <actionGroup ref="AdminCreateInvoiceAndCreditMemoActionGroup" stepKey="createCreditMemo"/> + + <actionGroup ref="AdminOrderViewCheckStatusActionGroup" stepKey="seeOrderClose"> + <argument name="status" value="Closed"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml index eea948d902282..07a5dfc95f918 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateInvoiceTest.xml @@ -71,8 +71,7 @@ <actionGroup ref="AdminOrderGridClickFirstRowActionGroup" stepKey="clickOrderRow"/> <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoice"/> <waitForPageLoad stepKey="waitForNewInvoicePageToLoad"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> - <waitForPageLoad stepKey="waitForInvoiceToBeCreated"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> <click selector="{{AdminOrderDetailsOrderViewSection.invoices}}" stepKey="clickInvoices"/> <waitForLoadingMaskToDisappear stepKey="waitForLoadingMask5" /> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml index 0be7e20be5aea..68a8e9d347ddd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithCustomerWithoutEmailTest.xml @@ -24,7 +24,9 @@ <requiredEntity createDataKey="category"/> </createData> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Clean up created test data.--> @@ -33,7 +35,9 @@ <!--Enable required 'email' field on create order page.--> <magentoCLI command="config:set {{EnableEmailRequiredForOrder.path}} {{EnableEmailRequiredForOrder.value}}" stepKey="enableRequiredFieldEmailForAdminOrderCreation"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create order.--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml index ade1f783c1309..1f6d7c40be99b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminCreateOrderWithMinimumAmountEnabledTest.xml @@ -32,7 +32,7 @@ <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> <createData entity="DisabledMinimumOrderAmount" stepKey="disableMinimumOrderAmount"/> <actionGroup ref="ClearCacheActionGroup" stepKey="clearCacheAfter"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <!--Admin creates order--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml index 1c59f6f936cef..f6196c3a911ef 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminFreeShippingNotAvailableIfMinimumOrderAmountNotMatchOrderTotalTest.xml @@ -28,7 +28,9 @@ <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotal" stepKey="setFreeShippingSubtotal"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -38,7 +40,9 @@ <createData entity="FreeShippinMethodDefault" stepKey="disableFreeShippingMethod"/> <createData entity="setFreeShippingSubtotalToDefault" stepKey="setFreeShippingSubtotalToDefault"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> - <magentoCLI command="cache:flush" stepKey="flushCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!--Create new order with existing customer--> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="goToCreateOrderPage"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml index a89e9f7ce6ebe..188002f3938a6 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelCompleteAndClosedTest.xml @@ -63,8 +63,7 @@ <actionGroup ref="AdminCreateInvoiceAndCreditMemoActionGroup" stepKey="createCreditMemo"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml index 45cbe23042e03..055388570479e 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersCancelProcessingAndClosedTest.xml @@ -63,8 +63,7 @@ <actionGroup ref="AdminCreateInvoiceAndCreditMemoActionGroup" stepKey="createCreditMemo"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml index 22b2d69a73090..9b2ded574b737 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnCompleteTest.xml @@ -50,8 +50,7 @@ <actionGroup ref="AdminCreateInvoiceAndShipmentActionGroup" stepKey="createShipment"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml index 4b690a00ee9ed..1a89c5656b2f1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersHoldOnPendingAndProcessingTest.xml @@ -60,8 +60,7 @@ <actionGroup ref="AdminCreateInvoiceActionGroup" stepKey="createInvoice"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml index e1d934f794142..2252c0a813bcf 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersReleasePendingOrderTest.xml @@ -47,8 +47,7 @@ </assertNotEmpty> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml index 86a3e381cb237..d4004c519b7df 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminMassOrdersUpdateCancelPendingOrderTest.xml @@ -46,8 +46,7 @@ </assertNotEmpty> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml new file mode 100644 index 0000000000000..fea3fe68fd522 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrderPagerTest.xml @@ -0,0 +1,140 @@ +<?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="AdminOrderPagerTest"> + <annotations> + <features value="Sales"/> + <stories value="Admin order pager"/> + <title value="Check pager is working"/> + <description value="Check Pager in order add products grid"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35349"/> + <useCaseId value="MC-35316"/> + <group value="sales"/> + </annotations> + <before> + <!-- 21 products created and category --> + <createData entity="_defaultCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct01"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct02"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct03"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct04"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct05"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct06"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct07"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct08"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="_defaultProduct" stepKey="createProduct09"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct10"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct11"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct12"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct13"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct14"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct15"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct16"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct17"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct18"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct19"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="ApiSimpleProduct" stepKey="createProduct20"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createProduct21"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <!-- Customer is created --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Login to Admin --> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + <!-- Delete products --> + <deleteData createDataKey="createProduct01" stepKey="deleteProduct1"/> + <deleteData createDataKey="createProduct02" stepKey="deleteProduct2"/> + <deleteData createDataKey="createProduct03" stepKey="deleteProduct3"/> + <deleteData createDataKey="createProduct04" stepKey="deleteProduct4"/> + <deleteData createDataKey="createProduct05" stepKey="deleteProduct5"/> + <deleteData createDataKey="createProduct06" stepKey="deleteProduct6"/> + <deleteData createDataKey="createProduct07" stepKey="deleteProduct7"/> + <deleteData createDataKey="createProduct08" stepKey="deleteProduct8"/> + <deleteData createDataKey="createProduct09" stepKey="deleteProduct9"/> + <deleteData createDataKey="createProduct10" stepKey="deleteProduct10"/> + <deleteData createDataKey="createProduct11" stepKey="deleteProduct11"/> + <deleteData createDataKey="createProduct12" stepKey="deleteProduct12"/> + <deleteData createDataKey="createProduct13" stepKey="deleteProduct13"/> + <deleteData createDataKey="createProduct14" stepKey="deleteProduct14"/> + <deleteData createDataKey="createProduct15" stepKey="deleteProduct15"/> + <deleteData createDataKey="createProduct16" stepKey="deleteProduct16"/> + <deleteData createDataKey="createProduct17" stepKey="deleteProduct17"/> + <deleteData createDataKey="createProduct18" stepKey="deleteProduct18"/> + <deleteData createDataKey="createProduct19" stepKey="deleteProduct19"/> + <deleteData createDataKey="createProduct20" stepKey="deleteProduct20"/> + <deleteData createDataKey="createProduct21" stepKey="deleteProduct21"/> + + <!-- Delete Category --> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Delete Customer --> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <!-- Initiate create new order --> + <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="navigateToNewOrderWithExistingCustomer"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + + <waitForElementVisible selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="waitForAddProductsButtonAppeared"/> + <click selector="{{AdminOrderFormItemsSection.addProducts}}" stepKey="clickAddProducts"/> + <dontSee selector="{{AdminDataGridPaginationSection.prevPageActive}}" stepKey="previousPageDisabled"/> + <click selector="{{AdminDataGridPaginationSection.nextPageActive}}" stepKey="clickNextPage"/> + <seeInField selector="{{AdminDataGridPaginationSection.selectedPage}}" userInput="2" stepKey="seeSecondPageOrderGrid"/> + <click selector="{{AdminDataGridPaginationSection.prevPageActive}}" stepKey="clickPreviousPage"/> + <seeInField selector="{{AdminDataGridPaginationSection.selectedPage}}" userInput="1" stepKey="seeFirstPageOrderGrid"/> + <dontSee selector="{{AdminDataGridPaginationSection.prevPageActive}}" stepKey="prevPageDisabled"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml index bfd75a69b81d6..28ce9661e259b 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminOrdersReleaseInUnholdStatusTest.xml @@ -52,8 +52,7 @@ <see userInput="You put the order on hold." stepKey="seeHoldMessage"/> <!-- Navigate to backend: Go to Sales > Orders --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrderPage"/> - <waitForPageLoad stepKey="waitForOrderPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrderPage"/> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearFilters"/> <waitForLoadingMaskToDisappear stepKey="waitForLoading"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml index 0ff5080bd8df2..4799984b76745 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminReorderWithCatalogPriceRuleDiscountTest.xml @@ -25,6 +25,16 @@ <createData entity="SimpleProduct2" stepKey="createSimpleProductApi"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <actionGroup ref="AdminCatalogPriceRuleDeleteAllActionGroup" stepKey="deleteAllCatalogPriceRule"/> + <!-- Clearing cache just in case --> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <amOnPage url="{{AdminCatalogPriceRuleGridPage.url}}" stepKey="goToAdminCatalogPriceRuleGridPage2"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded2"/> <!--Create the catalog price rule --> <createData entity="CatalogRuleToPercent" stepKey="createCatalogRule"/> <!--Create order via API--> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml index 85665dfc1b00e..bd6a21e3112ca 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderPaymentMethodValidationTest.xml @@ -33,8 +33,7 @@ </after> <!--Create order via Admin--> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> - <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml index 7615cc219d430..727aef99352ec 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutEmailTest.xml @@ -32,8 +32,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> - <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml index fd26ca1ca601e..2bedb16f3d1dc 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminSubmitsOrderWithAndWithoutFieldsValidationTest.xml @@ -31,8 +31,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <comment userInput="Admin creates order" stepKey="adminCreateOrderComment"/> <!--<actionGroup ref="NavigateToNewOrderPageNewCustomerActionGroup" stepKey="navigateToNewOrderPage"/>--> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="navigateToOrderIndexPage"/> - <waitForPageLoad stepKey="waitForIndexPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="navigateToOrderIndexPage"/> <see selector="{{AdminHeaderSection.pageTitle}}" userInput="Orders" stepKey="seeIndexPageTitle"/> <click selector="{{AdminOrdersGridSection.createNewOrder}}" stepKey="clickCreateNewOrder"/> <click selector="{{AdminOrderFormActionSection.CreateNewCustomer}}" stepKey="clickCreateCustomer"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml index 692f293ef3a75..226524341efdd 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AdminUnassignCustomOrderStatusTest.xml @@ -55,7 +55,7 @@ <!--Click unassign and verify AssertOrderStatusSuccessUnassignMessage--> <click selector="{{AdminOrderStatusGridSection.unassign}}" stepKey="clickUnassign"/> - <see selector="{{AdminMessagesSection.success}}" userInput="You have unassigned the order status." stepKey="seeAssertOrderStatusSuccessUnassignMessage"/> + <waitForText selector="{{AdminMessagesSection.success}}" userInput="You have unassigned the order status." stepKey="seeAssertOrderStatusSuccessUnassignMessage"/> <!--Verify the order status grid page shows the updated order status and verify AssertOrderStatusInGrid--> <actionGroup ref="AssertOrderStatusExistsInGrid" stepKey="seeAssertOrderStatusInGrid"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml index f0f4cf9d1a468..885f019b864de 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/AssignCustomOrderStatusNotVisibleOnStorefrontTest.xml @@ -85,8 +85,7 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Assert order status is correct --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -110,8 +109,7 @@ <see selector="{{StorefrontOrderInformationMainSection.emptyMessage}}" userInput="You have placed no orders." stepKey="seeEmptyMessage"/> <!-- Cancel order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToAdminOrdersPage"/> - <waitForPageLoad stepKey="waitForAdminOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToAdminOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridByOrderId"> <argument name="orderId" value="$getOrderId"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml index d8a3db76da05e..d0c1b51008684 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceAndCheckInvoiceOrderTest.xml @@ -77,8 +77,7 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -89,7 +88,7 @@ <fillField selector="{{AdminOrderInvoiceViewSection.invoiceQty}}" userInput="1" stepKey="fillInvoiceQuantity"/> <click selector="{{AdminOrderInvoiceViewSection.updateInvoiceBtn}}" stepKey="clickUpdateQtyInvoiceBtn"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> @@ -103,8 +102,7 @@ <grabFromCurrentUrl regex="~/invoice_id/(\d+)/~" stepKey="grabInvoiceId"/> <!-- Assert invoice in invoices tab --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdForAssertingInvoiceBtn"> <argument name="orderId" value="$getOrderId"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml index c58b95a41b157..e5cfd5dc4afa0 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithCashOnDeliveryPaymentMethodTest.xml @@ -77,8 +77,7 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -89,7 +88,7 @@ <fillField selector="{{AdminOrderInvoiceViewSection.invoiceQty}}" userInput="1" stepKey="fillInvoiceQuantity"/> <click selector="{{AdminOrderInvoiceViewSection.updateInvoiceBtn}}" stepKey="clickUpdateQtyInvoiceBtn"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml index 1c92c2dae3712..12b956be22cfb 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithShipmentAndCheckInvoicedOrderTest.xml @@ -71,8 +71,7 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -82,7 +81,7 @@ <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> <click selector="{{AdminInvoicePaymentShippingSection.CreateShipment}}" stepKey="createShipment"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="You created the invoice and shipment." stepKey="seeSuccessMessage"/> @@ -101,8 +100,7 @@ <grabFromCurrentUrl regex="~/invoice_id/(\d+)/~" stepKey="grabInvoiceId"/> <!-- Assert no invoice button --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrders"/> - <waitForPageLoad stepKey="waitForOrdersLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrders"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdForAssertingInvoiceBtn"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -146,8 +144,7 @@ <grabFromCurrentUrl regex="~/shipment_id/(\d+)/~" stepKey="grabShipmentId"/> <!-- Assert no ship button --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToAdminOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageToLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToAdminOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrderGridByIdForAssertingShipBtn"> <argument name="orderId" value="$getOrderId"/> </actionGroup> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml index b562073a1276f..780bffd359ba7 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateInvoiceWithZeroSubtotalCheckoutTest.xml @@ -86,8 +86,7 @@ <grabTextFrom selector="|Order # (\d+)|" stepKey="getOrderId"/> <!-- Open created order --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="goToOrdersPage"/> - <waitForPageLoad stepKey="waitForOrdersPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="goToOrdersPage"/> <actionGroup ref="FilterOrderGridByIdActionGroup" stepKey="filterOrdersGridById"> <argument name="orderId" value="$getOrderId"/> </actionGroup> @@ -96,7 +95,7 @@ <!-- Go to invoice tab and fill data --> <click selector="{{AdminOrderDetailsMainActionsSection.invoice}}" stepKey="clickInvoiceAction"/> <fillField selector="{{AdminInvoiceTotalSection.invoiceComment}}" userInput="comment" stepKey="writeComment"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <!-- Assert invoice with shipment success message --> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml index 776d84ac230b8..842faeb32cc33 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreateOrderFromEditCustomerPageTest.xml @@ -75,8 +75,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct1"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <createData entity="DisableFreeShippingConfig" stepKey="disableFreeShippingConfig"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml index bd76f5c10b488..37c2b59f79eb1 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/CreditMemoTotalAfterShippingDiscountTest.xml @@ -95,8 +95,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin1"/> <!-- Search for Order in the order grid --> - <amOnPage url="{{AdminOrdersPage.url}}" stepKey="onOrdersPage"/> - <waitForPageLoad time="30" stepKey="waitForOrderListPageLoad"/> + <actionGroup ref="AdminOrdersPageOpenActionGroup" stepKey="onOrdersPage"/> <conditionalClick selector="{{AdminOrdersGridSection.clearFilters}}" dependentSelector="{{AdminOrdersGridSection.clearFilters}}" visible="true" stepKey="clearExistingOrderFilter"/> <fillField selector="{{AdminOrdersGridSection.search}}" userInput="{$grabOrderNumber}" stepKey="searchOrderNum"/> <click selector="{{AdminOrdersGridSection.submitSearch}}" stepKey="submitSearch"/> @@ -129,7 +128,7 @@ <see selector="{{AdminInvoiceTotalSection.grandTotal}}" userInput="$113.00" stepKey="seeCorrectGrandTotal"/> <grabTextFrom selector="{{AdminInvoiceTotalSection.grandTotal}}" stepKey="grabInvoiceGrandTotal" after="seeCorrectGrandTotal"/> - <click selector="{{AdminInvoiceMainActionsSection.submitInvoice}}" stepKey="clickSubmitInvoice"/> + <actionGroup ref="AdminInvoiceClickSubmitActionGroup" stepKey="clickSubmitInvoice"/> <see selector="{{AdminOrderDetailsMessagesSection.successMessage}}" userInput="The invoice has been created." stepKey="seeSuccessMessage1"/> <see selector="{{AdminOrderDetailsInformationSection.orderStatus}}" userInput="Processing" stepKey="seeOrderProcessing"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml index f374741c247d4..c3fc7a4952143 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/MoveRecentlyViewedBundleFixedProductOnOrderPageTest.xml @@ -57,7 +57,9 @@ <!-- Change configuration --> <magentoCLI command="config:set reports/options/enabled 1" stepKey="enableReportModule"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <!-- Admin logout --> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml index 5cc4fae330d05..e99ffa95495ff 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerDisplayedTest.xml @@ -90,8 +90,12 @@ <createData entity="Simple_US_Customer" stepKey="createCustomer"/> <!-- Reindex and flush the cache to display products on the category page --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete category and products --> @@ -203,8 +207,7 @@ <!-- Place Order --> <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="onCheckout"/> <see userInput="21" selector="{{CheckoutOrderSummarySection.itemsQtyInCart}}" stepKey="see21Products"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForCheckoutLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="placeOrder"/> <waitForPageLoad stepKey="waitForSuccess"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml index 20261de502ea3..cba141e2ab271 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontOrderPagerIsAbsentTest.xml @@ -193,8 +193,7 @@ <!-- Place Order --> <actionGroup ref="StorefrontOpenCheckoutPageActionGroup" stepKey="onCheckout"/> <see userInput="20" selector="{{CheckoutOrderSummarySection.itemsQtyInCart}}" stepKey="see20Products"/> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickNextButton"/> - <waitForLoadingMaskToDisappear stepKey="waitForCheckoutLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNextButton"/> <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="placeOrder"/> <waitForPageLoad stepKey="waitForSuccess"/> <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber22}}" stepKey="grabOrderNumber"/> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml index 00117c56de439..9fdf577abd873 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontPrintOrderGuestTest.xml @@ -44,8 +44,8 @@ <checkOption selector="{{AdminProductDownloadableSection.isLinksPurchasedSeparately}}" stepKey="checkOption"/> <waitForAjaxLoad stepKey="waitForAjaxLoad"/> <grabValueFrom selector="{{AdminProductDownloadableSection.addLinkTitleInput('0')}}" stepKey="grabLink"/> - <click selector="{{AdminProductFormSection.save}}" stepKey="clickSave"/> - <waitForLoadingMaskToDisappear stepKey="waitForSave"/> + + <actionGroup ref="AdminProductFormSaveButtonClickActionGroup" stepKey="clickSave"/> <!-- Create configurable Product --> <createData entity="ApiConfigurableProduct" stepKey="createConfigProduct"> @@ -164,8 +164,12 @@ <!-- Create Customer Account --> <createData entity="Simple_US_Customer" stepKey="createCustomer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Place order with options according to dataset --> <actionGroup ref="NavigateToNewOrderPageExistingCustomerActionGroup" stepKey="newOrder"> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml new file mode 100644 index 0000000000000..0718783534925 --- /dev/null +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontReorderAsGuestTest.xml @@ -0,0 +1,82 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontReorderAsGuestTest"> + <annotations> + <stories value="Reorder"/> + <title value="Make reorder as guest on Frontend"/> + <description value="Make reorder as guest on Frontend"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-34465"/> + <group value="sales"/> + </annotations> + <before> + <!--Create simple product.--> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <!-- Create Customer Account --> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + <!-- Reindex and flush cache --> + <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <magentoCLI command="cache:flush" stepKey="flushCache"/> + </before> + <after> + <deleteData createDataKey="createCustomer" stepKey="deleteCreateCustomer"/> + <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + + <!-- Reindex invalidated indices after product attribute has been created/deleted --> + <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> + </after> + + <!-- Order a product --> + <amOnPage url="{{StorefrontProductPage.url($$createSimpleProduct.custom_attributes[url_key]$$)}}" stepKey="navigateToPDP"/> + <actionGroup ref="StorefrontAddProductToCartActionGroup" stepKey="cartAddSimpleProductToCart"> + <argument name="product" value="$$createSimpleProduct$$"/> + <argument name="productCount" value="1"/> + </actionGroup> + <actionGroup ref="GoToCheckoutFromMinicartActionGroup" stepKey="navigateToCheckout"/> + <waitForPageLoad stepKey="waitFroPaymentSelectionPageLoad"/> + <actionGroup ref="GuestCheckoutFillingShippingSectionActionGroup" stepKey="fillAddress"> + <argument name="customerVar" value="$$createCustomer$$"/> + <argument name="customerAddressVar" value="US_Address_TX"/> + </actionGroup> + <waitForElement selector="{{CheckoutPaymentSection.placeOrder}}" time="30" + stepKey="waitForPlaceOrderButtonVisible"/> + <click selector="{{CheckoutPaymentSection.placeOrder}}" stepKey="placeOrder"/> + <waitForPageLoad stepKey="waitUntilOrderPlaced"/> + <grabTextFrom selector="{{CheckoutSuccessMainSection.orderNumber}}" stepKey="getOrderId"/> + <assertNotEmpty stepKey="assertOrderIdIsNotEmpty" after="getOrderId"> + <actualResult type="const">$getOrderId</actualResult> + </assertNotEmpty> + + <!-- Find the Order on frontend > Navigate to: Orders and Returns --> + <amOnPage url="{{StorefrontGuestOrderSearchPage.url}}" stepKey="amOnOrdersAndReturns"/> + <waitForPageLoad stepKey="waiForStorefrontPage"/> + + <!-- Fill the form with correspondent Order data --> + <actionGroup ref="StorefrontFillOrdersAndReturnsFormActionGroup" stepKey="fillOrder"> + <argument name="orderNumber" value="{$getOrderId}"/> + <argument name="customer" value="$$createCustomer$$"/> + </actionGroup> + + <!-- Click on the "Continue" button --> + <click selector="{{StorefrontGuestOrderSearchSection.continue}}" stepKey="clickContinue"/> + <waitForPageLoad stepKey="waitForPageLoad"/> + + <!-- Click 'Reorder' link --> + <click selector="{{StorefrontGuestOrderViewSection.reorder}}" stepKey="clickReturnLink"/> + <waitForPageLoad stepKey="waitForPageLoad2"/> + + <!--Check that product from order is visible in cart after reorder --> + <seeElement selector="{{CheckoutCartProductSection.ProductLinkByName($$createSimpleProduct.name$$)}}" stepKey="seeProductInCart"/> + </test> +</tests> diff --git a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml index d49ea4cfcbec7..ecf71e2bc80b3 100644 --- a/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml +++ b/app/code/Magento/Sales/Test/Mftf/Test/StorefrontVerifySecureURLRedirectSalesTest.xml @@ -28,11 +28,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php index 4d8b8033f60da..7123a81306ef1 100644 --- a/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php +++ b/app/code/Magento/Sales/Test/Unit/Block/Order/Email/Items/DefaultItemsTest.php @@ -11,7 +11,7 @@ use Magento\Backend\Block\Template\Context; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Layout; -use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item as QuoteItem; use Magento\Sales\Block\Order\Email\Items\DefaultItems; use Magento\Sales\Model\Order\Item as OrderItem; use PHPUnit\Framework\MockObject\MockObject; @@ -20,7 +20,7 @@ class DefaultItemsTest extends TestCase { /** - * @var MockObject|\Magento\Sales\Block\Order\Email\Items\DefaultItem + * @var MockObject|DefaultItems */ protected $block; @@ -39,9 +39,16 @@ class DefaultItemsTest extends TestCase */ protected $objectManager; - /** @var MockObject|Item */ + /** + * @var MockObject|OrderItem + */ protected $itemMock; + /** + * @var MockObject|QuoteItem + */ + protected $quoteItemMock; + /** * Initialize required data */ @@ -54,16 +61,6 @@ protected function setUp(): void ->setMethods(['getBlock']) ->getMock(); - $this->block = $this->objectManager->getObject( - DefaultItems::class, - [ - 'context' => $this->objectManager->getObject( - Context::class, - ['layout' => $this->layoutMock] - ) - ] - ); - $this->priceRenderBlock = $this->getMockBuilder(Template::class) ->disableOriginalConstructor() ->setMethods(['setItem', 'toHtml']) @@ -72,16 +69,47 @@ protected function setUp(): void $this->itemMock = $this->getMockBuilder(OrderItem::class) ->disableOriginalConstructor() ->getMock(); + + $this->quoteItemMock = $this->getMockBuilder(QuoteItem::class) + ->disableOriginalConstructor() + ->setMethods(['getQty']) + ->getMock(); + + $this->block = $this->objectManager->getObject( + DefaultItems::class, + [ + 'context' => $this->objectManager->getObject( + Context::class, + ['layout' => $this->layoutMock] + ), + 'data' => [ + 'item' => $this->quoteItemMock + ] + ] + ); } - public function testGetItemPrice() + /** + * @param float $price + * @param string $html + * @param float $quantity + * @dataProvider getItemPriceDataProvider + * */ + public function testGetItemPrice($price, $html, $quantity) { - $html = '$34.28'; - $this->layoutMock->expects($this->once()) ->method('getBlock') ->with('item_price') ->willReturn($this->priceRenderBlock); + $this->quoteItemMock->expects($this->any()) + ->method('getQty') + ->willReturn($quantity); + $this->itemMock->expects($this->any()) + ->method('setRowTotal') + ->willReturn($price * $quantity); + $this->itemMock->expects($this->any()) + ->method('setBaseRowTotal') + ->willReturn($price * $quantity); $this->priceRenderBlock->expects($this->once()) ->method('setItem') @@ -93,4 +121,15 @@ public function testGetItemPrice() $this->assertEquals($html, $this->block->getItemPrice($this->itemMock)); } + + /** + * @return array + */ + public function getItemPriceDataProvider() + { + return [ + 'get default item price' => [34.28,'$34.28',1.0], + 'get item price with quantity 2.0' => [12.00,'$24.00',2.0] + ]; + } } diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php index 4cf571d3b6108..67f1931cf7bd1 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/CreditmemoFactoryTest.php @@ -49,10 +49,11 @@ class CreditmemoFactoryTest extends TestCase */ protected function setUp(): void { - $this->orderItemMock = $this->createPartialMock( - Item::class, - ['getChildrenItems', 'isDummy', 'getHasChildren', 'getId', 'getParentItemId'] - ); + $this->orderItemMock = $this->getMockBuilder(Item::class) + ->disableOriginalConstructor() + ->onlyMethods(['getChildrenItems', 'isDummy', 'getId', 'getParentItemId']) + ->addMethods(['getHasChildren']) + ->getMock(); $this->orderChildItemOneMock = $this->createPartialMock( Item::class, ['getQtyToRefund', 'getId'] diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php new file mode 100644 index 0000000000000..f7587031337a7 --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Invoice/Total/DiscountTest.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Test\Unit\Model\Order\Invoice\Total; + +use Magento\Sales\Model\Order\Invoice\Total\Discount; +use PHPUnit\Framework\TestCase; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; +use PHPUnit\Framework\MockObject\MockObject; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Invoice; +use Magento\Sales\Model\Order\Invoice\Item as InvoiceItem; +use Magento\Sales\Model\Order\Item as OrderItem; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DiscountTest extends TestCase +{ + /** + * @var Discount + */ + protected $model; + + /** + * @var Order|MockObject + */ + protected $order; + + /** + * @var ObjectManager + */ + protected $objectManager; + + /** + * @var Invoice|MockObject + */ + protected $invoice; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = new ObjectManager($this); + $this->model = $this->objectManager->getObject(Discount::class); + $this->order = $this->createPartialMock(Order::class, [ + 'getInvoiceCollection', + ]); + $this->invoice = $this->createPartialMock(Invoice::class, [ + 'getAllItems', + 'getOrder', + 'roundPrice', + 'isLast', + 'getGrandTotal', + 'getBaseGrandTotal', + 'setGrandTotal', + 'setBaseGrandTotal' + ]); + } + + /** + * Test for collect invoice + * + * @param array $invoiceData + * @dataProvider collectInvoiceData + * @return void + */ + public function testCollectInvoiceWithZeroGrandTotal(array $invoiceData): void + { + //Set up invoice mock + /** @var InvoiceItem[] $invoiceItems */ + $invoiceItems = []; + foreach ($invoiceData as $invoiceItemData) { + $invoiceItems[] = $this->getInvoiceItem($invoiceItemData); + } + $this->invoice->method('getOrder') + ->willReturn($this->order); + $this->order->method('getInvoiceCollection') + ->willReturn([]); + $this->invoice->method('getAllItems') + ->willReturn($invoiceItems); + $this->invoice->method('getGrandTotal') + ->willReturn(15.6801); + $this->invoice->method('getBaseGrandTotal') + ->willReturn(15.6801); + + $this->invoice->expects($this->exactly(1)) + ->method('setGrandTotal') + ->with(0); + $this->invoice->expects($this->exactly(1)) + ->method('setBaseGrandTotal') + ->with(0); + $this->model->collect($this->invoice); + } + + /** + * @return array + */ + public function collectInvoiceData(): array + { + return [ + [ + [ + [ + 'order_item' => [ + 'qty_ordered' => 1, + 'discount_amount' => 5.34, + 'base_discount_amount' => 5.34, + ], + 'is_last' => true, + 'qty' => 1, + ], + [ + 'order_item' => [ + 'qty_ordered' => 1, + 'discount_amount' => 10.34, + 'base_discount_amount' => 10.34, + ], + 'is_last' => true, + 'qty' => 1, + ], + ], + ], + ]; + } + + /** + * Get InvoiceItem + * + * @param $invoiceItemData array + * @return InvoiceItem|MockObject + */ + protected function getInvoiceItem($invoiceItemData) + { + /** @var OrderItem|MockObject $orderItem */ + $orderItem = $this->createPartialMock(OrderItem::class, [ + 'isDummy', + ]); + foreach ($invoiceItemData['order_item'] as $key => $value) { + $orderItem->setData($key, $value); + } + /** @var InvoiceItem|MockObject $invoiceItem */ + $invoiceItem = $this->createPartialMock(InvoiceItem::class, [ + 'getOrderItem', + 'isLast', + ]); + $invoiceItem->method('getOrderItem') + ->willReturn($orderItem); + $invoiceItem->method('isLast') + ->willReturn($invoiceItemData['is_last']); + $invoiceItem->getData('qty', $invoiceItemData['qty']); + return $invoiceItem; + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php index 3b3a2f2816118..d5f0525ae9bbe 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Payment/InfoTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Sales\Test\Unit\Model\Order\Payment; @@ -12,72 +13,90 @@ use Magento\Framework\Registry; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Payment\Helper\Data; -use Magento\Payment\Model\Method; use Magento\Payment\Model\Method\Substitution; use Magento\Payment\Model\MethodInterface; +use Magento\Sales\Api\Data\OrderInterface; use Magento\Sales\Model\Order\Payment\Info; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Exception\LocalizedException; +/** + * Test for \Magento\Sales\Model\Order\Payment\Info. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class InfoTest extends TestCase { - /** @var \Magento\Sales\Model\Order\Payment\Info */ - protected $info; - - /** @var ObjectManagerHelper */ - protected $objectManagerHelper; - - /** @var Context|MockObject */ - protected $contextMock; + /** + * @var Info + */ + private $info; - /** @var Registry|MockObject */ - protected $registryMock; + /** + * @var Data|MockObject + */ + private $paymentHelperMock; - /** @var Data|MockObject */ - protected $paymentHelperMock; + /** + * @var EncryptorInterface|MockObject + */ + private $encryptorInterfaceMock; - /** @var EncryptorInterface|MockObject */ - protected $encryptorInterfaceMock; + /** + * @var Data|MockObject + */ + private $methodInstanceMock; - /** @var Data|MockObject */ - protected $methodInstanceMock; + /** + * @var OrderInterface|MockObject + */ + private $orderMock; + /** + * @inheritdoc + */ protected function setUp(): void { - $this->contextMock = $this->createMock(Context::class); - $this->registryMock = $this->createMock(Registry::class); + $contextMock = $this->createMock(Context::class); + $registryMock = $this->createMock(Registry::class); $this->paymentHelperMock = $this->createPartialMock(Data::class, ['getMethodInstance']); $this->encryptorInterfaceMock = $this->getMockForAbstractClass(EncryptorInterface::class); - $this->methodInstanceMock = $this->getMockBuilder(MethodInterface::class) - ->getMockForAbstractClass(); + $this->methodInstanceMock = $this->getMockForAbstractClass(MethodInterface::class); + $this->orderMock = $this->createMock(OrderInterface::class); - $this->objectManagerHelper = new ObjectManagerHelper($this); - $this->info = $this->objectManagerHelper->getObject( + $objectManagerHelper = new ObjectManagerHelper($this); + $this->info = $objectManagerHelper->getObject( Info::class, [ - 'context' => $this->contextMock, - 'registry' => $this->registryMock, + 'context' => $contextMock, + 'registry' => $registryMock, 'paymentData' => $this->paymentHelperMock, 'encryptor' => $this->encryptorInterfaceMock ] ); + $this->info->setData('order', $this->orderMock); } /** + * Get data cc number + * * @dataProvider ccKeysDataProvider * @param string $keyCc * @param string $keyCcEnc + * @return void */ - public function testGetDataCcNumber($keyCc, $keyCcEnc) + public function testGetDataCcNumber($keyCc, $keyCcEnc): void { // no data was set $this->assertNull($this->info->getData($keyCc)); // we set encrypted data $this->info->setData($keyCcEnc, $keyCcEnc); - $this->encryptorInterfaceMock->expects($this->once())->method('decrypt')->with($keyCcEnc)->willReturn( - $keyCc - ); + $this->encryptorInterfaceMock->expects($this->once()) + ->method('decrypt') + ->with($keyCcEnc) + ->willReturn($keyCc); + $this->assertEquals($keyCc, $this->info->getData($keyCc)); } @@ -86,7 +105,7 @@ public function testGetDataCcNumber($keyCc, $keyCcEnc) * * @return array */ - public function ccKeysDataProvider() + public function ccKeysDataProvider(): array { return [ ['cc_number', 'cc_number_enc'], @@ -94,14 +113,26 @@ public function ccKeysDataProvider() ]; } - public function testGetMethodInstanceWithRealMethod() + /** + * Get method instance with real method + * + * @return void + */ + public function testGetMethodInstanceWithRealMethod(): void { + $storeId = 2; $method = 'real_method'; $this->info->setData('method', $method); + $this->orderMock->expects($this->once()) + ->method('getStoreId') + ->willReturn($storeId); $this->methodInstanceMock->expects($this->once()) ->method('setInfoInstance') ->with($this->info); + $this->methodInstanceMock->expects($this->once()) + ->method('setStore') + ->with($storeId); $this->paymentHelperMock->expects($this->once()) ->method('getMethodInstance') @@ -111,7 +142,12 @@ public function testGetMethodInstanceWithRealMethod() $this->info->getMethodInstance(); } - public function testGetMethodInstanceWithUnrealMethod() + /** + * Get method instance with unreal method + * + * @return void + */ + public function testGetMethodInstanceWithUnrealMethod(): void { $method = 'unreal_method'; $this->info->setData('method', $method); @@ -133,15 +169,26 @@ public function testGetMethodInstanceWithUnrealMethod() $this->info->getMethodInstance(); } - public function testGetMethodInstanceWithNoMethod() + /** + * Get method instance withot method + * + * @return void + */ + public function testGetMethodInstanceWithNoMethod(): void { - $this->expectException('Magento\Framework\Exception\LocalizedException'); + $this->expectException(LocalizedException::class); $this->expectExceptionMessage('The payment method you requested is not available.'); + $this->info->setData('method', false); $this->info->getMethodInstance(); } - public function testGetMethodInstanceRequestedMethod() + /** + * Get method instance requested method + * + * @return void + */ + public function testGetMethodInstanceRequestedMethod(): void { $code = 'real_method'; $this->info->setData('method', $code); @@ -160,40 +207,62 @@ public function testGetMethodInstanceRequestedMethod() $this->assertSame($this->methodInstanceMock, $this->info->getMethodInstance()); } - public function testEncrypt() + /** + * Encrypt test + * + * @return void + */ + public function testEncrypt(): void { $data = 'data'; $encryptedData = 'd1a2t3a4'; - $this->encryptorInterfaceMock->expects($this->once())->method('encrypt')->with($data)->willReturn( - $encryptedData - ); + $this->encryptorInterfaceMock->expects($this->once()) + ->method('encrypt') + ->with($data) + ->willReturn($encryptedData); + $this->assertEquals($encryptedData, $this->info->encrypt($data)); } - public function testDecrypt() + /** + * Decrypt test + * + * @return void + */ + public function testDecrypt(): void { $data = 'data'; $encryptedData = 'd1a2t3a4'; - $this->encryptorInterfaceMock->expects($this->once())->method('decrypt')->with($encryptedData)->willReturn( - $data - ); + $this->encryptorInterfaceMock->expects($this->once()) + ->method('decrypt') + ->with($encryptedData) + ->willReturn($data); + $this->assertEquals($data, $this->info->decrypt($encryptedData)); } - public function testSetAdditionalInformationException() + /** + * Set additional information exception + * + * @return void + */ + public function testSetAdditionalInformationException(): void { - $this->expectException('Magento\Framework\Exception\LocalizedException'); + $this->expectException(LocalizedException::class); $this->info->setAdditionalInformation('object', new \stdClass()); } /** + * Set additional info multiple types + * * @dataProvider additionalInformationDataProvider * @param mixed $key * @param mixed $value + * @return void */ - public function testSetAdditionalInformationMultipleTypes($key, $value = null) + public function testSetAdditionalInformationMultipleTypes($key, $value = null): void { $this->info->setAdditionalInformation($key, $value); $this->assertEquals($value ? [$key => $value] : $key, $this->info->getAdditionalInformation()); @@ -204,7 +273,7 @@ public function testSetAdditionalInformationMultipleTypes($key, $value = null) * * @return array */ - public function additionalInformationDataProvider() + public function additionalInformationDataProvider(): array { return [ [['key1' => 'data1', 'key2' => 'data2'], null], @@ -212,7 +281,12 @@ public function additionalInformationDataProvider() ]; } - public function testGetAdditionalInformationByKey() + /** + * Get additional info by key + * + * @return void + */ + public function testGetAdditionalInformationByKey(): void { $key = 'key'; $value = 'value'; @@ -220,7 +294,12 @@ public function testGetAdditionalInformationByKey() $this->assertEquals($value, $this->info->getAdditionalInformation($key)); } - public function testUnsAdditionalInformation() + /** + * Unsetter additional info + * + * @return void + */ + public function testUnsAdditionalInformation(): void { // set array to additional $data = ['key1' => 'data1', 'key2' => 'data2']; @@ -236,7 +315,12 @@ public function testUnsAdditionalInformation() $this->assertEmpty($this->info->unsAdditionalInformation()->getAdditionalInformation()); } - public function testHasAdditionalInformation() + /** + * Has additional info + * + * @return void + */ + public function testHasAdditionalInformation(): void { $this->assertFalse($this->info->hasAdditionalInformation()); @@ -248,7 +332,12 @@ public function testHasAdditionalInformation() $this->assertTrue($this->info->hasAdditionalInformation()); } - public function testInitAdditionalInformationWithUnserialize() + /** + * Init additional info with unserialize + * + * @return void + */ + public function testInitAdditionalInformationWithUnserialize(): void { $data = ['key1' => 'data1', 'key2' => 'data2']; $this->info->setData('additional_information', $data); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php index a64ad8c53bcf8..91a269300a7fc 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/PaymentTest.php @@ -637,7 +637,7 @@ public function testAcceptApprovePaymentTrue() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -690,7 +690,7 @@ public function testAcceptApprovePaymentFalse($isFraudDetected, $status) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -730,7 +730,7 @@ public function testAcceptApprovePaymentFalseOrderState($isFraudDetected) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -756,7 +756,7 @@ public function testDenyPaymentFalse() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -812,7 +812,7 @@ public function testDenyPaymentNegative($isFraudDetected, $status) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -846,7 +846,7 @@ public function testDenyPaymentNegativeStateReview() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -925,13 +925,13 @@ public function testUpdateOnlineTransactionApproved() $this->invoice->setBaseGrandTotal($baseGrandTotal); $this->mockResultTrueMethods($this->transactionId, $baseGrandTotal, $message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -985,13 +985,13 @@ public function testUpdateOnlineTransactionDenied() $this->mockInvoice($this->transactionId); $this->mockResultFalseMethods($message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -1027,13 +1027,13 @@ public function testUpdateOnlineTransactionDeniedFalse($isFraudDetected, $status $this->assertOrderUpdated(Order::STATE_PAYMENT_REVIEW, $status, $message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -1069,13 +1069,13 @@ public function testUpdateOnlineTransactionDeniedFalseHistoryComment() ->method('addStatusHistoryComment') ->with($message); - $this->order->expects($this->once()) + $this->order->expects($this->exactly(2)) ->method('getStoreId') ->willReturn($storeId); $this->helper->expects($this->once()) ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore') ->with($storeId) ->willReturn($this->paymentMethod); @@ -1144,7 +1144,7 @@ public function testAcceptWithoutInvoiceResultTrue() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) @@ -1178,7 +1178,7 @@ public function testDenyWithoutInvoiceResultFalse() ->method('getMethodInstance') ->willReturn($this->paymentMethod); - $this->paymentMethod->expects($this->once()) + $this->paymentMethod->expects($this->exactly(2)) ->method('setStore')->willReturnSelf(); $this->paymentMethod->expects($this->once()) diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/CreditmemoTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/CreditmemoTest.php new file mode 100644 index 0000000000000..2c62f0fb8122f --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/CreditmemoTest.php @@ -0,0 +1,202 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Pdf; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Address\Renderer; +use Magento\Sales\Model\Order\Creditmemo; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class CreditmemoTest + * + * Tests Sales Order Creditmemo PDF model + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreditmemoTest extends TestCase +{ + /** + * @var \Magento\Sales\Model\Order\Pdf\Invoice + */ + protected $_model; + + /** + * @var \Magento\Sales\Model\Order\Pdf\Config|MockObject + */ + protected $_pdfConfigMock; + + /** + * @var Database|MockObject + */ + protected $databaseMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + protected $scopeConfigMock; + + /** + * @var \Magento\Framework\Filesystem\Directory\Write|MockObject + */ + protected $directoryMock; + + /** + * @var Renderer|MockObject + */ + protected $addressRendererMock; + + /** + * @var \Magento\Payment\Helper\Data|MockObject + */ + protected $paymentDataMock; + + /** + * @var \Magento\Store\Model\App\Emulation + */ + private $appEmulation; + + protected function setUp(): void + { + $this->_pdfConfigMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Pdf\Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->directoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\Write::class); + $this->directoryMock->expects($this->any())->method('getAbsolutePath')->will( + $this->returnCallback( + function ($argument) { + return BP . '/' . $argument; + } + ) + ); + $filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystemMock->expects($this->any()) + ->method('getDirectoryRead') + ->will($this->returnValue($this->directoryMock)); + $filesystemMock->expects($this->any()) + ->method('getDirectoryWrite') + ->will($this->returnValue($this->directoryMock)); + + $this->databaseMock = $this->createMock(Database::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->addressRendererMock = $this->createMock(Renderer::class); + $this->paymentDataMock = $this->createMock(\Magento\Payment\Helper\Data::class); + $this->appEmulation = $this->createMock(\Magento\Store\Model\App\Emulation::class); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->_model = $helper->getObject( + \Magento\Sales\Model\Order\Pdf\Creditmemo::class, + [ + 'filesystem' => $filesystemMock, + 'pdfConfig' => $this->_pdfConfigMock, + 'fileStorageDatabase' => $this->databaseMock, + 'scopeConfig' => $this->scopeConfigMock, + 'addressRenderer' => $this->addressRendererMock, + 'string' => new \Magento\Framework\Stdlib\StringUtils(), + 'paymentData' => $this->paymentDataMock, + 'appEmulation' => $this->appEmulation + ] + ); + } + + public function testInsertLogoDatabaseMediaStorage() + { + $filename = 'image.jpg'; + $path = '/sales/store/logo/'; + $storeId = 1; + + $this->appEmulation->expects($this->once()) + ->method('startEnvironmentEmulation') + ->with( + $storeId, + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ) + ->willReturnSelf(); + $this->appEmulation->expects($this->once()) + ->method('stopEnvironmentEmulation') + ->willReturnSelf(); + $this->_pdfConfigMock->expects($this->once()) + ->method('getRenderersPerProduct') + ->with('creditmemo') + ->will($this->returnValue(['product_type_one' => 'Renderer_Type_One_Product_One'])); + $this->_pdfConfigMock->expects($this->any()) + ->method('getTotals') + ->will($this->returnValue([])); + + $block = $this->getMockBuilder(\Magento\Framework\View\Element\Template::class) + ->disableOriginalConstructor() + ->setMethods(['setIsSecureMode','toPdf']) + ->getMock(); + $block->expects($this->any()) + ->method('setIsSecureMode') + ->willReturn($block); + $block->expects($this->any()) + ->method('toPdf') + ->will($this->returnValue('')); + $this->paymentDataMock->expects($this->any()) + ->method('getInfoBlock') + ->willReturn($block); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->will($this->returnValue('')); + + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->will($this->returnValue(true)); + + $creditmemoMock = $this->createMock(Creditmemo::class); + $orderMock = $this->createMock(Order::class); + $addressMock = $this->createMock(Address::class); + $orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($addressMock); + $orderMock->expects($this->any()) + ->method('getIsVirtual') + ->will($this->returnValue(true)); + $infoMock = $this->createMock(\Magento\Payment\Model\InfoInterface::class); + $orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($infoMock); + $creditmemoMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + $creditmemoMock->expects($this->any()) + ->method('getOrder') + ->willReturn($orderMock); + $creditmemoMock->expects($this->any()) + ->method('getAllItems') + ->willReturn([]); + + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with('sales/identity/logo', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue($filename)); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with('sales/identity/address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue('')); + + $this->directoryMock->expects($this->any()) + ->method('isFile') + ->with($path . $filename) + ->willReturnOnConsecutiveCalls( + $this->returnValue(false), + $this->returnValue(false) + ); + + $this->databaseMock->expects($this->once()) + ->method('saveFileToFilesystem') + ->with($path . $filename); + + $this->_model->getPdf([$creditmemoMock]); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php index 9254abee50175..b628ffc48f81a 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/InvoiceTest.php @@ -21,11 +21,14 @@ use Magento\Sales\Model\Order\Address\Renderer; use Magento\Sales\Model\Order\Invoice; use Magento\Sales\Model\Order\Pdf\Config; +use Magento\Store\Model\App\Emulation; use Magento\Store\Model\ScopeInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** + * + * Tests Sales Order Invoice PDF model * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -66,6 +69,11 @@ class InvoiceTest extends TestCase */ protected $paymentDataMock; + /** + * @var Emulation + */ + private $appEmulation; + protected function setUp(): void { $this->_pdfConfigMock = $this->getMockBuilder(Config::class) @@ -89,6 +97,7 @@ function ($argument) { $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); $this->addressRendererMock = $this->createMock(Renderer::class); $this->paymentDataMock = $this->createMock(Data::class); + $this->appEmulation = $this->createMock(Emulation::class); $helper = new ObjectManager($this); $this->_model = $helper->getObject( @@ -100,7 +109,8 @@ function ($argument) { 'scopeConfig' => $this->scopeConfigMock, 'addressRenderer' => $this->addressRendererMock, 'string' => new StringUtils(), - 'paymentData' => $this->paymentDataMock + 'paymentData' => $this->paymentDataMock, + 'appEmulation' => $this->appEmulation ] ); } @@ -136,7 +146,19 @@ public function testInsertLogoDatabaseMediaStorage() { $filename = 'image.jpg'; $path = '/sales/store/logo/'; - + $storeId = 1; + + $this->appEmulation->expects($this->once()) + ->method('startEnvironmentEmulation') + ->with( + $storeId, + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ) + ->willReturnSelf(); + $this->appEmulation->expects($this->once()) + ->method('stopEnvironmentEmulation') + ->willReturnSelf(); $this->_pdfConfigMock->expects($this->once()) ->method('getRenderersPerProduct') ->with('invoice') @@ -180,6 +202,9 @@ public function testInsertLogoDatabaseMediaStorage() $orderMock->expects($this->any()) ->method('getPayment') ->willReturn($infoMock); + $invoiceMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); $invoiceMock->expects($this->any()) ->method('getOrder') ->willReturn($orderMock); diff --git a/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/ShipmentTest.php b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/ShipmentTest.php new file mode 100644 index 0000000000000..19c61e4d37bdb --- /dev/null +++ b/app/code/Magento/Sales/Test/Unit/Model/Order/Pdf/ShipmentTest.php @@ -0,0 +1,202 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Sales\Test\Unit\Model\Order\Pdf; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Address\Renderer; +use Magento\Sales\Model\Order\Shipment; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Class ShipmentTest + * + * Tests Sales Order Shipment PDF model + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ShipmentTest extends TestCase +{ + /** + * @var \Magento\Sales\Model\Order\Pdf\Invoice + */ + protected $_model; + + /** + * @var \Magento\Sales\Model\Order\Pdf\Config|MockObject + */ + protected $_pdfConfigMock; + + /** + * @var Database|MockObject + */ + protected $databaseMock; + + /** + * @var ScopeConfigInterface|MockObject + */ + protected $scopeConfigMock; + + /** + * @var \Magento\Framework\Filesystem\Directory\Write|MockObject + */ + protected $directoryMock; + + /** + * @var Renderer|MockObject + */ + protected $addressRendererMock; + + /** + * @var \Magento\Payment\Helper\Data|MockObject + */ + protected $paymentDataMock; + + /** + * @var \Magento\Store\Model\App\Emulation + */ + private $appEmulation; + + protected function setUp(): void + { + $this->_pdfConfigMock = $this->getMockBuilder(\Magento\Sales\Model\Order\Pdf\Config::class) + ->disableOriginalConstructor() + ->getMock(); + $this->directoryMock = $this->createMock(\Magento\Framework\Filesystem\Directory\Write::class); + $this->directoryMock->expects($this->any())->method('getAbsolutePath')->will( + $this->returnCallback( + function ($argument) { + return BP . '/' . $argument; + } + ) + ); + $filesystemMock = $this->createMock(\Magento\Framework\Filesystem::class); + $filesystemMock->expects($this->any()) + ->method('getDirectoryRead') + ->will($this->returnValue($this->directoryMock)); + $filesystemMock->expects($this->any()) + ->method('getDirectoryWrite') + ->will($this->returnValue($this->directoryMock)); + + $this->databaseMock = $this->createMock(Database::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + $this->addressRendererMock = $this->createMock(Renderer::class); + $this->paymentDataMock = $this->createMock(\Magento\Payment\Helper\Data::class); + $this->appEmulation = $this->createMock(\Magento\Store\Model\App\Emulation::class); + + $helper = new \Magento\Framework\TestFramework\Unit\Helper\ObjectManager($this); + $this->_model = $helper->getObject( + \Magento\Sales\Model\Order\Pdf\Shipment::class, + [ + 'filesystem' => $filesystemMock, + 'pdfConfig' => $this->_pdfConfigMock, + 'fileStorageDatabase' => $this->databaseMock, + 'scopeConfig' => $this->scopeConfigMock, + 'addressRenderer' => $this->addressRendererMock, + 'string' => new \Magento\Framework\Stdlib\StringUtils(), + 'paymentData' => $this->paymentDataMock, + 'appEmulation' => $this->appEmulation + ] + ); + } + + public function testInsertLogoDatabaseMediaStorage() + { + $filename = 'image.jpg'; + $path = '/sales/store/logo/'; + $storeId = 1; + + $this->appEmulation->expects($this->once()) + ->method('startEnvironmentEmulation') + ->with( + $storeId, + \Magento\Framework\App\Area::AREA_FRONTEND, + true + ) + ->willReturnSelf(); + $this->appEmulation->expects($this->once()) + ->method('stopEnvironmentEmulation') + ->willReturnSelf(); + $this->_pdfConfigMock->expects($this->once()) + ->method('getRenderersPerProduct') + ->with('shipment') + ->will($this->returnValue(['product_type_one' => 'Renderer_Type_One_Product_One'])); + $this->_pdfConfigMock->expects($this->any()) + ->method('getTotals') + ->will($this->returnValue([])); + + $block = $this->getMockBuilder(\Magento\Framework\View\Element\Template::class) + ->disableOriginalConstructor() + ->setMethods(['setIsSecureMode','toPdf']) + ->getMock(); + $block->expects($this->any()) + ->method('setIsSecureMode') + ->willReturn($block); + $block->expects($this->any()) + ->method('toPdf') + ->will($this->returnValue('')); + $this->paymentDataMock->expects($this->any()) + ->method('getInfoBlock') + ->willReturn($block); + + $this->addressRendererMock->expects($this->any()) + ->method('format') + ->will($this->returnValue('')); + + $this->databaseMock->expects($this->any()) + ->method('checkDbUsage') + ->will($this->returnValue(true)); + + $shipmentMock = $this->createMock(Shipment::class); + $orderMock = $this->createMock(Order::class); + $addressMock = $this->createMock(Address::class); + $orderMock->expects($this->any()) + ->method('getBillingAddress') + ->willReturn($addressMock); + $orderMock->expects($this->any()) + ->method('getIsVirtual') + ->will($this->returnValue(true)); + $infoMock = $this->createMock(\Magento\Payment\Model\InfoInterface::class); + $orderMock->expects($this->any()) + ->method('getPayment') + ->willReturn($infoMock); + $shipmentMock->expects($this->any()) + ->method('getStoreId') + ->willReturn($storeId); + $shipmentMock->expects($this->any()) + ->method('getOrder') + ->willReturn($orderMock); + $shipmentMock->expects($this->any()) + ->method('getAllItems') + ->willReturn([]); + + $this->scopeConfigMock->expects($this->at(0)) + ->method('getValue') + ->with('sales/identity/logo', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue($filename)); + $this->scopeConfigMock->expects($this->at(1)) + ->method('getValue') + ->with('sales/identity/address', \Magento\Store\Model\ScopeInterface::SCOPE_STORE, null) + ->will($this->returnValue('')); + + $this->directoryMock->expects($this->any()) + ->method('isFile') + ->with($path . $filename) + ->willReturnOnConsecutiveCalls( + $this->returnValue(false), + $this->returnValue(false) + ); + + $this->databaseMock->expects($this->once()) + ->method('saveFileToFilesystem') + ->with($path . $filename); + + $this->_model->getPdf([$shipmentMock]); + } +} diff --git a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php index 5267686a447cc..0978dda09f7a7 100644 --- a/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php +++ b/app/code/Magento/Sales/Test/Unit/Model/ResourceModel/Order/Handler/AddressTest.php @@ -133,6 +133,66 @@ public function testProcessShippingAddress() $this->assertEquals($this->address, $this->address->process($this->orderMock)); } + /** + * Test processing of the shipping address when shipping address id was not changed. + * setShippingAddressId and saveAttribute methods must not be executed. + */ + public function testProcessShippingAddressNotChanged() + { + $this->orderMock->expects($this->exactly(2)) + ->method('getAddresses') + ->willReturn([$this->addressMock]); + $this->addressMock->expects($this->once()) + ->method('save')->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getBillingAddress') + ->willReturn(null); + $this->orderMock->expects($this->once()) + ->method('getShippingAddress') + ->willReturn($this->addressMock); + $this->addressMock->expects($this->once()) + ->method('getId')->willReturn(1); + $this->orderMock->expects($this->once()) + ->method('getShippingAddressId') + ->willReturn(1); + $this->orderMock->expects($this->never()) + ->method('setShippingAddressId')->willReturnSelf(); + $this->attributeMock->expects($this->never()) + ->method('saveAttribute') + ->with($this->orderMock, ['shipping_address_id'])->willReturnSelf(); + $this->assertEquals($this->address, $this->address->process($this->orderMock)); + } + + /** + * Test processing of the billing address when billing address id was not changed. + * setBillingAddressId and saveAttribute methods must not be executed. + */ + public function testProcessBillingAddressNotChanged() + { + $this->orderMock->expects($this->exactly(2)) + ->method('getAddresses') + ->willReturn([$this->addressMock]); + $this->addressMock->expects($this->once()) + ->method('save')->willReturnSelf(); + $this->orderMock->expects($this->once()) + ->method('getBillingAddress') + ->willReturn($this->addressMock); + $this->orderMock->expects($this->once()) + ->method('getShippingAddress') + ->willReturn(null); + $this->addressMock->expects($this->once()) + ->method('getId')->willReturn(1); + $this->orderMock->expects($this->once()) + ->method('getBillingAddressId') + ->willReturn(1); + $this->orderMock->expects($this->never()) + ->method('setBillingAddressId')->willReturnSelf(); + $this->attributeMock->expects($this->never()) + ->method('saveAttribute') + ->with($this->orderMock, ['billing_address_id'])->willReturnSelf(); + $this->assertEquals($this->address, $this->address->process($this->orderMock)); + } + /** * Test method removeEmptyAddresses */ diff --git a/app/code/Magento/Sales/etc/db_schema.xml b/app/code/Magento/Sales/etc/db_schema.xml index 9ede9a79f7f8b..de062029fb53b 100644 --- a/app/code/Magento/Sales/etc/db_schema.xml +++ b/app/code/Magento/Sales/etc/db_schema.xml @@ -220,7 +220,7 @@ <column xsi:type="varchar" name="shipping_method" nullable="true" length="120"/> <column xsi:type="varchar" name="store_currency_code" nullable="true" length="3" comment="Store Currency Code"/> <column xsi:type="varchar" name="store_name" nullable="true" length="255" comment="Store Name"/> - <column xsi:type="varchar" name="x_forwarded_for" nullable="true" length="32" comment="X Forwarded For"/> + <column xsi:type="varchar" name="x_forwarded_for" nullable="true" length="255" comment="X Forwarded For"/> <column xsi:type="text" name="customer_note" nullable="true" comment="Customer Note"/> <column xsi:type="timestamp" name="created_at" on_update="false" nullable="false" default="CURRENT_TIMESTAMP" comment="Created At"/> diff --git a/app/code/Magento/Sales/i18n/en_US.csv b/app/code/Magento/Sales/i18n/en_US.csv index 44626027bac69..97c1706f975da 100644 --- a/app/code/Magento/Sales/i18n/en_US.csv +++ b/app/code/Magento/Sales/i18n/en_US.csv @@ -117,6 +117,7 @@ Capture,Capture "Total Paid","Total Paid" "Total Refunded","Total Refunded" "Total Due","Total Due" +"Total Canceled","Total Canceled" Edit,Edit "Are you sure you want to send an order email to customer?","Are you sure you want to send an order email to customer?" "This will create an offline refund. To create an online refund, open an invoice and create credit memo for it. Do you want to continue?","This will create an offline refund. To create an online refund, open an invoice and create credit memo for it. Do you want to continue?" diff --git a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml index c9b2f7c8de254..a3904ac09c6b4 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/items/column/name.phtml @@ -4,41 +4,64 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php -/* @var $block \Magento\Sales\Block\Adminhtml\Items\Column\Name */ +/** + * @var $block \Magento\Sales\Block\Adminhtml\Items\Column\Name + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> + +<?php +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper = $block->getData('catalogHelper'); ?> -<?php if ($_item = $block->getItem()) : ?> +<?php if ($_item = $block->getItem()): ?> <div id="order_item_<?= (int) $_item->getId() ?>_title" class="product-title"> <?= $block->escapeHtml($_item->getName()) ?> </div> <div class="product-sku-block"> - <span><?= $block->escapeHtml(__('SKU'))?>:</span> <?= /* @noEscape */ implode('<br />', $this->helper(\Magento\Catalog\Helper\Data::class)->splitSku($block->escapeHtml($block->getSku()))) ?> + <span><?= $block->escapeHtml(__('SKU'))?>:</span> + <?= /* @noEscape */ implode('<br />', $catalogHelper->splitSku($block->escapeHtml($block->getSku()))) ?> </div> - <?php if ($block->getOrderOptions()) : ?> + <?php if ($block->getOrderOptions()): ?> <dl class="item-options"> - <?php foreach ($block->getOrderOptions() as $_option) : ?> + <?php foreach ($block->getOrderOptions() as $_option): ?> <dt><?= $block->escapeHtml($_option['label']) ?>:</dt> <dd> - <?php if (isset($_option['custom_view']) && $_option['custom_view']) : ?> + <?php if (isset($_option['custom_view']) && $_option['custom_view']): ?> <?= /* @noEscape */ $block->getCustomizedOptionValue($_option) ?> - <?php else : ?> + <?php else: ?> <?php $_option = $block->getFormattedOption($_option['value']); ?> <?php $dots = 'dots' . uniqid(); ?> <?php $id = 'id' . uniqid(); ?> - <?= $block->escapeHtml($_option['value'], ['a', 'br']) ?><?php if (isset($_option['remainder']) && $_option['remainder']) : ?><span id="<?= /* @noEscape */ $dots; ?>"> ...</span><span id="<?= /* @noEscape */ $id; ?>"><?= $block->escapeHtml($_option['remainder'], ['a']) ?></span> - <script> + <?= $block->escapeHtml($_option['value'], ['a', 'br']) ?> + <?php if (isset($_option['remainder']) && $_option['remainder']): ?> + <span id="<?= /* @noEscape */ $dots; ?>"> ...</span> + <span id="<?= /* @noEscape */ $id; ?>"> + <?= $block->escapeHtml($_option['remainder'], ['a']) ?> + </span> + <?php $scriptString = <<<script require(['prototype'], function() { - $('<?= /* @noEscape */ $id; ?>').hide(); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $id; ?>').show();}); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseover', function(){$('<?= /* @noEscape */ $dots; ?>').hide();}); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $id; ?>').hide();}); - $('<?= /* @noEscape */ $id; ?>').up().observe('mouseout', function(){$('<?= /* @noEscape */ $dots; ?>').show();}); - }); - </script> + +script; + $scriptString .= "$('" . /* @noEscape */ $id . "').hide();" . PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseover', function(){ $('" . /* @noEscape */ $id . "').show();});" . + PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseover', function(){ $('" . /* @noEscape */ $dots . + "').hide();});" . PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseout', function(){ $('" . /* @noEscape */ $id . + "').hide();});" . PHP_EOL; + $scriptString .= "$('" . /* @noEscape */ $id . + "').up().observe('mouseout', function(){ $('" . /* @noEscape */ $dots . + "').show();});" . PHP_EOL . "});" . PHP_EOL; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> </dd> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml index 05e753c78f4a3..c3a7321a3052f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/comments/view.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($_entity = $block->getEntity()) : ?> +<?php if ($_entity = $block->getEntity()): ?> <div id="comments_block" class="edit-order-comments"> <div class="order-history-block"> <div class="admin__field field-row"> @@ -20,7 +22,7 @@ </div> <div class="admin__field"> <div class="order-history-comments-options"> - <?php if ($block->canSendCommentEmail()) : ?> + <?php if ($block->canSendCommentEmail()): ?> <div class="admin__field admin__field-option"> <input name="comment[is_customer_notified]" type="checkbox" @@ -48,31 +50,41 @@ </div> <ul class="note-list"> - <?php foreach ($_entity->getCommentsCollection(true) as $_comment) : ?> + <?php foreach ($_entity->getCommentsCollection(true) as $_comment): ?> <li> - <span class="note-list-date"><?= /* @noEscape */ $block->formatDate($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> - <span class="note-list-time"><?= /* @noEscape */ $block->formatTime($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> + <span class="note-list-date"> + <?= /* @noEscape */ $block->formatDate($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> + <span class="note-list-time"> + <?= /* @noEscape */ $block->formatTime($_comment->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> <span class="note-list-customer"> <?= $block->escapeHtml(__('Customer')) ?> - <?php if ($_comment->getIsCustomerNotified()) : ?> + <?php if ($_comment->getIsCustomerNotified()): ?> <span class="note-list-customer-notified"><?= $block->escapeHtml(__('Notified')) ?></span> - <?php else : ?> - <span class="note-list-customer-not-notified"><?= $block->escapeHtml(__('Not Notified')) ?></span> + <?php else: ?> + <span class="note-list-customer-not-notified"> + <?= $block->escapeHtml(__('Not Notified')) ?> + </span> <?php endif; ?> </span> - <div class="note-list-comment"><?= $block->escapeHtml($_comment->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?></div> + <div class="note-list-comment"> + <?= $block->escapeHtml($_comment->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?> + </div> </li> <?php endforeach; ?> </ul> </div> -<script> + <?php $scriptString = <<<script require(['prototype'], function(){ submitComment = function() { - submitAndReloadArea($('comments_block').parentNode, '<?= $block->escapeUrl($block->getSubmitUrl()) ?>') + submitAndReloadArea($('comments_block').parentNode, '{$block->escapeJs($block->getSubmitUrl())}') }; if ($('submit_comment_button')) { $('submit_comment_button').observe('click', submitComment); } }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml index e29c1d2db01ce..f1c8b249fe68a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/billing/method/form.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->hasMethods()) : ?> +<?php if ($block->hasMethods()): ?> <div id="order-billing_method_form"> <dl class="admin__payment-methods control"> <?php @@ -13,23 +15,27 @@ $_counter = 0; $currentSelectedMethod = $block->getSelectedMethodCode(); ?> - <?php foreach ($_methods as $_method) : + <?php foreach ($_methods as $_method): $_code = $_method->getCode(); $_counter++; ?> <dt class="admin__field-option"> - <?php if ($_methodsCount > 1) : ?> + <?php if ($_methodsCount > 1): ?> <input id="p_method_<?= $block->escapeHtmlAttr($_code); ?>" value="<?= $block->escapeHtmlAttr($_code); ?>" type="radio" name="payment[method]" title="<?= $block->escapeHtmlAttr($_method->getTitle()); ?>" - onclick="payment.switchMethod('<?= $block->escapeJs($_code); ?>')" - <?php if ($currentSelectedMethod == $_code) : ?> + <?php if ($currentSelectedMethod == $_code): ?> checked="checked" <?php endif; ?> data-validate="{'validate-one-required-by-name':true}" class="admin__control-radio"/> - <?php else :?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "payment.switchMethod('" . $block->escapeJs($_code) . "')", + 'input#p_method_' . $block->escapeJs($_code) + ) ?> + <?php else:?> <span class="no-display"> <input id="p_method_<?= $block->escapeHtmlAttr($_code); ?>" value="<?= $block->escapeHtmlAttr($_code); ?>" @@ -49,19 +55,30 @@ <?php endforeach; ?> </dl> </div> - <script> + <?php $scriptString = <<<script require([ 'mage/apply/main', 'Magento_Sales/order/create/form' ], function(mage) { mage.apply(); - <?php if ($_methodsCount !== 1) : ?> - order.setPaymentMethod('<?= $block->escapeJs($currentSelectedMethod); ?>'); - <?php else : ?> - payment.switchMethod('<?= $block->escapeJs($currentSelectedMethod); ?>'); - <?php endif; ?> + +script; + if ($_methodsCount !== 1): + $scriptString .= <<<script + order.setPaymentMethod('{$block->escapeJs($currentSelectedMethod)}'); +script; + else: + $scriptString .= <<<script + payment.switchMethod('{$block->escapeJs($currentSelectedMethod)}'); +script; + endif; + $scriptString .= <<<script + }); - </script> -<?php else : ?> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php else: ?> <div class="admin__message-empty"><?= $block->escapeHtml(__('No Payment Methods')); ?></div> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml index dfa6b5e6fff79..ae3d8831d276f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/comment.phtml @@ -4,11 +4,16 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Comment $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Comment $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="admin__field field-comment"> - <label for="order-comment" class="admin__field-label"><span><?= $block->escapeHtml(__('Order Comments')) ?></span></label> + <label for="order-comment" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Order Comments')) ?></span> + </label> <div class="admin__field-control"> <textarea id="order-comment" @@ -16,8 +21,10 @@ class="admin__control-textarea"><?= $block->escapeHtml($block->getCommentNote()) ?></textarea> </div> </div> -<script> +<?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ order.commentFieldsBind('order-comment') }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml index 87ef29c7d42ed..469155a80891a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/coupons/form.phtml @@ -5,31 +5,40 @@ */ ?> <?php -/* @var \Magento\Sales\Block\Adminhtml\Order\Create\Coupons $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Coupons $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="admin__field field-apply-coupon-code"> <label class="admin__field-label"><span><?= $block->escapeHtml(__('Apply Coupon Code')) ?></span></label> <div class="admin__field-control"> - <?php if (!$block->getCouponCode()) : ?> + <?php if (!$block->getCouponCode()): ?> <input type="text" class="admin__control-text" id="coupons:code" value="" name="coupon_code" /> <?= $block->getButtonHtml(__('Apply'), 'order.handleOnclickCoupon($F(\'coupons:code\'))') ?> <?php endif; ?> - <?php if ($block->getCouponCode()) : ?> + <?php if ($block->getCouponCode()): ?> <p class="added-coupon-code"> <span><?= $block->escapeHtml($block->getCouponCode()) ?></span> - <a href="#" onclick="order.applyCoupon(''); return false;" title="<?= $block->escapeHtmlAttr(__('Remove Coupon Code')) ?>" + <a href="#" title="<?= $block->escapeHtmlAttr(__('Remove Coupon Code')) ?>" class="action-remove"><span><?= $block->escapeHtml(__('Remove')) ?></span></a> </p> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.applyCoupon('');event.preventDefault();", + 'p.added-coupon-code a.action-remove' + ) ?> <?php endif; ?> - <script> + <?php $isVirtual = ($block->getQuote()->isVirtual() ? 'false' : 'true'); + $scriptString = <<<script require([ "jquery", 'Magento_Ui/js/modal/alert', 'mage/translate', "Magento_Sales/order/create/form" ], function($, alert) { - order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); - order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); + order.overlay('shipping-method-overlay', {$isVirtual}); + order.overlay('address-shipping-overlay', {$isVirtual}); order.handleOnclickCoupon = function (code) { if (!code) { alert({ @@ -40,6 +49,8 @@ } }; }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml index f5edf0949374b..ced1ea5e7b73a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/data.phtml @@ -5,22 +5,39 @@ */ /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Data $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="page-create-order"> - <script> + <?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.setCurrencySymbol('<?= $block->escapeJs($block->getCurrencySymbol($block->getCurrentCurrencyCode())) ?>') + order.setCurrencySymbol('{$block->escapeJs($block->getCurrencySymbol($block->getCurrentCurrencyCode()))}') }); -</script> - <div class="order-details<?php if ($block->getCustomerId()) : ?> order-details-existing-customer<?php endif; ?>"> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <div class="order-details<?php if ($block->getCustomerId()): ?> order-details-existing-customer<?php endif; ?>"> - <div id="order-additional_area" style="display: none" class="admin__page-section order-additional-area"> + <div id="order-additional_area" class="admin__page-section order-additional-area"> <?= $block->getChildHtml('additional_area') ?> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#order-additional_area' + ) ?> - <div id="order-search" style="display: none" class="admin__page-section order-search-items"> + <div id="order-search" class="admin__page-section order-search-items no-display"> <?= $block->getChildHtml('search') ?> </div> + <?= /* @noEscape */ $secureRenderer->renderTag( + 'script', + [], + "var elemOrderSearch = document.querySelector('div#order-search'); + if (elemOrderSearch) { + elemOrderSearch.style.display = 'none'; + elemOrderSearch.classList.remove('no-display'); + }", + false + ) ?> <section id="order-items" class="admin__page-section order-items" data-mage-init='{"loader": {}}'> <?= $block->getChildHtml('items') ?> @@ -60,7 +77,7 @@ </div> </section> - <?php if ($block->getChildBlock('card_validation')) : ?> + <?php if ($block->getChildBlock('card_validation')): ?> <section id="order-card_validation" class="admin__page-section order-card-validation"> <?= $block->getChildHtml('card_validation') ?> </section> @@ -85,7 +102,7 @@ </section> </div> - <?php if ($block->getCustomerId()) : ?> + <?php if ($block->getCustomerId()): ?> <div class="order-sidebar"> <div class="store-switcher order-currency"> <label class="admin__field-label" for="currency_switcher"> @@ -93,14 +110,22 @@ </label> <select id="currency_switcher" class="admin__control-select" - name="order[currency]" - onchange="order.setCurrencyId(this.value); order.setCurrencySymbol(this.options[this.selectedIndex].getAttribute('symbol'));"> - <?php foreach ($block->getAvailableCurrencies() as $_code) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"<?php if ($_code == $block->getCurrentCurrencyCode()) : ?> selected="selected"<?php endif; ?> symbol="<?= $block->escapeHtmlAttr($block->getCurrencySymbol($_code)) ?>"> + name="order[currency]"> + <?php foreach ($block->getAvailableCurrencies() as $_code): ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>" + <?php if ($_code == $block->getCurrentCurrencyCode()): ?> selected="selected"<?php endif; ?> + symbol="<?= $block->escapeHtmlAttr($block->getCurrencySymbol($_code)) ?>"> <?= $block->escapeHtml($block->getCurrencyName($_code)) ?> </option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "order.setCurrencyId(this.value); + order.setCurrencySymbol(this.options[this.selectedIndex].getAttribute('symbol'));", + 'select#currency_switcher' + ) ?> + </div> <div class="customer-current-activity" id="order-sidebar"> <?= $block->getChildHtml('sidebar') ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml index c38acb9b79e47..bd2e08d30ccdd 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form.phtml @@ -5,19 +5,50 @@ */ /** @var \Magento\Sales\Block\Adminhtml\Order\Create\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<form id="edit_form" data-order-config='<?= $block->escapeHtml($block->getOrderDataJson()) ?>' data-load-base-url="<?= $block->escapeUrl($block->getLoadBlockUrl()) ?>" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data"> +<form id="edit_form" data-order-config='<?= $block->escapeHtml($block->getOrderDataJson()) ?>' + data-load-base-url="<?= $block->escapeUrl($block->getLoadBlockUrl()) ?>" + action="<?= $block->escapeUrl($block->getSaveUrl()) ?>" method="post" enctype="multipart/form-data"> <?= $block->getBlockHtml('formkey') ?> <div id="order-message"> <?= $block->getChildHtml('message') ?> </div> - <div id="order-customer-selector" class="fieldset-wrapper order-customer-selector" style="display:<?= /* @noEscape */ $block->getCustomerSelectorDisplay() ?>"> + <div id="order-customer-selector" class="fieldset-wrapper order-customer-selector no-display"> <?= $block->getChildHtml('customer.grid.container') ?> </div> - <div id="order-store-selector" class="fieldset-wrapper" style="display:<?= /* @noEscape */ $block->getStoreSelectorDisplay() ?>"> + <div id="order-store-selector" class="fieldset-wrapper no-display"> <?= $block->getChildHtml('store') ?> </div> - <div id="order-data" style="display:<?= /* @noEscape */ $block->getDataSelectorDisplay() ?>"> + <div id="order-data" class="no-display"> <?= $block->getChildHtml('data') ?> </div> </form> +<?php $scriptString = <<<Script +require(['jquery'], function($){ + 'use strict'; + +Script; +if ($block->getCustomerSelectorDisplay()) { + $scriptString .= <<<Script + $('div#order-customer-selector').css('display', '{$block->getCustomerSelectorDisplay()}'); + $('div#order-customer-selector').removeClass('no-display'); +Script; +} +if ($block->getStoreSelectorDisplay()) { + $scriptString .= <<<Script + $('div#order-store-selector').css('display', '{$block->getStoreSelectorDisplay()}'); + $('div#order-store-selector').removeClass('no-display'); +Script; +} +if ($block->getDataSelectorDisplay()) { + $scriptString .= <<<Script + $('div#order-data').css('display', '{$block->getDataSelectorDisplay()}'); + $('div#order-data').removeClass('no-display'); +Script; +} +$scriptString .= <<<Script +}); +Script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml index 85ca9c8159bcc..39303568f8899 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/account.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account */ +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Form\Account + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="admin__page-section-title <?= $block->escapeHtmlAttr($block->getHeaderCssClass()) ?>"> @@ -14,9 +17,10 @@ <div id="customer_account_fields" class="admin__page-section-content"> <?= $block->getForm()->getHtml() ?> </div> - -<script> +<?php $scriptString = <<<script require(["prototype", "Magento_Sales/order/create/form"], function(){ order.accountFieldsBind($('customer_account_fields')); }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml index 5ce001474f5f5..dc007e4801b41 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/form/address.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Form\Address $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ /** * @var \Magento\Customer\Model\ResourceModel\Address\Collection $addressCollection @@ -12,7 +15,7 @@ $addressCollection = $block->getData('customerAddressCollection'); $addressArray = []; -if ($block->getCustomerId()) : +if ($block->getCustomerId()): $addressArray = $addressCollection->setCustomerFilter([$block->getCustomerId()])->toArray(); endif; @@ -22,28 +25,34 @@ endif; $customerAddressFormatter = $block->getData('customerAddressFormatter'); /** - * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address|\Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Billing\Address| + * \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address $block */ -if ($block->getIsShipping()) : +if ($block->getIsShipping()): $_fieldsContainerId = 'order-shipping_address_fields'; $_addressChoiceContainerId = 'order-shipping_address_choice'; - ?> - <script> + + $addressCollectionJson = /* @noEscape */ $block->getAddressCollectionJson(); + $scriptString= <<<script require(["Magento_Sales/order/create/form"], function(){ - order.shippingAddressContainer = '<?= $block->escapeJs($_fieldsContainerId) ?>'; - order.setAddresses(<?= /* @noEscape */ $block->getAddressCollectionJson() ?>); + order.shippingAddressContainer = '{$block->escapeJs($_fieldsContainerId)}'; + order.setAddresses({$addressCollectionJson}); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php -else : +else: $_fieldsContainerId = 'order-billing_address_fields'; $_addressChoiceContainerId = 'order-billing_address_choice'; ?> - <script> + <?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.billingAddressContainer = '<?= $block->escapeJs($_fieldsContainerId) ?>'; + order.billingAddressContainer = '{$block->escapeJs($_fieldsContainerId)}'; }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> @@ -52,33 +61,47 @@ endif; ?> <span><?= $block->escapeHtml($block->getHeaderText()) ?></span> </legend><br> - <fieldset id="<?= $block->escapeHtmlAttr($_addressChoiceContainerId) ?>" class="admin__fieldset order-choose-address"> - <?php if ($block->getIsShipping()) : ?> + <fieldset id="<?= $block->escapeHtmlAttr($_addressChoiceContainerId) ?>" + class="admin__fieldset order-choose-address"> + <?php if ($block->getIsShipping()): ?> <div class="admin__field admin__field-option admin__field-shipping-same-as-billing"> <input type="checkbox" id="order-shipping_same_as_billing" name="shipping_same_as_billing" - onclick="order.setShippingAsBilling(this.checked)" class="admin__control-checkbox" - <?php if ($block->getIsAsBilling()) : ?>checked<?php endif; ?> /> + class="admin__control-checkbox" + <?php if ($block->getIsAsBilling()): ?>checked<?php endif; ?> /> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.setShippingAsBilling(this.checked)", + 'input#order-shipping_same_as_billing' + ) ?> <label for="order-shipping_same_as_billing" class="admin__field-label"> <?= $block->escapeHtml(__('Same As Billing Address')) ?> </label> </div> <?php endif; ?> <div class="admin__field admin__field-select-from-existing-address"> - <label class="admin__field-label"><?= $block->escapeHtml(__('Select from existing customer addresses:')) ?></label> + <label class="admin__field-label"> + <?= $block->escapeHtml(__('Select from existing customer addresses:')) ?> + </label> <?php $_id = $block->getForm()->getHtmlIdPrefix() . 'customer_address_id' ?> <div class="admin__field-control"> <select id="<?= $block->escapeHtmlAttr($_id) ?>" - name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[customer_address_id]" - onchange="order.selectAddress(this, '<?= $block->escapeJs($_fieldsContainerId) ?>')" + name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) + ?>[customer_address_id]" class="admin__control-select"> <option value=""><?= $block->escapeHtml(__('Add New Address')) ?></option> - <?php foreach ($addressArray as $addressId => $address) : ?> + <?php foreach ($addressArray as $addressId => $address): ?> <option - value="<?= $block->escapeHtmlAttr($addressId) ?>"<?php if ($addressId == $block->getAddressId()) : ?> selected="selected"<?php endif; ?>> + value="<?= $block->escapeHtmlAttr($addressId) ?>" + <?php if ($addressId == $block->getAddressId()): ?> selected="selected"<?php endif; ?>> <?= $block->escapeHtml($customerAddressFormatter->getAddressAsString($address)) ?> </option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "order.selectAddress(this, '" . $block->escapeJs($_fieldsContainerId) . "')", + 'select#' . $block->escapeJs($_id) + ) ?> </div> </div> </fieldset> @@ -87,23 +110,40 @@ endif; ?> <?= $block->getForm()->toHtml() ?> <div class="admin__field admin__field-option order-save-in-address-book"> - <input name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[save_in_address_book]" type="checkbox" id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" value="1"<?php if (!$block->getDontSaveInAddressBook()) : ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> + <input name="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlNamePrefix()) ?>[save_in_address_book]" + type="checkbox" + id="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" + value="1" + <?php if (!$block->getDontSaveInAddressBook()): ?> checked="checked"<?php endif; ?> + class="admin__control-checkbox"/> <label for="<?= $block->escapeHtmlAttr($block->getForm()->getHtmlIdPrefix()) ?>save_in_address_book" class="admin__field-label"><?= $block->escapeHtml(__('Save in address book')) ?></label> </div> </div> <?php $hideElement = 'address-' . ($block->getIsShipping() ? 'shipping' : 'billing') . '-overlay'; ?> - <div style="display: none;" id="<?= /* @noEscape */ $hideElement ?>" class="order-methods-overlay"> + <div id="<?= /* @noEscape */ $hideElement ?>" class="order-methods-overlay"> <span><?= $block->escapeHtml(__('You don\'t need to select a shipping address.')) ?></span> </div> - - <script> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display: none;", + 'div#' . /* @noEscape */ $hideElement + ) ?> + <?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.bindAddressFields('<?= $block->escapeJs($_fieldsContainerId) ?>'); - order.bindAddressFields('<?= $block->escapeJs($_addressChoiceContainerId) ?>'); - <?php if ($block->getIsShipping() && $block->getIsAsBilling()) : ?> + order.bindAddressFields('{$block->escapeJs($_fieldsContainerId)}'); + order.bindAddressFields('{$block->escapeJs($_addressChoiceContainerId)}'); + +script; + if ($block->getIsShipping() && $block->getIsAsBilling()): + $scriptString .= <<<script order.disableShippingAddress(true); - <?php endif; ?> + +script; + endif; + $scriptString .= <<<script }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </fieldset> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml index d27782fd20b15..baf283e673e40 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/giftmessage.phtml @@ -4,25 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Giftmessage $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Giftmessage $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> +<?php /** @var \Magento\GiftMessage\Helper\Message $giftMessageHelper */ +$giftMessageHelper = $block->getData('giftMessageHelper'); ?> -<?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())) : ?> +<?php if ($giftMessageHelper->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())): ?> <?php $_items = $block->getItems(); ?> <div id="order-giftmessage" class="giftmessage-order-create"> <fieldset class="admin__fieldset"> - <legend class="admin__legend"><span><?= $block->escapeHtml(__('Gift Message for the Entire Order')) ?></span></legend> + <legend class="admin__legend"> + <span><?= $block->escapeHtml(__('Gift Message for the Entire Order')) ?></span> + </legend> <br> - <?php if ($this->helper(\Magento\GiftMessage\Helper\Message::class)->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())) : ?> - <p><?= $block->escapeHtml(__('Leave this box blank if you don\'t want to leave a gift message for the entire order.')) ?></p> + <?php if ($giftMessageHelper->isMessagesAllowed('main', $block->getQuote(), $block->getStoreId())): ?> + <p> + <?= $block->escapeHtml( + __('Leave this box blank if you don\'t want to leave a gift message for the entire order.') + ) ?> + </p> <?= $block->getFormHtml($block->getQuote(), 'main') ?> <?php endif; ?> </fieldset> - <script> + <?php $scriptString = <<<script require(['Magento_Sales/order/create/form'], function(){ order.giftmessageFieldsBind('order-giftmessage'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml index eee167dde50d6..ada5cb36cdbb0 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/items/grid.phtml @@ -4,16 +4,19 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php /** * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Items\Grid + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var \Magento\Catalog\Helper\Data $catalogHelper */ +$catalogHelper =$block->getData('catalogHelper'); ?> <?php $_items = $block->getItems() ?> -<?php if (empty($_items)) : ?> +<?php if (empty($_items)): ?> <div id="order-items_grid"> <div class="admin__table-wrapper"> <table class="data-table admin__table-primary order-tables"> @@ -36,9 +39,9 @@ </table> </div> </div> -<?php else : ?> +<?php else: ?> <div class="admin__table-wrapper" id="order-items_grid"> - <?php if (count($_items) > 10) : ?> + <?php if (count($_items) > 10): ?> <div class="actions update actions-update"> <?= $block->getButtonHtml(__('Update Items and Quantities'), 'order.itemsUpdate()', 'action-secondary') ?> </div> @@ -59,21 +62,31 @@ <tr> <td class="col-total"><?= $block->escapeHtml(__('Total %1 product(s)', count($_items))) ?></td> <td colspan="2" class="col-subtotal"><?= $block->escapeHtml(__('Subtotal:')) ?></td> - <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotal()) ?></strong></td> - <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getDiscountAmount()) ?></strong></td> - <td class="col-price"><strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotalWithDiscount()); ?></strong></td> + <td class="col-price"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotal()) ?></strong> + </td> + <td class="col-price"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getDiscountAmount()) ?></strong> + </td> + <td class="col-price"> + <strong><?= /* @noEscape */ $block->formatPrice($block->getSubtotalWithDiscount()); ?></strong> + </td> <td colspan="2"> </td> </tr> </tfoot> <?php $i = 0 ?> - <?php foreach ($_items as $_item) : $i++ ?> + <?php foreach ($_items as $_item): $i++ ?> <tbody class="<?= /* @noEscape */ ($i%2) ? 'even' : 'odd' ?>"> <tr> <td class="col-product"> - <span id="order_item_<?= (int) $_item->getId() ?>_title"><?= $block->escapeHtml($_item->getName()) ?></span> + <span id="order_item_<?= (int) $_item->getId() ?>_title"><?= + $block->escapeHtml($_item->getName()) ?></span> <div class="product-sku-block"> <span><?= $block->escapeHtml(__('SKU')) ?>:</span> - <?= /* @noEscape */ implode('<br />', $this->helper(\Magento\Catalog\Helper\Data::class)->splitSku($block->escapeHtml($_item->getSku()))) ?> + <?= /* @noEscape */ implode( + '<br />', + $catalogHelper->splitSku($block->escapeHtml($_item->getSku())) + ) ?> </div> <div class="product-configure-block"> <?= $block->getConfigureButtonHtml($_item) ?> @@ -84,33 +97,56 @@ <?= $block->getItemUnitPriceHtml($_item) ?> <?php $_isCustomPrice = $block->usedCustomPriceForItem($_item) ?> - <?php if ($_tier = $block->getTierHtml($_item)) : ?> - <div id="item_tier_block_<?= (int) $_item->getId() ?>"<?php if ($_isCustomPrice) : ?> style="display:none"<?php endif; ?>> - <a href="#" onclick="$('item_tier_<?= (int) $_item->getId() ?>').toggle();return false;"><?= $block->escapeHtml(__('Tier Pricing')) ?></a> - <div style="display:none" id="item_tier_<?= (int) $_item->getId() ?>"><?= /* @noEscape */ $_tier ?></div> + <?php if ($_tier = $block->getTierHtml($_item)): ?> + <div id="item_tier_block_<?= (int) $_item->getId() ?>"> + <a href="#"><?= $block->escapeHtml(__('Tier Pricing')) ?></a> + <div id="item_tier_<?= (int) $_item->getId() ?>"><?= /* @noEscape */ $_tier ?></div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#item_tier_' . (int) $_item->getId() + ) ?> </div> + <?php if ($_isCustomPrice): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'div#item_tier_block_' . (int) $_item->getId() + ) ?> + <?php endif; ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "$('item_tier_" . (int) $_item->getId() ."').toggle();event.preventDefault();", + 'div#item_tier_block_' . (int) $_item->getId() . ' a' + ) ?> <?php endif; ?> - <?php if ($block->canApplyCustomPrice($_item)) : ?> + <?php if ($block->canApplyCustomPrice($_item)): ?> <div class="custom-price-block"> <input type="checkbox" class="admin__control-checkbox" id="item_use_custom_price_<?= (int) $_item->getId() ?>" - <?php if ($_isCustomPrice) : ?> checked="checked"<?php endif; ?> - onclick="order.toggleCustomPrice(this, 'item_custom_price_<?= (int) $_item->getId() ?>', 'item_tier_block_<?= (int) $_item->getId() ?>');"/> + <?php if ($_isCustomPrice): ?> checked="checked"<?php endif; ?> /> <label class="normal admin__field-label" for="item_use_custom_price_<?= (int) $_item->getId() ?>"> <span><?= $block->escapeHtml(__('Custom Price')) ?>*</span></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.toggleCustomPrice(this, 'item_custom_price_" . (int) $_item->getId() . + "', 'item_tier_block_" . (int) $_item->getId() . "');", + 'input#item_use_custom_price_' . (int) $_item->getId() + ) ?> </div> <?php endif; ?> <input id="item_custom_price_<?= (int) $_item->getId() ?>" name="item[<?= (int) $_item->getId() ?>][custom_price]" value="<?= /* @noEscape */ sprintf("%.2f", $block->getOriginalEditablePrice($_item)) ?>" - <?php if (!$_isCustomPrice) : ?> - style="display:none" + <?php if (!$_isCustomPrice): ?> disabled="disabled" <?php endif; ?> class="input-text item-price admin__control-text"/> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none", + 'input#item_custom_price_' . (int) $_item->getId() + ) ?> </td> <td class="col-qty"> <input name="item[<?= (int) $_item->getId() ?>][qty]" @@ -127,7 +163,7 @@ <input id="item_use_discount_<?= (int) $_item->getId() ?>" class="admin__control-checkbox" name="item[<?= (int) $_item->getId() ?>][use_discount]" - <?php if (!$_item->getNoDiscount()) : ?>checked="checked"<?php endif; ?> + <?php if (!$_item->getNoDiscount()): ?>checked="checked"<?php endif; ?> value="1" type="checkbox" /> <label @@ -144,16 +180,19 @@ <select class="admin__control-select" name="item[<?= (int) $_item->getId() ?>][action]"> <option value=""><?= $block->escapeHtml(__('Please select')) ?></option> <option value="remove"><?= $block->escapeHtml(__('Remove')) ?></option> - <?php if ($block->getCustomerId() && $block->getMoveToCustomerStorage()) : ?> + <?php if ($block->getCustomerId() && $block->getMoveToCustomerStorage()): ?> <option value="cart"><?= $block->escapeHtml(__('Move to Shopping Cart')) ?></option> - <?php if ($block->isMoveToWishlistAllowed($_item)) : ?> + <?php if ($block->isMoveToWishlistAllowed($_item)): ?> <?php $wishlists = $block->getCustomerWishlists();?> - <?php if (count($wishlists) <= 1) : ?> - <option value="wishlist"><?= $block->escapeHtml(__('Move to Wish List')) ?></option> - <?php else : ?> + <?php if (count($wishlists) <= 1): ?> + <option value="wishlist"><?= $block->escapeHtml(__('Move to Wish List')) ?> + </option> + <?php else: ?> <optgroup label="<?= $block->escapeHtml(__('Move to Wish List')) ?>"> - <?php foreach ($wishlists as $wishlist) :?> - <option value="wishlist_<?= (int) $wishlist->getId() ?>"><?= $block->escapeHtml($wishlist->getName()) ?></option> + <?php foreach ($wishlists as $wishlist):?> + <option value="wishlist_<?= (int) $wishlist->getId() ?>"> + <?= $block->escapeHtml($wishlist->getName()) ?> + </option> <?php endforeach;?> </optgroup> <?php endif; ?> @@ -164,21 +203,22 @@ </tr> <?php $hasMessageError = false; ?> - <?php foreach ($_item->getMessage(false) as $messageError) : ?> - <?php if (!empty($messageError)) : + <?php foreach ($_item->getMessage(false) as $messageError): ?> + <?php if (!empty($messageError)): $hasMessageError = true; endif; ?> <?php endforeach; ?> - <?php if ($hasMessageError) : ?> + <?php if ($hasMessageError): ?> <tr class="row-messages-error"> <td colspan="100"> <!-- ToDo UI: remove the 100 --> - <?php foreach ($_item->getMessage(false) as $message) : + <?php foreach ($_item->getMessage(false) as $message): if (empty($message)) { continue; } ?> - <div class="message <?php if ($_item->getHasError()) : ?>message-error<?php else : ?>message-notice<?php endif; ?>"> + <div class="message <?php if ($_item->getHasError()): ?>message-error<?php else: + ?>message-notice<?php endif; ?>"> <?= $block->escapeHtml($message) ?> </div> <?php endforeach; ?> @@ -198,15 +238,17 @@ <div id="order-coupons" class="order-coupons"><?= $block->getChildHtml() ?></div> </div> - <script> + <?php $scriptString = <<<script require([ 'Magento_Sales/order/create/form' ], function(){ order.itemsOnchangeBind() }); - </script> - <?php if ($block->isGiftMessagesAvailable()) : ?> - <script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <?php if ($block->isGiftMessagesAvailable()): ?> + <?php $scriptString = <<<script require([ "prototype", "Magento_Sales/order/giftoptions_tooltip" @@ -241,6 +283,9 @@ //]]> }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml index eb39f71265cd6..0eb3caac12318 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/js.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require([ "prototype", "Magento_Sales/order/create/form", @@ -14,11 +18,14 @@ require([ order.sidebarHide(); if (window.productConfigure) { productConfigure.addListType('product_to_add', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureProductToAdd'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('sales/order_create/configureProductToAdd'))}' }); productConfigure.addListType('quote_items', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureQuoteItems'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('sales/order_create/configureQuoteItems'))}' }); } }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml index 1dcf57d879543..34f4bae1947e1 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/newsletter/form.phtml @@ -3,5 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<input type="checkbox" name="newsletter:subscribe"> <label for="newsletter:subscribe" style="width: 90%; float: none;"><?= $block->escapeHtml(__('Subscribe to Newsletter')) ?></label><br/> +<input type="checkbox" name="newsletter:subscribe"> +<label for="newsletter:subscribe"> + <?= $block->escapeHtml(__('Subscribe to Newsletter')) ?> +</label> +<?=/* @noEscape */ $secureRenderer->renderStyleAsTag("width: 90%; float: none;", "label[for='newsletter:subscribe']") ?> +<br/> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml index baaf4c078f2c7..fd5b7a55b4960 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/shipping/method/form.phtml @@ -4,40 +4,57 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> +<?php +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); ?> -<?php /** @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Method\Form */ ?> <?php $_shippingRateGroups = $block->getShippingRates(); ?> -<?php if ($_shippingRateGroups) : ?> - <div id="order-shipping-method-choose" class="control" style="display:none"> +<?php if ($_shippingRateGroups): ?> + <div id="order-shipping-method-choose" class="control"> <dl class="admin__order-shipment-methods"> - <?php foreach ($_shippingRateGroups as $code => $_rates) : ?> - <dt class="admin__order-shipment-methods-title"><?= $block->escapeHtml($block->getCarrierName($code)) ?></dt> + <?php foreach ($_shippingRateGroups as $code => $_rates): ?> + <dt class="admin__order-shipment-methods-title"><?= $block->escapeHtml($block->getCarrierName($code)) ?> + </dt> <dd class="admin__order-shipment-methods-options"> <ul class="admin__order-shipment-methods-options-list"> - <?php foreach ($_rates as $_rate) : ?> - <?php $_radioProperty = 'name="order[shipping_method]" type="radio" onclick="order.setShippingMethod(this.value)"' ?> + <?php foreach ($_rates as $_rate): ?> + <?php $_radioProperty = 'name="order[shipping_method]" type="radio"' ?> <?php $_code = $_rate->getCode() ?> <li class="admin__field-option"> - <?php if ($_rate->getErrorMessage()) : ?> + <?php if ($_rate->getErrorMessage()): ?> <div class="messages"> <div class="message message-error error"> <div><?= $block->escapeHtml($_rate->getErrorMessage()) ?></div> </div> </div> - <?php else : ?> + <?php else: ?> <?php $_checked = $block->isMethodActive($_code) ? 'checked="checked"' : '' ?> - <input <?= /* @noEscape */ $_radioProperty ?> value="<?= $block->escapeHtmlAttr($_code) ?>" - id="s_method_<?= $block->escapeHtmlAttr($_code) ?>" <?= /* @noEscape */ $_checked ?> - class="admin__control-radio required-entry"/> + <input <?= /* @noEscape */ $_radioProperty ?> + value="<?= $block->escapeHtmlAttr($_code) ?>" + id="s_method_<?= $block->escapeHtmlAttr($_code) ?>" <?= /* @noEscape */ $_checked ?> + class="admin__control-radio required-entry"/> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.setShippingMethod(this.value)", + 'input#s_method_' . $block->escapeHtmlAttr($_code) + ) ?> <label class="admin__field-label" for="s_method_<?= $block->escapeHtmlAttr($_code) ?>"> - <?= $block->escapeHtml($_rate->getMethodTitle() ? $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - + <?= $block->escapeHtml($_rate->getMethodTitle() ? + $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - <strong> - <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()); ?> + <?php $_excl = $block->getShippingPrice( + $_rate->getPrice(), + $taxHelper->displayShippingPriceIncludingTax() + ); ?> <?php $_incl = $block->getShippingPrice($_rate->getPrice(), true); ?> <?= /* @noEscape */ $_excl ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </strong> @@ -50,57 +67,83 @@ <?php endforeach; ?> </dl> </div> - <?php if ($_rate = $block->getActiveMethodRate()) : ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none", 'div#order-shipping-method-choose') ?> + <?php if ($_rate = $block->getActiveMethodRate()): ?> <div id="order-shipping-method-info" class="order-shipping-method-info"> <dl class="admin__order-shipment-methods"> <dt class="admin__order-shipment-methods-title"> <?= $block->escapeHtml($block->getCarrierName($_rate->getCarrier())) ?> </dt> <dd class="admin__order-shipment-methods-options"> - <?= $block->escapeHtml($_rate->getMethodTitle() ? $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - + <?= $block->escapeHtml($_rate->getMethodTitle() ? + $_rate->getMethodTitle() : $_rate->getMethodDescription()) ?> - <strong> - <?php $_excl = $block->getShippingPrice($_rate->getPrice(), $this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()); ?> + <?php $_excl = $block->getShippingPrice( + $_rate->getPrice(), + $taxHelper->displayShippingPriceIncludingTax() + ); ?> <?php $_incl = $block->getShippingPrice($_rate->getPrice(), true); ?> <?= /* @noEscape */ $_excl ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </strong> </dd> </dl> <a href="#" - onclick="$('order-shipping-method-info').hide();$('order-shipping-method-choose').show();return false" class="action-default"> <span><?= $block->escapeHtml(__('Click to change shipping method')) ?></span> </a> </div> - <?php else : ?> - <script> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "$('order-shipping-method-info').hide();$('order-shipping-method-choose').show();event.preventDefault()", + 'div#order-shipping-method-info a.action-default' + ) ?> + <?php else: ?> + <?php $scriptString = <<<script require(['prototype'], function(){ $('order-shipping-method-choose').show(); }); -</script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> -<?php elseif ($block->getIsRateRequest()) : ?> +<?php elseif ($block->getIsRateRequest()): ?> <div class="order-shipping-method-summary"> - <strong class="order-shipping-method-not-available"><?= $block->escapeHtml(__('Sorry, no quotes are available for this order.')) ?></strong> + <strong class="order-shipping-method-not-available"> + <?= $block->escapeHtml(__('Sorry, no quotes are available for this order.')) ?> + </strong> </div> -<?php else : ?> +<?php else: ?> <div id="order-shipping-method-summary" class="order-shipping-method-summary"> - <a href="#" onclick="order.loadShippingRates();return false" class="action-default"> + <a href="#" class="action-default"> <span><?= $block->escapeHtml(__('Get shipping methods and rates')) ?></span> </a> <input type="hidden" name="order[has_shipping]" value="" class="required-entry" /> </div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.loadShippingRates();event.preventDefault();", + 'div#order-shipping-method-summary a.action-default' + ) ?> <?php endif; ?> -<div style="display: none;" id="shipping-method-overlay" class="order-methods-overlay"> +<div id="shipping-method-overlay" class="order-methods-overlay"> <span><?= $block->escapeHtml(__('You don\'t need to select a shipping method.')) ?></span> </div> -<script> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#shipping-method-overlay') ?> +<?php $scriptString = <<<script require(["Magento_Sales/order/create/form"], function(){ - order.overlay('shipping-method-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); - order.overlay('address-shipping-overlay', <?php if ($block->getQuote()->isVirtual()) : ?>false<?php else : ?>true<?php endif; ?>); - order.isOnlyVirtualProduct = <?= /* @noEscape */ $block->getQuote()->isVirtual() ? 'true' : 'false'; ?>; + +script; +$scriptString .= "order.overlay('shipping-method-overlay', " . ($block->getQuote()->isVirtual() ? 'false' : 'true') . + ');' . PHP_EOL; +$scriptString .= "order.overlay('address-shipping-overlay', " . ($block->getQuote()->isVirtual() ? 'false' : 'true') . + ');' . PHP_EOL; +$scriptString .= "order.isOnlyVirtualProduct = " . ($block->getQuote()->isVirtual() ? 'true' : 'false') . ';' . PHP_EOL; +$scriptString .= <<<script }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml index d4dea4eb85a57..fe8910dc3e956 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar.phtml @@ -4,15 +4,18 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="customer-current-activity-inner"> <h4 class="customer-activity-title"><?= $block->escapeHtml(__('Customer\'s Activities')) ?></h4> <div class="create-order-sidebar-container"> <?= $block->getChildHtml('top_button') ?> - <?php foreach ($block->getLayout()->getChildBlocks($block->getNameInLayout()) as $_alias => $_child) : ?> - <?php if ($_alias != 'top_button' && $_alias != 'bottom_button') : ?> - <?php if ($block->canDisplay($_child)) : ?> + <?php foreach ($block->getLayout()->getChildBlocks($block->getNameInLayout()) as $_alias => $_child): ?> + <?php if ($_alias != 'top_button' && $_alias != 'bottom_button'): ?> + <?php if ($block->canDisplay($_child)): ?> <div class="order-sidebar-block" id="order-sidebar_<?= $block->escapeHtmlAttr($_alias) ?>"> <?= $block->getChildHtml($_alias) ?> </div> @@ -22,7 +25,7 @@ <?= $block->getChildHtml('bottom_button') ?> </div> </div> -<script> +<?php $scriptString = <<<script require([ "prototype", "Magento_Catalog/catalog/product/composite/configure" @@ -30,12 +33,12 @@ require([ function addSidebarCompositeListType() { productConfigure.addListType('sidebar', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/configureProductToAdd'))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/addConfigured'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('sales/order_create/configureProductToAdd'))}', + urlConfirm: '{$block->escapeJs($block->getUrl('sales/order_create/addConfigured'))}' }); productConfigure.addListType('sidebar_wishlist', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('sales/order_create/addConfigured'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))}', + urlConfirm: '{$block->escapeJs($block->getUrl('sales/order_create/addConfigured'))}' }); } @@ -55,4 +58,6 @@ require([ window.addSidebarCompositeListType = addSidebarCompositeListType; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml index afb58a626ada8..9b5bffcf01eef 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/sidebar/items.phtml @@ -3,13 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\AbstractSidebar */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$blockDataId = $block->getDataId(); +$jsEscapedBlockDataId = $block->escapeJs($blockDataId); ?> -<?php /* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Sidebar\AbstractSidebar */ ?> -<div class="create-order-sidebar-block" id="sidebar_data_<?= $block->escapeHtmlAttr($block->getDataId()) ?>"> +<div class="create-order-sidebar-block" id="sidebar_data_<?= $block->escapeHtmlAttr($blockDataId) ?>"> <div class="head sidebar-title-block"> - <a href="#" class="action-refresh" - title="<?= $block->escapeHtml(__('Refresh')) ?>" - onclick="order.loadArea('sidebar_<?= $block->escapeJs($block->getDataId()) ?>', 'sidebar_data_<?= $block->escapeJs($block->getDataId()) ?>');return false;"> + <a href="#" class="action-refresh" title="<?= $block->escapeHtml(__('Refresh')) ?>"> <span><?= $block->escapeHtml(__('Refresh')) ?></span> </a> <h5 class="create-order-sidebar-label"> @@ -17,23 +20,32 @@ <span class="normal">(<?= $block->escapeHtml($block->getItemCount()) ?>)</span> </h5> </div> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.loadArea( + 'sidebar_" . $jsEscapedBlockDataId ."', + 'sidebar_data_" . $jsEscapedBlockDataId . "' + ); + event.preventDefault();", + 'div#sidebar_data_'. $jsEscapedBlockDataId . ' a.action-refresh' + ) ?> <div class="content"> <div class="auto-scroll"> - <?php if ($block->getItemCount()) : ?> + <?php if ($block->getItemCount()): ?> <table class="admin__table-primary"> <thead> <tr> <th class="col-item"><?= $block->escapeHtml(__('Item')) ?></th> - <?php if ($block->canDisplayItemQty()) : ?> + <?php if ($block->canDisplayItemQty()): ?> <th class="col-qty"><?= $block->escapeHtml(__('Qty')) ?></th> <?php endif; ?> - <?php if ($block->canDisplayPrice()) : ?> + <?php if ($block->canDisplayPrice()): ?> <th class="col-price"><?= $block->escapeHtml(__('Price')) ?></th> <?php endif; ?> - <?php if ($block->canRemoveItems()) : ?> + <?php if ($block->canRemoveItems()): ?> <th class="col-remove"> <span title="<?= $block->escapeHtml(__('Remove')) ?>" class="icon icon-remove"> @@ -52,33 +64,37 @@ </thead> <tbody> - <?php foreach ($block->getItems() as $_item) : ?> - <tr> + <?php foreach ($block->getItems() as $_item): ?> + <tr id="product-id-<?= (int) $block->getProductId($_item) ?>"> <td class="col-item"><?= $block->escapeHtml($_item->getName()) ?></td> - <?php if ($block->canDisplayItemQty()) : ?> + <?php if ($block->canDisplayItemQty()): ?> <td class="col-qty"> <?= (float) $block->getItemQty($_item) ?> </td> <?php endif; ?> - <?php if ($block->canDisplayPrice()) : ?> + <?php if ($block->canDisplayPrice()): ?> <td class="col-price"> <?= /* @noEscape */ $block->getItemPrice($block->getProduct($_item)) ?> </td> <?php endif; ?> - <?php if ($block->canRemoveItems()) : ?> + <?php if ($block->canRemoveItems()): ?> <td class="col-remove"> <div class="admin__field-option"> - <input id="sidebar-remove-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getItemId($_item) ?>" + <input id="sidebar-remove-<?= + $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getItemId($_item) ?>" type="checkbox" class="admin__control-checkbox" name="sidebar[remove][<?= (int) $block->getItemId($_item) ?>]" - value="<?= $block->escapeHtmlAttr($block->getDataId()) ?>" + value="<?= $block->escapeHtmlAttr($blockDataId) ?>" title="<?= $block->escapeHtml(__('Remove')) ?>" /> <label class="admin__field-label" - for="sidebar-remove-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getItemId($_item) ?>"> + for="sidebar-remove-<?= + $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getItemId($_item) ?>"> </label> </div> </td> @@ -86,29 +102,44 @@ <td class="col-add"> <div class="admin__field-option"> - <?php if ($block->isConfigurationRequired($_item->getTypeId()) && $block->getDataId() == 'wishlist') : ?> + <?php if ($block->isConfigurationRequired($_item->getTypeId()) && + $blockDataId == 'wishlist'): ?> <a href="#" class="icon icon-configure" - title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>" - onclick="order.sidebarConfigureProduct('sidebar_wishlist', <?= (int) $block->getProductId($_item) ?>, <?= (int) $block->getItemId($_item) ?>); return false;"> + title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>"> <span><?= $block->escapeHtml(__('Configure and Add to Order')) ?></span> </a> - <?php elseif ($block->isConfigurationRequired($_item->getTypeId())) : ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.sidebarConfigureProduct('sidebar_wishlist', " . + (int) $block->getProductId($_item) . ", " . (int) $block->getItemId($_item) . + ");event.preventDefault();", + 'tr#product-id-' . (int) $block->getProductId($_item) .' a.icon.icon-configure' + ) ?> + <?php elseif ($block->isConfigurationRequired($_item->getTypeId())): ?> <a href="#" class="icon icon-configure" - title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>" - onclick="order.sidebarConfigureProduct('sidebar', <?= (int) $block->getProductId($_item) ?>); return false;"> + title="<?= $block->escapeHtml(__('Configure and Add to Order')) ?>"> <span><?= $block->escapeHtml(__('Configure and Add to Order')) ?></span> </a> - <?php else : ?> - <input id="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getIdentifierId($_item) ?>" + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.sidebarConfigureProduct('sidebar', " . + (int) $block->getProductId($_item) . ");event.preventDefault();", + 'tr#product-id-' . (int) $block->getProductId($_item) . ' a.icon.icon-configure' + ) ?> + <?php else: ?> + <input id="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getIdentifierId($_item) ?>" type="checkbox" class="admin__control-checkbox" - name="sidebar[<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>][<?= (int) $block->getIdentifierId($_item) ?>]" + name="sidebar[<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>][<?= (int) $block->getIdentifierId($_item) ?>]" value="<?= $block->canDisplayItemQty() ? (float) $_item->getQty() : 1 ?>" title="<?= $block->escapeHtml(__('Add To Order')) ?>"/> <label class="admin__field-label" - for="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) ?>-<?= (int) $block->getIdentifierId($_item) ?>"> + for="sidebar-<?= $block->escapeHtmlAttr($block->getSidebarStorageAction()) + ?>-<?= (int) $block->getIdentifierId($_item) ?>"> </label> <?php endif; ?> </div> @@ -117,11 +148,11 @@ <?php endforeach; ?> </tbody> </table> - <?php else : ?> + <?php else: ?> <span class="no-items"><?= $block->escapeHtml(__('No items')) ?></span> <?php endif ?> </div> - <?php if ($block->getItemCount() && $block->canRemoveItems()) : ?> + <?php if ($block->getItemCount() && $block->canRemoveItems()): ?> <?= $block->getChildHtml('empty_customer_cart_button') ?> <?php endif; ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml index 407bd0272e9fd..1c21d51a2df32 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/store/select.phtml @@ -3,17 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Store\Select */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php /* @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Store\Select */ ?> <div class="store-scope form-inline"> <div class="admin__fieldset tree-store-scope"> <?php $showHelpHint = 0; ?> - <?php foreach ($block->getWebsiteCollection() as $_website) : ?> + <?php foreach ($block->getWebsiteCollection() as $_website): ?> <?php $showWebsite = false; ?> - <?php foreach ($block->getGroupCollection($_website) as $_group) : ?> + <?php foreach ($block->getGroupCollection($_website) as $_group): ?> <?php $showGroup = false; ?> - <?php foreach ($block->getStoreCollection($_group) as $_store) : ?> - <?php if ($showWebsite == false) : ?> + <?php foreach ($block->getStoreCollection($_group) as $_store): ?> + <?php if ($showWebsite == false): ?> <?php $showWebsite = true; ?> <div class="admin__field field-website_label"> <label class="admin__field-label" for=""> @@ -21,7 +23,7 @@ </label> <div class="admin__field-control"> <div class="admin__field admin__field-option"> - <?php if ($showHelpHint == 0) : + <?php if ($showHelpHint == 0): echo $block->getHintHtml(); $showHelpHint = 1; endif; ?> @@ -30,29 +32,39 @@ </div> <?php endif; ?> - <?php if ($showGroup == false) : ?> + <?php if ($showGroup == false): ?> <?php $showGroup = true; ?> <div class="admin__field field-group_label"> - <label class="admin__field-label" for=""><span><?= $block->escapeHtml($_group->getName()) ?></span></label> + <label class="admin__field-label" for=""> + <span><?= $block->escapeHtml($_group->getName()) ?></span> + </label> <div class="admin__field-control"></div> </div> <?php endif; ?> <div class="admin__field field-store_label"> - <label class="admin__field-label" for=""><span><?= $block->escapeHtml($_group->getName()) ?></span></label> + <label class="admin__field-label" for=""> + <span><?= $block->escapeHtml($_group->getName()) ?></span> + </label> <div class="admin__field-control"> <div class="nested"> <div class="admin__field admin__field-option"> - <input type="radio" id="store_<?= (int) $_store->getId() ?>" class="admin__control-radio" onclick="order.setStoreId('<?= (int) $_store->getId() ?>')"/> + <input type="radio" + id="store_<?= (int) $_store->getId() ?>" class="admin__control-radio"/> <label class="admin__field-label" for="store_<?= (int) $_store->getId() ?>"> <?= $block->escapeHtml($_store->getName()) ?> </label> + <?= /* @noEscape*/ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "order.setStoreId('" . (int)$_store->getId() . "')", + 'input#store_' . (int)$_store->getId() + ) ?> </div> </div> </div> </div> <?php endforeach; ?> - <?php if ($showGroup) : ?> + <?php if ($showGroup): ?> <?php endif; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml index 9a901d99ae8f8..73f53c2eba03e 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals $block */ +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <legend class="admin__legend"><span><?= $block->escapeHtml(__('Order Totals')) ?></span></legend> <br> @@ -19,15 +22,19 @@ <div class="order-totals-actions"> <div class="admin__field admin__field-option field-append-comments"> <input type="checkbox" id="notify_customer" name="order[comment][customer_note_notify]" - value="1"<?php if ($block->getNoteNotify()) : ?> checked="checked"<?php endif; ?> + value="1"<?php if ($block->getNoteNotify()): ?> checked="checked"<?php endif; ?> class="admin__control-checkbox"/> - <label for="notify_customer" class="admin__field-label"><?= $block->escapeHtml(__('Append Comments')) ?></label> + <label for="notify_customer" class="admin__field-label"> + <?= $block->escapeHtml(__('Append Comments')) ?> + </label> </div> - <?php if ($block->canSendNewOrderConfirmationEmail()) : ?> + <?php if ($block->canSendNewOrderConfirmationEmail()): ?> <div class="admin__field admin__field-option field-email-order-confirmation"> <input type="checkbox" id="send_confirmation" name="order[send_confirmation]" value="1" checked="checked" class="admin__control-checkbox"/> - <label for="send_confirmation" class="admin__field-label"><?= $block->escapeHtml(__('Email Order Confirmation')) ?></label> + <label for="send_confirmation" class="admin__field-label"> + <?= $block->escapeHtml(__('Email Order Confirmation')) ?> + </label> </div> <?php endif; ?> <div class="actions"> @@ -35,7 +42,7 @@ </div> </div> -<script> +<?php $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ @@ -58,4 +65,6 @@ window.notifyCustomerUpdate = notifyCustomerUpdate; window.sendEmailCheckbox = sendEmailCheckbox; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml index 4a55eb609924f..7462e8ac1f87d 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/default.phtml @@ -3,16 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<tr class="<?= $block->escapeHtmlAttr($block->getTotal()->getCode()) ?> row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?><strong><?php endif; ?> +<tr id="totals-default" class="<?= $block->escapeHtmlAttr($block->getTotal()->getCode()) ?> row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?></strong><?php endif; ?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?><strong><?php endif; ?> + <td class="admin__total-amount"> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?><strong><?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> - <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()) : ?></strong><?php endif; ?> + <?php if ($block->getRenderingArea() == $block->getTotal()->getArea()): ?></strong><?php endif; ?> </td> </tr> +<?php if ($block->escapeHtmlAttr($block->getTotal()->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#totals-default td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#totals-default td.admin__total-amount' + ) ?> +<?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml index 4c4f94b5b3bb1..1ff1c68baaa52 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/grandtotal.phtml @@ -6,34 +6,65 @@ /** * @var $block \Magento\Tax\Block\Checkout\Grandtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Tax\Block\Checkout\Grandtotal */ ?> -<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0) : ?> - <tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> +<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0): ?> + <tr id="grand-total-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <strong><?= $block->escapeHtml(__('Grand Total Excl. Tax')) ?></strong> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <strong><?= /* @noEscape */ $block->formatPrice($block->getTotalExclTax()) ?></strong> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> <?= /* @noEscape */ $block->renderTotals('taxes', $block->getColspan()) ?> - <tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <tr id="grand-total-include-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <strong><?= $block->escapeHtml(__('Grand Total Incl. Tax')) ?></strong> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <strong><?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> - <?php else : ?> - <tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> + <?php else: ?> + <tr id="grand-total" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <strong><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></strong> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <strong><?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#grand-total-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml index db204a46f1f94..c842901f7c16a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/shipping.phtml @@ -6,42 +6,83 @@ /** * @var $block \Magento\Tax\Block\Checkout\Shipping + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Tax\Block\Checkout\Shipping */ ?> -<?php if ($block->displayBoth()) :?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> +<?php if ($block->displayBoth()):?> +<tr id="shipping-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getExcludeTaxLabel()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<tr id="shipping-include-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getIncludeTaxLabel()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> -<?php elseif ($block->displayIncludeTax()) : ?> -<tr> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<?php elseif ($block->displayIncludeTax()): ?> +<tr id="shipping-include-tax"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> -<?php else : ?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<?php else: ?> +<tr id="shipping-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#shipping-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml index a63458491baea..91ba11f90ba9a 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/subtotal.phtml @@ -6,33 +6,64 @@ /** * @var $block \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Subtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer * @see \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Subtotal */ ?> -<?php if ($block->displayBoth()) : ?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> +<?php if ($block->displayBoth()): ?> +<tr id="subtotal-exclude-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml(__('Subtotal (Excl. Tax)')) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValueExclTax()) ?> </td> </tr> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-exclude-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-exclude-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<tr id="subtotal-include-tax" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml(__('Subtotal (Incl. Tax)')) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValueInclTax()) ?> </td> </tr> -<?php else : ?> -<tr class="row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-include-tax td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-include-tax td.admin__total-amount' + ) ?> + <?php endif; ?> +<?php else: ?> +<tr id="subtotal-total" class="row-totals"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> </td> </tr> + <?php if ($block->escapeHtmlAttr($block->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-total td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getStyle()), + 'tr#subtotal-total td.admin__total-amount' + ) ?> + <?php endif; ?> <?php endif;?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml index 042b2f5113cac..5c39449d79840 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/create/totals/tax.phtml @@ -4,56 +4,91 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate // phpcs:disable Squiz.PHP.GlobalKeyword.NotAllowed +/** + * @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var \Magento\Sales\Block\Adminhtml\Order\Create\Totals\Tax $block */ - +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); $taxAmount = $block->getTotal()->getValue(); ?> -<?php if (($taxAmount == 0 && $this->helper(\Magento\Tax\Helper\Data::class)->displayZeroTax()) || ($taxAmount > 0)) : +<?php if (($taxAmount == 0 && $taxHelper->displayZeroTax()) || ($taxAmount > 0)): global $taxIter; $taxIter++; ?> - <?php $class = $block->escapeHtmlAttr("{$block->getTotal()->getCode()} " . ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary() ? 'summary-total' : '')); ?> - <tr<?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> - onclick="expandDetails(this, '.summary-details-<?= $block->escapeJs($taxIter) ?>')" - <?php endif; ?> + <?php $class = $block->escapeHtmlAttr("{$block->getTotal()->getCode()} " . ($taxHelper->displayFullSummary() ? + 'summary-total' : '')); ?> + <tr id="tax-summary-<?= $block->escapeHtmlAttr($taxIter) ?>" class="<?= /* @noEscape */ $class ?> row-totals"> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> + <?php if ($taxHelper->displayFullSummary()): ?> <div class="summary-collapse"><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></div> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> <?php endif;?> </td> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice($block->getTotal()->getValue()) ?> </td> </tr> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <?php if ($taxHelper->displayFullSummary()): ?> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "expandDetails(this, '.summary-details-" . $block->escapeJs($taxIter) ."')", + 'tr#tax-summary-' . $block->escapeHtmlAttr($taxIter) + ) ?> + <?php endif; ?> + <?php if ($block->escapeHtmlAttr($block->getTotal()->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary td.admin__total-amount' + ) ?> + <?php endif; ?> + <?php if ($taxHelper->displayFullSummary()): ?> <?php $isTop = 1; ?> - <?php foreach ($block->getTotal()->getFullInfo() as $info) : ?> - <?php if (isset($info['hidden']) && $info['hidden']) : + <?php foreach ($block->getTotal()->getFullInfo() as $info): ?> + <?php if (isset($info['hidden']) && $info['hidden']): continue; endif; ?> <?php $percent = $info['percent']; ?> <?php $amount = $info['amount']; ?> <?php $rates = $info['rates']; ?> - <?php foreach ($rates as $rate) : ?> - <tr class="summary-details-<?= $block->escapeHtmlAttr($taxIter) ?> summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> - <td class="admin__total-mark" style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" colspan="<?= (int) $block->getColspan() ?>"> + <?php foreach ($rates as $rate): ?> + <tr id="tax-summary-details-<?= $block->escapeHtmlAttr($taxIter) ?>" + class="summary-details-<?= $block->escapeHtmlAttr($taxIter) ?> + summary-details<?= ($isTop ? ' summary-details-first' : '') ?>"> + <td class="admin__total-mark" colspan="<?= (int) $block->getColspan() ?>"> <?= $block->escapeHtml($rate['title']) ?> - <?php if ($rate['percent'] !== null) : ?> + <?php if ($rate['percent'] !== null): ?> (<?= (float) $rate['percent'] ?>%) <?php endif; ?> <br /> </td> - <td style="<?= $block->escapeHtmlAttr($block->getTotal()->getStyle()) ?>" class="admin__total-amount"> + <td class="admin__total-amount"> <?= /* @noEscape */ $block->formatPrice(($amount*(float)$rate['percent'])/$percent) ?> </td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "display:none;", + 'tr#tax-summary-details-' . $block->escapeHtmlAttr($taxIter) + ) ?> + <?php if ($block->escapeHtmlAttr($block->getTotal()->getStyle())): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary-details-' . $block->escapeHtmlAttr($taxIter) . ' td.admin__total-mark' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + $block->escapeHtmlAttr($block->getTotal()->getStyle()), + 'tr#tax-summary-details-' . $block->escapeHtmlAttr($taxIter) . ' td.admin__total-amount' + ) ?> + <?php endif; ?> <?php $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml index 2bb085a51e377..8d50e3103b8a1 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/items.phtml @@ -5,6 +5,7 @@ */ /* @var \Magento\Sales\Block\Adminhtml\Order\Creditmemo\Create\Items $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php /** @var Magento\Sales\ViewModel\CreditMemo\Create\UpdateTotalsButton $viewModel */ @@ -142,7 +143,8 @@ $commentText = $block->getCreditmemo()->getCommentText(); </div> </section> -<script> +<?php $scriptString = <<<script + require(['jquery'], function(jQuery){ //<![CDATA[ @@ -220,4 +222,6 @@ window.checkButtonsRelation = checkButtonsRelation; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml index fc624bfd803b6..7aa264b9fd04c 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/creditmemo/create/totals/adjustments.phtml @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_source = $block->getSource() ?> <?php if ($_source): ?> @@ -43,14 +45,14 @@ value="<?= /* @noEscape */ $block->formatValue($_source->getBaseAdjustmentNegative()) ?>" class="input-text admin__control-text not-negative-amount" id="adjustment_negative"/> - <script> + <?php $scriptString = <<<script require(['prototype'], function(){ //<![CDATA[ Validation.addAllThese([ [ 'not-negative-amount', - '<?= $block->escapeJs(__('Please enter a positive number in this field.')) ?>', + '{$block->escapeJs(__('Please enter a positive number in this field.'))}', function (v) { if (v.length) return /^\s*\d+([,.]\d+)*\s*%?\s*$/.test(v); @@ -86,7 +88,9 @@ //]]> }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </td> </tr> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml index 70373f177d8be..10c44cf99471b 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/details.phtml @@ -4,85 +4,165 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/* -store view name = $_order->getStore()->getName() -web site name = $_order->getStore()->getWebsite()->getName() -store name = $_order->getStore()->getGroup()->getName() -*/ - /* @var \Magento\Sales\Block\Adminhtml\Order\Details $block */ -?> -<?php +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + /* @var \Magento\Sales\Model\Order $_order */ -$_order = $block->getOrder() ?> +$_order = $block->getOrder(); +/** @var \Magento\GiftMessage\Helper\Message $giftMessageHelper */ +$giftMessageHelper = $block->getData('giftMessageHelper'); +?> <div> -<?= $block->escapeHtml(__('Customer Name: %1', $_order->getCustomerFirstname() ? $_order->getCustomerName() : $_order->getBillingAddress()->getName())) ?><br /> +<?= $block->escapeHtml(__('Customer Name: %1', $_order->getCustomerFirstname() ? $_order->getCustomerName() : + $_order->getBillingAddress()->getName())) ?><br /> <?= $block->escapeHtml(__('Purchased From: %1', $_order->getStore()->getGroup()->getName())) ?><br /> </div> -<table cellpadding="0" border="0" width="100%" style="border:1px solid #bebcb7; background:#f8f7f5;"> +<table id="order-details" cellpadding="0" border="0" width="100%"> <thead> <tr> - <th align="left" bgcolor="#d9e5ee" style="padding:3px 9px">Item</th> - <th align="center" bgcolor="#d9e5ee" style="padding:3px 9px">Qty</th> - <th align="right" bgcolor="#d9e5ee" width="10%" style="padding:3px 9px">Subtotal</th> + <th align="left" bgcolor="#d9e5ee">Item</th> + <th align="center" bgcolor="#d9e5ee">Qty</th> + <th align="right" bgcolor="#d9e5ee" width="10%">Subtotal</th> </tr> </thead> <tbody> -<?php $i = 0; foreach ($_order->getAllItems() as $_item) : $i++ ?> - <tr <?= $i%2 ? 'bgcolor="#eeeded"' : '' ?>> - <td align="left" valign="top" style="padding:3px 9px"><strong><?= $block->escapeHtml($_item->getName()) ?></strong> - <?php if ($_item->getGiftMessageId() && $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_item->getGiftMessageId())) : ?> +<?php $i = 0; foreach ($_order->getAllItems() as $_item): $i++ ?> + <tr id="item-<?= /* @noEscape */ $i ?>" <?= $i%2 ? 'bgcolor="#eeeded"' : '' ?>> + <td align="left" valign="top"><strong><?= $block->escapeHtml($_item->getName()) ?></strong> + <?php if ($_item->getGiftMessageId() && + $_giftMessage = $giftMessageHelper->getGiftMessage($_item->getGiftMessageId())): ?> <br /><strong><?= $block->escapeHtml(__('Gift Message')) ?></strong> <br /><?= $block->escapeHtml(__('From:')) ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> <br /><?= $block->escapeHtml(__('To:')) ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> <br /><?= $block->escapeHtml(__('Message:')) ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> <?php endif; ?> </td> - <td align="center" valign="top" style="padding:3px 9px"><?= (float) $_item->getQtyOrdered() ?></td> - <td align="right" valign="top" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_item->getRowTotal()) ?></td> + <td align="center" valign="top"><?= (float) $_item->getQtyOrdered() ?></td> + <td align="right" valign="top"><?= /* @noEscape */ $_order->formatPrice($_item->getRowTotal()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'tr#item-' . $i + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'tr#item-' . $i . ' td:nth-child(1)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'tr#item-' . $i . ' td:nth-child(2)' + ) ?> <?php endforeach; ?> </tbody> <tfoot> - <?php if ($_order->getGiftMessageId() && $_giftMessage = $this->helper(\Magento\GiftMessage\Helper\Message::class)->getGiftMessage($_order->getGiftMessageId())) : ?> - <tr> - <td colspan="3" align="left" style="padding:3px 9px"> + <?php if ($_order->getGiftMessageId() && + $_giftMessage = $giftMessageHelper->getGiftMessage($_order->getGiftMessageId())): ?> + <tr id="gift-message"> + <td colspan="3" align="left"> <strong><?= $block->escapeHtml(__('Gift Message')) ?></strong> <br /><?= $block->escapeHtml(__('From:')) ?> <?= $block->escapeHtml($_giftMessage->getSender()) ?> <br /><?= $block->escapeHtml(__('To:')) ?> <?= $block->escapeHtml($_giftMessage->getRecipient()) ?> <br /><?= $block->escapeHtml(__('Message:')) ?><br /> <?= $block->escapeHtml($_giftMessage->getMessage()) ?> </td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#gift-message td' + ) ?> <?php endif; ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Subtotal')) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getSubtotal()) ?></td> + <tr id="subtotal"> + <td colspan="2" align="right"><?= $block->escapeHtml(__('Subtotal')) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice($_order->getSubtotal()) ?></td> </tr> - <?php if ($_order->getDiscountAmount() > 0) : ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml((($_order->getCouponCode()) ? __('Discount (%1)', $_order->getCouponCode()) : __('Discount'))) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice(0.00 - $_order->getDiscountAmount()) ?></td> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#subtotal td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#subtotal td:nth-child(1)' + ) ?> + + <?php if ($_order->getDiscountAmount() > 0): ?> + <tr id="discount"> + <td colspan="2" align="right"><?= $block->escapeHtml((($_order->getCouponCode()) ? + __('Discount (%1)', $_order->getCouponCode()) : __('Discount'))) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice(0.00 - $_order->getDiscountAmount()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#discount td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#discount td:nth-child(1)' + ) ?> <?php endif; ?> - <?php if ($_order->getShippingAmount() || $_order->getShippingDescription()) : ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Shipping & Handling')) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getShippingAmount()) ?></td> + <?php if ($_order->getShippingAmount() || $_order->getShippingDescription()): ?> + <tr id="shipping"> + <td colspan="2" align="right"><?= $block->escapeHtml(__('Shipping & Handling')) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice($_order->getShippingAmount()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#shipping td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#shipping td:nth-child(1)' + ) ?> <?php endif; ?> - <?php if ($_order->getTaxAmount() > 0) : ?> - <tr> - <td colspan="2" align="right" style="padding:3px 9px"><?= $block->escapeHtml(__('Tax')) ?></td> - <td align="right" style="padding:3px 9px"><?= /* @noEscape */ $_order->formatPrice($_order->getTaxAmount()) ?></td> + <?php if ($_order->getTaxAmount() > 0): ?> + <tr id="tax"> + <td colspan="2" align="right"><?= $block->escapeHtml(__('Tax')) ?></td> + <td align="right"><?= /* @noEscape */ $_order->formatPrice($_order->getTaxAmount()) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#tax td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#tax td:nth-child(1)' + ) ?> <?php endif; ?> - <tr bgcolor="#DEE5E8"> - <td colspan="2" align="right" style="padding:3px 9px"><strong style="font-size: larger"><?= $block->escapeHtml(__('Grand Total')) ?></strong></td> - <td align="right" style="padding:6px 9px"><strong style="font-size: larger"><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal()) ?></strong></td> + <tr id="grand-total" bgcolor="#DEE5E8"> + <td colspan="2" align="right"><strong><?= $block->escapeHtml(__('Grand Total')) ?></strong></td> + <td align="right"><strong><?= /* @noEscape */ $_order->formatPrice($_order->getGrandTotal())?></strong></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#grand-total td:nth-child(0)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'font-size: larger', + 'table#order-details tr#grand-total td:nth-child(0) strong' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details tr#grand-total td:nth-child(1)' + ) ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'font-size: larger', + 'table#order-details tr#grand-total td:nth-child(1) strong' + ) ?> </tfoot> </table> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "border:1px solid #bebcb7; background:#f8f7f5;", + 'table#order-details' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details thead tr th:nth-child(0)' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details thead tr th:nth-child(1)' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'padding:3px 9px', + 'table#order-details thead tr th:nth-child(2)' +) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml index da9f0d273af24..cf2311de5dbb0 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/form.phtml @@ -4,13 +4,16 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /* @var \Magento\Sales\Block\Adminhtml\Order\Invoice\Create\Form $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form id="edit_form" class="order-invoice-edit" method="post" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> <?= $block->getBlockHtml('formkey') ?> <?php $_order = $block->getInvoice()->getOrder() ?> + <?php + /** @var \Magento\Tax\Helper\Data $taxHelper */ + $taxHelper = $block->getData('taxHelper'); + ?> <?= $block->getChildHtml('order_info') ?> <section class="admin__page-section"> @@ -18,17 +21,20 @@ <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <div class="admin__page-section-item order-payment-method<?php if ($_order->getIsVirtual()) : ?> order-payment-method-virtual<?php endif; ?>"> + <div class="admin__page-section-item order-payment-method + <?php if ($_order->getIsVirtual()): ?> order-payment-method-virtual<?php endif; ?>"> <div class="admin__page-section-item-title"> <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getChildHtml('order_payment') ?></div> - <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> + <div class="order-payment-currency"> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> + </div> <div class="order-payment-additional"><?= $block->getChildHtml('order_payment_additional') ?></div> </div> </div> - <?php if (!$_order->getIsVirtual()) : ?> + <?php if (!$_order->getIsVirtual()): ?> <div class="admin__page-section-item order-shipping-address"> <?php /*Shipping Address */ ?> <div class="admin__page-section-item-title"> @@ -36,35 +42,45 @@ </div> <div class="admin__page-section-item-content"> <div class="shipping-description-wrapper"> - <div class="shipping-description-title"><?= $block->escapeHtml($_order->getShippingDescription()) ?></div> + <div class="shipping-description-title"> + <?= $block->escapeHtml($_order->getShippingDescription()) ?></div> <div class="shipping-description-content"> <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else : ?> + <?php else: ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> <?= /* @noEscape */ $_excl ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /* @noEscape */ $_incl ?>) <?php endif; ?> </div> </div> - <?php if ($block->canCreateShipment() && $block->canShipPartiallyItem()) : ?> + <?php if ($block->canCreateShipment() && $block->canShipPartiallyItem()): ?> <div class="admin__field admin__field-option"> <input type="checkbox" name="invoice[do_shipment]" id="invoice_do_shipment" value="1" - class="admin__control-checkbox" <?= $block->hasInvoiceShipmentTypeMismatch() ? ' disabled="disabled"' : '' ?> /> + class="admin__control-checkbox" + <?= $block->hasInvoiceShipmentTypeMismatch() ? ' disabled="disabled"' : '' ?> /> <label for="invoice_do_shipment" - class="admin__field-label"><span><?= $block->escapeHtml(__('Create Shipment')) ?></span></label> + class="admin__field-label"> + <span><?= $block->escapeHtml(__('Create Shipment')) ?></span> + </label> </div> - <?php if ($block->hasInvoiceShipmentTypeMismatch()) : ?> - <small><?= $block->escapeHtml(__('Invoice and shipment types do not match for some items on this order. You can create a shipment only after creating the invoice.')) ?></small> + <?php if ($block->hasInvoiceShipmentTypeMismatch()): ?> + <small> + <?= $block->escapeHtml(__( + 'Invoice and shipment types do not match for some items on this order. ' . + 'You can create a shipment only after creating the invoice.' + )) ?> + </small> <?php endif; ?> <?php endif; ?> - <div id="tracking" style="display:none;"><?= $block->getChildHtml('tracking', false) ?></div> + <div id="tracking"><?= $block->getChildHtml('tracking', false) ?></div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'div#tracking') ?> </div> </div> <?php endif; ?> @@ -75,7 +91,10 @@ <?= $block->getChildHtml('order_items') ?> </section> </form> -<script> + +<?php $forcedShipmentCreate = (int) $block->getForcedShipmentCreate(); +$scriptString = <<<script + require(['prototype'], function(){ //<![CDATA[ @@ -91,7 +110,7 @@ require(['prototype'], function(){ } /*forced creating of shipment*/ - var forcedShipmentCreate = <?= (int) $block->getForcedShipmentCreate() ?>; + var forcedShipmentCreate = {$forcedShipmentCreate}; var shipmentElement = $('invoice_do_shipment'); if (forcedShipmentCreate && shipmentElement) { shipmentElement.checked = true; @@ -105,4 +124,6 @@ require(['prototype'], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml index 9837a6b3c209b..37805a68a603f 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/invoice/create/items.phtml @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Sales\Block\Adminhtml\Order\Invoice\Create\Items $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <section class="admin__page-section"> <div class="admin__page-section-title"> - <?php $_itemsGridLabel = $block->getForcedShipmentCreate() ? 'Items to Invoice and Ship' : 'Items to Invoice'; ?> + <?php $_itemsGridLabel = $block->getForcedShipmentCreate() ? 'Items to Invoice and Ship' : 'Items to Invoice';?> <span class="title"><?= $block->escapeHtml(__('%1', $_itemsGridLabel)) ?></span> </div> <div class="admin__page-section-content grid"> @@ -24,7 +27,7 @@ <th class="col-total last"><span><?= $block->escapeHtml(__('Row Total')) ?></span></th> </tr> </thead> - <?php if ($block->canEditQty()) : ?> + <?php if ($block->canEditQty()): ?> <tfoot> <tr> <td colspan="3"> </td> @@ -34,10 +37,10 @@ </tfoot> <?php endif; ?> <?php $_items = $block->getInvoice()->getAllItems() ?> - <?php $_i = 0; foreach ($_items as $_item) : ?> - <?php if ($_item->getOrderItem()->getParentItem()) : + <?php $_i = 0; foreach ($_items as $_item): ?> + <?php if ($_item->getOrderItem()->getParentItem()): continue; - else : + else: $_i++; endif; ?> <tbody class="<?= /* @noEscape */ $_i%2 ? 'even' : 'odd' ?>"> @@ -52,7 +55,7 @@ <?php $orderTotalBar = $block->getChildHtml('order_totalbar'); ?> -<?php if (!empty($orderTotalBar)) : ?> +<?php if (!empty($orderTotalBar)): ?> <section class="admin__page-section"> <?= /* @noEscape */ $orderTotalBar ?> </section> @@ -73,8 +76,11 @@ <span><?= $block->escapeHtml(__('Invoice Comments')) ?></span> </label> <div class="admin__field-control"> - <textarea id="invoice_comment_text" name="invoice[comment_text]" class="admin__control-textarea" - rows="3" cols="5"><?= $block->escapeHtml($block->getInvoice()->getCommentText()) ?></textarea> + <textarea id="invoice_comment_text" + name="invoice[comment_text]" + class="admin__control-textarea" + rows="3" + cols="5"><?= $block->escapeHtml($block->getInvoice()->getCommentText())?></textarea> </div> </div> </div> @@ -86,37 +92,41 @@ </div> <div class="admin__page-section-item-content order-totals-actions"> <?= $block->getChildHtml('invoice_totals') ?> - <?php if ($block->isCaptureAllowed()) : ?> - <?php if ($block->canCapture()) : ?> + <?php if ($block->isCaptureAllowed()): ?> + <?php if ($block->canCapture()): ?> <div class="admin__field"> - <?php - /* - <label for="invoice_do_capture" class="normal"><?= __('Capture Amount') ?></label> - <input type="checkbox" name="invoice[do_capture]" id="invoice_do_capture" value="1" checked/> - */ - ?> - <label for="invoice_do_capture" class="admin__field-label"><?= $block->escapeHtml(__('Amount')) ?></label> + <label for="invoice_do_capture" class="admin__field-label"> + <?= $block->escapeHtml(__('Amount')) ?> + </label> <select class="admin__control-select" name="invoice[capture_case]"> <option value="online"><?= $block->escapeHtml(__('Capture Online')) ?></option> <option value="offline"><?= $block->escapeHtml(__('Capture Offline')) ?></option> <option value="not_capture"><?= $block->escapeHtml(__('Not Capture')) ?></option> </select> </div> - <?php elseif ($block->isGatewayUsed()) :?> + <?php elseif ($block->isGatewayUsed()):?> <input type="hidden" name="invoice[capture_case]" value="offline"/> - <div><?= $block->escapeHtml(__('The invoice will be created offline without the payment gateway.')) ?></div> + <div> + <?= $block->escapeHtml(__( + 'The invoice will be created offline without the payment gateway.' + )) ?> + </div> <?php endif; ?> <?php endif; ?> <div class="admin__field admin__field-option field-append"> <input id="notify_customer" name="invoice[comment_customer_notify]" value="1" type="checkbox" class="admin__control-checkbox" /> - <label class="admin__field-label" for="notify_customer"><?= $block->escapeHtml(__('Append Comments')) ?></label> + <label class="admin__field-label" for="notify_customer"> + <?= $block->escapeHtml(__('Append Comments')) ?> + </label> </div> - <?php if ($block->canSendInvoiceEmail()) : ?> + <?php if ($block->canSendInvoiceEmail()): ?> <div class="admin__field admin__field-option field-email"> <input id="send_email" name="invoice[send_email]" value="1" type="checkbox" class="admin__control-checkbox" /> - <label class="admin__field-label" for="send_email"><?= $block->escapeHtml(__('Email Copy of Invoice')) ?></label> + <label class="admin__field-label" for="send_email"> + <?= $block->escapeHtml(__('Email Copy of Invoice')) ?> + </label> </div> <?php endif; ?> <?= $block->getChildHtml('submit_before') ?> @@ -129,13 +139,16 @@ </div> </section> -<script> +<?php +$enableSubmitButton = (int) !$block->getDisableSubmitButton(); +$scriptString = <<<script + require(['jquery'], function(jQuery){ //<![CDATA[ var submitButtons = jQuery('.submit-button'); var updateButtons = jQuery('.update-button'); -var enableSubmitButtons = <?= (int) !$block->getDisableSubmitButton() ?>; +var enableSubmitButtons = {$enableSubmitButton}; var fields = jQuery('.qty-input'); function enableButtons(buttons) { @@ -193,4 +206,6 @@ window.checkButtonsRelation = checkButtonsRelation; //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml index f8e914a2c9b2f..7dc009a9bc662 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/due.phtml @@ -3,10 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->getCanDisplayTotalDue()) : ?> -<tr> - <td class="label"><strong style="font-size: larger"><?= $block->escapeHtml(__('Total Due')) ?></strong></td> - <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></td> +<?php if ($block->getCanDisplayTotalDue()): ?> +<tr id="total-due"> + <td class="label"><strong><?= $block->escapeHtml(__('Total Due')) ?></strong></td> + + <td class="emph"><?= /* @noEscape */ $block->displayPriceAttribute('total_due', true) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('font-size: larger', 'tr.total-due td.label strong') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('font-size: larger', 'tr.total-due td.emph') ?> <?php endif; ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml index af5d58d47fce1..d501069f1d979 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/grand.phtml @@ -3,19 +3,29 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_source = $block->getSource() ?> <?php $block->setPriceDataObject($_source) ?> -<tr> +<tr id="grand-totals"> <td class="label"> - <strong style="font-size: larger"> - <?php if ($block->getGrandTotalTitle()) : ?> + <strong> + <?php if ($block->getGrandTotalTitle()): ?> <?= $block->escapeHtml($block->getGrandTotalTitle()) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml(__('Grand Total')) ?> <?php endif; ?> </strong> </td> - <td class="emph" style="font-size: larger"><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></td> + <td class="emph"><?= /* @noEscape */ $block->displayPriceAttribute('grand_total', true) ?></td> </tr> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size: larger", + 'tr#grand-totals td.label strong' +) ?> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size: larger", + 'tr#grand-totals td.emph' +) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml index a68fb09fd2058..0ae7f71145dcc 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/totals/tax.phtml @@ -4,26 +4,32 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Sales\Block\Adminhtml\Order\Totals\Tax */ +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\Totals\Tax + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ /** @var $_source \Magento\Sales\Model\Order\Invoice */ $_source = $block->getSource(); $_order = $block->getOrder(); $_fullInfo = $block->getFullTaxInfo(); + +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +/** @var \Magento\Framework\Math\Random $randomHelper */ +$randomHelper = $block->getData('randomHelper'); ?> -<?php if ($block->displayFullSummary() && $_fullInfo) : ?> -<tr class="summary-total" onclick="expandDetails(this, '.summary-details')"> -<?php else : ?> +<?php if ($block->displayFullSummary() && $_fullInfo): ?> +<tr class="summary-total"> +<?php else: ?> <tr> - <?php endif; ?> +<?php endif; ?> <td class="label"> <div class="summary-collapse" tabindex="0"> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <?php if ($taxHelper->displayFullSummary()): ?> <?= $block->escapeHtml(__('Total Tax')) ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml(__('Tax')) ?> <?php endif;?> </div> @@ -32,11 +38,16 @@ $_fullInfo = $block->getFullTaxInfo(); <?= /* @noEscape */ $block->displayAmount($_source->getTaxAmount(), $_source->getBaseTaxAmount()) ?> </td> </tr> -<?php if ($block->displayFullSummary()) : ?> +<?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "expandDetails(this, '.summary-details')", + 'tr.summary-total' +) ?> +<?php if ($block->displayFullSummary()): ?> <?php $isTop = 1; ?> - <?php if (isset($_fullInfo[0]['rates'])) : ?> - <?php foreach ($_fullInfo as $info) : ?> - <?php if (isset($info['hidden']) && $info['hidden']) : + <?php if (isset($_fullInfo[0]['rates'])): ?> + <?php foreach ($_fullInfo as $info): ?> + <?php if (isset($info['hidden']) && $info['hidden']): continue; endif; ?> <?php @@ -47,39 +58,50 @@ $_fullInfo = $block->getFullTaxInfo(); $isFirst = 1; ?> - <?php foreach ($rates as $rate) : ?> - <tr class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> - <?php if ($rate['percent'] !== null) : ?> - <td class="admin__total-mark"><?= $block->escapeHtml($rate['title']) ?> (<?= (float)$rate['percent'] ?>%)<br /></td> - <?php else : ?> + <?php foreach ($rates as $rate): ?> + <tr id="rate-<?= /* @noEscape */ $rate->getId() ?>" + class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>"> + <?php if ($rate['percent'] !== null): ?> + <td class="admin__total-mark"> + <?= $block->escapeHtml($rate['title']) ?> (<?= (float)$rate['percent'] ?>%)<br /> + </td> + <?php else: ?> <td class="admin__total-mark"><?= $block->escapeHtml($rate['title']) ?><br /></td> <?php endif; ?> - <?php if ($isFirst) : ?> - <td rowspan="<?= count($rates) ?>"><?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?></td> + <?php if ($isFirst): ?> + <td rowspan="<?= count($rates) ?>"> + <?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?> + </td> <?php endif; ?> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'tr#rate-' . $rate->getId()) ?> <?php $isFirst = 0; $isTop = 0; ?> <?php endforeach; ?> <?php endforeach; ?> - <?php else : ?> - <?php foreach ($_fullInfo as $info) : ?> + <?php else: ?> + <?php foreach ($_fullInfo as $info): ?> <?php $percent = $info['percent']; $amount = $info['tax_amount']; $baseAmount = $info['base_tax_amount']; $isFirst = 1; + $infoId = $randomHelper->getRandomString(20); ?> - <tr class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>" style="display:none;"> - <?php if ($info['percent'] !== null) : ?> - <td class="admin__total-mark"><?= $block->escapeHtml($info['title']) ?> (<?= (float)$info['percent'] ?>%)<br /></td> - <?php else : ?> + <tr id="info-<?= /* @noEscape */ $infoId ?>" + class="summary-details<?= ($isTop ? ' summary-details-first' : '') ?>"> + <?php if ($info['percent'] !== null): ?> + <td class="admin__total-mark"> + <?= $block->escapeHtml($info['title']) ?> (<?= (float)$info['percent'] ?>%)<br /> + </td> + <?php else: ?> <td class="admin__total-mark"><?= $block->escapeHtml($info['title']) ?><br /></td> <?php endif; ?> <td><?= /* @noEscape */ $block->displayAmount($amount, $baseAmount) ?></td> </tr> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display:none;", 'tr#info-' . $infoId) ?> <?php $isFirst = 0; $isTop = 0; diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml index 16643a29a7fbe..a168a89ed5ef4 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/history.phtml @@ -5,17 +5,22 @@ */ /** @var \Magento\Sales\Block\Adminhtml\Order\View\History $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="order_history_block" class="edit-order-comments"> - <?php if ($block->canAddComment()) : ?> + <?php if ($block->canAddComment()): ?> <div class="order-history-block" id="history_form"> <div class="admin__field"> <label for="history_status" class="admin__field-label"><?= $block->escapeHtml(__('Status')) ?></label> <div class="admin__field-control"> <select name="history[status]" id="history_status" class="admin__control-select"> - <?php foreach ($block->getStatuses() as $_code => $_label) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"<?php if ($_code == $block->getOrder()->getStatus()) : ?> selected="selected"<?php endif; ?>><?= $block->escapeHtml($_label) ?></option> + <?php foreach ($block->getStatuses() as $_code => $_label): ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>" + <?php if ($_code == $block->getOrder()->getStatus()): ?> selected="selected" + <?php endif; ?>> + <?= $block->escapeHtml($_label) ?> + </option> <?php endforeach; ?> </select> </div> @@ -37,7 +42,7 @@ <div class="admin__field"> <div class="order-history-comments-options"> <div class="admin__field admin__field-option"> - <?php if ($block->canSendCommentEmail()) : ?> + <?php if ($block->canSendCommentEmail()): ?> <input name="history[is_customer_notified]" type="checkbox" id="history_notify" @@ -69,30 +74,40 @@ <?php endif;?> <ul class="note-list"> - <?php foreach ($block->getOrder()->getStatusHistoryCollection(true) as $_item) : ?> + <?php foreach ($block->getOrder()->getStatusHistoryCollection(true) as $_item): ?> <li class="note-list-item"> - <span class="note-list-date"><?= /* @noEscape */ $block->formatDate($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> - <span class="note-list-time"><?= /* @noEscape */ $block->formatTime($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?></span> + <span class="note-list-date"> + <?= /* @noEscape */ $block->formatDate($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> + <span class="note-list-time"> + <?= /* @noEscape */ $block->formatTime($_item->getCreatedAt(), \IntlDateFormatter::MEDIUM) ?> + </span> <span class="note-list-status"><?= $block->escapeHtml($_item->getStatusLabel()) ?></span> <span class="note-list-customer"> <?= $block->escapeHtml(__('Customer')) ?> - <?php if ($block->isCustomerNotificationNotApplicable($_item)) : ?> - <span class="note-list-customer-notapplicable"><?= $block->escapeHtml(__('Notification Not Applicable')) ?></span> - <?php elseif ($_item->getIsCustomerNotified()) : ?> + <?php if ($block->isCustomerNotificationNotApplicable($_item)): ?> + <span class="note-list-customer-notapplicable"> + <?= $block->escapeHtml(__('Notification Not Applicable')) ?> + </span> + <?php elseif ($_item->getIsCustomerNotified()): ?> <span class="note-list-customer-notified"><?= $block->escapeHtml(__('Notified')) ?></span> - <?php else : ?> + <?php else: ?> <span class="note-list-customer-not-notified"><?= $block->escapeHtml(__('Not Notified')) ?></span> <?php endif; ?> </span> - <?php if ($_item->getComment()) : ?> - <div class="note-list-comment"><?= $block->escapeHtml($_item->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?></div> + <?php if ($_item->getComment()): ?> + <div class="note-list-comment"> + <?= $block->escapeHtml($_item->getComment(), ['b', 'br', 'strong', 'i', 'u', 'a']) ?> + </div> <?php endif; ?> </li> <?php endforeach; ?> </ul> - <script> + <?php $scriptString = <<<script require(['prototype'], function(){ - if($('order_status'))$('order_status').update('<?= $block->escapeJs($block->escapeHtml($block->getOrder()->getStatusLabel())) ?>'); + if($('order_status'))$('order_status').update('{$block->escapeJs($block->getOrder()->getStatusLabel())}'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> diff --git a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml index 390adb7d5cfce..06f0603a21215 100644 --- a/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml +++ b/app/code/Magento/Sales/view/adminhtml/templates/order/view/tab/info.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Sales\Block\Adminhtml\Order\View\Tab\Info */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_order = $block->getOrder() ?> @@ -20,14 +21,17 @@ <span class="title"><?= $block->escapeHtml(__('Payment & Shipping Method')) ?></span> </div> <div class="admin__page-section-content"> - <div class="admin__page-section-item order-payment-method<?= ($_order->getIsVirtual() ? ' order-payment-method-virtual' : '') ?>"> + <div class="admin__page-section-item order-payment-method<?= ($_order->getIsVirtual() ? + ' order-payment-method-virtual' : '') ?>"> <?php /* Payment Method */ ?> <div class="admin__page-section-item-title"> <span class="title"><?= $block->escapeHtml(__('Payment Information')) ?></span> </div> <div class="admin__page-section-item-content"> <div class="order-payment-method-title"><?= $block->getPaymentHtml() ?></div> - <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> + <div class="order-payment-currency"> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> + </div> <div class="order-payment-additional"> <?= $block->getChildHtml('order_payment_additional') ?> <?= $block->getChildHtml('payment_additional_info') ?> @@ -72,7 +76,7 @@ <?= $block->getChildHtml('popup_window') ?> -<script> +<?php $scriptString = <<<script require([ "prototype", "Magento_Sales/order/giftoptions_tooltip" @@ -87,7 +91,7 @@ require([ var headerLine = null; var contentLine = null; - $$('#gift_options_data_' + itemId + ' .gift-options-tooltip-content').each(function (element) { + \$$('#gift_options_data_' + itemId + ' .gift-options-tooltip-content').each(function (element) { if (element.down(0)) { headerLine = element.down(0).innerHTML; contentLine = element.down(0).next().innerHTML; @@ -104,4 +108,6 @@ require([ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js index e138112ac3f5a..bbdd6f8fe8437 100644 --- a/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js +++ b/app/code/Magento/Sales/view/adminhtml/web/order/create/scripts.js @@ -84,7 +84,7 @@ define([ }, 10); }; - if (jQuery('#' + this.getAreaId('items')).is(':visible')) { + jQuery.async('#order-items .admin__page-section-title', (function () { this.dataArea.onLoad = this.dataArea.onLoad.wrap(function (proceed) { proceed(); this._parent.itemsArea.setNode($(this._parent.getAreaId('items'))); @@ -93,13 +93,15 @@ define([ this.itemsArea.onLoad = this.itemsArea.onLoad.wrap(function (proceed) { proceed(); - if ($(searchAreaId) && !$(searchAreaId).visible() && !$(searchButtonId)) { + if ($(searchAreaId) && !jQuery('#' + searchAreaId).is(':visible') && !$(searchButtonId)) { this.addControlButton(searchButton); } }); this.areasLoaded(); this.itemsArea.onLoad(); - } + + }).bind(this)); + }).bind(this)); jQuery('#edit_form') diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html index d8a8a0baeca98..f3d133a974082 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_new_guest.html @@ -10,7 +10,7 @@ "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var billing.name":"Guest Customer Name (Billing)", +"var order_data.customer_name":"Guest Customer Name (Billing)", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", @@ -30,7 +30,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. diff --git a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html index ed8f592b59638..64e9a61831956 100644 --- a/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/creditmemo_update_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", "var store.frontend_name":"Store Frontend Name", @@ -21,7 +21,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html index c06630fd249ab..b2109fad5478a 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", @@ -30,7 +30,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. diff --git a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html index 289c5113fe285..be3462d0e2fc9 100644 --- a/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/invoice_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", @@ -21,7 +21,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html index dc3a8e9f69aca..0529c66a04d8c 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_new_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var formattedBillingAddress|raw":"Billing Address", "var order_data.email_customer_note|escape|nl2br":"Email Order Note", -"var order.billing_address.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", @@ -29,7 +29,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send an email with a link to track your order."}} diff --git a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html index 1ce0d162ed76e..4f1f6862fab03 100644 --- a/app/code/Magento/Sales/view/frontend/email/order_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/order_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -20,7 +20,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html index 54c7f08506497..85473cb0ee8c6 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var comment|escape|nl2br":"Shipment Comment", @@ -31,7 +31,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans 'If you have questions about your order, you can email us at <a href="mailto:%store_email">%store_email</a>' store_email=$store_email |raw}}{{depend store_phone}} {{trans 'or call us at <a href="tel:%store_phone">%store_phone</a>' store_phone=$store_phone |raw}}{{/depend}}. diff --git a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html index 087cb0ddbf5bc..cf54379d4bb46 100644 --- a/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html +++ b/app/code/Magento/Sales/view/frontend/email/shipment_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -21,7 +21,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml index 0dff0710dd63a..39d6dafe57244 100644 --- a/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/guest/form.phtml @@ -4,6 +4,7 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form class="form form-orders-search" id="oar-widget-orders-and-returns-form" @@ -23,7 +24,9 @@ </div> </div> <div class="field lastname required"> - <label class="label" for="oar-billing-lastname"><span><?= $block->escapeHtml(__('Billing Last Name')) ?></span></label> + <label class="label" for="oar-billing-lastname"> + <span><?= $block->escapeHtml(__('Billing Last Name')) ?></span> + </label> <div class="control"> <input type="text" class="input-text" id="oar-billing-lastname" name="oar_billing_lastname" @@ -31,7 +34,9 @@ </div> </div> <div class="field find required"> - <label class="label" for="quick-search-type-id"><span><?= $block->escapeHtml(__('Find Order By')) ?></span></label> + <label class="label" for="quick-search-type-id"> + <span><?= $block->escapeHtml(__('Find Order By')) ?></span> + </label> <div class="control"> <select name="oar_type" id="quick-search-type-id" class="select"> @@ -48,13 +53,14 @@ data-validate="{required:true, 'validate-email':true}"/> </div> </div> - <div id="oar-zip" style="display: none;" class="field zip required"> + <div id="oar-zip" class="field zip required"> <label class="label" for="oar_zip"><span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span></label> <div class="control"> <input type="text" class="input-text" id="oar_zip" name="oar_zip" data-validate="{required:true}"/> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'div#oar-zip') ?> </fieldset> <div class="actions-toolbar"> <div class="primary"> diff --git a/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml b/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml index 3f12ca1f7b270..90b5f0f289e64 100644 --- a/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/order/print/shipment.phtml @@ -3,26 +3,38 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Sales\Block\Order\PrintOrder\Shipment $block + * @var \Magento\Framework\Escaper $escaper + */ ?> -<?php /* @var $block \Magento\Sales\Block\Order\PrintOrder\Shipment */ ?> <?php $order = $block->getOrder(); ?> -<?php if (!$block->getObjectData($order, 'is_virtual')) : ?> - <?php foreach ($block->getShipmentsCollection() as $shipment) : ?> +<?php if (!$block->getObjectData($order, 'is_virtual')): ?> + <?php foreach ($block->getShipmentsCollection() as $shipment): ?> <div class="order-details-items shipments"> <div class="order-title"> - <strong><?= $block->escapeHtml(__('Shipment #%1', $block->getObjectData($shipment, 'increment_id'))) ?></strong> + <strong> + <?= $escaper->escapeHtml( + __( + 'Shipment #%1', + $block->getObjectData($shipment, 'increment_id') + ) + ) ?> + </strong> </div> <div class="table-wrapper order-items-shipment"> - <table class="data table table-order-items shipment" id="my-shipment-table-<?= (int) $block->getObjectData($shipment, 'id') ?>"> - <caption class="table-caption"><?= $block->escapeHtml(__('Items Invoiced')) ?></caption> + <table class="data table table-order-items shipment" + id="my-shipment-table-<?= (int)$block->getObjectData($shipment, 'id') ?>"> + <caption class="table-caption"><?= $escaper->escapeHtml(__('Items Invoiced')) ?></caption> <thead> - <tr> - <th class="col name"><?= $block->escapeHtml(__('Product Name')) ?></th> - <th class="col sku"><?= $block->escapeHtml(__('SKU')) ?></th> - <th class="col price"><?= $block->escapeHtml(__('Qty Shipped')) ?></th> - </tr> + <tr> + <th class="col name"><?= $escaper->escapeHtml(__('Product Name')) ?></th> + <th class="col sku"><?= $escaper->escapeHtml(__('SKU')) ?></th> + <th class="col price"><?= $escaper->escapeHtml(__('Qty Shipped')) ?></th> + </tr> </thead> - <?php foreach ($block->getShipmentItems($shipment) as $item) : ?> + <?php foreach ($block->getShipmentItems($shipment) as $item): ?> <tbody> <?= $block->getItemHtml($item) ?> </tbody> @@ -31,12 +43,12 @@ </div> <div class="block block-order-details-view"> <div class="block-title"> - <strong><?= $block->escapeHtml(__('Order Information')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Order Information')) ?></strong> </div> <div class="block-content"> <div class="box box-order-shipping-address"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Shipping Address')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Shipping Address')) ?></strong> </div> <div class="box-content"> <address><?= $block->getShipmentAddressFormattedHtml($shipment) ?></address> @@ -45,25 +57,29 @@ <div class="box box-order-shipping-method"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Shipping Method')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Shipping Method')) ?></strong> </div> <div class="box-content"> - <?= $block->escapeHtml($block->getObjectData($order, 'shipping_description')) ?> + <?= $escaper->escapeHtml($block->getObjectData($order, 'shipping_description')) ?> <?php $tracks = $block->getShipmentTracks($shipment); - if ($tracks) : ?> + if ($tracks): ?> <dl class="order-tracking"> - <?php foreach ($tracks as $track) : ?> - <dt class="tracking-title"><?= $block->escapeHtml($block->getObjectData($track, 'title')) ?></dt> - <dd class="tracking-content"><?= $block->escapeHtml($block->getObjectData($track, 'number')) ?></dd> + <?php foreach ($tracks as $track): ?> + <dt class="tracking-title"> + <?= $escaper->escapeHtml($block->getObjectData($track, 'title')) ?> + </dt> + <dd class="tracking-content"> + <?= $escaper->escapeHtml($block->getObjectData($track, 'number')) ?> + </dd> <?php endforeach; ?> </dl> <?php endif; ?> </div> </div> - <div class="box box-order-billing-method"> + <div class="box box-order-billing-address"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Billing Address')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Billing Address')) ?></strong> </div> <div class="box-content"> <address><?= $block->getBillingAddressFormattedHtml($order) ?></address> @@ -72,7 +88,7 @@ <div class="box box-order-billing-method"> <div class="box-title"> - <strong><?= $block->escapeHtml(__('Payment Method')) ?></strong> + <strong><?= $escaper->escapeHtml(__('Payment Method')) ?></strong> </div> <div class="box-content"> <?= $block->getPaymentInfoHtml() ?> diff --git a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml index ba1204fac8ec5..6bdac443f657d 100644 --- a/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/reorder/sidebar.phtml @@ -8,12 +8,15 @@ * Last ordered items sidebar * * @var $block \Magento\Sales\Block\Reorder\Sidebar + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="block block-reorder" data-bind="scope: 'lastOrderedItems'"> <div class="block-title no-display" data-bind="css: {'no-display': !lastOrderedItems().items || lastOrderedItems().items.length === 0}"> - <strong id="block-reorder-heading" role="heading" aria-level="2"><?= $block->escapeHtml(__('Recently Ordered')) ?></strong> + <strong id="block-reorder-heading" role="heading" aria-level="2"> + <?= $block->escapeHtml(__('Recently Ordered')) ?> + </strong> </div> <div class="block-content no-display" data-bind="css: {'no-display': !lastOrderedItems().items || lastOrderedItems().items.length === 0}" @@ -33,7 +36,8 @@ data-bind="attr: { id: 'reorder-item-' + id, value: id, - title: is_saleable ? '<?= $block->escapeHtml(__('Add to Cart')) ?>' : '<?= $block->escapeHtml(__('Product is not salable.')) ?>' + title: is_saleable ? '<?= $block->escapeHtml(__('Add to Cart')) ?>' : + '<?= $block->escapeHtml(__('Product is not salable.')) ?>' }, disable: !is_saleable" class="checkbox" data-validate='{"validate-one-checkbox-required-by-name": true}'/> @@ -50,19 +54,21 @@ <div class="actions-toolbar"> <div class="primary" data-bind="visible: isShowAddToCart"> - <button type="submit" title="<?= $block->escapeHtml(__('Add to Cart')) ?>" class="action tocart primary"> + <button type="submit" title="<?= $block->escapeHtml(__('Add to Cart')) ?>" + class="action tocart primary"> <span><?= $block->escapeHtml(__('Add to Cart')) ?></span> </button> </div> <div class="secondary"> - <a class="action view" href="<?= $block->escapeUrl($block->getUrl('customer/account')) ?>#my-orders-table"> + <a class="action view" + href="<?= $block->escapeUrl($block->getUrl('customer/account')) ?>#my-orders-table"> <span><?= $block->escapeHtml(__('View All')) ?></span> </a> </div> </div> </form> </div> - <script> + <?php $scriptString = <<<script require(["jquery", "mage/mage"], function(jQuery){ jQuery('#reorder-validate-detail').mage('validation', { errorPlacement: function(error, element) { @@ -70,7 +76,9 @@ } }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml b/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml index 25926688c6f47..7772e7b9680fd 100644 --- a/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml +++ b/app/code/Magento/Sales/view/frontend/templates/widget/guest/form.phtml @@ -4,15 +4,19 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Sales\Block\Widget\Guest\Form */ +/** + * @var $block \Magento\Sales\Block\Widget\Guest\Form + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if ($block->isEnable()) : ?> +<?php if ($block->isEnable()): ?> <div class="widget block block-orders-returns"> <div class="block-title"> <strong role="heading" aria-level="2"><?= $block->escapeHtml(__('Orders and Returns')) ?></strong> </div> <div class="block-content"> - <form id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{},"validation":{}}' action="<?= $block->escapeUrl($block->getActionUrl()) ?>" method="post" + <form id="oar-widget-orders-and-returns-form" data-mage-init='{"ordersReturns":{},"validation":{}}' + action="<?= $block->escapeUrl($block->getActionUrl()) ?>" method="post" class="form form-orders-search" name="guest_post"> <fieldset class="fieldset"> <div class="field find required"> @@ -26,10 +30,13 @@ </div> </div> <div class="field id required"> - <label for="oar-order-id" class="label"><span><?= $block->escapeHtml(__('Order ID')) ?></span></label> + <label for="oar-order-id" class="label"> + <span><?= $block->escapeHtml(__('Order ID')) ?></span> + </label> <div class="control"> - <input type="text" class="input-text" id="oar-order-id" name="oar_order_id" autocomplete="off" + <input type="text" class="input-text" id="oar-order-id" name="oar_order_id" + autocomplete="off" data-validate="{required:true}"> </div> </div> @@ -50,14 +57,17 @@ data-validate="{required:true, 'validate-email':true}"> </div> </div> - <div id="oar-zip" style="display: none;" class="field zip required"> - <label for="oar_zip" class="label"><span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span></label> + <div id="oar-zip" class="field zip required"> + <label for="oar_zip" class="label"> + <span><?= $block->escapeHtml(__('Billing ZIP Code')) ?></span> + </label> <div class="control"> <input type="text" class="input-text" id="oar_zip" name="oar_zip" data-validate="{required:true}"/> </div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#oar-zip') ?> </fieldset> <div class="actions-toolbar"> <div class="primary"> diff --git a/app/code/Magento/SalesGraphQl/Model/Order/OrderAddress.php b/app/code/Magento/SalesGraphQl/Model/Order/OrderAddress.php new file mode 100644 index 0000000000000..08e67ee29cbdd --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Order/OrderAddress.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Order; + +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Class to get the order address details + */ +class OrderAddress +{ + /** + * Get the order Shipping address + * + * @param OrderInterface $order + * @return array|null + */ + public function getOrderShippingAddress( + OrderInterface $order + ): ?array { + $shippingAddress = null; + if ($order->getShippingAddress()) { + $shippingAddress = $this->formatAddressData($order->getShippingAddress()); + } + return $shippingAddress; + } + + /** + * Get the order billing address + * + * @param OrderInterface $order + * @return array|null + */ + public function getOrderBillingAddress( + OrderInterface $order + ): ?array { + $billingAddress = null; + if ($order->getBillingAddress()) { + $billingAddress = $this->formatAddressData($order->getBillingAddress()); + } + return $billingAddress; + } + + /** + * Customer Order address data formatter + * + * @param OrderAddressInterface $orderAddress + * @return array + */ + private function formatAddressData( + OrderAddressInterface $orderAddress + ): array { + return + [ + 'firstname' => $orderAddress->getFirstname(), + 'lastname' => $orderAddress->getLastname(), + 'middlename' => $orderAddress->getMiddlename(), + 'postcode' => $orderAddress->getPostcode(), + 'prefix' => $orderAddress->getPrefix(), + 'suffix' => $orderAddress->getSuffix(), + 'street' => $orderAddress->getStreet(), + 'country_code' => $orderAddress->getCountryId(), + 'city' => $orderAddress->getCity(), + 'company' => $orderAddress->getCompany(), + 'fax' => $orderAddress->getFax(), + 'telephone' => $orderAddress->getTelephone(), + 'vat_id' => $orderAddress->getVatId(), + 'region_id' => $orderAddress->getRegionId(), + 'region' => $orderAddress->getRegion() + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Order/OrderPayments.php b/app/code/Magento/SalesGraphQl/Model/Order/OrderPayments.php new file mode 100644 index 0000000000000..991f36663448b --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Order/OrderPayments.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Order; + +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Class to get the order payment details + */ +class OrderPayments +{ + /** + * Get the order payment method + * + * @param OrderInterface $orderModel + * @return array + */ + public function getOrderPaymentMethod(OrderInterface $orderModel): array + { + $orderPayment = $orderModel->getPayment(); + return [ + [ + 'name' => $orderPayment->getAdditionalInformation()['method_title'] ?? '', + 'type' => $orderPayment->getMethod(), + 'additional_data' => [] + ] + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php new file mode 100644 index 0000000000000..a69d9bf58ee8d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/DataProvider.php @@ -0,0 +1,237 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\OrderItem; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\Sales\Api\OrderItemRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; + +/** + * Data provider for order items + */ +class DataProvider +{ + /** + * @var OrderItemRepositoryInterface + */ + private $orderItemRepository; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var OptionsProcessor + */ + private $optionsProcessor; + + /** + * @var int[] + */ + private $orderItemIds = []; + + /** + * @var array + */ + private $orderItemList = []; + + /** + * @param OrderItemRepositoryInterface $orderItemRepository + * @param ProductRepositoryInterface $productRepository + * @param OrderRepositoryInterface $orderRepository + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param OptionsProcessor $optionsProcessor + */ + public function __construct( + OrderItemRepositoryInterface $orderItemRepository, + ProductRepositoryInterface $productRepository, + OrderRepositoryInterface $orderRepository, + SearchCriteriaBuilder $searchCriteriaBuilder, + OptionsProcessor $optionsProcessor + ) { + $this->orderItemRepository = $orderItemRepository; + $this->productRepository = $productRepository; + $this->orderRepository = $orderRepository; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->optionsProcessor = $optionsProcessor; + } + + /** + * Add order item id to list for fetching + * + * @param int $orderItemId + */ + public function addOrderItemId(int $orderItemId): void + { + if (!in_array($orderItemId, $this->orderItemIds)) { + $this->orderItemList = []; + $this->orderItemIds[] = $orderItemId; + } + } + + /** + * Get order item by item id + * + * @param int $orderItemId + * @return array + */ + public function getOrderItemById(int $orderItemId): array + { + $orderItems = $this->fetch(); + if (!isset($orderItems[$orderItemId])) { + return []; + } + return $orderItems[$orderItemId]; + } + + /** + * Fetch order items and return in format for GraphQl + * + * @return array + */ + private function fetch() + { + if (empty($this->orderItemIds) || !empty($this->orderItemList)) { + return $this->orderItemList; + } + + $itemSearchCriteria = $this->searchCriteriaBuilder + ->addFilter(OrderItemInterface::ITEM_ID, $this->orderItemIds, 'in') + ->create(); + + $orderItems = $this->orderItemRepository->getList($itemSearchCriteria)->getItems(); + $productList = $this->fetchProducts($orderItems); + $orderList = $this->fetchOrders($orderItems); + + foreach ($orderItems as $orderItem) { + /** @var ProductInterface $associatedProduct */ + $associatedProduct = $productList[$orderItem->getProductId()] ?? null; + /** @var OrderInterface $associatedOrder */ + $associatedOrder = $orderList[$orderItem->getOrderId()]; + $itemOptions = $this->optionsProcessor->getItemOptions($orderItem); + $this->orderItemList[$orderItem->getItemId()] = [ + 'id' => base64_encode($orderItem->getItemId()), + 'associatedProduct' => $associatedProduct, + 'model' => $orderItem, + 'product_name' => $orderItem->getName(), + 'product_sku' => $orderItem->getSku(), + 'product_url_key' => $associatedProduct ? $associatedProduct->getUrlKey() : null, + 'product_type' => $orderItem->getProductType(), + 'status' => $orderItem->getStatus(), + 'discounts' => $this->getDiscountDetails($associatedOrder, $orderItem), + 'product_sale_price' => [ + 'value' => $orderItem->getPrice(), + 'currency' => $associatedOrder->getOrderCurrencyCode() + ], + 'selected_options' => $itemOptions['selected_options'], + 'entered_options' => $itemOptions['entered_options'], + 'quantity_ordered' => $orderItem->getQtyOrdered(), + 'quantity_shipped' => $orderItem->getQtyShipped(), + 'quantity_refunded' => $orderItem->getQtyRefunded(), + 'quantity_invoiced' => $orderItem->getQtyInvoiced(), + 'quantity_canceled' => $orderItem->getQtyCanceled(), + 'quantity_returned' => $orderItem->getQtyReturned() + ]; + } + + return $this->orderItemList; + } + + /** + * Fetch associated products for order items + * + * @param array $orderItems + * @return array + */ + private function fetchProducts(array $orderItems): array + { + $productIds = array_map( + function ($orderItem) { + return $orderItem->getProductId(); + }, + $orderItems + ); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $productIds, 'in') + ->create(); + $products = $this->productRepository->getList($searchCriteria)->getItems(); + $productList = []; + foreach ($products as $product) { + $productList[$product->getId()] = $product; + } + return $productList; + } + + /** + * Fetch associated order for order items + * + * @param array $orderItems + * @return array + */ + private function fetchOrders(array $orderItems): array + { + $orderIds = array_map( + function ($orderItem) { + return $orderItem->getOrderId(); + }, + $orderItems + ); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('entity_id', $orderIds, 'in') + ->create(); + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + + $orderList = []; + foreach ($orders as $order) { + $orderList[$order->getEntityId()] = $order; + } + return $orderList; + } + + /** + * Returns information about an applied discount + * + * @param OrderInterface $associatedOrder + * @param OrderItemInterface $orderItem + * @return array + */ + private function getDiscountDetails(OrderInterface $associatedOrder, OrderItemInterface $orderItem) : array + { + if ($associatedOrder->getDiscountDescription() === null && $orderItem->getDiscountAmount() == 0 + && $associatedOrder->getDiscountAmount() == 0 + ) { + $discounts = []; + } else { + $discounts [] = [ + 'label' => $associatedOrder->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($orderItem->getDiscountAmount()) ?? 0, + 'currency' => $associatedOrder->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php b/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php new file mode 100644 index 0000000000000..83b7e0cc46d96 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/OrderItem/OptionsProcessor.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\OrderItem; + +use Magento\Sales\Api\Data\OrderItemInterface; + +/** + * Process order item options to format for GraphQl output + */ +class OptionsProcessor +{ + /** + * Get Order item options. + * + * @param OrderItemInterface $orderItem + * @return array + */ + public function getItemOptions(OrderItemInterface $orderItem): array + { + //build options array + $optionsTypes = ['selected_options' => [], 'entered_options' => []]; + $options = $orderItem->getProductOptions(); + if ($options) { + if (isset($options['options'])) { + $optionsTypes = $this->processOptions($options['options']); + } elseif (isset($options['attributes_info'])) { + $optionsTypes = $this->processAttributesInfo($options['attributes_info']); + } + } + return $optionsTypes; + } + + /** + * Process options data + * + * @param array $options + * @return array + */ + private function processOptions(array $options): array + { + $selectedOptions = []; + $enteredOptions = []; + foreach ($options ?? [] as $option) { + if (isset($option['option_type'])) { + if (in_array($option['option_type'], ['field', 'area', 'file', 'date', 'date_time', 'time'])) { + $selectedOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } elseif (in_array($option['option_type'], ['drop_down', 'radio', 'checkbox', 'multiple'])) { + $enteredOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } + } + } + return ['selected_options' => $selectedOptions, 'entered_options' => $enteredOptions]; + } + + /** + * Process attributes info data + * + * @param array $attributesInfo + * @return array + */ + private function processAttributesInfo(array $attributesInfo): array + { + $selectedOptions = []; + foreach ($attributesInfo ?? [] as $option) { + $selectedOptions[] = [ + 'id' => $option['label'], + 'value' => $option['print_value'] ?? $option['value'], + ]; + } + return ['selected_options' => $selectedOptions, 'entered_options' => []]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoComments.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoComments.php new file mode 100644 index 0000000000000..2c9fedf61b502 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoComments.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CreditMemo; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\CreditmemoInterface; + +/** + * Resolve credit memo comments + */ +class CreditMemoComments implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof CreditmemoInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $value['model']; + + $comments = []; + foreach ($creditMemo->getComments() as $comment) { + if ($comment->getIsVisibleOnFront()) { + $comments[] = [ + 'message' => $comment->getComment(), + 'timestamp' => $comment->getCreatedAt() + ]; + } + } + + return $comments; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php new file mode 100644 index 0000000000000..e1cee27e93f87 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoItems.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CreditMemo; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\CreditmemoItemInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve credit memos items data + */ +class CreditMemoItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof CreditmemoInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var CreditmemoInterface $creditMemoModel */ + $creditMemoModel = $value['model']; + /** @var OrderInterface $parentOrderModel */ + $parentOrderModel = $value['order']; + + return $this->valueFactory->create( + $this->getCreditMemoItems($parentOrderModel, $creditMemoModel->getItems()) + ); + } + + /** + * Get credit memo items data as a promise + * + * @param OrderInterface $order + * @param array $creditMemoItems + * @return \Closure + */ + private function getCreditMemoItems(OrderInterface $order, array $creditMemoItems): \Closure + { + $orderItems = []; + foreach ($creditMemoItems as $item) { + $this->orderItemProvider->addOrderItemId((int)$item->getOrderItemId()); + } + + return function () use ($order, $creditMemoItems, $orderItems): array { + foreach ($creditMemoItems as $creditMemoItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$creditMemoItem->getOrderItemId()); + /** @var OrderItemInterface $orderItemModel */ + $orderItemModel = $orderItem['model']; + if (!$orderItemModel->getParentItem()) { + $creditMemoItemData = $this->getCreditMemoItemData($order, $creditMemoItem); + if (!empty($creditMemoItemData)) { + $orderItems[$creditMemoItem->getOrderItemId()] = $creditMemoItemData; + } + } + } + return $orderItems; + }; + } + + /** + * Get credit memo item data + * + * @param OrderInterface $order + * @param CreditmemoItemInterface $creditMemoItem + * @return array + */ + private function getCreditMemoItemData(OrderInterface $order, CreditmemoItemInterface $creditMemoItem): array + { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$creditMemoItem->getOrderItemId()); + return [ + 'id' => base64_encode($creditMemoItem->getEntityId()), + 'product_name' => $creditMemoItem->getName(), + 'product_sku' => $creditMemoItem->getSku(), + 'product_sale_price' => [ + 'value' => $creditMemoItem->getPrice(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'quantity_refunded' => $creditMemoItem->getQty(), + 'model' => $creditMemoItem, + 'product_type' => $orderItem['product_type'], + 'discounts' => $this->formatDiscountDetails($order, $creditMemoItem) + ]; + } + + /** + * Returns formatted information about an applied discount + * + * @param OrderInterface $associatedOrder + * @param CreditmemoItemInterface $creditmemoItem + * @return array + */ + private function formatDiscountDetails( + OrderInterface $associatedOrder, + CreditmemoItemInterface $creditmemoItem + ): array { + if ($associatedOrder->getDiscountDescription() === null + && $creditmemoItem->getDiscountAmount() == 0 + && $associatedOrder->getDiscountAmount() == 0 + ) { + $discounts = []; + } else { + $discounts[] = [ + 'label' => $associatedOrder->getDiscountDescription() ?? _('Discount'), + 'amount' => [ + 'value' => abs($creditmemoItem->getDiscountAmount()) ?? 0, + 'currency' => $associatedOrder->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoTotal.php new file mode 100644 index 0000000000000..5a8f4f7f17ce6 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemo/CreditMemoTotal.php @@ -0,0 +1,187 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CreditMemo; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\SalesGraphQl\Model\SalesItem\ShippingTaxCalculator; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Tax\Helper\Data as TaxHelper; + +/** + * Resolve credit memo totals information + */ +class CreditMemoTotal implements ResolverInterface +{ + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @var ShippingTaxCalculator + */ + private $shippingTaxCalculator; + /** + * @param OrderTaxManagementInterface $orderTaxManagement + * @param TaxHelper $taxHelper + * @param ShippingTaxCalculator $shippingTaxCalculator + */ + public function __construct( + OrderTaxManagementInterface $orderTaxManagement, + TaxHelper $taxHelper, + ShippingTaxCalculator $shippingTaxCalculator + ) { + $this->taxHelper = $taxHelper; + $this->orderTaxManagement = $orderTaxManagement; + $this->shippingTaxCalculator = $shippingTaxCalculator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof CreditmemoInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['order']; + /** @var CreditmemoInterface $creditMemo */ + $creditMemo = $value['model']; + $currency = $orderModel->getOrderCurrencyCode(); + $baseCurrency = $orderModel->getBaseCurrencyCode(); + return [ + 'base_grand_total' => ['value' => $creditMemo->getBaseGrandTotal(), 'currency' => $baseCurrency], + 'grand_total' => ['value' => $creditMemo->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $creditMemo->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $creditMemo->getTaxAmount(), 'currency' => $currency], + 'total_shipping' => ['value' => $creditMemo->getShippingAmount(), 'currency' => $currency], + 'discounts' => $this->getDiscountDetails($creditMemo), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->taxHelper->getCalculatedTaxes($creditMemo), + ), + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $creditMemo->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'amount_including_tax' => [ + 'value' => $creditMemo->getShippingInclTax() ?? 0, + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $creditMemo->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'discounts' => $this->getShippingDiscountDetails($creditMemo, $orderModel), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->shippingTaxCalculator->calculateShippingTaxes($orderModel, $creditMemo), + ) + ], + 'adjustment' => [ + 'value' => abs($creditMemo->getAdjustment()), + 'currency' => $currency + ] + ]; + } + + /** + * Return information about an applied discount on shipping + * + * @param CreditmemoInterface $creditmemoModel + * @param OrderInterface $orderModel + * @return array + */ + private function getShippingDiscountDetails(CreditmemoInterface $creditmemoModel, $orderModel): array + { + $creditmemoShippingAmount = (float)$creditmemoModel->getShippingAmount(); + $orderShippingAmount = (float)$orderModel->getShippingAmount(); + $calculatedShippingRatio = (float)$creditmemoShippingAmount != 0 && $orderShippingAmount != 0 ? + ($creditmemoShippingAmount / $orderShippingAmount) : 0; + $orderShippingDiscount = (float)$orderModel->getShippingDiscountAmount(); + $calculatedCreditmemoShippingDiscount = $orderShippingDiscount * $calculatedShippingRatio; + + $shippingDiscounts = []; + if ($calculatedCreditmemoShippingDiscount != 0) { + $shippingDiscounts[] = [ + 'amount' => [ + 'value' => sprintf('%.2f', abs($calculatedCreditmemoShippingDiscount)), + 'currency' => $creditmemoModel->getOrderCurrencyCode() + ] + ]; + } + return $shippingDiscounts; + } + + /** + * Return information about an applied discount + * + * @param CreditmemoInterface $creditmemo + * @return array + */ + private function getDiscountDetails(CreditmemoInterface $creditmemo): array + { + $discounts = []; + if (!($creditmemo->getDiscountDescription() === null && $creditmemo->getDiscountAmount() == 0)) { + $discounts[] = [ + 'label' => $creditmemo->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($creditmemo->getDiscountAmount()), + 'currency' => $creditmemo->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } + + /** + * Format applied taxes + * + * @param OrderInterface $order + * @param array $appliedTaxes + * @return array + */ + private function formatTaxes(OrderInterface $order, array $appliedTaxes): array + { + $taxes = []; + foreach ($appliedTaxes as $appliedTax) { + $appliedTaxesArray = [ + 'rate' => $appliedTax['percent'] ?? 0, + 'title' => $appliedTax['title'] ?? null, + 'amount' => [ + 'value' => $appliedTax['tax_amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; + } + return $taxes; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemos.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemos.php new file mode 100644 index 0000000000000..69dbca9d66599 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CreditMemos.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\CreditmemoInterface; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Resolve credit memos for order + */ +class CreditMemos implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['model']; + + $creditMemos = []; + /** @var CreditmemoInterface $creditMemo */ + foreach ($orderModel->getCreditmemosCollection() as $creditMemo) { + $creditMemos[] = [ + 'id' => base64_encode($creditMemo->getEntityId()), + 'number' => $creditMemo->getIncrementId(), + 'order' => $orderModel, + 'model' => $creditMemo + ]; + } + return $creditMemos; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php new file mode 100644 index 0000000000000..8807dfa390ae8 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders.php @@ -0,0 +1,165 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\InputException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\SalesGraphQl\Model\Order\OrderAddress; +use Magento\SalesGraphQl\Model\Order\OrderPayments; +use Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query\OrderFilter; +use Magento\Store\Api\Data\StoreInterface; + +/** + * Orders data resolver + */ +class CustomerOrders implements ResolverInterface +{ + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var OrderAddress + */ + private $orderAddress; + + /** + * @var OrderPayments + */ + private $orderPayments; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var OrderFilter + */ + private $orderFilter; + + /** + * @param OrderRepositoryInterface $orderRepository + * @param OrderAddress $orderAddress + * @param OrderPayments $orderPayments + * @param SearchCriteriaBuilder $searchCriteriaBuilder + * @param OrderFilter $orderFilter + */ + public function __construct( + OrderRepositoryInterface $orderRepository, + OrderAddress $orderAddress, + OrderPayments $orderPayments, + SearchCriteriaBuilder $searchCriteriaBuilder, + OrderFilter $orderFilter + ) { + $this->orderRepository = $orderRepository; + $this->orderAddress = $orderAddress; + $this->orderPayments = $orderPayments; + $this->searchCriteriaBuilder = $searchCriteriaBuilder; + $this->orderFilter = $orderFilter; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + if ($args['currentPage'] < 1) { + throw new GraphQlInputException(__('currentPage value must be greater than 0.')); + } + if ($args['pageSize'] < 1) { + throw new GraphQlInputException(__('pageSize value must be greater than 0.')); + } + $userId = $context->getUserId(); + /** @var StoreInterface $store */ + $store = $context->getExtensionAttributes()->getStore(); + try { + $searchResult = $this->getSearchResult($args, (int)$userId, (int)$store->getId()); + $maxPages = (int)ceil($searchResult->getTotalCount() / $searchResult->getPageSize()); + } catch (InputException $e) { + throw new GraphQlInputException(__($e->getMessage())); + } + + return [ + 'total_count' => $searchResult->getTotalCount(), + 'items' => $this->formatOrdersArray($searchResult->getItems()), + 'page_info' => [ + 'page_size' => $searchResult->getPageSize(), + 'current_page' => $searchResult->getCurPage(), + 'total_pages' => $maxPages, + ] + ]; + } + + /** + * Format order models for graphql schema + * + * @param OrderInterface[] $orderModels + * @return array + */ + private function formatOrdersArray(array $orderModels) + { + $ordersArray = []; + foreach ($orderModels as $orderModel) { + $ordersArray[] = [ + 'created_at' => $orderModel->getCreatedAt(), + 'grand_total' => $orderModel->getGrandTotal(), + 'id' => base64_encode($orderModel->getEntityId()), + 'increment_id' => $orderModel->getIncrementId(), + 'number' => $orderModel->getIncrementId(), + 'order_date' => $orderModel->getCreatedAt(), + 'order_number' => $orderModel->getIncrementId(), + 'status' => $orderModel->getStatusLabel(), + 'shipping_method' => $orderModel->getShippingDescription(), + 'shipping_address' => $this->orderAddress->getOrderShippingAddress($orderModel), + 'billing_address' => $this->orderAddress->getOrderBillingAddress($orderModel), + 'payment_methods' => $this->orderPayments->getOrderPaymentMethod($orderModel), + 'model' => $orderModel, + ]; + } + return $ordersArray; + } + + /** + * Get search result from graphql query arguments + * + * @param array $args + * @param int $userId + * @param int $storeId + * @return \Magento\Sales\Api\Data\OrderSearchResultInterface + * @throws InputException + */ + private function getSearchResult(array $args, int $userId, int $storeId) + { + $filterGroups = $this->orderFilter->createFilterGroups($args, $userId, (int)$storeId); + $this->searchCriteriaBuilder->setFilterGroups($filterGroups); + if (isset($args['currentPage'])) { + $this->searchCriteriaBuilder->setCurrentPage($args['currentPage']); + } + if (isset($args['pageSize'])) { + $this->searchCriteriaBuilder->setPageSize($args['pageSize']); + } + return $this->orderRepository->getList($this->searchCriteriaBuilder->create()); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Carrier.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Carrier.php new file mode 100644 index 0000000000000..8fae5c3d19d20 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Carrier.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CustomerOrders; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Model\Order; +use Magento\Shipping\Model\Config\Source\Allmethods; + +/** + * Resolve shipping carrier for order + */ +class Carrier implements ResolverInterface +{ + /** + * @var Allmethods + */ + private $carrierMethods; + + /** + * @param Allmethods $carrierMethods + */ + public function __construct(Allmethods $carrierMethods) + { + $this->carrierMethods = $carrierMethods; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model']) && !($value['model'] instanceof Order)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Order $order */ + $order = $value['model']; + $methodCode = $order->getShippingMethod(); + if (null === $methodCode) { + return null; + } + + return $this->findCarrierByMethodCode($methodCode); + } + + /** + * Find carrier name by shipping method code + * + * @param string $methodCode + * @return string + */ + private function findCarrierByMethodCode(string $methodCode): ?string + { + $allCarrierMethods = $this->carrierMethods->toOptionArray(); + + foreach ($allCarrierMethods as $carrierMethods) { + $carrierLabel = $carrierMethods['label']; + $carrierMethodOptions = $carrierMethods['value']; + if (is_array($carrierMethodOptions)) { + foreach ($carrierMethodOptions as $option) { + if ($option['value'] === $methodCode) { + return $carrierLabel; + } + } + } + } + return null; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php new file mode 100644 index 0000000000000..b14b05042bb4d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/CustomerOrders/Query/OrderFilter.php @@ -0,0 +1,117 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\CustomerOrders\Query; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\InputException; +use Magento\Framework\Api\Search\FilterGroup; + +/** + * Order filter allows to filter collection using 'increment_id' as order number, from the search criteria. + */ +class OrderFilter +{ + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * Translator field from graphql to collection field + * + * @var string[] + */ + private $fieldTranslatorArray = [ + 'number' => 'increment_id', + ]; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param FilterBuilder $filterBuilder + * @param FilterGroupBuilder $filterGroupBuilder + * @param string[] $fieldTranslatorArray + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + FilterBuilder $filterBuilder, + FilterGroupBuilder $filterGroupBuilder, + array $fieldTranslatorArray = [] + ) { + $this->filterBuilder = $filterBuilder; + $this->filterGroupBuilder = $filterGroupBuilder; + $this->scopeConfig = $scopeConfig; + $this->fieldTranslatorArray = array_replace($this->fieldTranslatorArray, $fieldTranslatorArray); + } + + /** + * Create filter for filtering the requested categories id's based on url_key, ids, name in the result. + * + * @param array $args + * @param int $userId + * @param int $storeId + * @return FilterGroup[] + */ + public function createFilterGroups( + array $args, + int $userId, + int $storeId + ): array { + $filterGroups = []; + $this->filterGroupBuilder->setFilters( + [$this->filterBuilder->setField('customer_id')->setValue($userId)->setConditionType('eq')->create()] + ); + $filterGroups[] = $this->filterGroupBuilder->create(); + + $this->filterGroupBuilder->setFilters( + [$this->filterBuilder->setField('store_id')->setValue($storeId)->setConditionType('eq')->create()] + ); + $filterGroups[] = $this->filterGroupBuilder->create(); + + if (isset($args['filter'])) { + $filters = []; + foreach ($args['filter'] as $field => $cond) { + if (isset($this->fieldTranslatorArray[$field])) { + $field = $this->fieldTranslatorArray[$field]; + } + foreach ($cond as $condType => $value) { + if ($condType === 'match') { + if (is_array($value)) { + throw new InputException(__('Invalid match filter')); + } + $searchValue = str_replace('%', '', $value); + $filters[] = $this->filterBuilder->setField($field) + ->setValue("%{$searchValue}%") + ->setConditionType('like') + ->create(); + } else { + $filters[] = $this->filterBuilder->setField($field) + ->setValue($value) + ->setConditionType($condType) + ->create(); + } + } + } + + $this->filterGroupBuilder->setFilters($filters); + $filterGroups[] = $this->filterGroupBuilder->create(); + } + return $filterGroups; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php new file mode 100644 index 0000000000000..1e9d282d80d94 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceItems.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Invoice; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\InvoiceItemInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderItemInterface; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolver for Invoice Items + */ +class InvoiceItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof InvoiceInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var InvoiceInterface $invoiceModel */ + $invoiceModel = $value['model']; + /** @var OrderInterface $parentOrderModel */ + $parentOrderModel = $value['order']; + + return $this->valueFactory->create( + $this->getInvoiceItems($parentOrderModel, $invoiceModel->getItems()) + ); + } + + /** + * Get invoice items data as promise + * + * @param OrderInterface $order + * @param array $invoiceItems + * @return \Closure + */ + public function getInvoiceItems(OrderInterface $order, array $invoiceItems): \Closure + { + $itemsList = []; + foreach ($invoiceItems as $Item) { + $this->orderItemProvider->addOrderItemId((int)$Item->getOrderItemId()); + } + return function () use ($order, $invoiceItems, $itemsList): array { + foreach ($invoiceItems as $invoiceItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$invoiceItem->getOrderItemId()); + /** @var OrderItemInterface $orderItemModel */ + $orderItemModel = $orderItem['model']; + if (!$orderItemModel->getParentItem()) { + $invoiceItemData = $this->getInvoiceItemData($order, $invoiceItem); + if (!empty($invoiceItemData)) { + $itemsList[$invoiceItem->getOrderItemId()] = $invoiceItemData; + } + } + } + return $itemsList; + }; + } + + /** + * Get formatted invoice item data + * + * @param OrderInterface $order + * @param InvoiceItemInterface $invoiceItem + * @return array + */ + private function getInvoiceItemData(OrderInterface $order, InvoiceItemInterface $invoiceItem): array + { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$invoiceItem->getOrderItemId()); + return [ + 'id' => base64_encode($invoiceItem->getEntityId()), + 'product_name' => $invoiceItem->getName(), + 'product_sku' => $invoiceItem->getSku(), + 'product_sale_price' => [ + 'value' => $invoiceItem->getPrice(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'quantity_invoiced' => $invoiceItem->getQty(), + 'model' => $invoiceItem, + 'product_type' => $orderItem['product_type'], + 'order_item' => $orderItem, + 'discounts' => $this->formatDiscountDetails($order, $invoiceItem) + ]; + } + + /** + * Returns formatted information about an applied discount + * + * @param OrderInterface $associatedOrder + * @param InvoiceItemInterface $invoiceItem + * @return array + */ + private function formatDiscountDetails(OrderInterface $associatedOrder, InvoiceItemInterface $invoiceItem) : array + { + if ($associatedOrder->getDiscountDescription() === null + && $invoiceItem->getDiscountAmount() == 0 + && $associatedOrder->getDiscountAmount() == 0 + ) { + $discounts = []; + } else { + $discounts[] = [ + 'label' => $associatedOrder->getDiscountDescription() ?? _('Discount'), + 'amount' => [ + 'value' => abs($invoiceItem->getDiscountAmount()) ?? 0, + 'currency' => $associatedOrder->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceTotal.php new file mode 100644 index 0000000000000..b77fda9523843 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoice/InvoiceTotal.php @@ -0,0 +1,184 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Invoice; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\SalesGraphQl\Model\SalesItem\ShippingTaxCalculator; +use Magento\Tax\Helper\Data as TaxHelper; + +/** + * Resolver for Invoice total + */ +class InvoiceTotal implements ResolverInterface +{ + /** + * @var TaxHelper + */ + private $taxHelper; + + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @var ShippingTaxCalculator + */ + private $shippingTaxCalculator; + + /** + * @param OrderTaxManagementInterface $orderTaxManagement + * @param TaxHelper $taxHelper + * @param ShippingTaxCalculator $shippingTaxCalculator + */ + public function __construct( + OrderTaxManagementInterface $orderTaxManagement, + TaxHelper $taxHelper, + ShippingTaxCalculator $shippingTaxCalculator + ) { + $this->taxHelper = $taxHelper; + $this->orderTaxManagement = $orderTaxManagement; + $this->shippingTaxCalculator = $shippingTaxCalculator; + } + + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof InvoiceInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + if (!(($value['order'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"order" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['order']; + /** @var InvoiceInterface $invoiceModel */ + $invoiceModel = $value['model']; + $currency = $orderModel->getOrderCurrencyCode(); + $baseCurrency = $orderModel->getBaseCurrencyCode(); + return [ + 'base_grand_total' => ['value' => $invoiceModel->getBaseGrandTotal(), 'currency' => $baseCurrency], + 'grand_total' => ['value' => $invoiceModel->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $invoiceModel->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $invoiceModel->getTaxAmount(), 'currency' => $currency], + 'total_shipping' => ['value' => $invoiceModel->getShippingAmount(), 'currency' => $currency], + 'discounts' => $this->getDiscountDetails($invoiceModel), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->taxHelper->getCalculatedTaxes($invoiceModel), + ), + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $invoiceModel->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'amount_including_tax' => [ + 'value' => $invoiceModel->getShippingInclTax() ?? 0, + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $invoiceModel->getShippingAmount() ?? 0, + 'currency' => $currency + ], + 'discounts' => $this->getShippingDiscountDetails($invoiceModel, $orderModel), + 'taxes' => $this->formatTaxes( + $orderModel, + $this->shippingTaxCalculator->calculateShippingTaxes($orderModel, $invoiceModel), + ) + ] + ]; + } + + /** + * Return information about an applied discount on shipping + * + * @param InvoiceInterface $invoiceModel + * @param OrderInterface $orderModel + * @return array + */ + private function getShippingDiscountDetails(InvoiceInterface $invoiceModel, OrderInterface $orderModel): array + { + $invoiceShippingAmount = (float)$invoiceModel->getShippingAmount(); + $orderShippingAmount = (float)$orderModel->getShippingAmount(); + $calculatedShippingRatioFromOriginal = $invoiceShippingAmount != 0 && $orderShippingAmount != 0 ? + ($invoiceShippingAmount / $orderShippingAmount) : 0; + $orderShippingDiscount = (float)$orderModel->getShippingDiscountAmount(); + $calculatedInvoiceShippingDiscount = $orderShippingDiscount * $calculatedShippingRatioFromOriginal; + $shippingDiscounts = []; + if ($calculatedInvoiceShippingDiscount != 0) { + $shippingDiscounts[] = + [ + 'amount' => [ + 'value' => sprintf('%.2f', abs($calculatedInvoiceShippingDiscount)), + 'currency' => $invoiceModel->getOrderCurrencyCode() + ] + ]; + } + return $shippingDiscounts; + } + + /** + * Return information about an applied discount + * + * @param InvoiceInterface $invoice + * @return array + */ + private function getDiscountDetails(InvoiceInterface $invoice): array + { + $discounts = []; + if (!($invoice->getDiscountDescription() === null && $invoice->getDiscountAmount() == 0)) { + $discounts[] = [ + 'label' => $invoice->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($invoice->getDiscountAmount()), + 'currency' => $invoice->getOrderCurrencyCode() + ] + ]; + } + return $discounts; + } + + /** + * Format applied taxes + * + * @param OrderInterface $order + * @param array $appliedTaxes + * @return array + */ + private function formatTaxes(OrderInterface $order, array $appliedTaxes): array + { + $taxes = []; + foreach ($appliedTaxes as $appliedTax) { + $appliedTaxesArray = [ + 'rate' => $appliedTax['percent'] ?? 0, + 'title' => $appliedTax['title'] ?? null, + 'amount' => [ + 'value' => $appliedTax['tax_amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; + } + return $taxes; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php new file mode 100644 index 0000000000000..f106752075c25 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Invoices.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\InvoiceInterface; + +/** + * Resolver for Invoice + */ +class Invoices implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $orderModel */ + $orderModel = $value['model']; + $invoices = []; + /** @var InvoiceInterface $invoice */ + foreach ($orderModel->getInvoiceCollection() as $invoice) { + $invoices[] = [ + 'id' => base64_encode($invoice->getEntityId()), + 'number' => $invoice['increment_id'], + 'model' => $invoice, + 'order' => $orderModel + ]; + } + return $invoices; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php new file mode 100644 index 0000000000000..d7819277e56db --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItem.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve a single order item + */ +class OrderItem implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct(ValueFactory $valueFactory, OrderItemProvider $orderItemProvider) + { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + $parentItem = $value['model']; + + if (!method_exists($parentItem, 'getOrderItemId')) { + throw new LocalizedException(__('Unable to find associated order item.')); + } + + $orderItemId = $parentItem->getOrderItemId(); + $this->orderItemProvider->addOrderItemId((int)$orderItemId); + + return $this->valueFactory->create(function () use ($parentItem) { + $orderItem = $this->orderItemProvider->getOrderItemById((int)$parentItem->getOrderItemId()); + return empty($orderItem) ? null : $orderItem; + }); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php new file mode 100644 index 0000000000000..f0e768c513cd3 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderItems.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Query\Resolver\ValueFactory; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\SalesGraphQl\Model\OrderItem\DataProvider as OrderItemProvider; + +/** + * Resolve order items for order + */ +class OrderItems implements ResolverInterface +{ + /** + * @var ValueFactory + */ + private $valueFactory; + + /** + * @var OrderItemProvider + */ + private $orderItemProvider; + + /** + * @param ValueFactory $valueFactory + * @param OrderItemProvider $orderItemProvider + */ + public function __construct( + ValueFactory $valueFactory, + OrderItemProvider $orderItemProvider + ) { + $this->valueFactory = $valueFactory; + $this->orderItemProvider = $orderItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + /** @var ContextInterface $context */ + if (false === $context->getExtensionAttributes()->getIsCustomer()) { + throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); + } + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var OrderInterface $parentOrder */ + $parentOrder = $value['model']; + $orderItemIds = []; + foreach ($parentOrder->getItems() as $item) { + if (!$item->getParentItemId()) { + $orderItemIds[] = (int)$item->getItemId(); + } + $this->orderItemProvider->addOrderItemId((int)$item->getItemId()); + } + $itemsList = []; + foreach ($orderItemIds as $orderItemId) { + $itemsList[] = $this->valueFactory->create( + function () use ($orderItemId) { + return $this->orderItemProvider->getOrderItemById((int)$orderItemId); + } + ); + } + return $itemsList; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php new file mode 100644 index 0000000000000..ab3ace45f336c --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/OrderTotal.php @@ -0,0 +1,205 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\OrderInterface; + +/** + * Resolve order totals taxes and discounts for order + */ +class OrderTotal implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!(($value['model'] ?? null) instanceof OrderInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var OrderInterface $order */ + $order = $value['model']; + $currency = $order->getOrderCurrencyCode(); + $baseCurrency = $order->getBaseCurrencyCode(); + + return [ + 'base_grand_total' => ['value' => $order->getBaseGrandTotal(), 'currency' => $baseCurrency], + 'grand_total' => ['value' => $order->getGrandTotal(), 'currency' => $currency], + 'subtotal' => ['value' => $order->getSubtotal(), 'currency' => $currency], + 'total_tax' => ['value' => $order->getTaxAmount(), 'currency' => $currency], + 'taxes' => $this->getAppliedTaxesDetails($order), + 'discounts' => $this->getDiscountDetails($order), + 'total_shipping' => ['value' => $order->getShippingAmount(), 'currency' => $currency], + 'shipping_handling' => [ + 'amount_excluding_tax' => [ + 'value' => $order->getShippingAmount(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'amount_including_tax' => [ + 'value' => $order->getShippingInclTax(), + 'currency' => $currency + ], + 'total_amount' => [ + 'value' => $order->getShippingAmount(), + 'currency' => $currency + ], + 'taxes' => $this->getAppliedShippingTaxesDetails($order), + 'discounts' => $this->getShippingDiscountDetails($order), + ] + ]; + } + + /** + * Retrieve applied taxes that apply to the order + * + * @param OrderInterface $order + * @return array + */ + private function getAllAppliedTaxesOnOrders(OrderInterface $order): array + { + $extensionAttributes = $order->getExtensionAttributes(); + $appliedTaxes = $extensionAttributes->getAppliedTaxes() ?? []; + $allAppliedTaxOnOrders = []; + foreach ($appliedTaxes as $taxIndex => $appliedTaxesData) { + $allAppliedTaxOnOrders[$taxIndex] = [ + 'title' => $appliedTaxesData->getDataByKey('title'), + 'percent' => $appliedTaxesData->getDataByKey('percent'), + 'amount' => $appliedTaxesData->getDataByKey('amount'), + ]; + } + return $allAppliedTaxOnOrders; + } + + /** + * Return taxes applied to the current order + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedTaxesDetails(OrderInterface $order): array + { + $allAppliedTaxOnOrders = $this->getAllAppliedTaxesOnOrders($order); + $taxes = []; + foreach ($allAppliedTaxOnOrders as $appliedTaxes) { + $appliedTaxesArray = [ + 'rate' => $appliedTaxes['percent'] ?? 0, + 'title' => $appliedTaxes['title'] ?? null, + 'amount' => [ + 'value' => $appliedTaxes['amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $taxes[] = $appliedTaxesArray; + } + return $taxes; + } + + /** + * Return information about an applied discount + * + * @param OrderInterface $order + * @return array + */ + private function getDiscountDetails(OrderInterface $order): array + { + $orderDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getDiscountAmount() == 0)) { + $orderDiscounts[] = [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + } + return $orderDiscounts; + } + + /** + * Retrieve applied shipping taxes on items for the orders + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedShippingTaxesForItems(OrderInterface $order): array + { + $extensionAttributes = $order->getExtensionAttributes(); + $itemAppliedTaxes = $extensionAttributes->getItemAppliedTaxes() ?? []; + $appliedShippingTaxesForItems = []; + foreach ($itemAppliedTaxes as $appliedTaxForItem) { + if ($appliedTaxForItem->getType() === "shipping") { + foreach ($appliedTaxForItem->getAppliedTaxes() ?? [] as $taxLineItem) { + $taxItemIndexTitle = $taxLineItem->getDataByKey('title'); + $appliedShippingTaxesForItems[$taxItemIndexTitle] = [ + 'title' => $taxLineItem->getDataByKey('title'), + 'percent' => $taxLineItem->getDataByKey('percent'), + 'amount' => $taxLineItem->getDataByKey('amount') + ]; + } + } + } + return $appliedShippingTaxesForItems; + } + + /** + * Return taxes applied to the current order + * + * @param OrderInterface $order + * @return array + */ + private function getAppliedShippingTaxesDetails( + OrderInterface $order + ): array { + $appliedShippingTaxesForItems = $this->getAppliedShippingTaxesForItems($order); + $shippingTaxes = []; + foreach ($appliedShippingTaxesForItems as $appliedShippingTaxes) { + $appliedShippingTaxesArray = [ + 'rate' => $appliedShippingTaxes['percent'] ?? 0, + 'title' => $appliedShippingTaxes['title'] ?? null, + 'amount' => [ + 'value' => $appliedShippingTaxes['amount'] ?? 0, + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + $shippingTaxes[] = $appliedShippingTaxesArray; + } + return $shippingTaxes; + } + + /** + * Return information about an applied discount + * + * @param OrderInterface $order + * @return array + */ + private function getShippingDiscountDetails(OrderInterface $order): array + { + $shippingDiscounts = []; + if (!($order->getDiscountDescription() === null && $order->getShippingDiscountAmount() == 0)) { + $shippingDiscounts[] = + [ + 'label' => $order->getDiscountDescription() ?? __('Discount'), + 'amount' => [ + 'value' => abs($order->getShippingDiscountAmount()), + 'currency' => $order->getOrderCurrencyCode() + ] + ]; + } + return $shippingDiscounts; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php index 8d81afeab4c90..25a79fa5d3b6c 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Orders.php @@ -12,6 +12,7 @@ use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\GraphQl\Model\Query\ContextInterface; +use Magento\Sales\Model\Order; use Magento\Sales\Model\ResourceModel\Order\CollectionFactoryInterface; /** @@ -34,7 +35,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function resolve( Field $field, @@ -51,7 +52,7 @@ public function resolve( $items = []; $orders = $this->collectionFactory->create($context->getUserId()); - /** @var \Magento\Sales\Model\Order $order */ + /** @var Order $order */ foreach ($orders as $order) { $items[] = [ 'id' => $order->getId(), diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php index 8bf4220d1ec3d..70c411c379b62 100644 --- a/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Reorder.php @@ -49,7 +49,7 @@ public function __construct( } /** - * @inheritdoc + * @inheritDoc */ public function resolve( Field $field, diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentItems.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentItems.php new file mode 100644 index 0000000000000..dceb2848bda5b --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentItems.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Shipment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\SalesGraphQl\Model\Shipment\ItemProvider; + +/** + * Resolve items included in shipment + */ +class ShipmentItems implements ResolverInterface +{ + /** + * @var ItemProvider + */ + private $shipmentItemProvider; + + /** + * @param ItemProvider $shipmentItemProvider + */ + public function __construct(ItemProvider $shipmentItemProvider) + { + $this->shipmentItemProvider = $shipmentItemProvider; + } + + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!($value['model'] ?? null) instanceof ShipmentInterface) { + throw new LocalizedException(__('"model" value should be specified')); + } + + /** @var ShipmentInterface $shipment */ + $shipment = $value['model']; + + return $this->shipmentItemProvider->getItemData($shipment); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentTracking.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentTracking.php new file mode 100644 index 0000000000000..e6ef0b8442852 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipment/ShipmentTracking.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver\Shipment; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\ShipmentInterface; + +/** + * Resolve shipment tracking information + */ +class ShipmentTracking implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model']) && !($value['model'] instanceof ShipmentInterface)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var ShipmentInterface $shipment */ + $shipment = $value['model']; + $tracks = $shipment->getTracks(); + + $shipmentTracking = []; + foreach ($tracks as $tracking) { + $shipmentTracking[] = [ + 'title' => $tracking->getTitle(), + 'carrier' => $tracking->getCarrierCode(), + 'number' => $tracking->getTrackNumber(), + 'model' => $tracking + ]; + } + + return $shipmentTracking; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Resolver/Shipments.php b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipments.php new file mode 100644 index 0000000000000..8b6aaad09c304 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Resolver/Shipments.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Resolver; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Model\Order; + +/** + * Resolve shipment information for order + */ +class Shipments implements ResolverInterface +{ + /** + * @inheritDoc + */ + public function resolve(Field $field, $context, ResolveInfo $info, array $value = null, array $args = null) + { + if (!isset($value['model']) && !($value['model'] instanceof Order)) { + throw new LocalizedException(__('"model" value should be specified')); + } + /** @var Order $order */ + $order = $value['model']; + $shipments = $order->getShipmentsCollection()->getItems(); + + if (empty($shipments)) { + //Order does not have any shipments + return []; + } + + $orderShipments = []; + foreach ($shipments as $shipment) { + $orderShipments[] = + [ + 'id' => base64_encode($shipment->getIncrementId()), + 'number' => $shipment->getIncrementId(), + 'comments' => $this->getShipmentComments($shipment), + 'model' => $shipment, + 'order' => $order + ]; + } + return $orderShipments; + } + + /** + * Get comments shipments in proper format + * + * @param ShipmentInterface $shipment + * @return array + */ + private function getShipmentComments(ShipmentInterface $shipment): array + { + $comments = []; + foreach ($shipment->getComments() as $comment) { + if ($comment->getIsVisibleOnFront()) { + $comments[] = [ + 'timestamp' => $comment->getCreatedAt(), + 'message' => $comment->getComment() + ]; + } + } + return $comments; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/SalesItem/ShippingTaxCalculator.php b/app/code/Magento/SalesGraphQl/Model/SalesItem/ShippingTaxCalculator.php new file mode 100644 index 0000000000000..b063918de6ec0 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/SalesItem/ShippingTaxCalculator.php @@ -0,0 +1,112 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\SalesItem; + +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\EntityInterface; +use Magento\Tax\Api\Data\OrderTaxDetailsItemInterface; +use Magento\Tax\Api\OrderTaxManagementInterface; +use Magento\Quote\Model\Quote\Address; +use Magento\Framework\Exception\NoSuchEntityException; + +/** + * Calculates shipping taxes for sales items (Invoices, Credit memo) + */ +class ShippingTaxCalculator +{ + /** + * @var OrderTaxManagementInterface + */ + private $orderTaxManagement; + + /** + * @param OrderTaxManagementInterface $orderTaxManagement + */ + public function __construct( + OrderTaxManagementInterface $orderTaxManagement + ) { + $this->orderTaxManagement = $orderTaxManagement; + } + + /** + * Calculate shipping taxes for sales item + * + * @param OrderInterface $order + * @param EntityInterface $salesItem + * @return array + * @throws NoSuchEntityException + */ + public function calculateShippingTaxes( + OrderInterface $order, + EntityInterface $salesItem + ): array { + $orderTaxDetails = $this->orderTaxManagement->getOrderTaxDetails($order->getId()); + $taxClassBreakdown = []; + // Apply any taxes for shipping + $shippingTaxAmount = $salesItem->getShippingTaxAmount(); + $originalShippingTaxAmount = $order->getShippingTaxAmount(); + if ($shippingTaxAmount && $originalShippingTaxAmount && + $shippingTaxAmount != 0 && (float)$originalShippingTaxAmount + ) { + //An invoice or credit memo can have a different qty than its order + $shippingRatio = $shippingTaxAmount / $originalShippingTaxAmount; + $itemTaxDetails = $orderTaxDetails->getItems(); + foreach ($itemTaxDetails as $itemTaxDetail) { + //Aggregate taxable items associated with shipping + if ($itemTaxDetail->getType() == Address::TYPE_SHIPPING) { + $taxClassBreakdown = $this->aggregateTaxes($taxClassBreakdown, $itemTaxDetail, $shippingRatio); + } + } + } + return $taxClassBreakdown; + } + + /** + * Accumulates the pre-calculated taxes for each tax class + * + * This method accepts and returns the '$taxClassBreakdown' array with format: + * array( + * $index => array( + * 'tax_amount' => $taxAmount, + * 'base_tax_amount' => $baseTaxAmount, + * 'title' => $title, + * 'percent' => $percent + * ) + * ) + * + * @param array $taxClassBreakdown + * @param OrderTaxDetailsItemInterface $itemTaxDetail + * @param float $taxRatio + * @return array + */ + private function aggregateTaxes( + array $taxClassBreakdown, + OrderTaxDetailsItemInterface $itemTaxDetail, + float $taxRatio + ): array { + $itemAppliedTaxes = $itemTaxDetail->getAppliedTaxes(); + foreach ($itemAppliedTaxes as $itemAppliedTax) { + $taxAmount = $itemAppliedTax->getAmount() * $taxRatio; + $baseTaxAmount = $itemAppliedTax->getBaseAmount() * $taxRatio; + if (0 == $taxAmount && 0 == $baseTaxAmount) { + continue; + } + $taxCode = $itemAppliedTax->getCode(); + if (!isset($taxClassBreakdown[$taxCode])) { + $taxClassBreakdown[$taxCode]['title'] = $itemAppliedTax->getTitle(); + $taxClassBreakdown[$taxCode]['percent'] = $itemAppliedTax->getPercent(); + $taxClassBreakdown[$taxCode]['tax_amount'] = $taxAmount; + $taxClassBreakdown[$taxCode]['base_tax_amount'] = $baseTaxAmount; + } else { + $taxClassBreakdown[$taxCode]['tax_amount'] += $taxAmount; + $taxClassBreakdown[$taxCode]['base_tax_amount'] += $baseTaxAmount; + } + } + return $taxClassBreakdown; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Shipment/Item/FormatterInterface.php b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/FormatterInterface.php new file mode 100644 index 0000000000000..61ea89b5a81e6 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/FormatterInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Shipment\Item; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; + +/** + * Format shipment items for GraphQl output + */ +interface FormatterInterface +{ + /** + * Format a shipment item for GraphQl + * + * @param ShipmentInterface $shipment + * @param ShipmentItemInterface $item + * @return array|null + */ + public function formatShipmentItem(ShipmentInterface $shipment, ShipmentItemInterface $item): ?array; +} diff --git a/app/code/Magento/SalesGraphQl/Model/Shipment/Item/ShipmentItemFormatter.php b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/ShipmentItemFormatter.php new file mode 100644 index 0000000000000..e8ba6e5f784ec --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Shipment/Item/ShipmentItemFormatter.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Shipment\Item; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; + +/** + * Format shipment item for GraphQl output + */ +class ShipmentItemFormatter implements FormatterInterface +{ + /** + * @inheritDoc + */ + public function formatShipmentItem(ShipmentInterface $shipment, ShipmentItemInterface $item): ?array + { + $order = $shipment->getOrder(); + return [ + 'id' => base64_encode($item->getEntityId()), + 'product_name' => $item->getName(), + 'product_sku' => $item->getSku(), + 'product_sale_price' => [ + 'value' => $item->getPrice(), + 'currency' => $order->getOrderCurrencyCode() + ], + 'product_type' => $item->getOrderItem()->getProductType(), + 'quantity_shipped' => $item->getQty(), + 'model' => $item, + ]; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/Shipment/ItemProvider.php b/app/code/Magento/SalesGraphQl/Model/Shipment/ItemProvider.php new file mode 100644 index 0000000000000..49f8e3b119da2 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/Shipment/ItemProvider.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\Shipment; + +use Magento\Sales\Api\Data\ShipmentInterface; +use Magento\Sales\Api\Data\ShipmentItemInterface; +use Magento\SalesGraphQl\Model\Shipment\Item\FormatterInterface; + +/** + * Get shipment item data + */ +class ItemProvider +{ + /** + * @var FormatterInterface[] + */ + private $formatters; + + /** + * @param FormatterInterface[] $formatters + */ + public function __construct(array $formatters = []) + { + $this->formatters = $formatters; + } + + /** + * Get item data for shipment + * + * @param ShipmentInterface $shipment + * @return array + */ + public function getItemData(ShipmentInterface $shipment): array + { + $shipmentItems = []; + + foreach ($shipment->getItems() as $shipmentItem) { + $formattedItem = $this->formatItem($shipment, $shipmentItem); + if ($formattedItem) { + $shipmentItems[] = $formattedItem; + } + } + return $shipmentItems; + } + + /** + * Format individual shipment item + * + * @param ShipmentInterface $shipment + * @param ShipmentItemInterface $shipmentItem + * @return array|null + */ + private function formatItem(ShipmentInterface $shipment, ShipmentItemInterface $shipmentItem): ?array + { + $orderItem = $shipmentItem->getOrderItem(); + $formatter = $this->formatters[$orderItem->getProductType()] ?? $this->formatters['default']; + + return $formatter->formatShipmentItem($shipment, $shipmentItem); + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/CreditMemoItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/CreditMemoItem.php new file mode 100644 index 0000000000000..8fab97153231b --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/CreditMemoItem.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Resolve concrete type for CreditMemoItemInterface + */ +class CreditMemoItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct($productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/InvoiceItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/InvoiceItem.php new file mode 100644 index 0000000000000..e4ceab02fbbe9 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/InvoiceItem.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; + +/** + * Resolve concrete type for InvoiceItemInterface + */ +class InvoiceItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct($productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/OrderItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/OrderItem.php new file mode 100644 index 0000000000000..851a0daf2f50d --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/OrderItem.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Resolve concrete type for OrderItemInterface + */ +class OrderItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct(array $productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/Model/TypeResolver/ShipmentItem.php b/app/code/Magento/SalesGraphQl/Model/TypeResolver/ShipmentItem.php new file mode 100644 index 0000000000000..fd72a8729af27 --- /dev/null +++ b/app/code/Magento/SalesGraphQl/Model/TypeResolver/ShipmentItem.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesGraphQl\Model\TypeResolver; + +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\Resolver\TypeResolverInterface; + +/** + * Resolve concrete type of ShipmentItemInterface + */ +class ShipmentItem implements TypeResolverInterface +{ + /** + * @var array + */ + private $productTypeMap; + + /** + * @param array $productTypeMap + */ + public function __construct(array $productTypeMap = []) + { + $this->productTypeMap = $productTypeMap; + } + + /** + * @inheritDoc + */ + public function resolveType(array $data): string + { + if (!isset($data['product_type'])) { + throw new GraphQlInputException(__('Missing key %1 in sales item data', ['product_type'])); + } + if (isset($this->productTypeMap[$data['product_type']])) { + return $this->productTypeMap[$data['product_type']]; + } + + return $this->productTypeMap['default']; + } +} diff --git a/app/code/Magento/SalesGraphQl/composer.json b/app/code/Magento/SalesGraphQl/composer.json index 8e9d95836e189..b85d8c0f852da 100644 --- a/app/code/Magento/SalesGraphQl/composer.json +++ b/app/code/Magento/SalesGraphQl/composer.json @@ -6,7 +6,12 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-sales": "*", - "magento/module-graph-ql": "*" + "magento/module-store": "*", + "magento/module-catalog": "*", + "magento/module-tax": "*", + "magento/module-quote": "*", + "magento/module-graph-ql": "*", + "magento/module-shipping": "*" }, "license": [ "OSL-3.0", diff --git a/app/code/Magento/SalesGraphQl/etc/graphql/di.xml b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml new file mode 100644 index 0000000000000..b40d8e9331bbb --- /dev/null +++ b/app/code/Magento/SalesGraphQl/etc/graphql/di.xml @@ -0,0 +1,45 @@ +<?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\SalesGraphQl\Model\TypeResolver\OrderItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">OrderItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\InvoiceItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">InvoiceItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\CreditMemoItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">CreditMemoItem</item> + </argument> + </arguments> + </type> + <type name="Magento\SalesGraphQl\Model\TypeResolver\ShipmentItem"> + <arguments> + <argument name="productTypeMap" xsi:type="array"> + <item name="default" xsi:type="string">ShipmentItem</item> + </argument> + </arguments> + </type> + <preference for="Magento\SalesGraphQl\Model\Shipment\Item\FormatterInterface" type="Magento\SalesGraphQl\Model\Shipment\Item\ShipmentItemFormatter"/> + <type name="Magento\SalesGraphQl\Model\Shipment\ItemProvider"> + <arguments> + <argument name="formatters" xsi:type="array"> + <item name="default" xsi:type="object">Magento\SalesGraphQl\Model\Shipment\Item\ShipmentItemFormatter\Proxy</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/SalesGraphQl/etc/schema.graphqls b/app/code/Magento/SalesGraphQl/etc/schema.graphqls index f823c25cf2d9f..8b9d58e48d4b1 100644 --- a/app/code/Magento/SalesGraphQl/etc/schema.graphqls +++ b/app/code/Magento/SalesGraphQl/etc/schema.graphqls @@ -2,20 +2,7 @@ # See COPYING.txt for license details. type Query { - customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @doc(description: "List of customer orders") @cache(cacheable: false) -} - -type CustomerOrder @doc(description: "Order mapping fields") { - id: Int - increment_id: String @deprecated(reason: "Use the order_number instead.") - order_number: String! @doc(description: "The order number") - created_at: String - grand_total: Float - status: String -} - -type CustomerOrders { - items: [CustomerOrder] @doc(description: "Array of orders") + customerOrders: CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Orders") @deprecated(reason: "Use orders from customer instead") @cache(cacheable: false) } type Mutation { @@ -33,6 +20,227 @@ type CheckoutUserInputError @doc(description:"An error encountered while adding code: CheckoutUserInputErrorCodes! @doc(description: "Checkout-specific error code") } +type Customer { + orders ( + filter: CustomerOrdersFilterInput @doc(description: "Defines the filter to use for searching customer orders"), + currentPage: Int = 1 @doc(description: "Specifies which page of results to return. The default value is 1"), + pageSize: Int = 20 @doc(description: "Specifies the maximum number of results to return at once. The default value is 20"), + ): CustomerOrders @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders") @cache(cacheable: false) +} + +input CustomerOrdersFilterInput @doc(description: "Identifies the filter to use for filtering orders.") { + number: FilterStringTypeInput @doc(description: "Filters by order number.") +} + +type CustomerOrders @doc(description: "The collection of orders that match the conditions defined in the filter") { + items: [CustomerOrder]! @doc(description: "An array of customer orders") + page_info: SearchResultPageInfo @doc(description: "An object that includes the current_page, page_info, and page_size values specified in the query") + total_count: Int @doc(description: "The total count of customer orders") +} + +type CustomerOrder @doc(description: "Contains details about each of the customer's orders") { + id: ID! @doc(description: "Unique identifier for the order") + order_date: String! @doc(description: "The date the order was placed") + status: String! @doc(description: "The current status of the order") + number: String! @doc(description: "The order number") + items: [OrderItemInterface] @doc(description: "An array containing the items purchased in this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItems") + total: OrderTotal @doc(description: "Contains details about the calculated totals for this order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderTotal") + invoices: [Invoice]! @doc(description: "A list of invoices for the order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoices") + shipments: [OrderShipment] @doc(description: "A list of shipments for the order") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipments") + credit_memos: [CreditMemo] @doc(description: "A list of credit memos") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemos") + payment_methods: [PaymentMethod] @doc(description: "Payment details for the order") + shipping_address: OrderAddress @doc(description: "The shipping address for the order") + billing_address: OrderAddress @doc(description: "The billing address for the order") + carrier: String @doc(description: "The shipping carrier for the order delivery") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CustomerOrders\\Carrier") + shipping_method: String @doc(description: "The delivery method for the order") + comments: [CommentItem] @doc(description: "Comments about the order") + increment_id: String @deprecated(reason: "Use the id attribute instead") + order_number: String! @deprecated(reason: "Use the number attribute instead") + created_at: String @deprecated(reason: "Use the order_date attribute instead") + grand_total: Float @deprecated(reason: "Use the totals.grand_total attribute instead") +} + +type OrderAddress @doc(description: "OrderAddress contains detailed information about an order's billing and shipping addresses"){ + firstname: String! @doc(description: "The first name of the person associated with the shipping/billing address") + lastname: String! @doc(description: "The family name of the person associated with the shipping/billing address") + middlename: String @doc(description: "The middle name of the person associated with the shipping/billing address") + region: String @doc(description: "The state or province name") + region_id: ID @doc(description: "The unique ID for a pre-defined region") + country_code: CountryCodeEnum @doc(description: "The customer's country") + street: [String!]! @doc(description: "An array of strings that define the street number and name") + company: String @doc(description: "The customer's company") + telephone: String! @doc(description: "The telephone number") + fax: String @doc(description: "The fax number") + postcode: String @doc(description: "The customer's order ZIP or postal code") + city: String! @doc(description: "The city or town") + prefix: String @doc(description: "An honorific, such as Dr., Mr., or Mrs.") + suffix: String @doc(description: "A value such as Sr., Jr., or III") + vat_id: String @doc(description: "The customer's Value-added tax (VAT) number (for corporate customers)") +} + +interface OrderItemInterface @doc(description: "Order item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\OrderItem") { + id: ID! @doc(description: "The unique identifier of the order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_url_key: String @doc(description: "URL key of the base product") + product_type: String @doc(description: "The type of product, such as simple, configurable, etc.") + status: String @doc(description: "The status of the order item") + product_sale_price: Money! @doc(description: "The sale price of the base product, including selected options") + discounts: [Discount] @doc(description: "The final discount information for the product") + selected_options: [OrderItemOption] @doc(description: "The selected options for the base product, such as color or size") + entered_options: [OrderItemOption] @doc(description: "The entered option for the base product, such as a logo or image") + quantity_ordered: Float @doc(description: "The number of units ordered for this item") + quantity_shipped: Float @doc(description: "The number of shipped items") + quantity_refunded: Float @doc(description: "The number of refunded items") + quantity_invoiced: Float @doc(description: "The number of invoiced items") + quantity_canceled: Float @doc(description: "The number of canceled items") + quantity_returned: Float @doc(description: "The number of returned items") +} + +type OrderItem implements OrderItemInterface { +} + +type OrderItemOption @doc(description: "Represents order item options like selected or entered") { + id: String! @doc(description: "The name of the option") + value: String! @doc(description: "The value of the option") +} + +type TaxItem @doc(description: "The tax item details") { + amount: Money! @doc(description: "The amount of tax applied to the item") + title: String! @doc(description: "A title that describes the tax") + rate: Float! @doc(description: "The rate used to calculate the tax") +} + +type OrderTotal @doc(description: "Contains details about the sales total amounts used to calculate the final price") { + subtotal: Money! @doc(description: "The subtotal of the order, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the order") + total_tax: Money! @doc(description: "The amount of tax applied to the order") + taxes: [TaxItem] @doc(description: "The order tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the order") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the order") +} + +type Invoice @doc(description: "Invoice details") { + id: ID! @doc(description: "The ID of the invoice, used for API purposes") + number: String! @doc(description: "Sequential invoice number") + total: InvoiceTotal @doc(description: "Invoice total amount details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceTotal") + items: [InvoiceItemInterface] @doc(description: "Invoiced product details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Invoice\\InvoiceItems") + comments: [CommentItem] @doc(description: "Comments on the invoice") +} + +interface InvoiceItemInterface @doc(description: "Invoice item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\InvoiceItem") { + id: ID! @doc(description: "The unique ID of the invoice item") + order_item: OrderItemInterface @doc(description: "Contains details about an individual order item") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "The SKU of the base product") + product_sale_price: Money! @doc(description: "The sale price for the base product including selected options") + discounts: [Discount] @doc(description: "Contains information about the final discount amount for the base product, including discounts on options") + quantity_invoiced: Float @doc(description: "The number of invoiced items") +} + +type InvoiceItem implements InvoiceItemInterface { +} + +type InvoiceTotal @doc(description: "Contains price details from an invoice"){ + subtotal: Money! @doc(description: "The subtotal of the invoice, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the invoice") + total_tax: Money! @doc(description: "The amount of tax applied to the invoice") + taxes: [TaxItem] @doc(description: "The invoice tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the invoice") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the invoice") +} + +type ShippingHandling @doc(description: "The Shipping handling details") { + total_amount: Money! @doc(description: "The total amount for shipping") + amount_including_tax: Money @doc(description: "The shipping amount, including tax") + amount_excluding_tax: Money @doc(description: "The shipping amount, excluding tax") + taxes: [TaxItem] @doc(description: "Contains details about taxes applied for shipping") + discounts: [ShippingDiscount] @doc(description: "The applied discounts to the shipping") +} + +type ShippingDiscount @doc(description:"Defines an individual shipping discount. This discount can be applied to shipping.") { + amount: Money! @doc(description:"The amount of the discount") +} + +type OrderShipment @doc(description: "Order shipment details") { + id: ID! @doc(description: "The unique ID of the shipment") + number: String! @doc(description: "The sequential credit shipment number") + tracking: [ShipmentTracking] @doc(description: "Contains shipment tracking details") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentTracking") + items: [ShipmentItemInterface] @doc(description: "Contains items included in the shipment") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\Shipment\\ShipmentItems") + comments: [CommentItem] @doc(description: "Comments added to the shipment") +} + +type CommentItem @doc(description: "Comment item details") { + timestamp: String! @doc(description: "The timestamp of the comment") + message: String! @doc(description: "The text of the message") +} + +interface ShipmentItemInterface @doc(description: "Order shipment item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\ShipmentItem"){ + id: ID! @doc(description: "Shipment item unique identifier") + order_item: OrderItemInterface @doc(description: "Associated order item") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") + product_name: String @doc(description: "Name of the base product") + product_sku: String! @doc(description: "SKU of the base product") + product_sale_price: Money! @doc(description: "Sale price for the base product") + quantity_shipped: Float! @doc(description: "Number of shipped items") +} + +type ShipmentItem implements ShipmentItemInterface { +} + +type ShipmentTracking @doc(description: "Order shipment tracking details") { + title: String! @doc(description: "The shipment tracking title") + carrier: String! @doc(description: "The shipping carrier for the order delivery") + number: String @doc(description: "The tracking number of the order shipment") +} + +type PaymentMethod @doc(description: "Contains details about the payment method used to pay for the order") { + name: String! @doc(description: "The label that describes the payment method") + type: String! @doc(description: "The payment method code that indicates how the order was paid for") + additional_data: [KeyValue] @doc(description: "Additional data per payment method type") +} + +type CreditMemo @doc(description: "Credit memo details") { + id: ID! @doc(description: "The unique ID of the credit memo, used for API purposes") + number: String! @doc(description: "The sequential credit memo number") + items: [CreditMemoItemInterface] @doc(description: "An array containing details about refunded items") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoItems") + total: CreditMemoTotal @doc(description: "Contains details about the total refunded amount") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoTotal") + comments: [CommentItem] @doc(description: "Comments on the credit memo") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\CreditMemo\\CreditMemoComments") +} + +interface CreditMemoItemInterface @doc(description: "Credit memo item details") @typeResolver(class: "Magento\\SalesGraphQl\\Model\\TypeResolver\\CreditMemoItem") { + id: ID! @doc(description: "The unique ID of the credit memo item, used for API purposes") + order_item: OrderItemInterface @doc(description: "The order item the credit memo is applied to") @resolver(class: "Magento\\SalesGraphQl\\Model\\Resolver\\OrderItem") + product_name: String @doc(description: "The name of the base product") + product_sku: String! @doc(description: "SKU of the base product") + product_sale_price: Money! @doc(description: "The sale price for the base product, including selected options") + discounts: [Discount] @doc(description: "Contains information about the final discount amount for the base product, including discounts on options") + quantity_refunded: Float @doc(description: "The number of refunded items") +} + +type CreditMemoItem implements CreditMemoItemInterface { +} + +type CreditMemoTotal @doc(description: "Credit memo price details") { + subtotal: Money! @doc(description: "The subtotal of the invoice, excluding shipping, discounts, and taxes") + discounts: [Discount] @doc(description: "The applied discounts to the credit memo") + total_tax: Money! @doc(description: "The amount of tax applied to the credit memo") + taxes: [TaxItem] @doc(description: "The credit memo tax details") + grand_total: Money! @doc(description: "The final total amount, including shipping, discounts, and taxes") + base_grand_total: Money! @doc(description: "The final base grand total amount in the base currency") + total_shipping: Money! @doc(description: "The shipping amount for the credit memo") + shipping_handling: ShippingHandling @doc(description: "Contains details about the shipping and handling costs for the credit memo") + adjustment: Money! @doc(description: "An adjustment manually applied to the order") +} + +type KeyValue @doc(description: "The key-value type") { + name: String @doc(description: "The name part of the name/value pair") + value: String @doc(description: "The value part of the name/value pair") +} + enum CheckoutUserInputErrorCodes { REORDER_NOT_AVAILABLE PRODUCT_NOT_FOUND diff --git a/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php index 3f2ba38fa5a55..2739226c5fb5a 100644 --- a/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php +++ b/app/code/Magento/SalesInventory/Model/Order/ReturnProcessor.php @@ -12,6 +12,7 @@ * Class ReturnProcessor * * @api + * @since 100.0.0 */ class ReturnProcessor { @@ -68,6 +69,7 @@ public function __construct( * @param array $returnToStockItems * @param bool $isAutoReturn * @return void + * @since 100.0.0 */ public function execute( CreditmemoInterface $creditmemo, diff --git a/app/code/Magento/SalesRule/Api/Data/CouponInterface.php b/app/code/Magento/SalesRule/Api/Data/CouponInterface.php index bd44ea829fe66..2ab731f2f7974 100644 --- a/app/code/Magento/SalesRule/Api/Data/CouponInterface.php +++ b/app/code/Magento/SalesRule/Api/Data/CouponInterface.php @@ -110,7 +110,7 @@ public function setTimesUsed($timesUsed); * Get expiration date * * @return string|null - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function getExpirationDate(); @@ -119,7 +119,7 @@ public function getExpirationDate(); * * @param string $expirationDate * @return $this - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function setExpirationDate($expirationDate); diff --git a/app/code/Magento/SalesRule/Model/Coupon.php b/app/code/Magento/SalesRule/Model/Coupon.php index a8c77c6ceeec8..070ce89c1d474 100644 --- a/app/code/Magento/SalesRule/Model/Coupon.php +++ b/app/code/Magento/SalesRule/Model/Coupon.php @@ -207,7 +207,7 @@ public function setTimesUsed($timesUsed) * Get expiration date * * @return string|null - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function getExpirationDate() { @@ -219,7 +219,7 @@ public function getExpirationDate() * * @param string $expirationDate * @return $this - * @deprecated Coupon expiration must follow sales rule expiration date. + * @deprecated 101.1.3 Coupon expiration must follow sales rule expiration date. */ public function setExpirationDate($expirationDate) { diff --git a/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php new file mode 100644 index 0000000000000..0ee2ee09cad57 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/Quote/UpdateCouponUsages.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon\Quote; + +use Magento\Quote\Api\Data\CartInterface; +use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; + +/** + * Updates the coupon usages from quote + */ +class UpdateCouponUsages +{ + /** + * @var CouponUsageProcessor + */ + private $couponUsageProcessor; + + /** + * @var UpdateInfoFactory + */ + private $updateInfoFactory; + + /** + * @param CouponUsageProcessor $couponUsageProcessor + * @param UpdateInfoFactory $updateInfoFactory + */ + public function __construct( + CouponUsageProcessor $couponUsageProcessor, + UpdateInfoFactory $updateInfoFactory + ) { + $this->couponUsageProcessor = $couponUsageProcessor; + $this->updateInfoFactory = $updateInfoFactory; + } + + /** + * Executes the current command + * + * @param CartInterface $quote + * @param bool $increment + * @return void + */ + public function execute(CartInterface $quote, bool $increment): void + { + if (!$quote->getAppliedRuleIds()) { + return; + } + + /** @var UpdateInfo $updateInfo */ + $updateInfo = $this->updateInfoFactory->create(); + $updateInfo->setAppliedRuleIds(explode(',', $quote->getAppliedRuleIds())); + $updateInfo->setCouponCode((string)$quote->getCouponCode()); + $updateInfo->setCustomerId((int)$quote->getCustomerId()); + $updateInfo->setIsIncrement($increment); + + $this->couponUsageProcessor->process($updateInfo); + } +} diff --git a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php index 3236c80e1b7ed..1645f205d1e55 100644 --- a/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php +++ b/app/code/Magento/SalesRule/Model/Coupon/UpdateCouponUsages.php @@ -8,56 +8,39 @@ namespace Magento\SalesRule\Model\Coupon; use Magento\Sales\Api\Data\OrderInterface; -use Magento\SalesRule\Model\Coupon; -use Magento\SalesRule\Model\ResourceModel\Coupon\Usage; -use Magento\SalesRule\Model\Rule\CustomerFactory; -use Magento\SalesRule\Model\RuleFactory; +use Magento\SalesRule\Model\Coupon\Usage\Processor as CouponUsageProcessor; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfo; +use Magento\SalesRule\Model\Coupon\Usage\UpdateInfoFactory; /** - * Updates the coupon usages. + * Updates the coupon usages */ class UpdateCouponUsages { /** - * @var RuleFactory + * @var CouponUsageProcessor */ - private $ruleFactory; + private $couponUsageProcessor; /** - * @var RuleFactory + * @var UpdateInfoFactory */ - private $ruleCustomerFactory; + private $updateInfoFactory; /** - * @var Coupon - */ - private $coupon; - - /** - * @var Usage - */ - private $couponUsage; - - /** - * @param RuleFactory $ruleFactory - * @param CustomerFactory $ruleCustomerFactory - * @param Coupon $coupon - * @param Usage $couponUsage + * @param CouponUsageProcessor $couponUsageProcessor + * @param UpdateInfoFactory $updateInfoFactory */ public function __construct( - RuleFactory $ruleFactory, - CustomerFactory $ruleCustomerFactory, - Coupon $coupon, - Usage $couponUsage + CouponUsageProcessor $couponUsageProcessor, + UpdateInfoFactory $updateInfoFactory ) { - $this->ruleFactory = $ruleFactory; - $this->ruleCustomerFactory = $ruleCustomerFactory; - $this->coupon = $coupon; - $this->couponUsage = $couponUsage; + $this->couponUsageProcessor = $couponUsageProcessor; + $this->updateInfoFactory = $updateInfoFactory; } /** - * Executes the current command. + * Executes the current command * * @param OrderInterface $subject * @param bool $increment @@ -68,86 +51,16 @@ public function execute(OrderInterface $subject, bool $increment): OrderInterfac if (!$subject || !$subject->getAppliedRuleIds()) { return $subject; } - // lookup rule ids - $ruleIds = explode(',', $subject->getAppliedRuleIds()); - $ruleIds = array_unique($ruleIds); - $customerId = (int)$subject->getCustomerId(); - // use each rule (and apply to customer, if applicable) - foreach ($ruleIds as $ruleId) { - if (!$ruleId) { - continue; - } - $this->updateRuleUsages($increment, (int)$ruleId, $customerId); - } - $this->updateCouponUsages($subject, $increment, $customerId); - - return $subject; - } - /** - * Update the number of rule usages. - * - * @param bool $increment - * @param int $ruleId - * @param int $customerId - */ - private function updateRuleUsages(bool $increment, int $ruleId, int $customerId) - { - /** @var \Magento\SalesRule\Model\Rule $rule */ - $rule = $this->ruleFactory->create(); - $rule->load($ruleId); - if ($rule->getId()) { - $rule->loadCouponCode(); - if ($increment || $rule->getTimesUsed() > 0) { - $rule->setTimesUsed($rule->getTimesUsed() + ($increment ? 1 : -1)); - $rule->save(); - } - if ($customerId) { - $this->updateCustomerRuleUsages($increment, $ruleId, $customerId); - } - } - } + /** @var UpdateInfo $updateInfo */ + $updateInfo = $this->updateInfoFactory->create(); + $updateInfo->setAppliedRuleIds(explode(',', $subject->getAppliedRuleIds())); + $updateInfo->setCouponCode((string)$subject->getCouponCode()); + $updateInfo->setCustomerId((int)$subject->getCustomerId()); + $updateInfo->setIsIncrement($increment); - /** - * Update the number of rule usages per customer. - * - * @param bool $increment - * @param int $ruleId - * @param int $customerId - */ - private function updateCustomerRuleUsages(bool $increment, int $ruleId, int $customerId): void - { - /** @var \Magento\SalesRule\Model\Rule\Customer $ruleCustomer */ - $ruleCustomer = $this->ruleCustomerFactory->create(); - $ruleCustomer->loadByCustomerRule($customerId, $ruleId); - if ($ruleCustomer->getId()) { - if ($increment || $ruleCustomer->getTimesUsed() > 0) { - $ruleCustomer->setTimesUsed($ruleCustomer->getTimesUsed() + ($increment ? 1 : -1)); - } - } elseif ($increment) { - $ruleCustomer->setCustomerId($customerId)->setRuleId($ruleId)->setTimesUsed(1); - } - $ruleCustomer->save(); - } + $this->couponUsageProcessor->process($updateInfo); - /** - * Update the number of coupon usages. - * - * @param OrderInterface $subject - * @param bool $increment - * @param int $customerId - */ - private function updateCouponUsages(OrderInterface $subject, bool $increment, int $customerId): void - { - $this->coupon->load($subject->getCouponCode(), 'code'); - if ($this->coupon->getId()) { - if ($increment || $this->coupon->getTimesUsed() > 0) { - $this->coupon->setTimesUsed($this->coupon->getTimesUsed() + ($increment ? 1 : -1)); - $this->coupon->save(); - } - if ($customerId) { - $this->couponUsage->updateCustomerCouponTimesUsed($customerId, $this->coupon->getId(), $increment); - } - } + return $subject; } } diff --git a/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php b/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php new file mode 100644 index 0000000000000..90a456d5ff833 --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/Usage/Processor.php @@ -0,0 +1,149 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon\Usage; + +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\ResourceModel\Coupon\Usage; +use Magento\SalesRule\Model\Rule\CustomerFactory; +use Magento\SalesRule\Model\RuleFactory; + +/** + * Processor to update coupon usage + */ +class Processor +{ + /** + * @var RuleFactory + */ + private $ruleFactory; + + /** + * @var RuleFactory + */ + private $ruleCustomerFactory; + + /** + * @var Coupon + */ + private $coupon; + + /** + * @var Usage + */ + private $couponUsage; + + /** + * @param RuleFactory $ruleFactory + * @param CustomerFactory $ruleCustomerFactory + * @param Coupon $coupon + * @param Usage $couponUsage + */ + public function __construct( + RuleFactory $ruleFactory, + CustomerFactory $ruleCustomerFactory, + Coupon $coupon, + Usage $couponUsage + ) { + $this->ruleFactory = $ruleFactory; + $this->ruleCustomerFactory = $ruleCustomerFactory; + $this->coupon = $coupon; + $this->couponUsage = $couponUsage; + } + + /** + * Update coupon usage + * + * @param UpdateInfo $updateInfo + */ + public function process(UpdateInfo $updateInfo): void + { + if (empty($updateInfo->getAppliedRuleIds())) { + return; + } + + if (!empty($updateInfo->getCouponCode())) { + $this->updateCouponUsages($updateInfo); + } + $isIncrement = $updateInfo->isIncrement(); + $customerId = $updateInfo->getCustomerId(); + // use each rule (and apply to customer, if applicable) + foreach (array_unique($updateInfo->getAppliedRuleIds()) as $ruleId) { + if (!(int)$ruleId) { + continue; + } + $this->updateRuleUsages($isIncrement, (int)$ruleId); + if ($customerId) { + $this->updateCustomerRuleUsages($isIncrement, (int)$ruleId, $customerId); + } + } + } + + /** + * Update the number of coupon usages + * + * @param UpdateInfo $updateInfo + */ + private function updateCouponUsages(UpdateInfo $updateInfo): void + { + $isIncrement = $updateInfo->isIncrement(); + $this->coupon->load($updateInfo->getCouponCode(), 'code'); + if ($this->coupon->getId()) { + if ($updateInfo->isIncrement() || $this->coupon->getTimesUsed() > 0) { + $this->coupon->setTimesUsed($this->coupon->getTimesUsed() + ($isIncrement ? 1 : -1)); + $this->coupon->save(); + } + if ($updateInfo->getCustomerId()) { + $this->couponUsage->updateCustomerCouponTimesUsed( + $updateInfo->getCustomerId(), + $this->coupon->getId(), + $isIncrement + ); + } + } + } + + /** + * Update the number of rule usages + * + * @param bool $isIncrement + * @param int $ruleId + */ + private function updateRuleUsages(bool $isIncrement, int $ruleId): void + { + $rule = $this->ruleFactory->create(); + $rule->load($ruleId); + if ($rule->getId()) { + $rule->loadCouponCode(); + if ($isIncrement || $rule->getTimesUsed() > 0) { + $rule->setTimesUsed($rule->getTimesUsed() + ($isIncrement ? 1 : -1)); + $rule->save(); + } + } + } + + /** + * Update the number of rule usages per customer + * + * @param bool $isIncrement + * @param int $ruleId + * @param int $customerId + */ + private function updateCustomerRuleUsages(bool $isIncrement, int $ruleId, int $customerId): void + { + $ruleCustomer = $this->ruleCustomerFactory->create(); + $ruleCustomer->loadByCustomerRule($customerId, $ruleId); + if ($ruleCustomer->getId()) { + if ($isIncrement || $ruleCustomer->getTimesUsed() > 0) { + $ruleCustomer->setTimesUsed($ruleCustomer->getTimesUsed() + ($isIncrement ? 1 : -1)); + } + } elseif ($isIncrement) { + $ruleCustomer->setCustomerId($customerId)->setRuleId($ruleId)->setTimesUsed(1); + } + $ruleCustomer->save(); + } +} diff --git a/app/code/Magento/SalesRule/Model/Coupon/Usage/UpdateInfo.php b/app/code/Magento/SalesRule/Model/Coupon/Usage/UpdateInfo.php new file mode 100644 index 0000000000000..328093ca1af0e --- /dev/null +++ b/app/code/Magento/SalesRule/Model/Coupon/Usage/UpdateInfo.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Coupon\Usage; + +use Magento\Framework\DataObject; + +/** + * Coupon usages info to update + */ +class UpdateInfo extends DataObject +{ + private const APPLIED_RULE_IDS_KEY = 'applied_rule_ids'; + private const COUPON_CODE_KEY = 'coupon_code'; + private const CUSTOMER_ID_KEY = 'customer_id'; + private const IS_INCREMENT_KEY = 'is_increment'; + + /** + * Get applied rule ids + * + * @return array + */ + public function getAppliedRuleIds(): array + { + return (array)$this->getData(self::APPLIED_RULE_IDS_KEY); + } + + /** + * Set applied rule ids + * + * @param array $value + * @return void + */ + public function setAppliedRuleIds(array $value): void + { + $this->setData(self::APPLIED_RULE_IDS_KEY, $value); + } + + /** + * Get coupon code + * + * @return string + */ + public function getCouponCode(): string + { + return (string)$this->getData(self::COUPON_CODE_KEY); + } + + /** + * Set coupon code + * + * @param string $value + * @return void + */ + public function setCouponCode(string $value): void + { + $this->setData(self::COUPON_CODE_KEY, $value); + } + + /** + * Get customer id + * + * @return int|null + */ + public function getCustomerId(): ?int + { + return $this->getData(self::CUSTOMER_ID_KEY) !== null + ? (int) $this->getData(self::CUSTOMER_ID_KEY) + : null; + } + + /** + * Set customer id + * + * @param int|null $value + * @return void + */ + public function setCustomerId(?int $value): void + { + $this->setData(self::CUSTOMER_ID_KEY, $value); + } + + /** + * Get update mode: increment - true, decrement - false + * + * @return bool + */ + public function isIncrement(): bool + { + return (bool)$this->getData(self::IS_INCREMENT_KEY); + } + + /** + * Set update mode: increment - true, decrement - false + * + * @param bool $value + * @return void + */ + public function setIsIncrement(bool $value): void + { + $this->setData(self::IS_INCREMENT_KEY, $value); + } +} diff --git a/app/code/Magento/SalesRule/Model/CouponRepository.php b/app/code/Magento/SalesRule/Model/CouponRepository.php index 4c557832fa8d6..f32fbc3d12134 100644 --- a/app/code/Magento/SalesRule/Model/CouponRepository.php +++ b/app/code/Magento/SalesRule/Model/CouponRepository.php @@ -197,7 +197,7 @@ public function deleteById($couponId) * * @param FilterGroup $filterGroup * @param Collection $collection - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return void */ protected function addFilterGroupToCollection( @@ -219,7 +219,7 @@ protected function addFilterGroupToCollection( /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/SalesRule/Model/Quote/Discount.php b/app/code/Magento/SalesRule/Model/Quote/Discount.php index a580a8f9d2eaa..a32fe249920b1 100644 --- a/app/code/Magento/SalesRule/Model/Quote/Discount.php +++ b/app/code/Magento/SalesRule/Model/Quote/Discount.php @@ -6,37 +6,53 @@ namespace Magento\SalesRule\Model\Quote; use Magento\Framework\App\ObjectManager; -use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; +use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\Pricing\PriceCurrencyInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Address\Total; +use Magento\Quote\Model\Quote\Address\Total\AbstractTotal; +use Magento\Quote\Model\Quote\Item; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\SalesRule\Api\Data\DiscountDataInterface; use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; +use Magento\SalesRule\Api\Data\RuleDiscountInterfaceFactory; +use Magento\SalesRule\Model\Data\RuleDiscount; +use Magento\SalesRule\Model\Discount\PostProcessorFactory; +use Magento\SalesRule\Model\Validator; +use Magento\Store\Model\StoreManagerInterface; /** * Discount totals calculation model. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal +class Discount extends AbstractTotal { const COLLECTOR_TYPE_CODE = 'discount'; /** * Discount calculation object * - * @var \Magento\SalesRule\Model\Validator + * @var Validator */ protected $calculator; /** * Core event manager proxy * - * @var \Magento\Framework\Event\ManagerInterface + * @var ManagerInterface */ protected $eventManager = null; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $storeManager; /** - * @var \Magento\Framework\Pricing\PriceCurrencyInterface + * @var PriceCurrencyInterface */ protected $priceCurrency; @@ -51,18 +67,18 @@ class Discount extends \Magento\Quote\Model\Quote\Address\Total\AbstractTotal private $discountDataInterfaceFactory; /** - * @param \Magento\Framework\Event\ManagerInterface $eventManager - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\SalesRule\Model\Validator $validator - * @param \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency + * @param ManagerInterface $eventManager + * @param StoreManagerInterface $storeManager + * @param Validator $validator + * @param PriceCurrencyInterface $priceCurrency * @param RuleDiscountInterfaceFactory|null $discountInterfaceFactory * @param DiscountDataInterfaceFactory|null $discountDataInterfaceFactory */ public function __construct( - \Magento\Framework\Event\ManagerInterface $eventManager, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\SalesRule\Model\Validator $validator, - \Magento\Framework\Pricing\PriceCurrencyInterface $priceCurrency, + ManagerInterface $eventManager, + StoreManagerInterface $storeManager, + Validator $validator, + PriceCurrencyInterface $priceCurrency, RuleDiscountInterfaceFactory $discountInterfaceFactory = null, DiscountDataInterfaceFactory $discountDataInterfaceFactory = null ) { @@ -80,17 +96,17 @@ public function __construct( /** * Collect address discount amount * - * @param \Magento\Quote\Model\Quote $quote - * @param \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment - * @param \Magento\Quote\Model\Quote\Address\Total $total + * @param Quote $quote + * @param ShippingAssignmentInterface $shippingAssignment + * @param Total $total * @return $this * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ public function collect( - \Magento\Quote\Model\Quote $quote, - \Magento\Quote\Api\Data\ShippingAssignmentInterface $shippingAssignment, - \Magento\Quote\Model\Quote\Address\Total $total + Quote $quote, + ShippingAssignmentInterface $shippingAssignment, + Total $total ) { parent::collect($quote, $shippingAssignment, $total); @@ -122,7 +138,7 @@ public function collect( $address->getExtensionAttributes()->setDiscounts([]); $addressDiscountAggregator = []; - /** @var \Magento\Quote\Model\Quote\Item $item */ + /** @var Item $item */ foreach ($items as $item) { if ($item->getNoDiscount() || !$this->calculator->canApplyDiscount($item)) { $item->setDiscountAmount(0); @@ -147,7 +163,6 @@ public function collect( if ($item->getHasChildren() && $item->isChildrenCalculated()) { $this->calculator->process($item); - $this->distributeDiscount($item); foreach ($item->getChildren() as $child) { $eventArgs['item'] = $child; $this->eventManager->dispatch('sales_quote_address_discount_item', $eventArgs); @@ -175,13 +190,13 @@ public function collect( /** * Aggregate item discount information to total data and related properties * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\Quote\Model\Quote\Address\Total $total + * @param AbstractItem $item + * @param Total $total * @return $this */ protected function aggregateItemDiscount( - \Magento\Quote\Model\Quote\Item\AbstractItem $item, - \Magento\Quote\Model\Quote\Address\Total $total + AbstractItem $item, + Total $total ) { $total->addTotalAmount($this->getCode(), -$item->getDiscountAmount()); $total->addBaseTotalAmount($this->getCode(), -$item->getBaseDiscountAmount()); @@ -191,10 +206,12 @@ protected function aggregateItemDiscount( /** * Distribute discount at parent item to children items * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * @param AbstractItem $item * @return $this + * @deprecated No longer used. + * @see \Magento\SalesRule\Model\RulesApplier::applyRule() */ - protected function distributeDiscount(\Magento\Quote\Model\Quote\Item\AbstractItem $item) + protected function distributeDiscount(AbstractItem $item) { $parentBaseRowTotal = $item->getBaseRowTotal(); $keys = [ @@ -230,12 +247,12 @@ protected function distributeDiscount(\Magento\Quote\Model\Quote\Item\AbstractIt /** * Add discount total information to address * - * @param \Magento\Quote\Model\Quote $quote - * @param \Magento\Quote\Model\Quote\Address\Total $total + * @param Quote $quote + * @param Total $total * @return array|null * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ - public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Quote\Address\Total $total) + public function fetch(Quote $quote, Total $total) { $result = null; $amount = $total->getDiscountAmount(); @@ -254,25 +271,25 @@ public function fetch(\Magento\Quote\Model\Quote $quote, \Magento\Quote\Model\Qu /** * Aggregates discount per rule * - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item - * @param \Magento\Quote\Api\Data\AddressInterface $address + * @param AbstractItem $item + * @param AddressInterface $address * @param array $addressDiscountAggregator * @return void */ private function aggregateDiscountPerRule( - \Magento\Quote\Model\Quote\Item\AbstractItem $item, - \Magento\Quote\Api\Data\AddressInterface $address, + AbstractItem $item, + AddressInterface $address, array &$addressDiscountAggregator ) { $discountBreakdown = $item->getExtensionAttributes()->getDiscounts(); if ($discountBreakdown) { foreach ($discountBreakdown as $value) { - /* @var \Magento\SalesRule\Api\Data\DiscountDataInterface $discount */ + /* @var DiscountDataInterface $discount */ $discount = $value->getDiscountData(); $ruleLabel = $value->getRuleLabel(); $ruleID = $value->getRuleID(); if (isset($addressDiscountAggregator[$ruleID])) { - /** @var \Magento\SalesRule\Model\Data\RuleDiscount $cartDiscount */ + /** @var RuleDiscount $cartDiscount */ $cartDiscount = $addressDiscountAggregator[$ruleID]; $discountData = $cartDiscount->getDiscountData(); $discountData->setBaseAmount($discountData->getBaseAmount()+$discount->getBaseAmount()); @@ -294,12 +311,12 @@ private function aggregateDiscountPerRule( 'rule' => $ruleLabel, 'rule_id' => $ruleID, ]; - /** @var \Magento\SalesRule\Model\Data\RuleDiscount $cartDiscount */ + /** @var RuleDiscount $cartDiscount */ $cartDiscount = $this->discountInterfaceFactory->create(['data' => $data]); $addressDiscountAggregator[$ruleID] = $cartDiscount; } } } - $address->getExtensionAttributes()->setDiscounts(array_values($addressDiscountAggregator)); + $address->getExtensionAttributes()->setDiscounts(array_values($addressDiscountAggregator)); } } diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php index 114d2e6784b72..eaa2433e63ed0 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/BuyXGetY.php @@ -3,19 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\SalesRule\Model\Rule\Action\Discount; +use Magento\Quote\Model\Quote\Item\AbstractItem; +use Magento\SalesRule\Model\Rule; + class BuyXGetY extends AbstractDiscount { /** - * @param \Magento\SalesRule\Model\Rule $rule - * @param \Magento\Quote\Model\Quote\Item\AbstractItem $item + * Calculate discount data for BuyXGetY action. + * + * @param Rule $rule + * @param AbstractItem $item * @param float $qty - * @return \Magento\SalesRule\Model\Rule\Action\Discount\Data + * @return Data */ - public function calculate($rule, $item, $qty) + public function calculate($rule, $item, $qty): Data { - /** @var \Magento\SalesRule\Model\Rule\Action\Discount\Data $discountData */ $discountData = $this->discountFactory->create(); $itemPrice = $this->validator->getItemPrice($item); diff --git a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php index b4585bb047c44..1569c9551aa46 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php +++ b/app/code/Magento/SalesRule/Model/Rule/Action/Discount/CartFixed.php @@ -190,7 +190,7 @@ public function calculate($rule, $item, $qty) /** * Set information about usage cart fixed rule by quote address * - * @deprecated should be removed as it is not longer used + * @deprecated 101.2.0 should be removed as it is not longer used * @param int $ruleId * @param int $itemId * @return void @@ -203,7 +203,7 @@ protected function setCartFixedRuleUsedForAddress($ruleId, $itemId) /** * Retrieve information about usage cart fixed rule by quote address * - * @deprecated should be removed as it is not longer used + * @deprecated 101.2.0 should be removed as it is not longer used * @param int $ruleId * @return int|null */ diff --git a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php index 6ade7a064e849..35e7e62144611 100644 --- a/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php +++ b/app/code/Magento/SalesRule/Model/Rule/Condition/Product/Combine.php @@ -89,6 +89,7 @@ public function collectValidatedAttributes($productCollection) /** * @inheritdoc + * @since 101.0.6 */ protected function _isValid($entity) { diff --git a/app/code/Magento/SalesRule/Model/RuleRepository.php b/app/code/Magento/SalesRule/Model/RuleRepository.php index 2cff0d64dba01..2016ae0dde1c7 100644 --- a/app/code/Magento/SalesRule/Model/RuleRepository.php +++ b/app/code/Magento/SalesRule/Model/RuleRepository.php @@ -184,7 +184,7 @@ public function deleteById($id) * * @param FilterGroup $filterGroup * @param Collection $collection - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return void */ protected function addFilterGroupToCollection( @@ -206,7 +206,7 @@ protected function addFilterGroupToCollection( /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/SalesRule/Model/RulesApplier.php b/app/code/Magento/SalesRule/Model/RulesApplier.php index 270732c8e0278..ede889c79fb9d 100644 --- a/app/code/Magento/SalesRule/Model/RulesApplier.php +++ b/app/code/Magento/SalesRule/Model/RulesApplier.php @@ -16,9 +16,7 @@ use Magento\SalesRule\Api\Data\DiscountDataInterfaceFactory; /** - * Class RulesApplier - * - * @package Magento\SalesRule\Model\Validator + * Rule applier model */ class RulesApplier { @@ -115,7 +113,6 @@ public function applyRules($item, $rules, $skipValidation, $couponCode) if (!$this->validatorUtility->canProcessRule($rule, $address)) { continue; } - if (!$skipValidation && !$rule->getActions()->validate($item)) { if (!$this->childrenValidationLocator->isChildrenValidationRequired($item)) { continue; @@ -189,8 +186,22 @@ public function addDiscountDescription($address, $rule) */ protected function applyRule($item, $rule, $address, $couponCode) { - $discountData = $this->getDiscountData($item, $rule, $address); - $this->setDiscountData($discountData, $item); + if ($item->getChildren() && $item->isChildrenCalculated()) { + $cloneItem = clone $item; + /** + * validate without children + */ + $applyAll = $rule->getActions()->validate($cloneItem); + foreach ($item->getChildren() as $childItem) { + if ($applyAll || $rule->getActions()->validate($childItem)) { + $discountData = $this->getDiscountData($childItem, $rule, $address); + $this->setDiscountData($discountData, $childItem); + } + } + } else { + $discountData = $this->getDiscountData($item, $rule, $address); + $this->setDiscountData($discountData, $item); + } $this->maintainAddressCouponCode($address, $rule, $couponCode); $this->addDiscountDescription($address, $rule); diff --git a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php index b698190997d7e..7f355a62c4631 100644 --- a/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php +++ b/app/code/Magento/SalesRule/Model/Service/CouponManagementService.php @@ -19,7 +19,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement { /** * @var \Magento\SalesRule\Model\CouponFactory - * @deprecated + * @deprecated 101.1.2 */ protected $couponFactory; @@ -30,7 +30,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement /** * @var \Magento\SalesRule\Model\ResourceModel\Coupon\CollectionFactory - * @deprecated + * @deprecated 101.1.2 */ protected $collectionFactory; @@ -41,7 +41,7 @@ class CouponManagementService implements \Magento\SalesRule\Api\CouponManagement /** * @var \Magento\SalesRule\Model\Spi\CouponResourceInterface - * @deprecated + * @deprecated 101.1.2 */ protected $resourceModel; diff --git a/app/code/Magento/SalesRule/Model/Validator.php b/app/code/Magento/SalesRule/Model/Validator.php index 0fc0b062c7887..cc0333480f7b0 100644 --- a/app/code/Magento/SalesRule/Model/Validator.php +++ b/app/code/Magento/SalesRule/Model/Validator.php @@ -7,6 +7,7 @@ namespace Magento\SalesRule\Model; use Magento\Framework\App\ObjectManager; +use Magento\Quote\Model\Quote; use Magento\Quote\Model\Quote\Address; use Magento\Quote\Model\Quote\Item\AbstractItem; use Magento\SalesRule\Helper\CartFixedDiscount; @@ -280,6 +281,13 @@ public function process(AbstractItem $item) $item->setDiscountAmount(0); $item->setBaseDiscountAmount(0); $item->setDiscountPercent(0); + if ($item->getChildren() && $item->isChildrenCalculated()) { + foreach ($item->getChildren() as $child) { + $child->setDiscountAmount(0); + $child->setBaseDiscountAmount(0); + $child->setDiscountPercent(0); + } + } $itemPrice = $this->getItemPrice($item); if ($itemPrice < 0) { @@ -319,7 +327,7 @@ public function processShippingAmount(Address $address) $quote = $address->getQuote(); $appliedRuleIds = []; foreach ($this->_getRules($address) as $rule) { - /* @var \Magento\SalesRule\Model\Rule $rule */ + /* @var Rule $rule */ if (!$rule->getApplyToShipping() || !$this->validatorUtility->canProcessRule($rule, $address)) { continue; } @@ -328,28 +336,28 @@ public function processShippingAmount(Address $address) $baseDiscountAmount = 0; $rulePercent = min(100, $rule->getDiscountAmount()); switch ($rule->getSimpleAction()) { - case \Magento\SalesRule\Model\Rule::TO_PERCENT_ACTION: + case Rule::TO_PERCENT_ACTION: $rulePercent = max(0, 100 - $rule->getDiscountAmount()); // break is intentionally omitted // no break - case \Magento\SalesRule\Model\Rule::BY_PERCENT_ACTION: + case Rule::BY_PERCENT_ACTION: $discountAmount = ($shippingAmount - $address->getShippingDiscountAmount()) * $rulePercent / 100; $baseDiscountAmount = ($baseShippingAmount - $address->getBaseShippingDiscountAmount()) * $rulePercent / 100; $discountPercent = min(100, $address->getShippingDiscountPercent() + $rulePercent); $address->setShippingDiscountPercent($discountPercent); break; - case \Magento\SalesRule\Model\Rule::TO_FIXED_ACTION: + case Rule::TO_FIXED_ACTION: $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore()); $discountAmount = $shippingAmount - $quoteAmount; $baseDiscountAmount = $baseShippingAmount - $rule->getDiscountAmount(); break; - case \Magento\SalesRule\Model\Rule::BY_FIXED_ACTION: + case Rule::BY_FIXED_ACTION: $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore()); $discountAmount = $quoteAmount; $baseDiscountAmount = $rule->getDiscountAmount(); break; - case \Magento\SalesRule\Model\Rule::CART_FIXED_ACTION: + case Rule::CART_FIXED_ACTION: $cartRules = $address->getCartFixedRules(); $quoteAmount = $this->priceCurrency->convert($rule->getDiscountAmount(), $quote->getStore()); $isAppliedToShipping = (int) $rule->getApplyToShipping(); @@ -385,6 +393,12 @@ public function processShippingAmount(Address $address) } $address->setCartFixedRules($cartRules); break; + case Rule::BUY_X_GET_Y_ACTION: + $allQtyDiscount = $this->getDiscountQtyAllItemsBuyXGetYAction($quote, $rule); + $quoteAmount = $address->getBaseShippingAmount() / $quote->getItemsQty() * $allQtyDiscount; + $discountAmount = $this->priceCurrency->convert($quoteAmount, $quote->getStore()); + $baseDiscountAmount = $quoteAmount; + break; } $discountAmount = min($address->getShippingDiscountAmount() + $discountAmount, $shippingAmount); @@ -426,9 +440,9 @@ public function initTotals($items, Address $address) return $this; } - /** @var \Magento\SalesRule\Model\Rule $rule */ + /** @var Rule $rule */ foreach ($this->_getRules($address) as $rule) { - if (\Magento\SalesRule\Model\Rule::CART_FIXED_ACTION == $rule->getSimpleAction() + if (Rule::CART_FIXED_ACTION == $rule->getSimpleAction() && $this->validatorUtility->canProcessRule($rule, $address) ) { $ruleTotalItemsPrice = 0; @@ -481,6 +495,40 @@ private function isValidItemForRule(AbstractItem $item, Rule $rule) return true; } + /** + * Return discount Qty for all items at Buy_X_Get_Y_Action + * + * @param Quote $quote + * @param Rule $rule + * @return float + */ + private function getDiscountQtyAllItemsBuyXGetYAction(Quote $quote, Rule $rule): float + { + $discountAllQty = 0; + foreach ($quote->getItems() as $item) { + $qty = $item->getQty(); + + $discountStep = $rule->getDiscountStep(); + $discountAmount = $rule->getDiscountAmount(); + if (!$discountStep || $discountAmount > $discountStep) { + continue; + } + $buyAndDiscountQty = $discountStep + $discountAmount; + + $fullRuleQtyPeriod = floor($qty / $buyAndDiscountQty); + $freeQty = $qty - $fullRuleQtyPeriod * $buyAndDiscountQty; + + $discountQty = $fullRuleQtyPeriod * $discountAmount; + if ($freeQty > $discountStep) { + $discountQty += $freeQty - $discountStep; + } + + $discountAllQty += $discountQty; + } + + return $discountAllQty; + } + /** * Return item price * @@ -564,7 +612,7 @@ public function prepareDescription($address, $separator = ', ') public function sortItemsByPriority($items, Address $address = null) { $itemsSorted = []; - /** @var $rule \Magento\SalesRule\Model\Rule */ + /** @var $rule Rule */ foreach ($this->_getRules($address) as $rule) { foreach ($items as $itemKey => $itemValue) { if ($rule->getActions()->validate($itemValue)) { diff --git a/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php index 2d771e4560fcf..1d416fbcf4f52 100644 --- a/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php +++ b/app/code/Magento/SalesRule/Observer/AssignCouponDataAfterOrderCustomerAssignObserver.php @@ -45,9 +45,10 @@ public function execute(Observer $observer) $event = $observer->getEvent(); /** @var OrderInterface $order */ $order = $event->getData(self::EVENT_KEY_ORDER); - - if ($order->getCustomerId()) { - $this->updateCouponUsages->execute($order, true); + if (!$order->getCustomerId()) { + return; } + + $this->updateCouponUsages->execute($order, true); } } diff --git a/app/code/Magento/SalesRule/Observer/CouponUsagesDecrement.php b/app/code/Magento/SalesRule/Observer/CouponUsagesDecrement.php new file mode 100644 index 0000000000000..d0c7199405879 --- /dev/null +++ b/app/code/Magento/SalesRule/Observer/CouponUsagesDecrement.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Observer; + +use Magento\Framework\Event\Observer as EventObserver; +use Magento\Framework\Event\ObserverInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\SalesRule\Model\Coupon\Quote\UpdateCouponUsages; + +/** + * Decrement number of coupon usages after error of placing order + */ +class CouponUsagesDecrement implements ObserverInterface +{ + /** + * @var UpdateCouponUsages + */ + private $updateCouponUsages; + + /** + * @param UpdateCouponUsages $updateCouponUsages + */ + public function __construct(UpdateCouponUsages $updateCouponUsages) + { + $this->updateCouponUsages = $updateCouponUsages; + } + + /** + * @inheritdoc + */ + public function execute(EventObserver $observer) + { + /** @var CartInterface $quote */ + $quote = $observer->getQuote(); + $this->updateCouponUsages->execute($quote, false); + } +} diff --git a/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php b/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php index 87a7c2ed1bd38..3be801a288479 100644 --- a/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php +++ b/app/code/Magento/SalesRule/Plugin/CouponUsagesDecrement.php @@ -49,11 +49,13 @@ public function __construct( */ public function afterCancel(OrderService $subject, bool $result, $orderId): bool { - $order = $this->orderRepository->get($orderId); - if ($result) { - $this->updateCouponUsages->execute($order, false); + if (!$result) { + return $result; } + $order = $this->orderRepository->get($orderId); + $this->updateCouponUsages->execute($order, false); + return $result; } } diff --git a/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php b/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php index 14bbb5fce02a5..66a32f37eee2f 100644 --- a/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php +++ b/app/code/Magento/SalesRule/Plugin/CouponUsagesIncrement.php @@ -7,12 +7,13 @@ namespace Magento\SalesRule\Plugin; -use Magento\Sales\Api\Data\OrderInterface; -use Magento\Sales\Model\Service\OrderService; -use Magento\SalesRule\Model\Coupon\UpdateCouponUsages; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteManagement; +use Magento\SalesRule\Model\Coupon\Quote\UpdateCouponUsages; /** - * Increments number of coupon usages after placing order. + * Increments number of coupon usages before placing order */ class CouponUsagesIncrement { @@ -24,24 +25,28 @@ class CouponUsagesIncrement /** * @param UpdateCouponUsages $updateCouponUsages */ - public function __construct( - UpdateCouponUsages $updateCouponUsages - ) { + public function __construct(UpdateCouponUsages $updateCouponUsages) + { $this->updateCouponUsages = $updateCouponUsages; } /** - * Increments number of coupon usages after placing order. + * Increments number of coupon usages before placing order * - * @param OrderService $subject - * @param OrderInterface $result - * @return OrderInterface + * @param QuoteManagement $subject + * @param Quote $quote + * @param array $orderData + * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @throws NoSuchEntityException */ - public function afterPlace(OrderService $subject, OrderInterface $result): OrderInterface + public function beforeSubmit(QuoteManagement $subject, Quote $quote, $orderData = []) { - $this->updateCouponUsages->execute($result, true); + /* if coupon code has been canceled then need to notify the customer */ + if (!$quote->getCouponCode() && $quote->dataHasChangedFor('coupon_code')) { + throw new NoSuchEntityException(__("The coupon code isn't valid. Verify the code and try again.")); + } - return $result; + $this->updateCouponUsages->execute($quote, true); } } diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml new file mode 100644 index 0000000000000..85437650efc35 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleDeleteAllActionGroup.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCartPriceRuleDeleteAllActionGroup"> + <annotations> + <description>Open Cart Price Rule grid and delete all rules one by one. Need to avoid interference with other tests that test cart price rules.</description> + </annotations> + + <amOnPage url="{{AdminCartPriceRulesPage.url}}" stepKey="goToAdminCartPriceRuleGridPage"/> + <!-- It sometimes is loading too long for default 10s --> + <waitForPageLoad time="60" stepKey="waitForPageFullyLoaded"/> + <conditionalClick selector="{{AdminDataGridHeaderSection.clearFilters}}" dependentSelector="{{AdminDataGridHeaderSection.clearFilters}}" visible="true" stepKey="clearExistingFilters"/> + <helper class="\Magento\Rule\Test\Mftf\Helper\RuleHelper" method="deleteAllRulesOneByOne" stepKey="deleteAllRulesOneByOne"> + <argument name="firstNotEmptyRow">{{AdminDataGridTableSection.firstNotEmptyRow}}</argument> + <argument name="modalAcceptButton">{{AdminConfirmationModalSection.ok}}</argument> + <argument name="deleteButton">{{AdminMainActionsSection.delete}}</argument> + <argument name="successMessageContainer">{{AdminMessagesSection.success}}</argument> + <argument name="successMessage">You deleted the rule.</argument> + </helper> + <waitForElementVisible selector="{{AdminDataGridTableSection.dataGridEmpty}}" stepKey="waitDataGridEmptyMessageAppears"/> + <see selector="{{AdminDataGridTableSection.dataGridEmpty}}" userInput="We couldn't find any records." stepKey="assertDataGridEmptyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillActionsActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillActionsActionGroup.xml new file mode 100644 index 0000000000000..391a11cd7f1dc --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillActionsActionGroup.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCartPriceRuleFillActionsActionGroup"> + <annotations> + <description>Fill Cart Price Rule actions fields: Apply, Discount Amount, Discard subsequent rules.</description> + </annotations> + <arguments> + <argument name="apply" type="string" defaultValue="{{ApiSalesRule.simple_action}}"/> + <argument name="discountAmount" type="string" defaultValue="{{ApiSalesRule.discount_amount}}"/> + <argument name="discardSubsequentRules" type="string" defaultValue="1"/> + </arguments> + + <conditionalClick selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" dependentSelector="{{AdminCartPriceRulesFormSection.actionsHeaderOpen}}" visible="false" stepKey="clickToExpandActions"/> + <scrollTo selector="{{AdminCartPriceRulesFormSection.actionsHeader}}" stepKey="scrollToActionsFieldset"/> + <waitForElementVisible selector="{{AdminCartPriceRulesFormSection.apply}}" stepKey="waitActionsFieldsetFullyOpened"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.apply}}" userInput="{{apply}}" stepKey="fillDiscountType"/> + <fillField selector="{{AdminCartPriceRulesFormSection.discountAmount}}" userInput="{{discountAmount}}" stepKey="fillDiscountAmount"/> + <pressKey selector="{{AdminCartPriceRulesFormSection.discountAmount}}" parameterArray="[\Facebook\WebDriver\WebDriverKeys::TAB]" stepKey="pressTab"/> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.discardSubsequentRulesLabel}}" dependentSelector="{{AdminCartPriceRulesFormSection.discardSubsequentRulesByStatus(discardSubsequentRules)}}" visible="false" stepKey="fillDiscardSubsequentRules"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillMainInfoActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillMainInfoActionGroup.xml new file mode 100644 index 0000000000000..4624278d7f3f4 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleFillMainInfoActionGroup.xml @@ -0,0 +1,35 @@ +<?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="AdminCartPriceRuleFillMainInfoActionGroup"> + <annotations> + <description>Fill Cart Price Rule main info fields: Name, Description, Active (1/0), Priority.</description> + </annotations> + <arguments> + <argument name="name" type="string" defaultValue="{{ApiSalesRule.name}}"/> + <argument name="description" type="string" defaultValue="{{ApiSalesRule.description}}"/> + <argument name="active" type="string" defaultValue="1"/> + <argument name="websites" type="string" defaultValue="'Main Website'"/> + <argument name="groups" type="string" defaultValue="'NOT LOGGED IN','General','Wholesale','Retailer'"/> + <argument name="fromDate" type="string" defaultValue=""/> + <argument name="toDate" type="string" defaultValue=""/> + <argument name="priority" type="string" defaultValue=""/> + </arguments> + + <fillField selector="{{AdminCartPriceRulesFormSection.ruleName}}" userInput="{{name}}" stepKey="fillName"/> + <fillField selector="{{AdminCartPriceRulesFormSection.description}}" userInput="{{description}}" stepKey="fillDescription"/> + <conditionalClick selector="{{AdminCartPriceRulesFormSection.isActive}}" dependentSelector="{{AdminCartPriceRulesFormSection.activeByStatus(active)}}" visible="false" stepKey="fillActive"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.websites}}" parameterArray="[{{websites}}]" stepKey="selectSpecifiedWebsites"/> + <selectOption selector="{{AdminCartPriceRulesFormSection.customerGroups}}" parameterArray="[{{groups}}]" stepKey="selectSpecifiedCustomerGroups"/> + <fillField selector="{{AdminCartPriceRulesFormSection.fromDate}}" userInput="{{fromDate}}" stepKey="fillFromDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.toDate}}" userInput="{{toDate}}" stepKey="fillToDate"/> + <fillField selector="{{AdminCartPriceRulesFormSection.priority}}" userInput="{{priority}}" stepKey="fillPriority"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleSaveActionGroup.xml b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleSaveActionGroup.xml new file mode 100644 index 0000000000000..a94d5b4cf4889 --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/ActionGroup/AdminCartPriceRuleSaveActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminCartPriceRuleSaveActionGroup"> + <annotations> + <description>Clicks Save and Apply on a Admin Cart Price Rule creation/edit page. Validates that the Success Message is present.</description> + </annotations> + + <scrollToTopOfPage stepKey="scrollToTop"/> + <waitForElementVisible selector="{{AdminMainActionsSection.save}}" stepKey="waitForSaveButton"/> + <click selector="{{AdminMainActionsSection.save}}" stepKey="saveRule"/> + <waitForElementVisible selector="{{AdminMessagesSection.success}}" stepKey="waitForSuccessMessageAppears"/> + <see selector="{{AdminMessagesSection.success}}" userInput="You saved the rule." stepKey="checkSuccessSaveMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml index b164cdde33248..df126f05819d0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Section/AdminCartPriceRulesFormSection.xml @@ -10,7 +10,7 @@ <section name="AdminCartPriceRulesFormSection"> <element name="save" type="button" selector="#save" timeout="30"/> <element name="saveAndContinue" type="button" selector="#save_and_continue" timeout="30"/> - <element name="delete" type="button" selector="#delete" timeout="30"/> + <element name="delete" type="button" selector="button#delete" timeout="30"/> <element name="modalAcceptButton" type="button" selector="button.action-accept" timeout="30"/> <!-- Rule Information (the main form on the page) --> @@ -18,6 +18,8 @@ <element name="ruleName" type="input" selector="input[name='name']"/> <element name="description" type="textarea" selector="//div[@class='admin__field-control']/textarea[@name='description']"/> <element name="active" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='is_active']/../label"/> + <element name="isActive" type="text" selector="input[name='is_active']+label"/> + <element name="activeByStatus" type="text" selector="div.admin__actions-switch input[name='is_active'][value='{{value}}']+label" parameterized="true"/> <element name="websites" type="multiselect" selector="select[name='website_ids']"/> <element name="websitesOptions" type="select" selector="[name='website_ids'] option"/> <element name="customerGroups" type="multiselect" selector="select[name='customer_group_ids']"/> @@ -84,6 +86,8 @@ <element name="discountStep" type="input" selector="input[name='discount_step']"/> <element name="applyToShippingAmount" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='apply_to_shipping']/../label"/> <element name="discardSubsequentRules" type="checkbox" selector="//div[@class='admin__actions-switch']/input[@name='stop_rules_processing']/../label"/> + <element name="discardSubsequentRulesLabel" type="text" selector="div.admin__actions-switch input[name='stop_rules_processing']+label"/> + <element name="discardSubsequentRulesByStatus" type="text" selector="div.admin__actions-switch input[name='stop_rules_processing'][value='{{value}}']+label" parameterized="true"/> <element name="addRewardPoints" type="input" selector="input[name='extension_attributes[reward_points_delta]']"/> <element name="freeShipping" type="select" selector="//select[@name='simple_free_shipping']"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml index 1433d660d3535..61c80f32b6546 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCartRulesAppliedForProductInCartTest.xml @@ -50,8 +50,7 @@ </after> <!--Start creating a bundle product--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductList"/> - <waitForPageLoad stepKey="waitForProductList"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductList"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProduct"> <argument name="product" value="BundleProduct"/> </actionGroup> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml new file mode 100644 index 0000000000000..c65aa9980666f --- /dev/null +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateBuyXGetYFreeWithApplyShippingAmountTest.xml @@ -0,0 +1,29 @@ +<?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="AdminCreateBuyXGetYFreeWithApplyShippingAmountTest" extends="AdminCreateBuyXGetYFreeTest"> + <annotations> + <features value="SalesRule"/> + <stories value="Create cart price rule"/> + <title value="Admin should be able to create a cart price rule of type Buy X get Y free enable 'Apply to Shipping Amount' "/> + <description value="Use cart price rule of type Buy X get Y free with enable 'Apply to Shipping Amount'"/> + <severity value="MAJOR"/> + <group value="SalesRule"/> + </annotations> + + <remove keyForRemoval="verifyStorefront"/> + <click selector="{{AdminCartPriceRulesFormSection.applyDiscountToShippingLabel}}" stepKey="enabledApplyDiscountToShipping" after="fillDiscountStep"/> + <actionGroup ref="VerifyDiscountAmountActionGroup" stepKey="verifyStorefrontDiscount" after="fillProductFieldsInAdmin"> + <argument name="productUrl" value="{{_defaultProduct.urlKey}}.html"/> + <argument name="quantity" value="2"/> + <argument name="expectedDiscount" value="-$128.00"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml index 221f80b887fe5..f32442ca5bc98 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleEmptyFromDateTest.xml @@ -83,9 +83,7 @@ <!-- Spot check the storefront --> <amOnPage url="$$product.custom_attributes[url_key]$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyCoupon"> <argument name="coupon" value="_defaultCoupon"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml index e2a65685bd97e..557a585858868 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForCouponCodeTest.xml @@ -75,9 +75,7 @@ <!-- Spot check the storefront --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> <actionGroup ref="StorefrontApplyCouponActionGroup" stepKey="applyCoupon"> <argument name="coupon" value="_defaultCoupon"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml index 9f4168575595a..e18a9eaadcd23 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/AdminCreateCartPriceRuleForGeneratedCouponTest.xml @@ -79,9 +79,7 @@ <!-- Spot check the storefront --> <amOnPage url="{{_defaultProduct.urlKey}}.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> <conditionalClick selector="{{StorefrontSalesRuleCartCouponSection.couponHeader}}" dependentSelector="{{StorefrontSalesRuleCartCouponSection.discountBlockActive}}" visible="false" stepKey="clickCouponHeader"/> <waitForElementVisible selector="{{StorefrontSalesRuleCartCouponSection.couponField}}" stepKey="waitForCouponField" /> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml index bc608c0e06086..ad1ff69a60901 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/CartPriceRuleForConfigurableProductTest.xml @@ -126,15 +126,11 @@ <!-- Add the first product to the cart --> <amOnPage url="$$createConfigChildProduct1.sku$$.html" stepKey="goToProductPage1"/> <waitForPageLoad stepKey="waitForProductPageLoad1"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart1"/> - <waitForPageLoad stepKey="waitForAddToCart1"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Add the second product to the cart --> <amOnPage url="$$createConfigChildProduct2.sku$$.html" stepKey="goToProductPage2"/> <waitForPageLoad stepKey="waitForProductPageLoad2"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> - <waitForPageLoad stepKey="waitForAddToCart2"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"/> <!--View and edit cart--> <actionGroup ref="ClickViewAndEditCartFromMiniCartActionGroup" stepKey="clickViewAndEditCartFromMiniCart"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml index 51e25d3a7e255..eef5dadfbe5d8 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleCountryTest.xml @@ -66,9 +66,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not set country --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml index 420bc37d5c1b2..69097e3269fcb 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRulePostcodeTest.xml @@ -70,9 +70,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not filled in postcode --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml index 279747f87d66d..18057965c28e1 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleQuantityTest.xml @@ -68,9 +68,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have only 1 item in our cart --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> @@ -81,9 +79,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage2"/> <waitForPageLoad stepKey="waitForProductPageLoad2"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity2"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> - <waitForPageLoad stepKey="waitForAddToCart2"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"/> <!-- Now we should see the discount because we have more than 1 item --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage2"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml index a3f32c0781a52..c13b74b6990d0 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleStateTest.xml @@ -66,9 +66,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not filled in postcode --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml index 39ac14315110e..97b75ae772f08 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartPriceRuleSubtotalTest.xml @@ -66,9 +66,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage"/> <waitForPageLoad stepKey="waitForProductPageLoad"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart"/> - <waitForPageLoad stepKey="waitForAddToCart"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage"/> <!-- Should not see the discount yet because we have not exceeded $200 --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage"/> @@ -79,9 +77,7 @@ <amOnPage url="$$createPreReqProduct.name$$.html" stepKey="goToProductPage2"/> <waitForPageLoad stepKey="waitForProductPageLoad2"/> <fillField selector="{{StorefrontProductActionSection.quantity}}" userInput="1" stepKey="fillQuantity2"/> - <click selector="{{StorefrontProductActionSection.addToCart}}" stepKey="addProductToCart2"/> - <waitForPageLoad stepKey="waitForAddToCart2"/> - <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage2"/> + <actionGroup ref="StorefrontClickAddToCartOnProductPageActionGroup" stepKey="addToCartFromStorefrontProductPage2"/> <!-- Now we should see the discount because we exceeded $200 --> <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="goToCartPage2"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml index 9b5f8fbb2912d..1178ca2cfb328 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCartTotalValueWithFullDiscountUsingCartRuleTest.xml @@ -34,8 +34,7 @@ <createData entity="defaultTaxRule" stepKey="initialTaxRule"/> <createData entity="defaultTaxRate" stepKey="initialTaxRate"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> <!-- Add tax rule with 20% tax rate --> @@ -57,20 +56,22 @@ <createData entity="SimpleProduct2" stepKey="createSimpleProductThird"> <field key="price">5.50</field> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Removed created Data --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Delete the tax rate that were created --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> <argument name="name" value="{{SimpleTaxNYRate.state}}-{{SimpleTaxNYRate.rate}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> diff --git a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml index 85a30b3a3a2b4..6b634fa37da2c 100644 --- a/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml +++ b/app/code/Magento/SalesRule/Test/Mftf/Test/StorefrontCategoryRulesShouldApplyToComplexProductsTest.xml @@ -81,7 +81,7 @@ <argument name="actionValue" value="$$createCategory.id$$"/> </actionGroup> <!-- 2: Go to frontend and add an item from both CAT1 and CAT2 to your cart --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="goToFrontend"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToFrontend"/> <!-- 3: Open configurable product 1 and add all his child products to cart --> <amOnPage url="{{StorefrontProductPage.url($$createConfigProductCreateConfigurableProduct1.custom_attributes[url_key]$$)}}" stepKey="amOnConfigurableProductPage"/> <selectOption selector="{{StorefrontProductInfoMainSection.productOptionSelect('$$createConfigProductAttributeCreateConfigurableProduct1.attribute[frontend_labels][0][label]$$')}}" userInput="$$createConfigProductAttributeOption1CreateConfigurableProduct1.option[store_labels][0][label]$$" stepKey="selectOption"/> diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php index b8f879611e51c..bcebeae94be86 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/Quote/DiscountTest.php @@ -228,157 +228,6 @@ public function testCollectItemHasParent() ); } - /** - * @dataProvider collectItemHasChildrenDataProvider - */ - public function testCollectItemHasChildren($childItemData, $parentData, $expectedChildData) - { - $childItems = []; - foreach ($childItemData as $itemId => $itemData) { - $item = $this->objectManager->getObject(Item::class)->setData($itemData); - $childItems[$itemId] = $item; - } - - $itemWithChildren = $this->getMockBuilder(Item::class) - ->disableOriginalConstructor() - ->setMethods( - [ - 'getNoDiscount', - 'getParentItem', - 'getHasChildren', - 'isChildrenCalculated', - 'getChildren', - 'getExtensionAttributes', - ] - ) - ->getMock(); - $itemExtension = $this->getMockBuilder( - ExtensionAttributesInterface::class - )->setMethods(['setDiscounts', 'getDiscounts'])->getMock(); - $itemExtension->method('getDiscounts')->willReturn([]); - $itemExtension->expects($this->any()) - ->method('setDiscounts') - ->willReturn([]); - $itemWithChildren->expects( - $this->any() - )->method('getExtensionAttributes')->willReturn($itemExtension); - $itemWithChildren->expects($this->once())->method('getNoDiscount')->willReturn(false); - $itemWithChildren->expects($this->once())->method('getParentItem')->willReturn(false); - $itemWithChildren->expects($this->once())->method('getHasChildren')->willReturn(true); - $itemWithChildren->expects($this->once())->method('isChildrenCalculated')->willReturn(true); - $itemWithChildren->expects($this->any())->method('getChildren')->willReturn($childItems); - foreach ($parentData as $key => $value) { - $itemWithChildren->setData($key, $value); - } - - $this->validatorMock->expects($this->any())->method('canApplyDiscount')->willReturn(true); - $this->validatorMock->expects($this->once())->method('sortItemsByPriority') - ->with([$itemWithChildren], $this->addressMock) - ->willReturnArgument(0); - $this->validatorMock->expects($this->any())->method('canApplyRules')->willReturn(true); - - $storeMock = $this->getMockBuilder(Store::class) - ->disableOriginalConstructor() - ->setMethods(['getStore']) - ->getMock(); - $this->storeManagerMock->expects($this->any())->method('getStore')->willReturn($storeMock); - - $quoteMock = $this->getMockBuilder(Quote::class) - ->disableOriginalConstructor() - ->getMock(); - $this->addressMock->expects($this->any())->method('getQuote')->willReturn($quoteMock); - $this->addressMock->expects($this->any())->method('getShippingAmount')->willReturn(true); - - $this->shippingAssignmentMock->expects($this->any())->method('getItems')->willReturn([$itemWithChildren]); - $totalMock = $this->createMock(Total::class); - - $this->assertInstanceOf( - Discount::class, - $this->discount->collect($quoteMock, $this->shippingAssignmentMock, $totalMock) - ); - - foreach ($expectedChildData as $itemId => $expectedItemData) { - $childItem = $childItems[$itemId]; - foreach ($expectedItemData as $key => $value) { - $this->assertEquals($value, $childItem->getData($key), 'Incorrect value for ' . $key); - } - } - } - - /** - * @return array - */ - public function collectItemHasChildrenDataProvider() - { - $data = [ - // 3 items, each $100, testing that discount are distributed to item correctly - [ - 'child_item_data' => [ - 'item1' => [ - 'base_row_total' => 0, - ] - ], - 'parent_item_data' => [ - 'discount_amount' => 20, - 'base_discount_amount' => 10, - 'original_discount_amount' => 40, - 'base_original_discount_amount' => 20, - 'base_row_total' => 0, - ], - 'expected_child_item_data' => [ - 'item1' => [ - 'discount_amount' => 0, - 'base_discount_amount' => 0, - 'original_discount_amount' => 0, - 'base_original_discount_amount' => 0, - ] - ], - ], - [ - // 3 items, each $100, testing that discount are distributed to item correctly - 'child_item_data' => [ - 'item1' => [ - 'base_row_total' => 100, - ], - 'item2' => [ - 'base_row_total' => 100, - ], - 'item3' => [ - 'base_row_total' => 100, - ], - ], - 'parent_item_data' => [ - 'discount_amount' => 20, - 'base_discount_amount' => 10, - 'original_discount_amount' => 40, - 'base_original_discount_amount' => 20, - 'base_row_total' => 300, - ], - 'expected_child_item_data' => [ - 'item1' => [ - 'discount_amount' => 6.67, - 'base_discount_amount' => 3.33, - 'original_discount_amount' => 13.33, - 'base_original_discount_amount' => 6.67, - ], - 'item2' => [ - 'discount_amount' => 6.66, - 'base_discount_amount' => 3.34, - 'original_discount_amount' => 13.34, - 'base_original_discount_amount' => 6.66, - ], - 'item3' => [ - 'discount_amount' => 6.67, - 'base_discount_amount' => 3.33, - 'original_discount_amount' => 13.33, - 'base_original_discount_amount' => 6.67, - ], - ], - ], - ]; - return $data; - } - public function testCollectItemHasNoChildren() { $itemWithChildren = $this->getMockBuilder(Item::class) diff --git a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php index 199bcde93bc64..df3b1227e5386 100644 --- a/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php +++ b/app/code/Magento/SalesRule/Test/Unit/Model/RulesApplierTest.php @@ -7,6 +7,7 @@ namespace Magento\SalesRule\Test\Unit\Model; +use Magento\Catalog\Model\Product; use Magento\Framework\Api\ExtensionAttributesInterface; use Magento\Framework\Event\Manager; use Magento\Quote\Model\Quote; @@ -169,6 +170,10 @@ public function testApplyRulesWhenRuleWithStopRulesProcessingIsUsed($isChildren, ->method('validate') ->with($item) ->willReturn(!$isContinue); + $product = $this->createPartialMock(Product::class, []); + $item->expects($this->atLeastOnce()) + ->method('getProduct') + ->willReturn($product); } if (!$isContinue || !$isChildren) { @@ -247,7 +252,7 @@ protected function getPreparedItem() */ $item = $this->getMockBuilder(Item::class) ->addMethods(['setDiscountAmount', 'setBaseDiscountAmount', 'setDiscountPercent', 'setAppliedRuleIds']) - ->onlyMethods(['getAddress', 'getChildren', 'getExtensionAttributes']) + ->onlyMethods(['getAddress', 'getChildren', 'getExtensionAttributes', 'getProduct']) ->disableOriginalConstructor() ->getMock(); $itemExtension = $this->getMockBuilder( diff --git a/app/code/Magento/SalesRule/etc/di.xml b/app/code/Magento/SalesRule/etc/di.xml index c4bc9c3a6decb..05bd801c3b99f 100644 --- a/app/code/Magento/SalesRule/etc/di.xml +++ b/app/code/Magento/SalesRule/etc/di.xml @@ -191,6 +191,8 @@ </type> <type name="Magento\Sales\Model\Service\OrderService"> <plugin name="coupon_uses_decrement_plugin" type="Magento\SalesRule\Plugin\CouponUsagesDecrement" /> + </type> + <type name="\Magento\Quote\Model\QuoteManagement"> <plugin name="coupon_uses_increment_plugin" type="Magento\SalesRule\Plugin\CouponUsagesIncrement" sortOrder="20"/> </type> <preference diff --git a/app/code/Magento/SalesRule/etc/events.xml b/app/code/Magento/SalesRule/etc/events.xml index c55c37de71aac..0c8335b0a6716 100644 --- a/app/code/Magento/SalesRule/etc/events.xml +++ b/app/code/Magento/SalesRule/etc/events.xml @@ -39,4 +39,7 @@ <event name="sales_quote_collect_totals_before"> <observer name="salesrule_sales_quote_collect_totals_before" instance="\Magento\SalesRule\Observer\QuoteResetAppliedRulesObserver" /> </event> + <event name="sales_model_service_quote_submit_failure"> + <observer name="sales_rule_decrement_coupon_usage_quote_submit_failure" instance="\Magento\SalesRule\Observer\CouponUsagesDecrement" /> + </event> </config> diff --git a/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml b/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml index 64918c24cdc61..5a81d2fd94e14 100644 --- a/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml +++ b/app/code/Magento/SalesRule/view/adminhtml/templates/promo/salesrulejs.phtml @@ -3,8 +3,10 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script require([ 'jquery', "uiRegistry", @@ -41,15 +43,15 @@ function generateCouponCodes(idPrefix, generateUrl, grid) { var elements = $(idPrefix + 'information_fieldset').select('input', 'select', 'textarea'); elements = elements.concat( - $$('#rule_uses_per_coupon'), - $$('#rule_uses_per_customer'), - $$('#rule_to_date') + \$$('#rule_uses_per_coupon'), + \$$('#rule_uses_per_customer'), + \$$('#rule_to_date') ); var params = Form.serializeElements(elements, true); params.form_key = FORM_KEY; - if ($$('#'+idPrefix + 'information_fieldset .messages')) { - $$('#'+idPrefix + 'information_fieldset .messages')[0].update(); + if (\$$('#'+idPrefix + 'information_fieldset .messages')) { + \$$('#'+idPrefix + 'information_fieldset .messages')[0].update(); } if ($('messages')) { $('messages').update(); @@ -71,8 +73,8 @@ function generateCouponCodes(idPrefix, generateUrl, grid) { couponCodesGrid.reload(); } if (response && response.messages) { - if ($$('#'+idPrefix + 'information_fieldset .messages')) { - $$('#'+idPrefix + 'information_fieldset .messages')[0].update(response.messages); + if (\$$('#'+idPrefix + 'information_fieldset .messages')) { + \$$('#'+idPrefix + 'information_fieldset .messages')[0].update(response.messages); } else if ($('messages')) { $('messages').update(response.messages); } @@ -94,4 +96,6 @@ window.validateCouponGenerate = validateCouponGenerate; window.generateCouponCodes = generateCouponCodes; window.refreshCouponCodesGrid = refreshCouponCodesGrid; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js index 13b701c6fe65a..484020a573f07 100644 --- a/app/code/Magento/SalesRule/view/frontend/requirejs-config.js +++ b/app/code/Magento/SalesRule/view/frontend/requirejs-config.js @@ -8,6 +8,12 @@ var config = { mixins: { 'Magento_Checkout/js/action/select-payment-method': { 'Magento_SalesRule/js/action/select-payment-method-mixin': true + }, + 'Magento_Checkout/js/model/shipping-save-processor': { + 'Magento_SalesRule/js/model/shipping-save-processor-mixin': true + }, + 'Magento_Checkout/js/action/place-order': { + 'Magento_SalesRule/js/model/place-order-mixin': true } } } diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/model/place-order-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/model/place-order-mixin.js new file mode 100644 index 0000000000000..da4de3fa19c5e --- /dev/null +++ b/app/code/Magento/SalesRule/view/frontend/web/js/model/place-order-mixin.js @@ -0,0 +1,42 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'jquery', + 'mage/utils/wrapper', + 'Magento_Checkout/js/model/quote', + 'Magento_SalesRule/js/model/coupon', + 'Magento_Checkout/js/action/get-totals' +], function ($, wrapper, quote, coupon, getTotalsAction) { + 'use strict'; + + return function (placeOrderAction) { + return wrapper.wrap(placeOrderAction, function (originalAction, paymentData, messageContainer) { + var result; + + $.when( + result = originalAction(paymentData, messageContainer) + ).fail( + function () { + var deferred = $.Deferred(), + + /** + * Update coupon form + */ + updateCouponCallback = function () { + if (quote.totals() && !quote.totals()['coupon_code']) { + coupon.setCouponCode(''); + coupon.setIsApplied(false); + } + }; + + getTotalsAction([], deferred); + $.when(deferred).done(updateCouponCallback); + } + ); + + return result; + }); + }; +}); diff --git a/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js new file mode 100644 index 0000000000000..193acb8eed2f4 --- /dev/null +++ b/app/code/Magento/SalesRule/view/frontend/web/js/model/shipping-save-processor-mixin.js @@ -0,0 +1,34 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +define([ + 'mage/utils/wrapper', + 'Magento_Checkout/js/model/quote', + 'Magento_SalesRule/js/model/coupon' +], function (wrapper, quote, coupon) { + 'use strict'; + + return function (shippingSaveProcessor) { + shippingSaveProcessor.saveShippingInformation = wrapper.wrapSuper( + shippingSaveProcessor.saveShippingInformation, + function (type) { + var updateCouponCallback; + + /** + * Update coupon form + */ + updateCouponCallback = function () { + if (quote.totals() && !quote.totals()['coupon_code']) { + coupon.setCouponCode(''); + coupon.setIsApplied(false); + } + }; + + return this._super(type).done(updateCouponCallback); + } + ); + + return shippingSaveProcessor; + }; +}); diff --git a/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php b/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php index e86cc8b1b2e6d..d0c516b47577b 100644 --- a/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php +++ b/app/code/Magento/SalesSequence/Model/Sequence/DeleteByStore.php @@ -58,7 +58,7 @@ public function execute($storeId): void $metadataIds = $this->getMetadataIdsByStoreId($storeId); $profileIds = $this->getProfileIdsByMetadataIds($metadataIds); - $this->appResource->getConnection()->delete( + $this->appResource->getConnection('sales')->delete( $this->appResource->getTableName('sales_sequence_profile'), ['profile_id IN (?)' => $profileIds] ); @@ -70,7 +70,7 @@ public function execute($storeId): void continue; } - $this->appResource->getConnection()->dropTable( + $this->appResource->getConnection('sales')->dropTable( $metadata->getSequenceTable() ); $this->resourceMetadata->delete($metadata); @@ -85,7 +85,7 @@ public function execute($storeId): void */ private function getMetadataIdsByStoreId($storeId) { - $connection = $this->appResource->getConnection(); + $connection = $this->appResource->getConnection('sales'); $bind = ['store_id' => $storeId]; $select = $connection->select()->from( $this->appResource->getTableName('sales_sequence_meta'), @@ -105,7 +105,7 @@ private function getMetadataIdsByStoreId($storeId) */ private function getProfileIdsByMetadataIds(array $metadataIds) { - $connection = $this->appResource->getConnection(); + $connection = $this->appResource->getConnection('sales'); $select = $connection->select() ->from( $this->appResource->getTableName('sales_sequence_profile'), diff --git a/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php b/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php index 57093c8851c89..29d60ef7e6aa5 100644 --- a/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php +++ b/app/code/Magento/SalesSequence/Test/Unit/Model/Sequence/DeleteByStoreTest.php @@ -110,6 +110,7 @@ static function ($tableName) { } ); $this->resourceMock->method('getConnection') + ->with('sales') ->willReturn($this->connectionMock); $this->connectionMock ->method('select') diff --git a/app/code/Magento/SampleData/README.md b/app/code/Magento/SampleData/README.md index c71439b929013..e0666ba73fe24 100644 --- a/app/code/Magento/SampleData/README.md +++ b/app/code/Magento/SampleData/README.md @@ -11,7 +11,7 @@ You can deploy sample data from one of the following sources: * From the Magento composer repository, optionally using Magento CLI * From the Magento GitHub repository -If your Magento code base was cloned from the `master` branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch. +If your Magento code base was cloned from the mainline branch, you can use either source of the sample data. If it was cloned from the `develop` branch, use the GitHub repository and choose to get sample data modules from the `develop` branch. ### Deploy Sample Data from Composer Repository @@ -46,7 +46,7 @@ Each package corresponds to a sample data module. The complete list of available To deploy sample data from the GitHub repository: -1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the `master` branch, choose the `master` branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`. +1. Clone sample data from `https://github.com/magento/magento2-sample-data`. If your Magento instance was cloned from the mainline branch, choose the mainline branch when cloning sample data; choose the `develop` branch if Magento was cloned from `develop`. 2. Link the sample data and your Magento instance by running: `# php -f <sample-data_clone_dir>/dev/tools/build-sample-data.php -- --ce-source="<path_to_your_magento_instance>"` ## Install Sample Data diff --git a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php index a73edcce99760..3fa8fa9d417f3 100644 --- a/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php +++ b/app/code/Magento/Search/Block/Adminhtml/Synonyms/Edit/DeleteButton.php @@ -8,11 +8,13 @@ use Magento\Framework\View\Element\UiComponent\Control\ButtonProviderInterface; /** - * Class DeleteButton + * Delete Synonyms Group Button Class */ class DeleteButton extends GenericButton implements ButtonProviderInterface { /** + * Delete Button Data + * * @return array */ public function getButtonData() @@ -24,7 +26,7 @@ public function getButtonData() 'class' => 'delete', 'on_click' => 'deleteConfirm(\'' . __('Are you sure you want to delete this synonym group?') - . '\', \'' . $this->getDeleteUrl() . '\')', + . '\', \'' . $this->getDeleteUrl() . '\', {data: {}})', 'sort_order' => 20, ]; } @@ -32,6 +34,8 @@ public function getButtonData() } /** + * Delete Url + * * @return string */ public function getDeleteUrl() diff --git a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php index 9d8b612cefadf..06d15d4d7124e 100644 --- a/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php +++ b/app/code/Magento/Search/Controller/Adminhtml/Synonyms/Delete.php @@ -6,10 +6,12 @@ namespace Magento\Search\Controller\Adminhtml\Synonyms; +use Magento\Framework\App\Action\HttpPostActionInterface; + /** * Delete Controller */ -class Delete extends \Magento\Backend\App\Action +class Delete extends \Magento\Backend\App\Action implements HttpPostActionInterface { /** * Authorization level of a basic admin session diff --git a/app/code/Magento/Search/Model/AdapterFactory.php b/app/code/Magento/Search/Model/AdapterFactory.php index 917603ce57dc3..f6d2013bd4886 100644 --- a/app/code/Magento/Search/Model/AdapterFactory.php +++ b/app/code/Magento/Search/Model/AdapterFactory.php @@ -17,7 +17,7 @@ class AdapterFactory * Scope configuration * * @var \Magento\Framework\App\Config\ScopeConfigInterface - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $scopeConfig; @@ -32,13 +32,13 @@ class AdapterFactory * Config path * * @var string - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $path; /** * Config Scope - * @deprecated since it is not used anymore + * @deprecated 101.0.0 since it is not used anymore */ protected $scope; diff --git a/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php b/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php index 2fc71fc6a6d73..8d3db36e35dec 100644 --- a/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php +++ b/app/code/Magento/Search/Model/ResourceModel/Query/Collection.php @@ -168,6 +168,7 @@ public function setPopularQueryFilter($storeIds = null) * @param int $storeId * @param int $maxCountCacheableSearchTerms * @return bool + * @since 101.1.0 */ public function isTopSearchResult(string $term, int $storeId, int $maxCountCacheableSearchTerms):bool { diff --git a/app/code/Magento/Search/Model/Search/PageSizeProvider.php b/app/code/Magento/Search/Model/Search/PageSizeProvider.php index 5572bac6addc3..ae2a8ca954d63 100644 --- a/app/code/Magento/Search/Model/Search/PageSizeProvider.php +++ b/app/code/Magento/Search/Model/Search/PageSizeProvider.php @@ -10,6 +10,7 @@ /** * Returns max page size by search engine name * @api + * @since 101.0.0 */ class PageSizeProvider { @@ -39,6 +40,7 @@ public function __construct( * Returns max_page_size depends on engine * * @return integer + * @since 101.0.0 */ public function getMaxPageSize() : int { diff --git a/app/code/Magento/Search/Model/SearchEngine/Validator.php b/app/code/Magento/Search/Model/SearchEngine/Validator.php index f4fc8a9a62e0e..264e7c69dd520 100644 --- a/app/code/Magento/Search/Model/SearchEngine/Validator.php +++ b/app/code/Magento/Search/Model/SearchEngine/Validator.php @@ -22,7 +22,7 @@ class Validator implements ValidatorInterface /** * @var array */ - private $engineBlacklist = ['mysql' => 'MySQL']; + private $excludedEngineList = ['mysql' => 'MySQL']; /** * @var ValidatorInterface[] @@ -32,16 +32,16 @@ class Validator implements ValidatorInterface /** * @param ScopeConfigInterface $scopeConfig * @param array $engineValidators - * @param array $engineBlacklist + * @param array $excludedEngineList */ public function __construct( ScopeConfigInterface $scopeConfig, array $engineValidators = [], - array $engineBlacklist = [] + array $excludedEngineList = [] ) { $this->scopeConfig = $scopeConfig; $this->engineValidators = $engineValidators; - $this->engineBlacklist = array_merge($this->engineBlacklist, $engineBlacklist); + $this->excludedEngineList = array_merge($this->excludedEngineList, $excludedEngineList); } /** @@ -51,9 +51,9 @@ public function validate(): array { $errors = []; $currentEngine = $this->scopeConfig->getValue('catalog/search/engine'); - if (isset($this->engineBlacklist[$currentEngine])) { - $blacklistedEngine = $this->engineBlacklist[$currentEngine]; - $errors[] = "Your current search engine, '{$blacklistedEngine}', is not supported." + if (isset($this->excludedEngineList[$currentEngine])) { + $excludedEngine = $this->excludedEngineList[$currentEngine]; + $errors[] = "Your current search engine, '{$excludedEngine}', is not supported." . " You must install a supported search engine before upgrading." . " See the System Upgrade Guide for more information."; } diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminFillNewSearchSynonymsActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminFillNewSearchSynonymsActionGroup.xml new file mode 100644 index 0000000000000..ae4128e4f5d9a --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminFillNewSearchSynonymsActionGroup.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminFillNewSearchSynonymsActionGroup"> + <annotations> + <description>Fills the search synonyms form field.</description> + </annotations> + <arguments> + <argument name="scope_id" type="string"/> + <argument name="synonyms" type="string"/> + </arguments> + + <selectOption selector="{{AdminSearchSynonymsNewSection.scope}}" userInput="{{scope_id}}" stepKey="selectScope"/> + <fillField selector="{{AdminSearchSynonymsNewSection.synonyms}}" userInput="{{synonyms}}" stepKey="fillSynonyms"/> + <checkOption selector="{{AdminSearchSynonymsNewSection.merge}}" stepKey="checkCheckbox"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminNavigateToNewSearchSynonymsPageActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminNavigateToNewSearchSynonymsPageActionGroup.xml new file mode 100644 index 0000000000000..6c14aba36a139 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/AdminNavigateToNewSearchSynonymsPageActionGroup.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminNavigateToNewSearchSynonymsPageActionGroup"> + <click stepKey="clickNewSynonymsGroupButton" selector="{{AdminSearchSynonymsGridSection.add}}"/> + <waitForPageLoad stepKey="waitForNewSearchSynonymsPageLoaded"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontQuickSearchActionGroup.xml b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontQuickSearchActionGroup.xml index aec874e7b6d85..840d8439e3d63 100644 --- a/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontQuickSearchActionGroup.xml +++ b/app/code/Magento/Search/Test/Mftf/ActionGroup/StoreFrontQuickSearchActionGroup.xml @@ -16,5 +16,6 @@ <fillField stepKey="fillSearchField" selector="{{StorefrontQuickSearchSection.searchPhrase}}" userInput="{{query}}"/> <waitForElementVisible selector="{{StorefrontQuickSearchSection.searchButton}}" stepKey="waitForSubmitButton"/> <click stepKey="clickSearchButton" selector="{{StorefrontQuickSearchSection.searchButton}}"/> + <waitForPageLoad stepKey="waitForSearchResults"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Search/Test/Mftf/Data/SearchSynonymsData.xml b/app/code/Magento/Search/Test/Mftf/Data/SearchSynonymsData.xml new file mode 100644 index 0000000000000..e8242b5694739 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Data/SearchSynonymsData.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<entities xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataProfileSchema.xsd"> + <entity name="AdminSearchSynonyms" type="SearchSynonyms"> + <data key="pageTitle">Search Synonyms</data> + <data key="title">Search Synonyms</data> + <data key="dataUiId">magento-search-search-synonyms</data> + </entity> +</entities> diff --git a/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsGridSection.xml b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsGridSection.xml new file mode 100644 index 0000000000000..fe97fe7a5663d --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsGridSection.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="AdminSearchSynonymsGridSection"> + <element name="add" type="button" selector=".page-actions-buttons #add"/> + </section> +</sections> diff --git a/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsNewSection.xml b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsNewSection.xml new file mode 100644 index 0000000000000..73a39a25325f7 --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Section/AdminSearchSynonymsNewSection.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> + <section name="AdminSearchSynonymsNewSection"> + <element name="save" type="button" selector="//button[@id='save']"/> + <element name="resetButton" type="button" selector="//button[@id='reset']"/> + <element name="scope" type="select" selector="//select[@name='scope_id']"/> + <element name="synonyms" type="textarea" selector="//textarea[@name='synonyms']"/> + <element name="merge" type="checkbox" selector="//input[@name='mergeOnConflict']"/> + </section> +</sections> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml index 82ec95b24d3ca..189b8962957a2 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminGlobalSearchOnProductPageTest.xml @@ -38,8 +38,7 @@ </after> <!-- Create Simple Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="adminProductIndexPageAdd"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="adminProductIndexPageAdd"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="SimpleProduct"/> </actionGroup> diff --git a/app/code/Magento/Search/Test/Mftf/Test/AdminNewSearchSynonymsFormResetTest.xml b/app/code/Magento/Search/Test/Mftf/Test/AdminNewSearchSynonymsFormResetTest.xml new file mode 100644 index 0000000000000..24a5bf16704ff --- /dev/null +++ b/app/code/Magento/Search/Test/Mftf/Test/AdminNewSearchSynonymsFormResetTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="AdminNewSearchSynonymsFormResetTest"> + <annotations> + <features value="Search"/> + <stories value="Reset new search synonyms group form"/> + <title value="Admin reset new search synonyms group form"/> + <description value="When admin users reset button on new search synonyms form all fields should be set to default values"/> + <testCaseId value="MC-36382"/> + <severity value="AVERAGE"/> + <group value="Search"/> + </annotations> + <before> + <actionGroup ref="AdminLoginActionGroup" stepKey="LoginAsAdmin"/> + </before> + <after> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> + </after> + + <actionGroup ref="AdminNavigateMenuActionGroup" stepKey="navigateToSearchSynonymsPage"> + <argument name="menuUiId" value="{{AdminMenuMarketing.dataUiId}}"/> + <argument name="submenuUiId" value="{{AdminSearchSynonyms.dataUiId}}"/> + </actionGroup> + + <actionGroup ref="AdminNavigateToNewSearchSynonymsPageActionGroup" stepKey="navigateToNewSearchSynonymsPage"/> + + <actionGroup ref="AdminFillNewSearchSynonymsActionGroup" stepKey="fillNewSearchSynonyms"> + <argument name="scope_id" value="1:1"/> + <argument name="synonyms" value="Test Synonyms"/> + </actionGroup> + + <click selector="{{AdminSearchSynonymsNewSection.resetButton}}" stepKey="clickResetButton"/> + + <grabValueFrom selector="{{AdminSearchSynonymsNewSection.scope}}" stepKey="grabScopeValue"/> + <assertEquals stepKey="assertScopeDefaultValue"> + <expectedResult type="string">0:0</expectedResult> + <actualResult type="string">$grabScopeValue</actualResult> + </assertEquals> + + <grabValueFrom selector="{{AdminSearchSynonymsNewSection.synonyms}}" stepKey="grabSynonymsValue"/> + <assertEmpty stepKey="assertSynonymsDefaultValue"> + <actualResult type="string">$grabSynonymsValue</actualResult> + </assertEmpty> + + <grabValueFrom selector="{{AdminSearchSynonymsNewSection.merge}}" stepKey="grabMergeValue"/> + <assertEquals stepKey="assertMergeDefaultValue"> + <expectedResult type="string">false</expectedResult> + <actualResult type="string">$grabMergeValue</actualResult> + </assertEquals> + </test> +</tests> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml index 18f623288621d..b082014c7b120 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontUsingElasticSearchWithWeightAttributeTest.xml @@ -49,7 +49,9 @@ <!-- Step 3 --> <magentoCron groups="index" stepKey="reindexInvalidatedIndices"/> <!-- Step 4 --> - <magentoCLI command="cache:clean" arguments="full_page" stepKey="clearFPC"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="clearFPC"> + <argument name="tags" value="full_page"/> + </actionGroup> <!-- Step 5 --> <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="goToStorefront"/> <!-- Step 6 --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml index 3bfa777ac27d8..22fcbfc2920ff 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductDescriptionTest.xml @@ -24,8 +24,12 @@ <createData entity="SimpleProductWithDescription" stepKey="simpleProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete created product --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml index 93a3c8ca8e4a2..0b02b49433dda 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductNameTest.xml @@ -26,8 +26,12 @@ <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete create product --> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml index ebe3b6c129721..d88bb023c60b2 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductShortDescriptionTest.xml @@ -26,8 +26,12 @@ <createData entity="ApiProductWithDescription" stepKey="product"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml index e72f614593cfe..4c586d18fd3cf 100644 --- a/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml +++ b/app/code/Magento/Search/Test/Mftf/Test/StorefrontVerifySearchSuggestionByProductSkuTest.xml @@ -26,8 +26,12 @@ <createData entity="defaultSimpleProduct" stepKey="simpleProduct"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php index c91c0fce9dd47..cc272ccb60162 100644 --- a/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php +++ b/app/code/Magento/Search/Test/Unit/Model/SearchEngine/ValidatorTest.php @@ -34,7 +34,7 @@ protected function setUp(): void [ 'scopeConfig' => $this->scopeConfigMock, 'engineValidators' => ['otherEngine' => $this->otherEngineValidatorMock], - 'engineBlacklist' => ['badEngine' => 'Bad Engine'] + 'excludedEngineList' => ['badEngine' => 'Bad Engine'] ] ); } @@ -54,7 +54,7 @@ public function testValidateValid() $this->assertEquals($expectedErrors, $this->validator->validate()); } - public function testValidateBlacklist() + public function testValidateExcludedList() { $this->scopeConfigMock ->expects($this->once()) diff --git a/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php b/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php index 4613ef3d45ede..729a8a7e737fd 100644 --- a/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php +++ b/app/code/Magento/Search/Test/Unit/Ui/Component/Listing/Column/SynonymActionsTest.php @@ -112,6 +112,7 @@ public function testPrepareDataSourceWithItems() self::STUB_SYNONYM_GROUP_ID ) ], + 'post' => true ], 'edit' => [ 'href' => sprintf( diff --git a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php index 2fd569642375e..191726bd2689b 100644 --- a/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php +++ b/app/code/Magento/Search/Ui/Component/Listing/Column/SynonymActions.php @@ -64,6 +64,7 @@ public function prepareDataSource(array $dataSource) 'title' => __('Delete'), 'message' => __('Are you sure you want to delete synonym group with id: %1?', $item['group_id']) ], + 'post' => true ]; $item[$name]['edit'] = [ 'href' => $this->urlBuilder->getUrl(self::SYNONYM_URL_PATH_EDIT, ['group_id' => $item['group_id']]), diff --git a/app/code/Magento/Search/ViewModel/ConfigProvider.php b/app/code/Magento/Search/ViewModel/ConfigProvider.php new file mode 100644 index 0000000000000..be3366e62e965 --- /dev/null +++ b/app/code/Magento/Search/ViewModel/ConfigProvider.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\ViewModel; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\View\Element\Block\ArgumentInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * View model for search + */ +class ConfigProvider implements ArgumentInterface +{ + /** + * Suggestions settings config paths + */ + private const SEARCH_SUGGESTION_ENABLED = 'catalog/search/search_suggestion_enabled'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Is Search Suggestions Allowed + * + * @return bool + */ + public function isSuggestionsAllowed(): bool + { + return $this->scopeConfig->isSetFlag( + self::SEARCH_SUGGESTION_ENABLED, + ScopeInterface::SCOPE_STORE + ); + } +} diff --git a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml index b084a0ad16aaa..cf2b0704dc15b 100644 --- a/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml +++ b/app/code/Magento/Search/view/adminhtml/ui_component/search_synonyms_form.xml @@ -63,14 +63,14 @@ <argument name="data" xsi:type="array"> <item name="config" xsi:type="array"> <item name="source" xsi:type="string">block</item> - <item name="default" xsi:type="number">0</item> + <item name="default" xsi:type="string">0:0</item> </item> </argument> <settings> <validation> <rule name="required-entry" xsi:type="boolean">true</rule> </validation> - <dataType>int</dataType> + <dataType>text</dataType> <tooltip> <link>https://docs.magento.com/m2/ce/user_guide/stores/websites-stores-views.html</link> <description translate="true">You can adjust the scope of this synonym group by selecting an option from the list.</description> diff --git a/app/code/Magento/Search/view/frontend/layout/default.xml b/app/code/Magento/Search/view/frontend/layout/default.xml index 0cb18adedd952..69c99f979d51b 100644 --- a/app/code/Magento/Search/view/frontend/layout/default.xml +++ b/app/code/Magento/Search/view/frontend/layout/default.xml @@ -8,7 +8,11 @@ <page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> <body> <referenceContainer name="header-wrapper"> - <block class="Magento\Framework\View\Element\Template" name="top.search" as="topSearch" template="Magento_Search::form.mini.phtml" /> + <block class="Magento\Framework\View\Element\Template" name="top.search" as="topSearch" template="Magento_Search::form.mini.phtml"> + <arguments> + <argument name="configProvider" xsi:type="object">Magento\Search\ViewModel\ConfigProvider</argument> + </arguments> + </block> </referenceContainer> <referenceBlock name="footer_links"> <block class="Magento\Framework\View\Element\Html\Link\Current" ifconfig="catalog/seo/search_terms" name="search-term-popular-link"> diff --git a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml index 35f3876599731..80e720e2c2fe2 100644 --- a/app/code/Magento/Search/view/frontend/templates/form.mini.phtml +++ b/app/code/Magento/Search/view/frontend/templates/form.mini.phtml @@ -9,7 +9,9 @@ <?php /** @var $block \Magento\Framework\View\Element\Template */ /** @var $helper \Magento\Search\Helper\Data */ +/** @var $configProvider \Magento\Search\ViewModel\ConfigProvider */ $helper = $this->helper(\Magento\Search\Helper\Data::class); +$configProvider = $block->getData('configProvider'); ?> <div class="block block-search"> <div class="block block-title"><strong><?= $block->escapeHtml(__('Search')) ?></strong></div> @@ -22,12 +24,14 @@ $helper = $this->helper(\Magento\Search\Helper\Data::class); </label> <div class="control"> <input id="search" - data-mage-init='{"quickSearch":{ - "formSelector":"#search_mini_form", - "url":"<?= $block->escapeUrl($helper->getSuggestUrl())?>", - "destinationSelector":"#search_autocomplete", - "minSearchLength":"<?= $block->escapeHtml($helper->getMinQueryLength()) ?>"} - }' + <?php if ($configProvider->isSuggestionsAllowed()):?> + data-mage-init='{"quickSearch":{ + "formSelector":"#search_mini_form", + "url":"<?= $block->escapeUrl($helper->getSuggestUrl())?>", + "destinationSelector":"#search_autocomplete", + "minSearchLength":"<?= $block->escapeHtml($helper->getMinQueryLength()) ?>"} + }' + <?php endif;?> type="text" name="<?= $block->escapeHtmlAttr($helper->getQueryParamName()) ?>" value="<?= /* @noEscape */ $helper->getEscapedQueryText() ?>" diff --git a/app/code/Magento/Search/view/frontend/templates/term.phtml b/app/code/Magento/Search/view/frontend/templates/term.phtml index b06ebcfe66966..51f40e8247ccf 100644 --- a/app/code/Magento/Search/view/frontend/templates/term.phtml +++ b/app/code/Magento/Search/view/frontend/templates/term.phtml @@ -3,19 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** + * @var \Magento\Search\Block\Term $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php if (count($block->getTerms()) > 0) : ?> +<?php if (count($block->getTerms()) > 0): ?> <ul class="search-terms"> - <?php foreach ($block->getTerms() as $_term) : ?> - <li class="item"> - <a href="<?= $block->escapeUrl($block->getSearchUrl($_term)) ?>" - style="font-size:<?= /* @noEscape */ $_term->getRatio()*70+75 ?>%;"> + <?php foreach ($block->getTerms() as $_term): ?> + <li id="term-<?= /* @noEscape */ $_term->getId() ?>" class="item"> + <a href="<?= $block->escapeUrl($block->getSearchUrl($_term)) ?>"> <?= $block->escapeHtml($_term->getQueryText()) ?> </a> </li> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size:" . ($_term->getRatio()*70+75) . "%;", + 'li#term-' . $_term->getId() + ) ?> <?php endforeach; ?> </ul> -<?php else : ?> +<?php else: ?> <div class="message notice"> <div><?= $block->escapeHtml(__('There are no search terms available.')) ?></div> </div> diff --git a/app/code/Magento/Security/Model/Plugin/Auth.php b/app/code/Magento/Security/Model/Plugin/Auth.php index 833b4e4c1b774..b388ef6115867 100644 --- a/app/code/Magento/Security/Model/Plugin/Auth.php +++ b/app/code/Magento/Security/Model/Plugin/Auth.php @@ -35,6 +35,8 @@ public function __construct( } /** + * Add warning message if other sessions terminated + * * @param \Magento\Backend\Model\Auth $authModel * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) @@ -43,11 +45,13 @@ public function afterLogin(\Magento\Backend\Model\Auth $authModel) { $this->sessionsManager->processLogin(); if ($this->sessionsManager->getCurrentSession()->isOtherSessionsTerminated()) { - $this->messageManager->addWarning(__('All other open sessions for this account were terminated.')); + $this->messageManager->addWarningMessage(__('All other open sessions for this account were terminated.')); } } /** + * Handle logout process + * * @param \Magento\Backend\Model\Auth $authModel * @return void * @SuppressWarnings(PHPMD.UnusedFormalParameter) diff --git a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php index c431f1ecda332..dd86b3b574ead 100644 --- a/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php +++ b/app/code/Magento/Security/Test/Unit/Model/Plugin/AuthTest.php @@ -64,7 +64,7 @@ protected function setUp(): void $this->messageManager = $this->getMockForAbstractClass( ManagerInterface::class, - ['addWarning'], + ['addWarningMessage'], '', false ); @@ -100,7 +100,7 @@ public function testAfterLogin() ->method('isOtherSessionsTerminated') ->willReturn(true); $this->messageManager->expects($this->once()) - ->method('addWarning') + ->method('addWarningMessage') ->with($warningMessage); $this->model->afterLogin($this->authMock); diff --git a/app/code/Magento/Security/view/base/requirejs-config.js b/app/code/Magento/Security/view/base/requirejs-config.js new file mode 100644 index 0000000000000..579980336f2cb --- /dev/null +++ b/app/code/Magento/Security/view/base/requirejs-config.js @@ -0,0 +1,12 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + escaper: 'Magento_Security/js/escaper' + } + } +}; diff --git a/app/code/Magento/Security/view/base/web/js/escaper.js b/app/code/Magento/Security/view/base/web/js/escaper.js new file mode 100644 index 0000000000000..dc1c896ad6836 --- /dev/null +++ b/app/code/Magento/Security/view/base/web/js/escaper.js @@ -0,0 +1,174 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** + * A loose JavaScript version of Magento\Framework\Escaper + * + * Due to differences in how XML/HTML is processed in PHP vs JS there are a couple of minor differences in behavior + * from the PHP counterpart. + * + * The first difference is that the default invocation of escapeHtml without allowedTags will double-escape existing + * entities as the intention of such an invocation is that the input isn't supposed to contain any HTML. + * + * The second difference is that escapeHtml will not escape quotes. Since the input is actually being processed by the + * DOM there is no chance of quotes being mixed with HTML syntax. And, since escapeHtml is not + * intended to be used with raw injection into a HTML attribute, this is acceptable. + * + * @api + */ +define([], function () { + 'use strict'; + + return { + neverAllowedElements: ['script', 'img', 'embed', 'iframe', 'video', 'source', 'object', 'audio'], + generallyAllowedAttributes: ['id', 'class', 'href', 'title', 'style'], + forbiddenAttributesByElement: { + a: ['style'] + }, + + /** + * Escape a string for safe injection into HTML + * + * @param {String} data + * @param {Array|null} allowedTags + * @returns {String} + */ + escapeHtml: function (data, allowedTags) { + var domParser = new DOMParser(), + fragment = domParser.parseFromString('<div></div>', 'text/html'); + + fragment = fragment.body.childNodes[0]; + allowedTags = typeof allowedTags === 'object' && allowedTags.length ? allowedTags : null; + + if (allowedTags) { + fragment.innerHTML = data || ''; + allowedTags = this._filterProhibitedTags(allowedTags); + + this._removeComments(fragment); + this._removeNotAllowedElements(fragment, allowedTags); + this._removeNotAllowedAttributes(fragment); + + return fragment.innerHTML; + } + + fragment.textContent = data || ''; + + return fragment.innerHTML; + }, + + /** + * Remove the always forbidden tags from a list of provided tags + * + * @param {Array} tags + * @returns {Array} + * @private + */ + _filterProhibitedTags: function (tags) { + return tags.filter(function (n) { + return this.neverAllowedElements.indexOf(n) === -1; + }.bind(this)); + }, + + /** + * Remove comment nodes from the given node + * + * @param {Node} node + * @private + */ + _removeComments: function (node) { + var treeWalker = node.ownerDocument.createTreeWalker( + node, + NodeFilter.SHOW_COMMENT, + function () { + return NodeFilter.FILTER_ACCEPT; + }, + false + ), + nodesToRemove = []; + + while (treeWalker.nextNode()) { + nodesToRemove.push(treeWalker.currentNode); + } + + nodesToRemove.forEach(function (nodeToRemove) { + nodeToRemove.parentNode.removeChild(nodeToRemove); + }); + }, + + /** + * Strip the given node of all disallowed tags while permitting any nested text nodes + * + * @param {Node} node + * @param {Array|null} allowedTags + * @private + */ + _removeNotAllowedElements: function (node, allowedTags) { + var treeWalker = node.ownerDocument.createTreeWalker( + node, + NodeFilter.SHOW_ELEMENT, + function (currentNode) { + return allowedTags.indexOf(currentNode.nodeName.toLowerCase()) === -1 ? + NodeFilter.FILTER_ACCEPT + // SKIP instead of REJECT because REJECT also rejects child nodes + : NodeFilter.FILTER_SKIP; + }, + false + ), + nodesToRemove = []; + + while (treeWalker.nextNode()) { + if (allowedTags.indexOf(treeWalker.currentNode.nodeName.toLowerCase()) === -1) { + nodesToRemove.push(treeWalker.currentNode); + } + } + + nodesToRemove.forEach(function (nodeToRemove) { + nodeToRemove.parentNode.replaceChild( + node.ownerDocument.createTextNode(nodeToRemove.textContent), + nodeToRemove + ); + }); + }, + + /** + * Remove any invalid attributes from the given node + * + * @param {Node} node + * @private + */ + _removeNotAllowedAttributes: function (node) { + var treeWalker = node.ownerDocument.createTreeWalker( + node, + NodeFilter.SHOW_ELEMENT, + function () { + return NodeFilter.FILTER_ACCEPT; + }, + false + ), + i, + attribute, + nodeName, + attributesToRemove = []; + + while (treeWalker.nextNode()) { + for (i = 0; i < treeWalker.currentNode.attributes.length; i++) { + attribute = treeWalker.currentNode.attributes[i]; + nodeName = treeWalker.currentNode.nodeName.toLowerCase(); + + if (this.generallyAllowedAttributes.indexOf(attribute.name) === -1 || // eslint-disable-line max-depth,max-len + this.forbiddenAttributesByElement[nodeName] && + this.forbiddenAttributesByElement[nodeName].indexOf(attribute.name) !== -1 + ) { + attributesToRemove.push(attribute); + } + } + } + + attributesToRemove.forEach(function (attributeToRemove) { + attributeToRemove.ownerElement.removeAttribute(attributeToRemove.name); + }); + } + }; +}); diff --git a/app/code/Magento/SendFriend/Block/Send.php b/app/code/Magento/SendFriend/Block/Send.php index 1c4b550361359..6f2154ba29f47 100644 --- a/app/code/Magento/SendFriend/Block/Send.php +++ b/app/code/Magento/SendFriend/Block/Send.php @@ -228,6 +228,7 @@ public function canSend() /** * @inheritdoc + * @since 100.3.1 */ protected function _prepareLayout() { diff --git a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml index eb9318271c1d8..b1e3da8612f78 100644 --- a/app/code/Magento/SendFriend/view/frontend/templates/send.phtml +++ b/app/code/Magento/SendFriend/view/frontend/templates/send.phtml @@ -7,7 +7,10 @@ /** * Send to friend form */ -/** @var \Magento\SendFriend\Block\Send $block */ +/** + * @var \Magento\SendFriend\Block\Send $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <script id="add-recipient-tmpl" type="text/x-magento-template"> @@ -21,15 +24,20 @@ </div> <fieldset class="fieldset"> <div class="field name required"> - <label for="recipients-name<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Name')) ?></span></label> + <label for="recipients-name<%- data._index_ %>" class="label"> + <span><?= $block->escapeHtml(__('Name')) ?></span> + </label> <div class="control"> - <input name="recipients[name][<%- data._index_ %>]" type="text" title="<?= $block->escapeHtmlAttr(__('Name')) ?>" class="input-text" + <input name="recipients[name][<%- data._index_ %>]" type="text" + title="<?= $block->escapeHtmlAttr(__('Name')) ?>" class="input-text" id="recipients-name<%- data._index_ %>" data-validate="{required:true}"/> </div> </div> <div class="field email required"> - <label for="recipients-email<%- data._index_ %>" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> + <label for="recipients-email<%- data._index_ %>" class="label"> + <span><?= $block->escapeHtml(__('Email')) ?></span> + </label> <div class="control"> <input name="recipients[email][<%- data._index_ %>]" title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="recipients-email<%- data._index_ %>" type="email" class="input-text" @@ -71,7 +79,8 @@ <label for="sender-email" class="label"><span><?= $block->escapeHtml(__('Email')) ?></span></label> <div class="control"> <input name="sender[email]" value="<?= $block->escapeHtmlAttr($block->getEmail()) ?>" - title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="sender-email" type="email" class="input-text" + title="<?= $block->escapeHtmlAttr(__('Email')) ?>" id="sender-email" type="email" + class="input-text" data-mage-init='{"mage/trim-input":{}}' data-validate="{required:true, 'validate-email':true}"/> </div> @@ -91,14 +100,16 @@ <legend class="legend"><span><?= $block->escapeHtml(__('Invitee')) ?></span></legend> <br /> <div id="recipients-options"></div> - <?php if ($block->getMaxRecipients()) : ?> - <div id="max-recipient-message" style="display: none;" class="message notice limit" role="alert"> - <span><?= $block->escapeHtml(__('Maximum %1 email addresses allowed.', $block->getMaxRecipients())) ?></span> + <?php if ($block->getMaxRecipients()): ?> + <div id="max-recipient-message" class="message notice limit" role="alert"> + <span><?= $block->escapeHtml(__('Maximum %1 email addresses allowed.', $block->getMaxRecipients())) ?> + </span> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag("display: none;", 'div#max-recipient-message') ?> <?php endif; ?> <div class="actions-toolbar"> <div class="secondary"> - <?php if (1 < $block->getMaxRecipients()) : ?> + <?php if (1 < $block->getMaxRecipients()): ?> <button type="button" id="add-recipient-button" class="action add"> <span><?= $block->escapeHtml(__('Add Invitee')) ?></span></button> <?php endif; ?> @@ -110,7 +121,7 @@ <div class="actions-toolbar"> <div class="primary"> <button type="submit" - class="action submit primary"<?php if (!$block->canSend()) : ?> disabled="disabled"<?php endif ?>> + class="action submit primary"<?php if (!$block->canSend()): ?> disabled="disabled"<?php endif ?>> <span><?= $block->escapeHtml(__('Send Email')) ?></span></button> </div> <div class="secondary"> diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php b/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php index 17efc11856364..4869a685f3064 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Create/Form.php @@ -5,6 +5,9 @@ */ namespace Magento\Shipping\Block\Adminhtml\Create; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; + /** * Adminhtml shipment create form * @@ -13,6 +16,24 @@ */ class Form extends \Magento\Sales\Block\Adminhtml\Order\AbstractOrder { + /** + * @param \Magento\Backend\Block\Template\Context $context + * @param \Magento\Framework\Registry $registry + * @param \Magento\Sales\Helper\Admin $adminHelper + * @param array $data + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Backend\Block\Template\Context $context, + \Magento\Framework\Registry $registry, + \Magento\Sales\Helper\Admin $adminHelper, + array $data = [], + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct($context, $registry, $adminHelper, $data); + } + /** * Retrieve invoice order * @@ -44,6 +65,8 @@ public function getShipment() } /** + * Prepare layout. + * * @return \Magento\Framework\View\Element\AbstractBlock */ protected function _prepareLayout() @@ -53,6 +76,8 @@ protected function _prepareLayout() } /** + * Return payment html. + * * @return string */ public function getPaymentHtml() @@ -61,6 +86,8 @@ public function getPaymentHtml() } /** + * Return items html. + * * @return string */ public function getItemsHtml() @@ -69,6 +96,8 @@ public function getItemsHtml() } /** + * Generate save url. + * * @return string */ public function getSaveUrl() diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php index e5e419328eea4..ce4521c9baa51 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Packaging.php @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Shipping\Block\Adminhtml\Order; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Carrier; + /** * Adminhtml shipment packaging * @@ -44,6 +48,7 @@ class Packaging extends \Magento\Backend\Block\Template * @param \Magento\Framework\Registry $coreRegistry * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory * @param array $data + * @param Carrier|null $carrierHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, @@ -51,12 +56,14 @@ public function __construct( \Magento\Shipping\Model\Carrier\Source\GenericInterface $sourceSizeModel, \Magento\Framework\Registry $coreRegistry, \Magento\Shipping\Model\CarrierFactory $carrierFactory, - array $data = [] + array $data = [], + ?Carrier $carrierHelper = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_coreRegistry = $coreRegistry; $this->_sourceSizeModel = $sourceSizeModel; $this->_carrierFactory = $carrierFactory; + $data['carrierHelper'] = $carrierHelper ?? ObjectManager::getInstance()->get(Carrier::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php index 55eecfa00d6da..5830160b60791 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/Order/Tracking/View.php @@ -5,6 +5,9 @@ */ namespace Magento\Shipping\Block\Adminhtml\Order\Tracking; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Data as ShippingHelper; + /** * Shipment tracking control form * @@ -24,14 +27,17 @@ class View extends \Magento\Shipping\Block\Adminhtml\Order\Tracking * @param \Magento\Framework\Registry $registry * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory * @param array $data + * @param ShippingHelper|null $shippingHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Shipping\Model\Config $shippingConfig, \Magento\Framework\Registry $registry, \Magento\Shipping\Model\CarrierFactory $carrierFactory, - array $data = [] + array $data = [], + ?ShippingHelper $shippingHelper = null ) { + $data['shippingHelper'] = $shippingHelper ?? ObjectManager::getInstance()->get(ShippingHelper::class); parent::__construct($context, $shippingConfig, $registry, $data); $this->_carrierFactory = $carrierFactory; } diff --git a/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php b/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php index 409797780bcf6..8467a34ed0368 100644 --- a/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php +++ b/app/code/Magento/Shipping/Block/Adminhtml/View/Form.php @@ -11,6 +11,10 @@ */ namespace Magento\Shipping\Block\Adminhtml\View; +use Magento\Framework\App\ObjectManager; +use Magento\Shipping\Helper\Data as ShippingHelper; +use Magento\Tax\Helper\Data as TaxHelper; + /** * @api * @since 100.0.2 @@ -28,15 +32,21 @@ class Form extends \Magento\Sales\Block\Adminhtml\Order\AbstractOrder * @param \Magento\Sales\Helper\Admin $adminHelper * @param \Magento\Shipping\Model\CarrierFactory $carrierFactory * @param array $data + * @param ShippingHelper|null $shippingHelper + * @param TaxHelper|null $taxHelper */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Registry $registry, \Magento\Sales\Helper\Admin $adminHelper, \Magento\Shipping\Model\CarrierFactory $carrierFactory, - array $data = [] + array $data = [], + ?ShippingHelper $shippingHelper = null, + ?TaxHelper $taxHelper = null ) { $this->_carrierFactory = $carrierFactory; + $data['shippingHelper'] = $shippingHelper ?? ObjectManager::getInstance()->get(ShippingHelper::class); + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); parent::__construct($context, $registry, $adminHelper, $data); } diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php index 76555ce8a6d8c..0965c4a472c25 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrier.php @@ -332,6 +332,7 @@ public function checkAvailableShipCountries(\Magento\Framework\DataObject $reque * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @since 100.2.6 */ public function processAdditionalValidation(\Magento\Framework\DataObject $request) { @@ -343,7 +344,7 @@ public function processAdditionalValidation(\Magento\Framework\DataObject $reque * * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject - * @deprecated + * @deprecated 100.2.6 * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function proccessAdditionalValidation(\Magento\Framework\DataObject $request) diff --git a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php index 27047ae46bf1f..c2238ff1a3809 100644 --- a/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php +++ b/app/code/Magento/Shipping/Model/Carrier/AbstractCarrierOnline.php @@ -303,7 +303,7 @@ public function getAllItems(RateRequest $request) * * @param \Magento\Framework\DataObject $request * @return $this|bool|\Magento\Framework\DataObject - * @deprecated + * @deprecated 100.2.6 * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -319,6 +319,7 @@ public function proccessAdditionalValidation(\Magento\Framework\DataObject $requ * @return $this|bool|\Magento\Framework\DataObject * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * @since 100.2.6 */ public function processAdditionalValidation(\Magento\Framework\DataObject $request) { diff --git a/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php b/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php index 4ff9ba0008340..546afdca5028b 100644 --- a/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php +++ b/app/code/Magento/Shipping/Model/ShipmentProviderInterface.php @@ -11,6 +11,7 @@ * Provide shipment items data. * * @api + * @since 100.3.0 */ interface ShipmentProviderInterface { @@ -18,6 +19,7 @@ interface ShipmentProviderInterface * Retrieve shipment items. * * @return array + * @since 100.3.0 */ public function getShipmentData(): array; } diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml index 0e69dba36d41c..fe2a1bf86a8ce 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateOrderCustomStoreShippingMethodTableRatesTest.xml @@ -64,7 +64,9 @@ <argument name="file" value="usa_tablerates.csv"/> </actionGroup> <actionGroup ref="AdminSaveConfigActionGroup" stepKey="saveConfig"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Delete created data--> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml index 5e57224bfee48..9d501e4b34ef7 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreatePartialShipmentEntityTest.xml @@ -28,7 +28,9 @@ <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Free Shipping" --> <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <magentoCLI command="cache:clean config" stepKey="flushCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml index 6b388ae31e45e..a900a73fc36bc 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/AdminCreateShipmentEntityTest.xml @@ -28,7 +28,9 @@ <!-- Enable payment method one of "Check/Money Order" and shipping method one of "Free Shipping" --> <magentoCLI command="config:set {{enabledCheckMoneyOrder.label}} {{enabledCheckMoneyOrder.value}}" stepKey="enableCheckMoneyOrder"/> <createData entity="FreeShippinMethodConfig" stepKey="enableFreeShipping"/> - <magentoCLI command="cache:clean config" stepKey="flushCache"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanCache"> + <argument name="tags" value="config"/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml index 5fd9a6a29c0e3..d448f51a00406 100644 --- a/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml +++ b/app/code/Magento/Shipping/Test/Mftf/Test/StorefrontDisplayTableRatesShippingMethodForAETest.xml @@ -67,8 +67,7 @@ <argument name="shippingMethodName" value="Best Way"/> </actionGroup> <!--Proceed to Review and Payments section--> - <click selector="{{CheckoutShippingSection.next}}" stepKey="clickToSaveShippingInfo"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskAfterClickNext"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickToSaveShippingInfo"/> <waitForPageLoad stepKey="waitForReviewAndPaymentsPageIsLoaded"/> <!--Place order and assert the message of success--> <actionGroup ref="ClickPlaceOrderActionGroup" stepKey="placeOrderProductSuccessful"/> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml index d539a44f58a63..7de40943878cf 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/form.phtml @@ -3,8 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** + * @var \Magento\Shipping\Block\Adminhtml\Create\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); ?> <form id="edit_form" method="post" action="<?= $block->escapeUrl($block->getSaveUrl()) ?>"> <?= $block->getBlockHtml('formkey') ?> @@ -22,7 +28,9 @@ </div> <div class="admin__page-section-item-content"> <div><?= $block->getPaymentHtml() ?></div> - <div class="order-payment-currency"><?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?></div> + <div class="order-payment-currency"> + <?= $block->escapeHtml(__('The order was placed using %1.', $_order->getOrderCurrencyCode())) ?> + </div> </div> </div> <div class="admin__page-section-item order-shipping-address"> @@ -37,15 +45,15 @@ <div class="shipping-description-content"> <?= $block->escapeHtml(__('Total Shipping Charges')) ?>: - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $_excl = $block->displayShippingPriceInclTax($_order); ?> - <?php else : ?> + <?php else: ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($_order); ?> <?= /** @noEscape */ $_excl ?> - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingBothPrices() - && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() + && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /** @noEscape */ $_incl ?>) <?php endif; ?> </div> @@ -59,7 +67,8 @@ <?= $block->getItemsHtml() ?> </div> </form> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/mage", @@ -68,5 +77,8 @@ require([ jQuery('#edit_form').mage('form').mage('validation'); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?= $block->getChildHtml('shipment_packaging'); diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml index ddb5dde5dfac7..9b55d2b969d3f 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/create/items.phtml @@ -3,8 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace -//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <section class="admin__page-section"> @@ -17,17 +17,17 @@ <tr class="headings"> <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> <th class="col-ordered-qty"><span><?= $block->escapeHtml(__('Qty')) ?></span></th> - <th class="col-qty<?php if ($block->isShipmentRegular()) : ?> last<?php endif; ?>"> + <th class="col-qty<?php if ($block->isShipmentRegular()): ?> last<?php endif; ?>"> <span><?= $block->escapeHtml(__('Qty to Ship')) ?></span> </th> - <?php if (!$block->canShipPartiallyItem()) : ?> + <?php if (!$block->canShipPartiallyItem()): ?> <th class="col-ship last"><span><?= $block->escapeHtml(__('Ship')) ?></span></th> <?php endif; ?> </tr> </thead> <?php $_items = $block->getShipment()->getAllItems() ?> - <?php $_i = 0; foreach ($_items as $_item) : - if ($_item->getOrderItem()->getParentItem()) : + <?php $_i = 0; foreach ($_items as $_item): + if ($_item->getOrderItem()->getParentItem()): continue; endif; $_i++ ?> @@ -70,17 +70,21 @@ <span class="title"><?= $block->escapeHtml(__('Shipment Options')) ?></span> </div> <div class="admin__page-section-item-content"> - <?php if ($block->canCreateShippingLabel()) : ?> + <?php if ($block->canCreateShippingLabel()): ?> <div class="field choice admin__field admin__field-option field-create"> <input id="create_shipping_label" class="admin__control-checkbox" name="shipment[create_shipping_label]" value="1" - type="checkbox" - onclick="toggleCreateLabelCheckbox();"/> + type="checkbox"/> <label class="admin__field-label" for="create_shipping_label"> <span><?= $block->escapeHtml(__('Create Shipping Label')) ?></span></label> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'toggleCreateLabelCheckbox();', + 'input#create_shipping_label' + ) ?> </div> <?php endif ?> @@ -95,7 +99,7 @@ <span><?=$block->escapeHtml(__('Append Comments')) ?></span></label> </div> - <?php if ($block->canSendShipmentEmail()) : ?> + <?php if ($block->canSendShipmentEmail()): ?> <div class="field choice admin__field admin__field-option field-email"> <input id="send_email" class="admin__control-checkbox" @@ -115,7 +119,8 @@ </div> </div> </section> -<script> +<?php $scriptString = <<<script + require([ "jquery", "Magento_Ui/js/modal/alert", @@ -150,7 +155,7 @@ window.toggleCreateLabelCheckbox = function() { window.submitShipment = function(btn) { if (!validQtyItems()) { alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Invalid value(s) for Qty to Ship'))) ?>' + content: '{$block->escapeJs(__('Invalid value(s) for Qty to Ship'))}' }); return; } @@ -186,4 +191,7 @@ window.sendEmailCheckbox = sendEmailCheckbox; //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml index 22d546f4fb474..7ddfc068fb115 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/grid.phtml @@ -3,39 +3,50 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace -//phpcs:disable Squiz.WhiteSpace.ScopeClosingBrace.ContentBefore -//phpcs:disable Squiz.Operators.IncrementDecrementUsage.NotAllowed //phpcs:disable Squiz.PHP.NonExecutableCode.Unreachable +/** + * @var \Magento\Shipping\Block\Adminhtml\Order\Packaging $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="grid"> <?php $randomId = rand(); ?> <div class="admin__table-wrapper"> - <table class="data-grid"> + <table id="packaging-data-grid-<?= /* @noEscape */ $randomId ?>" class="data-grid"> <thead> - <tr> - <th class="data-grid-checkbox-cell"> - <label class="data-grid-checkbox-cell-inner"> - <input type="checkbox" - id="select-items-<?= /* @noEscape */ $randomId ?>" - onchange="packaging.checkAllItems(this);" - class="checkbox admin__control-checkbox" - title="<?= $block->escapeHtmlAttr(__('Select All')) ?>"> - <label for="select-items-<?= /* @noEscape */ $randomId ?>"></label> - </label> - </th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Product Name')) ?></th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Weight')) ?></th> - <th class="data-grid-th" <?= $block->displayCustomsValue() ? '' : 'style="display: none;"' ?>> - <?= $block->escapeHtml(__('Customs Value')) ?> - </th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Qty Ordered')) ?></th> - <th class="data-grid-th"><?= $block->escapeHtml(__('Qty')) ?></th> - </tr> + <tr> + <th class="data-grid-checkbox-cell"> + <label class="data-grid-checkbox-cell-inner"> + <input type="checkbox" + id="select-items-<?= /* @noEscape */ $randomId ?>" + class="checkbox admin__control-checkbox" + title="<?= $block->escapeHtmlAttr(__('Select All')) ?>"> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'packaging.checkAllItems(this);', + 'input#select-items-' . /* @noEscape */ $randomId + ) ?> + <label for="select-items-<?= /* @noEscape */ $randomId ?>"></label> + </label> + </th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Product Name')) ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Weight')) ?></th> + <th class="data-grid-th custom-value"> + <?= $block->escapeHtml(__('Customs Value')) ?> + </th> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + '#packaging-data-grid-' . $randomId . ' th.custom-value' + ) ?> + <?php endif ?> + <th class="data-grid-th"><?= $block->escapeHtml(__('Qty Ordered')) ?></th> + <th class="data-grid-th"><?= $block->escapeHtml(__('Qty')) ?></th> + </tr> </thead> <tbody> - <?php $i=0; ?> - <?php foreach ($block->getCollection() as $item) : ?> + <?php $i = 0; ?> + <?php foreach ($block->getCollection() as $item): ?> <?php $_order = $block->getShipment()->getOrder(); $_orderItem = $_order->getItemById($item->getOrderItemId()); @@ -44,17 +55,17 @@ || ($_orderItem->isShipSeparately() && !($_orderItem->getParentItemId() || $_orderItem->getParentItem())) || (!$_orderItem->isShipSeparately() - && ($_orderItem->getParentItemId() || $_orderItem->getParentItem()))) : ?> + && ($_orderItem->getParentItemId() || $_orderItem->getParentItem()))): ?> <?php continue; ?> <?php endif; ?> <tr class="data-grid-controls-row data-row <?= ($i++ % 2 != 0) ? '_odd-row' : '' ?>"> <td class="data-grid-checkbox-cell"> - <?php $id = $item->getId() ? $item->getId() : $item->getOrderItemId(); ?> + <?php $id = $item->getId() ?? $item->getOrderItemId(); ?> <label class="data-grid-checkbox-cell-inner"> <input type="checkbox" name="" id="select-item-<?= /* @noEscape */ $randomId . '-' . $id ?>" - value="<?= (int) $id ?>" + value="<?= (int)$id ?>" class="checkbox admin__control-checkbox"> <label for="select-item-<?= /* @noEscape */ $randomId . '-' . $id ?>"></label> </label> @@ -67,45 +78,61 @@ </td> <?php if ($block->displayCustomsValue()) { - $customsValueDisplay = ''; $customsValueValidation = ' validate-zero-or-greater '; } else { - $customsValueDisplay = ' style="display: none;" '; $customsValueValidation = ''; } ?> - <td <?= /* @noEscape */ $customsValueDisplay ?>> + <td id="custom-value-<?= /* @noEscape */ $randomId . '-' . $id ?>" class="custom-value"> <input type="text" name="customs_value" class="input-text admin__control-text <?= /* @noEscape */ $customsValueValidation ?>" value="<?= $block->escapeHtmlAttr($block->formatPrice($item->getPrice())) ?>" - size="10" - onblur="packaging.recalcContainerWeightAndCustomsValue(this);"> + size="10"> </td> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'td#custom-value-' . $randomId . '-' . $id + ) ?> + <?php endif ?> <td> - <?= /* @noEscape */ $item->getOrderItem()->getQtyOrdered()*1 ?> + <?= /* @noEscape */ $item->getOrderItem()->getQtyOrdered() * 1 ?> </td> <td> <input type="hidden" name="price" value="<?= $block->escapeHtml($item->getPrice()) ?>"> <input type="text" name="qty" - value="<?= /* @noEscape */ $item->getQty()*1 ?>" + value="<?= /* @noEscape */ $item->getQty() * 1 ?>" class="input-text admin__control-text qty - <?php if ($item->getOrderItem()->getIsQtyDecimal()) : ?> + <?php if ($item->getOrderItem()->getIsQtyDecimal()): ?> qty-decimal <?php endif ?>">  <button type="button" + id="packaging-delete-item-<?= /* @noEscape */ $randomId . '-' . $id ?>" class="action-delete" - data-action="package-delete-item" - onclick="packaging.deleteItem(this);" - style="display:none;"> + data-action="package-delete-item"> <span><?= $block->escapeHtml(__('Delete')) ?></span> </button> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'button#packaging-delete-item-' . $randomId . '-' . $id + ) ?> </td> </tr> <?php endforeach; ?> </tbody> </table> + <?php $scriptString = <<<script + require(['jquery'], function ($) { + $("#packaging-data-grid-{$randomId}").on('blur', 'td.custom-value input', + function(){packaging.recalcContainerWeightAndCustomsValue(this)}); + $("#packaging-data-grid-{$randomId}").on('click', 'button[data-action="package-delete-item"]', + function(){packaging.deleteItem(this)}); + }); +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml index 8d47f533449a7..90ecfa3862000 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/packed.phtml @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Files.LineLength.MaxExceeded -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var \Magento\Shipping\Helper\Carrier $carrierHelper */ +$carrierHelper = $block->getData('carrierHelper'); ?> <div id="packed_window"> -<?php foreach ($block->getPackages() as $packageId => $package) : ?> +<?php foreach ($block->getPackages() as $packageId => $package): ?> <?php $package = new \Magento\Framework\DataObject($package) ?> <?php $params = new \Magento\Framework\DataObject($package->getParams()) ?> <section class="admin__page-section"> @@ -27,15 +30,18 @@ </td> </tr> <tr> - <?php if ($block->displayCustomsValue()) : ?> + <?php if ($block->displayCustomsValue()): ?> <th><?= $block->escapeHtml(__('Customs Value')) ?></th> - <td><?= $block->escapeHtml($block->displayCustomsPrice($params->getCustomsValue())) ?></td> - <?php else : ?> + <td><?= $block->escapeHtml($block->displayCustomsPrice($params->getCustomsValue())) ?> + </td> + <?php else: ?> <th><?= $block->escapeHtml(__('Total Weight')) ?></th> - <td><?= $block->escapeHtml($params->getWeight() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureWeightName($params->getWeightUnits())) ?></td> + <td><?= $block->escapeHtml($params->getWeight() . ' ' . + $carrierHelper->getMeasureWeightName($params->getWeightUnits())) ?> + </td> <?php endif; ?> </tr> - <?php if ($params->getSize()) : ?> + <?php if ($params->getSize()): ?> <tr> <th><?= $block->escapeHtml(__('Size')) ?></th> <td><?= $block->escapeHtml(ucfirst(strtolower($params->getSize()))) ?></td> @@ -50,9 +56,10 @@ <tr> <th><?= $block->escapeHtml(__('Length')) ?></th> <td> - <?php if ($params->getLength() != null) : ?> - <?= $block->escapeHtml($params->getLength() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> - <?php else : ?> + <?php if ($params->getLength() != null): ?> + <?= $block->escapeHtml($params->getLength() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else: ?> -- <?php endif; ?> </td> @@ -60,9 +67,10 @@ <tr> <th><?= $block->escapeHtml(__('Width')) ?></th> <td> - <?php if ($params->getWidth() != null) : ?> - <?= $block->escapeHtml($params->getWidth() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> - <?php else : ?> + <?php if ($params->getWidth() != null): ?> + <?= $block->escapeHtml($params->getWidth() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else: ?> -- <?php endif; ?> </td> @@ -70,9 +78,10 @@ <tr> <th><?= $block->escapeHtml(__('Height')) ?></th> <td> - <?php if ($params->getHeight() != null) : ?> - <?= $block->escapeHtml($params->getHeight() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getDimensionUnits())) ?> - <?php else : ?> + <?php if ($params->getHeight() != null): ?> + <?= $block->escapeHtml($params->getHeight() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getDimensionUnits())) ?> + <?php else: ?> -- <?php endif; ?> </td> @@ -83,26 +92,33 @@ <div class="col-m-4"> <table class="admin__table-secondary"> <tbody> - <?php if ($params->getDeliveryConfirmation() != null) : ?> + <?php if ($params->getDeliveryConfirmation() != null): ?> <tr> <th><?= $block->escapeHtml(__('Signature Confirmation')) ?></th> - <td><?= $block->escapeHtml($block->getDeliveryConfirmationTypeByCode($params->getDeliveryConfirmation())) ?></td> + <td> + <?= $block->escapeHtml( + $block->getDeliveryConfirmationTypeByCode($params->getDeliveryConfirmation()) + ) ?></td> </tr> <?php endif; ?> - <?php if ($params->getContentType() != null) : ?> + <?php if ($params->getContentType() != null): ?> <tr> <th><?= $block->escapeHtml(__('Contents')) ?></th> - <?php if ($params->getContentType() == 'OTHER') : ?> + <?php if ($params->getContentType() == 'OTHER'): ?> <td><?= $block->escapeHtml($params->getContentTypeOther()) ?></td> - <?php else : ?> - <td><?= $block->escapeHtml($block->getContentTypeByCode($params->getContentType())) ?></td> + <?php else: ?> + <td> + <?= $block->escapeHtml($block->getContentTypeByCode($params->getContentType())) + ?></td> <?php endif; ?> </tr> <?php endif; ?> - <?php if ($params->getGirth()) : ?> + <?php if ($params->getGirth()): ?> <tr> <th><?= $block->escapeHtml(__('Girth')) ?></th> - <td><?= $block->escapeHtml($params->getGirth() . ' ' . $this->helper(Magento\Shipping\Helper\Carrier::class)->getMeasureDimensionName($params->getGirthDimensionUnits())) ?></td> + <td><?= $block->escapeHtml($params->getGirth() . ' ' . + $carrierHelper->getMeasureDimensionName($params->getGirthDimensionUnits())) ?> + </td> </tr> <?php endif; ?> </tbody> @@ -119,7 +135,7 @@ <tr class="headings"> <th class="col-product"><span><?= $block->escapeHtml(__('Product')) ?></span></th> <th class="col-weight"><span><?= $block->escapeHtml(__('Weight')) ?></span></th> - <?php if ($block->displayCustomsValue()) : ?> + <?php if ($block->displayCustomsValue()): ?> <th class="col-custom"><span><?= $block->escapeHtml(__('Customs Value')) ?></span></th> <?php endif; ?> <th class="col-qty"><span><?= $block->escapeHtml(__('Qty Ordered')) ?></span></th> @@ -127,7 +143,7 @@ </tr> </thead> <tbody id=""> - <?php foreach ($package->getItems() as $itemId => $item) : ?> + <?php foreach ($package->getItems() as $itemId => $item): ?> <?php $item = new \Magento\Framework\DataObject($item) ?> <tr title="#" id=""> <td class="col-product"> @@ -136,7 +152,7 @@ <td class="col-weight"> <?= $block->escapeHtml($item->getWeight()) ?> </td> - <?php if ($block->displayCustomsValue()) : ?> + <?php if ($block->displayCustomsValue()): ?> <td class="col-custom"> <?= $block->escapeHtml($block->displayCustomsPrice($item->getCustomsValue())) ?> </td> @@ -155,11 +171,15 @@ </section> <?php endforeach; ?> </div> -<script> +<?php $scriptString = <<<script + function showPackedWindow() { jQuery('#packed_window').modal('openModal'); } -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <script type="text/x-magento-init"> { diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml index 28322d9534926..206deb0f5c795 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup.phtml @@ -5,14 +5,20 @@ */ //phpcs:disable PSR2.Methods.FunctionCallSignature.SpaceBeforeOpenBracket //phpcs:disable Magento2.Security.IncludeFile.FoundIncludeFile + +/** + * @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php /** @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging */ ?> <?php $shippingMethod = $block->getShipment()->getOrder()->getShippingMethod(); $sizeSource = $block->getSourceSizeModel()->toOptionArray(); $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : 0; ?> -<script> + +<?php $scriptString = <<<script + require([ "jquery", "prototype", @@ -20,21 +26,21 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : "Magento_Ui/js/modal/modal" ], function(jQuery){ - window.packaging = new Packaging(<?= /* @noEscape */ $block->getConfigDataJson() ?>); + window.packaging = new Packaging({$block->getConfigDataJson()}); packaging.changeContainerType($$('select[name=package_container]')[0]); packaging.checkSizeAndGirthParameter( - $$('select[name=package_container]')[0], - <?= /* @noEscape */ $girthEnabled ?> + \$$('select[name=package_container]')[0], + {$girthEnabled} ); packaging.setConfirmPackagingCallback(function(){ packaging.setParamsCreateLabelRequest($('edit_form').serialize(true)); packaging.sendCreateLabelRequest(); }); packaging.setLabelCreatedCallback(function(response){ - setLocation("<?= $block->escapeJs($block->escapeUrl($block->getUrl( + setLocation("{$block->escapeJs($block->getUrl( 'sales/order/view', ['order_id' => $block->getShipment()->getOrderId()] - ))); ?>"); + ))}"); }); packaging.setCancelCallback(function() { if ($('create_shipping_label')) { @@ -51,23 +57,23 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : }); jQuery('#packaging_window').modal({ type: 'slide', - title: '<?= $block->escapeJs($block->escapeHtml(__('Create Packages'))) ?>', + title: '{$block->escapeJs(__('Create Packages'))}', buttons: [{ - text: '<?= $block->escapeJs($block->escapeHtml(__('Cancel'))) ?>', + text: '{$block->escapeJs(__('Cancel'))}', 'class': 'action-secondary', click: function () { packaging.cancelPackaging(); this.closeModal(); } }, { - text: '<?= $block->escapeJs($block->escapeHtml(__('Save'))) ?>', + text: '{$block->escapeJs(__('Save'))}', 'attr': {'disabled':'disabled', 'data-action':'save-packages'}, 'class': 'action-primary _disabled', click: function () { packaging.confirmPackaging(); } }, { - text: '<?= $block->escapeJs($block->escapeHtml(__('Add Package'))) ?>', + text: '{$block->escapeJs(__('Add Package'))}', 'attr': {'data-action':'add-packages'}, 'class': 'action-secondary', click: function () { @@ -78,5 +84,8 @@ $girthEnabled = $block->isDisplayGirthValue() && $block->isGirthAllowed() ? 1 : jQuery(document).trigger('packaging:inited'); jQuery(document).data('packagingInited', true); }); -</script> -<?php include ($block->getTemplateFile('Magento_Shipping::order/packaging/popup_content.phtml')) ?> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> +<?php include($block->getTemplateFile('Magento_Shipping::order/packaging/popup_content.phtml')) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml index f91741f439d46..c3418049a38a0 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/packaging/popup_content.phtml @@ -3,12 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** + * @var \Magento\Shipping\Block\Adminhtml\Order\Packaging $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<?php /** @var $block \Magento\Shipping\Block\Adminhtml\Order\Packaging */ ?> <div id="packaging_window"> - <div class="message message-warning" style="display: none"></div> - <section class="admin__page-section" id="package_template" style="display:none;"> + <div class="message message-warning"></div> + <section class="admin__page-section" id="package_template"> <div class="admin__page-section-title"> <span class="title"> <?= $block->escapeHtml(__('Package')) ?> <span data-role="package-number"></span> @@ -16,14 +19,12 @@ <div class="actions _primary"> <button type="button" class="action-secondary" - data-action="package-save-items" - onclick="packaging.packItems(this);"> + data-action="package-save-items"> <span><?= $block->escapeHtml(__('Add Selected Product(s) to Package')) ?></span> </button> <button type="button" class="action-secondary" - data-action="package-add-items" - onclick="packaging.getItemsForPack(this);"> + data-action="package-add-items"> <span><?= $block->escapeHtml(__('Add Products to Package')) ?></span> </button> </div> @@ -33,51 +34,54 @@ <thead> <tr> <th class="col-type"><?= $block->escapeHtml(__('Type')) ?></th> - <?php if ($girthEnabled == 1) : ?> + <?php if ($girthEnabled == 1): ?> <th class="col-size"><?= $block->escapeHtml(__('Size')) ?></th> <th class="col-girth"><?= $block->escapeHtml(__('Girth')) ?></th> <th> </th> <?php endif; ?> - <th class="col-custom" <?= $block->displayCustomsValue() ? '' : 'style="display: none;"' ?>> + <th class="col-custom"> <?= $block->escapeHtml(__('Customs Value')) ?> </th> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none', 'th.col-custom') ?> + <?php endif ?> <th class="col-total-weight"><?= $block->escapeHtml(__('Total Weight')) ?></th> <th class="col-length"><?= $block->escapeHtml(__('Length')) ?></th> <th class="col-width"><?= $block->escapeHtml(__('Width')) ?></th> <th class="col-height"><?= $block->escapeHtml(__('Height')) ?></th> <th> </th> - <?php if ($block->getDeliveryConfirmationTypes()) : ?> + <?php if ($block->getDeliveryConfirmationTypes()): ?> <th class="col-signature"><?= $block->escapeHtml(__('Signature Confirmation')) ?></th> <?php endif; ?> <th class="col-actions"> </th> </tr> </thead> + <tbody> <tr> <td class="col-type"> <?php $containers = $block->getContainers(); ?> <select name="package_container" - onchange="packaging.changeContainerType(this);packaging.checkSizeAndGirthParameter(this, <?= $block->escapeJs($girthEnabled) ?>);" - <?php if (empty($containers)) : ?> - title="<?= $block->escapeHtmlAttr(__('USPS domestic shipments don\'t use package types.')) ?>" + <?php if (empty($containers)): ?> + title="<?= $block->escapeHtmlAttr(__( + 'USPS domestic shipments don\'t use package types.' + )) ?>" disabled="" class="admin__control-select disabled" - <?php else : ?> + <?php else: ?> class="admin__control-select" <?php endif; ?>> - <?php foreach ($containers as $key => $value) : ?> + <?php foreach ($containers as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($key) ?>" > <?= $block->escapeHtml($value) ?> </option> <?php endforeach; ?> </select> </td> - <?php if ($girthEnabled == 1 && !empty($sizeSource)) : ?> + <?php if ($girthEnabled == 1 && !empty($sizeSource)): ?> <td> - <select name="package_size" - class="admin__control-select" - onchange="packaging.checkSizeAndGirthParameter(this, <?= $block->escapeJs($girthEnabled) ?>);"> - <?php foreach ($sizeSource as $key => $value) : ?> + <select name="package_size" class="admin__control-select"> + <?php foreach ($sizeSource as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($sizeSource[$key]['value']) ?>"> <?= $block->escapeHtml($sizeSource[$key]['label']) ?> </option> @@ -91,26 +95,28 @@ </td> <td> <select name="container_girth_dimension_units" - class="options-units-dimensions measures admin__control-select" - onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= $block->escapeHtml(__('in')) ?></option> - <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" ><?= $block->escapeHtml(__('cm')) ?></option> + class="options-units-dimensions measures admin__control-select"> + <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" > + <?= $block->escapeHtml(__('in')) ?> + </option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" > + <?= $block->escapeHtml(__('cm')) ?> + </option> </select> </td> <?php endif; ?> <?php if ($block->displayCustomsValue()) { - $customsValueDisplay = ''; $customsValueValidation = ' validate-zero-or-greater '; } else { - $customsValueDisplay = ' style="display: none;" '; $customsValueValidation = ''; } ?> - <td class="col-custom" <?= /* @noEscape */ $customsValueDisplay ?>> + <td class="col-custom"> <div class="admin__control-addon"> <input type="text" - class="customs-value input-text admin__control-text <?= /* @noEscape */ $customsValueValidation ?>" + class="customs-value input-text admin__control-text <?= + /* @noEscape */ $customsValueValidation ?>" name="package_customs_value" /> <span class="admin__addon-suffix"> <span class="customs-value-currency"> @@ -119,16 +125,23 @@ </span> </div> </td> + <?php if (!$block->displayCustomsValue()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none', 'td.col-custom') ?> + <?php endif ?> <td class="col-total-weight"> <div class="admin__control-addon"> <input type="text" - class="options-weight input-text admin__control-text required-entry validate-greater-than-zero" + class="options-weight input-text admin__control-text required-entry + validate-greater-than-zero" name="container_weight" /> <select name="container_weight_units" - class="options-units-weight measures admin__control-select" - onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @noEscape */ Zend_Measure_Weight::POUND ?>" selected="selected" ><?= $block->escapeHtml(__('lb')) ?></option> - <option value="<?= /* @noEscape */ Zend_Measure_Weight::KILOGRAM ?>" ><?= $block->escapeHtml(__('kg')) ?></option> + class="options-units-weight measures admin__control-select"> + <option value="<?= /* @noEscape */ Zend_Measure_Weight::POUND + ?>" selected="selected" ><?= $block->escapeHtml(__('lb')) ?> + </option> + <option value="<?= /* @noEscape */ Zend_Measure_Weight::KILOGRAM ?>" > + <?= $block->escapeHtml(__('kg')) ?> + </option> </select> <span class="admin__addon-prefix"></span> </div> @@ -150,16 +163,19 @@ </td> <td class="col-measure"> <select name="container_dimension_units" - class="options-units-dimensions measures admin__control-select" - onchange="packaging.changeMeasures(this);"> - <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" ><?= $block->escapeHtml(__('in')) ?></option> - <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" ><?= $block->escapeHtml(__('cm')) ?></option> + class="options-units-dimensions measures admin__control-select"> + <option value="<?= /* @noEscape */ Zend_Measure_Length::INCH ?>" selected="selected" > + <?= $block->escapeHtml(__('in')) ?> + </option> + <option value="<?= /* @noEscape */ Zend_Measure_Length::CENTIMETER ?>" > + <?= $block->escapeHtml(__('cm')) ?> + </option> </select> </td> - <?php if ($block->getDeliveryConfirmationTypes()) : ?> + <?php if ($block->getDeliveryConfirmationTypes()): ?> <td> <select name="delivery_confirmation_types" class="admin__control-select"> - <?php foreach ($block->getDeliveryConfirmationTypes() as $key => $value) : ?> + <?php foreach ($block->getDeliveryConfirmationTypes() as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($key) ?>" > <?= $block->escapeHtml($value) ?> </option> @@ -169,15 +185,14 @@ <?php endif; ?> <td class="col-actions"> <button type="button" - class="action-delete DeletePackageBtn" - onclick="packaging.deletePackage(this);"> + class="action-delete DeletePackageBtn"> <span><?= $block->escapeHtml(__('Delete Package')) ?></span> </button> </td> </tr> </tbody> </table> - <?php if ($block->getContentTypes()) : ?> + <?php if ($block->getContentTypes()): ?> <table class="data-table admin__control-table" cellspacing="0"> <thead> <tr> @@ -189,9 +204,8 @@ <tr> <td> <select name="content_type" - class="admin__control-select" - onchange="packaging.changeContentTypes(this);"> - <?php foreach ($block->getContentTypes() as $key => $value) : ?> + class="admin__control-select"> + <?php foreach ($block->getContentTypes() as $key => $value): ?> <option value="<?= $block->escapeHtmlAttr($key) ?>" > <?= $block->escapeHtml($value) ?> </option> @@ -213,5 +227,47 @@ <div class="grid_prepare admin__page-subsection"></div> </div> </section> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display:none', '#package_template') ?> <div id="packages_content"></div> + <?php $scriptString = <<<script +require(['jquery'], function($){ + $("div#packages_content").on('click', "button[data-action='package-save-items']", + function(){packaging.packItems(this)}); + $("div#packages_content").on('click', "button[data-action='package-add-items']", + function(){packaging.getItemsForPack(this)}); + $("div#packages_content").on('change', "select[name='package_container']", + function(){ + packaging.changeContainerType(this); + packaging.checkSizeAndGirthParameter(this, {$block->escapeJs($girthEnabled)}) + }); + $("div#packages_content").on('change', "select[name='container_weight_units']", + function(){packaging.changeMeasures(this)}); + $("div#packages_content").on('change', "select[name='container_dimension_units']", + function(){packaging.changeMeasures(this)}); + $("div#packages_content").on('click', "button.action-delete.DeletePackageBtn", + function(){packaging.deletePackage(this)}); +script; + if ($girthEnabled == 1 && !empty($sizeSource)) { + $scriptString .= <<<script + $("div#packages_content").on('change', "select[name='package_size']", + function(){packaging.checkSizeAndGirthParameter(this, {$block->escapeJs($girthEnabled)})}); + $("div#packages_content").on('change', "select[name='container_girth_dimension_units']", + function(){packaging.changeMeasures(this)}); +script; + } + if ($block->getContentTypes()) { + $scriptString .= <<<script + $("div#packages_content").on('change', "select[name='content_type']", + function(){packaging.changeContentTypes(this)}); +script; + } + $scriptString .= <<<script +}) +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'div#packaging_window div.message.message-warning' +) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml index d65fa819eaeed..1dcc7439532b6 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking.phtml @@ -4,8 +4,14 @@ * See COPYING.txt for license details. */ ?> -<?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking */?> -<script> +<?php +/** + * @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ +?> +<?php $scriptString = <<<script + require(['prototype'], function(){ //<![CDATA[ @@ -57,7 +63,11 @@ require(['prototype'], function(){ //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> + <script id="track_row_template" type="text/x-magento-template"> <tr> <td class="col-carrier"> @@ -65,7 +75,7 @@ require(['prototype'], function(){ id="trackingC<%- data.index %>" class="select admin__control-select carrier" disabled="disabled"> - <?php foreach ($block->getCarriers() as $_code => $_name) : ?> + <?php foreach ($block->getCarriers() as $_code => $_name): ?> <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> <?php endforeach; ?> </select> @@ -116,7 +126,8 @@ require(['prototype'], function(){ </tbody> </table> </div> -<script> +<?php $scriptString = <<<script + require([ 'mage/template', 'prototype' @@ -127,4 +138,7 @@ require([ //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml index a013abfd65f87..df303b777188c 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/tracking/view.phtml @@ -3,11 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** + * @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking\View + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Shipping\Helper\Data $shippingHelper */ +$shippingHelper = $block->getData('shippingHelper'); ?> -<?php /** @var $block Magento\Shipping\Block\Adminhtml\Order\Tracking\View */ ?> <div class="admin__control-table-wrapper"> <form id="tracking-shipping-form" data-mage-init='{"validation": {}}'> <table class="data-table admin__control-table" id="shipment_tracking_info"> @@ -22,13 +26,17 @@ <tfoot> <tr> <td class="col-carrier"> - <select name="carrier" - class="select admin__control-select" - onchange="selectCarrier(this)"> - <?php foreach ($block->getCarriers() as $_code => $_name) : ?> - <option value="<?= $block->escapeHtmlAttr($_code) ?>"><?= $block->escapeHtml($_name) ?></option> + <select name="carrier" class="select admin__control-select"> + <?php foreach ($block->getCarriers() as $_code => $_name): ?> + <option value="<?= $block->escapeHtmlAttr($_code) ?>"> + <?= $block->escapeHtml($_name) ?></option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'selectCarrier(this)', + "select[name='carrier']" + ) ?> </td> <td class="col-title"> <input class="input-text admin__control-text" @@ -47,23 +55,42 @@ <td class="col-delete last"><?= $block->getSaveButtonHtml() ?></td> </tr> </tfoot> - <?php if ($_tracks = $block->getShipment()->getAllTracks()) : ?> + <?php if ($_tracks = $block->getShipment()->getAllTracks()): ?> <tbody> - <?php $i = 0; foreach ($_tracks as $_track) :$i++ ?> + <?php $i = 0; foreach ($_tracks as $_track): $i++ ?> <tr class="<?= /* @noEscape */ ($i%2 == 0) ? 'even' : 'odd' ?>"> <td class="col-carrier"> <?= $block->escapeHtml($block->getCarrierTitle($_track->getCarrierCode())) ?> </td> <td class="col-title"><?= $block->escapeHtml($_track->getTitle()) ?></td> <td class="col-number"> - <?php if ($_track->isCustom()) : ?> + <?php if ($_track->isCustom()): ?> <?= $block->escapeHtml($_track->getNumber()) ?> - <?php else : ?> - <a href="#" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($_track))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')"><?= $block->escapeHtml($_track->getNumber()) ?></a> + <?php else: ?> + <a id="col-track-<?= (int) $_track->getId() ?>" href="#"> + <?= $block->escapeHtml($_track->getNumber()) ?> + </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "event.preventDefault(); + popWin('{$block->escapeJs($shippingHelper->getTrackingPopupUrlBySalesModel($_track))}', + 'trackorder','width=800,height=600,resizable=yes,scrollbars=yes')", + 'a#col-track-' . (int) $_track->getId() + ) ?> <div id="shipment_tracking_info_response_<?= (int) $_track->getId() ?>"></div> <?php endif; ?> </td> - <td class="col-delete last"><button class="action-delete" type="button" onclick="deleteTrackingNumber('<?= $block->escapeJs($block->escapeUrl($block->getRemoveUrl($_track))) ?>'); return false;"><span><?= $block->escapeHtml(__('Delete')) ?></span></button></td> + <td class="col-delete last"> + <button class="action-delete" type="button" id="del-track-<?= (int) $_track->getId() ?>"> + <span><?= $block->escapeHtml(__('Delete')) ?></span> + </button> + </td> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "deleteTrackingNumber('{$block->escapeJs($block->getRemoveUrl($_track))}'); + event.preventDefault();", + '#del-track-' . (int) $_track->getId() + ) ?> </tr> <?php endforeach; ?> </tbody> @@ -71,9 +98,9 @@ </table> </form> </div> +<?php $scriptString = <<<script -<script> -require(['prototype', 'jquery', 'Magento_Ui/js/modal/confirm'], function(prototype, $j, confirm) { +require(['prototype', 'jquery', 'Magento_Ui/js/modal/confirm'], function(prototype, \$j, confirm) { //<![CDATA[ function selectCarrier(elem) { var option = elem.options[elem.selectedIndex]; @@ -81,7 +108,7 @@ function selectCarrier(elem) { } function saveTrackingInfo(node, url) { - var form = $j('#tracking-shipping-form'); + var form = \$j('#tracking-shipping-form'); if (form.validation() && form.validation('isValid')) { submitAndReloadArea(node, url); @@ -90,7 +117,7 @@ function saveTrackingInfo(node, url) { function deleteTrackingNumber(url) { confirm({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Are you sure?'))) ?>', + content: '{$block->escapeJs(__('Are you sure?'))}', actions: { /** * Confirm action. @@ -108,4 +135,7 @@ window.saveTrackingInfo = saveTrackingInfo; //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml index 720b34983551d..002a960f3b38a 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/order/view/info.phtml @@ -3,11 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis + +/** + * @var $block \Magento\Sales\Block\Adminhtml\Order\AbstractOrder + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Shipping\Helper\Data $shippingHelper */ +$shippingHelper = $block->getData('shippingHelper'); +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); ?> -<?php /** @var $block \Magento\Shipping\Block\Adminhtml\View */ ?> <?php $order = $block->getOrder() ?> -<?php if ($order->getIsVirtual()) : +<?php if ($order->getIsVirtual()): return ''; endif; ?> @@ -17,25 +25,34 @@ endif; ?> <span class="title"><?= $block->escapeHtml(__('Shipping & Handling Information')) ?></span> </div> <div class="admin__page-section-item-content"> - <?php if ($order->getTracksCollection()->count()) : ?> - <p><a href="#" id="linkId" onclick="popWin('<?= $block->escapeJs($block->escapeUrl($this->helper(Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($order))) ?>','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')" title="<?= $block->escapeHtmlAttr(__('Track Order')) ?>"><?= $block->escapeHtml(__('Track Order')) ?></a></p> + <?php if ($order->getTracksCollection()->count()): ?> + <p> + <a href="#" id="linkId" title="<?= $block->escapeHtmlAttr(__('Track Order')) ?>"> + <?= $block->escapeHtml(__('Track Order')) ?> + </a> + </p> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "popWin('" . $block->escapeJs($shippingHelper->getTrackingPopupUrlBySalesModel($order)) . + "','trackorder','width=800,height=600,resizable=yes,scrollbars=yes')", + 'a#linkId' + ) ?> <?php endif; ?> - <?php if ($order->getShippingDescription()) : ?> + <?php if ($order->getShippingDescription()): ?> <strong><?= $block->escapeHtml($order->getShippingDescription()) ?></strong> - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $_excl = $block->displayShippingPriceInclTax($order); ?> - <?php else : ?> + <?php else: ?> <?php $_excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $_incl = $block->displayShippingPriceInclTax($order); ?> <?= /** @noEscape */ $_excl ?> - <?php if ($this->helper(Magento\Tax\Helper\Data::class)->displayShippingBothPrices() - && $_incl != $_excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $_incl != $_excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')) ?> <?= /** @noEscape */ $_incl ?>) <?php endif; ?> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml(__('No shipping information available')) ?> <?php endif; ?> </div> diff --git a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml index 44fe4b9ccd353..d023f614f55aa 100644 --- a/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml +++ b/app/code/Magento/Shipping/view/adminhtml/templates/view/form.phtml @@ -5,9 +5,14 @@ */ /** * @var \Magento\Shipping\Block\Adminhtml\View\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ -//phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -//phpcs:disable Magento2.Files.LineLength.MaxExceeded + +/** @var \Magento\Shipping\Helper\Data $shippingHelper */ +$shippingHelper = $block->getData('shippingHelper'); +/** @var \Magento\Tax\Helper\Data $taxHelper */ +$taxHelper = $block->getData('taxHelper'); +/** @var \Magento\Sales\Model\Order $order */ $order = $block->getShipment()->getOrder(); ?> <?= $block->getChildHtml('order_info'); ?> @@ -34,12 +39,19 @@ $order = $block->getShipment()->getOrder(); </div> <div class="admin__page-section-item-content"> <div class="shipping-description-wrapper"> - <?php if ($block->getShipment()->getTracksCollection()->count()) : ?> + <?php if ($block->getShipment()->getTracksCollection()->count()): ?> <p> - <a href="#" id="linkId" onclick="popWin('<?= $block->escapeUrl($this->helper(\Magento\Shipping\Helper\Data::class)->getTrackingPopupUrlBySalesModel($block->getShipment())); ?>','trackshipment','width=800,height=600,resizable=yes,scrollbars=yes')" - title="<?= $block->escapeHtml(__('Track this shipment')); ?>"> + <a href="#" id="linkId" title="<?= $block->escapeHtml(__('Track this shipment')); ?>"> <?= $block->escapeHtml(__('Track this shipment')); ?> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + 'event.preventDefault();' . + "popWin('{$block->escapeJs($shippingHelper->getTrackingPopupUrlBySalesModel( + $block->getShipment() + ))}','trackshipment','width=800,height=600,resizable=yes,scrollbars=yes')", + 'a#linkId' + ) ?> </p> <?php endif; ?> <div class="shipping-description-title"> @@ -48,34 +60,35 @@ $order = $block->getShipment()->getOrder(); <?= $block->escapeHtml(__('Total Shipping Charges')); ?>: - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingPriceIncludingTax()) : ?> + <?php if ($taxHelper->displayShippingPriceIncludingTax()): ?> <?php $excl = $block->displayShippingPriceInclTax($order); ?> - <?php else : ?> + <?php else: ?> <?php $excl = $block->displayPriceAttribute('shipping_amount', false, ' '); ?> <?php endif; ?> <?php $incl = $block->displayShippingPriceInclTax($order); ?> <?= /* @noEscape */ $excl; ?> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayShippingBothPrices() && $incl != $excl) : ?> + <?php if ($taxHelper->displayShippingBothPrices() && $incl != $excl): ?> (<?= $block->escapeHtml(__('Incl. Tax')); ?> <?= /* @noEscape */ $incl; ?>) <?php endif; ?> </div> <p> - <?php if ($block->canCreateShippingLabel()) : ?> + <?php if ($block->canCreateShippingLabel()): ?> <?= /* @noEscape */ $block->getCreateLabelButton(); ?> <?php endif ?> - <?php if ($block->getShipment()->getShippingLabel()) : ?> + <?php if ($block->getShipment()->getShippingLabel()): ?> <?= /* @noEscape */ $block->getPrintLabelButton(); ?> <?php endif ?> - <?php if ($block->getShipment()->getPackages()) : ?> + <?php if ($block->getShipment()->getPackages()): ?> <?= /* @noEscape */ $block->getShowPackagesButton(); ?> <?php endif ?> </p> <?= $block->getChildHtml('shipment_tracking'); ?> <?= $block->getChildHtml('shipment_packaging'); ?> - <script> + <?php $scriptString = <<<script + require([ 'jquery', 'prototype' @@ -85,7 +98,10 @@ $order = $block->getShipment()->getOrder(); window.packaging.sendCreateLabelRequest(); }); window.packaging.setLabelCreatedCallback(function () { - setLocation("<?= $block->escapeUrl($block->getUrl('adminhtml/order_shipment/view', ['shipment_id' => $block->getShipment()->getId()])); ?>"); + setLocation("{$block->escapeJs($block->getUrl( + 'adminhtml/order_shipment/view', + ['shipment_id' => $block->getShipment()->getId()] + ))}"); }); }; @@ -95,7 +111,10 @@ $order = $block->getShipment()->getOrder(); jQuery(document).on('packaging:inited', setCallbacks); } }); - </script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> </div> </div> diff --git a/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml b/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml index 1e8760b3afd6d..925dbd03db8e0 100644 --- a/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml +++ b/app/code/Magento/Shipping/view/frontend/templates/tracking/popup.phtml @@ -6,19 +6,23 @@ use Magento\Framework\View\Element\Template; -/** @var $block \Magento\Shipping\Block\Tracking\Popup */ -//phpcs:disable Magento2.Files.LineLength.MaxExceeded +/** + * @var $block \Magento\Shipping\Block\Tracking\Popup + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $results = $block->getTrackingInfo(); ?> <div class="page tracking"> - <?php if (!empty($results)) : ?> - <?php foreach ($results as $shipId => $result) : ?> - <?php if ($shipId) : ?> - <div class="order subtitle caption"><?= /* @noEscape */ $block->escapeHtml(__('Shipment #')) . $shipId ?></div> + <?php if (!empty($results)): ?> + <?php foreach ($results as $shipId => $result): ?> + <?php if ($shipId): ?> + <div class="order subtitle caption"> + <?= /* @noEscape */ $block->escapeHtml(__('Shipment #')) . $shipId ?> + </div> <?php endif; ?> - <?php if (!empty($result)) : ?> - <?php foreach ($result as $counter => $track) : ?> + <?php if (!empty($result)): ?> + <?php foreach ($result as $counter => $track): ?> <div class="table-wrapper"> <?php $shipmentBlockIdentifier = $shipId . '.' . $counter; @@ -28,25 +32,28 @@ $results = $block->getTrackingInfo(); 'storeSupportEmail' => $block->getStoreSupportEmail() ]); ?> - <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.details.' . $shipmentBlockIdentifier) ?> + <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.details.' . + $shipmentBlockIdentifier) ?> </div> - <?php if (is_object($track) && !empty($track->getProgressdetail())) : ?> + <?php if (is_object($track) && !empty($track->getProgressdetail())): ?> <?php - $block->addChild('shipping.tracking.progress.' . $shipmentBlockIdentifier, Template::class, [ - 'track' => $track, - 'template' => 'Magento_Shipping::tracking/progress.phtml' - ]); + $block->addChild( + 'shipping.tracking.progress.' . $shipmentBlockIdentifier, + Template::class, + ['track' => $track, 'template' => 'Magento_Shipping::tracking/progress.phtml'] + ); ?> - <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.progress.' . $shipmentBlockIdentifier) ?> + <?= /* @noEscape */ $block->getChildHtml('shipping.tracking.progress.' . + $shipmentBlockIdentifier) ?> <?php endif; ?> <?php endforeach; ?> - <?php else : ?> + <?php else: ?> <div class="message info empty"> <div><?= $block->escapeHtml(__('There is no tracking available for this shipment.')) ?></div> </div> <?php endif; ?> <?php endforeach; ?> - <?php else : ?> + <?php else: ?> <div class="message info empty"> <div><?= $block->escapeHtml(__('There is no tracking available.')) ?></div> </div> @@ -54,13 +61,18 @@ $results = $block->getTrackingInfo(); <div class="actions"> <button type="button" title="<?= $block->escapeHtml(__('Close Window')) ?>" - class="action close" - onclick="window.close(); window.opener.focus();"> + class="action close"> <span><?= $block->escapeHtml(__('Close Window')) ?></span> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.close(); window.opener.focus();", + 'button.action.close' + ) ?> </div> </div> -<script> +<?php $scriptString = <<<script + require([ 'jquery' ], function (jQuery) { @@ -69,4 +81,7 @@ $results = $block->getTrackingInfo(); jQuery('.actions button.close').hide(); } }); -</script> \ No newline at end of file + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Sitemap/Block/Robots.php b/app/code/Magento/Sitemap/Block/Robots.php index ac99b2ab1cd4a..a074e95ce2f80 100644 --- a/app/code/Magento/Sitemap/Block/Robots.php +++ b/app/code/Magento/Sitemap/Block/Robots.php @@ -11,6 +11,8 @@ use Magento\Robots\Model\Config\Value; use Magento\Sitemap\Helper\Data as SitemapHelper; use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; +use Magento\Sitemap\Model\SitemapConfigReader; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\StoreResolver; @@ -18,7 +20,7 @@ * Prepares sitemap links to add to the robots.txt file * * @api - * @since 100.2.0 + * @since 100.1.5 */ class Robots extends AbstractBlock implements IdentityInterface { @@ -29,6 +31,7 @@ class Robots extends AbstractBlock implements IdentityInterface /** * @var SitemapHelper + * @deprecated */ private $sitemapHelper; @@ -37,6 +40,11 @@ class Robots extends AbstractBlock implements IdentityInterface */ private $storeManager; + /** + * @var SitemapConfigReader + */ + private $sitemapConfigReader; + /** * @param Context $context * @param StoreResolver $storeResolver @@ -44,7 +52,7 @@ class Robots extends AbstractBlock implements IdentityInterface * @param SitemapHelper $sitemapHelper * @param StoreManagerInterface $storeManager * @param array $data - * + * @param SitemapConfigReader|null $sitemapConfigReader * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( @@ -53,11 +61,14 @@ public function __construct( CollectionFactory $sitemapCollectionFactory, SitemapHelper $sitemapHelper, StoreManagerInterface $storeManager, - array $data = [] + array $data = [], + ?SitemapConfigReader $sitemapConfigReader = null ) { $this->sitemapCollectionFactory = $sitemapCollectionFactory; $this->sitemapHelper = $sitemapHelper; $this->storeManager = $storeManager; + $this->sitemapConfigReader = $sitemapConfigReader + ?: ObjectManager::getInstance()->get(SitemapConfigReader::class); parent::__construct($context, $data); } @@ -70,26 +81,20 @@ public function __construct( * and adds links for this sitemap files into result data. * * @return string - * @since 100.2.0 + * @since 100.1.5 */ protected function _toHtml() { - $defaultStore = $this->storeManager->getDefaultStoreView(); - - /** @var \Magento\Store\Model\Website $website */ - $website = $this->storeManager->getWebsite($defaultStore->getWebsiteId()); + $website = $this->storeManager->getWebsite(); $storeIds = []; foreach ($website->getStoreIds() as $storeId) { - if ((bool)$this->sitemapHelper->getEnableSubmissionRobots($storeId)) { - $storeIds[] = (int)$storeId; + if ((bool) $this->sitemapConfigReader->getEnableSubmissionRobots($storeId)) { + $storeIds[] = (int) $storeId; } } - $links = []; - if ($storeIds) { - $links = array_merge($links, $this->getSitemapLinks($storeIds)); - } + $links = $storeIds ? $this->getSitemapLinks($storeIds) : []; return $links ? implode(PHP_EOL, $links) . PHP_EOL : ''; } @@ -102,22 +107,16 @@ protected function _toHtml() * * @param int[] $storeIds * @return array - * @since 100.2.0 + * @since 100.1.5 */ protected function getSitemapLinks(array $storeIds) { - $sitemapLinks = []; - - /** @var \Magento\Sitemap\Model\ResourceModel\Sitemap\Collection $collection */ $collection = $this->sitemapCollectionFactory->create(); $collection->addStoreFilter($storeIds); + $sitemapLinks = []; foreach ($collection as $sitemap) { - /** @var \Magento\Sitemap\Model\Sitemap $sitemap */ - $sitemapFilename = $sitemap->getSitemapFilename(); - $sitemapPath = $sitemap->getSitemapPath(); - - $sitemapUrl = $sitemap->getSitemapUrl($sitemapPath, $sitemapFilename); + $sitemapUrl = $sitemap->getSitemapUrl($sitemap->getSitemapPath(), $sitemap->getSitemapFilename()); $sitemapLinks[$sitemapUrl] = 'Sitemap: ' . $sitemapUrl; } @@ -128,7 +127,7 @@ protected function getSitemapLinks(array $storeIds) * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.5 */ public function getIdentities() { diff --git a/app/code/Magento/Sitemap/Helper/Data.php b/app/code/Magento/Sitemap/Helper/Data.php index 44661bbef888e..118aeff28a14f 100644 --- a/app/code/Magento/Sitemap/Helper/Data.php +++ b/app/code/Magento/Sitemap/Helper/Data.php @@ -12,7 +12,7 @@ use Magento\Store\Model\ScopeInterface; /** - * @deprecated + * @deprecated 100.3.0 */ class Data extends \Magento\Framework\App\Helper\AbstractHelper { @@ -70,7 +70,7 @@ class Data extends \Magento\Framework\App\Helper\AbstractHelper * * @param int $storeId * @return int - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getMaximumLinesNumber() */ public function getMaximumLinesNumber($storeId) @@ -87,7 +87,7 @@ public function getMaximumLinesNumber($storeId) * * @param int $storeId * @return int - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getMaximumFileSize() */ public function getMaximumFileSize($storeId) @@ -104,7 +104,7 @@ public function getMaximumFileSize($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CategoryConfigReader::getChangeFrequency() */ public function getCategoryChangefreq($storeId) @@ -121,7 +121,7 @@ public function getCategoryChangefreq($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see ProductConfigReader::getChangeFrequency() */ public function getProductChangefreq($storeId) @@ -138,7 +138,7 @@ public function getProductChangefreq($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CmsPageConfigReader::getChangeFrequency() */ public function getPageChangefreq($storeId) @@ -155,7 +155,7 @@ public function getPageChangefreq($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CategoryConfigReader::getPriority() */ public function getCategoryPriority($storeId) @@ -172,7 +172,7 @@ public function getCategoryPriority($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see ProductConfigReader::getPriority() */ public function getProductPriority($storeId) @@ -189,7 +189,7 @@ public function getProductPriority($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see CmsPageConfigReader::getPriority() */ public function getPagePriority($storeId) @@ -206,7 +206,7 @@ public function getPagePriority($storeId) * * @param int $storeId * @return int - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getEnableSubmissionRobots() */ public function getEnableSubmissionRobots($storeId) @@ -223,7 +223,7 @@ public function getEnableSubmissionRobots($storeId) * * @param int $storeId * @return string - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getProductImageIncludePolicy() */ public function getProductImageIncludePolicy($storeId) @@ -239,7 +239,7 @@ public function getProductImageIncludePolicy($storeId) * Get list valid paths for generate a sitemap XML file * * @return string[] - * @deprecated + * @deprecated 100.3.0 * @see SitemapConfigReader::getValidPaths() */ public function getValidPaths() diff --git a/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php b/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php index 1e8b545728a04..6c8ff087aeb60 100644 --- a/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php +++ b/app/code/Magento/Sitemap/Model/ItemProvider/ConfigReaderInterface.php @@ -10,6 +10,7 @@ * Item resolver config reader interface * * @api + * @since 100.3.0 */ interface ConfigReaderInterface { @@ -18,6 +19,7 @@ interface ConfigReaderInterface * * @param int $storeId * @return string + * @since 100.3.0 */ public function getPriority($storeId); @@ -26,6 +28,7 @@ public function getPriority($storeId); * * @param int $storeId * @return string + * @since 100.3.0 */ public function getChangeFrequency($storeId); } diff --git a/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php b/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php index 89ad2afdd01a2..da56f86b7237c 100644 --- a/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php +++ b/app/code/Magento/Sitemap/Model/ItemProvider/ItemProviderInterface.php @@ -11,6 +11,7 @@ * Sitemap item provider interface * * @api + * @since 100.3.0 */ interface ItemProviderInterface { @@ -19,6 +20,7 @@ interface ItemProviderInterface * * @param int $storeId * @return SitemapItemInterface[] + * @since 100.3.0 */ public function getItems($storeId); } diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php index 8b2154e6ee47a..dc15819b087b2 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Catalog/Product.php @@ -474,6 +474,7 @@ protected function _getMediaConfig() * * @param \Magento\Framework\DB\Select $select * @return \Magento\Framework\DB\Select + * @since 100.2.1 */ public function prepareSelectStatement(\Magento\Framework\DB\Select $select) { diff --git a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php index 01addd0c19666..92cbcbd500e8a 100644 --- a/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php +++ b/app/code/Magento/Sitemap/Model/ResourceModel/Cms/Page.php @@ -38,7 +38,6 @@ class Page extends AbstractDb /** * @var GetUtilityPageIdentifiersInterface - * @since 100.2.0 */ private $getUtilityPageIdentifiers; diff --git a/app/code/Magento/Sitemap/Model/Sitemap.php b/app/code/Magento/Sitemap/Model/Sitemap.php index 21839e1057125..9a8d2c57a280c 100644 --- a/app/code/Magento/Sitemap/Model/Sitemap.php +++ b/app/code/Magento/Sitemap/Model/Sitemap.php @@ -160,7 +160,7 @@ class Sitemap extends \Magento\Framework\Model\AbstractModel implements \Magento /** * @inheritdoc * - * @since 100.2.0 + * @since 100.1.5 */ protected $_cacheTag = [Value::CACHE_TAG]; @@ -297,8 +297,9 @@ protected function _getStream() * * @param DataObject $sitemapItem * @return $this - * @deprecated 100.2.0 + * @deprecated 100.3.0 * @see ItemProviderInterface + * @since 100.2.0 */ public function addSitemapItem(DataObject $sitemapItem) { @@ -311,8 +312,9 @@ public function addSitemapItem(DataObject $sitemapItem) * Collect all sitemap items * * @return void - * @deprecated 100.2.0 + * @deprecated 100.3.0 * @see ItemProviderInterface + * @since 100.2.0 */ public function collectSitemapItems() { @@ -723,7 +725,7 @@ protected function _getUrl($url, $type = UrlInterface::URL_TYPE_LINK) * * @param string $url * @return string - * @deprecated No longer used, as we're generating product image URLs inside collection instead + * @deprecated 100.2.0 No longer used, as we're generating product image URLs inside collection instead * @see \Magento\Sitemap\Model\ResourceModel\Catalog\Product::_loadProductImages() */ protected function _getMediaUrl($url) @@ -807,7 +809,7 @@ public function getSitemapUrl($sitemapPath, $sitemapFileName) * Check is enabled submission to robots.txt * * @return bool - * @deprecated Because the robots.txt file is not generated anymore, + * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. */ protected function _isEnabledSubmissionRobots() @@ -821,7 +823,7 @@ protected function _isEnabledSubmissionRobots() * * @param string $sitemapFileName * @return void - * @deprecated Because the robots.txt file is not generated anymore, + * @deprecated 100.1.5 Because the robots.txt file is not generated anymore, * this method is not needed and will be removed in major release. */ protected function _addSitemapToRobotsTxt($sitemapFileName) @@ -891,7 +893,7 @@ private function mapToSitemapItem() * Get unique page cache identities * * @return array - * @since 100.2.0 + * @since 100.1.5 */ public function getIdentities() { diff --git a/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php b/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php index f094b8856ab14..f11b54c5842f8 100644 --- a/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php +++ b/app/code/Magento/Sitemap/Model/SitemapConfigReaderInterface.php @@ -10,6 +10,7 @@ * Sitemap config reader interface * * @api + * @since 100.3.0 */ interface SitemapConfigReaderInterface { @@ -18,6 +19,7 @@ interface SitemapConfigReaderInterface * * @param int $storeId * @return int + * @since 100.3.0 */ public function getEnableSubmissionRobots($storeId); @@ -26,6 +28,7 @@ public function getEnableSubmissionRobots($storeId); * * @param int $storeId * @return int + * @since 100.3.0 */ public function getMaximumFileSize($storeId); @@ -34,6 +37,7 @@ public function getMaximumFileSize($storeId); * * @param int $storeId * @return int + * @since 100.3.0 */ public function getMaximumLinesNumber($storeId); @@ -42,6 +46,7 @@ public function getMaximumLinesNumber($storeId); * * @param int $storeId * @return string + * @since 100.3.0 */ public function getProductImageIncludePolicy($storeId); @@ -49,6 +54,7 @@ public function getProductImageIncludePolicy($storeId); * Get list valid paths for generate a sitemap XML file * * @return string[] + * @since 100.3.0 */ public function getValidPaths(); } diff --git a/app/code/Magento/Sitemap/Model/SitemapItemInterface.php b/app/code/Magento/Sitemap/Model/SitemapItemInterface.php index afd95768a2c84..94f19c5726b13 100644 --- a/app/code/Magento/Sitemap/Model/SitemapItemInterface.php +++ b/app/code/Magento/Sitemap/Model/SitemapItemInterface.php @@ -10,6 +10,7 @@ * Representation of sitemap item * * @api + * @since 100.3.0 */ interface SitemapItemInterface { @@ -18,6 +19,7 @@ interface SitemapItemInterface * Get url * * @return string + * @since 100.3.0 */ public function getUrl(); @@ -25,6 +27,7 @@ public function getUrl(); * Get priority * * @return string + * @since 100.3.0 */ public function getPriority(); @@ -32,6 +35,7 @@ public function getPriority(); * Get change frequency * * @return string + * @since 100.3.0 */ public function getChangeFrequency(); @@ -39,6 +43,7 @@ public function getChangeFrequency(); * Get images * * @return array|null + * @since 100.3.0 */ public function getImages(); @@ -46,6 +51,7 @@ public function getImages(); * Get last update date * * @return string|null + * @since 100.3.0 */ public function getUpdatedAt(); } diff --git a/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php b/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php index 0ddac1bb98fd1..3513fe32cd9b8 100644 --- a/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php +++ b/app/code/Magento/Sitemap/Test/Unit/Block/RobotsTest.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Sitemap\Test\Unit\Block; @@ -10,49 +11,35 @@ use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\DataObject; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\View\Element\Context; use Magento\Robots\Model\Config\Value; use Magento\Sitemap\Block\Robots; -use Magento\Sitemap\Helper\Data; use Magento\Sitemap\Model\ResourceModel\Sitemap\Collection; use Magento\Sitemap\Model\ResourceModel\Sitemap\CollectionFactory; use Magento\Sitemap\Model\Sitemap; +use Magento\Sitemap\Model\SitemapConfigReader; use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; -use Magento\Store\Model\StoreResolver; use Magento\Store\Model\Website; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; /** + * Test for \Magento\Sitemap\Block\Robots. * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class RobotsTest extends TestCase { - /** - * @var Context|MockObject - */ - private $context; - - /** - * @var StoreResolver|MockObject - */ - private $storeResolver; - /** * @var CollectionFactory|MockObject */ private $sitemapCollectionFactory; - /** - * @var Data|MockObject - */ - private $sitemapHelper; - /** * @var Robots */ - private $block; + private $model; /** * @var ManagerInterface|MockObject @@ -69,147 +56,112 @@ class RobotsTest extends TestCase */ private $storeManager; + /** + * @var SitemapConfigReader|MockObject + */ + private $siteMapConfigReader; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->eventManagerMock = $this->getMockBuilder(ManagerInterface::class) - ->getMockForAbstractClass(); - - $this->scopeConfigMock = $this->getMockBuilder(ScopeConfigInterface::class) - ->getMockForAbstractClass(); + $objectManager = new ObjectManager($this); - $this->context = $this->getMockBuilder(Context::class) - ->disableOriginalConstructor() - ->getMock(); + $this->eventManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $this->scopeConfigMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $this->context->expects($this->any()) + $context = $this->createMock(Context::class); + $context->expects($this->any()) ->method('getEventManager') ->willReturn($this->eventManagerMock); - - $this->context->expects($this->any()) + $context->expects($this->any()) ->method('getScopeConfig') ->willReturn($this->scopeConfigMock); - $this->storeResolver = $this->getMockBuilder(StoreResolver::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->sitemapCollectionFactory = $this->getMockBuilder( - CollectionFactory::class - ) - ->setMethods(['create']) - ->disableOriginalConstructor() - ->getMock(); - - $this->sitemapHelper = $this->getMockBuilder(Data::class) - ->disableOriginalConstructor() - ->getMock(); - - $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->getMockForAbstractClass(); - - $this->block = new Robots( - $this->context, - $this->storeResolver, - $this->sitemapCollectionFactory, - $this->sitemapHelper, - $this->storeManager + $this->sitemapCollectionFactory = $this->createMock(CollectionFactory::class); + $this->storeManager = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->siteMapConfigReader = $this->createMock(SitemapConfigReader::class); + + $this->model = $objectManager->getObject( + Robots::class, + [ + 'context' => $context, + 'sitemapCollectionFactory' => $this->sitemapCollectionFactory, + 'storeManager' => $this->storeManager, + 'sitemapConfigReader' => $this->siteMapConfigReader + ] ); } /** * Check toHtml() method in case when robots submission is disabled + * + * @return void */ - public function testToHtmlRobotsSubmissionIsDisabled() + public function testToHtmlRobotsSubmissionIsDisabled(): void { $defaultStoreId = 1; - $defaultWebsiteId = 1; - $expected = ''; $this->initEventManagerMock($expected); - $this->scopeConfigMock->expects($this->once())->method('getValue')->willReturn(false); - - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->getMockForAbstractClass(); - - $storeMock->expects($this->once()) - ->method('getWebsiteId') - ->willReturn($defaultWebsiteId); - - $this->storeManager->expects($this->once()) - ->method('getDefaultStoreView') - ->willReturn($storeMock); - - $storeMock->expects($this->any()) - ->method('getWebsiteId') - ->willReturn($defaultWebsiteId); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(false); - $websiteMock = $this->getMockBuilder(Website::class) - ->disableOriginalConstructor() - ->getMock(); - $websiteMock->expects($this->any()) + $websiteMock = $this->createMock(Website::class); + $websiteMock->expects($this->once()) ->method('getStoreIds') ->willReturn([$defaultStoreId]); $this->storeManager->expects($this->once()) ->method('getWebsite') - ->with($defaultWebsiteId) + ->with(null) ->willReturn($websiteMock); - $this->sitemapHelper->expects($this->once()) + $this->siteMapConfigReader->expects($this->once()) ->method('getEnableSubmissionRobots') ->with($defaultStoreId) ->willReturn(false); - $this->assertEquals($expected, $this->block->toHtml()); + $this->assertEquals($expected, $this->model->toHtml()); } /** * Check toHtml() method in case when robots submission is enabled + * + * @return void */ - public function testAfterGetDataRobotsSubmissionIsEnabled() + public function testAfterGetDataRobotsSubmissionIsEnabled(): void { $defaultStoreId = 1; $secondStoreId = 2; - $defaultWebsiteId = 1; $sitemapPath = '/'; $sitemapFilenameOne = 'sitemap.xml'; $sitemapFilenameTwo = 'sitemap_custom.xml'; $sitemapFilenameThree = 'sitemap.xml'; - $expected = 'Sitemap: ' . $sitemapFilenameOne - . PHP_EOL - . 'Sitemap: ' . $sitemapFilenameTwo - . PHP_EOL; + $expected = 'Sitemap: ' . $sitemapFilenameOne . PHP_EOL . 'Sitemap: ' . $sitemapFilenameTwo . PHP_EOL; $this->initEventManagerMock($expected); - $this->scopeConfigMock->expects($this->once())->method('getValue')->willReturn(false); - - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->getMockForAbstractClass(); - - $this->storeManager->expects($this->once()) - ->method('getDefaultStoreView') - ->willReturn($storeMock); - - $storeMock->expects($this->any()) - ->method('getWebsiteId') - ->willReturn($defaultWebsiteId); + $this->scopeConfigMock->expects($this->once()) + ->method('getValue') + ->willReturn(false); $websiteMock = $this->getMockBuilder(Website::class) ->disableOriginalConstructor() ->getMock(); - $websiteMock->expects($this->any()) + $websiteMock->expects($this->once()) ->method('getStoreIds') ->willReturn([$defaultStoreId, $secondStoreId]); $this->storeManager->expects($this->once()) ->method('getWebsite') - ->with($defaultWebsiteId) + ->with(null) ->willReturn($websiteMock); - $this->sitemapHelper->expects($this->any()) + $this->siteMapConfigReader->expects($this->atLeastOnce()) ->method('getEnableSubmissionRobots') ->willReturnMap([ [$defaultStoreId, true], @@ -223,12 +175,12 @@ public function testAfterGetDataRobotsSubmissionIsEnabled() $sitemapCollectionMock = $this->getMockBuilder(Collection::class) ->disableOriginalConstructor() ->getMock(); - $sitemapCollectionMock->expects($this->any()) + $sitemapCollectionMock->expects($this->once()) ->method('addStoreFilter') ->with([$defaultStoreId]) ->willReturnSelf(); - $sitemapCollectionMock->expects($this->any()) + $sitemapCollectionMock->expects($this->once()) ->method('getIterator') ->willReturn(new \ArrayIterator([$sitemapMockOne, $sitemapMockTwo, $sitemapMockThree])); @@ -236,18 +188,19 @@ public function testAfterGetDataRobotsSubmissionIsEnabled() ->method('create') ->willReturn($sitemapCollectionMock); - $this->assertEquals($expected, $this->block->toHtml()); + $this->assertEquals($expected, $this->model->toHtml()); } /** * Check that getIdentities() method returns specified cache tag + * + * @return void */ - public function testGetIdentities() + public function testGetIdentities(): void { $storeId = 1; - $storeMock = $this->getMockBuilder(StoreInterface::class) - ->getMockForAbstractClass(); + $storeMock = $this->getMockForAbstractClass(StoreInterface::class); $this->storeManager->expects($this->once()) ->method('getDefaultStoreView') @@ -257,10 +210,8 @@ public function testGetIdentities() ->method('getId') ->willReturn($storeId); - $expected = [ - Value::CACHE_TAG . '_' . $storeId, - ]; - $this->assertEquals($expected, $this->block->getIdentities()); + $expected = [Value::CACHE_TAG . '_' . $storeId]; + $this->assertEquals($expected, $this->model->getIdentities()); } /** @@ -269,23 +220,18 @@ public function testGetIdentities() * @param string $data * @return void */ - protected function initEventManagerMock($data) + protected function initEventManagerMock($data): void { $this->eventManagerMock->expects($this->any()) ->method('dispatch') ->willReturnMap([ [ 'view_block_abstract_to_html_before', - [ - 'block' => $this->block, - ], + ['block' => $this->model], ], [ 'view_block_abstract_to_html_after', - [ - 'block' => $this->block, - 'transport' => new DataObject(['html' => $data]), - ], + ['block' => $this->model, 'transport' => new DataObject(['html' => $data])], ], ]); } @@ -297,15 +243,12 @@ protected function initEventManagerMock($data) * @param string $sitemapFilename * @return MockObject */ - protected function getSitemapMock($sitemapPath, $sitemapFilename) + protected function getSitemapMock($sitemapPath, $sitemapFilename): MockObject { $sitemapMock = $this->getMockBuilder(Sitemap::class) ->disableOriginalConstructor() - ->setMethods([ - 'getSitemapFilename', - 'getSitemapPath', - 'getSitemapUrl', - ]) + ->onlyMethods(['getSitemapUrl']) + ->addMethods(['getSitemapFilename', 'getSitemapPath']) ->getMock(); $sitemapMock->expects($this->any()) diff --git a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php index 537fec4c75df6..8f6011f1ae56f 100644 --- a/app/code/Magento/Store/Api/Data/StoreConfigInterface.php +++ b/app/code/Magento/Store/Api/Data/StoreConfigInterface.php @@ -6,7 +6,7 @@ namespace Magento\Store\Api\Data; /** - * StoreConfig interface + * Interface for store config * * @api * @since 100.0.2 @@ -141,7 +141,7 @@ public function setWeightUnit($weightUnit); public function getBaseUrl(); /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -201,7 +201,7 @@ public function setBaseMediaUrl($baseMediaUrl); public function getSecureBaseUrl(); /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this diff --git a/app/code/Magento/Store/Api/Data/StoreInterface.php b/app/code/Magento/Store/Api/Data/StoreInterface.php index 0f724a23fc096..527a7038e261a 100644 --- a/app/code/Magento/Store/Api/Data/StoreInterface.php +++ b/app/code/Magento/Store/Api/Data/StoreInterface.php @@ -69,11 +69,13 @@ public function getStoreGroupId(); /** * @param int $isActive * @return $this + * @since 101.0.0 */ public function setIsActive($isActive); /** * @return int + * @since 101.0.0 */ public function getIsActive(); diff --git a/app/code/Magento/Store/Api/StoreResolverInterface.php b/app/code/Magento/Store/Api/StoreResolverInterface.php index 7c32e321fa6c4..d03d68d213135 100644 --- a/app/code/Magento/Store/Api/StoreResolverInterface.php +++ b/app/code/Magento/Store/Api/StoreResolverInterface.php @@ -8,7 +8,7 @@ /** * Store resolver interface * - * @deprecated + * @deprecated 101.0.0 * @see \Magento\Store\Model\StoreManagerInterface */ interface StoreResolverInterface diff --git a/app/code/Magento/Store/App/Response/Redirect.php b/app/code/Magento/Store/App/Response/Redirect.php index da0c49aa1bc11..7984939108d89 100644 --- a/app/code/Magento/Store/App/Response/Redirect.php +++ b/app/code/Magento/Store/App/Response/Redirect.php @@ -7,36 +7,55 @@ */ namespace Magento\Store\App\Response; +use Laminas\Uri\Uri; +use Magento\Framework\App\ActionInterface; +use Magento\Framework\App\Area; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\RequestInterface; +use Magento\Framework\App\Response\RedirectInterface; +use Magento\Framework\App\ResponseInterface; +use Magento\Framework\App\State; +use Magento\Framework\Encryption\UrlCoder; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Session\SessionManagerInterface; +use Magento\Framework\Session\SidResolverInterface; +use Magento\Framework\UrlInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; /** * Class Redirect computes redirect urls responses. * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Redirect implements \Magento\Framework\App\Response\RedirectInterface +class Redirect implements RedirectInterface { + private const XML_PATH_USE_CUSTOM_ADMIN_URL = 'admin/url/use_custom'; + private const XML_PATH_CUSTOM_ADMIN_URL = 'admin/url/custom'; + /** - * @var \Magento\Framework\App\RequestInterface + * @var RequestInterface */ protected $_request; /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\Encryption\UrlCoder + * @var UrlCoder */ protected $_urlCoder; /** - * @var \Magento\Framework\Session\SessionManagerInterface + * @var SessionManagerInterface */ protected $_session; /** - * @var \Magento\Framework\Session\SidResolverInterface + * @var SidResolverInterface */ protected $_sidResolver; @@ -46,36 +65,51 @@ class Redirect implements \Magento\Framework\App\Response\RedirectInterface protected $_canUseSessionIdInParam; /** - * @var \Magento\Framework\UrlInterface + * @var UrlInterface */ protected $_urlBuilder; /** - * @var \Laminas\Uri\Uri|null + * @var Uri */ private $uri; + /** + * @var State + */ + private $appState; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** * Constructor * - * @param \Magento\Framework\App\RequestInterface $request - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\Encryption\UrlCoder $urlCoder - * @param \Magento\Framework\Session\SessionManagerInterface $session - * @param \Magento\Framework\Session\SidResolverInterface $sidResolver - * @param \Magento\Framework\UrlInterface $urlBuilder - * @param \Laminas\Uri\Uri|null $uri + * @param RequestInterface $request + * @param StoreManagerInterface $storeManager + * @param UrlCoder $urlCoder + * @param SessionManagerInterface $session + * @param SidResolverInterface $sidResolver + * @param UrlInterface $urlBuilder + * @param Uri|null $uri * @param bool $canUseSessionIdInParam + * @param State|null $appState + * @param ScopeConfigInterface|null $scopeConfig + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\RequestInterface $request, - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\Encryption\UrlCoder $urlCoder, - \Magento\Framework\Session\SessionManagerInterface $session, - \Magento\Framework\Session\SidResolverInterface $sidResolver, - \Magento\Framework\UrlInterface $urlBuilder, - \Laminas\Uri\Uri $uri = null, - $canUseSessionIdInParam = true + RequestInterface $request, + StoreManagerInterface $storeManager, + UrlCoder $urlCoder, + SessionManagerInterface $session, + SidResolverInterface $sidResolver, + UrlInterface $urlBuilder, + Uri $uri = null, + $canUseSessionIdInParam = true, + ?State $appState = null, + ?ScopeConfigInterface $scopeConfig = null ) { $this->_canUseSessionIdInParam = $canUseSessionIdInParam; $this->_request = $request; @@ -84,20 +118,22 @@ public function __construct( $this->_session = $session; $this->_sidResolver = $sidResolver; $this->_urlBuilder = $urlBuilder; - $this->uri = $uri ?: ObjectManager::getInstance()->get(\Laminas\Uri\Uri::class); + $this->uri = $uri ?: ObjectManager::getInstance()->get(Uri::class); + $this->appState = $appState ?: ObjectManager::getInstance()->get(State::class); + $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class); } /** * Get the referrer url. * * @return string - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ protected function _getUrl() { $refererUrl = $this->_request->getServer('HTTP_REFERER'); - $encodedUrl = $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_URL_ENCODED) - ?: $this->_request->getParam(\Magento\Framework\App\ActionInterface::PARAM_NAME_BASE64_URL); + $encodedUrl = $this->_request->getParam(ActionInterface::PARAM_NAME_URL_ENCODED) + ?: $this->_request->getParam(ActionInterface::PARAM_NAME_BASE64_URL); if ($encodedUrl) { $refererUrl = $this->_urlCoder->decode($encodedUrl); @@ -113,6 +149,7 @@ protected function _getUrl() } else { $refererUrl = $this->normalizeRefererUrl($refererUrl); } + return $refererUrl; } @@ -130,9 +167,9 @@ public function getRefererUrl() * Set referer url for redirect in response * * @param string $defaultUrl - * @return \Magento\Framework\App\ActionInterface + * @return ActionInterface * - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function getRedirectUrl($defaultUrl = null) { @@ -149,7 +186,7 @@ public function getRedirectUrl($defaultUrl = null) * @param string $defaultUrl * @return string * - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function error($defaultUrl) { @@ -160,6 +197,7 @@ public function error($defaultUrl) if (!$this->_isUrlInternal($errorUrl)) { $errorUrl = $this->_storeManager->getStore()->getBaseUrl(); } + return $errorUrl; } @@ -169,17 +207,17 @@ public function error($defaultUrl) * @param string $defaultUrl * @return string * - * @throws \Magento\Framework\Exception\NoSuchEntityException + * @throws NoSuchEntityException */ public function success($defaultUrl) { $successUrl = $this->_request->getParam(self::PARAM_NAME_SUCCESS_URL); - if (empty($successUrl)) { - $successUrl = $defaultUrl; - } + $successUrl = $successUrl ?: $defaultUrl; + if (!$this->_isUrlInternal($successUrl)) { $successUrl = $this->_storeManager->getStore()->getBaseUrl(); } + return $successUrl; } @@ -194,12 +232,12 @@ public function updatePathParams(array $arguments) /** * Set redirect into response * - * @param \Magento\Framework\App\ResponseInterface $response + * @param ResponseInterface $response * @param string $path * @param array $arguments * @return void */ - public function redirect(\Magento\Framework\App\ResponseInterface $response, $path, $arguments = []) + public function redirect(ResponseInterface $response, $path, $arguments = []) { $arguments = $this->updatePathParams($arguments); $response->setRedirect($this->_urlBuilder->getUrl($path, $arguments)); @@ -213,15 +251,69 @@ public function redirect(\Magento\Framework\App\ResponseInterface $response, $pa */ protected function _isUrlInternal($url) { - if (strpos($url, 'http') !== false) { - $directLinkType = \Magento\Framework\UrlInterface::URL_TYPE_DIRECT_LINK; - $unsecureBaseUrl = $this->_storeManager->getStore()->getBaseUrl($directLinkType, false); - $secureBaseUrl = $this->_storeManager->getStore()->getBaseUrl($directLinkType, true); - return (strpos($url, (string) $unsecureBaseUrl) === 0) || (strpos($url, (string) $secureBaseUrl) === 0); + return strpos($url, 'http') !== false + ? $this->isInternalUrl($url) || $this->isCustomAdminUrl($url) + : false; + } + + /** + * Is `Use Custom Admin URL` config enabled + * + * @return bool + */ + private function isUseCustomAdminUrlEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_USE_CUSTOM_ADMIN_URL, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Returns custom admin url + * + * @return string + */ + private function getCustomAdminUrl(): string + { + return $this->scopeConfig->getValue( + self::XML_PATH_CUSTOM_ADMIN_URL, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * Is internal custom admin url + * + * @param string $url + * @return bool + */ + private function isCustomAdminUrl(string $url): bool + { + if ($this->appState->getAreaCode() === Area::AREA_ADMINHTML && $this->isUseCustomAdminUrlEnabled()) { + return strpos($url, $this->getCustomAdminUrl()) === 0; } + return false; } + /** + * Is url internal + * + * @param string $url + * @return bool + */ + private function isInternalUrl(string $url): bool + { + $directLinkType = UrlInterface::URL_TYPE_DIRECT_LINK; + $unsecureBaseUrl = $this->_storeManager->getStore() + ->getBaseUrl($directLinkType, false); + $secureBaseUrl = $this->_storeManager->getStore() + ->getBaseUrl($directLinkType, true); + + return strpos($url, (string) $unsecureBaseUrl) === 0 || strpos($url, (string) $secureBaseUrl) === 0; + } + /** * Normalize path to avoid wrong store change * @@ -264,10 +356,10 @@ protected function normalizeRefererQueryParts($refererQuery) $store = $this->_storeManager->getStore(); if ($store - && !empty($refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME]) - && ($refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME] !== $store->getCode()) + && !empty($refererQuery[StoreManagerInterface::PARAM_NAME]) + && ($refererQuery[StoreManagerInterface::PARAM_NAME] !== $store->getCode()) ) { - $refererQuery[\Magento\Store\Model\StoreManagerInterface::PARAM_NAME] = $store->getCode(); + $refererQuery[StoreManagerInterface::PARAM_NAME] = $store->getCode(); } return $refererQuery; diff --git a/app/code/Magento/Store/Controller/Store/SwitchAction.php b/app/code/Magento/Store/Controller/Store/SwitchAction.php index 41acb1605ec7c..bea7dbbaaa5fb 100644 --- a/app/code/Magento/Store/Controller/Store/SwitchAction.php +++ b/app/code/Magento/Store/Controller/Store/SwitchAction.php @@ -36,7 +36,7 @@ class SwitchAction extends Action implements HttpGetActionInterface, HttpPostAct /** * @var HttpContext - * @deprecated + * @deprecated 100.2.5 */ protected $httpContext; @@ -47,7 +47,7 @@ class SwitchAction extends Action implements HttpGetActionInterface, HttpPostAct /** * @var StoreManagerInterface - * @deprecated + * @deprecated 100.2.5 */ protected $storeManager; diff --git a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php index 317e4bf43e42c..2cbd4aeb20d4d 100644 --- a/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php +++ b/app/code/Magento/Store/Model/Config/Importer/Processor/Create.php @@ -51,7 +51,7 @@ class Create implements ProcessorInterface /** * The event manager. * - * @deprecated logic moved inside of "afterSave" method + * @deprecated 100.2.5 logic moved inside of "afterSave" method * \Magento\Store\Model\Website::afterSave * \Magento\Store\Model\Group::afterSave * \Magento\Store\Model\Store::afterSave diff --git a/app/code/Magento/Store/Model/Config/Placeholder.php b/app/code/Magento/Store/Model/Config/Placeholder.php index be84c7f444c44..e04dc81c1359e 100644 --- a/app/code/Magento/Store/Model/Config/Placeholder.php +++ b/app/code/Magento/Store/Model/Config/Placeholder.php @@ -84,7 +84,7 @@ public function process(array $data = []) /** * Process array data recursively * - * @deprecated This method isn't used in process() implementation anymore + * @deprecated 101.0.4 This method isn't used in process() implementation anymore * * @param array &$data * @param string $path @@ -178,7 +178,7 @@ protected function _getValue($path, array $data) /** * Set array value by path * - * @deprecated This method isn't used in process() implementation anymore + * @deprecated 101.0.4 This method isn't used in process() implementation anymore * * @param array &$container * @param string $path diff --git a/app/code/Magento/Store/Model/Config/Processor/Fallback.php b/app/code/Magento/Store/Model/Config/Processor/Fallback.php index 4e8b3bca14c92..537802d312eed 100644 --- a/app/code/Magento/Store/Model/Config/Processor/Fallback.php +++ b/app/code/Magento/Store/Model/Config/Processor/Fallback.php @@ -178,20 +178,18 @@ private function getWebsiteConfig(array $websites, $id) */ private function loadScopes(): void { - $loaded = false; try { if ($this->deploymentConfig->isDbAvailable()) { $this->storeData = $this->storeResource->readAllStores(); $this->websiteData = $this->websiteResource->readAllWebsites(); - $loaded = true; + } else { + $this->storeData = $this->scopes->get('stores'); + $this->websiteData = $this->scopes->get('websites'); } } catch (TableNotFoundException $exception) { // database is empty or not setup - $loaded = false; - } - if (!$loaded) { - $this->storeData = $this->scopes->get('stores'); - $this->websiteData = $this->scopes->get('websites'); + $this->storeData = []; + $this->websiteData = []; } } } diff --git a/app/code/Magento/Store/Model/Data/StoreConfig.php b/app/code/Magento/Store/Model/Data/StoreConfig.php index 6634e2cb05bd9..e68d98b162613 100644 --- a/app/code/Magento/Store/Model/Data/StoreConfig.php +++ b/app/code/Magento/Store/Model/Data/StoreConfig.php @@ -6,7 +6,7 @@ namespace Magento\Store\Model\Data; /** - * Class StoreConfig + * Allows to get and set store config values * * @codeCoverageIgnore */ @@ -188,7 +188,7 @@ public function getBaseUrl() } /** - * set base URL + * Set base URL * * @param string $baseUrl * @return $this @@ -293,7 +293,7 @@ public function getSecureBaseUrl() } /** - * set secure base URL + * Set secure base URL * * @param string $secureBaseUrl * @return $this @@ -367,7 +367,7 @@ public function setSecureBaseMediaUrl($secureBaseMediaUrl) } /** - * {@inheritdoc} + * @inheritdoc * * @return \Magento\Store\Api\Data\StoreConfigExtensionInterface|null */ @@ -377,7 +377,7 @@ public function getExtensionAttributes() } /** - * {@inheritdoc} + * @inheritdoc * * @param \Magento\Store\Api\Data\StoreConfigExtensionInterface $extensionAttributes * @return $this diff --git a/app/code/Magento/Store/Model/Group.php b/app/code/Magento/Store/Model/Group.php index 4cd7bc93d3334..7f1e71c422251 100644 --- a/app/code/Magento/Store/Model/Group.php +++ b/app/code/Magento/Store/Model/Group.php @@ -454,6 +454,7 @@ public function afterDelete() /** * @inheritdoc + * @since 100.2.5 */ public function afterSave() { diff --git a/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php b/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php index 7fcd9ac28c3ec..19bb45de24d64 100644 --- a/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php +++ b/app/code/Magento/Store/Model/ResourceModel/StoreWebsiteRelation.php @@ -27,6 +27,8 @@ public function __construct(ResourceConnection $resource) } /** + * Get store by website id + * * @param int $websiteId * @return array */ @@ -41,4 +43,29 @@ public function getStoreByWebsiteId($websiteId) $data = $connection->fetchCol($storeSelect); return $data; } + + /** + * Get website store data + * + * @param int $websiteId + * @param bool $available + * @return array + */ + public function getWebsiteStores(int $websiteId, bool $available = false): array + { + $connection = $this->resource->getConnection(); + $storeTable = $this->resource->getTableName('store'); + $storeSelect = $connection->select()->from($storeTable)->where( + 'website_id = ?', + $websiteId + ); + + if ($available) { + $storeSelect = $storeSelect->where( + 'is_active = 1' + ); + } + + return $connection->fetchAll($storeSelect); + } } diff --git a/app/code/Magento/Store/Model/Service/StoreConfigManager.php b/app/code/Magento/Store/Model/Service/StoreConfigManager.php index b3c2208a58361..ebc73036f7e37 100644 --- a/app/code/Magento/Store/Model/Service/StoreConfigManager.php +++ b/app/code/Magento/Store/Model/Service/StoreConfigManager.php @@ -3,24 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Store\Model\Service; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Api\Data\StoreConfigInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\Data\StoreConfig; +use Magento\Store\Model\Data\StoreConfigFactory; +use Magento\Store\Model\ResourceModel\Store\CollectionFactory; +use Magento\Store\Model\Store; + class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterface { /** - * @var \Magento\Store\Model\ResourceModel\Store\CollectionFactory + * @var CollectionFactory */ protected $storeCollectionFactory; /** - * @var \Magento\Store\Model\Data\StoreConfigFactory + * @var StoreConfigFactory */ protected $storeConfigFactory; /** * Core store config * - * @var \Magento\Framework\App\Config\ScopeConfigInterface + * @var ScopeConfigInterface */ protected $scopeConfig; @@ -38,14 +47,14 @@ class StoreConfigManager implements \Magento\Store\Api\StoreConfigManagerInterfa ]; /** - * @param \Magento\Store\Model\ResourceModel\Store\CollectionFactory $storeCollectionFactory - * @param \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig - * @param \Magento\Store\Model\Data\StoreConfigFactory $storeConfigFactory + * @param CollectionFactory $storeCollectionFactory + * @param ScopeConfigInterface $scopeConfig + * @param StoreConfigFactory $storeConfigFactory */ public function __construct( - \Magento\Store\Model\ResourceModel\Store\CollectionFactory $storeCollectionFactory, - \Magento\Framework\App\Config\ScopeConfigInterface $scopeConfig, - \Magento\Store\Model\Data\StoreConfigFactory $storeConfigFactory + CollectionFactory $storeCollectionFactory, + ScopeConfigInterface $scopeConfig, + StoreConfigFactory $storeConfigFactory ) { $this->storeCollectionFactory = $storeCollectionFactory; $this->scopeConfig = $scopeConfig; @@ -53,8 +62,10 @@ public function __construct( } /** + * Get store configurations + * * @param string[] $storeCodes list of stores by store codes, will return all if storeCodes is not set - * @return \Magento\Store\Api\Data\StoreConfigInterface[] + * @return StoreConfigInterface[] */ public function getStoreConfigs(array $storeCodes = null) { @@ -71,12 +82,14 @@ public function getStoreConfigs(array $storeCodes = null) } /** - * @param \Magento\Store\Model\Store $store - * @return \Magento\Store\Api\Data\StoreConfigInterface + * Get store specific configs + * + * @param Store|StoreInterface $store + * @return StoreConfigInterface */ protected function getStoreConfig($store) { - /** @var \Magento\Store\Model\Data\StoreConfig $storeConfig */ + /** @var StoreConfig $storeConfig */ $storeConfig = $this->storeConfigFactory->create(); $storeConfig->setId($store->getId()) diff --git a/app/code/Magento/Store/Model/Store.php b/app/code/Magento/Store/Model/Store.php index 5187bb8776632..7bcb3282ba552 100644 --- a/app/code/Magento/Store/Model/Store.php +++ b/app/code/Magento/Store/Model/Store.php @@ -208,7 +208,7 @@ class Store extends AbstractExtensibleModel implements * Flag that shows that backend URLs are secure * * @var boolean|null - * @deprecated unused protected property + * @deprecated 101.0.0 unused protected property */ protected $_isAdminSecure = null; @@ -278,7 +278,7 @@ class Store extends AbstractExtensibleModel implements /** * @var \Magento\Framework\Session\SidResolverInterface - * @deprecated Not used anymore. + * @deprecated 101.0.5 Not used anymore. */ protected $_sidResolver; @@ -1134,6 +1134,7 @@ public function setStoreGroupId($storeGroupId) /** * @inheritdoc + * @since 101.0.0 */ public function getIsActive() { @@ -1142,6 +1143,7 @@ public function getIsActive() /** * @inheritdoc + * @since 101.0.0 */ public function setIsActive($isActive) { @@ -1389,6 +1391,7 @@ public function getStorePath() /** * @inheritdoc + * @since 100.1.0 */ public function getScopeType() { @@ -1397,6 +1400,7 @@ public function getScopeType() /** * @inheritdoc + * @since 100.1.0 */ public function getScopeTypeName() { diff --git a/app/code/Magento/Store/Model/StoreResolver.php b/app/code/Magento/Store/Model/StoreResolver.php index aafdd15138981..2a950b699abe7 100644 --- a/app/code/Magento/Store/Model/StoreResolver.php +++ b/app/code/Magento/Store/Model/StoreResolver.php @@ -28,12 +28,12 @@ class StoreResolver implements \Magento\Store\Api\StoreResolverInterface protected $storeCookieManager; /** - * @deprecated + * @deprecated 101.0.0 */ protected $cache; /** - * @deprecated + * @deprecated 101.0.0 */ protected $readerList; @@ -142,7 +142,7 @@ protected function getStoresData() : array * Read stores data. First element is allowed store ids, second is default store id * * @return array - * @deprecated + * @deprecated 101.0.0 * @see \Magento\Store\Model\StoreResolver::getStoresData */ protected function readStoresData() : array diff --git a/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php b/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php index 3328a21e8f5e1..c907ab14c0137 100644 --- a/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php +++ b/app/code/Magento/Store/Model/StoreSwitcher/CleanTargetUrl.php @@ -14,6 +14,8 @@ /** * Remove SID, from_store, store from target url. + * + * Used in store-switching process in HTML frontend. */ class CleanTargetUrl implements StoreSwitcherInterface { @@ -22,42 +24,27 @@ class CleanTargetUrl implements StoreSwitcherInterface */ private $urlHelper; - /** - * @var \Magento\Framework\Session\Generic - */ - private $session; - - /** - * @var \Magento\Framework\Session\SidResolverInterface - */ - private $sidResolver; - /** * @param UrlHelper $urlHelper - * @param \Magento\Framework\Session\Generic $session - * @param \Magento\Framework\Session\SidResolverInterface $sidResolver */ public function __construct( - UrlHelper $urlHelper, - \Magento\Framework\Session\Generic $session, - \Magento\Framework\Session\SidResolverInterface $sidResolver + UrlHelper $urlHelper ) { $this->urlHelper = $urlHelper; - $this->session = $session; - $this->sidResolver = $sidResolver; } /** + * Generate target URL to switch stores through other mechanism then via URL params. + * * @param StoreInterface $fromStore store where we came from * @param StoreInterface $targetStore store where to go to * @param string $redirectUrl original url requested for redirect after switching * @return string redirect url + * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function switch(StoreInterface $fromStore, StoreInterface $targetStore, string $redirectUrl): string { $targetUrl = $redirectUrl; - $sidName = $this->sidResolver->getSessionIdQueryParam($this->session); - $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, $sidName); $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, '___from_store'); $targetUrl = $this->urlHelper->removeRequestParam($targetUrl, StoreResolverInterface::PARAM_NAME); diff --git a/app/code/Magento/Store/Model/System/Store.php b/app/code/Magento/Store/Model/System/Store.php index d13781b8c146b..a56cdcc37dd54 100644 --- a/app/code/Magento/Store/Model/System/Store.php +++ b/app/code/Magento/Store/Model/System/Store.php @@ -229,6 +229,7 @@ public function getStoresStructure($isAll = false, $storeIds = [], $groupIds = [ * @param array $groupIds * @param array $websiteIds * @return array Format: array(array('value' => '<value>', 'label' => '<label>'), ...) + * @since 101.1.0 */ public function getStoreOptionsTree($isAll = false, $storeIds = [], $groupIds = [], $websiteIds = []): array { diff --git a/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php b/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php index 05e46e04b5c96..dc1932bdd8943 100644 --- a/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php +++ b/app/code/Magento/Store/Setup/Patch/Schema/InitializeStoresAndWebsites.php @@ -142,7 +142,7 @@ public function apply() /** * Get default category. * - * @deprecated 100.1.0 + * @deprecated 101.0.0 * @return DefaultCategory */ private function getDefaultCategory() diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSystemStoreOpenPageActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSystemStoreOpenPageActionGroup.xml new file mode 100644 index 0000000000000..7e898cd1d8f78 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/AdminSystemStoreOpenPageActionGroup.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminSystemStoreOpenPageActionGroup"> + <annotations> + <description>Go to admin system store page.</description> + </annotations> + <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToSystemStore"/> + <waitForPageLoad stepKey="waitForPageAdminSystemStoreLoad" /> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml new file mode 100644 index 0000000000000..4a403364a91e3 --- /dev/null +++ b/app/code/Magento/Store/Test/Mftf/ActionGroup/StorefrontSwitchStoreActionGroup.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontSwitchStoreActionGroup"> + <annotations> + <description>Switch the Storefront to the provided Store.</description> + </annotations> + <arguments> + <argument name="storeName" type="string"/> + </arguments> + <click selector="{{StorefrontFooterSection.switchStoreButton}}" stepKey="clickOnSwitchStoreButton"/> + <click selector="{{StorefrontFooterSection.storeLink(storeName)}}" stepKey="selectStoreToSwitchOn"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml index 4171aa6f08915..9de820baa93bf 100644 --- a/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml +++ b/app/code/Magento/Store/Test/Mftf/Test/AdminCreateStoreViewTest.xml @@ -33,7 +33,7 @@ </after> <!--Filter grid and see created store view--> - <amOnPage url="{{AdminSystemStorePage.url}}" stepKey="navigateToStoresIndex"/> + <actionGroup ref="AdminSystemStoreOpenPageActionGroup" stepKey="navigateToStoresIndex"/> <click selector="{{AdminStoresGridSection.resetButton}}" stepKey="resetSearchFilter"/> <fillField selector="{{AdminStoresGridSection.storeFilterTextField}}" userInput="{{customStore.name}}" stepKey="fillStoreViewFilterField"/> <click selector="{{AdminStoresGridSection.searchButton}}" stepKey="clickSearch"/> diff --git a/app/code/Magento/Store/Test/Unit/App/Response/RedirectTest.php b/app/code/Magento/Store/Test/Unit/App/Response/RedirectTest.php index 9648af9ddf61f..20879bd31ac16 100644 --- a/app/code/Magento/Store/Test/Unit/App/Response/RedirectTest.php +++ b/app/code/Magento/Store/Test/Unit/App/Response/RedirectTest.php @@ -5,110 +5,139 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Store\Test\Unit\App\Response; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Request\Http; -use Magento\Framework\Encryption\UrlCoder; -use Magento\Framework\Session\SessionManagerInterface; -use Magento\Framework\Session\SidResolverInterface; -use Magento\Framework\UrlInterface; +use Magento\Framework\App\State; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Store\App\Response\Redirect; use Magento\Store\Model\Store; use Magento\Store\Model\StoreManagerInterface; +use Magento\Framework\App\Area; +use Magento\Store\Model\ScopeInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Store\App\Response\Redirect. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class RedirectTest extends TestCase { - /** - * @var Redirect - */ - protected $_model; + private const XML_PATH_USE_CUSTOM_ADMIN_URL = 'admin/url/use_custom'; + private const XML_PATH_CUSTOM_ADMIN_URL = 'admin/url/custom'; + + private const STUB_INTERNAL_URL = 'http://internalurl.com/'; + private const STUB_EXTERNAL_URL = 'http://externalurl.com/'; + private const STUB_CUSTOM_ADMIN_URL = 'http://externalurl.com/admin/'; /** - * @var MockObject + * @var Redirect */ - protected $_requestMock; + private $model; /** - * @var MockObject + * @var Http|MockObject */ - protected $_storeManagerMock; + private $requestMock; /** - * @var MockObject + * @var StoreManagerInterface|MockObject */ - protected $_urlCoderMock; + private $storeManagerMock; /** - * @var MockObject + * @var State|MockObject */ - protected $_sessionMock; + private $appStateMock; /** - * @var MockObject + * @var ScopeConfigInterface|MockObject */ - protected $_sidResolverMock; + private $scopeConfigMock; /** - * @var MockObject + * @inheritDoc */ - protected $_urlBuilderMock; - protected function setUp(): void { - $this->_requestMock = $this->getMockBuilder(Http::class) - ->disableOriginalConstructor() - ->getMock(); - $this->_storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); - $this->_urlCoderMock = $this->createMock(UrlCoder::class); - $this->_sessionMock = $this->getMockForAbstractClass(SessionManagerInterface::class); - $this->_sidResolverMock = $this->getMockForAbstractClass(SidResolverInterface::class); - $this->_urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); - - $this->_model = new Redirect( - $this->_requestMock, - $this->_storeManagerMock, - $this->_urlCoderMock, - $this->_sessionMock, - $this->_sidResolverMock, - $this->_urlBuilderMock + $objectManager = new ObjectManager($this); + + $this->requestMock = $this->createMock(Http::class); + $this->storeManagerMock = $this->getMockForAbstractClass(StoreManagerInterface::class); + $this->appStateMock = $this->createMock(State::class); + $this->scopeConfigMock = $this->createMock(ScopeConfigInterface::class); + + $this->model = $objectManager->getObject( + Redirect::class, + [ + 'request' => $this->requestMock, + 'storeManager' => $this->storeManagerMock, + 'appState' => $this->appStateMock, + 'scopeConfig' => $this->scopeConfigMock, + ] ); } /** + * Success url test + * * @dataProvider urlAddresses - * @param string $baseUrl - * @param string $successUrl + * + * @param string $url + * @param string $area + * @param bool $isCustomAdminUrlEnabled + * @param string $expectedUrl + * @return void */ - public function testSuccessUrl($baseUrl, $successUrl) - { + public function testSuccessUrl( + string $url, + string $area, + bool $isCustomAdminUrlEnabled, + string $expectedUrl + ): void { $testStoreMock = $this->createMock(Store::class); - $testStoreMock->expects($this->any())->method('getBaseUrl')->willReturn($baseUrl); - $this->_requestMock->expects($this->any())->method('getParam')->willReturn(null); - $this->_storeManagerMock->expects($this->any())->method('getStore') + $testStoreMock->expects($this->atLeastOnce()) + ->method('getBaseUrl') + ->willReturn(self::STUB_INTERNAL_URL); + $this->requestMock->expects($this->once()) + ->method('getParam') + ->willReturn(null); + $this->storeManagerMock->expects($this->atLeastOnce()) + ->method('getStore') ->willReturn($testStoreMock); - $this->assertEquals($baseUrl, $this->_model->success($successUrl)); + $this->appStateMock->expects($this->once()) + ->method('getAreaCode') + ->willReturn($area); + $this->scopeConfigMock->expects($this->any()) + ->method('isSetFlag') + ->with(self::XML_PATH_USE_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE) + ->willReturn($isCustomAdminUrlEnabled); + $this->scopeConfigMock->expects($this->any()) + ->method('getValue') + ->with(self::XML_PATH_CUSTOM_ADMIN_URL, ScopeInterface::SCOPE_STORE) + ->willReturn(self::STUB_CUSTOM_ADMIN_URL); + + $this->assertEquals($expectedUrl, $this->model->success($url)); } /** - * DataProvider with the test urls + * Data provider for testSuccessUrlWithCustomAdminUrl * * @return array */ - public function urlAddresses() + public function urlAddresses(): array { return [ - [ - 'http://externalurl.com/', - 'http://internalurl.com/', - ], - [ - 'http://internalurl.com/', - 'http://internalurl.com/' - ] + [self::STUB_CUSTOM_ADMIN_URL, Area::AREA_ADMINHTML, true, self::STUB_CUSTOM_ADMIN_URL], + [self::STUB_CUSTOM_ADMIN_URL, Area::AREA_ADMINHTML, false, self::STUB_INTERNAL_URL], + [self::STUB_CUSTOM_ADMIN_URL, Area::AREA_FRONTEND, true, self::STUB_INTERNAL_URL], + [self::STUB_EXTERNAL_URL, Area::AREA_ADMINHTML, true, self::STUB_INTERNAL_URL], + [self::STUB_EXTERNAL_URL, Area::AREA_FRONTEND, true, self::STUB_INTERNAL_URL], ]; } } diff --git a/app/code/Magento/Store/etc/config.xml b/app/code/Magento/Store/etc/config.xml index 07e4c8b0b6529..83bb4432ac18f 100644 --- a/app/code/Magento/Store/etc/config.xml +++ b/app/code/Magento/Store/etc/config.xml @@ -132,6 +132,7 @@ <shtml>shtml</shtml> <phpt>phpt</phpt> <pht>pht</pht> + <svg>svg</svg> </protected_extensions> <public_files_valid_paths> <protected> diff --git a/app/code/Magento/Store/etc/di.xml b/app/code/Magento/Store/etc/di.xml index 5bd8f6e2349fc..2da9e91e1fddd 100644 --- a/app/code/Magento/Store/etc/di.xml +++ b/app/code/Magento/Store/etc/di.xml @@ -65,7 +65,6 @@ <preference for="Magento\Framework\App\Router\PathConfigInterface" type="Magento\Store\Model\PathConfig" /> <type name="Magento\Framework\App\ActionInterface"> <plugin name="storeCheck" type="Magento\Store\App\Action\Plugin\StoreCheck"/> - <plugin name="designLoader" type="Magento\Framework\App\Action\Plugin\LoadDesignPlugin"/> <plugin name="eventDispatch" type="Magento\Framework\App\Action\Plugin\EventDispatchPlugin"/> <plugin name="actionFlagNoDispatch" type="Magento\Framework\App\Action\Plugin\ActionFlagNoDispatchPlugin"/> </type> diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/AvailableStoresResolver.php b/app/code/Magento/StoreGraphQl/Model/Resolver/AvailableStoresResolver.php new file mode 100644 index 0000000000000..eedd7e21fa058 --- /dev/null +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/AvailableStoresResolver.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\StoreGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider; + +/** + * AvailableStores page field resolver, used for GraphQL request processing. + */ +class AvailableStoresResolver implements ResolverInterface +{ + /** + * @var StoreConfigDataProvider + */ + private $storeConfigDataProvider; + + /** + * @param StoreConfigDataProvider $storeConfigsDataProvider + */ + public function __construct( + StoreConfigDataProvider $storeConfigsDataProvider + ) { + $this->storeConfigDataProvider = $storeConfigsDataProvider; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + return $this->storeConfigDataProvider->getAvailableStoreConfig( + (int)$context->getExtensionAttributes()->getStore()->getWebsiteId() + ); + } +} diff --git a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php index 59f9831789a35..6538d87de9780 100644 --- a/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php +++ b/app/code/Magento/StoreGraphQl/Model/Resolver/Store/StoreConfigDataProvider.php @@ -8,7 +8,9 @@ namespace Magento\StoreGraphQl\Model\Resolver\Store; use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Api\Data\StoreConfigInterface; use Magento\Store\Api\StoreConfigManagerInterface; +use Magento\Store\Model\ResourceModel\StoreWebsiteRelation; use Magento\Store\Model\ScopeInterface; use Magento\Store\Api\Data\StoreInterface; @@ -32,19 +34,27 @@ class StoreConfigDataProvider */ private $extendedConfigData; + /** + * @var StoreWebsiteRelation + */ + private $storeWebsiteRelation; + /** * @param StoreConfigManagerInterface $storeConfigManager * @param ScopeConfigInterface $scopeConfig + * @param StoreWebsiteRelation $storeWebsiteRelation * @param array $extendedConfigData */ public function __construct( StoreConfigManagerInterface $storeConfigManager, ScopeConfigInterface $scopeConfig, + StoreWebsiteRelation $storeWebsiteRelation, array $extendedConfigData = [] ) { $this->storeConfigManager = $storeConfigManager; $this->scopeConfig = $scopeConfig; $this->extendedConfigData = $extendedConfigData; + $this->storeWebsiteRelation = $storeWebsiteRelation; } /** @@ -55,24 +65,42 @@ public function __construct( */ public function getStoreConfigData(StoreInterface $store): array { - $storeConfigData = array_merge( - $this->getBaseConfigData($store), - $this->getExtendedConfigData((int)$store->getId()) - ); - return $storeConfigData; + $defaultStoreConfig = $this->storeConfigManager->getStoreConfigs([$store->getCode()]); + return $this->prepareStoreConfigData(current($defaultStoreConfig), $store->getName()); } /** - * Get base config data + * Get available website stores * - * @param StoreInterface $store + * @param int $websiteId * @return array */ - private function getBaseConfigData(StoreInterface $store) : array + public function getAvailableStoreConfig(int $websiteId): array { - $storeConfig = current($this->storeConfigManager->getStoreConfigs([$store->getCode()])); + $websiteStores = $this->storeWebsiteRelation->getWebsiteStores($websiteId, true); + $storeCodes = array_column($websiteStores, 'code'); + + $storeConfigs = $this->storeConfigManager->getStoreConfigs($storeCodes); + $storesConfigData = []; + + foreach ($storeConfigs as $storeConfig) { + $key = array_search($storeConfig->getCode(), array_column($websiteStores, 'code'), true); + $storesConfigData[] = $this->prepareStoreConfigData($storeConfig, $websiteStores[$key]['name']); + } - $storeConfigData = [ + return $storesConfigData; + } + + /** + * Prepare store config data + * + * @param StoreConfigInterface $storeConfig + * @param string $storeName + * @return array + */ + private function prepareStoreConfigData(StoreConfigInterface $storeConfig, string $storeName): array + { + return array_merge([ 'id' => $storeConfig->getId(), 'code' => $storeConfig->getCode(), 'website_id' => $storeConfig->getWebsiteId(), @@ -83,14 +111,14 @@ private function getBaseConfigData(StoreInterface $store) : array 'weight_unit' => $storeConfig->getWeightUnit(), 'base_url' => $storeConfig->getBaseUrl(), 'base_link_url' => $storeConfig->getBaseLinkUrl(), - 'base_static_url' => $storeConfig->getSecureBaseStaticUrl(), + 'base_static_url' => $storeConfig->getBaseStaticUrl(), 'base_media_url' => $storeConfig->getBaseMediaUrl(), 'secure_base_url' => $storeConfig->getSecureBaseUrl(), 'secure_base_link_url' => $storeConfig->getSecureBaseLinkUrl(), 'secure_base_static_url' => $storeConfig->getSecureBaseStaticUrl(), - 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl() - ]; - return $storeConfigData; + 'secure_base_media_url' => $storeConfig->getSecureBaseMediaUrl(), + 'store_name' => $storeName, + ], $this->getExtendedConfigData((int)$storeConfig->getId())); } /** @@ -99,7 +127,7 @@ private function getBaseConfigData(StoreInterface $store) : array * @param int $storeId * @return array */ - private function getExtendedConfigData(int $storeId) + private function getExtendedConfigData(int $storeId): array { $extendedConfigData = []; foreach ($this->extendedConfigData as $key => $path) { diff --git a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml index f3771b704c3e9..3a0143821d8b9 100644 --- a/app/code/Magento/StoreGraphQl/etc/graphql/di.xml +++ b/app/code/Magento/StoreGraphQl/etc/graphql/di.xml @@ -23,11 +23,4 @@ </argument> </arguments> </type> - <type name="Magento\StoreGraphQl\Model\Resolver\Store\StoreConfigDataProvider"> - <arguments> - <argument name="extendedConfigData" xsi:type="array"> - <item name="store_name" xsi:type="string">store/information/name</item> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/StoreGraphQl/etc/schema.graphqls b/app/code/Magento/StoreGraphQl/etc/schema.graphqls index 919c94684eb21..d85bac7801f39 100644 --- a/app/code/Magento/StoreGraphQl/etc/schema.graphqls +++ b/app/code/Magento/StoreGraphQl/etc/schema.graphqls @@ -2,6 +2,7 @@ # See COPYING.txt for license details. type Query { storeConfig : StoreConfig @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\StoreConfigResolver") @doc(description: "The store config query") @cache(cacheable: false) + availableStores: [StoreConfig] @resolver(class: "Magento\\StoreGraphQl\\Model\\Resolver\\AvailableStoresResolver") @doc(description: "Get a list of available store views and their config information.") } type Website @doc(description: "Website is deprecated because it is should not be used on storefront. The type contains information about a website") { diff --git a/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php b/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php index f1bc6fcc105dc..f7167f6494312 100644 --- a/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php +++ b/app/code/Magento/Swagger/Api/Data/SchemaTypeInterface.php @@ -14,6 +14,7 @@ * Swagger Schema Type. * * @api + * @since 100.2.4 */ interface SchemaTypeInterface extends ArgumentInterface { @@ -21,6 +22,7 @@ interface SchemaTypeInterface extends ArgumentInterface * Retrieve the available types of Swagger schema. * * @return string + * @since 100.2.4 */ public function getCode(); @@ -29,6 +31,7 @@ public function getCode(); * * @param string|null $store * @return string + * @since 100.2.4 */ public function getSchemaUrlPath($store = null); } diff --git a/app/code/Magento/Swagger/Block/Index.php b/app/code/Magento/Swagger/Block/Index.php index 549495190ef34..8eecfbb24935d 100644 --- a/app/code/Magento/Swagger/Block/Index.php +++ b/app/code/Magento/Swagger/Block/Index.php @@ -17,6 +17,7 @@ * @method SchemaTypeInterface[] getSchemaTypes() * @method bool hasSchemaTypes() * @method string getDefaultSchemaTypeCode() + * @since 100.2.1 */ class Index extends Template { @@ -53,6 +54,7 @@ private function getSchemaType() /** * @return string|null + * @since 100.2.1 */ public function getSchemaUrl() { diff --git a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php index fc13372520945..9ba1083adab74 100644 --- a/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php +++ b/app/code/Magento/Swatches/Block/LayeredNavigation/RenderLayered.php @@ -5,11 +5,17 @@ */ namespace Magento\Swatches\Block\LayeredNavigation; -use Magento\Eav\Model\Entity\Attribute; +use Magento\Catalog\Model\Layer\Filter\AbstractFilter; +use Magento\Catalog\Model\Layer\Filter\Item as FilterItem; use Magento\Catalog\Model\ResourceModel\Layer\Filter\AttributeFactory; -use Magento\Framework\View\Element\Template; +use Magento\Eav\Model\Entity\Attribute; use Magento\Eav\Model\Entity\Attribute\Option; -use Magento\Catalog\Model\Layer\Filter\Item as FilterItem; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Element\Template\Context; +use Magento\Swatches\Helper\Data; +use Magento\Swatches\Helper\Media; +use Magento\Theme\Block\Html\Pager; /** * Class RenderLayered Render Swatches at Layered Navigation @@ -37,7 +43,7 @@ class RenderLayered extends Template protected $eavAttribute; /** - * @var \Magento\Catalog\Model\Layer\Filter\AbstractFilter + * @var AbstractFilter */ protected $filter; @@ -47,41 +53,52 @@ class RenderLayered extends Template protected $layerAttribute; /** - * @var \Magento\Swatches\Helper\Data + * @var Data */ protected $swatchHelper; /** - * @var \Magento\Swatches\Helper\Media + * @var Media */ protected $mediaHelper; /** - * @param Template\Context $context + * @var Pager + */ + private $htmlPagerBlock; + + /** + * @param Context $context * @param Attribute $eavAttribute * @param AttributeFactory $layerAttribute - * @param \Magento\Swatches\Helper\Data $swatchHelper - * @param \Magento\Swatches\Helper\Media $mediaHelper + * @param Data $swatchHelper + * @param Media $mediaHelper * @param array $data + * @param Pager|null $htmlPagerBlock */ public function __construct( - \Magento\Framework\View\Element\Template\Context $context, + Context $context, Attribute $eavAttribute, AttributeFactory $layerAttribute, - \Magento\Swatches\Helper\Data $swatchHelper, - \Magento\Swatches\Helper\Media $mediaHelper, - array $data = [] + Data $swatchHelper, + Media $mediaHelper, + array $data = [], + ?Pager $htmlPagerBlock = null ) { $this->eavAttribute = $eavAttribute; $this->layerAttribute = $layerAttribute; $this->swatchHelper = $swatchHelper; $this->mediaHelper = $mediaHelper; + $this->htmlPagerBlock = $htmlPagerBlock ?? ObjectManager::getInstance()->get(Pager::class); parent::__construct($context, $data); } /** + * Set filter and attribute objects + * * @param \Magento\Catalog\Model\Layer\Filter\AbstractFilter $filter + * * @return $this * @throws \Magento\Framework\Exception\LocalizedException */ @@ -94,6 +111,8 @@ public function setSwatchFilter(\Magento\Catalog\Model\Layer\Filter\AbstractFilt } /** + * Get attribute swatch data + * * @return array */ public function getSwatchData() @@ -114,30 +133,46 @@ public function getSwatchData() $attributeOptionIds = array_keys($attributeOptions); $swatches = $this->swatchHelper->getSwatchesByOptionsId($attributeOptionIds); - $data = [ + return [ 'attribute_id' => $this->eavAttribute->getId(), 'attribute_code' => $this->eavAttribute->getAttributeCode(), 'attribute_label' => $this->eavAttribute->getStoreLabel(), 'options' => $attributeOptions, 'swatches' => $swatches, ]; - - return $data; } /** + * Build filter option url + * * @param string $attributeCode * @param int $optionId + * * @return string */ public function buildUrl($attributeCode, $optionId) { - $query = [$attributeCode => $optionId]; - return $this->_urlBuilder->getUrl('*/*/*', ['_current' => true, '_use_rewrite' => true, '_query' => $query]); + $query = [ + $attributeCode => $optionId, + // exclude current page from urls + $this->htmlPagerBlock->getPageVarName() => null + ]; + + return $this->_urlBuilder->getUrl( + '*/*/*', + [ + '_current' => true, + '_use_rewrite' => true, + '_query' => $query + ] + ); } /** + * Get view data for option with no results + * * @param Option $swatchOption + * * @return array */ protected function getUnusedOption(Option $swatchOption) @@ -150,8 +185,11 @@ protected function getUnusedOption(Option $swatchOption) } /** + * Get option data if visible + * * @param FilterItem[] $filterItems * @param Option $swatchOption + * * @return array */ protected function getFilterOption(array $filterItems, Option $swatchOption) @@ -166,8 +204,11 @@ protected function getFilterOption(array $filterItems, Option $swatchOption) } /** + * Get view data for option + * * @param FilterItem $filterItem * @param Option $swatchOption + * * @return array */ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOption) @@ -187,15 +228,20 @@ protected function getOptionViewData(FilterItem $filterItem, Option $swatchOptio } /** + * Check if option should be visible + * * @param FilterItem $filterItem + * * @return bool */ protected function isOptionVisible(FilterItem $filterItem) { - return $this->isOptionDisabled($filterItem) && $this->isShowEmptyResults() ? false : true; + return !($this->isOptionDisabled($filterItem) && $this->isShowEmptyResults()); } /** + * Check if attribute values should be visible with no results + * * @return bool */ protected function isShowEmptyResults() @@ -204,7 +250,10 @@ protected function isShowEmptyResults() } /** + * Check if option should be disabled + * * @param FilterItem $filterItem + * * @return bool */ protected function isOptionDisabled(FilterItem $filterItem) @@ -213,8 +262,11 @@ protected function isOptionDisabled(FilterItem $filterItem) } /** + * Retrieve filter item by id + * * @param FilterItem[] $filterItems * @param integer $id + * * @return bool|FilterItem */ protected function getFilterItemById(array $filterItems, $id) @@ -228,14 +280,15 @@ protected function getFilterItemById(array $filterItems, $id) } /** + * Get swatch image path + * * @param string $type * @param string $filename + * * @return string */ public function getSwatchPath($type, $filename) { - $imagePath = $this->mediaHelper->getSwatchAttributeImage($type, $filename); - - return $imagePath; + return $this->mediaHelper->getSwatchAttributeImage($type, $filename); } } diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php index a2cae7f7b5a20..32df3daf4599b 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Configurable.php @@ -81,7 +81,7 @@ class Configurable extends \Magento\ConfigurableProduct\Block\Product\View\Type\ /** * Indicate if product has one or more Swatch attributes * - * @deprecated 100.1.5 unused + * @deprecated 100.1.0 unused * * @var boolean */ @@ -250,7 +250,7 @@ protected function getSwatchAttributesData() /** * Init isProductHasSwatchAttribute. * - * @deprecated 100.1.5 Method isProductHasSwatchAttribute() is used instead of this. + * @deprecated 100.2.0 Method isProductHasSwatchAttribute() is used instead of this. * * @codeCoverageIgnore * @return void @@ -510,6 +510,7 @@ public function getIdentities() * Get Swatch image size config data. * * @return string + * @since 100.2.5 */ public function getJsonSwatchSizeConfig() { diff --git a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php index 6e0a1e8d01360..e9b813003a09b 100644 --- a/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php +++ b/app/code/Magento/Swatches/Block/Product/Renderer/Listing/Configurable.php @@ -158,6 +158,7 @@ public function getJsonConfig() * Composes configuration for js price format * * @return string + * @since 100.2.3 */ public function getPriceFormatJson() { @@ -168,6 +169,7 @@ public function getPriceFormatJson() * Composes configuration for js price * * @return string + * @since 100.2.3 */ public function getPricesJson() { diff --git a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php index 9ad62265be21f..121d85ecc181d 100644 --- a/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php +++ b/app/code/Magento/Swatches/Model/ResourceModel/Swatch.php @@ -47,6 +47,7 @@ public function saveDefaultSwatchOption($id, $defaultValue) * @param array $optionIDs * @param int $type * @throws \Magento\Framework\Exception\LocalizedException + * @since 100.2.4 */ public function clearSwatchOptionByOptionIdAndType($optionIDs, $type = null) { diff --git a/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php index 795c48f12ebcc..43a44534aa942 100644 --- a/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php +++ b/app/code/Magento/Swatches/Plugin/Eav/Model/Entity/Attribute/OptionManagement.php @@ -8,6 +8,9 @@ namespace Magento\Swatches\Plugin\Eav\Model\Entity\Attribute; use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Model\Product\Attribute\OptionManagement as CatalogOptionManagement; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Eav\Model\AttributeRepository; use Magento\Store\Model\Store; use Magento\Swatches\Helper\Data; @@ -41,28 +44,61 @@ public function __construct( /** * Add swatch value to the attribute option * - * @param \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject + * @param CatalogOptionManagement $subject * @param string $attributeCode - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @param AttributeOptionInterface $option * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function beforeAdd( - \Magento\Catalog\Model\Product\Attribute\OptionManagement $subject, + CatalogOptionManagement $subject, ?string $attributeCode, - \Magento\Eav\Api\Data\AttributeOptionInterface $option + AttributeOptionInterface $option ) { - if (empty($attributeCode)) { + $attribute = $this->initAttribute($attributeCode); + if (!$attribute) { return; } - $attribute = $this->attributeRepository->get( - ProductAttributeInterface::ENTITY_TYPE_CODE, - $attributeCode - ); - if (!$attribute || !$this->swatchHelper->isSwatchAttribute($attribute)) { + + $optionId = $this->getNewOptionId($option); + $this->setSwatchAttributeOption($attribute, $option, $optionId); + } + + /** + * Update swatch value of attribute option + * + * @param CatalogOptionManagement $subject + * @param string $attributeCode + * @param int $optionId + * @param AttributeOptionInterface $option + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function beforeUpdate( + CatalogOptionManagement $subject, + $attributeCode, + $optionId, + AttributeOptionInterface $option + ) { + $attribute = $this->initAttribute($attributeCode); + if (!$attribute) { return; } - $optionId = $this->getOptionId($option); - $optionsValue = $option->getValue(); + + $this->setSwatchAttributeOption($attribute, $option, (string)$optionId); + } + + /** + * Set attribute swatch option + * + * @param AttributeInterface $attribute + * @param AttributeOptionInterface $option + * @param string $optionId + */ + private function setSwatchAttributeOption( + AttributeInterface $attribute, + AttributeOptionInterface $option, + string $optionId + ): void { + $optionsValue = trim($option->getValue() ?: ''); if ($this->swatchHelper->isVisualSwatch($attribute)) { $attribute->setData('swatchvisual', ['value' => [$optionId => $optionsValue]]); } else { @@ -80,13 +116,40 @@ public function beforeAdd( } /** - * Returns option id + * Init swatch attribute * - * @param \Magento\Eav\Api\Data\AttributeOptionInterface $option + * @param string $attributeCode + * @return bool|AttributeInterface + */ + private function initAttribute($attributeCode) + { + if (empty($attributeCode)) { + return false; + } + $attribute = $this->attributeRepository->get( + ProductAttributeInterface::ENTITY_TYPE_CODE, + $attributeCode + ); + if (!$attribute || !$this->swatchHelper->isSwatchAttribute($attribute)) { + return false; + } + + return $attribute; + } + + /** + * Get option id to create new option + * + * @param AttributeOptionInterface $option * @return string */ - private function getOptionId(\Magento\Eav\Api\Data\AttributeOptionInterface $option) : string + private function getNewOptionId(AttributeOptionInterface $option): string { - return 'id_' . ($option->getValue() ?: 'new_option'); + $optionId = trim($option->getValue() ?: ''); + if (empty($optionId)) { + $optionId = 'new_option'; + } + + return 'id_' . $optionId; } } diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml index 97a391137d8e3..5f3ec07bd4983 100644 --- a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/AddTextSwatchToProductActionGroup.xml @@ -19,6 +19,7 @@ <argument name="option2" defaultValue="textSwatchOption2" type="string"/> <argument name="option3" defaultValue="textSwatchOption3" type="string"/> <argument name="usedInProductListing" defaultValue="No" type="string"/> + <argument name="usedInLayeredNavigation" defaultValue="No" type="string"/> </arguments> <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> @@ -41,6 +42,7 @@ <click selector="{{StorefrontPropertiesSection.StoreFrontPropertiesTab}}" stepKey="clickStorefrontPropertiesTab"/> <waitForElementVisible selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" stepKey="waitForTabSwitch"/> <selectOption selector="{{AdvancedAttributePropertiesSection.UseInProductListing}}" userInput="{{usedInProductListing}}" stepKey="useInProductListing"/> + <selectOption selector="{{AttributePropertiesSection.useInLayeredNavigation}}" userInput="{{usedInLayeredNavigation}}" stepKey="useInLayeredNavigation"/> <click selector="{{AttributePropertiesSection.SaveAndEdit}}" stepKey="clickSave"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup.xml b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup.xml new file mode 100644 index 0000000000000..cc1b8fc4249bf --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/ActionGroup/StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <!-- Add Configurable Product with Swatch attribute to the cart --> + <actionGroup name="StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup"> + <annotations> + <description>Select Product product option. Fills in the provided Product Quantity. Clicks on Add To Cart. Validates that the Success Message is present.</description> + </annotations> + <arguments> + <argument name="product"/> + <argument name="productOption" type="string"/> + <argument name="productQty" type="string" /> + </arguments> + + <waitForPageLoad stepKey="waitForStorefrontProductPageLoad"/> + <click selector="{{StorefrontProductInfoMainSection.swatchOptionByLabel(productOption)}}" stepKey="clickSwatchOption"/> + <fillField selector="{{StorefrontProductPageSection.qtyInput}}" userInput="{{productQty}}" stepKey="fillProduct1Quantity"/> + <click selector="{{StorefrontProductPageSection.addToCartBtn}}" stepKey="clickOnAddToCartButton"/> + <waitForPageLoad stepKey="waitForProductToAddInCart"/> + <waitForElementVisible selector="{{StorefrontMessagesSection.success}}" stepKey="waitForSuccessMessage"/> + <seeElement selector="{{StorefrontProductPageSection.successMsg}}" stepKey="seeSuccessSaveMessage"/> + + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml index a4bff2227ffbb..811d3af735321 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Section/StorefrontProductInfoMainSection.xml @@ -16,6 +16,7 @@ <element name="nthSwatchOptionText" type="button" selector="div.swatch-option.text:nth-of-type({{n}})" parameterized="true"/> <element name="productSwatch" type="button" selector="//div[@class='swatch-option'][@aria-label='{{var1}}']" parameterized="true"/> <element name="visualSwatchOption" type="button" selector=".swatch-option[data-option-tooltip-value='#{{visualSwatchOption}}']" parameterized="true"/> + <element name="visualSwatchOptionText" type="button" selector=".swatch-option.text[data-option-tooltip-value='{{visualSwatchOptionText}}']" parameterized="true"/> <element name="swatchOptionTooltip" type="block" selector="div.swatch-option-tooltip"/> <element name="swatchAttributeSelectedOption" type="text" selector="#product-options-wrapper .swatch-option.selected"/> </section> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml index a4fc0bdcfd1fb..9833ee79a297a 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCheckColorUploadChooserVisualSwatchTest.xml @@ -19,7 +19,7 @@ <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="addNewProductAttribute"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="addNewProductAttribute"/> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="{{visualSwatchAttribute.input_type}}" stepKey="fillInputType"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml index e67d0c763308c..a972456e22ac5 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateImageSwatchTest.xml @@ -35,8 +35,7 @@ </after> <!-- Begin creating a new product attribute of type "Image Swatch" --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select visual swatch --> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml index 6b2a29d8ec451..683251b88a8bd 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateTextSwatchTest.xml @@ -25,8 +25,7 @@ </after> <!-- Create a new product attribute of type "Text Swatch" --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <selectOption selector="{{AttributePropertiesSection.InputType}}" userInput="swatch_text" stepKey="selectInputType"/> <click selector="{{AdminManageSwatchSection.addSwatchText}}" stepKey="clickAddSwatch0"/> @@ -51,8 +50,7 @@ <seeInField selector="{{AdminManageSwatchSection.nthSwatchAdminDescription('3')}}" userInput="Something blue." stepKey="seeDescription2"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml index 1a6c0341c0704..d59800c2bc0cd 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchTest.xml @@ -24,8 +24,7 @@ </before> <after> <!-- Clean up our modifications to the existing color attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> @@ -39,8 +38,7 @@ </after> <!-- Go to the edit page for the "color" attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> <click selector="{{AdminProductAttributeGridSection.AttributeCode('color')}}" stepKey="clickRowToEdit"/> @@ -102,8 +100,7 @@ </actionGroup> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> <fillField userInput="{{_defaultProduct.name}}" selector="{{AdminProductFormSection.productName}}" stepKey="fillName"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml index e93a27d377a52..0d58ba8fc9917 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminCreateVisualSwatchWithNonValidOptionsTest.xml @@ -27,8 +27,7 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="navigateToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForPageLoad"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="navigateToNewProductAttributePage"/> <!-- Set attribute properties --> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml index 8fd21acbd51d9..b48f181c8d199 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/AdminDisablingSwatchTooltipsTest.xml @@ -26,8 +26,7 @@ </before> <after> <!-- Clean up our modifications to the existing color attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> @@ -44,12 +43,13 @@ <!-- Enable swatch tooltips --> <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 1" stepKey="disableTooltips"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterEnabling"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnabling"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Go to the edit page for the "color" attribute --> - <amOnPage url="{{AdminProductAttributeGridPage.url}}" stepKey="goToProductAttributes"/> - <waitForPageLoad stepKey="waitForProductAttributes"/> + <actionGroup ref="AdminOpenProductAttributePageActionGroup" stepKey="goToProductAttributes"/> <fillField selector="{{AdminProductAttributeGridSection.FilterByAttributeCode}}" userInput="color" stepKey="fillFilter"/> <click selector="{{AdminProductAttributeGridSection.Search}}" stepKey="clickSearch"/> @@ -83,8 +83,7 @@ </actionGroup> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <click selector="{{AdminProductGridActionSection.addProductToggle}}" stepKey="clickOnAddProductToggle"/> <click selector="{{AdminProductGridActionSection.addConfigurableProduct}}" stepKey="clickOnAddConfigurableProduct"/> @@ -149,7 +148,9 @@ <!-- Disable swatch tooltips --> <magentoCLI command="config:set catalog/frontend/show_swatch_tooltip 0" stepKey="disableTooltips"/> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterDisabling"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisabling"> + <argument name="tags" value=""/> + </actionGroup> <!-- Verify swatch tooltips are not visible --> <reloadPage stepKey="refreshPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest.xml new file mode 100644 index 0000000000000..e89d3157e4624 --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontConfigurableProductSwatchUpdateCartItemTierPriceTest"> + <annotations> + <features value="Swatches"/> + <stories value="Configurable product with swatch attribute"/> + <title value="Swatch option should show the tier price on product page when Cart Item edited."/> + <description value="Configurable product with swatch attribute should show the tier price on product page when added Cart Item."/> + <severity value="CRITICAL"/> + <testCaseId value="MC-36047"/> + <group value="Swatches"/> + </annotations> + <before> + <createData entity="ApiCategory" stepKey="createCategory"/> + <createData entity="ApiConfigurableProduct" stepKey="createConfigurableProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + </before> + <after> + <deleteData createDataKey="createConfigurableProduct" stepKey="deleteConfigurableProduct"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteColorAttribute"> + <argument name="ProductAttribute" value="ProductColorAttribute"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + </after> + + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addColorAttribute"> + <argument name="attributeName" value="{{ProductColorAttribute.frontend_label}}"/> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + <argument name="option1" value="Black"/> + <argument name="option2" value="White"/> + <argument name="option3" value="Blue"/> + </actionGroup> + + <amOnPage url="{{AdminProductEditPage.url($createConfigurableProduct.id$)}}" stepKey="goToConfigurableProduct"/> + + <actionGroup ref="GenerateConfigurationsByAttributeCodeActionGroup" stepKey="createProductConfigurations"> + <argument name="attributeCode" value="{{ProductColorAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveConfigurableProductAddToCurrentAttributeSetActionGroup" stepKey="saveConfigurableProduct"/> + + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="$$createConfigurableProduct.sku$$-White"/> + </actionGroup> + <actionGroup ref="ProductSetAdvancedPricingActionGroup" stepKey="addTierPriceToSimpleProduct"> + <argument name="group" value="ALL GROUPS"/> + <argument name="quantity" value="5"/> + <argument name="price" value="Discount"/> + <argument name="amount" value="50"/> + </actionGroup> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveSimpleProduct"/> + + <actionGroup ref="StorefrontOpenProductPageActionGroup" stepKey="openConfigurableProductPage"> + <argument name="productUrl" value="$createConfigurableProduct.custom_attributes[url_key]$"/> + </actionGroup> + <waitForPageLoad stepKey="waitForConfigurableProductPage"/> + + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageActionGroup" stepKey="selectWhiteOption"> + <argument name="optionName" value="White"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceActionGroup" stepKey="assertProductTierPriceText"> + <argument name="tierProductPriceDiscountQuantity" value="5"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="61.50"/> + <argument name="productSavedPricePercent" value="50"/> + </actionGroup> + + <actionGroup ref="StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup" stepKey="addConfigurableProductToTheCart"> + <argument name="productQty" value="1"/> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="productOption" value="Blue"/> + </actionGroup> + + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage"/> + + <actionGroup ref="StorefrontUpdateCartItemEditParametersProductActionGroup" stepKey="updateCartItem"> + <argument name="rowNumber" value="1"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageActionGroup" stepKey="selectWhiteOption2"> + <argument name="optionName" value="White"/> + </actionGroup> + + <actionGroup ref="AssertStorefrontProductDetailPageTierPriceActionGroup" stepKey="assertProductTierPriceText2"> + <argument name="tierProductPriceDiscountQuantity" value="5"/> + <argument name="productPriceWithAppliedTierPriceDiscount" value="61.50"/> + <argument name="productSavedPricePercent" value="50"/> + </actionGroup> + + <actionGroup ref="StorefrontSelectSwatchOptionOnProductPageActionGroup" stepKey="selectWhiteOption3"> + <argument name="optionName" value="Blue"/> + </actionGroup> + + <dontSee selector="{{StorefrontProductInfoMainSection.tierPriceText}}" stepKey="dontSeeTierPriceForOption"/> + + <actionGroup ref="StorefrontAddProductWithSwatchesTextOptionToTheCartActionGroup" stepKey="addUpdatedConfigurableProductToTheCart"> + <argument name="productQty" value="10"/> + <argument name="product" value="ApiConfigurableProduct"/> + <argument name="productOption" value="White"/> + </actionGroup> + + <actionGroup ref="StorefrontCartPageOpenActionGroup" stepKey="openShoppingCartPage2"/> + + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml index 0999b43c48820..b661ecb338bde 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontDisplayAllCharactersOnTextSwatchTest.xml @@ -30,7 +30,9 @@ <fillField selector="{{AdminManageSwatchSection.swatchAdminDescriptionByIndex('3')}}" userInput="123456789012345678901BrownD" stepKey="fillDescription3" after="fillSwatch3"/> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '3')}}" userInput="123456789012345678901" stepKey="seeGreen" after="seeBlue"/> <see selector="{{StorefrontCategorySidebarSection.attributeNthOption(ProductAttributeFrontendLabel.label, '4')}}" userInput="123456789012345678901" stepKey="seeBrown" after="seeGreen"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml index 2ca26d84d45c7..4ed8824d9e39b 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByImageSwatchTest.xml @@ -34,8 +34,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select visual swatch --> @@ -54,8 +53,8 @@ </actionGroup> <click selector="{{AdminManageSwatchSection.nthUploadFile('1')}}" stepKey="clickUploadFile1"/> <attachFile selector="input[name='datafile']" userInput="adobe-thumb.jpg" stepKey="attachFile1"/> + <waitForPageLoad stepKey="waitFileAttached1"/> <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('0')}}" userInput="adobe-thumb" stepKey="fillAdmin1"/> - <click selector="{{AdminManageSwatchSection.swatchWindow('0')}}" stepKey="clicksWatchWindow1"/> <!-- Set swatch #2 image using the file upload --> <click selector="{{AdminManageSwatchSection.addSwatch}}" stepKey="clickAddSwatch2"/> @@ -64,6 +63,7 @@ </actionGroup> <click selector="{{AdminManageSwatchSection.nthUploadFile('2')}}" stepKey="clickUploadFile2"/> <attachFile selector="input[name='datafile']" userInput="adobe-small.jpg" stepKey="attachFile2"/> + <waitForPageLoad stepKey="waitFileAttached2"/> <fillField selector="{{AdminManageSwatchSection.adminInputByIndex('1')}}" userInput="adobe-small" stepKey="fillAdmin2"/> <!-- Set scope to global --> @@ -80,8 +80,7 @@ <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="ApiConfigurableProduct"/> </actionGroup> @@ -105,7 +104,9 @@ </actionGroup> <!-- Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml index 82dbff950d62f..4a78b4380fb68 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByTextSwatchTest.xml @@ -32,8 +32,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select text swatch --> @@ -67,8 +66,7 @@ <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> @@ -83,7 +81,9 @@ </actionGroup> <!--Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml index bf820863cf9b6..2a986463a3d14 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontFilterByVisualSwatchTest.xml @@ -34,8 +34,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> - <waitForPageLoad stepKey="waitForNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <fillField selector="{{AttributePropertiesSection.DefaultLabel}}" userInput="{{ProductAttributeFrontendLabel.label}}" stepKey="fillDefaultLabel"/> <!-- Select visual swatch --> @@ -79,8 +78,7 @@ <waitForElementVisible selector="{{AdminProductMessagesSection.successMessage}}" stepKey="waitForSuccess"/> <!-- Create a configurable product to verify the storefront with --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> - <waitForPageLoad time="30" stepKey="waitForProductGrid"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateConfigurableProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> @@ -95,7 +93,9 @@ </actionGroup> <!-- Run re-index task--> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> <!-- Go to the category page --> <amOnPage url="$$createCategory.name$$.html" stepKey="amOnCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml index 551a91f47c165..734294ba977ba 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontImageColorWhenFilterByColorFilterTest.xml @@ -71,8 +71,12 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductForm"/> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--Select any option in the Layered navigation and verify product image--> <amOnPage url="{{StorefrontCategoryPage.url($$createCategory.name$$)}}" stepKey="navigateToCategoryPage"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml new file mode 100644 index 0000000000000..c6266e034bffc --- /dev/null +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontRedirectToFirstPageOnFilteringBySwatchTest.xml @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontRedirectToFirstPageOnFilteringBySwatchTest"> + <annotations> + <features value="Swatches"/> + <stories value="Filter by swatch attribute on plp layered navigation"/> + <title value="Customers are redirected to first plp page after filtering by swatch"/> + <description value="Customers are redirected to first plp page after filtering by swatch"/> + <severity value="MINOR"/> + <group value="Swatches"/> + </annotations> + + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createSimpleProduct1"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct2"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <createData entity="SimpleProduct" stepKey="createSimpleProduct3"> + <requiredEntity createDataKey="createCategory"/> + </createData> + + <magentoCLI command="config:set catalog/frontend/grid_per_page 1" stepKey="setOneProductPerPage"/> + <magentoCLI command="config:set catalog/frontend/grid_per_page_values 1" stepKey="setGridPerPage"/> + + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="AddTextSwatchToProductActionGroup" stepKey="addSwatchAttribute"> + <argument name="usedInLayeredNavigation" value="1"/> + </actionGroup> + </before> + + <after> + <actionGroup ref="DeleteProductAttributeActionGroup" stepKey="deleteSwatchAttribute"> + <argument name="ProductAttribute" value="textSwatchAttribute"/> + </actionGroup> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + + <magentoCLI command="config:set catalog/frontend/grid_per_page 12" stepKey="setDefaultProductsPerPage"/> + <magentoCLI command="config:set catalog/frontend/grid_per_page_values 12,24,36" stepKey="setDefaultGridPerPage"/> + <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createSimpleProduct1" stepKey="deleteSimpleProduct1"/> + <deleteData createDataKey="createSimpleProduct2" stepKey="deleteSimpleProduct2"/> + <deleteData createDataKey="createSimpleProduct3" stepKey="deleteSimpleProduct3"/> + </after> + + <amOnPage url="{{AdminProductAttributeSetEditPage.url}}/{{AddToDefaultSet.attributeSetId}}/" stepKey="onAttributeSetEdit"/> + <actionGroup ref="AssignAttributeToGroupActionGroup" stepKey="assignAttributeToGroup"> + <argument name="group" value="Product Details"/> + <argument name="attribute" value="{{textSwatchAttribute.attribute_code}}"/> + </actionGroup> + <actionGroup ref="SaveAttributeSetActionGroup" stepKey="SaveAttributeSet"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndexPage"/> + <actionGroup ref="ClearFiltersAdminProductGridActionGroup" stepKey="clearFiltersOnProductIndexPage"/> + + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct1EditPage"> + <argument name="product" value="$$createSimpleProduct1$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="selectProduct1AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct1"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductsGridPage2"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct2EditPage"> + <argument name="product" value="$$createSimpleProduct2$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption1" stepKey="selectProduct2AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct2"/> + + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductsGridPage3"/> + <actionGroup ref="OpenEditProductOnBackendActionGroup" stepKey="goToProduct3EditPage"> + <argument name="product" value="$$createSimpleProduct3$$"/> + </actionGroup> + <selectOption selector="{{AdminProductAttributesSection.attributeDropdownByCode(textSwatchAttribute.attribute_code)}}" userInput="textSwatchOption2" stepKey="selectProduct3AttributeOption"/> + <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct3"/> + + <magentoCron groups="index" stepKey="runCronIndexer"/> + + <actionGroup ref="StorefrontNavigateCategoryPageActionGroup" stepKey="navigateToCategoryPage"> + <argument name="category" value="$$createCategory$$"/> + </actionGroup> + <actionGroup ref="StorefrontNavigateCategoryNextPageActionGroup" stepKey="navigateToCategoryNextPage"/> + + <click selector="{{StorefrontCategorySidebarSection.filterOptionTitle(textSwatchAttribute.default_label)}}" stepKey="expandAttribute"/> + <click selector="{{StorefrontCategorySidebarSection.attributeNthOption(textSwatchAttribute.attribute_code, '1')}}" stepKey="filterBySwatch1"/> + <waitForPageLoad stepKey="waitForPageToLoad2"/> + + <actionGroup ref="AssertStorefrontCategoryCurrentPageIsNthActionGroup" stepKey="assertCurrentPageIsFirst"> + <argument name="expectedPage" value="1"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml index 43944ceef33ef..27cbb01eafff0 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSeeProductImagesMatchingProductSwatchesTest.xml @@ -38,7 +38,7 @@ </after> <!-- Begin creating a new product attribute --> - <amOnPage url="{{ProductAttributePage.url}}" stepKey="goToNewProductAttributePage"/> + <actionGroup ref="AdminNavigateToNewProductAttributePageActionGroup" stepKey="goToNewProductAttributePage"/> <actionGroup ref="AdminFillProductAttributePropertiesActionGroup" stepKey="fillProductAttributeProperties"> <argument name="attributeName" value="{{VisualSwatchProductAttribute.attribute_code}}"/> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml index 5ae53858374e7..77a16e639c8a4 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchAttributesDisplayInWidgetCMSTest.xml @@ -33,8 +33,7 @@ <actionGroup ref="DeleteProductUsingProductGridActionGroup" stepKey="deleteProduct"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="visitAdminProductPage"/> - <waitForPageLoad stepKey="waitForAdminProductGridLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="visitAdminProductPage"/> <conditionalClick selector="{{AdminProductGridFilterSection.clearFilters}}" dependentSelector="{{AdminProductGridFilterSection.clearFilters}}" visible="true" stepKey="clickClearFiltersInitial"/> <actionGroup ref="AdminDeleteProductAttributeByLabelActionGroup" stepKey="deleteAttribute"> <argument name="productAttributeLabel" value="{{visualSwatchAttribute.default_label}}"/> @@ -55,8 +54,7 @@ <!--Login--> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToAdmin"/> <!--Create a configurable swatch product via the UI --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> diff --git a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml index 1b77e773ef283..0e25a14d6c170 100644 --- a/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml +++ b/app/code/Magento/Swatches/Test/Mftf/Test/StorefrontSwatchProductWithFileCustomOptionTest.xml @@ -32,8 +32,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> <!-- Create a configurable swatch product via the UI --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="goToProductIndex"/> - <waitForPageLoad stepKey="waitForProductPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="goToProductIndex"/> <actionGroup ref="GoToCreateProductPageActionGroup" stepKey="goToCreateProductPage"> <argument name="product" value="BaseConfigurableProduct"/> </actionGroup> @@ -45,7 +44,7 @@ <actionGroup ref="AddVisualSwatchToProductActionGroup" stepKey="addSwatchToProduct"/> <!--Add custom option to configurable product--> <actionGroup ref="AddProductCustomOptionFileActionGroup" stepKey="addCustomOptionToProduct"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveProduct"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveProduct"/> <!--Go to storefront--> <amOnPage url="" stepKey="goToHomePage"/> diff --git a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php index 4056bf27f571e..06960c409b476 100644 --- a/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php +++ b/app/code/Magento/Swatches/Test/Unit/Block/LayeredNavigation/RenderLayeredTest.php @@ -18,6 +18,7 @@ use Magento\Swatches\Block\LayeredNavigation\RenderLayered; use Magento\Swatches\Helper\Data; use Magento\Swatches\Helper\Media; +use Magento\Theme\Block\Html\Pager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -28,35 +29,60 @@ */ class RenderLayeredTest extends TestCase { - /** @var MockObject */ - protected $contextMock; - - /** @var MockObject */ - protected $requestMock; - - /** @var MockObject */ - protected $urlBuilder; - - /** @var MockObject */ - protected $eavAttributeMock; - - /** @var MockObject */ - protected $layerAttributeFactoryMock; - - /** @var MockObject */ - protected $layerAttributeMock; - - /** @var MockObject */ - protected $swatchHelperMock; - - /** @var MockObject */ - protected $mediaHelperMock; - - /** @var MockObject */ - protected $filterMock; - - /** @var MockObject */ - protected $block; + /** + * @var RenderLayered|MockObject + */ + private $block; + + /** + * @var Context|MockObject + */ + private $contextMock; + + /** + * @var RequestInterface|MockObject + */ + private $requestMock; + + /** + * @var Url|MockObject + */ + private $urlBuilder; + + /** + * @var Attribute|MockObject + */ + private $eavAttributeMock; + + /** + * @var AttributeFactory|MockObject + */ + private $layerAttributeFactoryMock; + + /** + * @var \Magento\Catalog\Model\ResourceModel\Layer\Filter\Attribute|MockObject + */ + private $layerAttributeMock; + + /** + * @var Data|MockObject + */ + private $swatchHelperMock; + + /** + * @var Media|MockObject + */ + private $mediaHelperMock; + + /** + * @var AbstractFilter|MockObject + */ + private $filterMock; + + /** + * @var Pager|MockObject + */ + private $htmlBlockPagerMock; protected function setUp(): void { @@ -66,8 +92,8 @@ protected function setUp(): void Url::class, ['getCurrentUrl', 'getRedirectUrl', 'getUrl'] ); - $this->contextMock->expects($this->any())->method('getRequest')->willReturn($this->requestMock); - $this->contextMock->expects($this->any())->method('getUrlBuilder')->willReturn($this->urlBuilder); + $this->contextMock->method('getRequest')->willReturn($this->requestMock); + $this->contextMock->method('getUrlBuilder')->willReturn($this->urlBuilder); $this->eavAttributeMock = $this->createMock(Attribute::class); $this->layerAttributeFactoryMock = $this->createPartialMock( AttributeFactory::class, @@ -80,6 +106,7 @@ protected function setUp(): void $this->swatchHelperMock = $this->createMock(Data::class); $this->mediaHelperMock = $this->createMock(Media::class); $this->filterMock = $this->createMock(AbstractFilter::class); + $this->htmlBlockPagerMock = $this->createMock(Pager::class); $this->block = $this->getMockBuilder(RenderLayered::class) ->setMethods(['filter', 'eavAttribute']) @@ -91,6 +118,7 @@ protected function setUp(): void $this->swatchHelperMock, $this->mediaHelperMock, [], + $this->htmlBlockPagerMock ] ) ->getMock(); @@ -114,7 +142,7 @@ public function testGetSwatchData() $item3 = $this->createMock(Item::class); $item4 = $this->createMock(Item::class); - $item1->expects($this->any())->method('__call')->withConsecutive( + $item1->method('__call')->withConsecutive( ['getValue'], ['getCount'], ['getValue'], @@ -128,9 +156,9 @@ public function testGetSwatchData() 'Yellow' ); - $item2->expects($this->any())->method('__call')->with('getValue')->willReturn('blue'); + $item2->method('__call')->with('getValue')->willReturn('blue'); - $item3->expects($this->any())->method('__call')->withConsecutive( + $item3->method('__call')->withConsecutive( ['getValue'], ['getCount'] )->willReturnOnConsecutiveCalls( @@ -138,7 +166,7 @@ public function testGetSwatchData() 0 ); - $item4->expects($this->any())->method('__call')->withConsecutive( + $item4->method('__call')->withConsecutive( ['getValue'], ['getCount'], ['getValue'], @@ -162,22 +190,22 @@ public function testGetSwatchData() $this->block->method('filter')->willReturn($this->filterMock); $option1 = $this->createMock(Option::class); - $option1->expects($this->any())->method('getValue')->willReturn('yellow'); + $option1->method('getValue')->willReturn('yellow'); $option2 = $this->createMock(Option::class); - $option2->expects($this->any())->method('getValue')->willReturn(null); + $option2->method('getValue')->willReturn(null); $option3 = $this->createMock(Option::class); - $option3->expects($this->any())->method('getValue')->willReturn('red'); + $option3->method('getValue')->willReturn('red'); $option4 = $this->createMock(Option::class); - $option4->expects($this->any())->method('getValue')->willReturn('green'); + $option4->method('getValue')->willReturn('green'); $eavAttribute = $this->createMock(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $eavAttribute->expects($this->once()) ->method('getOptions') ->willReturn([$option1, $option2, $option3, $option4]); - $eavAttribute->expects($this->any())->method('getIsFilterable')->willReturn(0); + $eavAttribute->method('getIsFilterable')->willReturn(0); $this->filterMock->expects($this->once())->method('getAttributeModel')->willReturn($eavAttribute); $this->block->method('eavAttribute')->willReturn($eavAttribute); @@ -200,7 +228,7 @@ public function testGetSwatchDataException() { $this->block->method('filter')->willReturn($this->filterMock); $this->block->setSwatchFilter($this->filterMock); - $this->expectException('\RuntimeException'); + $this->expectException(\RuntimeException::class); $this->block->getSwatchData(); } diff --git a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js index ad5926d451e88..84389083447ae 100644 --- a/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js +++ b/app/code/Magento/Swatches/view/base/web/js/swatch-renderer.js @@ -311,6 +311,7 @@ define([ if ($(this.element).attr('data-rendered')) { return; } + $(this.element).attr('data-rendered', true); if (_.isEmpty(this.options.jsonConfig.images)) { @@ -320,6 +321,8 @@ define([ this._debouncedLoadProductMedia = _.debounce(this._LoadProductMedia.bind(this), 500); } + this.options.tierPriceTemplate = $(this.options.tierPriceTemplateSelector).html(); + if (this.options.jsonConfig !== '' && this.options.jsonSwatchConfig !== '') { // store unsorted attributes this.options.jsonConfig.mappedAttributes = _.clone(this.options.jsonConfig.attributes); @@ -330,7 +333,6 @@ define([ } else { console.log('SwatchRenderer: No input data received'); } - this.options.tierPriceTemplate = $(this.options.tierPriceTemplateSelector).html(); }, /** diff --git a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml index 70eb51651f663..bae3820042de0 100644 --- a/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml +++ b/app/code/Magento/Swatches/view/frontend/templates/product/layered/renderer.phtml @@ -8,6 +8,8 @@ // phpcs:disable Generic.WhiteSpace.ScopeIndent /** @var $block \Magento\Swatches\Block\LayeredNavigation\RenderLayered */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +/** @var \Magento\Framework\Escaper $escaper */ ?> <?php $swatchData = $block->getSwatchData(); ?> <div class="swatch-attribute swatch-layered <?= $block->escapeHtmlAttr($swatchData['attribute_code']) ?>" @@ -49,11 +51,18 @@ data-option-id="<?= $block->escapeHtmlAttr($option) ?>" data-option-label="<?= $block->escapeHtmlAttr($label['label']) ?>" data-option-tooltip-thumb="<?= $block->escapeUrl($swatchThumbPath) ?>" - data-option-tooltip-value="" - style="background: url(<?= - /* @noEscape */ $escapedUrl - ?>) no-repeat center; background-size: initial;"> + data-option-tooltip-value=""> </div> + <?php + $element = 'swatchImageOption' .$escaper->escapeJs($option); + $script = 'var ' .$element + .' = document.querySelector(\'div[data-option-id="' .$escaper->escapeJs($option) + .'"]\');' .PHP_EOL; + $script .= $element .'.style.background = "url(\'' + .$escapedUrl .'\') no-repeat center";' .PHP_EOL; + $script .= $element .'.style.backgroundSize = "initial";'; + ?> + <?= /* @noEscape*/ $secureRenderer->renderTag('script', [], $script, false); ?> <?php break; case '1': ?> @@ -65,11 +74,21 @@ data-option-tooltip-thumb="" data-option-tooltip-value="<?= $block->escapeHtmlAttr( $swatchData['swatches'][$option]['value'] - ) ?>" - style="background: <?= $block->escapeHtmlAttr( - $swatchData['swatches'][$option]['value'] - ) ?> no-repeat center; background-size: initial;"> + ) ?>"> </div> + <?php + $element = 'swatchImageOption' .$escaper->escapeJs($option); + $backgroundValue = $escaper->escapeJs( + str_replace('\'', '\\\'', $swatchData['swatches'][$option]['value']) + ); + $script = 'var ' .$element + .' = document.querySelector(\'div[data-option-id="' .$escaper->escapeJs($option) + .'"]\');' .PHP_EOL; + $script .= $element .'.style.background = "' .$backgroundValue + .' no-repeat center";' .PHP_EOL; + $script .= $element .'.style.backgroundSize = "initial";'; + ?> + <?= /* @noEscape*/ $secureRenderer->renderTag('script', [], $script, false); ?> <?php break; case '0': default: @@ -89,11 +108,14 @@ <?php endforeach; ?> </div> </div> +<?php $scriptString = <<<script -<script> require(["jquery", "Magento_Swatches/js/swatch-renderer"], function ($) { - $('.swatch-layered.<?= $block->escapeJs($swatchData['attribute_code']) ?>') + $('.swatch-layered.{$block->escapeJs($swatchData['attribute_code'])}') .find('[data-option-type="1"], [data-option-type="2"], [data-option-type="0"], [data-option-type="3"]') .SwatchRendererTooltip(); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/SwatchesGraphQl/composer.json b/app/code/Magento/SwatchesGraphQl/composer.json index 383575302e6ae..1b98b4044a2ff 100644 --- a/app/code/Magento/SwatchesGraphQl/composer.json +++ b/app/code/Magento/SwatchesGraphQl/composer.json @@ -6,9 +6,7 @@ "php": "~7.3.0||~7.4.0", "magento/framework": "*", "magento/module-swatches": "*", - "magento/module-catalog": "*" - }, - "suggest": { + "magento/module-catalog": "*", "magento/module-catalog-graph-ql": "*" }, "license": [ diff --git a/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php b/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php index 7b66c4fd964c6..ae5da9e15cf53 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Frontend/Region/Updater.php @@ -5,7 +5,9 @@ */ namespace Magento\Tax\Block\Adminhtml\Frontend\Region; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form\Element\AbstractElement; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class Updater extends \Magento\Config\Block\System\Config\Form\Field { @@ -14,21 +16,31 @@ class Updater extends \Magento\Config\Block\System\Config\Form\Field */ protected $_directoryHelper; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Directory\Helper\Data $directoryHelper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Directory\Helper\Data $directoryHelper, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_directoryHelper = $directoryHelper; parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** + * Return element html. + * * @param AbstractElement $element * @return string */ @@ -36,8 +48,7 @@ protected function _getElementHtml(AbstractElement $element) { $html = parent::_getElementHtml($element); - $js = '<script> - require(["prototype", "mage/adminhtml/form"], function(){ + $js = 'require(["prototype", "mage/adminhtml/form"], function(){ updater = new RegionUpdater("tax_defaults_country", "none", "tax_defaults_region", %s, "nullify"); if(updater.lastCountryId) { var tmpRegionId = $("tax_defaults_region").value; @@ -49,10 +60,12 @@ protected function _getElementHtml(AbstractElement $element) } else { updater.update(); } - }); - </script>'; + });'; + + $scriptString = sprintf($js, $this->_directoryHelper->getRegionJson()); + + $html .= /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); - $html .= sprintf($js, $this->_directoryHelper->getRegionJson()); return $html; } } diff --git a/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php b/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php index 376adba63db62..a1f538e0b0c70 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Items/Price/Renderer.php @@ -22,7 +22,7 @@ class Renderer extends \Magento\Backend\Block\Template { /** * @var \Magento\Tax\Helper\Data - * @deprecated + * @deprecated 100.3.0 * Marked as deprecated as it is unused. */ protected $taxHelper; diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php index 1884b247e530a..7ec16fd7f5373 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Form.php @@ -13,6 +13,8 @@ namespace Magento\Tax\Block\Adminhtml\Rate; +use Magento\Directory\Helper\Data as DirectoryHelper; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Tax\Controller\RegistryConstants; @@ -86,6 +88,7 @@ class Form extends \Magento\Backend\Block\Widget\Form\Generic * @param \Magento\Tax\Model\TaxRateCollection $taxRateCollection * @param \Magento\Tax\Model\Calculation\Rate\Converter $taxRateConverter * @param array $data + * @param DirectoryHelper|null $directoryHelper * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -99,7 +102,8 @@ public function __construct( \Magento\Tax\Api\TaxRateRepositoryInterface $taxRateRepository, \Magento\Tax\Model\TaxRateCollection $taxRateCollection, \Magento\Tax\Model\Calculation\Rate\Converter $taxRateConverter, - array $data = [] + array $data = [], + ?DirectoryHelper $directoryHelper = null ) { $this->_regionFactory = $regionFactory; $this->_country = $country; @@ -108,6 +112,7 @@ public function __construct( $this->_taxRateRepository = $taxRateRepository; $this->_taxRateCollection = $taxRateCollection; $this->_taxRateConverter = $taxRateConverter; + $data['directoryHelper'] = $directoryHelper ?? ObjectManager::getInstance()->get(DirectoryHelper::class); parent::__construct($context, $registry, $formFactory, $data); } diff --git a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php index 87e9d9e006064..8ba846dc710b2 100644 --- a/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php +++ b/app/code/Magento/Tax/Block/Adminhtml/Rate/Toolbar/Save.php @@ -133,7 +133,7 @@ protected function _prepareLayout() ) . '\', \'' . $this->getUrl( 'tax/*/delete', ['rate' => $rate] - ) . '\')', + ) . '\', {data: {}})', 'class' => 'delete' ] ); diff --git a/app/code/Magento/Tax/Block/Checkout/Tax.php b/app/code/Magento/Tax/Block/Checkout/Tax.php index 0a86c0312ab1c..a53db42be2ad6 100644 --- a/app/code/Magento/Tax/Block/Checkout/Tax.php +++ b/app/code/Magento/Tax/Block/Checkout/Tax.php @@ -9,8 +9,48 @@ */ namespace Magento\Tax\Block\Checkout; +use Magento\Checkout\Helper\Data as CheckoutHelper; +use Magento\Framework\App\ObjectManager; +use Magento\Tax\Helper\Data as TaxHelper; +use Magento\Sales\Model\ConfigInterface; + +/** + * Class for manage tax amount. + */ class Tax extends \Magento\Checkout\Block\Total\DefaultTotal { + /** + * @param \Magento\Framework\View\Element\Template\Context $context + * @param \Magento\Customer\Model\Session $customerSession + * @param \Magento\Checkout\Model\Session $checkoutSession + * @param ConfigInterface $salesConfig + * @param array $layoutProcessors + * @param array $data + * @param CheckoutHelper|null $checkoutHelper + * @param TaxHelper|null $taxHelper + */ + public function __construct( + \Magento\Framework\View\Element\Template\Context $context, + \Magento\Customer\Model\Session $customerSession, + \Magento\Checkout\Model\Session $checkoutSession, + ConfigInterface $salesConfig, + array $layoutProcessors = [], + array $data = [], + ?CheckoutHelper $checkoutHelper = null, + ?TaxHelper $taxHelper = null + ) { + $data['taxHelper'] = $taxHelper ?? ObjectManager::getInstance()->get(TaxHelper::class); + parent::__construct( + $context, + $customerSession, + $checkoutSession, + $salesConfig, + $layoutProcessors, + $data, + $checkoutHelper + ); + } + /** * @var string */ diff --git a/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php b/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php index 1c5013c34c6e9..a77216cd3b46a 100644 --- a/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php +++ b/app/code/Magento/Tax/Controller/Adminhtml/Rate/Delete.php @@ -8,8 +8,9 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\App\Action\HttpPostActionInterface; -class Delete extends \Magento\Tax\Controller\Adminhtml\Rate +class Delete extends \Magento\Tax\Controller\Adminhtml\Rate implements HttpPostActionInterface { /** * Delete Rate and Data diff --git a/app/code/Magento/Tax/Model/System/Message/Notifications.php b/app/code/Magento/Tax/Model/System/Message/Notifications.php index ca59ab9eec3bf..163054d9e9394 100644 --- a/app/code/Magento/Tax/Model/System/Message/Notifications.php +++ b/app/code/Magento/Tax/Model/System/Message/Notifications.php @@ -16,7 +16,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface * Store manager object * * @var \Magento\Store\Model\StoreManagerInterface - * @deprecated 100.1.3 + * @deprecated 100.1.0 */ protected $storeManager; @@ -36,7 +36,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface * Stores with invalid display settings * * @var array - * @deprecated 100.1.3 + * @deprecated 100.1.0 * @see \Magento\Tax\Model\System\Message\Notification\RoundingErrors */ protected $storesWithInvalidDisplaySettings; @@ -45,7 +45,7 @@ class Notifications implements \Magento\Framework\Notification\MessageInterface * Websites with invalid discount settings * * @var array - * @deprecated 100.1.3 + * @deprecated 100.1.0 * @see \Magento\Tax\Model\System\Message\Notification\DiscountErrors */ protected $storesWithInvalidDiscountSettings; diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRateGridOpenPageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRateGridOpenPageActionGroup.xml new file mode 100644 index 0000000000000..58762e4fa02ef --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRateGridOpenPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminTaxRateGridOpenPageActionGroup"> + <annotations> + <description>Go to tax rate grid page.</description> + </annotations> + + <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatePage"/> + <waitForPageLoad stepKey="waitForTaxRatePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRuleGridOpenPageActionGroup.xml b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRuleGridOpenPageActionGroup.xml new file mode 100644 index 0000000000000..768dcd6cb42a8 --- /dev/null +++ b/app/code/Magento/Tax/Test/Mftf/ActionGroup/AdminTaxRuleGridOpenPageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminTaxRuleGridOpenPageActionGroup"> + <annotations> + <description>Go to tax rule grid page.</description> + </annotations> + + <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleGridPage"/> + <waitForPageLoad stepKey="waitForTaxRulePage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml index fc4b6dd8b84c5..c873e14797470 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckCreditMemoTotalsTest.xml @@ -50,7 +50,7 @@ <!-- Reset admin order filter --> <actionGroup ref="AdminOrdersGridClearFiltersActionGroup" stepKey="clearOrderFilters"/> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="{{defaultTaxRule.code}}"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml index 01e1677ec8d8a..84278468a0590 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCheckingTaxReportGridTest.xml @@ -106,7 +106,7 @@ <deleteData createDataKey="createSecondProductTaxClass" stepKey="deleteSecondProductTaxClass"/> <!-- Clear filter Product --> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="amOnProductGridPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="amOnProductGridPage"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFilterProduct"/> <!-- Delete Customer and clear filter --> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml index 3a5f905d89dd5..07968c281c68b 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateDefaultsTaxRuleTest.xml @@ -29,8 +29,7 @@ <deleteData stepKey="deleteTaxRate" createDataKey="initialTaxRate" /> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> <!-- Create a tax rule with defaults --> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml index e132b86ab4417..be7185a5166a2 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateAllPostCodesTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> @@ -31,8 +31,7 @@ <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate with * for postcodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -43,8 +42,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify the tax rate grid page shows the tax rate we just created --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> <selectOption selector="{{AdminTaxRateGridSection.filterByCountry}}" userInput="Australia" stepKey="fillCountryFilter"/> @@ -55,8 +53,7 @@ <see selector="{{AdminTaxRateGridSection.grid}}" userInput="*" stepKey="seePostCode"/> <!-- Go to the tax rate edit page for our new tax rate --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex3"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex3"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter2"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> @@ -69,8 +66,7 @@ <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="20.0000" stepKey="seeRate"/> <!-- Go to the tax rule grid page and verify our tax rate can be used in the rule --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminGridMainControls.add}}" stepKey="clickAddNewTaxRule"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml index 3a6e4dfef5bac..6f8379e460c34 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateInvalidPostcodeTestLengthTest.xml @@ -22,8 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate for large postcodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{taxRateWithInvalidPostCodeLength.code}}" stepKey="fillRuleName"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml index 0f1b5b08ffcec..c8e4defc40c9f 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateLargeRateTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> @@ -31,8 +31,7 @@ <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate for large postcodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -43,8 +42,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Create a tax rate for large postcodes and verify we see expected values on the tax rate grid page --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -60,8 +58,7 @@ <seeInField selector="{{AdminTaxRateFormSection.rate}}" userInput="999.0000" stepKey="seeRate"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml index 379164d134448..c6a5a6c69e788 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateSpecificPostcodeTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> @@ -31,7 +31,7 @@ <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <waitForPageLoad stepKey="waitForTaxRateIndex1"/> <!-- Create a tax rate with specific postcode --> @@ -43,8 +43,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Verify the tax rate grid page shows the specific postcode we just created --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -58,8 +57,7 @@ <seeInField selector="{{AdminTaxRateFormSection.country}}" userInput="Canada" stepKey="seeCountry2"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml index d276a6a276b2c..ef9b66041893d 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateWiderZipCodeRangeTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> @@ -31,8 +31,7 @@ <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate with range from 1-7800935 for zipCodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -44,8 +43,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Create a tax rate for zipCodeRange and verify we see expected values on the tax rate grid page --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -61,8 +59,7 @@ <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="United Kingdom" stepKey="seeCountry2"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml index 998c948d869d0..23c4ffd78a88d 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRateZipCodeRangeTest.xml @@ -22,7 +22,7 @@ <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> </before> <after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillNameFilter"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch"/> @@ -31,8 +31,7 @@ <click selector="{{AdminTaxRateFormSection.ok}}" stepKey="clickOk"/> </after> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <!-- Create a tax rate with range from 90001-96162 for zipCodes --> <click selector="{{AdminTaxRateGridSection.add}}" stepKey="clickAddNewTaxRateButton"/> <fillField selector="{{AdminTaxRateFormSection.taxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillRuleName"/> @@ -45,8 +44,7 @@ <click selector="{{AdminTaxRateFormSection.save}}" stepKey="clickSave"/> <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex3"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <!-- Create a tax rate for zipCodeRange and verify we see expected values on the tax rate grid page --> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{SimpleTaxRate.code}}" stepKey="fillTaxIdentifierField2"/> @@ -63,8 +61,7 @@ <seeOptionIsSelected selector="{{AdminTaxRateFormSection.country}}" userInput="United States" stepKey="seeCountry2"/> <!-- Verify we see expected values on the tax rule form page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex4"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAdd"/> <see selector="{{AdminTaxRulesSection.taxRateMultiSelectItems}}" userInput="{{SimpleTaxRate.code}}" stepKey="seeTaxRateOnNewTaxRulePage"/> </test> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml index 6e2ca794379f6..ba0834da7c0e7 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithCustomerAndProductTaxClassTest.xml @@ -39,8 +39,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> @@ -90,4 +89,4 @@ <seeInField selector="{{AdminTaxRuleFormSection.priority}}" userInput="{{taxRuleWithCustomPriorityPosition.priority}}" stepKey="seePriority"/> <seeInField selector="{{AdminTaxRuleFormSection.sortOrder}}" userInput="{{taxRuleWithCustomPriorityPosition.position}}" stepKey="seeSortOrder"/> </test> -</tests> \ No newline at end of file +</tests> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml index 895bb920973c8..ae37bc8a8930a 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewAndExistingTaxRateAndCustomerAndProductTaxClassTest.xml @@ -40,8 +40,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml index 43ce4059ad84e..2a008991c2dc8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithNewTaxClassesAndTaxRateTest.xml @@ -40,8 +40,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml index 0293e04293daf..de55453fcabc4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminCreateTaxRuleWithZipRangeTest.xml @@ -40,8 +40,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml index 770cdd1e3b2c4..37b90300aad28 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminDeleteTaxRuleTest.xml @@ -30,8 +30,7 @@ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> @@ -46,8 +45,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="The tax rule has been deleted." stepKey="seeAssertTaxRuleDeleteMessage"/> <!-- Confirm Deleted Tax Rule(from the above step) on the tax rule grid page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex2"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex2"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch2"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml index 61b09eabe7d35..fd445326976e4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateDefaultTaxRuleTest.xml @@ -38,8 +38,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml index b7ffe05ebf5c2..3a5607ea598ca 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithCustomClassesTest.xml @@ -39,8 +39,7 @@ <deleteData stepKey="deleteProductTaxClass" createDataKey="createProductTaxClass"/> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml index 14df3f8987f5e..fa42ce5ddafa3 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/AdminUpdateTaxRuleWithFixedZipUtahTest.xml @@ -43,8 +43,7 @@ <deleteData stepKey="deleteCustomer" createDataKey="customer" /> </after> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex1"/> - <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRuleGridSection.code}}" userInput="$$initialTaxRule.code$$" stepKey="fillTaxCodeSearch"/> <click selector="{{AdminTaxRuleGridSection.search}}" stepKey="clickSearch1"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml index 4b34121b10829..881e09e5e35f4 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/DeleteTaxRateEntityTest.xml @@ -24,8 +24,7 @@ </before> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -37,15 +36,14 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You Deleted the tax rate." stepKey="seeSuccess1"/> <!-- Confirm Deleted TaxIdentifier(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRate.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> <see selector="{{AdminTaxRateGridSection.emptyText}}" userInput="We couldn't find any records." stepKey="seeSuccess"/> <!-- Confirm Deleted TaxIdentifier on the tax rule grid page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRuleIndex3"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRuleIndex3"/> <click selector="{{AdminTaxRuleGridSection.add}}" stepKey="clickAddNewTaxRuleButton"/> <waitForPageLoad stepKey="waitForTaxRuleIndex1"/> <fillField selector="{{AdminTaxRuleFormSection.taxRateSearch}}" userInput="$$initialTaxRate.code$$" stepKey="fillTaxRateSearch"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml index b1e91886960c5..f7d23baa534fb 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml index 8a04156f3d857..e08d366a37cd8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartGuestVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml index b76f015679ae2..bab6b7c45ff60 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -59,16 +58,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -84,7 +81,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml index 5f98093ec874f..b29b1a127189e 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCartTest/StorefrontTaxQuoteCartLoggedInVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -58,16 +57,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -83,7 +80,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml index d005f4b657448..b9c3baab8c0dd 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> @@ -89,8 +86,7 @@ <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress"> <argument name="Address" value="US_Address_CA"/> </actionGroup> - <click stepKey="clickNext" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForAddressToLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment"/> <see stepKey="seeAddress" selector="{{CheckoutShippingSection.defaultShipping}}" userInput="{{SimpleTaxCA.state}}"/> @@ -109,8 +105,7 @@ <actionGroup ref="LoggedInCheckoutFillNewBillingAddressActionGroup" stepKey="changeAddress2"> <argument name="Address" value="US_Address_NY"/> </actionGroup> - <click stepKey="clickNext2" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForShippingToLoad"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext2"/> <!-- Checkout select Check/Money Order payment --> <actionGroup ref="CheckoutSelectCheckMoneyOrderPaymentActionGroup" stepKey="selectCheckMoneyPayment2"/> <see stepKey="seeShipTo2" selector="{{CheckoutPaymentSection.shipToInformation}}" userInput="{{SimpleTaxNY.state}}"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml index d1fc0654fc496..8fafbd9986c64 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutGuestVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml index 18a1a11d35fd2..ae988cd43efd5 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInSimpleTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> </after> @@ -100,8 +97,7 @@ <amOnPage url="{{CheckoutPage.url}}" stepKey="goToCheckout"/> <waitForPageLoad stepKey="waitForShippingSection"/> <see stepKey="seeAddress" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{SimpleTaxNY.state}}"/> - <click stepKey="clickNext" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForReviewAndPayments"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext"/> <waitForElementVisible stepKey="waitForOverviewVisible" selector="{{CheckoutPaymentSection.tax}}"/> <see stepKey="seeTax" selector="{{CheckoutPaymentSection.tax}}" userInput="$10.30"/> @@ -123,8 +119,7 @@ <waitForPageLoad stepKey="waitForAddressSaved"/> <see stepKey="seeAddress2" selector="{{CheckoutShippingSection.selectedShippingAddress}}" userInput="{{SimpleTaxCA.state}}"/> - <click stepKey="clickNext2" selector="{{CheckoutShippingSection.next}}"/> - <waitForPageLoad stepKey="waitForReviewAndPayments2"/> + <actionGroup ref="StorefrontCheckoutClickNextOnShippingStepActionGroup" stepKey="clickNext2"/> <!-- Assert that taxes are applied correctly for CA --> <waitForElementVisible stepKey="waitForOverviewVisible2" selector="{{CheckoutPaymentSection.tax}}"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml index 35a483da7f690..6552d31a8a523 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/StorefrontTaxQuoteCheckoutTest/StorefrontTaxQuoteCheckoutLoggedInVirtualTest.xml @@ -25,8 +25,7 @@ <actionGroup ref="EditTaxConfigurationByUIActionGroup" stepKey="fillDefaultTaxForms"/> <!-- Go to tax rule page --> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulePage"/> - <waitForPageLoad stepKey="waitForTaxRatePage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulePage"/> <click stepKey="addNewTaxRate" selector="{{AdminGridMainControls.add}}"/> <fillField stepKey="fillRuleName" selector="{{AdminTaxRulesSection.ruleName}}" userInput="SampleRule"/> @@ -45,16 +44,14 @@ </before> <after> <!-- Go to the tax rule page and delete the row we created--> - <amOnPage url="{{AdminTaxRuleGridPage.url}}" stepKey="goToTaxRulesPage"/> - <waitForPageLoad stepKey="waitForRulesPage"/> + <actionGroup ref="AdminTaxRuleGridOpenPageActionGroup" stepKey="goToTaxRulesPage"/> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteRule"> <argument name="name" value="SampleRule"/> <argument name="searchInput" value="{{AdminSecondaryGridSection.taxIdentifierSearch}}"/> </actionGroup> <!-- Go to the tax rate page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRatesPage"/> - <waitForPageLoad stepKey="waitForRatesPage"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRatesPage"/> <!-- Delete the two tax rates that were created --> <actionGroup ref="deleteEntitySecondaryGrid" stepKey="deleteNYRate"> @@ -70,7 +67,7 @@ <!-- Ensure tax won't be shown in the cart --> <actionGroup ref="ChangeToDefaultTaxConfigurationUIActionGroup" stepKey="changeToDefaultTaxConfiguration"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> <deleteData createDataKey="virtualProduct1" stepKey="deleteVirtualProduct1"/> </after> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml index 306216422adea..ff75b1e95646a 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update01TaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -45,8 +44,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated 0.1 tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex4"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateFrance.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml index c22bab774de29..ebfa1288b59dd 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update100TaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -44,8 +43,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated TaxIdentifier(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex4"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateUS.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml index 6f93d07b76eed..ed1c126930df8 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/Update1299TaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax identifier on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode1"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -45,8 +44,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateUK.code}}" stepKey="fillTaxIdentifierField2"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml index c3986e6a8d0cc..7a2f0664d7757 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateAnyRegionTaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -44,8 +43,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated any region tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{taxRateCustomRateCanada.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml index fb1eff1d74067..03aba8da8ae19 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateDecimalTaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -46,8 +45,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex2"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex2"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRateWithZipRange.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml index 1f0406244a926..37b8bb8d95618 100644 --- a/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml +++ b/app/code/Magento/Tax/Test/Mftf/Test/UpdateLargeTaxRateEntityTest.xml @@ -27,8 +27,7 @@ </after> <!-- Search the tax rate on tax grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex1"/> - <waitForPageLoad stepKey="waitForTaxRateIndex1"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex1"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters1"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="$$initialTaxRate.code$$" stepKey="fillCode"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch1"/> @@ -43,8 +42,7 @@ <see selector="{{AdminMessagesSection.success}}" userInput="You saved the tax rate." stepKey="seeSuccess"/> <!-- Verify we see updated large tax rate(from the above step) on the tax rate grid page --> - <amOnPage url="{{AdminTaxRateGridPage.url}}" stepKey="goToTaxRateIndex4"/> - <waitForPageLoad stepKey="waitForTaxRateIndex2"/> + <actionGroup ref="AdminTaxRateGridOpenPageActionGroup" stepKey="goToTaxRateIndex4"/> <click selector="{{AdminDataGridHeaderSection.clearFilters}}" stepKey="clickClearFilters2"/> <fillField selector="{{AdminTaxRateGridSection.filterByTaxIdentifier}}" userInput="{{defaultTaxRateWithLargeRate.code}}" stepKey="fillTaxIdentifierField3"/> <click selector="{{AdminTaxRateGridSection.search}}" stepKey="clickSearch2"/> diff --git a/app/code/Magento/Tax/composer.json b/app/code/Magento/Tax/composer.json index 65c668553cd14..2fe0597c85a63 100644 --- a/app/code/Magento/Tax/composer.json +++ b/app/code/Magento/Tax/composer.json @@ -19,7 +19,8 @@ "magento/module-reports": "*", "magento/module-sales": "*", "magento/module-shipping": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-ui": "*" }, "suggest": { "magento/module-tax-sample-data": "*" diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml index fec108d53948f..d7b04f8e29f27 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rate/js.phtml @@ -4,15 +4,20 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** @var \Magento\Tax\Block\Adminhtml\Rate\Form $tmpBlock */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php /** @var \Magento\Directory\Helper\Data $jsonHelper */ +$jsonHelper = $tmpBlock->getData('directoryHelper'); +$regionJson = /* @noEscape */ $jsonHelper->getRegionJson(); +$scriptString = <<<script require([ "jquery", "mage/adminhtml/form" ], function(jQuery){ - var updater = new RegionUpdater('tax_country_id', 'tax_region', 'tax_region_id', <?= /* @noEscape */ $this->helper(\Magento\Directory\Helper\Data::class)->getRegionJson() ?>, 'disable'); + var updater = new RegionUpdater('tax_country_id', 'tax_region', 'tax_region_id', {$regionJson}, 'disable'); updater.disableRegionValidation(); (function ($) { @@ -54,4 +59,6 @@ require([ window.updater = updater; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml index 3558d359aa4d6..0141101ef5a78 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rule/edit.phtml @@ -5,8 +5,12 @@ */ /** @var $block \Magento\Tax\Block\Adminhtml\Rule\Edit\Form */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $formElementId = /* @noEscape */ \Magento\Tax\Block\Adminhtml\Rate\Form::FORM_ELEMENT_ID; +$jsId = /* @noEscape */ $block->getJsId(); +//phpcs:ignore Magento2.SQL.RawQuery +$scriptString = <<<script require([ 'jquery', 'Magento_Ui/js/modal/alert', @@ -77,7 +81,7 @@ require([ $.ajax({ type: "POST", data: {id:id}, - url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateLoadUrl())) ?>', + url: '{$block->escapeJs($block->getTaxRateLoadUrl())}', success: function(result, status) { $('body').trigger('processStop'); if (result.success) { @@ -94,14 +98,14 @@ require([ }); else alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }, error: function () { $('body').trigger('processStop'); alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); }, dataType: "json" @@ -112,9 +116,9 @@ require([ var options = { mselectContainer: '#tax_rate + section.mselect-list', toggleAddButton:false, - addText: '<?= $block->escapeJs($block->escapeHtml(__('Add New Tax Rate'))) ?>', + addText: '{$block->escapeJs(__('Add New Tax Rate'))}', parse: null, - nextPageUrl: '<?= $block->escapeHtml($block->getTaxRatesPageUrl()) ?>', + nextPageUrl: '{$block->escapeJs($block->getTaxRatesPageUrl())}', selectedValues: this.settings.selected_values, mselectInputSubmitCallback: function (value, options) { var select = $('#tax_rate'); @@ -137,7 +141,7 @@ require([ var taxRate = $('#tax_rate'), taxRateField = taxRate.parent(), taxRateForm = $('#tax-rate-form'), - taxRateFormElement = $('#<?= /* @noEscape */ \Magento\Tax\Block\Adminhtml\Rate\Form::FORM_ELEMENT_ID ?>'); + taxRateFormElement = $('#{$formElementId}'); if (!this.isEntityEditable) { // Override default layout of editable multiselect @@ -162,11 +166,16 @@ require([ .on('click.mselect-edit', '.mselect-edit', this.edit) .on("click.mselect-delete", ".mselect-delete", function () { var that = $(this), - select = that.closest('.mselect-list').prev(), + +script; + // phpcs:ignore Magento2.SQL.RawQuery + $scriptString .= "select = that.closest('.mselect-list').prev()," . PHP_EOL; + $scriptString .= <<<script + rateValue = that.parent().find('input[type="checkbox"]').val(); confirm({ - content: '<?= $block->escapeJs(__('Do you really want to delete this tax rate?')) ?>', + content: '{$block->escapeJs(__('Do you really want to delete this tax rate?'))}', actions: { /** * Confirm action. @@ -180,7 +189,7 @@ require([ form_key: $('input[name="form_key"]').val() }, dataType: 'json', - url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateDeleteUrl())) ?>', + url: '{$block->escapeJs($block->getTaxRateDeleteUrl())}', success: function(result, status) { $('body').trigger('processStop'); if (result.success) { @@ -198,14 +207,14 @@ require([ }); else alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }, error: function () { $('body').trigger('processStop'); alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }; @@ -228,15 +237,15 @@ require([ taxRateFormElement.mage('form').mage('validation'); taxRateForm.dialogRates({ - title: '<?= $block->escapeJs($block->escapeHtml(__('Tax Rate'))) ?>', + title: '{$block->escapeJs(__('Tax Rate'))}', type: 'slide', - id: '<?= /* @noEscape */ $block->getJsId() ?>', + id: '{$jsId}', modalClass: 'tax-rate-popup', closed: function () { taxRateFormElement.data('validation').clearError(); }, buttons: [{ - text: '<?= $block->escapeJs($block->escapeHtml(__('Save'))) ?>', + text: '{$block->escapeJs(__('Save'))}', 'class': 'action-save action-primary', click: function() { this.updateItemRate(); @@ -244,7 +253,12 @@ require([ itemRateData = $.extend({}, itemRate); if (itemRateData.itemElement) { - delete itemRateData.itemElement; + +script; + //phpcs:ignore Magento2.SQL.RawQuerys + $scriptString .= ' delete itemRateData.itemElement;'; +$scriptString.= <<<script + } if (!taxRateFormElement.validation().valid()) { @@ -256,7 +270,7 @@ require([ type: 'POST', data: itemRateData, dataType: 'json', - url: '<?= $block->escapeJs($block->escapeUrl($block->getTaxRateSaveUrl())) ?>', + url: '{$block->escapeJs($block->getTaxRateSaveUrl())}', success: function(result, status) { $('body').trigger('processStop'); if (result.success) { @@ -281,14 +295,14 @@ require([ }); else alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }, error: function () { $('body').trigger('processStop'); alert({ - content: '<?= $block->escapeJs($block->escapeHtml(__('An error occurred'))) ?>' + content: '{$block->escapeJs(__('An error occurred'))}' }); } }; @@ -302,4 +316,6 @@ require([ window.TaxRateEditableMultiselect = TaxRateEditableMultiselect; }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml b/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml index f09af05303f36..e6b7282cf4d27 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/rule/rate/form.phtml @@ -10,7 +10,7 @@ <div class="grid-loader"></div> </div> -<div class="form-inline" id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>" style="display:none"> +<div class="form-inline no-display" id="<?= $block->escapeHtmlAttr($block->getNameInLayout()) ?>"> <?= $block->getFormHtml() ?> <?= $block->getChildHtml('form_after') ?> </div> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml index d5017f83affe4..8a4cfd9d6b574 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/add.phtml @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ // @deprecated +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> - <button type="button" onclick="window.location.href='<?= $block->escapeUrl($createUrl) ?>'"> + <button type="button" id="addNewClass"> <?= $block->escapeHtml(__('Add New Class')) ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.location.href='" . $block->escapeJs($createUrl) . "'", + 'button#addNewClass' + ) ?> </div> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml index fa9fcb8fbcfcd..91860c70fd086 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/class/save.phtml @@ -4,19 +4,23 @@ * See COPYING.txt for license details. */ // @deprecated + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> <?= $block->getResetButtonHtml() ?> <?= $block->getSaveButtonHtml() ?> </div> -<?php if ($form) : ?> +<?php if ($form): ?> <?= $form->toHtml() ?> - <script> + <?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ - jQuery('#<?= $block->escapeJs($form->getForm()->getId()) ?>').mage('form').mage('validation'); + jQuery('#{$block->escapeJs($form->getForm()->getId())}').mage('form').mage('validation'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml index 58c79bbfe9715..7053cd61e29ac 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rate/save.phtml @@ -3,16 +3,19 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Tax\Block\Adminhtml\Rate\Form $form */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($form) : ?> +<?php if ($form): ?> <?= $form->toHtml() ?> - <script> + <?php $scriptString = <<<script require([ "jquery", "mage/mage" ], function($){ - $('#<?= $block->escapeJs($form->getForm()->getId()) ?>').mage('form').mage('validation'); + $('#{$block->escapeJs($form->getForm()->getId())}').mage('form').mage('validation'); $(document).ready(function () { 'use strict'; @@ -42,5 +45,7 @@ }); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml index e21dbb099ff5d..f7af88b8207dc 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/add.phtml @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ // @deprecated +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> - <button type="button" onclick="window.location.href='<?= $block->escapeUrl($createUrl) ?>'"> + <button type="button" id="addNewTaxRule"> <?= $block->escapeHtml(__('Add New Tax Rule')) ?> </button> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "window.location.href='" . $block->escapeJs($createUrl) . "'", + 'button#addNewClass' + ) ?> </div> diff --git a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml index 10251e2805f2f..3830be23d80a6 100644 --- a/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml +++ b/app/code/Magento/Tax/view/adminhtml/templates/toolbar/rule/save.phtml @@ -4,6 +4,8 @@ * See COPYING.txt for license details. */ // @deprecated + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-mage-init='{"floatingHeader": {}}' class="page-actions"> <?= $block->getBackButtonHtml() ?> @@ -11,13 +13,15 @@ <?= $block->getSaveButtonHtml() ?> <?= $block->getDeleteButtonHtml() ?> </div> -<?php if ($form) : ?> +<?php if ($form): ?> <?= $form->toHtml() ?> - <script> + <?php $scriptString = <<<script require(['jquery', "mage/mage"], function(jQuery){ - jQuery('#<?= $block->escapeJs($form->getForm()->getId()) ?>').mage('form').mage('validation'); + jQuery('#{$block->escapeJs($form->getForm()->getId())}').mage('form').mage('validation'); }); - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/frontend/templates/checkout/grandtotal.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/grandtotal.phtml index df177b6180511..e9a92120f8cf1 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/grandtotal.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/grandtotal.phtml @@ -4,41 +4,54 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Grandtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $style = $block->escapeHtmlAttr($block->getStyle()); $colspan = (int) $block->getColspan(); +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> -<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0) : ?> +<?php if ($block->includeTax() && $block->getTotalExclTax() >= 0): ?> <tr class="grand totals excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <strong><?= $block->escapeHtml(__('Grand Total Excl. Tax')) ?></strong> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Excl. Tax')) ?>"> - <strong><?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotalExclTax()) ?></strong> + <td class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Excl. Tax')) ?>"> + <strong><?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotalExclTax()) ?></strong> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.excl td.amount') ?> + <?php endif; ?> <?= /* @noEscape */ $block->renderTotals('taxes', $colspan) ?> <tr class="grand totals incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <strong><?= $block->escapeHtml(__('Grand Total Incl. Tax')) ?></strong> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Incl. Tax')) ?>"> - <strong><?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()) ?></strong> + <td class="amount" data-th="<?= $block->escapeHtmlAttr(__('Grand Total Incl. Tax')) ?>"> + <strong><?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> -<?php else : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals.incl td.amount') ?> + <?php endif; ?> +<?php else: ?> <tr class="grand totals"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <strong><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></strong> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <strong><?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()) ?></strong> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <strong><?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValue()) ?></strong> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.grand.totals td.amount') ?> + <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml index 3f5a55e5fa325..e2989d8313283 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/shipping.phtml @@ -4,52 +4,70 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Shipping * @see \Magento\Tax\Block\Checkout\Shipping + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->displayShipping()) : ?> +<?php if ($block->displayShipping()): ?> <?php $style = $block->escapeHtmlAttr($block->getStyle()); $colspan = (int) $block->getColspan(); + /** @var \Magento\Checkout\Helper\Data $checkoutHelper */ + $checkoutHelper = $block->getData('checkoutHelper'); ?> - <?php if ($block->displayBoth()) : ?> + <?php if ($block->displayBoth()): ?> <tr class="totals shipping excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getExcludeTaxLabel()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getExcludeTaxLabel()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingExcludeTax()) ?> + + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getExcludeTaxLabel()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl td.amount') ?> + <?php endif; ?> <tr class="totals shipping incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getIncludeTaxLabel()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getIncludeTaxLabel()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingIncludeTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getIncludeTaxLabel()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> - <?php elseif ($block->displayIncludeTax()) : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl td.amount') ?> + <?php endif; ?> + <?php elseif ($block->displayIncludeTax()): ?> <tr class="totals shipping incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingIncludeTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingIncludeTax()) ?> </td> </tr> - <?php else : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.incl td.amount') ?> + <?php endif; ?> + <?php else: ?> <tr class="totals shipping excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getShippingExcludeTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getShippingExcludeTax()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.shipping.excl td.amount') ?> + <?php endif; ?> <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml index 010a7b8dcfe4a..dc9034fc9f694 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/subtotal.phtml @@ -4,41 +4,54 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Subtotal * @see \Magento\Tax\Block\Checkout\Subtotal + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $style = $block->escapeHtmlAttr($block->getStyle()); $colspan = (int) $block->getColspan(); +/** @var \Magento\Checkout\Helper\Data $checkoutHelper */ +$checkoutHelper = $block->getData('checkoutHelper'); ?> -<?php if ($block->displayBoth()) : ?> +<?php if ($block->displayBoth()): ?> <tr class="totals sub excl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml(__('Subtotal (Excl. Tax)')) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Excl. Tax)')) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValueExclTax()) ?> + <tdclass="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Excl. Tax)')) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValueExclTax()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.excl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.excl td.amount') ?> + <?php endif; ?> <tr class="totals sub incl"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml(__('Subtotal (Incl. Tax)')) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Incl. Tax)')) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValueInclTax()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr(__('Subtotal (Incl. Tax)')) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValueInclTax()) ?> </td> </tr> -<?php else : ?> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.incl th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub.incl td.amount') ?> + <?php endif; ?> +<?php else: ?> <tr class="totals sub"> - <th style="<?= /* @noEscape */ $style ?>" class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> + <th class="mark" colspan="<?= /* @noEscape */ $colspan ?>" scope="row"> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> </th> - <td style="<?= /* @noEscape */ $style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($block->getTotal()->getValue()) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($block->getTotal()->getValue()) ?> </td> </tr> + <?php if ($style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($style, 'tr.totals.sub td.amount') ?> + <?php endif; ?> <?php endif; ?> diff --git a/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml b/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml index 0329db406fa16..e265c029578a6 100644 --- a/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml +++ b/app/code/Magento/Tax/view/frontend/templates/checkout/tax.phtml @@ -4,60 +4,72 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - /** * @var $block \Magento\Tax\Block\Checkout\Tax * @see \Magento\Tax\Block\Checkout\Tax + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_value = $block->getTotal()->getValue(); $_style = $block->escapeHtmlAttr($block->getTotal()->getStyle()); - + /** @var \Magento\Tax\Helper\Data $taxHelper */ + $taxHelper = $block->getData('taxHelper'); + /** @var \Magento\Checkout\Helper\Data $checkoutHelper */ + $checkoutHelper = $block->getData('checkoutHelper'); $attributes = 'class="totals-tax"'; -if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary() && $_value != 0) { - $attributes = 'class="totals-tax-summary" data-mage-init=\'{"toggleAdvanced": {"selectorsToggleClass": "shown", "baseToggleClass": "expanded", "toggleContainers": ".totals-tax-details"}}\''; +if ($taxHelper->displayFullSummary() && $_value != 0) { + $attributes = 'class="totals-tax-summary" data-mage-init=\'{"toggleAdvanced": {"selectorsToggleClass": "shown", + "baseToggleClass": "expanded", "toggleContainers": ".totals-tax-details"}}\''; } ?> <tr <?= /* @noEscape */ $attributes ?>> - <th style="<?= /* @noEscape */ $_style ?>" class="mark" colspan="<?= (int) $block->getColspan() ?>" scope="row"> - <?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary()) : ?> + <th class="mark" colspan="<?= (int) $block->getColspan() ?>" scope="row"> + <?php if ($taxHelper->displayFullSummary()): ?> <span class="detailed"><?= $block->escapeHtml($block->getTotal()->getTitle()) ?></span> - <?php else : ?> + <?php else: ?> <?= $block->escapeHtml($block->getTotal()->getTitle()) ?> <?php endif; ?> </th> - <td style="<?= /* @noEscape */ $_style ?>" class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($_value) ?> + <td class="amount" data-th="<?= $block->escapeHtmlAttr($block->getTotal()->getTitle()) ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($_value) ?> </td> </tr> +<?php if ($_style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax td.amount') ?> +<?php endif; ?> -<?php if ($this->helper(\Magento\Tax\Helper\Data::class)->displayFullSummary() && $_value != 0) : ?> - <?php foreach ($block->getTotal()->getFullInfo() as $info) : ?> +<?php if ($taxHelper->displayFullSummary() && $_value != 0): ?> + <?php foreach ($block->getTotal()->getFullInfo() as $info): ?> <?php if (isset($info['hidden']) && $info['hidden']) { continue; } ?> <?php $percent = $info['percent']; ?> <?php $amount = $info['amount']; ?> <?php $rates = $info['rates']; ?> <?php $isFirst = 1; ?> - <?php foreach ($rates as $rate) : ?> + <?php foreach ($rates as $rate): ?> <tr class="totals-tax-details"> - <th class="mark" style="<?= /* @noEscape */ $_style ?>" colspan="<?= (int) $block->getColspan() ?>" scope="row"> + <th class="mark" colspan="<?= (int) $block->getColspan() ?>" scope="row"> <?= $block->escapeHtml($rate['title']) ?> - <?php if ($rate['percent'] !== null) : ?> + <?php if ($rate['percent'] !== null): ?> (<?= (float) $rate['percent'] ?>%) <?php endif; ?> </th> - <?php if ($isFirst) : ?> - <td style="<?= /* @noEscape */ $_style ?>" class="amount" rowspan="<?= count($rates) ?>" - data-th="<?= $block->escapeHtmlAttr($rate['title']) ?><?php if ($rate['percent'] !== null) : ?>(<?= (float) $rate['percent'] ?>%)<?php endif; ?>"> - <?= /* @noEscape */ $this->helper(\Magento\Checkout\Helper\Data::class)->formatPrice($amount) ?> + <?php if ($isFirst): ?> + <td class="amount" rowspan="<?= count($rates) ?>" + data-th="<?= $block->escapeHtmlAttr($rate['title']) ?> + <?php if ($rate['percent'] !== null): ?>(<?= (float) $rate['percent'] ?>%)<?php endif; ?>"> + <?= /* @noEscape */ $checkoutHelper->formatPrice($amount) ?> </td> <?php endif; ?> </tr> + <?php if ($_style): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax-details th.mark') ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag($_style, 'tr.totals-tax-details td.amount') ?> + <?php endif; ?> <?php $isFirst = 0; ?> <?php endforeach; ?> <?php endforeach; ?> diff --git a/app/code/Magento/TaxImportExport/composer.json b/app/code/Magento/TaxImportExport/composer.json index ee24deb9d3246..01c069b4299c1 100644 --- a/app/code/Magento/TaxImportExport/composer.json +++ b/app/code/Magento/TaxImportExport/composer.json @@ -10,7 +10,8 @@ "magento/module-backend": "*", "magento/module-directory": "*", "magento/module-store": "*", - "magento/module-tax": "*" + "magento/module-tax": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml index 1c6b267cd9289..79d833771768d 100644 --- a/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml +++ b/app/code/Magento/TaxImportExport/view/adminhtml/templates/importExport.phtml @@ -5,11 +5,12 @@ */ /** @var $block \Magento\TaxImportExport\Block\Adminhtml\Rate\ImportExport */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div class="import-export-tax-rates"> - <?php if (!$block->getIsReadonly()) :?> + <?php if (!$block->getIsReadonly()):?> <div class="import-tax-rates"> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> <form id="import-form" class="admin__fieldset" action="<?= $block->escapeUrl($block->getUrl('tax/rate/importPost')) ?>" @@ -18,7 +19,9 @@ <?php endif; ?> <?= $block->getBlockHtml('formkey') ?> <div class="fieldset admin__field"> - <label for="import_rates_file" class="admin__field-label"><span><?= $block->escapeHtml(__('Import Tax Rates')) ?></span></label> + <label for="import_rates_file" class="admin__field-label"> + <span><?= $block->escapeHtml(__('Import Tax Rates')) ?></span> + </label> <div class="admin__field-control"> <input type="file" id="import_rates_file" @@ -27,11 +30,13 @@ <?= $block->getButtonHtml(__('Import Tax Rates'), '', 'import-submit') ?> </div> </div> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> </form> <?php endif; ?> - <script> -require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'mage/translate'], function(jQuery, uiAlert){ + <?php $scriptString = <<<script + + require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'mage/translate'], + function(jQuery, uiAlert){ jQuery('#import-form').mage('form').mage('validation'); (function ($) { @@ -51,11 +56,14 @@ require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'ma })(jQuery); }); -</script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> </div> <?php endif; ?> <div class="export-tax-rates <?= ($block->getIsReadonly()) ? 'box-left' : 'box-right' ?>"> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> <form id="export_form" class="admin__fieldset" action="<?= $block->escapeUrl($block->getUrl('tax/rate/exportPost')) ?>" @@ -69,7 +77,7 @@ require(['jquery', 'Magento_Ui/js/modal/alert', "mage/mage", "loadingPopup", 'ma <?= $block->getButtonHtml(__('Export Tax Rates'), "this.form.submit()") ?> </div> </div> - <?php if ($block->getUseContainer()) :?> + <?php if ($block->getUseContainer()):?> </form> <?php endif; ?> </div> diff --git a/app/code/Magento/Theme/Block/Html/Footer.php b/app/code/Magento/Theme/Block/Html/Footer.php index cdb350336f38f..7f9b9cf86a809 100644 --- a/app/code/Magento/Theme/Block/Html/Footer.php +++ b/app/code/Magento/Theme/Block/Html/Footer.php @@ -127,6 +127,7 @@ public function getIdentities() * Get block cache life time * * @return int + * @since 100.2.4 */ protected function getCacheLifetime() { diff --git a/app/code/Magento/Theme/Block/Html/Header/Logo.php b/app/code/Magento/Theme/Block/Html/Header/Logo.php index 626a771b4e309..792ee95de4995 100644 --- a/app/code/Magento/Theme/Block/Html/Header/Logo.php +++ b/app/code/Magento/Theme/Block/Html/Header/Logo.php @@ -43,7 +43,7 @@ public function __construct( /** * Check if current url is url for home page * - * @deprecated This function is no longer used. It was previously used by + * @deprecated 101.0.1 This function is no longer used. It was previously used by * Magento/Theme/view/frontend/templates/html/header/logo.phtml * to check if the logo should be clickable on the homepage. * diff --git a/app/code/Magento/Theme/Block/Html/Pager.php b/app/code/Magento/Theme/Block/Html/Pager.php index 5798b94e31a70..764b2e9ca42f0 100644 --- a/app/code/Magento/Theme/Block/Html/Pager.php +++ b/app/code/Magento/Theme/Block/Html/Pager.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Theme\Block\Html; /** @@ -466,7 +467,26 @@ public function getPageUrl($page) */ public function getLimitUrl($limit) { - return $this->getPagerUrl([$this->getLimitVarName() => $limit]); + return $this->getPagerUrl($this->getPageLimitParams($limit)); + } + + /** + * Return page limit params + * + * @param int $limit + * @return array + */ + private function getPageLimitParams(int $limit): array + { + $data = [$this->getLimitVarName() => $limit]; + + $currentPage = $this->getCurrentPage(); + $availableCount = (int) ceil($this->getTotalNum() / $limit); + if ($currentPage !== 1 && $availableCount < $currentPage) { + $data = array_merge($data, [$this->getPageVarName() => $availableCount === 1 ? null : $availableCount]); + } + + return $data; } /** diff --git a/app/code/Magento/Theme/Block/Html/Title.php b/app/code/Magento/Theme/Block/Html/Title.php index 9059afe19ab05..a2ef83117ccf5 100644 --- a/app/code/Magento/Theme/Block/Html/Title.php +++ b/app/code/Magento/Theme/Block/Html/Title.php @@ -101,7 +101,7 @@ public function setPageTitle($pageTitle) private function shouldTranslateTitle(): bool { return $this->scopeConfig->isSetFlag( - static::XML_PATH_HEADER_TRANSLATE_TITLE, + self::XML_PATH_HEADER_TRANSLATE_TITLE, ScopeInterface::SCOPE_STORE ); } diff --git a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php index fc396615e71e7..fb2ceb0a91fc9 100644 --- a/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php +++ b/app/code/Magento/Theme/Controller/Adminhtml/System/Design/Theme/UploadJs.php @@ -11,7 +11,7 @@ /** * Class UploadJs - * @deprecated + * @deprecated 101.0.0 */ class UploadJs extends \Magento\Theme\Controller\Adminhtml\System\Design\Theme implements HttpGetActionInterface { diff --git a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php index 83172df748a47..10cba6e869030 100644 --- a/app/code/Magento/Theme/Controller/Result/MessagePlugin.php +++ b/app/code/Magento/Theme/Controller/Result/MessagePlugin.php @@ -14,6 +14,8 @@ /** * Plugin for putting messages to cookies + * + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ class MessagePlugin { @@ -116,7 +118,6 @@ public function afterRenderResult( * ], * ] * - * * @param array $messages List of Magento messages that must be set as 'mage-messages' cookie. * @return void */ diff --git a/app/code/Magento/Theme/Helper/Storage.php b/app/code/Magento/Theme/Helper/Storage.php index e41bc8b145e38..aff70c5d1ee97 100644 --- a/app/code/Magento/Theme/Helper/Storage.php +++ b/app/code/Magento/Theme/Helper/Storage.php @@ -11,6 +11,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem\DriverInterface; /** * Handles the storage of media files like images and fonts. @@ -97,6 +98,10 @@ class Storage extends \Magento\Framework\App\Helper\AbstractHelper * @var \Magento\Framework\Filesystem\Io\File */ private $file; + /** + * @var DriverInterface + */ + private $filesystemDriver; /** * @param \Magento\Framework\App\Helper\Context $context @@ -105,6 +110,7 @@ class Storage extends \Magento\Framework\App\Helper\AbstractHelper * @param \Magento\Framework\View\Design\Theme\FlyweightFactory $themeFactory * @param \Magento\Framework\Filesystem\Io\File|null $file * + * @param DriverInterface|null $filesystemDriver * @throws \Magento\Framework\Exception\FileSystemException * @throws \Magento\Framework\Exception\ValidatorException */ @@ -113,7 +119,8 @@ public function __construct( \Magento\Framework\Filesystem $filesystem, \Magento\Backend\Model\Session $session, \Magento\Framework\View\Design\Theme\FlyweightFactory $themeFactory, - \Magento\Framework\Filesystem\Io\File $file = null + \Magento\Framework\Filesystem\Io\File $file = null, + DriverInterface $filesystemDriver = null ) { parent::__construct($context); $this->filesystem = $filesystem; @@ -124,6 +131,7 @@ public function __construct( $this->file = $file ?: ObjectManager::getInstance()->get( \Magento\Framework\Filesystem\Io\File::class ); + $this->filesystemDriver = $filesystemDriver ?: ObjectManager::getInstance()->get(DriverInterface::class); } /** @@ -247,7 +255,16 @@ public function getCurrentPath() if ($path && $path !== self::NODE_ROOT) { $path = $this->convertIdToPath($path); - if ($this->mediaDirectoryWrite->isDirectory($path) && 0 === strpos($path, (string) $currentPath)) { + $path = $this->filesystemDriver->getRealPathSafety($path); + + if (strpos($path, $currentPath) !== 0) { + $path = $currentPath; + } + + if ($this->mediaDirectoryWrite->isDirectory($path) + && strpos($path, $currentPath) === 0 + && $path !== $currentPath + ) { $currentPath = $this->mediaDirectoryWrite->getRelativePath($path); } } diff --git a/app/code/Magento/Theme/Model/Config/Customization.php b/app/code/Magento/Theme/Model/Config/Customization.php index 6a6872d794b1b..7430730451110 100644 --- a/app/code/Magento/Theme/Model/Config/Customization.php +++ b/app/code/Magento/Theme/Model/Config/Customization.php @@ -5,23 +5,34 @@ */ namespace Magento\Theme\Model\Config; +use Magento\Framework\App\Area; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Design\Theme\ThemeProviderInterface; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; +use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver; + /** * Theme customization config model */ class Customization { /** - * @var \Magento\Store\Model\StoreManagerInterface + * @var StoreManagerInterface */ protected $_storeManager; /** - * @var \Magento\Framework\View\DesignInterface + * @var DesignInterface */ protected $_design; /** - * @var \Magento\Framework\View\Design\Theme\ThemeProviderInterface + * @var ThemeProviderInterface */ protected $themeProvider; @@ -40,20 +51,28 @@ class Customization * @see self::_prepareThemeCustomizations() */ protected $_unassignedTheme; + /** + * @var StoreUserAgentThemeResolver|mixed|null + */ + private $storeThemesResolver; /** - * @param \Magento\Store\Model\StoreManagerInterface $storeManager - * @param \Magento\Framework\View\DesignInterface $design - * @param \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider + * @param StoreManagerInterface $storeManager + * @param DesignInterface $design + * @param ThemeProviderInterface $themeProvider + * @param StoreThemesResolverInterface|null $storeThemesResolver */ public function __construct( - \Magento\Store\Model\StoreManagerInterface $storeManager, - \Magento\Framework\View\DesignInterface $design, - \Magento\Framework\View\Design\Theme\ThemeProviderInterface $themeProvider + StoreManagerInterface $storeManager, + DesignInterface $design, + ThemeProviderInterface $themeProvider, + ?StoreThemesResolverInterface $storeThemesResolver = null ) { $this->_storeManager = $storeManager; $this->_design = $design; $this->themeProvider = $themeProvider; + $this->storeThemesResolver = $storeThemesResolver + ?? ObjectManager::getInstance()->get(StoreThemesResolverInterface::class); } /** @@ -93,13 +112,14 @@ public function getStoresByThemes() { $storesByThemes = []; $stores = $this->_storeManager->getStores(); - /** @var $store \Magento\Store\Model\Store */ + /** @var $store Store */ foreach ($stores as $store) { - $themeId = $this->_getConfigurationThemeId($store); - if (!isset($storesByThemes[$themeId])) { - $storesByThemes[$themeId] = []; + foreach ($this->storeThemesResolver->getThemes($store) as $themeId) { + if (!isset($storesByThemes[$themeId])) { + $storesByThemes[$themeId] = []; + } + $storesByThemes[$themeId][] = $store; } - $storesByThemes[$themeId][] = $store; } return $storesByThemes; } @@ -107,8 +127,8 @@ public function getStoresByThemes() /** * Check if current theme has assigned to any store * - * @param \Magento\Framework\View\Design\ThemeInterface $theme - * @param null|\Magento\Store\Model\Store $store + * @param ThemeInterface $theme + * @param null|Store $store * @return bool */ public function isThemeAssignedToStore($theme, $store = null) @@ -133,8 +153,8 @@ public function hasThemeAssigned() /** * Is theme assigned to specific store * - * @param \Magento\Framework\View\Design\ThemeInterface $theme - * @param \Magento\Store\Model\Store $store + * @param ThemeInterface $theme + * @param Store $store * @return bool */ protected function _isThemeAssignedToSpecificStore($theme, $store) @@ -145,21 +165,21 @@ protected function _isThemeAssignedToSpecificStore($theme, $store) /** * Get configuration theme id * - * @param \Magento\Store\Model\Store $store + * @param Store $store * @return int */ protected function _getConfigurationThemeId($store) { return $this->_design->getConfigurationDesignTheme( - \Magento\Framework\App\Area::AREA_FRONTEND, + Area::AREA_FRONTEND, ['store' => $store] ); } /** * Fetch theme customization and sort them out to arrays: - * self::_assignedTheme and self::_unassignedTheme. * + * Set self::_assignedTheme and self::_unassignedTheme. * NOTE: To get into "assigned" list theme customization not necessary should be assigned to store-view directly. * It can be set to website or as default theme and be used by store-view via config fallback mechanism. * @@ -167,15 +187,15 @@ protected function _getConfigurationThemeId($store) */ protected function _prepareThemeCustomizations() { - /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $themeCollection */ - $themeCollection = $this->themeProvider->getThemeCustomizations(\Magento\Framework\App\Area::AREA_FRONTEND); + /** @var Collection $themeCollection */ + $themeCollection = $this->themeProvider->getThemeCustomizations(Area::AREA_FRONTEND); $assignedThemes = $this->getStoresByThemes(); $this->_assignedTheme = []; $this->_unassignedTheme = []; - /** @var $theme \Magento\Framework\View\Design\ThemeInterface */ + /** @var $theme ThemeInterface */ foreach ($themeCollection as $theme) { if (isset($assignedThemes[$theme->getId()])) { $theme->setAssignedStores($assignedThemes[$theme->getId()]); diff --git a/app/code/Magento/Theme/Model/Design/Backend/File.php b/app/code/Magento/Theme/Model/Design/Backend/File.php index 8f81ace8c9047..143889364781f 100644 --- a/app/code/Magento/Theme/Model/Design/Backend/File.php +++ b/app/code/Magento/Theme/Model/Design/Backend/File.php @@ -4,10 +4,12 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Theme\Model\Design\Backend; -use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; use Magento\Config\Model\Config\Backend\File as BackendFile; +use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; use Magento\Framework\App\Cache\TypeListInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; @@ -15,13 +17,14 @@ use Magento\Framework\Exception\LocalizedException; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File as IoFileSystem; use Magento\Framework\Model\Context; use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Registry; use Magento\Framework\UrlInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\MediaStorage\Model\File\UploaderFactory; use Magento\Theme\Model\Design\Config\FileUploader\FileProcessor; -use Magento\MediaStorage\Helper\File\Storage\Database; /** * File Backend @@ -40,6 +43,11 @@ class File extends BackendFile */ private $mime; + /** + * @var IoFileSystem + */ + private $ioFileSystem; + /** * @var Database */ @@ -58,6 +66,7 @@ class File extends BackendFile * @param AbstractDb|null $resourceCollection * @param array $data * @param Database $databaseHelper + * @param IoFileSystem $ioFileSystem * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -72,7 +81,8 @@ public function __construct( AbstractResource $resource = null, AbstractDb $resourceCollection = null, array $data = [], - Database $databaseHelper = null + Database $databaseHelper = null, + IoFileSystem $ioFileSystem = null ) { parent::__construct( $context, @@ -88,6 +98,7 @@ public function __construct( ); $this->urlBuilder = $urlBuilder; $this->databaseHelper = $databaseHelper ?: ObjectManager::getInstance()->get(Database::class); + $this->ioFileSystem = $ioFileSystem ?: ObjectManager::getInstance()->get(IoFileSystem::class); } /** @@ -108,11 +119,21 @@ public function beforeSave() __('%1 does not contain field \'file\'', $this->getData('field_config/field')) ); } + + if (!empty($this->getAllowedExtensions()) && + (!isset($this->ioFileSystem->getPathInfo($file)['extension']) || + !in_array($this->ioFileSystem->getPathInfo($file)['extension'], $this->getAllowedExtensions())) + ) { + throw new LocalizedException( + __('Something is wrong with the file upload settings.') + ); + } + if (isset($value['exists'])) { $this->setValue($file); return $this; } - + //phpcs:ignore Magento2.Functions.DiscouragedFunction $this->updateMediaDirectory(basename($file), $value['url']); @@ -196,7 +217,7 @@ protected function getStoreMediaUrl($fileName) $urlType = ['_type' => empty($baseUrl['type']) ? 'link' : (string)$baseUrl['type']]; $baseUrl = $baseUrl['value'] . '/'; } - return $this->urlBuilder->getBaseUrl($urlType) . $baseUrl . $fileName; + return $this->urlBuilder->getBaseUrl($urlType) . $baseUrl . $fileName; } /** diff --git a/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php b/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php index c4a7bb11a78f7..4ee6880c8190d 100644 --- a/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php +++ b/app/code/Magento/Theme/Model/ResourceModel/Theme/Grid/Collection.php @@ -7,7 +7,7 @@ /** * Theme grid collection - * @deprecated + * @deprecated 101.0.0 * @see \Magento\Theme\Ui\Component\Theme\DataProvider\SearchResult */ class Collection extends \Magento\Theme\Model\ResourceModel\Theme\Collection diff --git a/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php new file mode 100644 index 0000000000000..26bd5604294d1 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreDefaultThemeResolver.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store default theme resolver. + * + * Use system config fallback mechanism if no theme is directly assigned to the store-view. + */ +class StoreDefaultThemeResolver implements StoreThemesResolverInterface +{ + /** + * @var CollectionFactory + */ + private $themeCollectionFactory; + /** + * @var DesignInterface + */ + private $design; + /** + * @var ThemeInterface[] + */ + private $registeredThemes; + + /** + * @param CollectionFactory $themeCollectionFactory + * @param DesignInterface $design + */ + public function __construct( + CollectionFactory $themeCollectionFactory, + DesignInterface $design + ) { + $this->design = $design; + $this->themeCollectionFactory = $themeCollectionFactory; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $theme = $this->design->getConfigurationDesignTheme( + Area::AREA_FRONTEND, + ['store' => $store] + ); + $themes = []; + if ($theme) { + if (!is_numeric($theme)) { + $registeredThemes = $this->getRegisteredThemes(); + if (isset($registeredThemes[$theme])) { + $themes[] = $registeredThemes[$theme]->getId(); + } + } else { + $themes[] = $theme; + } + } + return $themes; + } + + /** + * Get system registered themes. + * + * @return ThemeInterface[] + */ + private function getRegisteredThemes(): array + { + if ($this->registeredThemes === null) { + $this->registeredThemes = []; + /** @var \Magento\Theme\Model\ResourceModel\Theme\Collection $collection */ + $collection = $this->themeCollectionFactory->create(); + $themes = $collection->loadRegisteredThemes(); + /** @var ThemeInterface $theme */ + foreach ($themes as $theme) { + $this->registeredThemes[$theme->getCode()] = $theme; + } + } + return $this->registeredThemes; + } +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php new file mode 100644 index 0000000000000..5be86c08f7c51 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolver.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use InvalidArgumentException; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store associated themes resolver. + */ +class StoreThemesResolver implements StoreThemesResolverInterface +{ + /** + * @var StoreThemesResolverInterface[] + */ + private $resolvers; + + /** + * @param StoreThemesResolverInterface[] $resolvers + */ + public function __construct( + array $resolvers + ) { + foreach ($resolvers as $resolver) { + if (!$resolver instanceof StoreThemesResolverInterface) { + throw new InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + StoreThemesResolverInterface::class, + get_class($resolver) + ) + ); + } + } + $this->resolvers = $resolvers; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $themes = []; + foreach ($this->resolvers as $resolver) { + foreach ($resolver->getThemes($store) as $theme) { + $themes[] = $theme; + } + } + return array_values(array_unique($themes)); + } +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php new file mode 100644 index 0000000000000..bb2cd73300c02 --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreThemesResolverInterface.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Store\Api\Data\StoreInterface; + +/** + * Store associated themes resolver. + */ +interface StoreThemesResolverInterface +{ + /** + * Get themes associated with a store view + * + * @param StoreInterface $store + * @return int[] + */ + public function getThemes(StoreInterface $store): array; +} diff --git a/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php b/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php new file mode 100644 index 0000000000000..fb5d68e37c99b --- /dev/null +++ b/app/code/Magento/Theme/Model/Theme/StoreUserAgentThemeResolver.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; + +/** + * Store associated themes in user-agent rules resolver, + */ +class StoreUserAgentThemeResolver implements StoreThemesResolverInterface +{ + private const XML_PATH_THEME_USER_AGENT = 'design/theme/ua_regexp'; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + /** + * @var Json + */ + private $serializer; + + /** + * @param ScopeConfigInterface $scopeConfig + * @param Json $serializer + */ + public function __construct( + ScopeConfigInterface $scopeConfig, + Json $serializer + ) { + $this->scopeConfig = $scopeConfig; + $this->serializer = $serializer; + } + + /** + * @inheritDoc + */ + public function getThemes(StoreInterface $store): array + { + $config = $this->scopeConfig->getValue( + self::XML_PATH_THEME_USER_AGENT, + ScopeInterface::SCOPE_STORE, + $store + ); + $rules = $config ? $this->serializer->unserialize($config) : []; + $themes = []; + if ($rules) { + $themes = array_values(array_unique(array_column($rules, 'value'))); + } + return $themes; + } +} diff --git a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php index edf8c148f8e68..5c38d99dd6a22 100644 --- a/app/code/Magento/Theme/Model/Wysiwyg/Storage.php +++ b/app/code/Magento/Theme/Model/Wysiwyg/Storage.php @@ -8,6 +8,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\App\ObjectManager; +use Magento\Framework\Filesystem\DriverInterface; /** * Theme wysiwyg storage model @@ -18,11 +19,15 @@ class Storage { /** * Type font + * + * Represents the font type */ const TYPE_FONT = 'font'; /** * Type image + * + * Represents the image type */ const TYPE_IMAGE = 'image'; @@ -82,6 +87,11 @@ class Storage */ private $file; + /** + * @var DriverInterface + */ + private $filesystemDriver; + /** * Initialize dependencies * @@ -92,6 +102,7 @@ class Storage * @param \Magento\Framework\Url\EncoderInterface $urlEncoder * @param \Magento\Framework\Url\DecoderInterface $urlDecoder * @param \Magento\Framework\Filesystem\Io\File|null $file + * @param DriverInterface|null $filesystemDriver * * @throws \Magento\Framework\Exception\FileSystemException */ @@ -102,7 +113,8 @@ public function __construct( \Magento\Framework\Image\AdapterFactory $imageFactory, \Magento\Framework\Url\EncoderInterface $urlEncoder, \Magento\Framework\Url\DecoderInterface $urlDecoder, - \Magento\Framework\Filesystem\Io\File $file = null + \Magento\Framework\Filesystem\Io\File $file = null, + DriverInterface $filesystemDriver = null ) { $this->mediaWriteDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); $this->_helper = $helper; @@ -113,6 +125,8 @@ public function __construct( $this->file = $file ?: ObjectManager::getInstance()->get( \Magento\Framework\Filesystem\Io\File::class ); + $this->filesystemDriver = $filesystemDriver ?: ObjectManager::getInstance() + ->get(DriverInterface::class); } /** @@ -327,8 +341,12 @@ public function deleteDirectory($path) { $rootCmp = rtrim($this->_helper->getStorageRoot(), '/'); $pathCmp = rtrim($path, '/'); + $absolutePath = rtrim( + $this->filesystemDriver->getRealPathSafety($this->mediaWriteDirectory->getAbsolutePath($path)), + '/' + ); - if ($rootCmp == $pathCmp) { + if ($rootCmp == $pathCmp || $rootCmp === $absolutePath) { throw new \Magento\Framework\Exception\LocalizedException( __('We can\'t delete root directory %1 right now.', $path) ); diff --git a/app/code/Magento/Theme/Plugin/Data/Collection.php b/app/code/Magento/Theme/Plugin/Data/Collection.php new file mode 100644 index 0000000000000..11ff95db25769 --- /dev/null +++ b/app/code/Magento/Theme/Plugin/Data/Collection.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Plugin\Data; + +use Magento\Framework\Data\Collection as DataCollection; + +/** + * Plugin to return last page if current page greater then collection size. + */ +class Collection +{ + /** + * Return last page if current page greater then last page. + * + * @param DataCollection $subject + * @param int $result + * @return int + */ + public function afterGetCurPage(DataCollection $subject, int $result): int + { + if ($result > $subject->getLastPageNumber()) { + $result = 1; + } + + return $result; + } +} diff --git a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php similarity index 88% rename from lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php rename to app/code/Magento/Theme/Plugin/LoadDesignPlugin.php index 2cda49c43c2ce..c4f8d3a905d0f 100644 --- a/lib/internal/Magento/Framework/App/Action/Plugin/LoadDesignPlugin.php +++ b/app/code/Magento/Theme/Plugin/LoadDesignPlugin.php @@ -4,7 +4,7 @@ * See COPYING.txt for license details. */ -namespace Magento\Framework\App\Action\Plugin; +namespace Magento\Theme\Plugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Config\Dom\ValidationException; @@ -21,12 +21,12 @@ class LoadDesignPlugin /** * @var DesignLoader */ - protected $_designLoader; + private $designLoader; /** * @var MessageManagerInterface */ - protected $messageManager; + private $messageManager; /** * @param DesignLoader $designLoader @@ -36,7 +36,7 @@ public function __construct( DesignLoader $designLoader, MessageManagerInterface $messageManager ) { - $this->_designLoader = $designLoader; + $this->designLoader = $designLoader; $this->messageManager = $messageManager; } @@ -50,7 +50,7 @@ public function __construct( public function beforeExecute(ActionInterface $subject) { try { - $this->_designLoader->load(); + $this->designLoader->load(); } catch (LocalizedException $e) { if ($e->getPrevious() instanceof ValidationException) { /** @var MessageInterface $message */ diff --git a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php index ac16c56b17f1b..fd0ef1db0219a 100644 --- a/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php +++ b/app/code/Magento/Theme/Test/Unit/Block/Html/PagerTest.php @@ -91,6 +91,60 @@ public function testGetPages(): void $this->assertEquals($expectedPages, $this->pager->getPages()); } + /** + * Test get limit url. + * + * @dataProvider limitUrlDataProvider + * + * @param int $page + * @param int $size + * @param int $limit + * @param array $expectedParams + * @return void + */ + public function testGetLimitUrl(int $page, int $size, int $limit, array $expectedParams): void + { + $expectedArray = [ + '_current' => true, + '_escape' => true, + '_use_rewrite' => true, + '_fragment' => null, + '_query' => $expectedParams, + ]; + + $collectionMock = $this->createMock(Collection::class); + $collectionMock->expects($this->once()) + ->method('getCurPage') + ->willReturn($page); + $collectionMock->expects($this->once()) + ->method('getSize') + ->willReturn($size); + $this->setCollectionProperty($collectionMock); + + $this->urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with('*/*/*', $expectedArray); + + $this->pager->getLimitUrl($limit); + } + + /** + * DataProvider for testGetLimitUrl + * + * @return array + */ + public function limitUrlDataProvider(): array + { + return [ + [2, 21, 10, ['limit' => 10]], + [3, 21, 10, ['limit' => 10]], + [2, 21, 20, ['limit' => 20]], + [3, 21, 50, ['limit' => 50, 'p' => null]], + [2, 11, 20, ['limit' => 20, 'p' => null]], + [4, 40, 20, ['limit' => 20, 'p' => 2]], + ]; + } + /** * Set Collection * diff --git a/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php b/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php index 2df86d5263e3d..0d5e5fa393398 100644 --- a/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php +++ b/app/code/Magento/Theme/Test/Unit/Helper/StorageTest.php @@ -19,9 +19,10 @@ use Magento\Framework\Url\EncoderInterface; use Magento\Framework\View\Design\Theme\Customization; use Magento\Framework\View\Design\Theme\FlyweightFactory; +use Magento\Framework\Filesystem\DriverInterface; use Magento\Theme\Helper\Storage; -use Magento\Theme\Model\Theme; use PHPUnit\Framework\MockObject\MockObject; +use Magento\Theme\Model\Theme; use PHPUnit\Framework\TestCase; /** @@ -91,6 +92,11 @@ class StorageTest extends TestCase protected $requestParams; + /** + * @var DriverInterface|MockObject + */ + private $filesystemDriver; + protected function setUp(): void { $this->customizationPath = '/' . implode('/', ['var', 'theme']); @@ -117,6 +123,7 @@ protected function setUp(): void $this->contextHelper->expects($this->any())->method('getUrlEncoder')->willReturn($this->urlEncoder); $this->contextHelper->expects($this->any())->method('getUrlDecoder')->willReturn($this->urlDecoder); $this->themeFactory->expects($this->any())->method('create')->willReturn($this->theme); + $this->filesystemDriver = $this->createMock(DriverInterface::class); $this->theme->expects($this->any()) ->method('getCustomization') @@ -135,7 +142,9 @@ protected function setUp(): void $this->contextHelper, $this->filesystem, $this->session, - $this->themeFactory + $this->themeFactory, + null, + $this->filesystemDriver ); } @@ -279,6 +288,9 @@ public function testGetThumbnailPathNotFound() { $this->expectException('InvalidArgumentException'); $this->expectExceptionMessage('The image not found'); + + $this->filesystemDriver->method('getRealpathSafety') + ->willReturnArgument(0); $image = 'notFoundImage.png'; $root = '/image'; $sourceNode = '/not/a/root'; @@ -456,4 +468,67 @@ public function testGetThemeNotFound() ); $helper->getStorageRoot(); } + + /** + * @dataProvider getCurrentPathDataProvider + */ + public function testGetCurrentPathCachesResult() + { + $this->request->expects($this->once()) + ->method('getParam') + ->with(Storage::PARAM_NODE) + ->willReturn(Storage::NODE_ROOT); + + $actualPath = $this->helper->getCurrentPath(); + self::assertSame('/image', $actualPath); + } + + /** + * @dataProvider getCurrentPathDataProvider + */ + public function testGetCurrentPath( + string $expectedPath, + string $requestedPath, + ?bool $isDirectory = null, + ?string $relativePath = null, + ?string $resolvedPath = null + ) { + $this->directoryWrite->method('isDirectory') + ->willReturn($isDirectory); + + $this->directoryWrite->method('getRelativePath') + ->willReturn($relativePath); + + $this->urlDecoder->method('decode') + ->willReturnArgument(0); + + if ($resolvedPath) { + $this->filesystemDriver->method('getRealpathSafety') + ->willReturn($resolvedPath); + } else { + $this->filesystemDriver->method('getRealpathSafety') + ->willReturnArgument(0); + } + + $this->request->method('getParam') + ->with(Storage::PARAM_NODE) + ->willReturn($requestedPath); + + $actualPath = $this->helper->getCurrentPath(); + + self::assertSame($expectedPath, $actualPath); + } + + public function getCurrentPathDataProvider(): array + { + $rootPath = '/' . \Magento\Theme\Model\Wysiwyg\Storage::TYPE_IMAGE; + + return [ + 'requested path "root" should short-circuit' => [$rootPath, Storage::NODE_ROOT], + 'non-existent directory should default to the base path' => [$rootPath, $rootPath . '/foo'], + 'requested path that resolves to a bad path should default to root' => + [$rootPath, $rootPath . '/something', true, null, '/bar'], + 'real path should resolve to relative path' => ['foo/', $rootPath . '/foo', true, 'foo/'], + ]; + } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php index 82678d4b4277d..438853b9935e6 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Config/CustomizationTest.php @@ -13,9 +13,10 @@ use Magento\Framework\App\Area; use Magento\Framework\DataObject; use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; use Magento\Store\Model\StoreManagerInterface; use Magento\Theme\Model\Config\Customization; -use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; use Magento\Theme\Model\Theme\ThemeProvider; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -32,47 +33,37 @@ class CustomizationTest extends TestCase */ protected $designPackage; - /** - * @var Collection - */ - protected $themeCollection; - /** * @var Customization */ protected $model; /** - * @var ThemeProvider|\PHPUnit\Framework\MockObject_MockBuilder + * @var ThemeProvider|MockObject */ protected $themeProviderMock; + /** + * @var StoreThemesResolverInterface|MockObject + */ + private $storeThemesResolver; protected function setUp(): void { - $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class) - ->getMock(); - $this->designPackage = $this->getMockBuilder(DesignInterface::class) - ->getMock(); - $this->themeCollection = $this->getMockBuilder(Collection::class) - ->disableOriginalConstructor() - ->getMock(); - - $collectionFactory = $this->getMockBuilder(\Magento\Theme\Model\ResourceModel\Theme\CollectionFactory::class) - ->disableOriginalConstructor() - ->setMethods(['create']) - ->getMock(); - - $collectionFactory->expects($this->any())->method('create')->willReturn($this->themeCollection); + $this->storeManager = $this->getMockBuilder(StoreManagerInterface::class)->getMock(); + $this->designPackage = $this->getMockBuilder(DesignInterface::class)->getMock(); $this->themeProviderMock = $this->getMockBuilder(ThemeProvider::class) ->disableOriginalConstructor() ->setMethods(['getThemeCustomizations', 'getThemeByFullPath']) ->getMock(); + $this->storeThemesResolver = $this->createMock(StoreThemesResolverInterface::class); + $this->model = new Customization( $this->storeManager, $this->designPackage, - $this->themeProviderMock + $this->themeProviderMock, + $this->storeThemesResolver ); } @@ -84,13 +75,15 @@ protected function setUp(): void */ public function testGetAssignedThemeCustomizations() { - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); - + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); + + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -108,13 +101,15 @@ public function testGetAssignedThemeCustomizations() */ public function testGetUnassignedThemeCustomizations() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -131,13 +126,15 @@ public function testGetUnassignedThemeCustomizations() */ public function testGetStoresByThemes() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $stores = $this->model->getStoresByThemes(); $this->assertArrayHasKey($this->getAssignedTheme()->getId(), $stores); @@ -148,15 +145,17 @@ public function testGetStoresByThemes() * @covers \Magento\Theme\Model\Config\Customization::_getConfigurationThemeId * @covers \Magento\Theme\Model\Config\Customization::__construct */ - public function testIsThemeAssignedToDefaultStore() + public function testIsThemeAssignedToAnyStore() { + $store = $this->getStore(); $this->storeManager->expects($this->once()) ->method('getStores') - ->willReturn([$this->getStore()]); + ->willReturn([$store]); - $this->designPackage->expects($this->once()) - ->method('getConfigurationDesignTheme') - ->willReturn($this->getAssignedTheme()->getId()); + $this->storeThemesResolver->expects($this->once()) + ->method('getThemes') + ->with($store) + ->willReturn([$this->getAssignedTheme()->getId()]); $this->themeProviderMock->expects($this->once()) ->method('getThemeCustomizations') @@ -198,10 +197,10 @@ protected function getUnassignedTheme() } /** - * @return DataObject + * @return StoreInterface|MockObject */ protected function getStore() { - return new DataObject(['id' => 55]); + return $this->createConfiguredMock(StoreInterface::class, ['getId' => 55]); } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php index 7a48aa968392a..78a56013ae042 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/FileTest.php @@ -11,14 +11,17 @@ use Magento\Framework\App\Cache\TypeListInterface; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Data\Collection\AbstractDb; use Magento\Framework\File\Mime; use Magento\Framework\Filesystem; use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\Filesystem\Io\File as IoFileSystem; use Magento\Framework\Model\Context; use Magento\Framework\Model\ResourceModel\AbstractResource; use Magento\Framework\Registry; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; + use Magento\Framework\UrlInterface; use Magento\MediaStorage\Helper\File\Storage\Database; use Magento\Theme\Model\Design\Backend\File; @@ -31,13 +34,16 @@ class FileTest extends TestCase { /** @var WriteInterface|MockObject */ - protected $mediaDirectory; + private $mediaDirectory; /** @var UrlInterface|MockObject */ - protected $urlBuilder; + private $urlBuilder; /** @var File */ - protected $fileBackend; + private $fileBackend; + + /** @var IoFileSystem|\PHPUnit\Framework\MockObject\MockObject */ + private $ioFileSystem; /** * @var Mime|MockObject @@ -49,6 +55,9 @@ class FileTest extends TestCase */ private $databaseHelper; + /** + * @inheritdoc + */ protected function setUp(): void { $context = $this->getMockObject(Context::class); @@ -62,16 +71,18 @@ protected function setUp(): void $filesystem = $this->getMockBuilder(Filesystem::class) ->disableOriginalConstructor() ->getMock(); - $this->mediaDirectory = $this->getMockBuilder(WriteInterface::class) + $this->mediaDirectory = $this->getMockBuilder( + WriteInterface::class + ) ->getMockForAbstractClass(); - $filesystem->expects($this->once()) ->method('getDirectoryWrite') ->with(DirectoryList::MEDIA) ->willReturn($this->mediaDirectory); $this->urlBuilder = $this->getMockBuilder(UrlInterface::class) ->getMockForAbstractClass(); - + $this->ioFileSystem = $this->getMockBuilder(IoFileSystem::class) + ->getMockForAbstractClass(); $this->mime = $this->getMockBuilder(Mime::class) ->disableOriginalConstructor() ->getMock(); @@ -86,7 +97,6 @@ protected function setUp(): void $abstractDb = $this->getMockBuilder(AbstractDb::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); - $this->fileBackend = new File( $context, $registry, @@ -99,7 +109,8 @@ protected function setUp(): void $abstractResource, $abstractDb, [], - $this->databaseHelper + $this->databaseHelper, + $this->ioFileSystem ); $objectManager = new ObjectManager($this); @@ -110,17 +121,22 @@ protected function setUp(): void ); } + /** + * @inheritdoc + */ protected function tearDown(): void { unset($this->fileBackend); } /** + * Gets the mock object. + * * @param string $class * @param array $methods * @return MockObject */ - protected function getMockObject($class, $methods = []) + private function getMockObject(string $class, array $methods = []): \PHPUnit\Framework\MockObject\MockObject { $builder = $this->getMockBuilder($class) ->disableOriginalConstructor(); @@ -131,15 +147,20 @@ protected function getMockObject($class, $methods = []) } /** + * Gets mock objects for abstract class. + * * @param string $class * @return MockObject */ - protected function getMockObjectForAbstractClass($class) + private function getMockObjectForAbstractClass(string $class): \PHPUnit\Framework\MockObject\MockObject { return $this->getMockBuilder($class) ->getMockForAbstractClass(); } + /** + * Test for afterLoad method. + */ public function testAfterLoad() { $value = 'filename.jpg'; @@ -147,16 +168,18 @@ public function testAfterLoad() $absoluteFilePath = '/absolute_path/' . $value; - $this->fileBackend->setValue($value); - $this->fileBackend->setFieldConfig( + $this->fileBackend->setData( [ - 'upload_dir' => [ - 'value' => 'value', - 'config' => 'system/filesystem/media', - ], - 'base_url' => [ - 'type' => 'media', - 'value' => 'design/file' + 'value' => $value, + 'field_config' => [ + 'upload_dir' => [ + 'value' => 'value', + 'config' => 'system/filesystem/media', + ], + 'base_url' => [ + 'type' => 'media', + 'value' => 'design/file' + ], ], ] ); @@ -169,7 +192,6 @@ public function testAfterLoad() ->method('getAbsolutePath') ->with('value/' . $value) ->willReturn($absoluteFilePath); - $this->urlBuilder->expects($this->once()) ->method('getBaseUrl') ->with(['_type' => UrlInterface::URL_TYPE_MEDIA]) @@ -182,12 +204,10 @@ public function testAfterLoad() ->method('stat') ->with('value/' . $value) ->willReturn(['size' => 234234]); - $this->mime->expects($this->once()) ->method('getMimeType') ->with($absoluteFilePath) ->willReturn($mime); - $this->fileBackend->afterLoad(); $this->assertEquals( [ @@ -205,29 +225,32 @@ public function testAfterLoad() } /** + * Test for beforeSave method. + * * @dataProvider beforeSaveDataProvider * @param string $fileName + * @throws LocalizedException */ - public function testBeforeSave($fileName) + public function testBeforeSave(string $fileName) { $expectedFileName = basename($fileName); $expectedTmpMediaPath = 'tmp/design/file/' . $expectedFileName; - $this->fileBackend->setScope('store'); - $this->fileBackend->setScopeId(1); - $this->fileBackend->setValue( - [ - [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $fileName, - 'file' => $fileName, - 'size' => 234234, - ] - ] - ); - $this->fileBackend->setFieldConfig( + $this->fileBackend->setData( [ - 'upload_dir' => [ - 'value' => 'value', - 'config' => 'system/filesystem/media', + 'scope' => 'store', + 'scope_id' => 1, + 'value' => [ + [ + 'url' => 'http://magento2.com/pub/media/tmp/image/' . $fileName, + 'file' => $fileName, + 'size' => 234234, + ] + ], + 'field_config' => [ + 'upload_dir' => [ + 'value' => 'value', + 'config' => 'system/filesystem/media', + ], ], ] ); @@ -250,13 +273,15 @@ public function testBeforeSave($fileName) } /** + * Data provider for testBeforeSave. + * * @return array */ - public function beforeSaveDataProvider() + public function beforeSaveDataProvider(): array { return [ 'Normal file name' => ['filename.jpg'], - 'Vulnerable file name' => ['../../../../../../../../etc/passwd'], + 'Vulnerable file name' => ['../../../../../../../../etc/pass'], ]; } @@ -277,19 +302,27 @@ public function testBeforeSaveWithoutFile() $this->fileBackend->beforeSave(); } + /** + * Test for beforeSave method with existing file. + * + * @throws LocalizedException + */ public function testBeforeSaveWithExistingFile() { $value = 'filename.jpg'; - $this->fileBackend->setValue( + $this->fileBackend->setData( [ - [ - 'url' => 'http://magento2.com/pub/media/tmp/image/' . $value, - 'file' => $value, - 'size' => 234234, - 'exists' => true - ] + 'value' => [ + [ + 'url' => 'http://magento2.com/pub/media/tmp/image/' . $value, + 'file' => $value, + 'size' => 234234, + 'exists' => true + ] + ], ] ); + $this->fileBackend->beforeSave(); $this->assertEquals( $value, @@ -303,6 +336,7 @@ public function testBeforeSaveWithExistingFile() * @param string $path * @param string $filename * @dataProvider getRelativeMediaPathDataProvider + * @throws \ReflectionException */ public function testGetRelativeMediaPath(string $path, string $filename) { @@ -324,7 +358,7 @@ public function getRelativeMediaPathDataProvider(): array { return [ 'Normal path' => ['pub/media/', 'filename.jpg'], - 'Complex path' => ['somepath/pub/media/', 'filename.jpg'], + 'Complex path' => ['some_path/pub/media/', 'filename.jpg'], ]; } } diff --git a/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/ImageTest.php b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/ImageTest.php new file mode 100644 index 0000000000000..f1c2b1d755971 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Design/Backend/ImageTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Design\Backend; + +use Magento\Config\Model\Config\Backend\File\RequestData\RequestDataInterface; +use Magento\Framework\App\Cache\TypeListInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Collection\AbstractDb; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Io\File; +use Magento\Framework\Model\Context; +use Magento\Framework\Model\ResourceModel\AbstractResource; +use Magento\Framework\Registry; +use Magento\Framework\UrlInterface; +use Magento\MediaStorage\Helper\File\Storage\Database; +use Magento\MediaStorage\Model\File\UploaderFactory; +use Magento\Theme\Model\Design\Backend\Image; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class ImageTest extends \PHPUnit\Framework\TestCase +{ + /** @var Image */ + private $imageBackend; + + /** @var File */ + private $ioFileSystem; + + /** + * @inheritdoc + */ + public function setUp(): void + { + $context = $this->getMockObject(Context::class); + $registry = $this->getMockObject(Registry::class); + $config = $this->getMockObject(ScopeConfigInterface::class); + $cacheTypeList = $this->getMockObject(TypeListInterface::class); + $uploaderFactory = $this->getMockObject(UploaderFactory::class); + $requestData = $this->getMockObject(RequestDataInterface::class); + $filesystem = $this->getMockObject(Filesystem::class); + $urlBuilder = $this->getMockObject(UrlInterface::class); + $databaseHelper = $this->getMockObject(Database::class); + $abstractResource = $this->getMockObject(AbstractResource::class); + $abstractDb = $this->getMockObject(AbstractDb::class); + $this->ioFileSystem = $this->getMockObject(File::class); + $this->imageBackend = new Image( + $context, + $registry, + $config, + $cacheTypeList, + $uploaderFactory, + $requestData, + $filesystem, + $urlBuilder, + $abstractResource, + $abstractDb, + [], + $databaseHelper, + $this->ioFileSystem + ); + } + + /** + * @inheritdoc + */ + public function tearDown(): void + { + unset($this->imageBackend); + } + + /** + * @param string $class + * @param array $methods + * @return \PHPUnit\Framework\MockObject\MockObject + */ + private function getMockObject(string $class, array $methods = []): \PHPUnit\Framework\MockObject\MockObject + { + $builder = $this->getMockBuilder($class) + ->disableOriginalConstructor(); + if (count($methods)) { + $builder->setMethods($methods); + } + return $builder->getMock(); + } + + /** + * Test for beforeSave method with invalid file extension. + */ + public function testBeforeSaveWithInvalidExtensionFile() + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + $this->expectExceptionMessage( + 'Something is wrong with the file upload settings.' + ); + + $invalidFileName = 'fileName.invalidExtension'; + $this->imageBackend->setData( + [ + 'value' => [ + [ + 'file' => $invalidFileName, + ] + ], + ] + ); + $expectedPathInfo = [ + 'extension' => 'invalidExtension' + ]; + $this->ioFileSystem + ->expects($this->any()) + ->method('getPathInfo') + ->with($invalidFileName) + ->willReturn($expectedPathInfo); + $this->imageBackend->beforeSave(); + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php new file mode 100644 index 0000000000000..939b47a42ce85 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreDefaultThemeResolverTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use ArrayIterator; +use Magento\Framework\App\Area; +use Magento\Framework\View\Design\ThemeInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; +use Magento\Theme\Model\Theme\StoreDefaultThemeResolver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store default theme resolver. + */ +class StoreDefaultThemeResolverTest extends TestCase +{ + /** + * @var DesignInterface|MockObject + */ + private $design; + /** + * @var StoreDefaultThemeResolver + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $themeCollectionFactory = $this->createMock(CollectionFactory::class); + $this->design = $this->createMock(DesignInterface::class); + $this->model = new StoreDefaultThemeResolver( + $themeCollectionFactory, + $this->design + ); + $registeredThemes = []; + $registeredThemes[] = $this->createConfiguredMock( + ThemeInterface::class, + [ + 'getId' => 1, + 'getCode' => 'Magento/luma', + ] + ); + $registeredThemes[] = $this->createConfiguredMock( + ThemeInterface::class, + [ + 'getId' => 2, + 'getCode' => 'Magento/blank', + ] + ); + $collection = $this->createMock(Collection::class); + $collection->method('getIterator') + ->willReturn(new ArrayIterator($registeredThemes)); + $collection->method('loadRegisteredThemes') + ->willReturnSelf(); + $themeCollectionFactory->method('create') + ->willReturn($collection); + } + + /** + * Test that method returns default theme associated to given store. + * + * @param string|null $defaultTheme + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(?string $defaultTheme, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + $this->design->expects($this->once()) + ->method('getConfigurationDesignTheme') + ->with( + Area::AREA_FRONTEND, + ['store' => $store] + ) + ->willReturn($defaultTheme); + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + null, + [] + ], + [ + '1', + [1] + ], + [ + 'Magento/blank', + [2] + ], + [ + 'Magento/theme', + [] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php new file mode 100644 index 0000000000000..b80ec4ae83887 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreThemesResolverTest.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use Magento\Store\Api\Data\StoreInterface; +use Magento\Theme\Model\Theme\StoreThemesResolver; +use Magento\Theme\Model\Theme\StoreThemesResolverInterface; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store composite themes resolver model. + */ +class StoreThemesResolverTest extends TestCase +{ + /** + * @var StoreThemesResolverInterface[]|MockObject[] + */ + private $resolvers; + /** + * @var StoreThemesResolver + */ + private $model; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->resolvers = []; + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->resolvers[] = $this->createMock(StoreThemesResolverInterface::class); + $this->model = new StoreThemesResolver($this->resolvers); + } + + /** + * Test that constructor SHOULD throw an exception when resolver is not instance of StoreThemesResolverInterface. + */ + public function testInvalidConstructorArguments(): void + { + $resolver = $this->createMock(StoreInterface::class); + $this->expectExceptionObject( + new \InvalidArgumentException( + sprintf( + 'Instance of %s is expected, got %s instead.', + StoreThemesResolverInterface::class, + get_class($resolver) + ) + ) + ); + $this->model = new StoreThemesResolver( + [ + $resolver + ] + ); + } + + /** + * Test that method returns aggregated themes from resolvers + * + * @param array $themes + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(array $themes, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + foreach ($this->resolvers as $key => $resolver) { + $resolver->expects($this->once()) + ->method('getThemes') + ->willReturn($themes[$key]); + } + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + [ + [], + [], + [] + ], + [] + ], + [ + [ + ['1'], + [], + ['1'] + ], + ['1'] + ], + [ + [ + ['1'], + ['2'], + ['1'] + ], + ['1', '2'] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php new file mode 100644 index 0000000000000..1ef4b17ca6562 --- /dev/null +++ b/app/code/Magento/Theme/Test/Unit/Model/Theme/StoreUserAgentThemeResolverTest.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Test\Unit\Model\Theme; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Theme\Model\Theme\StoreUserAgentThemeResolver; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test store associated themes in user-agent rules resolver. + */ +class StoreUserAgentThemeResolverTest extends TestCase +{ + /** + * @var ScopeConfigInterface|MockObject + */ + private $scopeConfig; + /** + * @var Json + */ + private $serializer; + /** + * @var StoreUserAgentThemeResolver + */ + private $model; + + protected function setUp(): void + { + parent::setUp(); + $this->scopeConfig = $this->createMock(ScopeConfigInterface::class); + $this->serializer = new Json(); + $this->model = new StoreUserAgentThemeResolver( + $this->scopeConfig, + $this->serializer + ); + } + + /** + * Test that method returns user-agent rules associated themes. + * + * @param array|null $config + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(?array $config, array $expected): void + { + $store = $this->createMock(StoreInterface::class); + $this->scopeConfig->expects($this->once()) + ->method('getValue') + ->with('design/theme/ua_regexp', ScopeInterface::SCOPE_STORE, $store) + ->willReturn($config !== null ? $this->serializer->serialize($config) : $config); + $this->assertEquals($expected, $this->model->getThemes($store)); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + null, + [] + ], + [ + [], + [] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => '1', + ], + ], + ['1'] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => '1', + ], + [ + 'search' => '\/mozila\/i', + 'regexp' => '\/mozila\/i', + 'value' => '2', + ], + ], + ['1', '2'] + ] + ]; + } +} diff --git a/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php b/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php index a735ba1927477..8f421ac3121fb 100644 --- a/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php +++ b/app/code/Magento/Theme/Test/Unit/Model/Wysiwyg/StorageTest.php @@ -23,6 +23,7 @@ use Magento\Theme\Helper\Storage; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Filesystem\DriverInterface; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -74,6 +75,11 @@ class StorageTest extends TestCase */ protected $urlDecoder; + /** + * @var DriverInterface|MockObject + */ + private $filesystemDriver; + protected function setUp(): void { $this->_filesystem = $this->createMock(Filesystem::class); @@ -114,6 +120,7 @@ function ($path) { $this->directoryWrite = $this->createMock(Write::class); $this->urlEncoder = $this->createPartialMock(EncoderInterface::class, ['encode']); $this->urlDecoder = $this->createPartialMock(DecoderInterface::class, ['decode']); + $this->filesystemDriver = $this->createMock(DriverInterface::class); $this->_filesystem->expects( $this->once() @@ -129,7 +136,9 @@ function ($path) { $this->_objectManager, $this->_imageFactory, $this->urlEncoder, - $this->urlDecoder + $this->urlDecoder, + null, + $this->filesystemDriver ); $this->_storageRoot = '/root'; @@ -577,6 +586,33 @@ public function testDeleteRootDirectory() $this->_storageModel->deleteDirectory($directoryPath); } + /** + * cover \Magento\Theme\Model\Wysiwyg\Storage::deleteDirectory + */ + public function testDeleteRootDirectoryRelative() + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + + $directoryPath = $this->_storageRoot; + $fakePath = 'fake/relative/path'; + + $this->directoryWrite->method('getAbsolutePath') + ->with($fakePath) + ->willReturn($directoryPath); + + $this->filesystemDriver->method('getRealPathSafety') + ->with($directoryPath) + ->willReturn($directoryPath); + + $this->_helperStorage + ->method('getStorageRoot') + ->willReturn($directoryPath); + + $this->_storageModel->deleteDirectory($fakePath); + } + /** * @return array */ diff --git a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php similarity index 80% rename from lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php rename to app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php index 549d45a986cf0..4efcc584986d1 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/Action/Plugin/LoadDesignPluginTest.php +++ b/app/code/Magento/Theme/Test/Unit/Plugin/LoadDesignPluginTest.php @@ -3,15 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - -namespace Magento\Framework\App\Test\Unit\Action\Plugin; +namespace Magento\Theme\Test\Unit\Plugin; use Magento\Framework\App\Action\Action; -use Magento\Framework\App\Action\Plugin\LoadDesignPlugin; use Magento\Framework\App\ActionInterface; use Magento\Framework\Message\ManagerInterface; use Magento\Framework\View\DesignLoader; +use Magento\Theme\Plugin\LoadDesignPlugin; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -26,7 +24,7 @@ public function testBeforeExecute() $designLoaderMock = $this->createMock(DesignLoader::class); /** @var MockObject|ManagerInterface $messageManagerMock */ - $messageManagerMock = $this->getMockForAbstractClass(ManagerInterface::class); + $messageManagerMock = $this->createMock(ManagerInterface::class); $plugin = new LoadDesignPlugin($designLoaderMock, $messageManagerMock); diff --git a/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php b/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php index 4b71fc6faba15..f0e668d10c3a6 100644 --- a/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php +++ b/app/code/Magento/Theme/Ui/Component/Design/Config/SearchRobots/ResetButton.php @@ -14,7 +14,7 @@ * ResetButton field instance * * @api - * @since 100.2.0 + * @since 100.1.9 */ class ResetButton extends Field { @@ -66,7 +66,7 @@ private function getRobotsDefaultCustomInstructions() * * @return void * @throws \Magento\Framework\Exception\LocalizedException - * @since 100.2.0 + * @since 100.1.9 */ public function prepare() { diff --git a/app/code/Magento/Theme/etc/adminhtml/system.xml b/app/code/Magento/Theme/etc/adminhtml/system.xml index af5e952584d1c..caafda9bdd827 100644 --- a/app/code/Magento/Theme/etc/adminhtml/system.xml +++ b/app/code/Magento/Theme/etc/adminhtml/system.xml @@ -19,7 +19,7 @@ <label>Use CSS critical path</label> <source_model>Magento\Config\Model\Config\Source\Yesno</source_model> <comment> - <![CDATA[<strong style="color:red">Warning!</strong> Be sure that you have critical.css file for your theme. Other CSS files will be loaded asynchronously.]]> + <![CDATA[<strong class="colorRed">Warning!</strong> Be sure that you have critical.css file for your theme. Other CSS files will be loaded asynchronously.]]> </comment> </field> </group> diff --git a/app/code/Magento/Theme/etc/di.xml b/app/code/Magento/Theme/etc/di.xml index 921e6bfc6ecf1..d6fe3f8fef355 100644 --- a/app/code/Magento/Theme/etc/di.xml +++ b/app/code/Magento/Theme/etc/di.xml @@ -18,6 +18,7 @@ <preference for="Magento\Theme\Api\DesignConfigRepositoryInterface" type="Magento\Theme\Model\DesignConfigRepository"/> <preference for="Magento\Framework\View\Model\PageLayout\Config\BuilderInterface" type="Magento\Theme\Model\PageLayout\Config\Builder"/> <preference for="Magento\Theme\Model\Design\Config\MetadataProviderInterface" type="Magento\Theme\Model\Design\Config\MetadataProvider"/> + <preference for="Magento\Theme\Model\Theme\StoreThemesResolverInterface" type="Magento\Theme\Model\Theme\StoreThemesResolver"/> <type name="Magento\Theme\Model\Config"> <arguments> <argument name="configCache" xsi:type="object">Magento\Framework\App\Cache\Type\Config</argument> @@ -104,6 +105,9 @@ <argument name="scope" xsi:type="const">Magento\Store\Model\ScopeInterface::SCOPE_STORE</argument> </arguments> </virtualType> + <type name="Magento\Framework\App\ActionInterface"> + <plugin name="designLoader" type="Magento\Theme\Plugin\LoadDesignPlugin"/> + </type> <type name="Magento\Framework\View\Element\UiComponent\DataProvider\CollectionFactory"> <arguments> <argument name="collections" xsi:type="array"> @@ -309,4 +313,20 @@ <argument name="cache" xsi:type="object">configured_design_cache</argument> </arguments> </type> + <type name="Magento\Theme\Model\Theme\StoreThemesResolver"> + <arguments> + <argument name="resolvers" xsi:type="array"> + <item name="storeDefaultTheme" xsi:type="object">Magento\Theme\Model\Theme\StoreDefaultThemeResolver</item> + <item name="storeUserAgentTheme" xsi:type="object">Magento\Theme\Model\Theme\StoreUserAgentThemeResolver</item> + </argument> + </arguments> + </type> + <type name="Magento\Theme\Helper\Storage"> + <arguments> + <argument name="filesystemDriver" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument> + </arguments> + </type> + <type name="Magento\Framework\Data\Collection"> + <plugin name="currentPageDetection" type="Magento\Theme\Plugin\Data\Collection" /> + </type> </config> diff --git a/app/code/Magento/Theme/etc/webapi_rest/di.xml b/app/code/Magento/Theme/etc/webapi_rest/di.xml new file mode 100644 index 0000000000000..8abda8502238d --- /dev/null +++ b/app/code/Magento/Theme/etc/webapi_rest/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\Data\Collection"> + <plugin name="currentPageDetection" disabled="true"/> + </type> +</config> diff --git a/app/code/Magento/Theme/etc/webapi_soap/di.xml b/app/code/Magento/Theme/etc/webapi_soap/di.xml new file mode 100644 index 0000000000000..8abda8502238d --- /dev/null +++ b/app/code/Magento/Theme/etc/webapi_soap/di.xml @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Framework\Data\Collection"> + <plugin name="currentPageDetection" disabled="true"/> + </type> +</config> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml b/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml index 67c9084b2756e..66456ae403818 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/browser/content/uploader.phtml @@ -5,6 +5,7 @@ */ /** @var $block \Magento\Theme\Block\Adminhtml\Wysiwyg\Files\Content\Uploader */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div id="<?= $block->getHtmlId() ?>" class="uploader"> @@ -18,14 +19,18 @@ <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress"></div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> <div class="clear"></div> </div> </script> </div> +<?php $scriptString= <<<script -<script> require([ 'jquery', 'mage/template', @@ -41,9 +46,9 @@ require([ form_key: FORM_KEY }, sequentialUploads: true, - maxFileSize: <?= $block->escapeJs($block->getFileSizeService()->getMaxFileSize()) ?> , + maxFileSize: {$block->escapeJs($block->getFileSizeService()->getMaxFileSize())} , add: function (e, data) { - var progressTmpl = mageTemplate('#<?= $block->getHtmlId() ?>-template'), + var progressTmpl = mageTemplate('#{$block->getHtmlId()}-template'), fileSize, tmpl; @@ -62,7 +67,7 @@ require([ } }); - $(tmpl).appendTo('#<?= $block->getHtmlId() ?>'); + $(tmpl).appendTo('#{$block->getHtmlId()}'); }); $(this).fileupload('process', data).done(function () { @@ -91,4 +96,7 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml b/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml index 902daf98182f0..53228243ffd19 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/tabs/css.phtml @@ -3,12 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -/** @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Css */ +/** + * @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Css + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?= $block->getFormHtml() ?> -<script> +<?php $scriptString = <<<script + require([ "jquery", "Magento_Ui/js/modal/alert", @@ -19,7 +23,7 @@ require([ $( '#css_file_uploader' ).fileupload({ dataType: 'json', replaceFileInput: false, - url : '<?= $block->escapeJs($block->escapeUrl($block->getUrl('*/system_design_theme/uploadcss'))) ?>', + url : '{$block->escapeJs($block->getUrl('*/system_design_theme/uploadcss'))}', acceptFileTypes: /(.|\/)(css)$/i, /** @@ -76,4 +80,7 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml b/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml index b50f68cd9353b..e15ac4a088e03 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/tabs/fieldset/js.phtml @@ -4,18 +4,27 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate.FoundThis -/** @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset */ +/** + * @var $block \Magento\Backend\Block\Widget\Form\Renderer\Fieldset + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); ?> <div id="js-file-uploader" class="uploader"> </div> -<script id="js-file-uploader-template" type="text/x-magento-template"> +<script id="js-file-uploader-template" type="text/x-magento-template"> <div id="<%- data.id %>" class="file-row"> <span class="file-info"><%- data.name %> (<%- data.size %>)</span> <div class="progressbar-container"> - <div class="progressbar upload-progress" style="width: 0%;"></div> + <div class="progressbar upload-progress""></div> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "width: 0%;", + "div.progressbar-container div.progressbar.upload-progress" + ) ?> <div class="clear"></div> </div> </script> @@ -40,8 +49,8 @@ </script> <ul id="js-files-container" class="js-files-container ui-sortable" ></ul> +<?php $scriptString = <<<script -<script> require([ "jquery", "jquery/ui", @@ -61,10 +70,13 @@ jQuery(function($) { $('body').trigger( 'refreshJsList', { - jsList: <?= /* @noEscape */ $this->helper(Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getJsFiles()) ?> + jsList: {$jsonHelper->jsonEncode($block->getJsFiles())} } ); }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml b/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml index 1b4633d0965f3..4edc895c559e2 100644 --- a/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml +++ b/app/code/Magento/Theme/view/adminhtml/templates/tabs/js.phtml @@ -4,11 +4,15 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Js */ +/** + * @var $block \Magento\Theme\Block\Adminhtml\System\Design\Theme\Edit\Tab\Js + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?= $block->getFormHtml() ?> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/template", @@ -22,7 +26,7 @@ require([ dataType: 'json', replaceFileInput: false, sequentialUploads: true, - url: '<?= $block->escapeJs($block->escapeUrl($block->getJsUploadUrl())) ?>', + url: '{$block->escapeJs($block->getJsUploadUrl())}', /** * Add data @@ -125,4 +129,7 @@ require([ }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/base/requirejs-config.js b/app/code/Magento/Theme/view/base/requirejs-config.js index 423ac707c6572..4bd854f2e4670 100644 --- a/app/code/Magento/Theme/view/base/requirejs-config.js +++ b/app/code/Magento/Theme/view/base/requirejs-config.js @@ -60,27 +60,6 @@ var config = { } }; -/* eslint-disable max-depth */ -/** - * Adds polyfills only for browser contexts which prevents bundlers from including them. - */ -if (typeof window !== 'undefined' && window.document) { - /** - * Polyfill localStorage and sessionStorage for browsers that do not support them. - */ - try { - if (!window.localStorage || !window.sessionStorage) { - throw new Error(); - } - - localStorage.setItem('storage_test', 1); - localStorage.removeItem('storage_test'); - } catch (e) { - config.deps.push('mage/polyfill'); - } -} -/* eslint-enable max-depth */ - require(['jquery'], function ($) { 'use strict'; diff --git a/app/code/Magento/Theme/view/frontend/requirejs-config.js b/app/code/Magento/Theme/view/frontend/requirejs-config.js index e14c93d329a07..79c8e69d94338 100644 --- a/app/code/Magento/Theme/view/frontend/requirejs-config.js +++ b/app/code/Magento/Theme/view/frontend/requirejs-config.js @@ -49,3 +49,24 @@ var config = { } } }; + +/* eslint-disable max-depth */ +/** + * Adds polyfills only for browser contexts which prevents bundlers from including them. + */ +if (typeof window !== 'undefined' && window.document) { + /** + * Polyfill localStorage and sessionStorage for browsers that do not support them. + */ + try { + if (!window.localStorage || !window.sessionStorage) { + throw new Error(); + } + + localStorage.setItem('storage_test', 1); + localStorage.removeItem('storage_test'); + } catch (e) { + config.deps.push('mage/polyfill'); + } +} +/* eslint-enable max-depth */ diff --git a/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml b/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml index 2c1c7db75b111..6c2f17b6ffb08 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/main_css_preloader.phtml @@ -3,11 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <div data-role="main-css-loader" class="loading-mask"> <div class="loader"> <img src="<?= $block->escapeUrl($block->getViewFileUrl('images/loader-1.gif')); ?>" - alt="<?= $block->escapeHtml(__('Loading...')); ?>" - style="position: absolute;"> + alt="<?= $block->escapeHtml(__('Loading...')); ?>"> </div> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "position: absolute;", + "div.loader img" + ) ?> </div> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml b/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml index 1414c21c6e9bc..bb9d5cb2fd2e0 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/notices.phtml @@ -6,30 +6,41 @@ /** * @var $block \Magento\Theme\Block\Html\Notices + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->displayNoscriptNotice()) : ?> +<?php if ($block->displayNoscriptNotice()): ?> <noscript> <div class="message global noscript"> <div class="content"> <p> <strong><?= $block->escapeHtml(__('JavaScript seems to be disabled in your browser.')) ?></strong> - <span><?= $block->escapeHtml(__('For the best experience on our site, be sure to turn on Javascript in your browser.')) ?></span> + <span> + <?= $block->escapeHtml( + __('For the best experience on our site, be sure to turn on Javascript in your browser.') + ) ?> + </span> </p> </div> </div> </noscript> <?php endif; ?> -<?php if ($block->displayNoLocalStorageNotice()) : ?> - <div class="notice global site local_storage" style="display: none;"> +<?php if ($block->displayNoLocalStorageNotice()): ?> + <div class="notice global site local_storage"> <div class="content"> <p> - <strong><?= $block->escapeHtml(__('Local Storage seems to be disabled in your browser.')) ?></strong><br /> - <?= $block->escapeHtml(__('For the best experience on our site, be sure to turn on Local Storage in your browser.')) ?> + <strong><?= $block->escapeHtml(__('Local Storage seems to be disabled in your browser.')) ?></strong> + <br /> + <?= $block->escapeHtml( + __('For the best experience on our site, be sure to turn on Local Storage in your browser.') + ) ?> </p> </div> </div> - <script> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'div.notice.global.site.local_storage') ?> + + <?php $scriptString = <<<script + require(['jquery'], function(jQuery){ // <![CDATA[ @@ -45,9 +56,12 @@ require(['jquery'], function(jQuery){ // ]]> }); -</script> + +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> -<?php if ($block->displayDemoNotice()) : ?> +<?php if ($block->displayDemoNotice()): ?> <div class="message global demo"> <div class="content"> <p><?= $block->escapeHtml(__('This is a demo store. No orders will be fulfilled.')) ?></p> diff --git a/app/code/Magento/Theme/view/frontend/templates/html/print.phtml b/app/code/Magento/Theme/view/frontend/templates/html/print.phtml index d05faac66ffd1..e939ad40aafb6 100644 --- a/app/code/Magento/Theme/view/frontend/templates/html/print.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/html/print.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require( [ 'jquery' @@ -15,4 +19,7 @@ }); } ); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml index 55798169cdf75..d2803a741d9a2 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/calendar.phtml @@ -4,6 +4,9 @@ * See COPYING.txt for license details. */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +/** @var \Magento\Framework\View\Element\Html\Calendar $block */ + /** * Calendar localization script. Should be put into page header. * @@ -11,32 +14,33 @@ */ ?> -<script> +<?php $intFirstDay = (int)$firstDay; +$scriptString = <<<script + require([ 'jquery', - 'jquery-ui-modules/datepicker' ], function($){ //<![CDATA[ $.extend(true, $, { calendarConfig: { - dayNames: <?= /* @noEscape */ $days['wide'] ?>, - dayNamesMin: <?= /* @noEscape */ $days['abbreviated'] ?>, - monthNames: <?= /* @noEscape */ $months['wide'] ?>, - monthNamesShort: <?= /* @noEscape */ $months['abbreviated'] ?>, - infoTitle: "<?= $block->escapeJs(__('About the calendar')) ?>", - firstDay: <?= (int)$firstDay ?>, - closeText: "<?= $block->escapeJs(__('Close')) ?>", - currentText: "<?= $block->escapeJs(__('Go Today')) ?>", - prevText: "<?= $block->escapeJs(__('Previous')) ?>", - nextText: "<?= $block->escapeJs(__('Next')) ?>", - weekHeader: "<?= $block->escapeJs(__('WK')) ?>", - timeText: "<?= $block->escapeJs(__('Time')) ?>", - hourText: "<?= $block->escapeJs(__('Hour')) ?>", - minuteText: "<?= $block->escapeJs(__('Minute')) ?>", - dateFormat: $.datepicker.RFC_2822, - showOn: "button", - showAnim: "", + dayNames: {$days['wide']}, + dayNamesMin: {$days['abbreviated']}, + monthNames: {$months['wide']}, + monthNamesShort: {$months['abbreviated']}, + infoTitle: '{$block->escapeJs(__('About the calendar'))}', + firstDay: {$intFirstDay}, + closeText: '{$block->escapeJs(__('Close'))}', + currentText: '{$block->escapeJs(__('Go Today'))}', + prevText: '{$block->escapeJs(__('Previous'))}', + nextText: '{$block->escapeJs(__('Next'))}', + weekHeader: '{$block->escapeJs(__('WK'))}', + timeText: '{$block->escapeJs(__('Time'))}', + hourText: '{$block->escapeJs(__('Hour'))}', + minuteText: '{$block->escapeJs(__('Minute'))}', + dateFormat: "D, d M yy", // $.datepicker.RFC_2822 + showOn: 'button', + showAnim: '', changeMonth: true, changeYear: true, buttonImageOnly: null, @@ -50,8 +54,10 @@ require([ } }); - enUS = <?= /* @noEscape */ $enUS ?>; // en_US locale reference + enUS = {$enUS}; // en_US locale reference //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml b/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml index 2da71c90b5657..7d43ffcbb8063 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/cookie_status.phtml @@ -3,11 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<div id="cookie-status" style="display: none"> +<div id="cookie-status"> <?= $block->escapeHtml(__('The store will not work correctly in the case when cookies are disabled.')); ?> </div> +<?php +$script = 'document.querySelector("#cookie-status").style.display = "none";'; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', ['type' => 'text/javascript'], $script, false); ?> <script type="text/x-magento-init"> { @@ -16,5 +22,3 @@ } } </script> - - diff --git a/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml b/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml index d90d528ffc6f8..11a388a0fec20 100644 --- a/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/js/css_rel_preload.phtml @@ -3,8 +3,26 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + /*! loadCSS rel=preload polyfill. [c]2017 Filament Group, Inc. MIT License */ - !function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={};if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")}catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){var e=t.media||"all";function a(){t.media=e}t.addEventListener?t.addEventListener("load",a):t.attachEvent&&t.attachEvent("onload",a),setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(a,3e3)},e.poly=function(){if(!e.support())for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel||"style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")||(o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500);t.addEventListener?t.addEventListener("load",function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload",function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS}("undefined"!=typeof global?global:this); -</script> + !function(t){"use strict";t.loadCSS||(t.loadCSS=function(){});var e=loadCSS.relpreload={}; + if(e.support=function(){var e;try{e=t.document.createElement("link").relList.supports("preload")} + catch(t){e=!1}return function(){return e}}(),e.bindMediaToggle=function(t){var e=t.media||"all"; + function a(){t.media=e}t.addEventListener?t.addEventListener("load",a):t.attachEvent&&t.attachEvent("onload",a), + setTimeout(function(){t.rel="stylesheet",t.media="only x"}),setTimeout(a,3e3)},e.poly=function(){if(!e.support()) + for(var a=t.document.getElementsByTagName("link"),n=0;n<a.length;n++){var o=a[n];"preload"!==o.rel|| + "style"!==o.getAttribute("as")||o.getAttribute("data-loadcss")|| + (o.setAttribute("data-loadcss",!0),e.bindMediaToggle(o))}},!e.support()){e.poly();var a=t.setInterval(e.poly,500); + t.addEventListener?t.addEventListener("load", + function(){e.poly(),t.clearInterval(a)}):t.attachEvent&&t.attachEvent("onload", + function(){e.poly(),t.clearInterval(a)})}"undefined"!=typeof exports?exports.loadCSS=loadCSS:t.loadCSS=loadCSS} + ("undefined"!=typeof global?global:this); + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Theme/view/frontend/templates/messages.phtml b/app/code/Magento/Theme/view/frontend/templates/messages.phtml index dd9b81ecb38b9..85e752635fb3a 100644 --- a/app/code/Magento/Theme/view/frontend/templates/messages.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/messages.phtml @@ -11,17 +11,18 @@ class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type }"> - <div data-bind="html: message.text"></div> + <div data-bind="html: $parent.prepareMessageForHtml(message.text)"></div> </div> </div> <!-- /ko --> + <!-- ko if: messages().messages && messages().messages.length > 0 --> <div role="alert" data-bind="foreach: { data: messages().messages, as: 'message' }" class="messages"> <div data-bind="attr: { class: 'message-' + message.type + ' ' + message.type + ' message', 'data-ui-id': 'message-' + message.type }"> - <div data-bind="html: message.text"></div> + <div data-bind="html: $parent.prepareMessageForHtml(message.text)"></div> </div> </div> <!-- /ko --> diff --git a/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml b/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml index ad998c56b963f..a83d510ee0926 100644 --- a/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml +++ b/app/code/Magento/Theme/view/frontend/templates/page/js/require_js.phtml @@ -3,10 +3,13 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -?> -<script> - var BASE_URL = '<?= $block->escapeUrl($block->getBaseUrl()) ?>'; + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$scriptString = ' + var BASE_URL = \'' . /* @noEscape */ $block->escapeJs($block->getBaseUrl()) .'\'; var require = { - "baseUrl": "<?= $block->escapeUrl($block->getViewFileUrl('/')) ?>" - }; -</script> + \'baseUrl\': \'' . /* @noEscape */ $block->escapeJs($block->getViewFileUrl('/')) . '\' + };'; + +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js index cefbf11d73933..8d2ffed2d3bac 100644 --- a/app/code/Magento/Theme/view/frontend/web/js/view/messages.js +++ b/app/code/Magento/Theme/view/frontend/web/js/view/messages.js @@ -11,14 +11,16 @@ define([ 'uiComponent', 'Magento_Customer/js/customer-data', 'underscore', + 'escaper', 'jquery/jquery-storageapi' -], function ($, Component, customerData, _) { +], function ($, Component, customerData, _, escaper) { 'use strict'; return Component.extend({ defaults: { cookieMessages: [], - messages: [] + messages: [], + allowedTags: ['div', 'span', 'b', 'strong', 'i', 'em', 'u', 'a'] }, /** @@ -38,6 +40,16 @@ define([ } $.cookieStorage.set('mage-messages', ''); + }, + + /** + * Prepare the given message to be rendered as HTML + * + * @param {String} message + * @return {String} + */ + prepareMessageForHtml: function (message) { + return escaper.escapeHtml(message, this.allowedTags); } }); }); diff --git a/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php b/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php index e59aa2934e4ed..e525bf62cc3a3 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Gallery/Config.php @@ -12,7 +12,7 @@ /** * Class Config adds information about required configurations to display media gallery of tinymce3 editor * - * @deprecated use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead + * @deprecated 100.3.0 use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php b/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php index 00f1a82698381..96a3d42d15f36 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php +++ b/app/code/Magento/Tinymce3/Model/Config/Source/Wysiwyg/Editor.php @@ -9,7 +9,7 @@ /** * Class Editor provides configuration value for TinyMCE3 editor - * @deprecated use as configuration value tinymce4 path: mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter + * @deprecated 100.3.0 use as configuration value tinymce4 path: mage/adminhtml/wysiwyg/tiny_mce/tinymce4Adapter */ class Editor { diff --git a/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php b/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php index 2d016a5101abe..97aab0f38c4ee 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Variable/Config.php @@ -9,7 +9,7 @@ /** * Class Config adds variable plugin information required for tinymce3 editor - * @deprecated use \Magento\Variable\Model\Variable\ConfigProvider instead + * @deprecated 100.3.0 use \Magento\Variable\Model\Variable\ConfigProvider instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php b/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php index de548df4bc9f3..fcb8235495d47 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Widget/Config.php @@ -9,7 +9,7 @@ /** * Class Config adds widget plugin information required for tinymce3 editor - * @deprecated use \Magento\Widget\Model\Widget\Config instead + * @deprecated 100.3.0 use \Magento\Widget\Model\Widget\Config instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php b/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php index 1ab3de708dd26..7004beea70f1c 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php +++ b/app/code/Magento/Tinymce3/Model/Config/Widget/PlaceholderImagesPool.php @@ -8,7 +8,7 @@ /** * Class PlaceholderImages provide ability to override placeholder images for Widgets - * @deprecated + * @deprecated 100.3.0 */ class PlaceholderImagesPool { diff --git a/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php b/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php index f3dc4c8591cbd..c70ad28a114df 100644 --- a/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php +++ b/app/code/Magento/Tinymce3/Model/Config/Wysiwyg/Config.php @@ -9,7 +9,7 @@ /** * Class Config adds information about required css files for tinymce3 editor - * @deprecated use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead + * @deprecated 100.3.0 use \Magento\Cms\Model\Wysiwyg\DefaultConfigProvider instead */ class Config implements \Magento\Framework\Data\Wysiwyg\ConfigProviderInterface { diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml index 14002028d9da4..13917561629bc 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/NewsletterWYSIWYGSection.xml @@ -8,6 +8,6 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="NewsletterWYSIWYGSection"> - <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl"/> + <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl" deprecated="This version of TinyMCE is no longer supported"/> </section> </sections> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml index 9ce4e067169ec..bc666d3590a8f 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/ProductWYSIWYGSection.xml @@ -7,7 +7,7 @@ --> <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> - <section name="ProductWYSIWYGSection"> + <section name="ProductWYSIWYGSection" deprecated="This version of TinyMCE is no longer supported"> <element name="Tinymce3MSG" type="button" selector=".admin__field-error"/> </section> </sections> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml index cb46bed781e5a..38b2d907ecf44 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Section/AdminTinymce3FileldsSection/TinyMCESection.xml @@ -8,7 +8,7 @@ <sections xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Page/etc/SectionObject.xsd"> <section name="TinyMCESection"> - <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl"/> - <element name="InsertImageBtnTinyMCE3" type="button" selector="#cms_page_form_content_image"/> + <element name="TinyMCE3" type="text" selector="#cms_page_form_content_tbl" deprecated="Deprecated this version of TinyMCE is no longer supported"/> + <element name="InsertImageBtnTinyMCE3" type="button" selector="#cms_page_form_content_image" deprecated="Deprecated this version of TinyMCE is no longer supported"/> </section> </sections> diff --git a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml index 9c4e7bf3a646a..01b2101d3346c 100644 --- a/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml +++ b/app/code/Magento/Tinymce3/Test/Mftf/Test/AdminSwitchWYSIWYGOptionsTest.xml @@ -8,7 +8,7 @@ <tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> - <test name="AdminSwitchWYSIWYGOptionsTest"> + <test name="AdminSwitchWYSIWYGOptionsTest" deprecated="TinyMCE3 is no longer supported"> <annotations> <features value="Cms"/> <stories value="MAGETWO-51829-Extensible list of WYSIWYG editors available in Magento"/> @@ -17,6 +17,9 @@ <description value="Admin should able to switch between versions of TinyMCE"/> <severity value="CRITICAL"/> <testCaseId value="MC-6114"/> + <skip> + <issueId value="DEPRECATED">TinyMCE3 is no longer supported</issueId> + </skip> </annotations> <before> <actionGroup ref="AdminLoginActionGroup" stepKey="loginGetFromGeneralFile"/> @@ -63,8 +66,8 @@ <waitForPageLoad stepKey="wait5"/> <fillField selector="{{CmsNewPagePageBasicFieldsSection.pageTitle}}" userInput="{{_defaultCmsPage.title}}" stepKey="fillFieldTitle2"/> <click selector="{{CmsNewPagePageContentSection.header}}" stepKey="clickContentTab2" /> - <waitForElementVisible selector="{{TinyMCESection.TinyMCE3}}" stepKey="waitForTinyMCE3"/> - <seeElement selector="{{TinyMCESection.TinyMCE3}}" stepKey="seeTinyMCE3" /> + <comment userInput="removing deprecated element" stepKey="waitForTinyMCE3"/> + <comment userInput="removing deprecated element" stepKey="seeTinyMCE3" /> <executeJS function="tinyMCE.activeEditor.setContent('Hello TinyMCE3!');" stepKey="executeJSFillContent2"/> <click selector="{{CmsWYSIWYGSection.ShowHideBtn}}" stepKey="clickShowHideBtn2" /> <scrollTo selector="{{CmsNewPagePageSeoSection.header}}" stepKey="scrollToSearchEngineTab2" /> diff --git a/app/code/Magento/Tinymce3/etc/adminhtml/di.xml b/app/code/Magento/Tinymce3/etc/adminhtml/di.xml index 53ab66c7ef21f..bcccf44594103 100644 --- a/app/code/Magento/Tinymce3/etc/adminhtml/di.xml +++ b/app/code/Magento/Tinymce3/etc/adminhtml/di.xml @@ -39,14 +39,4 @@ </argument> </arguments> </type> - <type name="Magento\Cms\Model\Config\Source\Wysiwyg\Editor"> - <arguments> - <argument name="adapterOptions" xsi:type="array"> - <item name="tinymce3" xsi:type="array"> - <item name="value" xsi:type="const">Magento\Tinymce3\Model\Config\Source\Wysiwyg\Editor::WYSIWYG_EDITOR_CONFIG_VALUE</item> - <item name="label" xsi:type="string" translatable="true">TinyMCE 3 (deprecated)</item> - </item> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/Tinymce3/etc/csp_whitelist.xml b/app/code/Magento/Tinymce3/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..e2b2364dc80e8 --- /dev/null +++ b/app/code/Magento/Tinymce3/etc/csp_whitelist.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="style-src"> + <values> + <value id="firebug" type="host">getfirebug.com</value> + </values> + </policy> + <policy id="script-src"> + <values> + <value id="www_youtube" type="host">www.youtube.com</value> + <value id="google_video" type="host">video.google.com</value> + </values> + </policy> + <policy id="img-src"> + <values> + <value id="youtube_cdn" type="host">s.ytimg.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/app/code/Magento/Tinymce3/etc/di.xml b/app/code/Magento/Tinymce3/etc/di.xml index e03d865ce4e01..0b1175b0cd94c 100644 --- a/app/code/Magento/Tinymce3/etc/di.xml +++ b/app/code/Magento/Tinymce3/etc/di.xml @@ -6,11 +6,4 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> - <type name="Magento\Ui\Block\Wysiwyg\ActiveEditor"> - <arguments> - <argument name="availableAdapterPaths" xsi:type="array"> - <item name="Magento_Tinymce3/tinymce3Adapter" xsi:type="string"/> - </argument> - </arguments> - </type> </config> diff --git a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js index a3bd16cab718e..2119426a5c157 100644 --- a/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js +++ b/app/code/Magento/Tinymce3/view/base/web/tiny_mce/plugins/lists/editor_plugin_src.js @@ -82,9 +82,9 @@ } } - function attemptMerge(e1, e2, differentStylesMasterElement, mergeParagraphs) { - if (canMerge(e1, e2, !!differentStylesMasterElement, mergeParagraphs)) { - return merge(e1, e2, differentStylesMasterElement); + function attemptMerge(e1, e2, differentStylesMainElement, mergeParagraphs) { + if (canMerge(e1, e2, !!differentStylesMainElement, mergeParagraphs)) { + return merge(e1, e2, differentStylesMainElement); } else if (e1 && e1.tagName === 'LI' && isList(e2)) { // Fix invalidly nested lists. e1.appendChild(e2); @@ -112,7 +112,7 @@ return firstChild && lastChild && firstChild === lastChild && isList(firstChild); } - function merge(e1, e2, masterElement) { + function merge(e1, e2, mainElement) { var lastOriginal = skipWhitespaceNodesBackwards(e1.lastChild), firstNew = skipWhitespaceNodesForwards(e2.firstChild); if (e1.tagName === 'P') { e1.appendChild(e1.ownerDocument.createElement('br')); @@ -120,8 +120,8 @@ while (e2.firstChild) { e1.appendChild(e2.firstChild); } - if (masterElement) { - e1.style.listStyleType = masterElement.style.listStyleType; + if (mainElement) { + e1.style.listStyleType = mainElement.style.listStyleType; } e2.parentNode.removeChild(e2); attemptMerge(lastOriginal, firstNew, false); @@ -164,7 +164,7 @@ } return false; } - + // If we are at the end of a paragraph in a list item, pressing enter should create a new list item instead of a new paragraph. function isEndOfParagraph() { var node = ed.selection.getNode(); @@ -241,7 +241,7 @@ Event.cancel(e); } } - + // Creates a new list item after the current selection's list item parent function createNewLi(ed, e) { if (state == LIST_PARAGRAPH) { diff --git a/app/code/Magento/Translation/Block/Js.php b/app/code/Magento/Translation/Block/Js.php index db26feb8067ff..fa3d6905f5868 100644 --- a/app/code/Magento/Translation/Block/Js.php +++ b/app/code/Magento/Translation/Block/Js.php @@ -14,6 +14,10 @@ * * @api * @since 100.0.2 + * @deprecated logic was refactored in order to not use localstorage at all. + * + * You can see details in app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js + * These block and view file were left in order to keep backward compatibility */ class Js extends Template { @@ -78,6 +82,7 @@ public function getTranslationFilePath() * Gets current version of the translation file. * * @return string + * @since 100.3.0 */ public function getTranslationFileVersion() { diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationOnProductPageTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationOnProductPageTest.xml new file mode 100644 index 0000000000000..f3103c4bea51c --- /dev/null +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationOnProductPageTest.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontButtonsInlineTranslationOnProductPageTest"> + <annotations> + <features value="Translation"/> + <stories value="Inline Translation"/> + <title value="Buttons inline translation on product page"/> + <description value="A merchant should be able to translate buttons by an inline translation tool"/> + <severity value="CRITICAL"/> + <testCaseId value="MC-27118"/> + <useCaseId value="MC-24186"/> + <group value="translation"/> + <group value="catalog"/> + <group value="developer_mode_only"/> + </annotations> + <before> + <!-- Enable Translate Inline For Storefront --> + <magentoCLI command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" stepKey="enableTranslateInlineForStorefront"/> + <!-- Create Simple Product --> + <createData entity="SimpleProduct2" stepKey="createProduct"/> + </before> + <after> + <!-- Disable Translate Inline For Storefront --> + <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> + <!-- Delete Simple Product --> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + </after> + + <!-- Add product to cart on storefront --> + <amOnPage url="{{StorefrontProductPage.url($createProduct.custom_attributes[url_key]$)}}" stepKey="goToProductPage"/> + <waitForPageLoad stepKey="waitForProductPageLoad"/> + <waitForElementVisible selector="{{StorefrontProductActionSection.addToCartEnabledWithTranslation}}" stepKey="waitForAddToCartButtonEnabled"/> + <actionGroup ref="AddToCartFromStorefrontProductPageActionGroup" stepKey="addProductToCart"> + <argument name="productName" value="$createProduct.name$"/> + </actionGroup> + + <!-- Open Mini Cart --> + <actionGroup ref="StorefrontOpenMiniCartActionGroup" stepKey="openMiniCart"/> + + <!-- Check button "Proceed to Checkout". There must be red borders and "book" icons on labels that can be translated. --> + <actionGroup ref="AssertElementInTranslateInlineModeActionGroup" stepKey="assertRedBordersAndBookIcon"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + </actionGroup> + + <!-- Open Inline Translation popup --> + <actionGroup ref="StorefrontOpenInlineTranslationPopupActionGroup" stepKey="openInlineTranslationPopup"> + <argument name="elementSelector" value="{{StorefrontMinicartSection.goToCheckout}}"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml index 1ba3236185148..3d617360a9d28 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontButtonsInlineTranslationTest.xml @@ -20,6 +20,9 @@ <group value="translation"/> <group value="catalog"/> <group value="developer_mode_only"/> + <skip> + <issueId value="DEPRECATED">Use StorefrontButtonsInlineTranslationOnProductPageTest instead</issueId> + </skip> </annotations> <before> <!-- Enable Translate Inline For Storefront --> diff --git a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml index 9255923213839..e30ab98982b78 100644 --- a/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml +++ b/app/code/Magento/Translation/Test/Mftf/Test/StorefrontInlineTranslationOnCheckoutTest.xml @@ -115,7 +115,9 @@ <magentoCLI command="config:set {{EnableTranslateInlineForStorefront.path}} {{EnableTranslateInlineForStorefront.value}}" stepKey="enableTranslateInlineForStorefront"/> <!-- 2. Refresh magento cache --> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterTranslateEnabled"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateEnabled"> + <argument name="tags" value=""/> + </actionGroup> <!-- 3. Go to storefront and click on cart button on the top --> <reloadPage stepKey="reloadPage"/> @@ -476,7 +478,9 @@ <!-- 7. Set *Enabled for Storefront* option to *No* and save configuration --> <magentoCLI command="config:set {{DisableTranslateInlineForStorefront.path}} {{DisableTranslateInlineForStorefront.value}}" stepKey="disableTranslateInlineForStorefront"/> <!-- 8. Clear magento cache --> - <magentoCLI command="cache:flush" stepKey="flushCacheAfterTranslateDisabled"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterTranslateDisabled"> + <argument name="tags" value=""/> + </actionGroup> <magentoCLI command="setup:static-content:deploy -f" stepKey="deployStaticContent"/> diff --git a/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml b/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml index 6b6327a5679ea..67dd55d3d6372 100644 --- a/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml +++ b/app/code/Magento/Translation/view/adminhtml/templates/translate_inline.phtml @@ -4,10 +4,15 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Framework\View\Element\Template $block */ +/** + * @var \Magento\Framework\View\Element\Template $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> -<link rel="stylesheet" type="text/css" href="<?= $block->escapeUrl($block->getViewFileUrl('prototype/windows/themes/default.css')) ?>"/> -<link rel="stylesheet" type="text/css" href="<?= $block->escapeUrl($block->getViewFileUrl('mage/translate-inline.css')) ?>"/> +<link rel="stylesheet" type="text/css" + href="<?= $block->escapeUrl($block->getViewFileUrl('prototype/windows/themes/default.css')) ?>"/> +<link rel="stylesheet" type="text/css" + href="<?= $block->escapeUrl($block->getViewFileUrl('mage/translate-inline.css')) ?>"/> <script id="translate-inline-icon" type="text/x-magento-template"> <img src="<%- data.img %>" height="16" width="16" class="translate-edit-icon"> @@ -51,8 +56,12 @@ <% } %> </script> -<div data-role="translate-dialog" data-mage-init='{"translateInline":{"ajaxUrl":"<?= $block->escapeJs($block->escapeUrl($block->getAjaxUrl())) ?>"},"loader":{}}'></div> -<script> +<div data-role="translate-dialog" + data-mage-init='{"translateInline":{"ajaxUrl":"<?= $block->escapeJs($block->escapeUrl($block->getAjaxUrl())) ?>"}, + "loader":{}}'> +</div> +<?php $scriptString = <<<script + require([ "jquery", "mage/edit-trigger", @@ -60,12 +69,15 @@ require([ ], function($){ $('body').editTrigger( { - img: '<?= $block->escapeJs($block->escapeUrl($block->getViewFileUrl('Magento_Theme::fam_book_open.png'))) ?>', + img: '{$block->escapeJs($block->getViewFileUrl('Magento_Theme::fam_book_open.png'))}', alwaysShown: true, singleElement: false } ); - + $('body').addClass('trnslate-inline-area'); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Translation/view/base/requirejs-config.js b/app/code/Magento/Translation/view/base/requirejs-config.js new file mode 100644 index 0000000000000..682c3fca81117 --- /dev/null +++ b/app/code/Magento/Translation/view/base/requirejs-config.js @@ -0,0 +1,15 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +var config = { + map: { + '*': { + mageTranslationDictionary: 'Magento_Translation/js/mage-translation-dictionary' + } + }, + deps: [ + 'mageTranslationDictionary' + ] +}; diff --git a/app/code/Magento/Translation/view/base/templates/translate.phtml b/app/code/Magento/Translation/view/base/templates/translate.phtml index 4c257eb76843f..98997398c0938 100644 --- a/app/code/Magento/Translation/view/base/templates/translate.phtml +++ b/app/code/Magento/Translation/view/base/templates/translate.phtml @@ -4,57 +4,11 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Translation\Block\Js $block */ -?> -<!-- - For frontend area dictionary file is inserted into html head in Magento/Translation/view/base/templates/dictionary.phtml - Same translation mechanism should be introduced for admin area in 2.4 version. ---> -<?php if ($block->dictionaryEnabled()) : ?> - <script> - require.config({ - deps: [ - 'jquery', - 'mage/translate', - 'jquery/jquery-storageapi' - ], - callback: function ($) { - 'use strict'; - - var dependencies = [], - versionObj; - - $.initNamespaceStorage('mage-translation-storage'); - $.initNamespaceStorage('mage-translation-file-version'); - versionObj = $.localStorage.get('mage-translation-file-version'); - - <?php $version = $block->getTranslationFileVersion(); ?> - - if (versionObj.version !== '<?= $block->escapeJs($version) ?>') { - dependencies.push( - 'text!<?= /* @noEscape */ Magento\Translation\Model\Js\Config::DICTIONARY_FILE_NAME ?>' - ); - - } - - require.config({ - deps: dependencies, - callback: function (string) { - if (typeof string === 'string') { - $.mage.translate.add(JSON.parse(string)); - $.localStorage.set('mage-translation-storage', string); - $.localStorage.set( - 'mage-translation-file-version', - { - version: '<?= $block->escapeJs($version) ?>' - } - ); - } else { - $.mage.translate.add($.localStorage.get('mage-translation-storage')); - } - } - }); - } - }); - </script> -<?php endif; ?> +/** + * @var \Magento\Translation\Block\Js $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + * @deprecated logic was refactored in order to not use localstorage at all. + * + * You can see details in app/code/Magento/Translation/view/base/web/js/mage-translation-dictionary.js + * These block and view file were left in order to keep backward compatibility + */ diff --git a/app/code/Magento/Translation/view/frontend/requirejs-config.js b/app/code/Magento/Translation/view/frontend/requirejs-config.js index b5351b9d471cf..b4b3ce0f8c554 100644 --- a/app/code/Magento/Translation/view/frontend/requirejs-config.js +++ b/app/code/Magento/Translation/view/frontend/requirejs-config.js @@ -8,12 +8,10 @@ var config = { '*': { editTrigger: 'mage/edit-trigger', addClass: 'Magento_Translation/js/add-class', - 'Magento_Translation/add-class': 'Magento_Translation/js/add-class', - mageTranslationDictionary: 'Magento_Translation/js/mage-translation-dictionary' + 'Magento_Translation/add-class': 'Magento_Translation/js/add-class' } }, deps: [ - 'mage/translate-inline', - 'mageTranslationDictionary' + 'mage/translate-inline' ] }; diff --git a/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php b/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php index b6077b7b1625d..462e4a4695ef0 100644 --- a/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php +++ b/app/code/Magento/Ui/Block/Wysiwyg/ActiveEditor.php @@ -13,6 +13,7 @@ * ActiveEditor block * * @api + * @since 101.1.0 */ class ActiveEditor extends \Magento\Framework\View\Element\Template { @@ -50,6 +51,7 @@ public function __construct( * Get active wysiwyg adapter path * * @return string + * @since 101.1.0 */ public function getWysiwygAdapterPath() { diff --git a/app/code/Magento/Ui/Component/Control/Button.php b/app/code/Magento/Ui/Component/Control/Button.php index 952f1f62fa2d7..fbbf0e1f0fa61 100644 --- a/app/code/Magento/Ui/Component/Control/Button.php +++ b/app/code/Magento/Ui/Component/Control/Button.php @@ -5,14 +5,45 @@ */ namespace Magento\Ui\Component\Control; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; use Magento\Framework\View\Element\Template; use Magento\Framework\View\Element\UiComponent\Control\ControlInterface; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\View\Element\Template\Context; /** - * Class Button + * Widget for standard button. */ class Button extends Template implements ControlInterface { + /** + * @var Random + */ + private $random; + + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @param Context $context + * @param array $data + * @param Random|null $random + * @param SecureHtmlRenderer|null $htmlRenderer + */ + public function __construct( + Context $context, + array $data = [], + ?Random $random = null, + ?SecureHtmlRenderer $htmlRenderer = null + ) { + parent::__construct($context, $data); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + $this->secureRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + } + /** * Define block template * @@ -91,6 +122,18 @@ public function getOnClick() } } + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + $this->setData('ui_button_widget_hook_id', 'buttonId' .$this->random->getRandomString(10)); + + return $this; + } + /** * Prepare attributes * @@ -107,8 +150,6 @@ protected function prepareAttributes($title, $classes, $disabled) 'title' => $title, 'type' => $this->getType(), 'class' => implode(' ', $classes), - 'onclick' => $this->getOnClick(), - 'style' => $this->getStyle(), 'value' => $this->getValue(), 'disabled' => $disabled, ]; @@ -117,6 +158,9 @@ protected function prepareAttributes($title, $classes, $disabled) $attributes['data-' . $key] = is_scalar($attr) ? $attr : json_encode($attr); } } + if ($this->hasData('ui_button_widget_hook_id')) { + $attributes['ui-button-widget-hook-id'] = $this->getData('ui_button_widget_hook_id'); + } return $attributes; } @@ -139,4 +183,31 @@ protected function attributesToHtml($attributes) return $html; } + + /** + * Return HTML to be rendered after the button. + * + * @return string|null + */ + public function getAfterHtml(): ?string + { + $afterHtml = $this->getData('after_html'); + $buttonId = $this->getData('ui_button_widget_hook_id'); + if ($handler = $this->getOnClick()) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + $handler, + "*[ui-button-widget-hook-id='$buttonId']" + ); + } + if ($this->getStyle()) { + $selector = "*[ui-button-widget-hook-id='$buttonId']"; + if ($this->getId()) { + $selector = "#{$this->getId()}"; + } + $afterHtml .= $this->secureRenderer->renderStyleAsTag($this->getStyle(), $selector); + } + + return $afterHtml; + } } diff --git a/app/code/Magento/Ui/Component/Control/SplitButton.php b/app/code/Magento/Ui/Component/Control/SplitButton.php index 5c9d09565fc66..64ca6cf3dfee6 100644 --- a/app/code/Magento/Ui/Component/Control/SplitButton.php +++ b/app/code/Magento/Ui/Component/Control/SplitButton.php @@ -6,8 +6,13 @@ namespace Magento\Ui\Component\Control; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Element\Template\Context; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** - * Class SplitButton + * Widget for standard button with a selection. * * @method string getTitle * @method string getLabel @@ -22,6 +27,32 @@ */ class SplitButton extends Button { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @inheritDoc + */ + public function __construct( + Context $context, + array $data = [], + ?Random $random = null, + ?SecureHtmlRenderer $htmlRenderer = null + ) { + $random = $random ?? ObjectManager::getInstance()->get(Random::class); + $htmlRenderer = $htmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($context, $data, $random, $htmlRenderer); + $this->random = $random; + $this->secureRenderer = $htmlRenderer; + } + /** * @inheritdoc */ @@ -54,6 +85,16 @@ public function getAttributesHtml() return $this->attributesToHtml(['title' => $title, 'class' => join(' ', $classes)]); } + /** + * Get main button's "id" attribute value. + * + * @return string + */ + private function getButtonId(): string + { + return $this->getId() .'-button'; + } + /** * Retrieve button attributes html * @@ -77,11 +118,10 @@ public function getButtonAttributesHtml() } $attributes = [ - 'id' => $this->getId() . '-button', + 'id' => $this->getButtonId(), 'title' => $title, 'class' => join(' ', $classes), 'disabled' => $disabled, - 'style' => $this->getStyle(), ]; if ($idHard = $this->getIdHard()) { @@ -159,6 +199,21 @@ public function getOptionAttributesHtml($key, $option) return $html; } + /** + * Retrieve "id" attribute value for an option. + * + * @param array $option + * @return string + */ + private function identifyOption(array $option): string + { + return isset($option['id']) + ? $this->getId() .'-' .$option['id'] + : (isset($option['id_attribute']) ? + $option['id_attribute'] + : $this->getId() .'-optId' .$this->random->getRandomString(10)); + } + /** * Prepare option attributes * @@ -172,11 +227,9 @@ public function getOptionAttributesHtml($key, $option) protected function prepareOptionAttributes($option, $title, $classes, $disabled) { $attributes = [ - 'id' => isset($option['id']) ? $this->getId() . '-' . $option['id'] : '', + 'id' => $this->identifyOption($option), 'title' => $title, 'class' => join(' ', $classes), - 'onclick' => isset($option['onclick']) ? $option['onclick'] : '', - 'style' => isset($option['style']) ? $option['style'] : '', 'disabled' => $disabled, ]; @@ -215,4 +268,43 @@ protected function getDataAttributes($data, &$attributes) $attributes['data-' . $key] = is_scalar($attr) ? $attr : json_encode($attr); } } + + /** + * @inheritDoc + */ + protected function _beforeToHtml() + { + parent::_beforeToHtml(); + + /** @var array|null $options */ + $options = $this->getOptions() ?? []; + foreach ($options as &$option) { + $option['id_attribute'] = $this->identifyOption($option); + } + $this->setOptions($options); + + return $this; + } + + /** + * @inheritDoc + */ + public function getAfterHtml(): ?string + { + $afterHtml = parent::getAfterHtml(); + + /** @var array|null $options */ + $options = $this->getOptions() ?? []; + foreach ($options as $option) { + $id = $this->identifyOption($option); + if (!empty($option['onclick'])) { + $afterHtml .= $this->secureRenderer->renderEventListenerAsTag('onclick', $option['onclick'], "#$id"); + } + if (!empty($option['style'])) { + $afterHtml .= $this->secureRenderer->renderStyleAsTag($option['style'], "#$id"); + } + } + + return $afterHtml; + } } diff --git a/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php b/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php index b1925b4641d0b..23188363cc1d1 100644 --- a/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php +++ b/app/code/Magento/Ui/Component/Form/Element/ColorPicker.php @@ -17,6 +17,7 @@ * Prepares Color Picker UI component with mode and format * * @api + * @since 101.1.0 */ class ColorPicker extends AbstractElement { @@ -54,6 +55,7 @@ public function __construct( * Get component name * * @return string + * @since 101.1.0 */ public function getComponentName(): string { @@ -64,6 +66,7 @@ public function getComponentName(): string * Prepare component configuration * * @return void + * @since 101.1.0 */ public function prepare() : void { diff --git a/app/code/Magento/Ui/Component/Listing/Columns/Date.php b/app/code/Magento/Ui/Component/Listing/Columns/Date.php index f0b3ee4334d4f..d141499718a76 100644 --- a/app/code/Magento/Ui/Component/Listing/Columns/Date.php +++ b/app/code/Magento/Ui/Component/Listing/Columns/Date.php @@ -77,6 +77,7 @@ public function __construct( /** * @inheritdoc + * @since 101.1.1 */ public function prepare() { diff --git a/app/code/Magento/Ui/Component/Wrapper/Block.php b/app/code/Magento/Ui/Component/Wrapper/Block.php index a4e5bbf213062..0380a447e0cb9 100644 --- a/app/code/Magento/Ui/Component/Wrapper/Block.php +++ b/app/code/Magento/Ui/Component/Wrapper/Block.php @@ -11,7 +11,7 @@ use Magento\Framework\View\Element\UiComponent\BlockWrapperInterface; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class Block extends AbstractComponent implements BlockWrapperInterface { diff --git a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php index b45880c1ce726..803495439d65e 100644 --- a/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php +++ b/app/code/Magento/Ui/Controller/Adminhtml/Bookmark/Save.php @@ -54,7 +54,7 @@ class Save extends AbstractAction implements HttpPostActionInterface /** * @var DecoderInterface - * @deprecated + * @deprecated 101.1.0 */ protected $jsonDecoder; diff --git a/app/code/Magento/Ui/Controller/Index/Render.php b/app/code/Magento/Ui/Controller/Index/Render.php index 42818686840aa..3ec58784ef53b 100644 --- a/app/code/Magento/Ui/Controller/Index/Render.php +++ b/app/code/Magento/Ui/Controller/Index/Render.php @@ -97,11 +97,8 @@ public function __construct( public function execute() { if ($this->_request->getParam('namespace') === null) { - $this->_redirect('admin/noroute'); - - return; + return $this->_redirect('noroute'); } - try { $component = $this->uiComponentFactory->create($this->getRequest()->getParam('namespace')); if ($this->validateAclResource($component->getContext()->getDataProvider()->getConfigData())) { @@ -110,6 +107,7 @@ public function execute() $contentType = $this->contentTypeResolver->resolve($component->getContext()); $this->getResponse()->setHeader('Content-Type', $contentType, true); + return $this->getResponse(); } else { /** @var \Magento\Framework\Controller\Result\Json $resultJson */ $resultJson = $this->resultJsonFactory->create(); diff --git a/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php b/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php index abbc79859a038..6e4e488619e86 100644 --- a/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php +++ b/app/code/Magento/Ui/DataProvider/AbstractDataProvider.php @@ -297,7 +297,7 @@ public function setConfigData($config) * Retrieve all ids from collection * * @return int[] - * @since 100.2.0 + * @since 101.0.0 */ public function getAllIds() { diff --git a/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php b/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php index 6336d8f8fb828..5c70a06dad318 100644 --- a/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php +++ b/app/code/Magento/Ui/DataProvider/Modifier/WysiwygModifierInterface.php @@ -7,20 +7,25 @@ /** * @api + * @since 101.1.0 */ interface WysiwygModifierInterface { /** - * Provide editor name - * For example tmce3 or tmce4 + * Provide editor name for example tmce4 * * @return array + * @since 101.1.0 */ public function getEditorName(); /** + * Modifies the meta + * * @param array $meta + * * @return array + * @since 101.1.0 */ public function modifyMeta(array $meta); } diff --git a/app/code/Magento/Ui/DataProvider/SearchResultFactory.php b/app/code/Magento/Ui/DataProvider/SearchResultFactory.php index f2ed0677d4cd9..83d06c7cf5fc1 100644 --- a/app/code/Magento/Ui/DataProvider/SearchResultFactory.php +++ b/app/code/Magento/Ui/DataProvider/SearchResultFactory.php @@ -17,6 +17,7 @@ * Allows to use Repositories (instead of Collections) in UI Components Data providers * * @api + * @since 101.1.0 */ class SearchResultFactory { @@ -64,6 +65,7 @@ public function __construct( * @param SearchCriteriaInterface SearchCriteriaInterface $searchCriteria * @param string $idFieldName * @return SearchResultInterface + * @since 101.1.0 */ public function create( array $items, diff --git a/app/code/Magento/Ui/Model/Bookmark.php b/app/code/Magento/Ui/Model/Bookmark.php index b404e8d3b475f..2cb5666063067 100644 --- a/app/code/Magento/Ui/Model/Bookmark.php +++ b/app/code/Magento/Ui/Model/Bookmark.php @@ -23,7 +23,7 @@ class Bookmark extends AbstractExtensibleModel implements BookmarkInterface { /** * @var DecoderInterface - * @deprecated + * @deprecated 101.1.0 */ protected $jsonDecoder; diff --git a/app/code/Magento/Ui/Model/Manager.php b/app/code/Magento/Ui/Model/Manager.php index 1bacdc80a5c5e..357a41285e275 100644 --- a/app/code/Magento/Ui/Model/Manager.php +++ b/app/code/Magento/Ui/Model/Manager.php @@ -24,7 +24,7 @@ /** * @inheritdoc * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Manager implements ManagerInterface diff --git a/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php b/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php index 3e738baa404c8..f773daaaf50c4 100644 --- a/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php +++ b/app/code/Magento/Ui/Model/ResourceModel/BookmarkRepository.php @@ -162,7 +162,7 @@ public function deleteById($bookmarkId) * @param FilterGroup $filterGroup * @param Collection $collection * @return void - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @throws \Magento\Framework\Exception\InputException */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collection $collection) @@ -176,7 +176,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti /** * Retrieve collection processor * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAddColumnToAdminGridActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAddColumnToAdminGridActionGroup.xml new file mode 100644 index 0000000000000..25cc4b5c17d92 --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/AdminAddColumnToAdminGridActionGroup.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AdminAddColumnToAdminGridActionGroup"> + <annotations> + <description value="Adds column to admin grid"/> + </annotations> + <arguments> + <argument name="columnName" defaultValue="Email" type="string"/> + </arguments> + <waitForElementVisible selector="{{AdminGridColumnsControls.columns}}" stepKey="waitForAdminGridColumnControlsColumn"/> + <click selector="{{AdminGridColumnsControls.columns}}" stepKey="clickAdminGridColumnControlsColumn"/> + <waitForElementVisible selector="{{AdminDataGridHeaderSection.columnCheckbox(columnName)}}" stepKey="verifyAdminGridColumnControlsForSelectedColumnVisible"/> + <click selector="{{AdminDataGridHeaderSection.columnCheckbox(columnName)}}" stepKey="clickForAdminGridControlForSelectedColumn"/> + <waitForElementVisible selector="{{AdminGridHeaders.headerByName(columnName)}}" stepKey="waitForAdminGridColumnHeaderForSelectedColumn"/> + <click selector="{{AdminGridColumnsControls.columns}}" stepKey="closeAdminGridColumnControls"/> + <waitForElementNotVisible selector="{{AdminGridColumnsControls.columnName(columnName)}}" stepKey="verifyAdminGridColumnControlsForSelectedColumnNotVisible"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/ActionGroup/StorefrontAssertErrorMessageActionGroup.xml b/app/code/Magento/Ui/Test/Mftf/ActionGroup/StorefrontAssertErrorMessageActionGroup.xml new file mode 100644 index 0000000000000..fa9b7c377e32b --- /dev/null +++ b/app/code/Magento/Ui/Test/Mftf/ActionGroup/StorefrontAssertErrorMessageActionGroup.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="StorefrontAssertErrorMessageActionGroup"> + <arguments> + <argument name="message" type="string"/> + <argument name="messageType" type="string" defaultValue="success"/> + </arguments> + + <see userInput="{{message}}" selector="{{StorefrontMessagesSection.messageByType(messageType)}}" stepKey="verifyMessage"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml index 133836761174d..51cebdb01a74d 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/AdminDataGridPaginationSection.xml @@ -21,5 +21,8 @@ <element name="currentPage" type="input" selector="div.admin__data-grid-pager > input[data-ui-id='current-page-input']"/> <element name="totalPages" type="text" selector="div.admin__data-grid-pager > label"/> <element name="perPageDropDownValue" type="input" selector=".selectmenu-value input" timeout="30"/> + <element name="selectedPage" type="input" selector="#sales_order_create_search_grid_page-current" timeout="30"/> + <element name="nextPageActive" type="button" selector="div.admin__data-grid-pager > button.action-next:not(.disabled)" timeout="30"/> + <element name="prevPageActive" type="button" selector="div.admin__data-grid-pager > button.action-previous:not(.disabled)" timeout="30"/> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml index c58479a7b73e5..f46e25a832134 100644 --- a/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml +++ b/app/code/Magento/Ui/Test/Mftf/Section/StorefrontMessagesSection.xml @@ -12,5 +12,6 @@ <element name="success" type="text" selector="div.message-success.success.message"/> <element name="error" type="text" selector="div.message-error.error.message"/> <element name="noticeMessage" type="text" selector="div.message.notice div"/> + <element name="messageByType" type="text" selector=".messages .message-{{messageType}}" parameterized="true" /> </section> </sections> diff --git a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml index d38e065914617..3c93ed38b4eed 100644 --- a/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml +++ b/app/code/Magento/Ui/Test/Mftf/Test/AdminGridFilterDeleteAndVerifyErrorMessageTest.xml @@ -51,8 +51,7 @@ </after> <!--Filter created simple product in grid and add category and website created in create data--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="openProductCatalogPage"/> - <waitForPageLoad stepKey="waitForProductCatalogPage"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductCatalogPage"/> <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="$$createProduct$$"/> </actionGroup> diff --git a/app/code/Magento/Ui/Test/Unit/Component/Control/ButtonTest.php b/app/code/Magento/Ui/Test/Unit/Component/Control/ButtonTest.php index b62075c3d8cb1..77c0bfd62865e 100644 --- a/app/code/Magento/Ui/Test/Unit/Component/Control/ButtonTest.php +++ b/app/code/Magento/Ui/Test/Unit/Component/Control/ButtonTest.php @@ -60,7 +60,7 @@ public function testGetType() public function testGetAttributesHtml() { $expected = 'type="button" class="action- scalable classValue disabled" ' - . 'onclick="location.href = 'url2';" disabled="disabled" data-attributeKey="attributeValue" '; + . 'disabled="disabled" data-attributeKey="attributeValue" '; $this->button->setDisabled(true); $this->button->setData('url', 'url2'); $this->button->setData('class', 'classValue'); diff --git a/app/code/Magento/Ui/view/base/templates/control/button/split.phtml b/app/code/Magento/Ui/view/base/templates/control/button/split.phtml index 08230184d5a4d..ce7112c400509 100644 --- a/app/code/Magento/Ui/view/base/templates/control/button/split.phtml +++ b/app/code/Magento/Ui/view/base/templates/control/button/split.phtml @@ -11,19 +11,19 @@ <button <?= $block->getButtonAttributesHtml() ?>> <span><?= $block->escapeHtml($block->getLabel()) ?></span> </button> - <?php if ($block->hasSplit()) : ?> + <?php if ($block->hasSplit()): ?> <button <?= $block->getToggleAttributesHtml() ?>> <span><?= $block->escapeHtml(__('Select')) ?></span> </button> - <?php if (!$block->getDisabled()) : ?> + <?php if (!$block->getDisabled()): ?> <ul class="dropdown-menu" <?= /* @noEscape */ $block->getUiId("dropdown-menu") ?>> - <?php foreach ($block->getOptions() as $key => $option) : ?> + <?php foreach ($block->getOptions() as $key => $option): ?> <li> <span <?= $block->getOptionAttributesHtml($key, $option) ?>> <?= $block->escapeHtml($option['label']) ?> </span> - <?php if (isset($option['hint'])) : ?> + <?php if (isset($option['hint'])): ?> <div class="tooltip" <?= /* @noEscape */ $block->getUiId('item', $key, 'tooltip') ?>> <a href="<?= $block->escapeUrl($option['hint']['href']) ?>" class="help"> <?= $block->escapeHtml($option['hint']['label']) ?> @@ -36,6 +36,7 @@ <?php endif; ?> <?php endif; ?> </div> +<?= /* @noEscape */$block->getAfterHtml() ?> <script type="text/x-magento-init"> { ".actions-split": { diff --git a/app/code/Magento/Ui/view/base/templates/logger.phtml b/app/code/Magento/Ui/view/base/templates/logger.phtml index 7466781a606f1..b93ac7542464b 100644 --- a/app/code/Magento/Ui/view/base/templates/logger.phtml +++ b/app/code/Magento/Ui/view/base/templates/logger.phtml @@ -5,11 +5,13 @@ */ /** @var $block \Magento\Ui\Block\Logger */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->isLoggingEnabled()) : ?> - <script> +<?php if ($block->isLoggingEnabled()): ?> + <?php $scriptString = <<<script + window.onerror = function(msg, url, line) { - var key = "<?= $block->escapeJs($block->getSessionStorageKey()) ?>"; + var key = "{$block->escapeJs($block->getSessionStorageKey())}"; var errors = {}; if (sessionStorage.getItem(key)) { errors = JSON.parse(sessionStorage.getItem(key)); @@ -20,5 +22,7 @@ errors[window.location.href].push("error: \'" + msg + "\' " + "file: " + url + " " + "line: " + line); sessionStorage.setItem(key, JSON.stringify(errors)); }; - </script> +script; + ?> + <?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php endif; ?> diff --git a/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml b/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml index a7c279c431665..ff8236478bbbd 100644 --- a/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml +++ b/app/code/Magento/Ui/view/base/templates/wysiwyg/active_editor.phtml @@ -5,13 +5,17 @@ */ /** @var Magento\Ui\Block\Wysiwyg\ActiveEditor $block */ -?> -<script> +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +$wysiwygAdapterPath = /* @noEscape */ $block->getWysiwygAdapterPath(); +$scriptString = <<<script require.config({ map: { '*': { - wysiwygAdapter: '<?= /* @noEscape */ $block->getWysiwygAdapterPath() ?>' + wysiwygAdapter: '{$wysiwygAdapterPath}' } } }); -</script> +script; + +echo /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); diff --git a/app/code/Magento/Ui/view/base/web/js/block-loader.js b/app/code/Magento/Ui/view/base/web/js/block-loader.js index 531591b41b0d8..e509f7dc23fea 100644 --- a/app/code/Magento/Ui/view/base/web/js/block-loader.js +++ b/app/code/Magento/Ui/view/base/web/js/block-loader.js @@ -15,14 +15,18 @@ define([ blockContentLoadingClass = '_block-content-loading', blockLoader, blockLoaderClass, - loaderImageHref; + blockLoaderElement = $.Deferred(), + loaderImageHref = $.Deferred(); templateLoader.loadTemplate(blockLoaderTemplatePath).done(function (blockLoaderTemplate) { - blockLoader = template($.trim(blockLoaderTemplate), { - loaderImageHref: loaderImageHref + loaderImageHref.done(function (loaderHref) { + blockLoader = template($.trim(blockLoaderTemplate), { + loaderImageHref: loaderHref + }); + blockLoader = $(blockLoader); + blockLoaderClass = '.' + blockLoader.attr('class'); + blockLoaderElement.resolve(); }); - blockLoader = $(blockLoader); - blockLoaderClass = '.' + blockLoader.attr('class'); }); /** @@ -70,7 +74,7 @@ define([ } return function (loaderHref) { - loaderImageHref = loaderHref; + loaderImageHref.resolve(loaderHref); ko.bindingHandlers.blockLoader = { /** * Process loader for block @@ -81,9 +85,9 @@ define([ element = $(element); if (ko.unwrap(displayBlockLoader())) { - addBlockLoader(element); + blockLoaderElement.done(addBlockLoader(element)); } else { - removeBlockLoader(element); + blockLoaderElement.done(removeBlockLoader(element)); } } }; diff --git a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js index ac1de4631e908..5240fe55f6a74 100644 --- a/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js +++ b/app/code/Magento/Ui/view/base/web/js/core/renderer/layout.js @@ -499,7 +499,7 @@ define([ component = registry.get(val.path); if (component) { - component.cleanData().destroy(); + component.destroy(); } }); diff --git a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js index b488a4b2f8c16..9a34e57df86c7 100644 --- a/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js +++ b/app/code/Magento/Ui/view/base/web/js/form/element/ui-select.js @@ -291,6 +291,13 @@ define([ return false; }, + /** + * Return empty options html + */ + getEmptyOptionsUnsanitizedHtml: function () { + return this.emptyOptionsHtml; + }, + /** * Check options length and set to cache * if some options is added @@ -748,11 +755,6 @@ define([ return this.value() ? !!this.value().length : false; }, - /** - * @deprecated - */ - onMousemove: function () {}, - /** * Handles hover on list items. * @@ -1167,7 +1169,7 @@ define([ return; } - if (searchKey !== this.lastSearchKey) { + if (currentPage === 1) { this.options([]); } this.processRequest(searchKey, currentPage); diff --git a/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js b/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js index e8e1cf3246c76..611a14ce778de 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/columns/image.js @@ -11,6 +11,7 @@ define([ defaults: { bodyTmpl: 'ui/grid/columns/image', modules: { + masonry: '${ $.parentName }', previewComponent: '${ $.parentName }.preview' }, previewRowId: null, @@ -35,6 +36,15 @@ define([ return this; }, + /** + * Updates styles when image loaded. + * + * @param {Object} record + */ + updateStyles: function (record) { + !record.lastInRow || this.masonry().updateStyles(); + }, + /** * Returns url to given record. * diff --git a/app/code/Magento/Ui/view/base/web/js/grid/masonry.js b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js index e4c72ee950c26..ac17c7fb565e1 100644 --- a/app/code/Magento/Ui/view/base/web/js/grid/masonry.js +++ b/app/code/Magento/Ui/view/base/web/js/grid/masonry.js @@ -112,13 +112,20 @@ define([ */ setEventListener: function () { window.addEventListener('resize', function () { - raf(function () { - this.containerWidth = window.innerWidth; - this.setLayoutStyles(); - }.bind(this), this.refreshFPS); + this.updateStyles(); }.bind(this)); }, + /** + * Updates styles for component. + */ + updateStyles: function () { + raf(function () { + this.containerWidth = window.innerWidth; + this.setLayoutStyles(); + }.bind(this), this.refreshFPS); + }, + /** * Set layout styles inside the container */ diff --git a/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js new file mode 100644 index 0000000000000..1f870e9e819a1 --- /dev/null +++ b/app/code/Magento/Ui/view/base/web/js/grid/url-filter-applier.js @@ -0,0 +1,85 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'uiComponent', + 'underscore' +], function (Component, _) { + 'use strict'; + + return Component.extend({ + defaults: { + listingNamespace: null, + filterProvider: 'componentType = filters, ns = ${ $.listingNamespace }', + filterKey: 'filters', + searchString: location.search, + modules: { + filterComponent: '${ $.filterProvider }' + } + }, + + /** + * Init component + * + * @return {exports} + */ + initialize: function () { + this._super(); + this.apply(); + + return this; + }, + + /** + * Apply filter + */ + apply: function () { + var urlFilter = this.getFilterParam(this.searchString); + + if (_.isUndefined(this.filterComponent())) { + setTimeout(function () { + this.apply(); + }.bind(this), 100); + + return; + } + + if (Object.keys(urlFilter).length) { + this.filterComponent().setData(urlFilter, false); + this.filterComponent().apply(); + } + }, + + /** + * Get filter param from url + * + * @returns {Object} + */ + getFilterParam: function (url) { + var searchString = decodeURI(url), + itemArray; + + return _.chain(searchString.slice(1).split('&')) + .map(function (item) { + + if (item && item.search(this.filterKey) !== -1) { + itemArray = item.split('='); + + if (itemArray[1].search('\\[') === 0) { + itemArray[1] = itemArray[1].replace(/[\[\]]/g, '').split(','); + } + + itemArray[0] = itemArray[0].replace(this.filterKey, '') + .replace(/[\[\]]/g, ''); + + return itemArray; + } + }.bind(this)) + .compact() + .object() + .value(); + } + }); +}); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js index 2fab8c219c02a..284d395d8120b 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/datepicker.js @@ -7,11 +7,8 @@ define([ 'ko', 'underscore', 'jquery', - 'mage/translate', - 'mage/calendar', - 'moment', - 'mageUtils' -], function (ko, _, $, $t, calendar, moment, utils) { + 'mage/translate' +], function (ko, _, $, $t) { 'use strict'; var defaults = { @@ -46,10 +43,12 @@ define([ observable = config; } - $(el).calendar(options); + require(['mage/calendar'], function () { + $(el).calendar(options); - ko.utils.registerEventHandler(el, 'change', function () { - observable(this.value); + ko.utils.registerEventHandler(el, 'change', function () { + observable(this.value); + }); }); }, @@ -62,6 +61,7 @@ define([ */ update: function (element, valueAccessor) { var config = valueAccessor(), + $element = $(element), observable, options = {}, newVal; @@ -75,26 +75,21 @@ define([ observable = config; } - if (_.isEmpty(observable())) { - if ($(element).datepicker('getDate')) { - $(element).datepicker('setDate', null); - $(element).blur(); + require(['moment', 'mage/utils/misc', 'mage/calendar'], function (moment, utils) { + if (_.isEmpty(observable())) { + newVal = null; + } else { + newVal = moment( + observable(), + utils.convertToMomentFormat( + options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') + ) + ).toDate(); } - } else { - newVal = moment( - observable(), - utils.convertToMomentFormat( - options.dateFormat + (options.showsTime ? ' ' + options.timeFormat : '') - ) - ).toDate(); - if ($(element).datepicker('getDate') == null || - newVal.valueOf() !== $(element).datepicker('getDate').valueOf() - ) { - $(element).datepicker('setDate', newVal); - $(element).blur(); - } - } + $element.datepicker('setDate', newVal); + $element.blur(); + }); } }; }); diff --git a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js index 1dda3254f4613..52031dc0c3792 100644 --- a/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js +++ b/app/code/Magento/Ui/view/base/web/js/lib/knockout/bindings/range.js @@ -7,13 +7,18 @@ define([ 'ko', 'jquery', 'underscore', - '../template/renderer', - 'jquery-ui-modules/slider' + '../template/renderer' ], function (ko, $, _, renderer) { 'use strict'; var isTouchDevice = !_.isUndefined(document.ontouchstart), - sliderFn = 'slider'; + sliderFn = 'slider', + sliderModule = 'jquery-ui-modules/slider'; + + if (isTouchDevice) { + sliderFn = 'touchSlider'; + sliderModule = 'mage/touch-slider'; + } ko.bindingHandlers.range = { @@ -41,7 +46,9 @@ define([ } }); - $(element)[sliderFn](config); + require([sliderModule], function () { + $(element)[sliderFn](config); + }); }, /** @@ -55,149 +62,11 @@ define([ config.value = ko.unwrap(config.value); - $(element)[sliderFn]('option', config); + require([sliderModule], function () { + $(element)[sliderFn]('option', config); + }); } }; renderer.addAttribute('range'); - - if (!isTouchDevice) { - return; - } - - $.widget('mage.touchSlider', $.ui.slider, { - - /** - * Creates instance of widget. - * - * @override - */ - _create: function () { - _.bindAll( - this, - '_mouseDown', - '_mouseMove', - '_onTouchEnd' - ); - - return this._superApply(arguments); - }, - - /** - * Initializes mouse events on element. - * @override - */ - _mouseInit: function () { - var result = this._superApply(arguments); - - this.element - .off('mousedown.' + this.widgetName) - .on('touchstart.' + this.widgetName, this._mouseDown); - - return result; - }, - - /** - * Elements' 'mousedown' event handler polyfill. - * @override - */ - _mouseDown: function (event) { - var prevDelegate = this._mouseMoveDelegate, - result; - - event = this._touchToMouse(event); - result = this._super(event); - - if (prevDelegate === this._mouseMoveDelegate) { - return result; - } - - $(document) - .off('mousemove.' + this.widgetName) - .off('mouseup.' + this.widgetName); - - $(document) - .on('touchmove.' + this.widgetName, this._mouseMove) - .on('touchend.' + this.widgetName, this._onTouchEnd) - .on('tochleave.' + this.widgetName, this._onTouchEnd); - - return result; - }, - - /** - * Documents' 'mousemove' event handler polyfill. - * - * @override - * @param {Event} event - Touch event object. - */ - _mouseMove: function (event) { - event = this._touchToMouse(event); - - return this._super(event); - }, - - /** - * Documents' 'touchend' event handler. - */ - _onTouchEnd: function (event) { - $(document).trigger('mouseup'); - - return this._mouseUp(event); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseUp: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes previously assigned touch handlers. - * - * @override - */ - _mouseDestroy: function () { - this._removeTouchHandlers(); - - return this._superApply(arguments); - }, - - /** - * Removes touch events from document object. - */ - _removeTouchHandlers: function () { - $(document) - .off('touchmove.' + this.widgetName) - .off('touchend.' + this.widgetName) - .off('touchleave.' + this.widgetName); - }, - - /** - * Adds properties to the touch event to mimic mouse event. - * - * @param {Event} event - Touch event object. - * @returns {Event} - */ - _touchToMouse: function (event) { - var orig = event.originalEvent, - touch = orig.touches[0]; - - return _.extend(event, { - which: 1, - pageX: touch.pageX, - pageY: touch.pageY, - clientX: touch.clientX, - clientY: touch.clientY, - screenX: touch.screenX, - screenY: touch.screenY - }); - } - }); - - sliderFn = 'touchSlider'; }); diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html index fa0074ad72283..e9834ac449cce 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/columns/image.html @@ -5,5 +5,5 @@ */ --> <div class="masonry-image-block" ko-style="$col.getStyles($row())" css="{'active': $col.getIsActive($row())}" attr="'data-id': $col.getId($row())"> - <img attr="src: $col.getUrl($row())" css="$col.getClasses($row())" click="function(){ expandPreview($row()) }" data-role="thumbnail"/> + <img data-bind="event: { load: updateStyles($row()) }" attr="src: $col.getUrl($row())" css="$col.getClasses($row())" click="function(){ expandPreview($row()) }" data-role="thumbnail"/> </div> diff --git a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html index b9425c020c0e9..3e5108b53ccd5 100644 --- a/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html +++ b/app/code/Magento/Ui/view/base/web/templates/grid/filters/elements/ui-select.html @@ -141,13 +141,12 @@ </div> <div ifnot="options().length" class="admin__action-multiselect-empty-area"> - <ul data-bind="html: emptyOptionsHtml"/> + <ul data-bind="html: getEmptyOptionsUnsanitizedHtml"/> </div> <!-- /ko --> <ul class="admin__action-multiselect-menu-inner _root" data-bind=" event: { - mousemove: function(data, event){onMousemove($data, $index(), event)}, scroll: function(data, event){onScrollDown(data, event)} } "> diff --git a/app/code/Magento/Ups/Block/Backend/System/CarrierConfig.php b/app/code/Magento/Ups/Block/Backend/System/CarrierConfig.php index 15e782efd0757..77e8c2a4b10e2 100644 --- a/app/code/Magento/Ups/Block/Backend/System/CarrierConfig.php +++ b/app/code/Magento/Ups/Block/Backend/System/CarrierConfig.php @@ -7,8 +7,10 @@ use Magento\Backend\Block\Template; use Magento\Backend\Block\Template\Context as TemplateContext; +use Magento\Framework\App\ObjectManager; use Magento\Store\Model\Website; use Magento\Ups\Helper\Config as ConfigHelper; +use Magento\Framework\Json\Helper\Data as JsonHelper; /** * Backend shipping UPS content block @@ -35,15 +37,18 @@ class CarrierConfig extends Template * @param \Magento\Ups\Helper\Config $carrierConfig * @param \Magento\Store\Model\Website $websiteModel * @param array $data + * @param JsonHelper|null $jsonHelper */ public function __construct( TemplateContext $context, ConfigHelper $carrierConfig, Website $websiteModel, - array $data = [] + array $data = [], + ?JsonHelper $jsonHelper = null ) { $this->carrierConfig = $carrierConfig; $this->_websiteModel = $websiteModel; + $data['jsonHelper'] = $jsonHelper ?? ObjectManager::getInstance()->get(JsonHelper::class); parent::__construct($context, $data); } diff --git a/app/code/Magento/Ups/Model/Carrier.php b/app/code/Magento/Ups/Model/Carrier.php index 57ad12a972dab..b6e539bdadcb9 100644 --- a/app/code/Magento/Ups/Model/Carrier.php +++ b/app/code/Magento/Ups/Model/Carrier.php @@ -1594,7 +1594,7 @@ private function generateShipmentDescription(array $items): string * * @param Element $shipmentConfirmResponse * @return DataObject - * @deprecated New asynchronous methods introduced. + * @deprecated 100.3.3 New asynchronous methods introduced. * @see requestToShipment */ protected function _sendShipmentAcceptRequest(Element $shipmentConfirmResponse) @@ -1789,7 +1789,7 @@ private function requestShipments(array $quoteIds): array * * @param DataObject $request * @return DataObject - * @deprecated New asynchronous methods introduced. + * @deprecated 100.3.3 New asynchronous methods introduced. * @see requestToShipment */ protected function _doShipmentRequest(DataObject $request) 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 c4298cd8dc046..f068b0cf0079f 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,10 +4,10 @@ * 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 */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + $upsCarrierConfig = $block->getCarrierConfig(); $orShipArr = $upsCarrierConfig->getCode('originShipment'); $defShipArr = $upsCarrierConfig->getCode('method'); @@ -15,6 +15,8 @@ $defShipArr = $upsCarrierConfig->getCode('method'); $sectionCode = $block->getRequest()->getParam('section'); $websiteCode = $block->getRequest()->getParam('website'); $storeCode = $block->getRequest()->getParam('store'); +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); if (!$storeCode && $websiteCode) { /** @var $web \Magento\Store\Model\Website */ @@ -35,7 +37,8 @@ if (!$storeCode && $websiteCode) { $storedUpsType = $block->escapeHtml($block->getConfig('carriers/ups/type')); } ?> -<script> + +<?php $scriptString = <<<script require(["prototype"], function(){ //<![CDATA[ @@ -85,14 +88,17 @@ require(["prototype"], function(){ 'carriers_ups_mode_xml','carriers_ups_include_taxes']; this.onlyUpsElements = ['carriers_ups_gateway_url']; - this.storedOriginShipment = '<?= /* @noEscape */ $storedOriginShipment ?>'; - this.storedFreeShipment = '<?= /* @noEscape */ $storedFreeShipment ?>'; - this.storedUpsType = '<?= /* @noEscape */ $storedUpsType ?>'; - <?php /** @var $jsonHelper \Magento\Framework\Json\Helper\Data */ $jsonHelper = $this->helper(\Magento\Framework\Json\Helper\Data::class); ?> - this.storedAllowedMethods = <?= /* @noEscape */ $jsonHelper->jsonEncode($storedAllowedMethods) ?>; - this.originShipmentObj = <?= /* @noEscape */ $jsonHelper->jsonEncode($orShipArr) ?>; - this.originShipmentObj['default'] = <?= /* @noEscape */ $jsonHelper->jsonEncode($defShipArr) ?>; +script; +$scriptString .= 'this.storedOriginShipment = \'' . /* @noEscape */ $storedOriginShipment . '\'; + this.storedFreeShipment = \'' . /* @noEscape */ $storedFreeShipment . '\'; + this.storedUpsType = \'' . /* @noEscape */ $storedUpsType . '\';'; +?> +<?php $scriptString .= 'this.storedAllowedMethods = ' . /* @noEscape */ $jsonHelper->jsonEncode($storedAllowedMethods) . + '; + this.originShipmentObj = ' . /* @noEscape */ $jsonHelper->jsonEncode($orShipArr) . '; + this.originShipmentObj[\'default\'] = ' . /* @noEscape */ $jsonHelper->jsonEncode($defShipArr) . ';'; +$scriptString .= <<<script this.setFormValues(); Event.observe($(this.carriersUpsTypeId), 'change', this.setFormValues.bind(this)); Event.observe($(this.carriersUpsActiveId), 'change', this.setFormValues.bind(this)); @@ -110,8 +116,13 @@ require(["prototype"], function(){ while (freeMethod.length > 0) { freeMethod.remove(0); } - freeMethod.insert(new Element('option', {value:''}).update('<?= $block->escapeHtml(__('None')) ?>')); +script; + +$scriptString .= 'freeMethod.insert(new Element(\'option\', {value:\'\'}).update(\'' . $block->escapeHtml(__('None')) . + '\'));'; + +$scriptString .= <<<script var code, option; for (code in originShipment) { option = new Element('option', {value:code}).update(originShipment[code]); @@ -145,7 +156,7 @@ require(["prototype"], function(){ setFormValues: function() { var a; - if ($F(this.carriersUpsTypeId) == 'UPS') { + if (\$F(this.carriersUpsTypeId) == 'UPS') { for (a = 0; a < this.checkingUpsXmlId.length; a++) { $(this.checkingUpsXmlId[a]).removeClassName('required-entry'); } @@ -173,13 +184,13 @@ require(["prototype"], function(){ }, changeOriginShipment: function(Event, key) { - this.originShipmentTitle = key ? key : $F('carriers_ups_origin_shipment'); + 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]).disabled = \$F(this.carriersUpsActiveId) !== '1' || $(fields[key] + '_inherit') !== null - && $F(fields[key] + '_inherit') === '1'; + && \$F(fields[key] + '_inherit') === '1'; if ($(fields[key]).next() !== undefined) { $(fields[key]).removeClassName('mage-error').next().remove(); @@ -191,4 +202,6 @@ require(["prototype"], function(){ //]]> }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false); ?> diff --git a/app/code/Magento/UrlRewrite/Block/GridContainer.php b/app/code/Magento/UrlRewrite/Block/GridContainer.php index 1a3a6e89fa854..066b76ed610e0 100644 --- a/app/code/Magento/UrlRewrite/Block/GridContainer.php +++ b/app/code/Magento/UrlRewrite/Block/GridContainer.php @@ -10,7 +10,7 @@ * Url rewrite grid container class * * @api - * @deprecated Moved to UI component implementation + * @deprecated 102.0.0 Moved to UI component implementation * @since 100.0.2 */ class GridContainer extends \Magento\Backend\Block\Widget\Grid\Container diff --git a/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php b/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php index 906ba1f625477..12620edf460d2 100644 --- a/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php +++ b/app/code/Magento/UrlRewrite/Model/Exception/UrlAlreadyExistsException.php @@ -11,7 +11,7 @@ * Exception for already created url. * * @api - * @since 100.2.0 + * @since 101.0.0 */ class UrlAlreadyExistsException extends \Magento\Framework\Exception\AlreadyExistsException { @@ -39,7 +39,7 @@ public function __construct(Phrase $phrase = null, \Exception $cause = null, $co * Get URLs * * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function getUrls() { diff --git a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php index f187408d45a9d..9a62ed211d2b1 100644 --- a/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php +++ b/app/code/Magento/UrlRewrite/Model/Storage/DbStorage.php @@ -353,7 +353,7 @@ protected function insertMultiple($data): void * * @param UrlRewrite[] $urls * @return array - * @deprecated Not used anymore. + * @deprecated 101.0.3 Not used anymore. */ protected function createFilterDataBasedOnUrls($urls): array { diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..a409860811837 --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathInUrlRewriteGrigActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminTargetPathInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the target path is shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="targetPath" type="string"/> + </arguments> + + <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', targetPath)}}" + stepKey="seeValueInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml new file mode 100644 index 0000000000000..739675ba264ea --- /dev/null +++ b/app/code/Magento/UrlRewrite/Test/Mftf/ActionGroup/AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup"> + <annotations> + <description>Assert the target path is not shown in the URL Rewrite grid.</description> + </annotations> + <arguments> + <argument name="targetPath" type="string"/> + </arguments> + + <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', targetPath)}}" + stepKey="valueIsNotShownInGrid"/> + </actionGroup> +</actionGroups> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml index 4e46ed8e4fc79..3b140aed5f572 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminCheckUrlRewritesMultipleStoreviewsProductImportTest.xml @@ -47,84 +47,103 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearFiltersIfSet"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> + <!--Change category name and URL key for EN Store View--> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewEn"> <argument name="Store" value="customStoreENNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForENStoreView"> + <argument name="categoryName" value="categoryEN"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyENStoreView"> <argument name="value" value="category-english"/> </actionGroup> + + <!--Change category name and URL key for NL Store View--> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewNl"> <argument name="Store" value="customStoreNLNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForNLStoreView"> + <argument name="categoryName" value="categoryNL"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyNLStoreView"> <argument name="value" value="category-dutch"/> </actionGroup> - <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> - <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> - <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> - <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> - <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> - <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> - <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> - <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> - <argument name="productName" value="productformagetwo68980"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_updated.csv"/> + <argument name="importNoticeMessage" value="Created: 1, Updated: 0, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="productformagetwo68980"/> + </actionGroup> <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + <!--Open Marketing - SEO & Search - URL Rewrites--> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters2"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlRewriteForENStoreView"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters3"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters3"/> - <waitForPageLoad stepKey="waitForPageToLoad3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlRewriteForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters4"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english/productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters4"/> - <waitForPageLoad stepKey="waitForPageToLoad4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters5"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch/productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView3"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters5"/> - <waitForPageLoad stepKey="waitForPageToLoad5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml index 1d604ef7648dc..6f7bb6ccb2b84 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesCorrectlyGeneratedForMultipleStoreviewsDuringProductImportTest/AdminUrlRewriteMultipleStoreviewsProductImportWithConfigTurnedOffTest.xml @@ -22,7 +22,9 @@ <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="ApiCategory" stepKey="createCategory"> <field key="name">category-admin</field> </createData> @@ -39,7 +41,9 @@ <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> @@ -58,15 +62,17 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <actionGroup ref="SwitchCategoryStoreViewActionGroup" stepKey="switchToStoreViewEn"> <argument name="Store" value="customStoreENNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValueENStoreView"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-english" stepKey="changeNameField"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForENStoreView"> + <argument name="categoryName" value="categoryenglish"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyENStoreView"> <argument name="value" value="category-english"/> </actionGroup> @@ -74,82 +80,114 @@ <argument name="Store" value="customStoreNLNotUnique.name"/> <argument name="CatName" value="$$createCategory.name$$"/> </actionGroup> - <uncheckOption selector="{{AdminCategoryBasicFieldSection.categoryNameUseDefault}}" stepKey="uncheckUseDefaultValue1"/> - <fillField selector="{{AdminCategoryBasicFieldSection.CategoryNameInput}}" userInput="category-dutch" stepKey="changeNameFieldNLStoreView"/> - <click selector="{{AdminCategorySEOSection.SectionHeader}}" stepKey="clickOnSectionHeader2"/> + <actionGroup ref="AdminChangeCategoryNameOnStoreViewLevelActionGroup" stepKey="changeCategoryNameForNLStoreView"> + <argument name="categoryName" value="categorydutch"/> + </actionGroup> <actionGroup ref="ChangeSeoUrlKeyForSubCategoryActionGroup" stepKey="changeSeoUrlKeyNLStoreView"> <argument name="value" value="category-dutch"/> </actionGroup> - <amOnPage url="{{AdminImportIndexPage.url}}" stepKey="navigateToSystemImport"/> - <selectOption selector="{{AdminImportMainSection.entityType}}" userInput="Products" stepKey="selectProductsOption"/> - <waitForElementVisible selector="{{AdminImportMainSection.importBehavior}}" stepKey="waitForImportBehaviorElementVisible"/> - <selectOption selector="{{AdminImportMainSection.importBehavior}}" userInput="Add/Update" stepKey="selectAddUpdateOption"/> - <attachFile selector="{{AdminImportMainSection.selectFileToImport}}" userInput="import_updated.csv" stepKey="attachFileForImport"/> - <click selector="{{AdminImportHeaderSection.checkDataButton}}" stepKey="clickCheckDataButton"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0" stepKey="assertNotice"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="File is valid! To start import process press "Import" button" stepKey="assertSuccessMessage"/> - <click selector="{{AdminImportMainSection.importButton}}" stepKey="clickImportButton"/> - <see selector="{{AdminImportValidationMessagesSection.success}}" userInput="Import successfully done" stepKey="assertSuccessMessage1"/> - <see selector="{{AdminImportValidationMessagesSection.notice}}" userInput="Created: 1, Updated: 0, Deleted: 0" stepKey="assertNotice1"/> - <actionGroup ref="SearchForProductOnBackendByNameActionGroup" stepKey="searchForProductOnBackend"> - <argument name="productName" value="productformagetwo68980"/> - </actionGroup> - <click selector="{{AdminProductGridSection.productRowBySku('productformagetwo68980')}}" stepKey="clickOnProductRow"/> + + <!-- Import products with add/update behavior --> + <actionGroup ref="AdminImportProductsWithCheckValidationResultActionGroup" stepKey="importProduct"> + <argument name="behavior" value="Add/Update"/> + <argument name="importFile" value="import_updated.csv"/> + <argument name="importNoticeMessage" value="Created: 1, Updated: 0, Deleted: 0"/> + <argument name="validationNoticeMessage" value="Checked rows: 3, checked entities: 1, invalid rows: 0, total errors: 0"/> + </actionGroup> + + <!--Filter Product in product page and get the Product ID --> + <actionGroup ref="FilterAndSelectProductActionGroup" stepKey="filterProduct"> + <argument name="productSku" value="productformagetwo68980"/> + </actionGroup> <grabFromCurrentUrl regex="~/id/(\d+)/~" stepKey="grabProductIdFromUrl"/> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="goToUrlRewritesIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-english.html" stepKey="inputCategoryUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english.html')}}" stepKey="seeUrlInRequestPathColumn"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn"/> + <!-- Open Marketing - SEO & Search - URL Rewrites --> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForENStoreView"> + <argument name="requestPath" value="category-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForENStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="category-dutch.html" stepKey="inputCategoryUrlForNLStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch.html')}}" stepKey="seeUrlInRequestPathColumn1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/category/view/id/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn1"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingCategoryUrlRewriteForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInRequestPathColumnForNLStoreView"> + <argument name="requestPath" value="category-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeUrlInTargetPathColumnForNLStoreView"> + <argument name="targetPath" value="catalog/category/view/id/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters2"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-english.html" stepKey="inputProductUrlForENStoreView"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters2"/> - <waitForPageLoad stepKey="waitForPageToLoad2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn2"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn2"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-english/productformagetwo68980-english.html')}}" stepKey="seeUrlInRequestPathColumn4"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn4"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrl"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeRequestPathForProduct"> + <argument name="requestPath" value="productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeTargetPathForProduct"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeRequestPathForENStoreView"> + <argument name="requestPath" value="category-english/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeTargetPathForENStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters3"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="productformagetwo68980-dutch.html" stepKey="inputProductUrlForENStoreView1"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters3"/> - <waitForPageLoad stepKey="waitForPageToLoad3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn3"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', 'catalog/product/view/id/$grabProductIdFromUrl')}}" stepKey="seeUrlInTargetPathColumn3"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', 'category-dutch/productformagetwo68980-dutch.html')}}" stepKey="seeUrlInRequestPathColumn5"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Target Path', catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$)}}" stepKey="seeUrlInTargetPathColumn5"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingProductUrlForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeRequestPathForProductForNLStoreView"> + <argument name="requestPath" value="productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathInUrlRewriteGrigActionGroup" stepKey="seeTargetPathForProductForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeRequestPathForNLStoreView"> + <argument name="requestPath" value="category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertAdminTargetPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="doNotSeeTargetPathForNLStoreView"> + <argument name="targetPath" value="catalog/product/view/id/$grabProductIdFromUrl/category/$$createCategory.id$$"/> + </actionGroup> <!-- Switch StoreView --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="amOnStoreFrontHomePage"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView"> <argument name="storeView" value="customStoreENNotUnique"/> </actionGroup> - <amOnPage url="/productformagetwo68980-english.html" stepKey="navigateToProductPage"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-english" stepKey="seeProductName"/> - <amOnPage url="/category-english/productformagetwo68980-english.html" stepKey="navigateToProductPage2"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-english" stepKey="seeProductName2"/> + <!-- Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPage"> + <argument name="productName" value="productformagetwo68980-english"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/productformagetwo68980-english.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageSecondAttempt"> + <argument name="productName" value="productformagetwo68980-english"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/category-english/productformagetwo68980-english.html"/> + </actionGroup> <!-- Switch StoreView --> - <amOnPage url="{{StorefrontHomePage.url}}" stepKey="amOnProduct4Page2"/> + <actionGroup ref="StorefrontOpenHomePageActionGroup" stepKey="backToHomePage"/> <actionGroup ref="StorefrontSwitchStoreViewActionGroup" stepKey="switchToCustomStoreView2"> <argument name="storeView" value="customStoreNLNotUnique"/> </actionGroup> - <amOnPage url="/productformagetwo68980-dutch.html" stepKey="navigateToProductPage3"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-dutch" stepKey="seeProductName3"/> - <amOnPage url="/category-dutch/productformagetwo68980-dutch.html" stepKey="navigateToProductPage4"/> - <see selector="{{StorefrontProductInfoMainSection.productName}}" userInput="productformagetwo68980-dutch" stepKey="seeProductName4"/> + <!-- Assert Redirects work and Product is present on StoreFront--> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageThirdAttempt"> + <argument name="productName" value="productformagetwo68980-dutch"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/productformagetwo68980-dutch.html"/> + </actionGroup> + <actionGroup ref="AssertStorefrontProductRedirectActionGroup" stepKey="verifyProductInStoreFrontPageFourthAttempt"> + <argument name="productName" value="productformagetwo68980-dutch"/> + <argument name="productSku" value="productformagetwo68980"/> + <argument name="productRequestPath" value="/category-dutch/productformagetwo68980-dutch.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml index 20e6392091998..fee13adcb433c 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCheckUrlRewritesInCatalogCategoriesAfterChangingUrlKeyForStoreViewAndMovingCategory2Test.xml @@ -35,7 +35,9 @@ <!--Create additional Store View in Main Website Store --> <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createStoreView"/> - <magentoCLI command="indexer:reindex" stepKey="reindexAll"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindexAll"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml index d890cde5ecf9d..c14d0b175d2c0 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminCreateProductWithSeveralWebsitesAndCheckURLRewritesTest.xml @@ -64,6 +64,7 @@ <!-- Create simple product with categories created in create data --> <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="openProductsGrid"/> + <actionGroup ref="FilterProductGridBySkuActionGroup" stepKey="filterProduct"> <argument name="product" value="$$createProduct$$"/> </actionGroup> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml index 9d6b267055f70..10b377eebd313 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminGenerateUrlRewritesForProductInCategoriesSwitchOffTest.xml @@ -22,7 +22,9 @@ <comment userInput="Enable config to generate category/product URL Rewrites" stepKey="commentEnableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="login"/> <createData entity="SimpleSubCategory" stepKey="createCategory"/> @@ -38,40 +40,41 @@ <comment userInput="Enable config to generate category/product URL Rewrites" stepKey="commentEnableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> </after> <!-- 1. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters"/> - <waitForPageLoad stepKey="waitForPageToLoad"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewrite"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGrid"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeCategoryUrlInGrid"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> <!-- 2. Set the configuration for Generate "category/product" URL Rewrites to No--> - <amOnPage url="{{CatalogConfigPage.url}}" stepKey="amOnCatalogConfigPage"/> - <conditionalClick selector="{{CatalogSection.seo}}" dependentSelector="{{CatalogSection.CheckIfSeoTabExpand}}" visible="true" stepKey="expandSeoTab" /> - <waitForElementVisible selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="GenerateUrlRewritesSelect"/> - <selectOption userInput="0" selector="{{CatalogSection.GenerateUrlRewrites}}" stepKey="selectUrlGenerationNo" /> - <waitForElementVisible selector="{{GenerateUrlRewritesConfirm.title}}" stepKey="waitForConfirmModal"/> - <click selector="{{GenerateUrlRewritesConfirm.ok}}" stepKey="confirmSwitchingGenerationOff"/> - <click selector="{{CatalogSection.save}}" stepKey="saveConfig" /> - <waitForPageLoad stepKey="waitForSavingSystemConfiguration"/> + <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!-- 3. Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- 4. Open Marketing - SEO & Search - URL Rewrites --> - <amOnPage url="{{AdminUrlRewriteIndexPage.url}}" stepKey="amOnUrlRewriteIndexPage2"/> - <click selector="{{AdminDataGridHeaderSection.filters}}" stepKey="openUrlRewriteGridFilters1"/> - <fillField selector="{{AdminDataGridHeaderSection.filterFieldInput('request_path')}}" userInput="$createSimpleProduct.custom_attributes[url_key]$.html" stepKey="inputProductName2"/> - <click selector="{{AdminDataGridHeaderSection.applyFilters}}" stepKey="clickOrderApplyFilters1"/> - <waitForPageLoad stepKey="waitForPageToLoad1"/> - <seeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="seeInListValue1"/> - <dontSeeElement selector="{{AdminUrlRewriteIndexSection.gridCellByColumnValue('Request Path', $createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html)}}" stepKey="dontSeeValue2"/> + <actionGroup ref="AdminSearchUrlRewriteByRequestPathActionGroup" stepKey="searchingUrlRewriteAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathInUrlRewriteGrigActionGroup" stepKey="seeProductUrlInGridAfterDisablingTheConfig"> + <argument name="requestPath" value="$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> + <actionGroup ref="AssertAdminRequestPathIsNotFoundInUrlRewriteGrigActionGroup" stepKey="categoryUrlIsNotShownAfterDisablingTheConfig"> + <argument name="requestPath" value="$createCategory.custom_attributes[url_key]$/$createSimpleProduct.custom_attributes[url_key]$.html"/> + </actionGroup> </test> </tests> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml index f03d9ae1bad67..d102286bd9e6d 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminProductCreateUrlRewriteForCustomStoreViewTest.xml @@ -31,7 +31,9 @@ <actionGroup ref="AdminCreateStoreViewActionGroup" stepKey="createCustomStoreView"> <argument name="customStore" value="customStore"/> </actionGroup> - <magentoCLI command="indexer:reindex" stepKey="runReindex"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="runReindex"> + <argument name="indices" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createProduct" stepKey="deleteProduct" /> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml index a70065dc1d307..2dd7df9cbb548 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductAfterImportTest.xml @@ -43,7 +43,7 @@ <deleteData createDataKey="simpleSubCategory1" stepKey="deleteSimpleSubCategory1"/> <comment userInput="Disable config to generate category/product URL Rewrites " stepKey="commentDisableConfig" /> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="disableGenerateUrlRewrite"/> - <amOnPage url="{{AdminLogoutPage.url}}" stepKey="amOnLogoutPage"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="amOnLogoutPage"/> </after> <comment userInput="1. Log in to Admin " stepKey="commentAdminLogin" /> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml index 885e09f775c36..78bd397c69289 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductInAnchorCategoriesTestWithConfigurationTurnedOffTest.xml @@ -25,7 +25,9 @@ <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> <createData entity="SubCategoryWithParent" stepKey="simpleSubCategory2"> @@ -42,7 +44,9 @@ <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> @@ -50,7 +54,9 @@ <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- Steps --> <!-- 1. Log in to Admin --> diff --git a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml index 99be6028a3908..bfe8a28064496 100644 --- a/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml +++ b/app/code/Magento/UrlRewrite/Test/Mftf/Test/AdminUrlRewritesForProductInAnchorCategoriesTest/AdminUrlRewritesForProductsWithConfigurationTurnedOffTest.xml @@ -22,7 +22,9 @@ <comment userInput="Enable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentEnableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="enableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache1"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterEnableConfig"> + <argument name="tags" value=""/> + </actionGroup> <createData entity="SimpleSubCategory" stepKey="simpleSubCategory1"/> <!-- Create Simple product 1 and assign it to Category 1 --> <createData entity="ApiSimpleProduct" stepKey="createSimpleProduct"> @@ -32,14 +34,18 @@ <comment userInput="Disable SEO configuration setting to generate category/product URL Rewrites" stepKey="commentDisableUrlRewriteConfig"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 0" stepKey="disableGenerateUrlRewrite"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCacheAfterDisableConfig"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="simpleSubCategory1" stepKey="deletesimpleSubCategory1"/> <magentoCLI command="config:set catalog/seo/generate_category_product_rewrites 1" stepKey="resetConfigurationSetting"/> <!--Flush cache--> - <magentoCLI command="cache:flush" stepKey="cleanCache2"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </after> <!-- 1. Log in to Admin --> diff --git a/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml b/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml index 84abf64af9757..837c528d6cfda 100644 --- a/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml +++ b/app/code/Magento/UrlRewrite/view/adminhtml/templates/selector.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var \Magento\UrlRewrite\Block\Selector $block */ +/** + * @var \Magento\UrlRewrite\Block\Selector $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <div class="form-inline"> <fieldset class="admin__fieldset fieldset" data-container-for="entity-type-selector"> @@ -13,11 +16,18 @@ <span><?= $block->escapeHtml($block->getSelectorLabel()) ?></span> </label> <div class="admin__field-control control"> - <select data-role="entity-type-selector" class="admin__control-select select" onchange="window.location = this.value;" id="entity-type-selector"> - <?php foreach ($block->getModes() as $mode => $label) : ?> - <option <?= /* @noEscape */ $block->isMode($mode) ? 'selected="selected" ' : '' ?>value="<?= $block->escapeUrl($block->getModeUrl($mode)) ?>"><?= $block->escapeHtml($label) ?></option> + <select data-role="entity-type-selector" class="admin__control-select select" id="entity-type-selector"> + <?php foreach ($block->getModes() as $mode => $label): ?> + <option <?= /* @noEscape */ $block->isMode($mode) ? 'selected="selected" ' : '' ?> + value="<?= $block->escapeUrl($block->getModeUrl($mode)) ?>"><?= $block->escapeHtml($label) ?> + </option> <?php endforeach; ?> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + 'window.location = this.value;', + 'select#entity-type-selector' + ) ?> </div> </div> </fieldset> diff --git a/app/code/Magento/User/Block/User/Edit.php b/app/code/Magento/User/Block/User/Edit.php index 6e036cf20fa25..233fe1e0cfee5 100644 --- a/app/code/Magento/User/Block/User/Edit.php +++ b/app/code/Magento/User/Block/User/Edit.php @@ -87,7 +87,7 @@ protected function _construct() * - click "Delete User" at top left part of the page; * * @return \Magento\Framework\Phrase - * @since 100.2.0 + * @since 101.0.0 */ public function getDeleteMessage() { @@ -100,7 +100,7 @@ public function getDeleteMessage() * Magento\User\Controller\Adminhtml\User\Delete * * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getDeleteUrl() { @@ -113,7 +113,7 @@ public function getDeleteUrl() * to create a new user account OR to edit the previously created user account * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getObjectId() { diff --git a/app/code/Magento/User/Model/Notificator.php b/app/code/Magento/User/Model/Notificator.php index 3a5522db4c533..3e36cd1387e39 100644 --- a/app/code/Magento/User/Model/Notificator.php +++ b/app/code/Magento/User/Model/Notificator.php @@ -107,6 +107,7 @@ public function sendForgotPassword(UserInterface $user): void $this->sendNotification( 'admin/emails/forgot_email_template', [ + 'username' => $user->getFirstName().' '.$user->getLastName(), 'user' => $user, 'store' => $this->storeManager->getStore( Store::DEFAULT_STORE_ID diff --git a/app/code/Magento/User/Model/ResourceModel/User/Collection.php b/app/code/Magento/User/Model/ResourceModel/User/Collection.php index 7683adae84365..422afb47f0a0e 100644 --- a/app/code/Magento/User/Model/ResourceModel/User/Collection.php +++ b/app/code/Magento/User/Model/ResourceModel/User/Collection.php @@ -27,6 +27,7 @@ protected function _construct() * Collection Init Select * * @return $this + * @since 101.1.0 */ protected function _initSelect() { diff --git a/app/code/Magento/User/Model/User.php b/app/code/Magento/User/Model/User.php index 00d2aa140a991..61af14d943615 100644 --- a/app/code/Magento/User/Model/User.php +++ b/app/code/Magento/User/Model/User.php @@ -120,12 +120,12 @@ class User extends AbstractModel implements StorageInterface, UserInterface protected $_encryptor; /** - * @deprecated + * @deprecated 101.1.0 */ protected $_transportBuilder; /** - * @deprecated + * @deprecated 101.1.0 */ protected $_storeManager; @@ -145,7 +145,7 @@ class User extends AbstractModel implements StorageInterface, UserInterface private $notificator; /** - * @deprecated + * @deprecated 101.1.0 */ private $deploymentConfig; @@ -451,7 +451,7 @@ public function roleUserExists() * * @return $this * @throws NotificationExceptionInterface - * @deprecated + * @deprecated 101.1.0 * @see NotificatorInterface::sendForgotPassword() */ public function sendPasswordResetConfirmationEmail() @@ -529,7 +529,7 @@ protected function createChangesDescriptionString() * @throws NotificationExceptionInterface * @return $this * @since 100.1.0 - * @deprecated + * @deprecated 101.1.0 * @see NotificatorInterface::sendUpdated() * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml index 8abb4e9224b0a..139d6b2a0f49c 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminCreateUserActionGroup.xml @@ -29,9 +29,10 @@ <fillField selector="{{AdminEditUserSection.currentPasswordField}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}" stepKey="enterCurrentPassword"/> <scrollToTopOfPage stepKey="scrollToTopOfPage"/> <click selector="{{AdminEditUserSection.userRoleTab}}" stepKey="clickUserRole"/> + <waitForPageLoad stepKey="waitForAdminUserRoleTabLoad"/> <fillField selector="{{AdminEditUserSection.roleNameFilterTextField}}" userInput="{{role.name}}" stepKey="filterRole"/> <click selector="{{AdminEditUserSection.searchButton}}" stepKey="clickSearch"/> - <waitForLoadingMaskToDisappear stepKey="waitForLoadingMaskToDisappear1"/> + <waitForPageLoad stepKey="waitForLoadingMaskToDisappear1"/> <click selector="{{AdminEditUserSection.searchResultFirstRow}}" stepKey="selectRole"/> <click selector="{{AdminEditUserSection.saveButton}}" stepKey="clickSaveUser"/> <waitForPageLoad stepKey="waitForPageLoad2"/> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml index 2957e6953dad8..46ad2e228c6c1 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/AdminDeleteRoleActionGroup.xml @@ -10,15 +10,32 @@ xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> <actionGroup name="AdminDeleteRoleActionGroup"> <annotations> - <description>Deletes a User Role that contains the text 'Role'. PLEASE NOTE: The Action Group values are Hardcoded.</description> + <description>Deletes a User Role.</description> </annotations> + <arguments> + <argument name="role" defaultValue=""/> + </arguments> - <click stepKey="clickOnRole" selector="{{AdminDeleteRoleSection.theRole}}"/> + <click stepKey="clickResetFilterButtonBefore" selector="{{AdminRoleGridSection.resetButton}}"/> + <waitForPageLoad stepKey="waitForRolesGridFilterResetBefore" time="10"/> + <fillField stepKey="TypeRoleFilter" selector="{{AdminRoleGridSection.roleNameFilterTextField}}" userInput="{{role.name}}"/> + <waitForElementVisible stepKey="waitForFilterSearchButtonBefore" selector="{{AdminRoleGridSection.searchButton}}" time="10"/> + <click stepKey="clickFilterSearchButton" selector="{{AdminRoleGridSection.searchButton}}"/> + <waitForPageLoad stepKey="waitForUserRoleFilter" time="10"/> + <waitForElementVisible stepKey="waitForRoleInRoleGrid" selector="{{AdminDeleteRoleSection.role(role.name)}}" time="10"/> + <click stepKey="clickOnRole" selector="{{AdminDeleteRoleSection.role(role.name)}}"/> + <waitForPageLoad stepKey="waitForRolePageToLoad" time="10"/> <fillField stepKey="TypeCurrentPassword" selector="{{AdminDeleteRoleSection.current_pass}}" userInput="{{_ENV.MAGENTO_ADMIN_PASSWORD}}"/> + <waitForElementVisible stepKey="waitForDeleteRoleButton" selector="{{AdminDeleteRoleSection.delete}}" time="10"/> <click stepKey="clickToDeleteRole" selector="{{AdminDeleteRoleSection.delete}}"/> - <waitForAjaxLoad stepKey="waitForDeleteConfirmationPopup" time="5"/> + <waitForPageLoad stepKey="waitForDeleteConfirmationPopup" time="5"/> + <waitForElementVisible stepKey="waitForConfirmButton" selector="{{AdminDeleteRoleSection.confirm}}" time="10"/> <click stepKey="clickToConfirm" selector="{{AdminDeleteRoleSection.confirm}}"/> <waitForPageLoad stepKey="waitForPageLoad" time="10"/> <see stepKey="seeSuccessMessage" userInput="You deleted the role."/> + <waitForPageLoad stepKey="waitForRolesGridLoad" /> + <waitForElementVisible stepKey="waitForResetFilterButtonAfter" selector="{{AdminRoleGridSection.resetButton}}" time="10"/> + <click stepKey="clickResetFilterButtonAfter" selector="{{AdminRoleGridSection.resetButton}}"/> + <waitForPageLoad stepKey="waitForRolesGridFilterResetAfter" time="10"/> </actionGroup> -</actionGroups> \ No newline at end of file +</actionGroups> diff --git a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml similarity index 83% rename from app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml rename to app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml index 4049e60e83455..d41ed63678783 100644 --- a/app/code/Magento/User/Test/Mftf/ActionGroup/AdminUserActionGroup.xml +++ b/app/code/Magento/User/Test/Mftf/ActionGroup/LoginNewUserActionGroup.xml @@ -5,10 +5,8 @@ * 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"> - <!--Login New User--> - <actionGroup name="LoginNewUser"> +<actionGroups xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/actionGroupSchema.xsd"> + <actionGroup name="LoginNewUserActionGroup" deprecated="Use AdminLoginActionGroup instead"> <annotations> <description>Goes to the Backend Admin Login page. Fill Username and Password. Click on Sign In.</description> </annotations> diff --git a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml index a3a82f6ce38e0..6a0d0c9210396 100644 --- a/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml +++ b/app/code/Magento/User/Test/Mftf/Section/AdminDeleteRoleSection.xml @@ -11,6 +11,7 @@ <section name="AdminDeleteRoleSection"> <element name="theRole" selector="//td[contains(text(), 'Role')]" type="button"/> <element name="salesRole" selector="//td[contains(text(), 'Sales')]" type="button"/> + <element name="role" parameterized="true" selector="//td[contains(@class,'col-role_name') and contains(text(), '{{roleName}}')]" type="button"/> <element name="current_pass" type="button" selector="#current_password"/> <element name="delete" selector="//button/span[contains(text(), 'Delete Role')]" type="button"/> <element name="confirm" selector="//*[@class='action-primary action-accept']" type="button"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml index 668ae550f1b3d..ba8d6ef433e13 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateActiveUserEntityTest.xml @@ -32,7 +32,7 @@ <argument name="user" value="activeAdmin"/> <argument name="role" value="roleDefaultAdministrator"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMasterAdmin"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutMainAdmin"/> <actionGroup ref="AdminLoginActionGroup" stepKey="loginToNewAdmin"> <argument name="username" value="{{activeAdmin.username}}"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml index 23a30246bd999..c26821d5be4b2 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminCreateInactiveUserEntityTest.xml @@ -20,7 +20,7 @@ <group value="mtf_migrated"/> </annotations> - <actionGroup ref="AdminLoginActionGroup" stepKey="adminMasterLogin"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="adminMainLogin"/> <actionGroup ref="AdminCreateUserWithRoleAndIsActiveActionGroup" stepKey="createAdminUser"> <argument name="user" value="inactiveAdmin"/> <argument name="role" value="roleDefaultAdministrator"/> @@ -29,7 +29,7 @@ <actionGroup ref="AssertAdminUserIsInGridActionGroup" stepKey="assertAdminIsInGrid"> <argument name="user" value="inactiveAdmin"/> </actionGroup> - <actionGroup ref="AdminLogoutActionGroup" stepKey="adminMasterLogout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminMainLogout"/> <actionGroup ref="AdminLoginActionGroup" stepKey="adminNewLogin"> <argument name="username" value="{{inactiveAdmin.username}}"/> diff --git a/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml b/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml index 850fa04549e84..6750f21311d3a 100644 --- a/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml +++ b/app/code/Magento/User/Test/Mftf/Test/AdminLockAdminUserEntityTest.xml @@ -22,13 +22,17 @@ <before> <magentoCLI command="config:set admin/captcha/enable 0" stepKey="disableAdminCaptcha"/> <magentoCLI command="config:set admin/security/lockout_failures 2" stepKey="setDefaultMaximumLoginFailures"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches1"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches1"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLoginActionGroup" stepKey="adminLogin"/> </before> <after> <magentoCLI command="config:set admin/captcha/enable 1" stepKey="enableAdminCaptcha"/> <magentoCLI command="config:set admin/security/lockout_failures 6" stepKey="setDefaultMaximumLoginFailures"/> - <magentoCLI command="cache:clean config full_page" stepKey="cleanInvalidatedCaches"/> + <actionGroup ref="CliCacheCleanActionGroup" stepKey="cleanInvalidatedCaches"> + <argument name="tags" value="config full_page"/> + </actionGroup> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> diff --git a/app/code/Magento/User/composer.json b/app/code/Magento/User/composer.json index 8ac1677bdfe81..6ba4be749cc7c 100644 --- a/app/code/Magento/User/composer.json +++ b/app/code/Magento/User/composer.json @@ -12,7 +12,8 @@ "magento/module-email": "*", "magento/module-integration": "*", "magento/module-security": "*", - "magento/module-store": "*" + "magento/module-store": "*", + "magento/module-ui": "*" }, "type": "magento2-module", "license": [ diff --git a/app/code/Magento/User/etc/webapi_rest/di.xml b/app/code/Magento/User/etc/webapi_rest/di.xml index 7c6cccb454df7..930e505648d9c 100644 --- a/app/code/Magento/User/etc/webapi_rest/di.xml +++ b/app/code/Magento/User/etc/webapi_rest/di.xml @@ -10,7 +10,7 @@ <arguments> <argument name="userContexts" xsi:type="array"> <item name="adminSessionUserContext" xsi:type="array"> - <item name="type" xsi:type="object">Magento\User\Model\Authorization\AdminSessionUserContext</item> + <item name="type" xsi:type="object">Magento\User\Model\Authorization\AdminSessionUserContext\Proxy</item> <item name="sortOrder" xsi:type="string">30</item> </item> </argument> diff --git a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html index dacfa640464a3..42240bff3b8db 100644 --- a/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html +++ b/app/code/Magento/User/view/adminhtml/email/password_reset_confirmation.html @@ -4,16 +4,17 @@ * See COPYING.txt for license details. */ --> -<!--@subject {{trans "Password Reset Confirmation for %name" name=$user.name}} @--> +<!--@subject {{trans "Password Reset Confirmation for %name" name=$username}} @--> <!--@vars { "var store.frontend_name":"Store Name", "var user.id":"Account Holder Id", "var user.rp_token":"Reset Password Token", "var user.name":"Account Holder Name", -"store url=\"admin\/auth\/resetpassword\/\" _query_id=$user.id _query_token=$user.rp_token":"Reset Password URL" +"store url=\"admin\/auth\/resetpassword\/\" _query_id=$user.id _query_token=$user.rp_token":"Reset Password URL", +"var username":"Account Holder Name" } @--> -{{trans "%name," name=$user.name}} +{{trans "%name," name=$username}} {{trans "There was recently a request to change the password for your account."}} diff --git a/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml b/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml index 97308204be854..84567a81660f2 100644 --- a/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/role/edit.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var $block \Magento\User\Block\Role\Tab\Edit */ +/** + * @var $block \Magento\User\Block\Role\Tab\Edit + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ ?> <?= $block->getChildHtml() ?> @@ -18,8 +21,7 @@ <label class="label" for="all"><span><?= $block->escapeHtml(__('Resource Access')) ?></span></label> <div class="control"> - <select id="all" name="all" - onchange="jQuery('[data-role=tree-resources-container]').toggle()" class="select"> + <select id="all" name="all" class="select"> <option value="0" <?= ($block->isEverythingAllowed() ? '' : 'selected="selected"') ?>> <?= $block->escapeHtml(__('Custom')) ?> </option> @@ -27,11 +29,16 @@ <?= $block->escapeHtml(__('All')) ?> </option> </select> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onchange', + "jQuery('[data-role=tree-resources-container]').toggle()", + 'select#all' + ) ?> </div> </div> <div class="field - <?php if ($block->isEverythingAllowed()) :?> + <?php if ($block->isEverythingAllowed()):?> no-display <?php endif ?>" data-role="tree-resources-container"> diff --git a/app/code/Magento/User/view/adminhtml/templates/role/info.phtml b/app/code/Magento/User/view/adminhtml/templates/role/info.phtml index 6cf1bb373541d..f6375b17086f9 100644 --- a/app/code/Magento/User/view/adminhtml/templates/role/info.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/role/info.phtml @@ -3,11 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <form action="<?= $block->escapeUrl($block->getUrl('*/*/saverole')) ?>" method="post" id="role-edit-form"> <?= $block->getBlockHtml('formkey') ?> </form> -<script> +<?php $scriptString = <<<script + require([ "jquery", "mage/mage" @@ -18,4 +21,7 @@ require([ }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> 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 a3b5dc68050ac..2042479832898 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 @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'Magento_Ui/js/modal/confirm', @@ -12,9 +15,14 @@ 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) ?>); + +script; + +$myBlock = $block->getLayout()->getBlock('roleUsersGrid'); +if (is_object($myBlock) && $myBlock->getJsObjectName()): + $scriptString .= <<<script + + var checkBoxes = \$H({$myBlock->getUsers(true)}); var warning = false; if (checkBoxes.size() > 0) { warning = true; @@ -43,7 +51,8 @@ require([ if (checked) { confirm({ - content: "<?= $myBlock->escapeHtml(__('Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?')) ?>", + content: "{$myBlock->escapeJs(__('Warning!\r\nThis action will remove this user from already ' . + 'assigned role\r\nAre you sure?'))}", actions: { confirm: function () { checkbox[0].checked = false; @@ -92,7 +101,9 @@ require([ if (!allCheckbox.checked && _.size(checkBoxes._object) > 0) { allCheckbox.checked = true; confirm({ - content: "<?= $myBlock->escapeHtml(__('Warning!\r\nThis action will remove those users from already assigned roles\r\nAre you sure?')) ?>", + content: "{$myBlock->escapeJs( + __('Warning!\r\nThis action will remove those users from already assigned roles\r\nAre you sure?') + )}", actions: { confirm: function () { allCheckbox.checked = false; @@ -105,25 +116,25 @@ require([ } } function markCheckboxes(value) { - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.rows.each(function(row) + {$myBlock->escapeJs($myBlock->getJsObjectName())}.rows.each(function(row) { $(row).getElementsByClassName('checkbox')[0].checked = value; - roleUsersRowInit(<?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>, row); + roleUsersRowInit({$myBlock->escapeJs($myBlock->getJsObjectName())}, row); }); } function onLoad() { - if (typeof <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?> !== 'undefined') { - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + if (typeof {$myBlock->escapeJs($myBlock->getJsObjectName())} !== 'undefined') { + {$myBlock->escapeJs($myBlock->getJsObjectName())}. rowClickCallback = roleUsersRowClick; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. initRowCallback = roleUsersRowInit; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. checkboxCheckCallback = registerUserRole; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. checkCheckboxes = massSelectUsers; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>. + {$myBlock->escapeJs($myBlock->getJsObjectName())}. rows.each(function (row) { - roleUsersRowInit(<?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>, row) + roleUsersRowInit({$myBlock->escapeJs($myBlock->getJsObjectName())}, row) }); $('in_role_user_old').value = $('in_role_user').value; } else { @@ -131,7 +142,13 @@ require([ } } onLoad(); -<?php endif; ?> + +script; +endif; +$scriptString .= <<<script }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml b/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml index 92a97e825ea67..71a866f945693 100644 --- a/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml +++ b/app/code/Magento/User/view/adminhtml/templates/user/roles_grid_js.phtml @@ -3,18 +3,25 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ "mage/adminhtml/grid", "prototype" ], function(){ -<?php $myBlock = $block->getLayout()->getBlock('user.roles.grid'); ?> -<?php if (is_object($myBlock) && $myBlock->getJsObjectName()) : ?> - var radioBoxes = $H({}); +script; + +$myBlock = $block->getLayout()->getBlock('user.roles.grid'); +if (is_object($myBlock) && $myBlock->getJsObjectName()): + $scriptString .= <<<script + + var radioBoxes = \$H({}); var warning = false; - var userRoles = $H(<?= /* @noEscape */ $myBlock->getSelectedRoles(true) ?>); + var userRoles = \$H({$myBlock->getSelectedRoles(true)}); if (userRoles.size() > 0) warning = true; $('user_user_roles').value = userRoles.toQueryString(); @@ -37,7 +44,9 @@ require([ if(checkbox[0] && !checkbox[0].checked){ var checked = isInput ? checkbox[0].checked : !checkbox[0].checked; if (checked && warning && radioBoxes.size() > 0) { - if ( !confirm("<?= $myBlock->escapeHtml(__('Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?')) ?>") ) { + if ( !confirm("{$myBlock->escapeJs( + __('Warning!\r\nThis action will remove this user from already assigned role\r\nAre you sure?') + )}") ) { checkbox[0].checked = false; for(i in radioBoxes) { if( radioBoxes[i].status == 1) { @@ -48,7 +57,7 @@ require([ } warning = false; } - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.setCheckboxChecked(checkbox[0], checked); + {$myBlock->escapeJs($myBlock->getJsObjectName())}.setCheckboxChecked(checkbox[0], checked); } } } @@ -60,19 +69,24 @@ require([ } } - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.rowClickCallback = roleRowClick; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.initRowCallback = rolesRowInit; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.checkboxCheckCallback = registerUserRole; - <?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>.rows.each(function(row){ - rolesRowInit(<?= $myBlock->escapeJs($myBlock->getJsObjectName()) ?>, row) + {$myBlock->escapeJs($myBlock->getJsObjectName())}.rowClickCallback = roleRowClick; + {$myBlock->escapeJs($myBlock->getJsObjectName())}.initRowCallback = rolesRowInit; + {$myBlock->escapeJs($myBlock->getJsObjectName())}.checkboxCheckCallback = registerUserRole; + {$myBlock->escapeJs($myBlock->getJsObjectName())}.rows.each(function(row){ + rolesRowInit({$myBlock->escapeJs($myBlock->getJsObjectName())}, row) }); -<?php endif; ?> + +script; +endif; +$scriptString .= <<<script }); -</script> +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> <?php $editBlock = $block->getLayout()->getBlock('adminhtml.user.edit'); ?> -<?php if (is_object($editBlock)) : ?> +<?php if (is_object($editBlock)): ?> <script type="text/x-magento-init"> { "[data-role=delete-user]" : { diff --git a/app/code/Magento/Usps/Model/Carrier.php b/app/code/Magento/Usps/Model/Carrier.php index 1c8ff0ce9efa9..85e0cf2f6999a 100644 --- a/app/code/Magento/Usps/Model/Carrier.php +++ b/app/code/Magento/Usps/Model/Carrier.php @@ -1470,7 +1470,7 @@ protected function _filterServiceName($name) * * @param \Magento\Framework\DataObject $request * @return string - * @deprecated This method should not be used anymore. + * @deprecated 100.2.1 This method should not be used anymore. * @see \Magento\Usps\Model\Carrier::_doShipmentRequest method doc block. */ protected function _formUsExpressShipmentRequest(\Magento\Framework\DataObject $request) @@ -1647,7 +1647,7 @@ protected function _convertPoundOunces($weightInPounds) * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) - * @deprecated Should not be used anymore. + * @deprecated 100.2.1 Should not be used anymore. * @see \Magento\Usps\Model\Carrier::_doShipmentRequest doc block. */ protected function _formIntlShipmentRequest(\Magento\Framework\DataObject $request) @@ -1902,7 +1902,7 @@ protected function _formIntlShipmentRequest(\Magento\Framework\DataObject $reque * @param \Magento\Framework\DataObject $request * @return \Magento\Framework\DataObject * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @deprecated This method must not be used anymore. Starting from 23.02.2018 USPS elimates API usage for + * @deprecated 100.2.1 This method must not be used anymore. Starting from 23.02.2018 USPS elimates API usage for * free shipping labels generating. */ protected function _doShipmentRequest(\Magento\Framework\DataObject $request) diff --git a/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml b/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml index a569b8e71a055..28f66d4c913e2 100644 --- a/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml +++ b/app/code/Magento/Variable/view/adminhtml/templates/system/variable/js.phtml @@ -3,8 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> + +<?php $scriptString = <<<script + require([ 'prototype' ], function () { @@ -27,4 +31,7 @@ window.toggleValueElement = function(element) { } }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php b/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php index bb6343691f726..94d14dc14228c 100644 --- a/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php +++ b/app/code/Magento/Vault/Api/Data/PaymentTokenFactoryInterface.php @@ -9,7 +9,7 @@ /** * Interface PaymentTokenFactoryInterface * @api - * @since 100.3.0 + * @since 101.0.0 */ interface PaymentTokenFactoryInterface { @@ -24,7 +24,7 @@ interface PaymentTokenFactoryInterface * Create payment token entity * @param $type string|null * @return PaymentTokenInterface - * @since 100.3.0 + * @since 101.0.0 */ public function create($type = null); } diff --git a/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php b/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php index 1a854ec814844..501e516841b6e 100644 --- a/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php +++ b/app/code/Magento/Vault/Api/Data/PaymentTokenInterfaceFactory.php @@ -8,7 +8,7 @@ /** * Interface PaymentTokenInterfaceFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface * @codingStandardsIgnoreStart */ diff --git a/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php b/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php index d568a91c0421b..ab1e2ccd783d5 100644 --- a/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php +++ b/app/code/Magento/Vault/Model/AbstractPaymentTokenFactory.php @@ -12,7 +12,7 @@ /** * Class AbstractPaymentTokenFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface */ abstract class AbstractPaymentTokenFactory implements PaymentTokenInterfaceFactory diff --git a/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php b/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php index 8fb060b41a24f..e9178ccaf50a8 100644 --- a/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php +++ b/app/code/Magento/Vault/Model/AccountPaymentTokenFactory.php @@ -7,7 +7,7 @@ /** * Class AccountPaymentTokenFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface */ class AccountPaymentTokenFactory extends AbstractPaymentTokenFactory diff --git a/app/code/Magento/Vault/Model/CreditCardTokenFactory.php b/app/code/Magento/Vault/Model/CreditCardTokenFactory.php index 735dc7c706f62..b0015e3f78316 100644 --- a/app/code/Magento/Vault/Model/CreditCardTokenFactory.php +++ b/app/code/Magento/Vault/Model/CreditCardTokenFactory.php @@ -7,7 +7,7 @@ /** * Class CreditCardTokenFactory - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @see PaymentTokenFactoryInterface */ class CreditCardTokenFactory extends AbstractPaymentTokenFactory diff --git a/app/code/Magento/Vault/Model/PaymentTokenFactory.php b/app/code/Magento/Vault/Model/PaymentTokenFactory.php index 6249fa4944a2c..cee838d622749 100644 --- a/app/code/Magento/Vault/Model/PaymentTokenFactory.php +++ b/app/code/Magento/Vault/Model/PaymentTokenFactory.php @@ -13,7 +13,7 @@ /** * PaymentTokenFactory class * @api - * @since 100.3.0 + * @since 101.0.0 */ class PaymentTokenFactory implements PaymentTokenFactoryInterface { @@ -42,7 +42,7 @@ public function __construct(ObjectManagerInterface $objectManager, array $tokenT * Create payment token entity * @param $type string * @return PaymentTokenInterface - * @since 100.3.0 + * @since 101.0.0 */ public function create($type = null) { diff --git a/app/code/Magento/Vault/Model/PaymentTokenRepository.php b/app/code/Magento/Vault/Model/PaymentTokenRepository.php index 2ccd6181b9b81..46d7b6d2e80fe 100644 --- a/app/code/Magento/Vault/Model/PaymentTokenRepository.php +++ b/app/code/Magento/Vault/Model/PaymentTokenRepository.php @@ -158,7 +158,7 @@ public function save(Data\PaymentTokenInterface $paymentToken) * @param FilterGroup $filterGroup * @param Collection $collection * @return void - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @throws \Magento\Framework\Exception\InputException */ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collection $collection) @@ -172,7 +172,7 @@ protected function addFilterGroupToCollection(FilterGroup $filterGroup, Collecti /** * Retrieve collection processor * - * @deprecated 100.3.0 + * @deprecated 101.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() diff --git a/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml index f496e500a4d9b..a43d6578925b2 100644 --- a/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml +++ b/app/code/Magento/Vault/Test/Mftf/Test/StorefrontVerifySecureURLRedirectVaultTest.xml @@ -28,11 +28,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml b/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml index fb0666cde976f..8311ff374c3d1 100644 --- a/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml +++ b/app/code/Magento/Vault/view/adminhtml/templates/form/vault.phtml @@ -4,7 +4,10 @@ * See COPYING.txt for license details. */ -/** @var Magento\Vault\Block\Form $block */ +/** + * @var Magento\Vault\Block\Form $block + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $code = $block->escapeHtml($block->getMethodCode()); ?> <fieldset data-mage-init='{ @@ -12,10 +15,11 @@ $code = $block->escapeHtml($block->getMethodCode()); "code": "<?= /* @noEscape */ $code ?>", "fieldset": "payment_form_<?= /* @noEscape */ $code ?>" } - }' class="admin__fieldset payment-method" - id="payment_form_<?= /* @noEscape */ $code ?>" - style="display:none" - > + }' class="admin__fieldset payment-method" id="payment_form_<?= /* @noEscape */ $code ?>"> <input type="hidden" name="payment[public_hash]" id="<?= /* @noEscape */ $code ?>_public_hash" value="" /> <?= $block->getChildHtml() ?> </fieldset> +<?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + 'display:none', + 'fieldset#payment_form_' . /* @noEscape */ $code +) ?> diff --git a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php index 5e50cdee794ce..a01c5054f9b5f 100644 --- a/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php +++ b/app/code/Magento/Webapi/Controller/Soap/Request/Handler.php @@ -179,7 +179,7 @@ private function prepareOperationInput(string $serviceClass, array $methodMetada * @param string $serviceMethod * @param array $arguments * @return array - * @deprecated + * @deprecated 100.3.2 * @see Handler::prepareOperationInput() */ protected function _prepareRequestData($serviceClass, $serviceMethod, $arguments) diff --git a/app/code/Magento/Webapi/Model/ConfigInterface.php b/app/code/Magento/Webapi/Model/ConfigInterface.php index 338c18795595f..a0467fb840ccb 100644 --- a/app/code/Magento/Webapi/Model/ConfigInterface.php +++ b/app/code/Magento/Webapi/Model/ConfigInterface.php @@ -12,6 +12,7 @@ * This class gives access to consolidated web API configuration from <Module_Name>/etc/webapi.xml files. * * @api + * @since 100.2.4 */ interface ConfigInterface { @@ -19,6 +20,7 @@ interface ConfigInterface * Return services loaded from cache if enabled or from files merged previously * * @return array + * @since 100.2.4 */ public function getServices(); } diff --git a/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php b/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php index a0499087c35b9..febe7cba0b7fc 100644 --- a/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php +++ b/app/code/Magento/WebapiAsync/Model/BulkServiceConfig.php @@ -15,6 +15,7 @@ /** * @api + * @since 100.2.0 */ class BulkServiceConfig implements \Magento\Webapi\Model\ConfigInterface { @@ -58,6 +59,7 @@ public function __construct( * Return services loaded from cache if enabled or from files merged previously * * @return array + * @since 100.2.0 */ public function getServices() { diff --git a/app/code/Magento/WebapiAsync/Model/OperationRepository.php b/app/code/Magento/WebapiAsync/Model/OperationRepository.php index 7af8ff877ebbc..87db3dfb59e2c 100644 --- a/app/code/Magento/WebapiAsync/Model/OperationRepository.php +++ b/app/code/Magento/WebapiAsync/Model/OperationRepository.php @@ -72,6 +72,7 @@ public function __construct( */ public function create($topicName, $entityParams, $groupId, $operationId): OperationInterface { + $this->messageValidator->validate($topicName, $entityParams); $requestData = $this->inputParamsResolver->getInputData(); if ($operationId === null || !isset($requestData[$operationId])) { @@ -88,13 +89,13 @@ public function create($topicName, $entityParams, $groupId, $operationId): Opera ]; $data = [ 'data' => [ - OperationInterface::BULK_ID => $groupId, - OperationInterface::TOPIC_NAME => $topicName, + OperationInterface::ID => $operationId, + OperationInterface::BULK_ID => $groupId, + OperationInterface::TOPIC_NAME => $topicName, OperationInterface::SERIALIZED_DATA => $this->jsonSerializer->serialize($serializedData), - OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, + OperationInterface::STATUS => OperationInterface::STATUS_TYPE_OPEN, ], ]; - /** @var OperationInterface $operation */ $operation = $this->operationFactory->create($data); return $operation; diff --git a/app/code/Magento/WebapiAsync/Model/ServiceConfig.php b/app/code/Magento/WebapiAsync/Model/ServiceConfig.php index 4c085935090bd..8387b2dc53118 100644 --- a/app/code/Magento/WebapiAsync/Model/ServiceConfig.php +++ b/app/code/Magento/WebapiAsync/Model/ServiceConfig.php @@ -17,6 +17,7 @@ * This class gives access to consolidated web API configuration from <Module_Name>/etc/webapi_async.xml files. * * @api + * @since 100.2.0 */ class ServiceConfig { @@ -63,6 +64,7 @@ public function __construct( * Return services loaded from cache if enabled or from files merged previously * * @return array + * @since 100.2.0 */ public function getServices() { diff --git a/app/code/Magento/Weee/Block/Item/Price/Renderer.php b/app/code/Magento/Weee/Block/Item/Price/Renderer.php index 721df2c83f460..e29dd9d58f0b4 100644 --- a/app/code/Magento/Weee/Block/Item/Price/Renderer.php +++ b/app/code/Magento/Weee/Block/Item/Price/Renderer.php @@ -40,6 +40,7 @@ public function __construct( array $data = [] ) { $this->weeeHelper = $weeeHelper; + $data['weeeHelper'] = $this->weeeHelper; parent::__construct($context, $taxHelper, $priceCurrency, $data); $this->_isScopePrivate = true; } diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml index 77a8e6e6fd20c..0f4a7f9a55d26 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminFixedTaxValSavedForSpecificWebsiteTest.xml @@ -51,8 +51,12 @@ <!--Set catalog price scope to Global--> <comment userInput="Set catalog price scope to Global" stepKey="commentSetPriceScope"/> <magentoCLI command="config:set catalog/price/scope 0" stepKey="setPriceScopeGlobal"/> - <magentoCLI command="indexer:reindex catalog_product_price" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalog_product_price"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!--Set catalog price scope to Global--> @@ -97,8 +101,12 @@ <!--Set catalog price scope to Website--> <comment userInput="Set catalog price scope to Website" stepKey="commentSetPriceScope"/> <magentoCLI command="config:set catalog/price/scope 1" stepKey="setPriceScopeWebsite"/> - <magentoCLI command="indexer:reindex catalog_product_price" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value="catalog_product_price"/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!--See available websites only 'All Websites'--> <comment userInput="See available websites 'All Websites', 'Main Website' and Second website" stepKey="commentCheckWebsitesInProductPage"/> <actionGroup ref="AdminProductPageOpenByIdActionGroup" stepKey="goToProductPageSecondTime"> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml index 60c39dd5058b5..0d7c21b6efffc 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/AdminRemoveProductWeeeAttributeOptionTest.xml @@ -38,8 +38,7 @@ <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProductInitial"/> </before> <after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductListing"/> - <waitForPageLoad stepKey="waitForProductListingPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductListing"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetGridToDefaultKeywordSearch"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logout"/> <deleteData createDataKey="createProductFPTAttribute" stepKey="deleteProductFPTAttribute"/> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml index 74d6c2a97b089..e78036458301b 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForCustomerPhysicalQuoteTest.xml @@ -68,7 +68,7 @@ <createData entity="WeeeConfigDisable" stepKey="disableFPT"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <magentoCron groups="index" stepKey="reindexBrokenIndices"/> diff --git a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml index 92f526c79e926..74ba7c1f2bff3 100644 --- a/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml +++ b/app/code/Magento/Weee/Test/Mftf/Test/StorefrontFPTTaxInformationInShoppingCartForGuestPhysicalQuoteTest.xml @@ -63,7 +63,7 @@ <createData entity="DefaultTaxConfig" stepKey="defaultTaxConfiguration"/> <createData entity="WeeeConfigDisable" stepKey="disableFPT"/> <deleteData createDataKey="createSimpleProduct" stepKey="deleteSimpleProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsGridFilters"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="logoutFromAdmin"/> <magentoCron groups="index" stepKey="reindexBrokenIndices"/> diff --git a/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml b/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml index 1b77231640868..1eff06bb4b985 100644 --- a/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml +++ b/app/code/Magento/Weee/view/adminhtml/templates/renderer/tax.phtml @@ -4,28 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate ?> <?php -/** @var $block \Magento\Weee\Block\Renderer\Weee\Tax */ +/** + * @var $block \Magento\Weee\Block\Renderer\Weee\Tax + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ + +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +/** @var \Magento\Directory\Helper\Data $directoryHelper */ +$directoryHelper = $block->getData('directoryHelper'); + $data = ['fptAttribute' => [ - 'region' => $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonDecode( - $this->helper(\Magento\Directory\Helper\Data::class)->getRegionJson() - ), + 'region' => $jsonHelper->jsonDecode($directoryHelper->getRegionJson()), 'itemsData' => $block->getValues(), 'bundlePriceType' => '#price_type', ]]; ?> <div id="attribute-<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>-container" class="field" data-attribute-code="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" - data-mage-init="<?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($data) ?>"> + data-mage-init="<?= /* @noEscape */ $jsonHelper->jsonEncode($data) ?>"> <label class="label"><span><?= $block->escapeHtml($block->getElement()->getLabel()) ?></span></label> <div class="control"> <table class="data-table"> <thead> <tr> - <th class="col-website" <?php if (!$block->isMultiWebsites()) : ?>style="display: none;"<?php endif; ?>><?= $block->escapeHtml(__('Website')) ?></th> + <th class="col-website"><?= $block->escapeHtml(__('Website')) ?></th> + <?php if (!$block->isMultiWebsites()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'th.col-website') ?> + <?php endif; ?> <th class="col-country required"><?= $block->escapeHtml(__('Country/State')) ?></th> <th class="col-tax required"><?= $block->escapeHtml(__('Tax')) ?></th> <th class="col-action"><?= $block->escapeHtml(__('Action')) ?></th> @@ -43,7 +52,8 @@ $data = ['fptAttribute' => [ Hidden field below with attribute code id is necessary for jQuery validation plugin. Validation message will be displayed after this field. --> - <input type="hidden" name="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" disabled="disabled"> + <input type="hidden" name="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" + id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>" disabled="disabled"> </div> <script data-role="row-template" type="text/x-magento-template"> @@ -51,22 +61,32 @@ $data = ['fptAttribute' => [ $elementName = $block->escapeHtmlAttr($block->getElement()->getName()); $elementClass = $block->escapeHtmlAttr($block->getElement()->getClass()); ?> - <tr id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>_weee_tax_row_<%- data.index %>" data-role="fpt-item-row"> - <td class="col-website" <?php if (!$block->isMultiWebsites()) : ?>style="display: none"<?php endif; ?>> + <tr id="<?= /* @noEscape */ $block->getElement()->getHtmlId() ?>_weee_tax_row_<%- data.index %>" + data-role="fpt-item-row"> + <td class="col-website"> <select id="<?= /* @noEscape */ $elementName ?>_weee_tax_row_<%- data.index %>_website" name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][website_id]" class="<?= /* @noEscape */ $elementClass ?> website required-entry" data-role="select-website"> - <?php foreach ($block->getWebsites() as $_websiteId => $_info) : ?> - <option value="<?= /* @noEscape */ $_websiteId ?>"><?= $block->escapeHtml($_info['name']) ?><?php if (!empty($_info['currency'])) : ?>[<?= /* @noEscape */ $_info['currency'] ?>]<?php endif; ?></option> + <?php foreach ($block->getWebsites() as $_websiteId => $_info): ?> + <option value="<?= /* @noEscape */ $_websiteId ?>"><?= $block->escapeHtml($_info['name']) ?> + <?php if (!empty($_info['currency'])): ?> + [<?= /* @noEscape */ $_info['currency'] ?>] + <?php endif; ?> + </option> <?php endforeach ?> </select> </td> + <?php if (!$block->isMultiWebsites()): ?> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag('display: none;', 'td.col-website') ?> + <?php endif; ?> <td class="col-country"> <select id="<?= /* @noEscape */ $elementName ?>_weee_tax_row_<%- data.index %>_country" name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][country]" class="<?= /* @noEscape */ $elementClass ?> country required-entry" data-role="select-country"> - <?php foreach ($block->getCountries() as $_country) : ?> - <option value="<?= $block->escapeHtmlAttr($_country['value']) ?>"><?= $block->escapeHtml($_country['label']) ?></option> + <?php foreach ($block->getCountries() as $_country): ?> + <option value="<?= $block->escapeHtmlAttr($_country['value']) ?>"> + <?= $block->escapeHtml($_country['label']) ?> + </option> <?php endforeach ?> </select> <select id="<?= /* @noEscape */ $elementName ?>_weee_tax_row_<%- data.index %>_state" @@ -81,7 +101,8 @@ $data = ['fptAttribute' => [ type="text" value="<%- data.value %>"/> </td> <td class="col-action"> - <input name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][delete]" class="delete" type="hidden" value="" data-role="delete-fpt-item"/> + <input name="<?= /* @noEscape */ $elementName ?>[<%- data.index %>][delete]" class="delete" + type="hidden" value="" data-role="delete-fpt-item"/> <?= $block->getChildHtml('delete_button') ?> </td> </tr> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml index 15abae5c889fe..b9b5a00d0a157 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_excl_tax.phtml @@ -4,31 +4,39 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $_item = $block->getItem(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceExclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="esubtotal-item-tax-details<?= (int) $_item->getId() ?>" style="display: none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['row_amount'], true, true) ?></span> +<?php if ($weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="esubtotal-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['row_amount'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml index b848698b8b829..38f2c528b15c9 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/row_incl_tax.phtml @@ -4,34 +4,39 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $_item = $block->getItem(); /** @var $_weeeHelper \Magento\Weee\Helper\Data */ -$_weeeHelper = $this->helper(\Magento\Weee\Helper\Data::class); +$_weeeHelper = $block->getData('weeeHelper'); ?> <?php $_incl = $_item->getRowTotalInclTax(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceInclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="subtotal-item-tax-details<?= (int) $_item->getId() ?>" style="display: none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['row_amount_incl_tax'], true, true) ?></span> +<?php if ($_weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="subtotal-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($_weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['row_amount_incl_tax'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceInclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml index a485de90c871d..8bb331c109119 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_excl_tax.phtml @@ -4,31 +4,39 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $_item = $block->getItem(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceExclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="eunit-item-tax-details<?= (int) $_item->getId() ?>" style="display:none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['amount'], true, true) ?></span> +<?php if ($weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="eunit-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['amount'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalUnitDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml index 0dada610e181e..e667796825327 100644 --- a/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/checkout/onepage/review/item/price/unit_incl_tax.phtml @@ -4,35 +4,40 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate - -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ $_item = $block->getItem(); /** @var $_weeeHelper \Magento\Weee\Helper\Data */ -$_weeeHelper = $this->helper(\Magento\Weee\Helper\Data::class); +$_weeeHelper = $block->getData('weeeHelper'); ?> <?php $_incl = $_item->getPriceInclTax(); ?> -<?php if ($block->displayPriceWithWeeeDetails()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> -<?php else : ?> +<?php if ($block->displayPriceWithWeeeDetails()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> +<?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceInclTax()) ?> </span> -<?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item)) : ?> - <span class="cart-tax-info" id="unit-item-tax-details<?= (int) $_item->getId() ?>" style="display: none;"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($_item) as $tax) : ?> - <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"><?= /* @noEscape */ $block->formatPrice($tax['amount_incl_tax'], true, true) ?></span> +<?php if ($_weeeHelper->getApplied($_item)): ?> + <span class="cart-tax-info no-display" id="unit-item-tax-details<?= (int) $_item->getId() ?>"> + <?php if ($block->displayPriceWithWeeeDetails()): ?> + <?php foreach ($_weeeHelper->getApplied($_item) as $tax): ?> + <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> + <?= /* @noEscape */ $block->formatPrice($tax['amount_incl_tax'], true, true) ?> + </span> <?php endforeach; ?> <?php endif; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> - <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> + <?php if ($block->displayFinalPrice()): ?> + <span class="cart-tax-total" + data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $_item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalUnitDisplayPriceInclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml b/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml index 37aa852871408..5bd2a2f81acbc 100644 --- a/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/item/price/row.phtml @@ -4,35 +4,40 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $item = $block->getItem(); ?> -<?php if (($block->displayPriceInclTax() || $block->displayBothPrices()) && !$item->getNoSubtotal()) : ?> +<?php if (($block->displayPriceInclTax() || $block->displayBothPrices()) && !$item->getNoSubtotal()): ?> <span class="price-including-tax" data-label="<?= $block->escapeHtmlAttr(__('Incl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceInclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <div class="cart-tax-info" id="subtotal-item-tax-details<?= (int) $item->getId() ?>" style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <div class="cart-tax-info no-display" id="subtotal-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['row_amount_incl_tax'], true, true) ?> </span> <?php endforeach; ?> </div> - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> + data-mage-init='{"taxToggle": {"itemTaxId" : "#subtotal-item-tax-details<?= (int) $item->getId() + ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceInclTax()) ?> </span> @@ -42,30 +47,30 @@ $item = $block->getItem(); </span> <?php endif; ?> -<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()) : ?> +<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()): ?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtmlAttr(__('Excl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $item->getId()?>"}}'> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getRowDisplayPriceExclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <span class="cart-tax-info" id="esubtotal-item-tax-details<?= (int) $item->getId() ?>" - style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <span class="cart-tax-info no-display" id="esubtotal-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['row_amount'], true, true) ?> </span> <?php endforeach; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int) $item->getId() ?>"}}'> + data-mage-init='{"taxToggle": {"itemTaxId" : "#esubtotal-item-tax-details<?= (int)$item->getId() + ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalRowDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml b/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml index 4e62409ad00f4..39d0bc59653d4 100644 --- a/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml +++ b/app/code/Magento/Weee/view/frontend/templates/item/price/unit.phtml @@ -4,33 +4,37 @@ * See COPYING.txt for license details. */ -// phpcs:disable Magento2.Templates.ThisInTemplate +/** + * @var $block \Magento\Weee\Block\Item\Price\Renderer + * @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer + */ -/** @var $block \Magento\Weee\Block\Item\Price\Renderer */ +/** @var \Magento\Weee\Helper\Data $weeeHelper */ +$weeeHelper = $block->getData('weeeHelper'); $item = $block->getItem(); ?> -<?php if ($block->displayPriceInclTax() || $block->displayBothPrices()) : ?> +<?php if ($block->displayPriceInclTax() || $block->displayBothPrices()): ?> <span class="price-including-tax" data-label="<?= $block->escapeHtmlAttr(__('Incl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceInclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <span class="cart-tax-info" id="unit-item-tax-details<?= (int) $item->getId() ?>" style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <span class="cart-tax-info no-display" id="unit-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['amount_incl_tax'], true, true) ?> </span> <?php endforeach; ?> </span> - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#unit-item-tax-details<?= (int) $item->getId() ?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total Incl. Tax')) ?>"> @@ -42,30 +46,28 @@ $item = $block->getItem(); </span> <?php endif; ?> -<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()) : ?> +<?php if ($block->displayPriceExclTax() || $block->displayBothPrices()): ?> <span class="price-excluding-tax" data-label="<?= $block->escapeHtmlAttr(__('Excl. Tax')) ?>"> - <?php if ($block->displayPriceWithWeeeDetails()) : ?> + <?php if ($block->displayPriceWithWeeeDetails()): ?> <span class="cart-tax-total" data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $item->getId() ?>"}}'> - <?php else : ?> + <?php else: ?> <span class="cart-price"> <?php endif; ?> <?= /* @noEscape */ $block->formatPrice($block->getUnitDisplayPriceExclTax()) ?> </span> - <?php if ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item)) : ?> - <span class="cart-tax-info" id="eunit-item-tax-details<?= (int) $item->getId() ?>" - style="display: none;"> - <?php foreach ($this->helper(\Magento\Weee\Helper\Data::class)->getApplied($item) as $tax) : ?> + <?php if ($weeeHelper->getApplied($item)): ?> + <span class="cart-tax-info no-display" id="eunit-item-tax-details<?= (int) $item->getId() ?>"> + <?php foreach ($weeeHelper->getApplied($item) as $tax): ?> <span class="weee" data-label="<?= $block->escapeHtmlAttr($tax['title']) ?>"> <?= /* @noEscape */ $block->formatPrice($tax['amount'], true, true) ?> </span> <?php endforeach; ?> </span> - - <?php if ($block->displayFinalPrice()) : ?> + <?php if ($block->displayFinalPrice()): ?> <span class="cart-tax-total" - data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?= (int) $item->getId() ?>"}}'> + data-mage-init='{"taxToggle": {"itemTaxId" : "#eunit-item-tax-details<?=(int)$item->getId()?>"}}'> <span class="weee" data-label="<?= $block->escapeHtmlAttr(__('Total')) ?>"> <?= /* @noEscape */ $block->formatPrice($block->getFinalUnitDisplayPriceExclTax()) ?> </span> diff --git a/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php b/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php index 45b3056eac68d..f10a821c510e1 100644 --- a/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php +++ b/app/code/Magento/Widget/Block/Adminhtml/Widget/Chooser.php @@ -11,6 +11,9 @@ */ namespace Magento\Widget\Block\Adminhtml\Widget; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\View\Helper\SecureHtmlRenderer; + /** * Chooser widget block. */ @@ -26,20 +29,28 @@ class Chooser extends \Magento\Backend\Block\Template */ protected $_jsonEncoder; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param \Magento\Backend\Block\Template\Context $context * @param \Magento\Framework\Json\EncoderInterface $jsonEncoder * @param \Magento\Framework\Data\Form\Element\Factory $elementFactory * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( \Magento\Backend\Block\Template\Context $context, \Magento\Framework\Json\EncoderInterface $jsonEncoder, \Magento\Framework\Data\Form\Element\Factory $elementFactory, - array $data = [] + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { $this->_jsonEncoder = $jsonEncoder; $this->_elementFactory = $elementFactory; + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); parent::__construct($context, $data); } @@ -189,33 +200,35 @@ protected function _toHtml() '</label> <div id="' . $chooserId . - 'advice-container" class="hidden"></div> - <script> - require(["prototype", "mage/adminhtml/wysiwyg/widget"], function(){ + 'advice-container" class="hidden"></div>' . + $this->secureRenderer->renderTag( + 'script', + [], + 'require(["prototype", "mage/adminhtml/wysiwyg/widget"], function(){ //<![CDATA[ (function() { var instantiateChooser = function() { window.' . - $chooserId . - ' = new WysiwygWidget.chooser( + $chooserId . + ' = new WysiwygWidget.chooser( "' . - $chooserId . - '", + $chooserId . + '", "' . - $this->getSourceUrl() . - '", + $this->getSourceUrl() . + '", ' . - $configJson . - ' + $configJson . + ' ); if ($("' . - $chooserId . - 'value")) { + $chooserId . + 'value")) { $("' . - $chooserId . - 'value").advaiceContainer = "' . - $chooserId . - 'advice-container"; + $chooserId . + 'value").advaiceContainer = "' . + $chooserId . + 'advice-container"; } } @@ -223,7 +236,8 @@ protected function _toHtml() })(); //]]> }); - </script> - '; + ', + false + ); } } diff --git a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php index 03d9d10311382..fe96cbecb425a 100644 --- a/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php +++ b/app/code/Magento/Widget/Controller/Adminhtml/Widget/LoadOptions.php @@ -60,7 +60,7 @@ public function execute() /** * @return \Magento\Widget\Helper\Conditions - * @deprecated 100.1.4 + * @deprecated 101.0.0 */ private function getConditionsHelper() { diff --git a/app/code/Magento/Widget/Model/ResourceModel/Widget.php b/app/code/Magento/Widget/Model/ResourceModel/Widget.php index 8d78bb5a56800..4ed77b48f297d 100644 --- a/app/code/Magento/Widget/Model/ResourceModel/Widget.php +++ b/app/code/Magento/Widget/Model/ResourceModel/Widget.php @@ -9,7 +9,7 @@ /** * Resource model for widget. * - * @deprecated 100.2.0 Data from this table was moved to xml(widget.xml). + * @deprecated 101.0.0 Data from this table was moved to xml(widget.xml). */ class Widget extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb { diff --git a/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php b/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php index 8e5d8d63840fb..0532c57f0306d 100644 --- a/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php +++ b/app/code/Magento/Widget/Model/ResourceModel/Widget/Instance/Options/ThemeId.php @@ -9,7 +9,7 @@ /** * Widget Instance Theme Id Options * - * @deprecated 100.2.0 created new class that correctly loads theme options and whose name follows naming convention + * @deprecated 100.1.7 created new class that correctly loads theme options and whose name follows naming convention * @see \Magento\Widget\Model\ResourceModel\Widget\Instance\Options\Themes */ class ThemeId implements \Magento\Framework\Option\ArrayInterface diff --git a/app/code/Magento/Widget/Model/Widget.php b/app/code/Magento/Widget/Model/Widget.php index d07e84186b2c9..195c3f397ff18 100644 --- a/app/code/Magento/Widget/Model/Widget.php +++ b/app/code/Magento/Widget/Model/Widget.php @@ -88,7 +88,7 @@ public function __construct( * * @return \Magento\Framework\Math\Random * - * @deprecated 100.1.0 + * @deprecated 100.0.10 */ private function getMathRandom() { @@ -127,7 +127,7 @@ public function getWidgetByClassType($type) * @param string $type Widget type * @return null|\Magento\Framework\Simplexml\Element * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function getConfigAsXml($type) { diff --git a/app/code/Magento/Widget/Model/Widget/Instance.php b/app/code/Magento/Widget/Model/Widget/Instance.php index 4ca126e659e09..7f4e3ae8610ba 100644 --- a/app/code/Magento/Widget/Model/Widget/Instance.php +++ b/app/code/Magento/Widget/Model/Widget/Instance.php @@ -99,11 +99,13 @@ class Instance extends \Magento\Framework\Model\AbstractModel /** * @var \Magento\Catalog\Model\Product\Type + * @since 101.0.4 */ protected $_productType; /** * @var \Magento\Widget\Model\Config\Reader + * @since 101.0.4 */ protected $_reader; diff --git a/app/code/Magento/Widget/composer.json b/app/code/Magento/Widget/composer.json index 3f0f7fb212d4a..2cf8429095ce7 100644 --- a/app/code/Magento/Widget/composer.json +++ b/app/code/Magento/Widget/composer.json @@ -12,7 +12,8 @@ "magento/module-cms": "*", "magento/module-store": "*", "magento/module-theme": "*", - "magento/module-variable": "*" + "magento/module-variable": "*", + "magento/module-ui": "*" }, "suggest": { "magento/module-widget-sample-data": "*" diff --git a/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml b/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml index 72de9550654d3..5bb6756bf4ebe 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/catalog/category/widget/tree.phtml @@ -5,19 +5,31 @@ */ /** @var \Magento\Catalog\Block\Adminhtml\Category\Widget\Chooser $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <?php $_divId = 'tree' . $block->getId() ?> <div id="<?= $block->escapeHtmlAttr($_divId) ?>" class="tree"></div> <script id="ie-deferred-loader" defer="defer" src="//:"></script> -<script> +<?php +$useMassaction = /* @noEscape */ $block->getUseMassaction() ? 1 : 0; +$isAnchorOnly = /* @noEscape */ $block->getIsAnchorOnly() ? 1 : 0; +$nodeClickListener = /* @noEscape */ $block->getNodeClickListener(); +$withEmpltyNode = /* @noEscape */ ($block->getWithEmptyNode() ? 'false' : 'true'); +$isVisible = (bool) $block->getRoot()->getIsVisible(); +$categoryId = (int) $block->getCategoryId(); +$rootId = (int) $block->getRoot()->getId(); +$isWasExpanded = (int) $block->getIsWasExpanded(); +$treeJson = /* @noEscape */ $block->getTreeJson(); +$scriptString = <<<script + require(['jquery', "prototype", "extjs/ext-tree-checkbox"], function(jQuery){ -var tree<?= $block->escapeJs($block->getId()) ?>; +var tree{$block->escapeJs($block->getId())}; -var useMassaction = <?= /* @noEscape */ $block->getUseMassaction() ? 1 : 0 ?>; +var useMassaction = {$useMassaction}; -var isAnchorOnly = <?= /* @noEscape */ $block->getIsAnchorOnly() ? 1 : 0 ?>; +var isAnchorOnly = {$isAnchorOnly}; Ext.tree.TreePanel.Enhanced = function(el, config) { @@ -41,9 +53,17 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { this.setRootNode(root); if (firstLoad) { - <?php if ($block->getNodeClickListener()) : ?> - this.addListener('click', <?= /* @noEscape */ $block->getNodeClickListener() ?>.createDelegate(this)); - <?php endif; ?> + +script; +if ($block->getNodeClickListener()): + $scriptString .= <<<script + + this.addListener('click', {$nodeClickListener}.createDelegate(this)); + +script; +endif; +$scriptString .= <<<script + } this.loader.buildCategoryTree(root, data); @@ -55,10 +75,10 @@ Ext.extend(Ext.tree.TreePanel.Enhanced, Ext.tree.TreePanel, { jQuery(function() { - var emptyNodeAdded = <?= /* @noEscape */ ($block->getWithEmptyNode() ? 'false' : 'true') ?>; + var emptyNodeAdded = {$withEmpltyNode}; var categoryLoader = new Ext.tree.TreeLoader({ - dataUrl: '<?= $block->escapeUrl($block->getLoadTreeUrl()) ?>' + dataUrl: '{$block->escapeJs($block->getLoadTreeUrl())}' }); categoryLoader.buildCategoryTree = function(parent, config) @@ -77,7 +97,7 @@ jQuery(function() // Add empty node to reset category filter if(!emptyNodeAdded) { var empty = Object.clone(_node); - empty.text = '<?= $block->escapeJs($block->escapeHtml(__('None'))) ?>'; + empty.text = '{$block->escapeJs($block->escapeHtml(__('None')))}'; empty.children = []; empty.id = 'none'; empty.path = '1/none'; @@ -148,39 +168,42 @@ jQuery(function() }; categoryLoader.on("beforeload", function(treeLoader, node) { - $('<?= $block->escapeJs($_divId) ?>').fire('category:beforeLoad', {treeLoader:treeLoader}); + $('{$block->escapeJs($_divId)}').fire('category:beforeLoad', {treeLoader:treeLoader}); treeLoader.baseParams.id = node.attributes.id; }); - tree<?= $block->escapeJs($block->getId()) ?> = new Ext.tree.TreePanel.Enhanced('<?= $block->escapeJs($_divId) ?>', { + tree{$block->escapeJs($block->getId())} = new Ext.tree.TreePanel.Enhanced('{$block->escapeJs($_divId)}', { animate: false, loader: categoryLoader, enableDD: false, containerScroll: true, - rootVisible: '<?= (bool) $block->getRoot()->getIsVisible() ?>', + rootVisible: '{$isVisible}', useAjax: true, - currentNodeId: <?= (int) $block->getCategoryId() ?>, + currentNodeId: {$categoryId}, addNodeTo: false }); if (useMassaction) { - tree<?= $block->escapeJs($block->getId()) ?>.on('check', function(node) { - $('<?= $block->escapeJs($_divId) ?>').fire('node:changed', {node:node}); - }, tree<?= $block->escapeJs($block->getId()) ?>); + tree{$block->escapeJs($block->getId())}.on('check', function(node) { + $('{$block->escapeJs($_divId)}').fire('node:changed', {node:node}); + }, tree{$block->escapeJs($block->getId())}); } // set the root node var parameters = { text: 'Psw', draggable: false, - id: <?= (int) $block->getRoot()->getId() ?>, - expanded: <?= (int) $block->getIsWasExpanded() ?>, - category_id: <?= (int) $block->getCategoryId() ?> + id: {$rootId}, + expanded: {$isWasExpanded}, + category_id: {$categoryId} }; - tree<?= $block->escapeJs($block->getId()) ?>.loadTree({parameters:parameters, data:<?= /* @noEscape */ $block->getTreeJson() ?>},true); + tree{$block->escapeJs($block->getId())}.loadTree({parameters:parameters, data:{$treeJson}},true); }); }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml index c45ef65f4f242..6dab476115cee 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/instance/edit/layout.phtml @@ -5,6 +5,7 @@ */ /** @var \Magento\Widget\Block\Adminhtml\Widget\Instance\Edit\Tab\Main\Layout $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> <fieldset class="fieldset"> @@ -16,7 +17,11 @@ </div> </fieldset> <script id="ie-deferred-loader" defer="defer" src="//:"></script> -<script> +<?php +/** @var \Magento\Framework\Json\Helper\Data $jsonHelper */ +$jsonHelper = $block->getData('jsonHelper'); +$scriptString = <<<script + require([ 'jquery', 'mage/template', @@ -30,33 +35,68 @@ require([ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id="page_group_container_<%- data.id %>">'+ '<div class="fieldset-wrapper-title">'+ '<label for="widget_instance[<%- data.id %>][page_group]">Display on <span class="required">*</span></label>'+ - '<?= $block->getDisplayOnSelectHtml() ?>'+ + '{$block->getDisplayOnSelectHtml()}'+ '<div class="actions">'+ - <?= /* @noEscape */ $this->helper(\Magento\Framework\Json\Helper\Data::class)->jsonEncode($block->getRemoveLayoutButtonHtml()) ?> + + {$jsonHelper->jsonEncode($block->getRemoveLayoutButtonHtml())} + '</div>'+ '</div>'+ '<div class="fieldset-wrapper-content">'+ -<?php foreach ($block->getDisplayOnContainers() as $container) : ?> - '<div class="no-display <?= $block->escapeJs($container['code']) ?> group_container" id="<?= $block->escapeJs($container['name']) ?>_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="layout_handle_pattern" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][layout_handle]" value="<?= $block->escapeJs($container['layout_handle']) ?>" />'+ + +script; +foreach ($block->getDisplayOnContainers() as $container): + $scriptString .= <<<script + '<div class="no-display {$block->escapeJs($container['code'])} group_container" '+ + 'id="{$block->escapeJs($container['name'])}_<%- data.id %>">'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}]" />'+ + '<input disabled="disabled" type="hidden" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="layout_handle_pattern" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][layout_handle]" '+ + 'value="{$block->escapeJs($container['layout_handle'])}" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('%1', $container['label'])) ?></label></th>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('%1', $container['label']))}</label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ '<tr>'+ '<td>'+ - '<input disabled="disabled" type="radio" class="radio for_all" id="all_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][for]" value="all" onclick="WidgetInstance.togglePageGroupChooser(this)" checked="checked" /> '+ - '<label for="all_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>"><?= $block->escapeJs(__('All')) ?></label><br />'+ - '<input disabled="disabled" type="radio" class="radio for_specific" id="specific_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][for]" value="specific" onclick="WidgetInstance.togglePageGroupChooser(this)" /> '+ - '<label for="specific_<?= $block->escapeJs($container['name']) ?>_<%- data.id %>"><?= $block->escapeJs(__('Specific %1', $container['label'])) ?></label>'+ + '<input disabled="disabled" type="radio" class="radio for_all" '+ + 'id="all_{$block->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][for]" '+ + 'value="all" checked="checked" /> '+ + '<label for="all_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$block->escapeJs(__('All'))}</label><br />'+ + '<input disabled="disabled" type="radio" class="radio for_specific" '+ + 'id="specific_{$block->escapeJs($container['name'])}_<%- data.id %>" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][for]" '+ + 'value="specific" /> '+ + '<label for="specific_{$block->escapeJs($container['name'])}_<%- data.id %>">'+ + '{$block->escapeJs(__('Specific %1', $container['label']))}</label>'+ + +script; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + "onclick", + "WidgetInstance.togglePageGroupChooser(this)", + "all_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + "onclick", + "WidgetInstance.togglePageGroupChooser(this)", + "specific_" . $block->escapeJs($container['name']) . "_<%- data.id %>" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + + $scriptString .= <<<script '</td>'+ '<td>'+ '<div class="block_reference_container">'+ @@ -71,33 +111,72 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '</tr>'+ '</tbody>'+ '</table>'+ - '<div class="no-display chooser_container" id="<?= $block->escapeJs($container['name']) ?>_ids_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="is_anchor_only" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][is_anchor_only]" value="<?= $block->escapeJs($container['is_anchor_only']) ?>" />'+ - '<input disabled="disabled" type="hidden" class="product_type_id" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][product_type_id]" value="<?= $block->escapeJs($container['product_type_id']) ?>" />'+ + '<div class="no-display chooser_container" id="{$block->escapeJs($container['name'])}_ids_<%- data.id %>">'+ + '<input disabled="disabled" type="hidden" class="is_anchor_only" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][is_anchor_only]" '+ + 'value="{$block->escapeJs($container['is_anchor_only'])}" />'+ + '<input disabled="disabled" type="hidden" class="product_type_id" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][product_type_id]" '+ + 'value="{$block->escapeJs($container['product_type_id'])}" />'+ '<p>' + - '<input disabled="disabled" type="text" class="input-text entities" name="widget_instance[<%- data.id %>][<?= $block->escapeJs($container['name']) ?>][entities]" value="<%- data.<?= $block->escapeJs($container['name']) ?>_entities %>" readonly="readonly" /> ' + - '<a class="widget-option-chooser" href="javascript:void(0)" onclick="WidgetInstance.displayEntityChooser(\'<?= $block->escapeJs($container['code']) ?>\', \'<?= $block->escapeJs($container['name']) ?>_ids_<%- data.id %>\')" title="<?= $block->escapeJs(__('Open Chooser')) ?>">' + - '<img src="<?= $block->escapeUrl($block->getViewFileUrl('images/rule_chooser_trigger.gif')) ?>" alt="<?= $block->escapeJs(__('Open Chooser')) ?>" />' + + '<input disabled="disabled" type="text" class="input-text entities" '+ + 'name="widget_instance[<%- data.id %>][{$block->escapeJs($container['name'])}][entities]" '+ + 'value="<%- data.{$block->escapeJs($container['name'])}_entities %>" readonly="readonly" /> ' + + '<a class="widget-option-chooser" href="#" '+ + 'title="{$block->escapeJs(__('Open Chooser'))}">' + + '<img src="{$block->escapeJs($block->getViewFileUrl('images/rule_chooser_trigger.gif'))}" '+ + 'alt="{$block->escapeJs(__('Open Chooser'))}" />' + '</a> ' + - '<a href="javascript:void(0)" onclick="WidgetInstance.hideEntityChooser(\'<?= $block->escapeJs($container['name']) ?>_ids_<%- data.id %>\')" title="<?= $block->escapeJs(__('Apply')) ?>">' + - '<img src="<?= $block->escapeUrl($block->getViewFileUrl('images/rule_component_apply.gif')) ?>" alt="<?= $block->escapeJs(__('Apply')) ?>" />' + + '<a id="widget-apply-<%- data.id %>" href="#" '+ + 'title="{$block->escapeJs(__('Apply'))}">' + + '<img src="{$block->escapeJs($block->getViewFileUrl('images/rule_component_apply.gif'))}" '+ + 'alt="{$block->escapeJs(__('Apply'))}" />' + '</a>' + '</p>'+ '<div class="chooser"></div>'+ '</div>'+ + +script; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + "onclick", + "event.preventDefault(); + WidgetInstance.displayEntityChooser('" .$block->escapeJs($container['code']) . + "', '" . $block->escapeJs($container['name']) . "_ids_<%- data.id %>')", + "div#" . $block->escapeJs($container['name']) . "_ids_<%- data.id %> a.widget-option-chooser" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + + $scriptString1 = $secureRenderer->renderEventListenerAsTag( + 'onclick', + "event.preventDefault(); + WidgetInstance.hideEntityChooser('" . $block->escapeJs($container['name']) . "_ids_<%- data.id %>')", + "a#widget-apply-<%- data.id %>" + ); + $scriptString .= "'" . $block->escapeJs($scriptString1) . "'+" . PHP_EOL; + $scriptString .= <<<script + '</div>'+ -<?php endforeach; ?> + +script; +endforeach; +$scriptString .= <<<script + '<div class="no-display all_pages group_container" id="all_pages_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][all_pages]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][all_pages][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="layout_handle_pattern" name="widget_instance[<%- data.id %>][all_pages][layout_handle]" value="default" />'+ - '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][all_pages][for]" value="all" />'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][all_pages]" />'+ + '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][all_pages][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="layout_handle_pattern" '+ + 'name="widget_instance[<%- data.id %>][all_pages][layout_handle]" value="default" />'+ + '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][all_pages][for]" '+ + 'value="all" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '<th> </th>'+ '</tr>'+ '</thead>'+ @@ -119,21 +198,24 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '</table>'+ '</div>'+ '<div class="no-display ignore-validate pages group_container" id="pages_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][pages]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][pages][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][pages][for]" value="all" />'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][pages]" />'+ + '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][pages][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][pages][for]" '+ + 'value="all" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('Page')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ '<tr>'+ - '<td><?= /* @noEscape */ $block->getLayoutsChooser() ?></td>'+ + '<td>{$block->getLayoutsChooser()}</td>'+ '<td>'+ '<div class="block_reference_container">'+ '<div class="block_reference"></div>'+ @@ -150,21 +232,24 @@ var pageGroupTemplate = '<div class="fieldset-wrapper page_group_container" id=" '</table>'+ '</div>'+ '<div class="no-display ignore-validate pages group_container" id="page_layouts_<%- data.id %>">'+ - '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" value="widget_instance[<%- data.id %>][page_layouts]" />'+ - '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][page_layouts][page_id]" value="<%- data.page_id %>" />'+ - '<input disabled="disabled" type="hidden" class="for_all" name="widget_instance[<%- data.id %>][page_layouts][for]" value="all" />'+ + '<input disabled="disabled" type="hidden" class="container_name" name="__[container_name]" '+ + 'value="widget_instance[<%- data.id %>][page_layouts]" />'+ + '<input disabled="disabled" type="hidden" name="widget_instance[<%- data.id %>][page_layouts][page_id]" '+ + 'value="<%- data.page_id %>" />'+ + '<input disabled="disabled" type="hidden" class="for_all" '+ + 'name="widget_instance[<%- data.id %>][page_layouts][for]" value="all" />'+ '<table class="data-table">'+ '<col width="200" />'+ '<thead>'+ '<tr>'+ - '<th><label><?= $block->escapeJs(__('Page')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Container')) ?> <span class="required">*</span></label></th>'+ - '<th><label><?= $block->escapeJs(__('Template')) ?></label></th>'+ + '<th><label>{$block->escapeJs(__('Page'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Container'))} <span class="required">*</span></label></th>'+ + '<th><label>{$block->escapeJs(__('Template'))}</label></th>'+ '</tr>'+ '</thead>'+ '<tbody>'+ '<tr>'+ - '<td><?= /* @noEscape */ $block->getPageLayoutsPageChooser() ?></td>'+ + '<td>{$block->getPageLayoutsPageChooser()}</td>'+ '<td>'+ '<div class="block_reference_container">'+ '<div class="block_reference"></div>'+ @@ -189,7 +274,7 @@ var WidgetInstance = { pageGroupTemplate : pageGroupTemplate, pageGroupContainerId : 'page_group_container', count : 0, - activePageGroups : $H({}), + activePageGroups : \$H({}), selectedItems : {}, addPageGroup : function(data) { @@ -266,7 +351,7 @@ var WidgetInstance = { }, addProductItemToSelection: function(groupId, item) { if (undefined == this.selectedItems[groupId]) { - this.selectedItems[groupId] = $H({}); + this.selectedItems[groupId] = \$H({}); } if (!isNaN(parseInt(item))) { this.selectedItems[groupId].set(item, 1); @@ -327,11 +412,11 @@ var WidgetInstance = { additional = {}; } if (type == 'categories') { - additional.url = '<?= $block->escapeUrl($block->getCategoriesChooserUrl()) ?>'; - additional.post_parameters = $H({'is_anchor_only':$(chooser).down('input.is_anchor_only').value}); + additional.url = '{$block->escapeJs($block->getCategoriesChooserUrl())}'; + additional.post_parameters = \$H({'is_anchor_only':$(chooser).down('input.is_anchor_only').value}); } else if (type == 'products') { - additional.url = '<?= $block->escapeUrl($block->getProductsChooserUrl()) ?>'; - additional.post_parameters = $H({'product_type_id':$(chooser).down('input.product_type_id').value}); + additional.url = '{$block->escapeUrl($block->getProductsChooserUrl())}'; + additional.post_parameters = \$H({'product_type_id':$(chooser).down('input.product_type_id').value}); } if (chooser && additional) { this.displayChooser(chooser, additional); @@ -347,7 +432,7 @@ var WidgetInstance = { displayChooser : function(chooser, additional) { chooser = $(chooser).down('div.chooser'); entities = chooser.up('div.chooser_container').down('input[type="text"].entities').value; - postParameters = $H({selected:entities}); + postParameters = \$H({selected:entities}); url = ''; if (additional) { if (additional.url) url = additional.url; @@ -436,13 +521,13 @@ var WidgetInstance = { selected = ''; parameters = {}; if (type == 'block_reference') { - url = '<?= $block->escapeUrl($block->getBlockChooserUrl()) ?>'; + url = '{$block->escapeJs($block->getBlockChooserUrl())}'; if (additional.selectedBlock) { selected = additional.selectedBlock; } parameters.layout = value; } else if (type == 'block_template') { - url = '<?= $block->escapeUrl($block->getTemplateChooserUrl()) ?>'; + url = '{$block->escapeJs($block->getTemplateChooserUrl())}'; if (additional.selectedTemplate) { selected = additional.selectedTemplate; } @@ -479,9 +564,18 @@ var WidgetInstance = { window.WidgetInstance = WidgetInstance; jQuery(function(){ - <?php foreach ($block->getPageGroups() as $pageGroup) : ?> - WidgetInstance.addPageGroup(<?= /* @noEscape */ $pageGroup ?>); - <?php endforeach; ?> + +script; +foreach ($block->getPageGroups() as $pageGroup): + $scriptString .= <<<script + + WidgetInstance.addPageGroup({$pageGroup}); + +script; +endforeach; + +$scriptString .= <<<script + Event.observe(document, 'product:changed', function(event){ WidgetInstance.checkProduct(event); }); @@ -497,4 +591,7 @@ jQuery(function(){ //]]> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml b/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml index 90aee2baeb4f3..cb82c1dbca75a 100644 --- a/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml +++ b/app/code/Magento/Widget/view/adminhtml/templates/instance/js.phtml @@ -3,8 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ 'jquery', 'mage/template', @@ -27,4 +30,7 @@ setSettings = function(urlTemplate, codeElement, themeElement) { setLocation(url); }; }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Wishlist/Block/AbstractBlock.php b/app/code/Magento/Wishlist/Block/AbstractBlock.php index 981a0da1d241f..5f4a7c8f3814b 100644 --- a/app/code/Magento/Wishlist/Block/AbstractBlock.php +++ b/app/code/Magento/Wishlist/Block/AbstractBlock.php @@ -231,7 +231,7 @@ public function hasDescription($item) * Retrieve formatted Date * * @param string $date - * @deprecated + * @deprecated 101.1.1 * @return string */ public function getFormatedDate($date) diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php index d02f2229401c1..bc94f53a7625a 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist.php @@ -31,6 +31,7 @@ class Wishlist extends \Magento\Wishlist\Block\AbstractBlock /** * @var \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @since 101.1.1 */ protected $_collection; @@ -101,6 +102,7 @@ private function paginateCollection() * Retrieve Wishlist Product Items collection * * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + * @since 101.1.1 */ public function getWishlistItems() { diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php index 40882ae00dae1..db92a10907d39 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Actions.php @@ -13,7 +13,7 @@ * Model for item column in customer wishlist. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Actions extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php index 53f67626e956d..57d182dee4e1c 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Comment.php @@ -13,7 +13,7 @@ * Wishlist block customer item cart column. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Comment extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php index c4c786961694b..f41146051ae96 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Edit.php @@ -13,7 +13,7 @@ * Edit item in customer wishlist table. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Edit extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php index 5595d189b15eb..c578e9d1c5d22 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Image.php @@ -53,6 +53,7 @@ public function __construct( * Identify the product from which thumbnail should be taken. * * @return \Magento\Catalog\Model\Product + * @since 101.0.5 */ public function getProductForThumbnail(\Magento\Wishlist\Model\Item $item) : \Magento\Catalog\Model\Product { diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php index b7eaf53fc23b5..092ede9ceb016 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Info.php @@ -13,7 +13,7 @@ * Wishlist block customer item cart column. * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Info extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php index 09f5014edead6..472cd3cc70f09 100644 --- a/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php +++ b/app/code/Magento/Wishlist/Block/Customer/Wishlist/Item/Column/Remove.php @@ -13,7 +13,7 @@ * Delete item column in customer wishlist table * * @api - * @deprecated + * @deprecated 101.1.2 * @since 100.0.2 */ class Remove extends \Magento\Wishlist\Block\Customer\Wishlist\Item\Column diff --git a/app/code/Magento/Wishlist/Block/Link.php b/app/code/Magento/Wishlist/Block/Link.php index 2d78852f0fd32..c410a1254edee 100644 --- a/app/code/Magento/Wishlist/Block/Link.php +++ b/app/code/Magento/Wishlist/Block/Link.php @@ -75,7 +75,7 @@ public function getLabel() /** * {@inheritdoc} - * @since 100.2.0 + * @since 101.0.0 */ public function getSortOrder() { diff --git a/app/code/Magento/Wishlist/Block/Rss/Link.php b/app/code/Magento/Wishlist/Block/Rss/Link.php index 3e716c863862b..28affa8d3372d 100644 --- a/app/code/Magento/Wishlist/Block/Rss/Link.php +++ b/app/code/Magento/Wishlist/Block/Rss/Link.php @@ -44,6 +44,7 @@ public function __construct( \Magento\Framework\Url\EncoderInterface $urlEncoder, array $data = [] ) { + $data['wishlistHelper'] = $this->wishlistHelper; parent::__construct($context, $data); $this->wishlistHelper = $wishlistHelper; $this->rssUrlBuilder = $rssUrlBuilder; @@ -51,6 +52,8 @@ public function __construct( } /** + * Return link. + * * @return string */ public function getLink() @@ -72,6 +75,8 @@ public function isRssAllowed() } /** + * Return link params. + * * @return array */ protected function getLinkParams() diff --git a/app/code/Magento/Wishlist/Block/Share/Email/Items.php b/app/code/Magento/Wishlist/Block/Share/Email/Items.php index 130c7cb136afb..077f8ce3c4930 100644 --- a/app/code/Magento/Wishlist/Block/Share/Email/Items.php +++ b/app/code/Magento/Wishlist/Block/Share/Email/Items.php @@ -59,6 +59,7 @@ public function __construct( * @param Item $item * * @return Product + * @since 101.2.0 */ public function getProductForThumbnail(Item $item): Product { diff --git a/app/code/Magento/Wishlist/Controller/Index/Cart.php b/app/code/Magento/Wishlist/Controller/Index/Cart.php index 023d0756bae6f..7e47a6b9d7d8a 100644 --- a/app/code/Magento/Wishlist/Controller/Index/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Index/Cart.php @@ -220,12 +220,13 @@ public function execute() $wishlist->save(); if (!$this->cart->getQuote()->getHasError()) { - $message = __( - 'You added %1 to your shopping cart.', - $this->escaper->escapeHtml($item->getProduct()->getName()) + $this->messageManager->addComplexSuccessMessage( + 'addCartSuccessMessage', + [ + 'product_name' => $item->getProduct()->getName(), + 'cart_url' => $this->cartHelper->getCartUrl() + ] ); - $this->messageManager->addSuccessMessage($message); - $productsToAdd = [ [ 'sku' => $item->getProduct()->getSku(), diff --git a/app/code/Magento/Wishlist/Controller/Shared/Cart.php b/app/code/Magento/Wishlist/Controller/Shared/Cart.php index 38f100602972a..c0a394ce9d762 100644 --- a/app/code/Magento/Wishlist/Controller/Shared/Cart.php +++ b/app/code/Magento/Wishlist/Controller/Shared/Cart.php @@ -3,13 +3,17 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Wishlist\Controller\Shared; use Magento\Catalog\Model\Product\Exception as ProductException; use Magento\Checkout\Helper\Cart as CartHelper; use Magento\Checkout\Model\Cart as CustomerCart; +use Magento\Framework\App\Action\Action; use Magento\Framework\App\Action\Context as ActionContext; -use Magento\Framework\App\Action\HttpGetActionInterface; +use Magento\Framework\App\Action\HttpPostActionInterface; +use Magento\Framework\Controller\Result\Redirect; use Magento\Framework\Controller\ResultFactory; use Magento\Framework\Escaper; use Magento\Framework\Exception\LocalizedException; @@ -23,7 +27,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class Cart extends \Magento\Framework\App\Action\Action implements HttpGetActionInterface +class Cart extends Action implements HttpPostActionInterface { /** * @var CustomerCart @@ -80,7 +84,7 @@ public function __construct( * If Product has required options - redirect * to product view page with message about needed defined required options * - * @return \Magento\Framework\Controller\Result\Redirect + * @return Redirect */ public function execute() { @@ -120,7 +124,7 @@ public function execute() } catch (\Exception $e) { $this->messageManager->addExceptionMessage($e, __('We can\'t add the item to the cart right now.')); } - /** @var \Magento\Framework\Controller\Result\Redirect $resultRedirect */ + /** @var Redirect $resultRedirect */ $resultRedirect = $this->resultFactory->create(ResultFactory::TYPE_REDIRECT); $resultRedirect->setUrl($redirectUrl); return $resultRedirect; diff --git a/app/code/Magento/Wishlist/Model/Adminhtml/ResourceModel/Item/Product/CollectionBuilder.php b/app/code/Magento/Wishlist/Model/Adminhtml/ResourceModel/Item/Product/CollectionBuilder.php new file mode 100644 index 0000000000000..aa54c17c243b0 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Adminhtml/ResourceModel/Item/Product/CollectionBuilder.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Adminhtml\ResourceModel\Item\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\Collection as WishlistItemCollection; +use Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface; + +/** + * Wishlist items products collection builder for adminhtml area + */ +class CollectionBuilder implements CollectionBuilderInterface +{ + /** + * @inheritDoc + */ + public function build(WishlistItemCollection $wishlistItemCollection, Collection $productCollection): Collection + { + return $productCollection; + } +} diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php index 61d444f786ca8..5d9b1911bc292 100644 --- a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Collection.php @@ -11,6 +11,7 @@ use Magento\Framework\App\ObjectManager; use Magento\Framework\EntityManager\MetadataPool; use Magento\Sales\Model\ConfigInterface; +use Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface; /** * Wishlist item collection @@ -157,6 +158,10 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @var ConfigInterface */ private $salesConfig; + /** + * @var CollectionBuilderInterface + */ + private $productCollectionBuilder; /** * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory @@ -178,8 +183,8 @@ class Collection extends \Magento\Framework\Model\ResourceModel\Db\Collection\Ab * @param \Magento\Framework\App\State $appState * @param \Magento\Framework\DB\Adapter\AdapterInterface $connection * @param TableMaintainer|null $tableMaintainer - * @param ConfigInterface|null $salesConfig - * + * @param ConfigInterface|null $salesConfig + * @param CollectionBuilderInterface|null $productCollectionBuilder * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -202,7 +207,8 @@ public function __construct( \Magento\Framework\App\State $appState, \Magento\Framework\DB\Adapter\AdapterInterface $connection = null, TableMaintainer $tableMaintainer = null, - ConfigInterface $salesConfig = null + ConfigInterface $salesConfig = null, + ?CollectionBuilderInterface $productCollectionBuilder = null ) { $this->stockConfiguration = $stockConfiguration; $this->_adminhtmlSales = $adminhtmlSales; @@ -219,6 +225,8 @@ public function __construct( parent::__construct($entityFactory, $logger, $fetchStrategy, $eventManager, $connection, $resource); $this->tableMaintainer = $tableMaintainer ?: ObjectManager::getInstance()->get(TableMaintainer::class); $this->salesConfig = $salesConfig ?: ObjectManager::getInstance()->get(ConfigInterface::class); + $this->productCollectionBuilder = $productCollectionBuilder + ?: ObjectManager::getInstance()->get(CollectionBuilderInterface::class); } /** @@ -309,12 +317,10 @@ protected function _assignProducts() $productCollection->setVisibility($this->_productVisibility->getVisibleInSiteIds()); } - $productCollection->addPriceData() - ->addTaxPercents() - ->addIdFilter($this->_productIds) - ->addAttributeToSelect($this->_wishlistConfig->getProductAttributes()) - ->addOptionsToResult() - ->addUrlRewrite(); + $productCollection->addIdFilter($this->_productIds) + ->addAttributeToSelect($this->_wishlistConfig->getProductAttributes()); + + $productCollection = $this->productCollectionBuilder->build($this, $productCollection); if ($this->_productSalable) { $productCollection = $this->_adminhtmlSales->applySalableProductTypesFilter($productCollection); @@ -352,6 +358,7 @@ protected function _assignProducts() /** * @inheritdoc + * @since 101.1.3 */ protected function _renderFiltersBefore() { @@ -575,10 +582,15 @@ protected function _joinProductNameTable() $storeId = $this->_storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId(); $entityMetadata = $this->getMetadataPool()->getMetadata(ProductInterface::class); + $linkField = $entityMetadata->getLinkField(); $this->getSelect()->join( + ['product_entity' => $this->getTable('catalog_product_entity')], + 'product_entity.entity_id = main_table.product_id', + [] + )->join( ['product_name_table' => $attribute->getBackendTable()], - 'product_name_table.' . $entityMetadata->getLinkField() . ' = main_table.product_id' . + 'product_name_table.' . $linkField . ' = product_entity.' . $linkField . ' AND product_name_table.store_id = ' . $storeId . ' AND product_name_table.attribute_id = ' . @@ -588,6 +600,7 @@ protected function _joinProductNameTable() $this->_isProductNameJoined = true; } + return $this; } diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilder.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilder.php new file mode 100644 index 0000000000000..05255d1fe39f7 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilder.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\ResourceModel\Item\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\Collection as WishlistItemCollection; + +/** + * Wishlist items products collection builder + */ +class CollectionBuilder implements CollectionBuilderInterface +{ + /** + * @inheritDoc + */ + public function build(WishlistItemCollection $wishlistItemCollection, Collection $productCollection): Collection + { + return $productCollection->addPriceData() + ->addTaxPercents() + ->addOptionsToResult() + ->addUrlRewrite(); + } +} diff --git a/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilderInterface.php b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilderInterface.php new file mode 100644 index 0000000000000..1984d92e08a60 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/ResourceModel/Item/Product/CollectionBuilderInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\ResourceModel\Item\Product; + +use Magento\Catalog\Model\ResourceModel\Product\Collection; +use Magento\Wishlist\Model\ResourceModel\Item\Collection as WishlistItemCollection; + +/** + * Wishlist items products collection builder + */ +interface CollectionBuilderInterface +{ + /** + * Modify product collection + * + * @param WishlistItemCollection $wishlistItemCollection + * @param Collection $productCollection + * @return Collection + */ + public function build(WishlistItemCollection $wishlistItemCollection, Collection $productCollection): Collection; +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist.php b/app/code/Magento/Wishlist/Model/Wishlist.php index 9b7ff5177afae..437b3c757f9cf 100644 --- a/app/code/Magento/Wishlist/Model/Wishlist.php +++ b/app/code/Magento/Wishlist/Model/Wishlist.php @@ -181,6 +181,7 @@ class Wishlist extends AbstractModel implements IdentityInterface * @param Json|null $serializer * @param StockRegistryInterface|null $stockRegistry * @param ScopeConfigInterface|null $scopeConfig + * * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -226,6 +227,7 @@ public function __construct( * * @param int $customerId * @param bool $create Create wishlist if don't exists + * * @return $this */ public function loadByCustomerId($customerId, $create = false) @@ -274,6 +276,7 @@ public function generateSharingCode() * Load by sharing code * * @param string $code + * * @return $this */ public function loadByCode($code) @@ -370,6 +373,7 @@ protected function _addCatalogProduct(Product $product, $qty = 1, $forciblySetQt * Retrieve wishlist item collection * * @return \Magento\Wishlist\Model\ResourceModel\Item\Collection + * * @throws NoSuchEntityException */ public function getItemCollection() @@ -379,7 +383,7 @@ public function getItemCollection() $this )->addStoreFilter( $this->getSharedStoreIds() - )->setVisibilityFilter(); + )->setVisibilityFilter($this->_useCurrentWebsite); } return $this->_itemCollection; @@ -389,6 +393,7 @@ public function getItemCollection() * Retrieve wishlist item collection * * @param int $itemId + * * @return false|Item */ public function getItem($itemId) @@ -403,7 +408,9 @@ public function getItem($itemId) * Adding item to wishlist * * @param Item $item + * * @return $this + * * @throws Exception */ public function addItem(Item $item) @@ -424,9 +431,12 @@ public function addItem(Item $item) * @param int|Product $product * @param DataObject|array|string|null $buyRequest * @param bool $forciblySetQty + * * @return Item|string + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) + * * @throws LocalizedException * @throws InvalidArgumentException */ @@ -465,6 +475,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false $_buyRequest = $buyRequest; } elseif (is_string($buyRequest)) { $isInvalidItemConfiguration = false; + $buyRequestData = []; try { $buyRequestData = $this->serializer->unserialize($buyRequest); if (!is_array($buyRequestData)) { @@ -482,6 +493,9 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false } else { $_buyRequest = new DataObject(); } + if ($_buyRequest->getData('action') !== 'updateItem') { + $_buyRequest->setData('action', 'add'); + } /* @var $product Product */ $cartCandidates = $product->getTypeInstance()->processConfiguration($_buyRequest, clone $product); @@ -502,6 +516,7 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false $errors = []; $items = []; + $item = null; foreach ($cartCandidates as $candidate) { if ($candidate->getParentProductId()) { @@ -529,7 +544,9 @@ public function addNewItem($product, $buyRequest = null, $forciblySetQty = false * Set customer id * * @param int $customerId + * * @return $this + * * @throws LocalizedException */ public function setCustomerId($customerId) @@ -541,6 +558,7 @@ public function setCustomerId($customerId) * Retrieve customer id * * @return int + * * @throws LocalizedException */ public function getCustomerId() @@ -552,6 +570,7 @@ public function getCustomerId() * Retrieve data for save * * @return array + * * @throws LocalizedException */ public function getDataForSave() @@ -567,6 +586,7 @@ public function getDataForSave() * Retrieve shared store ids for current website or all stores if $current is false * * @return array + * * @throws NoSuchEntityException */ public function getSharedStoreIds() @@ -590,6 +610,7 @@ public function getSharedStoreIds() * Set shared store ids * * @param array $storeIds + * * @return $this */ public function setSharedStoreIds($storeIds) @@ -602,6 +623,7 @@ public function setSharedStoreIds($storeIds) * Retrieve wishlist store object * * @return \Magento\Store\Model\Store + * * @throws NoSuchEntityException */ public function getStore() @@ -616,6 +638,7 @@ public function getStore() * Set wishlist store * * @param Store $store + * * @return $this */ public function setStore($store) @@ -653,6 +676,7 @@ public function isSalable() * Retrieve if product has stock or config is set for showing out of stock products * * @param int $productId + * * @return bool */ private function isInStock($productId) @@ -671,7 +695,9 @@ private function isInStock($productId) * Check customer is owner this wishlist * * @param int $customerId + * * @return bool + * * @throws LocalizedException */ public function isOwner($customerId) @@ -696,10 +722,13 @@ public function isOwner($customerId) * @param int|Item $itemId * @param DataObject $buyRequest * @param null|array|DataObject $params + * * @return $this + * * @throws LocalizedException * * @see \Magento\Catalog\Helper\Product::addParamsToBuyRequest() + * * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) */ @@ -726,15 +755,16 @@ public function updateItem($itemId, $buyRequest, $params = null) } $params->setCurrentConfig($item->getBuyRequest()); $buyRequest = $this->_catalogProduct->addParamsToBuyRequest($buyRequest, $params); + $buyRequest->setData('action', 'updateItem'); $product->setWishlistStoreId($item->getStoreId()); $items = $this->getItemCollection(); $isForceSetQuantity = true; - foreach ($items as $_item) { - /* @var $_item Item */ - if ($_item->getProductId() == $product->getId() && $_item->representProduct( - $product - ) && $_item->getId() != $item->getId() + foreach ($items as $wishlistItem) { + /* @var $wishlistItem Item */ + if ($wishlistItem->getProductId() == $product->getId() + && $wishlistItem->getId() != $item->getId() + && $wishlistItem->representProduct($product) ) { // We do not add new wishlist item, but updating the existing one $isForceSetQuantity = false; @@ -748,10 +778,11 @@ public function updateItem($itemId, $buyRequest, $params = null) throw new LocalizedException(__($resultItem)); } + if ($resultItem->getDescription() != $item->getDescription()) { + $resultItem->setDescription($item->getDescription())->save(); + } + if ($resultItem->getId() != $itemId) { - if ($resultItem->getDescription() != $item->getDescription()) { - $resultItem->setDescription($item->getDescription())->save(); - } $item->isDeleted(true); $this->setDataChanges(true); } else { diff --git a/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php new file mode 100644 index 0000000000000..7acfb503a5ad0 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/AddProductsToWishlist.php @@ -0,0 +1,164 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AlreadyExistsException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Adding products to wishlist + */ +class AddProductsToWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param ProductRepositoryInterface $productRepository + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistResourceModel $wishlistResource + */ + public function __construct( + ProductRepositoryInterface $productRepository, + BuyRequestBuilder $buyRequestBuilder, + WishlistResourceModel $wishlistResource + ) { + $this->productRepository = $productRepository; + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistResource = $wishlistResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + * + * @throws AlreadyExistsException + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->addItemToWishlist($wishlist, $wishlistItem); + } + + $wishlistOutput = $this->prepareOutput($wishlist); + + if ($wishlist->isObjectNew() || count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return $wishlistOutput; + } + + /** + * Add product item to wishlist + * + * @param Wishlist $wishlist + * @param WishlistItem $wishlistItem + * + * @return void + */ + private function addItemToWishlist(Wishlist $wishlist, WishlistItem $wishlistItem): void + { + $sku = $wishlistItem->getParentSku() ?? $wishlistItem->getSku(); + + try { + $product = $this->productRepository->get($sku, false, null, true); + } catch (NoSuchEntityException $e) { + $this->addError( + __('Could not find a product with SKU "%sku"', ['sku' => $sku])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + + return; + } + + try { + $options = $this->buyRequestBuilder->build($wishlistItem, (int) $product->getId()); + $result = $wishlist->addNewItem($product, $options); + + if (is_string($result)) { + $this->addError($result); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } catch (\Throwable $e) { + $this->addError( + __( + 'Could not add the product with SKU "%sku" to the wishlist:: %message', + ['sku' => $sku, 'message' => $e->getMessage()] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php new file mode 100644 index 0000000000000..1cfa316c3cd01 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BundleDataProvider.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for bundle product buy requests + */ +class BundleDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'bundle'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $bundleOptionsData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $optionId, $optionValueId, $optionQuantity] = $optionData; + + $bundleOptionsData['bundle_option'][$optionId] = $optionValueId; + $bundleOptionsData['bundle_option_qty'][$optionId] = $optionQuantity; + } + + return $bundleOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php new file mode 100644 index 0000000000000..1f7ddce345b1c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestBuilder.php @@ -0,0 +1,63 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Framework\DataObject; +use Magento\Framework\DataObjectFactory; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Building buy request for all product types + */ +class BuyRequestBuilder +{ + /** + * @var BuyRequestDataProviderInterface[] + */ + private $providers; + + /** + * @var DataObjectFactory + */ + private $dataObjectFactory; + + /** + * @param DataObjectFactory $dataObjectFactory + * @param array $providers + */ + public function __construct( + DataObjectFactory $dataObjectFactory, + array $providers = [] + ) { + $this->dataObjectFactory = $dataObjectFactory; + $this->providers = $providers; + } + + /** + * Build product buy request for adding to wishlist + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return DataObject + */ + public function build(WishlistItem $wishlistItemData, ?int $productId = null): DataObject + { + $requestData = [ + [ + 'qty' => $wishlistItemData->getQuantity(), + ] + ]; + + foreach ($this->providers as $provider) { + $requestData[] = $provider->execute($wishlistItemData, $productId); + } + + return $this->dataObjectFactory->create(['data' => array_merge(...$requestData)]); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php new file mode 100644 index 0000000000000..fac45d7f86c7c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/BuyRequestDataProviderInterface.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Build buy request for adding products to wishlist + */ +interface BuyRequestDataProviderInterface +{ + /** + * Provide buy request data from add to wishlist item request + * + * @param WishlistItem $wishlistItemData + * @param int|null $productId + * + * @return array + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array; +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php new file mode 100644 index 0000000000000..8bf12206336a8 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/CustomizableOptionDataProvider.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for custom options buy requests + */ +class CustomizableOptionDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'custom-option'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $customizableOptionsData = []; + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [$optionType, $optionId, $optionValue] = $optionData; + + if ($optionType == self::PROVIDER_OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $optionValue; + } + } + + foreach ($wishlistItemData->getEnteredOptions() as $option) { + $optionData = \explode('/', base64_decode($option->getUid())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [$optionType, $optionId] = $optionData; + + if ($optionType == self::PROVIDER_OPTION_TYPE) { + $customizableOptionsData[$optionId][] = $option->getValue(); + } + } + + if (empty($customizableOptionsData)) { + return $customizableOptionsData; + } + + $result = ['options' => $this->flattenOptionValues($customizableOptionsData)]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Flatten option values for non-multiselect customizable options + * + * @param array $customizableOptionsData + * + * @return array + */ + private function flattenOptionValues(array $customizableOptionsData): array + { + foreach ($customizableOptionsData as $optionId => $optionValue) { + if (count($optionValue) === 1) { + $customizableOptionsData[$optionId] = $optionValue[0]; + } + } + + return $customizableOptionsData; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php new file mode 100644 index 0000000000000..1ad1a0b64706a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/DownloadableLinkDataProvider.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for downloadable product buy requests + */ +class DownloadableLinkDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'downloadable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItem, ?int $productId): array + { + $linksData = []; + + foreach ($wishlistItem->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $linkId] = $optionData; + + $linksData[] = $linkId; + } + + return $linksData ? ['links' => $linksData] : []; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php new file mode 100644 index 0000000000000..01e29bcf80c0b --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperAttributeDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for configurable product buy requests + */ +class SuperAttributeDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'configurable'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $configurableData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $attributeId, $valueIndex] = $optionData; + + $configurableData[$attributeId] = $valueIndex; + } + + if (empty($configurableData)) { + return $configurableData; + } + + $result = ['super_attribute' => $configurableData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php new file mode 100644 index 0000000000000..a11f631f1f5aa --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/BuyRequest/SuperGroupDataProvider.php @@ -0,0 +1,64 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\BuyRequest; + +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem; + +/** + * Data provider for grouped product buy requests + */ +class SuperGroupDataProvider implements BuyRequestDataProviderInterface +{ + private const PROVIDER_OPTION_TYPE = 'grouped'; + + /** + * @inheritdoc + * + * @phpcs:disable Magento2.Functions.DiscouragedFunction + */ + public function execute(WishlistItem $wishlistItemData, ?int $productId): array + { + $groupedData = []; + + foreach ($wishlistItemData->getSelectedOptions() as $optionData) { + $optionData = \explode('/', base64_decode($optionData->getId())); + + if ($this->isProviderApplicable($optionData) === false) { + continue; + } + + [, $simpleProductId, $quantity] = $optionData; + + $groupedData[$simpleProductId] = $quantity; + } + + if (empty($groupedData)) { + return $groupedData; + } + + $result = ['super_group' => $groupedData]; + + if ($productId) { + $result += ['product' => $productId]; + } + + return $result; + } + + /** + * Checks whether this provider is applicable for the current option + * + * @param array $optionData + * + * @return bool + */ + private function isProviderApplicable(array $optionData): bool + { + return $optionData[0] === self::PROVIDER_OPTION_TYPE; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Config.php b/app/code/Magento/Wishlist/Model/Wishlist/Config.php new file mode 100644 index 0000000000000..041e9f1ca0a21 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Config.php @@ -0,0 +1,46 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; + +/** + * Provides wishlist configuration + */ +class Config +{ + const XML_PATH_WISHLIST_ACTIVE = 'wishlist/general/active'; + + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @param ScopeConfigInterface $scopeConfig + */ + public function __construct( + ScopeConfigInterface $scopeConfig + ) { + $this->scopeConfig = $scopeConfig; + } + + /** + * Check whether the wishlist is enabled or not + * + * @return bool + */ + public function isEnabled(): bool + { + return $this->scopeConfig->isSetFlag( + self::XML_PATH_WISHLIST_ACTIVE, + ScopeInterface::SCOPE_STORES + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php new file mode 100644 index 0000000000000..edbf84781da38 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/EnteredOption.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents entered options + */ +class EnteredOption +{ + /** + * @var string + */ + private $uid; + + /** + * @var string + */ + private $value; + + /** + * @param string $uid + * @param string $value + */ + public function __construct(string $uid, string $value) + { + $this->uid = $uid; + $this->value = $value; + } + + /** + * Get entered option id + * + * @return string + */ + public function getUid(): string + { + return $this->uid; + } + + /** + * Get entered option value + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php new file mode 100644 index 0000000000000..cb8420169fa8a --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/Error.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents error item + */ +class Error +{ + /** + * @var string + */ + private $message; + + /** + * @var string + */ + private $code; + + /** + * @param string $message + * @param string $code + */ + public function __construct(string $message, string $code) + { + $this->message = $message; + $this->code = $code; + } + + /** + * Get error message + * + * @return string + */ + public function getMessage(): string + { + return $this->message; + } + + /** + * Get error code + * + * @return string + */ + public function getCode(): string + { + return $this->code; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php new file mode 100644 index 0000000000000..129a61c0a2a6c --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/SelectedOption.php @@ -0,0 +1,37 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents selected option + */ +class SelectedOption +{ + /** + * @var string + */ + private $id; + + /** + * @param string $id + */ + public function __construct(string $id) + { + $this->id = $id; + } + + /** + * Get selected option id + * + * @return string + */ + public function getId(): string + { + return $this->id; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php new file mode 100644 index 0000000000000..236b7f1eee72d --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItem.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +/** + * DTO represents Wishlist Item data + */ +class WishlistItem +{ + /** + * @var float + */ + private $quantity; + + /** + * @var string|null + */ + private $sku; + + /** + * @var string + */ + private $parentSku; + + /** + * @var int|null + */ + private $id; + + /** + * @var string|null + */ + private $description; + + /** + * @var SelectedOption[] + */ + private $selectedOptions; + + /** + * @var EnteredOption[] + */ + private $enteredOptions; + + /** + * @param float $quantity + * @param string|null $sku + * @param string|null $parentSku + * @param int|null $id + * @param string|null $description + * @param array|null $selectedOptions + * @param array|null $enteredOptions + */ + public function __construct( + float $quantity, + string $sku = null, + string $parentSku = null, + int $id = null, + string $description = null, + array $selectedOptions = null, + array $enteredOptions = null + ) { + $this->quantity = $quantity; + $this->sku = $sku; + $this->parentSku = $parentSku; + $this->id = $id; + $this->description = $description; + $this->selectedOptions = $selectedOptions; + $this->enteredOptions = $enteredOptions; + } + + /** + * Get wishlist item id + * + * @return int|null + */ + public function getId(): ?int + { + return $this->id; + } + + /** + * Get wishlist item description + * + * @return string|null + */ + public function getDescription(): ?string + { + return $this->description; + } + + /** + * Get sku + * + * @return string|null + */ + public function getSku(): ?string + { + return $this->sku; + } + + /** + * Get quantity + * + * @return float + */ + public function getQuantity(): float + { + return $this->quantity; + } + + /** + * Get parent sku + * + * @return string|null + */ + public function getParentSku(): ?string + { + return $this->parentSku; + } + + /** + * Get selected options + * + * @return SelectedOption[]|null + */ + public function getSelectedOptions(): ?array + { + return $this->selectedOptions; + } + + /** + * Get entered options + * + * @return EnteredOption[]|null + */ + public function getEnteredOptions(): ?array + { + return $this->enteredOptions; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php new file mode 100644 index 0000000000000..aef3cbf571ff6 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistItemFactory.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Framework\Exception\InputException; + +/** + * Create WishlistItem DTO + */ +class WishlistItemFactory +{ + /** + * Create wishlist item DTO + * + * @param array $data + * + * @return WishlistItem + */ + public function create(array $data): WishlistItem + { + return new WishlistItem( + $data['quantity'], + $data['sku'] ?? null, + $data['parent_sku'] ?? null, + isset($data['wishlist_item_id']) ? (int) $data['wishlist_item_id'] : null, + $data['description'] ?? null, + isset($data['selected_options']) ? $this->createSelectedOptions($data['selected_options']) : [], + isset($data['entered_options']) ? $this->createEnteredOptions($data['entered_options']) : [] + ); + } + + /** + * Create array of Entered Options + * + * @param array $options + * + * @return EnteredOption[] + */ + private function createEnteredOptions(array $options): array + { + return \array_map( + function (array $option) { + if (!isset($option['uid'], $option['value'])) { + throw new InputException( + __('Required fields are not present EnteredOption.uid, EnteredOption.value') + ); + } + return new EnteredOption($option['uid'], $option['value']); + }, + $options + ); + } + + /** + * Create array of Selected Options + * + * @param string[] $options + * + * @return SelectedOption[] + */ + private function createSelectedOptions(array $options): array + { + return \array_map( + function ($option) { + return new SelectedOption($option); + }, + $options + ); + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php new file mode 100644 index 0000000000000..fc7db9ec910fb --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/Data/WishlistOutput.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist\Data; + +use Magento\Wishlist\Model\Wishlist; + +/** + * DTO represent output for \Magento\WishlistGraphQl\Model\Resolver\AddProductsToWishlistResolver + */ +class WishlistOutput +{ + /** + * @var Wishlist + */ + private $wishlist; + + /** + * @var Error[] + */ + private $errors; + + /** + * @param Wishlist $wishlist + * @param Error[] $errors + */ + public function __construct(Wishlist $wishlist, array $errors) + { + $this->wishlist = $wishlist; + $this->errors = $errors; + } + + /** + * Get Wishlist + * + * @return Wishlist + */ + public function getWishlist(): Wishlist + { + return $this->wishlist; + } + + /** + * Get errors happened during adding products to wishlist + * + * @return Error[] + */ + public function getErrors(): array + { + return $this->errors; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php new file mode 100644 index 0000000000000..d143830064752 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/RemoveProductsFromWishlist.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Remove product items from wishlist + */ +class RemoveProductsFromWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_PRODUCT_NOT_FOUND = 'PRODUCT_NOT_FOUND'; + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Removing items from wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItemsIds + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItemsIds): WishlistOutput + { + foreach ($wishlistItemsIds as $wishlistItemId) { + $this->removeItemFromWishlist((int) $wishlistItemId); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Remove product item from wishlist + * + * @param int $wishlistItemId + * + * @return void + */ + private function removeItemFromWishlist(int $wishlistItemId): void + { + try { + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemId); + if (!$wishlistItem->getId()) { + $this->addError( + __('Could not find a wishlist item with ID "%id"', ['id' => $wishlistItemId])->render(), + self::ERROR_PRODUCT_NOT_FOUND + ); + } + + $this->wishlistItemResource->delete($wishlistItem); + } catch (\Exception $e) { + $this->addError( + __( + 'We can\'t delete the item with ID "%id" from the Wish List right now.', + ['id' => $wishlistItemId] + )->render() + ); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php new file mode 100644 index 0000000000000..4abcada138362 --- /dev/null +++ b/app/code/Magento/Wishlist/Model/Wishlist/UpdateProductsInWishlist.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Wishlist\Model\Wishlist; + +use Magento\Framework\Exception\LocalizedException; +use Magento\Wishlist\Model\Item as WishlistItem; +use Magento\Wishlist\Model\ItemFactory as WishlistItemFactory; +use Magento\Wishlist\Model\ResourceModel\Item as WishlistItemResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItem as WishlistItemData; +use Magento\Wishlist\Model\Wishlist\Data\WishlistOutput; + +/** + * Updating product items in wishlist + */ +class UpdateProductsInWishlist +{ + /**#@+ + * Error message codes + */ + private const ERROR_UNDEFINED = 'UNDEFINED'; + /**#@-*/ + + /** + * @var array + */ + private $errors = []; + + /** + * @var BuyRequestBuilder + */ + private $buyRequestBuilder; + + /** + * @var WishlistItemFactory + */ + private $wishlistItemFactory; + + /** + * @var WishlistItemResource + */ + private $wishlistItemResource; + + /** + * @param BuyRequestBuilder $buyRequestBuilder + * @param WishlistItemFactory $wishlistItemFactory + * @param WishlistItemResource $wishlistItemResource + */ + public function __construct( + BuyRequestBuilder $buyRequestBuilder, + WishlistItemFactory $wishlistItemFactory, + WishlistItemResource $wishlistItemResource + ) { + $this->buyRequestBuilder = $buyRequestBuilder; + $this->wishlistItemFactory = $wishlistItemFactory; + $this->wishlistItemResource = $wishlistItemResource; + } + + /** + * Adding products to wishlist + * + * @param Wishlist $wishlist + * @param array $wishlistItems + * + * @return WishlistOutput + */ + public function execute(Wishlist $wishlist, array $wishlistItems): WishlistOutput + { + foreach ($wishlistItems as $wishlistItem) { + $this->updateItemInWishlist($wishlist, $wishlistItem); + } + + return $this->prepareOutput($wishlist); + } + + /** + * Update product item in wishlist + * + * @param Wishlist $wishlist + * @param WishlistItemData $wishlistItemData + * + * @return void + */ + private function updateItemInWishlist(Wishlist $wishlist, WishlistItemData $wishlistItemData): void + { + try { + $options = $this->buyRequestBuilder->build($wishlistItemData); + /** @var WishlistItem $wishlistItem */ + $wishlistItem = $this->wishlistItemFactory->create(); + $this->wishlistItemResource->load($wishlistItem, $wishlistItemData->getId()); + $wishlistItem->setDescription($wishlistItemData->getDescription()); + $resultItem = $wishlist->updateItem($wishlistItem, $options); + + if (is_string($resultItem)) { + $this->addError($resultItem); + } + } catch (LocalizedException $exception) { + $this->addError($exception->getMessage()); + } + } + + /** + * Add wishlist line item error + * + * @param string $message + * @param string|null $code + * + * @return void + */ + private function addError(string $message, string $code = null): void + { + $this->errors[] = new Data\Error( + $message, + $code ?? self::ERROR_UNDEFINED + ); + } + + /** + * Prepare output + * + * @param Wishlist $wishlist + * + * @return WishlistOutput + */ + private function prepareOutput(Wishlist $wishlist): WishlistOutput + { + $output = new WishlistOutput($wishlist, $this->errors); + $this->errors = []; + + return $output; + } +} diff --git a/app/code/Magento/Wishlist/Observer/AddToCart.php b/app/code/Magento/Wishlist/Observer/AddToCart.php index 1ab24d87efbf7..e31a8993670c6 100644 --- a/app/code/Magento/Wishlist/Observer/AddToCart.php +++ b/app/code/Magento/Wishlist/Observer/AddToCart.php @@ -15,7 +15,7 @@ /** * Class AddToCart - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @package Magento\Wishlist\Observer */ class AddToCart implements ObserverInterface diff --git a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml index a28a80c57fb67..512ffdd9ba442 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/ActionGroup/StorefrontCustomerAddProductToCartFromWishlistUsingSidebarActionGroup.xml @@ -19,5 +19,6 @@ <click selector="{{StorefrontCustomerWishlistSidebarSection.ProductAddToCartByName(product.name)}}" stepKey="AddProductToCartFromWishlistUsingSidebarClickAddToCartFromWishlist"/> <waitForElement selector="{{StorefrontCategoryMainSection.SuccessMsg}}" time="30" stepKey="AddProductToCartFromWishlistUsingSidebarWaitForSuccessMessage"/> <see selector="{{StorefrontCategoryMainSection.SuccessMsg}}" userInput="You added {{product.name}} to your shopping cart." stepKey="AddProductToCartFromWishlistUsingSidebarSeeProductNameAddedToCartFromWishlist"/> + <seeLink userInput="shopping cart" stepKey="seeLinkInSuccessMsg"/> </actionGroup> </actionGroups> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml new file mode 100644 index 0000000000000..28a17d30aea2b --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/AdminDeleteCustomerWishListItemTest.xml @@ -0,0 +1,67 @@ +<?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="AdminDeleteCustomerWishlistItemTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Wishlist items deleting"/> + <title value="Admin deletes an item from customer wishlist"/> + <description value="Admin Should be able delete items from customer wishlist"/> + <severity value="AVERAGE"/> + <testCaseId value="MC-35170"/> + <group value="wishlist"/> + </annotations> + <before> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenProductFromCategoryPageActionGroup" stepKey="openProductFromCategory"> + <argument name="category" value="$createCategory$"/> + <argument name="product" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logout"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogoutBeforeCheck"/> + <actionGroup ref="AdminLoginActionGroup" stepKey="loginAsAdmin"/> + <actionGroup ref="OpenEditCustomerFromAdminActionGroup" stepKey="navigateToCustomerEditPage"> + <argument name="customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="AdminNavigateCustomerWishlistTabActionGroup" stepKey="navigateToWishlistTab"/> + <actionGroup ref="AdminCustomerFindWishlistItemActionGroup" stepKey="findWishlistItem"> + <argument name="productName" value="$$createProduct.name$$"/> + </actionGroup> + <actionGroup ref="AdminCustomerDeleteWishlistItemActionGroup" stepKey="deleteItem"/> + <actionGroup ref="AssertAdminCustomerNoItemsInWishlistActionGroup" stepKey="assertNoItems"/> + <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginOnStoreFront"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="NavigateThroughCustomerTabsActionGroup" stepKey="navigateToWishlist"> + <argument name="navigationItemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerWishlistIsEmptyActionGroup" stepKey="assertNoItemsInWishlist"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml index e3c4baf8aa813..6a0603fcee502 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/ConfigurableProductChildImageShouldBeShownOnWishListTest.xml @@ -34,8 +34,7 @@ <deleteData createDataKey="customer" stepKey="deleteCustomer"/> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> </after> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ResetProductGridToDefaultViewActionGroup" stepKey="resetFiltersIfPresent"/> <actionGroup ref="SearchProductGridByKeywordActionGroup" stepKey="searchProductGrid"> <argument name="keyword" value="_defaultProduct.name"/> @@ -46,8 +45,7 @@ <argument name="image" value="MagentoLogo"/> </actionGroup> <actionGroup ref="SaveProductFormActionGroup" stepKey="saveProduct"/> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex1"/> - <waitForPageLoad stepKey="waitForProductIndexPageLoad1"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex1"/> <click selector="{{AdminProductGridSection.selectRowBasedOnName(colorProductAttribute1.name)}}" stepKey="selectProductToAddImage1"/> <waitForPageLoad stepKey="waitForProductEditPageLoad1"/> <actionGroup ref="AddProductImageActionGroup" stepKey="addImageForChildProduct"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml index eed4dc8d4767e..aafd8b0b0d4d3 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddMultipleStoreProductsToWishlistTest.xml @@ -45,7 +45,7 @@ <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearWebsitesGridFilter"/> <!--Clear products filter--> - <amOnPage url="{{AdminProductIndexPage.url}}" stepKey="navigateToProductIndex"/> + <actionGroup ref="AdminOpenProductIndexPageActionGroup" stepKey="navigateToProductIndex"/> <actionGroup ref="ClearFiltersAdminDataGridActionGroup" stepKey="clearProductsFilters"/> <!--Logout everywhere--> <actionGroup ref="AdminLogoutActionGroup" stepKey="adminLogout"/> @@ -67,7 +67,7 @@ <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="acceptStoreSwitchingForProduct1"/> <click selector="{{AdminProductFormSection.visibilityUseDefault}}" stepKey="uncheckVisibilityUseDefaultValueForProduct1"/> <selectOption userInput="Not Visible Individually" selector="{{AdminProductFormSection.visibility}}" stepKey="makeProductNotVisibleOnSecondaryStoreView"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveEditedProductForProduct1"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveEditedProductForProduct1"/> <actionGroup ref="SearchForProductOnBackendActionGroup" stepKey="searchForProduct2"> <argument name="product" value="$$secondProduct$$"/> </actionGroup> @@ -82,7 +82,7 @@ <click selector="{{AdminProductFormChangeStoreSection.acceptButton}}" stepKey="acceptStoreSwitchingForProduct2"/> <click selector="{{AdminProductFormSection.visibilityUseDefault}}" stepKey="uncheckVisibilityUseDefaultValueForProduct2"/> <selectOption userInput="Not Visible Individually" selector="{{AdminProductFormSection.visibility}}" stepKey="makeProductNotVisibleOnDefaultStoreView"/> - <click selector="{{AdminProductFormActionSection.saveButton}}" stepKey="saveEditedProductForProduct2"/> + <actionGroup ref="AdminProductFormSaveActionGroup" stepKey="saveEditedProductForProduct2"/> <!-- Sign in as customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="customerLogin"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml index a6cab4b9f9715..c279adbfe876c 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontAddProductsToCartFromWishlistUsingSidebarTest.xml @@ -26,16 +26,24 @@ <requiredEntity createDataKey="categorySecond"/> </createData> <createData entity="Simple_US_Customer" stepKey="customer"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <deleteData createDataKey="simpleProduct1" stepKey="deleteSimpleProduct1"/> <deleteData createDataKey="simpleProduct2" stepKey="deleteSimpleProduct2"/> <deleteData createDataKey="categoryFirst" stepKey="deleteCategoryFirst"/> <deleteData createDataKey="categorySecond" stepKey="deleteCategorySecond"/> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <!-- Sign in as customer --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml index a23788d2c508f..31bc9f6a31de7 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteBundleFixedProductFromWishlistTest.xml @@ -45,8 +45,12 @@ <requiredEntity createDataKey="createBundleOption1_1"/> <requiredEntity createDataKey="simpleProduct2"/> </createData> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml index 4ad87095ecd30..da2cec8284c46 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDeleteConfigurableProductFromWishlistTest.xml @@ -105,8 +105,12 @@ <requiredEntity createDataKey="createConfigProduct"/> <requiredEntity createDataKey="createConfigChildProduct3"/> </createData> - <magentoCLI stepKey="reindex" command="indexer:reindex"/> - <magentoCLI stepKey="flushCache" command="cache:flush"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml new file mode 100644 index 0000000000000..65cce1dcfc1c3 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontDisabledCustomerWishlistFunctionalityTest.xml @@ -0,0 +1,52 @@ +<?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="StorefrontDisabledCustomerWishlistFunctionalityTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Disabled Wishlist Functionality"/> + <title value="Wishlist Functionality is disabled in system configurations and not visible on FE"/> + <description value="Customer should not see wishlist functionality if it's disabled"/> + <testCaseId value="MC-35200"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/general/active 0" stepKey="disableWishlist"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <magentoCLI command="config:set wishlist/general/active 1" stepKey="enableWishlist"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertCustomerSidebarItemIsNotPresentActionGroup" stepKey="assertItemIsNotPresent"> + <argument name="itemName" value="My Wish List"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontAssertProductPageAddToWishlistButtonIsNotPresentActionGroup" stepKey="assertButtonIsAbsent"/> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml index a5081ca2ad338..05a42314ddb71 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontMoveConfigurableProductFromShoppingCartToWishlistTest.xml @@ -107,8 +107,12 @@ <requiredEntity createDataKey="createConfigChildProduct3"/> </createData> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <!-- Delete data --> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml index 97551a596e978..b2364b72f7db8 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontRemoveProductsFromWishlistUsingSidebarTest.xml @@ -36,8 +36,12 @@ </after> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <!-- Sign in as customer --> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml new file mode 100644 index 0000000000000..281272293e6a9 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShareWishlistWithMoreThanMaximumAllowedEmailsQtyTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Sharing wishlist with more than Maximum Allowed Emails qty"/> + <title value="Sharing wishlist with more than Maximum Allowed Emails qty"/> + <description value="Customer should not have a possibility share wishlist with more than maximum allowed emails qty"/> + <testCaseId value="MC-35167"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/email/number_limit 1" stepKey="changeEmailsQtyLimit"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <magentoCLI command="config:set wishlist/email/number_limit 10" stepKey="returnDefaultValue"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishList"> + <argument name="email" value="{{Wishlist.shareInfo_emails}}"/> + <argument name="message" value="{{Wishlist.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertMessage"> + <argument name="message" value="Maximum of 1 emails can be sent."/> + <argument name="messageType" value="error"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest.xml new file mode 100644 index 0000000000000..65e67f75eb7e8 --- /dev/null +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest.xml @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + /** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> + +<tests xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:mftf:Test/etc/testSchema.xsd"> + <test name="StorefrontShareWishlistWithMoreThanMaximumAllowedTextLengthLimitTest"> + <annotations> + <features value="Wishlist"/> + <stories value="Sharing wishlist with more than Maximum Allowed Text Length Limit"/> + <title value="Sharing wishlist with more than Maximum Allowed Text Length Limit"/> + <description value="Customer should not have a possibility share wishlist with more than maximum allowed Email Text Length Limit"/> + <testCaseId value="MC-35647"/> + <severity value="AVERAGE"/> + <group value="wishlist"/> + <group value="configuration"/> + </annotations> + <before> + <magentoCLI command="config:set wishlist/email/text_limit 10" stepKey="changeTextLengthLimit"/> + <magentoCLI command="cache:clean config" stepKey="cleanCache"/> + <createData entity="SimpleSubCategory" stepKey="createCategory"/> + <createData entity="SimpleProduct" stepKey="createProduct"> + <requiredEntity createDataKey="createCategory"/> + </createData> + <magentoCLI command="cron:run --group=index" stepKey="runCronIndexer"/> + <createData entity="Simple_US_Customer" stepKey="createCustomer"/> + </before> + <after> + <magentoCLI command="config:set wishlist/email/text_limit 255" stepKey="returnDefaultValue"/> + <magentoCLI command="cache:clean config" stepKey="cacheClean"/> + <deleteData createDataKey="createCategory" stepKey="deleteCategory"/> + <deleteData createDataKey="createProduct" stepKey="deleteProduct"/> + <actionGroup ref="StorefrontCustomerLogoutActionGroup" stepKey="logoutCustomer"/> + <deleteData createDataKey="createCustomer" stepKey="deleteCustomer"/> + </after> + + <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> + <argument name="Customer" value="$createCustomer$"/> + </actionGroup> + <actionGroup ref="OpenStoreFrontProductPageActionGroup" stepKey="openProductPage"> + <argument name="productUrlKey" value="$$createProduct.custom_attributes[url_key]$$"/> + </actionGroup> + <actionGroup ref="StorefrontCustomerAddProductToWishlistActionGroup" stepKey="addToWishlistProduct"> + <argument name="productVar" value="$createProduct$"/> + </actionGroup> + <actionGroup ref="StorefrontShareCustomerWishlistActionGroup" stepKey="shareWishList"> + <argument name="email" value="{{Wishlist.shareInfo_emails}}"/> + <argument name="message" value="{{Wishlist.shareInfo_message}}"/> + </actionGroup> + <actionGroup ref="AssertMessageCustomerChangeAccountInfoActionGroup" stepKey="assertMessage"> + <argument name="message" value="Message length must not exceed 10 symbols"/> + <argument name="messageType" value="error"/> + </actionGroup> + </test> +</tests> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml index 20881fa64f8f8..0438a1b58e771 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontShareWishlistWithNotValidEmailAddressTest.xml @@ -14,6 +14,7 @@ <stories value="Customer Wishlist"/> <title value="Customer is not able to share wishlist with invalid email addresses"/> <description value="Customer is not able to share wishlist with invalid email addresses"/> + <severity value="AVERAGE"/> <group value="wishlist"/> </annotations> <before> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml index 08698658588ae..86d09783e0f55 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontUpdateWishlistTest.xml @@ -27,8 +27,12 @@ </before> <!-- Perform reindex and flush cache --> - <magentoCLI command="indexer:reindex" stepKey="reindex"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliIndexerReindexActionGroup" stepKey="reindex"> + <argument name="indices" value=""/> + </actionGroup> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <actionGroup ref="LoginToStorefrontActionGroup" stepKey="loginToStorefrontAccount"> <argument name="Customer" value="$$customer$$"/> diff --git a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml index 72f5bab1e6af5..f5958f5efd414 100644 --- a/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml +++ b/app/code/Magento/Wishlist/Test/Mftf/Test/StorefrontVerifySecureURLRedirectWishlistTest.xml @@ -28,11 +28,15 @@ <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"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> </before> <after> <magentoCLI command="config:set web/secure/use_in_frontend 0" stepKey="dontUseSecureURLsOnStorefront"/> - <magentoCLI command="cache:flush" stepKey="flushCache"/> + <actionGroup ref="CliCacheFlushActionGroup" stepKey="flushCache"> + <argument name="tags" value=""/> + </actionGroup> <deleteData createDataKey="customer" stepKey="deleteCustomer"/> </after> <executeJS function="return window.location.host" stepKey="hostname"/> diff --git a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php index 58d888d8ccb6e..47bb930cde3b9 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Controller/Index/CartTest.php @@ -10,8 +10,8 @@ use Magento\Catalog\Helper\Product as ProductHelper; use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Exception as ProductException; -use Magento\Checkout\Model\Cart as CheckoutCart; use Magento\Checkout\Helper\Cart as CartHelper; +use Magento\Checkout\Model\Cart as CheckoutCart; use Magento\Framework\App\Action\Context; use Magento\Framework\App\RequestInterface; use Magento\Framework\App\Response\RedirectInterface; @@ -620,14 +620,8 @@ protected function prepareExecuteWithQuantityArray($isAjax = false) ->method('getName') ->willReturn($productName); - $this->escaperMock->expects($this->once()) - ->method('escapeHtml') - ->with($productName, null) - ->willReturn($productName); - $this->messageManagerMock->expects($this->once()) - ->method('addSuccessMessage') - ->with('You added ' . $productName . ' to your shopping cart.', null) + ->method('addComplexSuccessMessage') ->willReturnSelf(); $this->cartHelperMock->expects($this->once()) diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php index 72705acb8cd06..28705d54e6e20 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/ResourceModel/Item/CollectionTest.php @@ -54,7 +54,8 @@ class CollectionTest extends TestCase /** @var string */ protected $sql = "SELECT `main_table`.* FROM `testMainTableName` AS `main_table` - INNER JOIN `testBackendTableName` AS `product_name_table` ON product_name_table.entity_id = main_table.product_id + INNER JOIN `testEntityTableName` AS `product_entity` ON product_entity.entity_id = main_table.product_id + INNER JOIN `testBackendTableName` AS `product_name_table` ON product_name_table.entity_id = product_entity.entity_id AND product_name_table.store_id = 1 AND product_name_table.attribute_id = 12 WHERE (INSTR(product_name_table.value, 'TestProductName'))"; @@ -90,18 +91,13 @@ protected function setUp(): void ->expects($this->any()) ->method('getConnection') ->willReturn($connection); - $resource - ->expects($this->any()) - ->method('getMainTable') - ->willReturn('testMainTableName'); - $resource - ->expects($this->any()) - ->method('getTableName') - ->willReturn('testMainTableName'); $resource ->expects($this->any()) ->method('getTable') - ->willReturn('testMainTableName'); + ->willReturnOnConsecutiveCalls( + 'testMainTableName', + 'testEntityTableName' + ); $catalogConfFactory = $this->createPartialMock( ConfigFactory::class, diff --git a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php index 19bfb3598f0e3..e09491813877b 100644 --- a/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php +++ b/app/code/Magento/Wishlist/Test/Unit/Model/WishlistTest.php @@ -243,34 +243,22 @@ public function testLoadByCustomerId() /** * @param int|Item|MockObject $itemId - * @param DataObject $buyRequest + * @param DataObject|MockObject $buyRequest * @param null|array|DataObject $param * @throws LocalizedException * * @dataProvider updateItemDataProvider */ - public function testUpdateItem($itemId, $buyRequest, $param) + public function testUpdateItem($itemId, $buyRequest, $param): void { $storeId = 1; $productId = 1; $stores = [(new DataObject())->setId($storeId)]; - $newItem = $this->getMockBuilder(Item::class) - ->setMethods( - ['setProductId', 'setWishlistId', 'setStoreId', 'setOptions', 'setProduct', 'setQty', 'getItem', 'save'] - ) - ->disableOriginalConstructor() - ->getMock(); - $newItem->expects($this->any())->method('setProductId')->willReturnSelf(); - $newItem->expects($this->any())->method('setWishlistId')->willReturnSelf(); - $newItem->expects($this->any())->method('setStoreId')->willReturnSelf(); - $newItem->expects($this->any())->method('setOptions')->willReturnSelf(); - $newItem->expects($this->any())->method('setProduct')->willReturnSelf(); - $newItem->expects($this->any())->method('setQty')->willReturnSelf(); - $newItem->expects($this->any())->method('getItem')->willReturn(2); - $newItem->expects($this->any())->method('save')->willReturnSelf(); + $newItem = $this->prepareWishlistItem(); $this->itemFactory->expects($this->once())->method('create')->willReturn($newItem); + $this->productHelper->expects($this->once())->method('addParamsToBuyRequest')->willReturn($buyRequest); $this->storeManager->expects($this->any())->method('getStores')->willReturn($stores); $this->storeManager->expects($this->any())->method('getStore')->willReturn($stores[0]); @@ -355,13 +343,48 @@ public function testUpdateItem($itemId, $buyRequest, $param) ); } + /** + * Prepare wishlist item mock. + * + * @return MockObject + */ + private function prepareWishlistItem(): MockObject + { + $newItem = $this->getMockBuilder(Item::class) + ->setMethods( + ['setProductId', 'setWishlistId', 'setStoreId', 'setOptions', 'setProduct', 'setQty', 'getItem', 'save'] + ) + ->disableOriginalConstructor() + ->getMock(); + $newItem->expects($this->any())->method('setProductId')->willReturnSelf(); + $newItem->expects($this->any())->method('setWishlistId')->willReturnSelf(); + $newItem->expects($this->any())->method('setStoreId')->willReturnSelf(); + $newItem->expects($this->any())->method('setOptions')->willReturnSelf(); + $newItem->expects($this->any())->method('setProduct')->willReturnSelf(); + $newItem->expects($this->any())->method('setQty')->willReturnSelf(); + $newItem->expects($this->any())->method('getItem')->willReturn(2); + $newItem->expects($this->any())->method('save')->willReturnSelf(); + + return $newItem; + } + /** * @return array */ - public function updateItemDataProvider() + public function updateItemDataProvider(): array { + $dataObjectMock = $this->createMock(DataObject::class); + $dataObjectMock->expects($this->once()) + ->method('setData') + ->with('action', 'updateItem') + ->willReturnSelf(); + $dataObjectMock->expects($this->once()) + ->method('getData') + ->with('action') + ->willReturn('updateItem'); + return [ - '0' => [1, new DataObject(), null] + '0' => [1, $dataObjectMock, null] ]; } diff --git a/app/code/Magento/Wishlist/etc/adminhtml/di.xml b/app/code/Magento/Wishlist/etc/adminhtml/di.xml index 124b8c17c3f36..be4d1966a46e9 100644 --- a/app/code/Magento/Wishlist/etc/adminhtml/di.xml +++ b/app/code/Magento/Wishlist/etc/adminhtml/di.xml @@ -24,4 +24,6 @@ <type name="Magento\Catalog\Model\ResourceModel\Product"> <plugin name="cleanups_wishlist_item_after_product_delete" type="Magento\Wishlist\Plugin\Model\ResourceModel\Product" /> </type> + <preference for="Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface" + type="Magento\Wishlist\Model\Adminhtml\ResourceModel\Item\Product\CollectionBuilder"/> </config> diff --git a/app/code/Magento/Wishlist/etc/di.xml b/app/code/Magento/Wishlist/etc/di.xml index c0230a5326f40..924bdfa3eb584 100644 --- a/app/code/Magento/Wishlist/etc/di.xml +++ b/app/code/Magento/Wishlist/etc/di.xml @@ -78,4 +78,6 @@ </argument> </arguments> </type> + <preference for="Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilderInterface" + type="Magento\Wishlist\Model\ResourceModel\Item\Product\CollectionBuilder"/> </config> diff --git a/app/code/Magento/Wishlist/etc/graphql/di.xml b/app/code/Magento/Wishlist/etc/graphql/di.xml new file mode 100644 index 0000000000000..9726376bf30be --- /dev/null +++ b/app/code/Magento/Wishlist/etc/graphql/di.xml @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd"> + <type name="Magento\Wishlist\Model\Wishlist\BuyRequest\BuyRequestBuilder"> + <arguments> + <argument name="providers" xsi:type="array"> + <item name="super_attribute" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperAttributeDataProvider</item> + <item name="customizable_option" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\CustomizableOptionDataProvider</item> + <item name="bundle" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\BundleDataProvider</item> + <item name="downloadable" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\DownloadableLinkDataProvider</item> + <item name="grouped" xsi:type="object">Magento\Wishlist\Model\Wishlist\BuyRequest\SuperGroupDataProvider</item> + </argument> + </arguments> + </type> +</config> diff --git a/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml b/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml index e364087405ed9..0ee4233029105 100644 --- a/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml +++ b/app/code/Magento/Wishlist/view/adminhtml/layout/customer_index_wishlist.xml @@ -99,6 +99,7 @@ <item name="caption" xsi:type="string" translate="true">Delete</item> <item name="url" xsi:type="string">#</item> <item name="onclick" xsi:type="string">return wishlistControl.removeItem($wishlist_item_id);</item> + <item name="class" xsi:type="string">wishlist-remove-button</item> </item> </argument> </arguments> diff --git a/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml b/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml index e6c0608f4b450..7ee04bf192f29 100644 --- a/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml +++ b/app/code/Magento/Wishlist/view/adminhtml/templates/customer/edit/tab/wishlist.phtml @@ -5,8 +5,10 @@ */ /** @var \Magento\Framework\View\Element\Template $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<script> +<?php $scriptString = <<<script + require([ "Magento_Ui/js/modal/confirm", "prototype", @@ -19,13 +21,14 @@ if (!urlParams) { urlParams = ''; } - var url = <?= $block->escapeJs($block->escapeUrl($block->getJsObjectName())) ?>.url + '?ajax=true' + urlParams; + var url = {$block->escapeJs($block->getJsObjectName())}.url + '?ajax=true' + urlParams; new Ajax.Updater( - <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.containerId, + {$block->escapeJs($block->getJsObjectName())}.containerId, url, { parameters: {form_key: FORM_KEY}, - onComplete: <?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>.initGrid.bind(<?= $block->escapeJs($block->escapeHtml($block->getJsObjectName())) ?>), + onComplete: {$block->escapeJs($block->getJsObjectName())}.initGrid + .bind({$block->escapeJs($block->getJsObjectName())}), evalScripts:true } ); @@ -48,7 +51,7 @@ var self = this; confirm({ - content: '<?= $block->escapeJs($block->escapeHtml(__('Are you sure you want to remove this item?'))) ?>', + content: '{$block->escapeJs(__('Are you sure you want to remove this item?'))}', actions: { confirm: function () { self.reload('&delete=' + itemId); @@ -61,11 +64,14 @@ productConfigure.addListType( 'wishlist', { - urlFetch: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))) ?>', - urlConfirm: '<?= $block->escapeJs($block->escapeUrl($block->getUrl('customer/wishlist_product_composite_wishlist/update'))) ?>' + urlFetch: '{$block->escapeJs($block->getUrl('customer/wishlist_product_composite_wishlist/configure'))}', + urlConfirm: '{$block->escapeJs($block->getUrl('customer/wishlist_product_composite_wishlist/update'))}' } ); //--> }); -</script> + +script; +?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], $scriptString, false) ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml b/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml index 4a97173fde318..f221e50608ca4 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/rss/email.phtml @@ -5,11 +5,19 @@ */ /* @var \Magento\Wishlist\Block\Rss\EmailLink $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ + +/** @var \Magento\Wishlist\Helper\Data $wishlistHelper */ +$wishlistHelper = $block->getData('wishlistHelper'); ?> -<?php if ($block->getLink()) : ?> -<p style="font-size:12px; line-height:16px; margin:0 0 16px;"> - <?= $block->escapeHtml(__("RSS link to %1's wishlist", $this->helper(\Magento\Wishlist\Helper\Data::class)->getCustomerName())) ?> +<?php if ($block->getLink()): ?> +<p id="wishlist-rss-email-link"> + <?= $block->escapeHtml(__("RSS link to %1's wishlist", $wishlistHelper->getCustomerName())) ?> <br /> <a href="<?= $block->escapeUrl($block->getLink()) ?>"><?= $block->escapeUrl($block->getLink()) ?></a> </p> + <?= /* @noEscape */ $secureRenderer->renderStyleAsTag( + "font-size:12px; line-height:16px; margin:0 0 16px;", + 'p#wishlist-rss-email-link' + ) ?> <?php endif; ?> diff --git a/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml b/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml index 0fcaa6c853ff0..0d3158abb0532 100644 --- a/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml +++ b/app/code/Magento/Wishlist/view/frontend/templates/shared.phtml @@ -5,10 +5,12 @@ */ /** @var \Magento\Wishlist\Block\Share\Wishlist $block */ +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ ?> -<?php if ($block->hasWishlistItems()) : ?> - <form class="form shared wishlist" action="<?= $block->escapeUrl($block->getUrl('wishlist/index/update')) ?>" method="post"> +<?php if ($block->hasWishlistItems()): ?> + <form class="form shared wishlist" action="<?= $block->escapeUrl($block->getUrl('wishlist/index/update')) ?>" + method="post"> <div class="wishlist table-wrapper"> <table class="table data wishlist" id="wishlist-table"> <caption class="table-caption"><?= $block->escapeHtml(__('Wish List')) ?></caption> @@ -20,14 +22,15 @@ </tr> </thead> <tbody> - <?php foreach ($block->getWishlistItems() as $item) : ?> + <?php foreach ($block->getWishlistItems() as $item): ?> <?php $product = $item->getProduct(); $isVisibleProduct = $product->isVisibleInSiteVisibility(); ?> <tr> <td data-th="<?= $block->escapeHtmlAttr(__('Product')) ?>" class="col product"> - <a class="product photo" href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" title="<?= $block->escapeHtmlAttr($product->getName()) ?>"> + <a class="product photo" href="<?= $block->escapeUrl($block->getProductUrl($item)) ?>" + title="<?= $block->escapeHtmlAttr($product->getName()) ?>"> <?= $block->getImage($product, 'customer_shared_wishlist')->toHtml() ?> </a> <strong class="product name"> @@ -45,10 +48,13 @@ ?> <?= $block->getDetailsHtml($item) ?> </td> - <td data-th="<?= $block->escapeHtmlAttr(__('Comment')) ?>" class="col comment"><?= /* @noEscape */ $block->getEscapedDescription($item) ?></td> - <td data-th="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" class="col actions" data-role="add-to-links"> - <?php if ($product->isSaleable()) : ?> - <?php if ($isVisibleProduct) : ?> + <td data-th="<?= $block->escapeHtmlAttr(__('Comment')) ?>" + class="col comment"><?= /* @noEscape */ $block->getEscapedDescription($item) ?> + </td> + <td data-th="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" class="col actions" + data-role="add-to-links"> + <?php if ($product->isSaleable()): ?> + <?php if ($isVisibleProduct): ?> <button type="button" title="<?= $block->escapeHtmlAttr(__('Add to Cart')) ?>" data-post='<?= /* @noEscape */ $block->getSharedItemAddToCartUrl($item) ?>' @@ -57,9 +63,16 @@ </button> <?php endif ?> <?php endif; ?> - <a href="#" data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($item) ?>' onclick="location.assign(this.href); return false;" class="action towishlist" data-action="add-to-wishlist"> + <a href="#" data-post='<?= /* @noEscape */ $block->getAddToWishlistParams($item) ?>' + id="wishlist-shared-item-<?= /* @noEscape */ $item->getId() ?>" + class="action towishlist" data-action="add-to-wishlist"> <span><?= $block->escapeHtml(__('Add to Wish List')) ?></span> </a> + <?= /* @noEscape */ $secureRenderer->renderEventListenerAsTag( + 'onclick', + "location.assign(this.href); event.preventDefault();", + 'a#wishlist-shared-item-' . $item->getId() + ) ?> </td> </tr> <?php endforeach ?> @@ -68,7 +81,7 @@ </div> <div class="actions-toolbar"> - <?php if ($block->isSaleable()) : ?> + <?php if ($block->isSaleable()): ?> <div class="primary"> <button type="button" title="<?= $block->escapeHtmlAttr(__('Add All to Cart')) ?>" @@ -85,6 +98,6 @@ </div> </div> </form> -<?php else : ?> +<?php else: ?> <div class="message info empty"><div><?= $block->escapeHtml(__('Wish List is empty now.')) ?></div></div> <?php endif ?> diff --git a/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php new file mode 100644 index 0000000000000..9cc1404613e41 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Mapper/WishlistDataMapper.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Mapper; + +use Magento\Wishlist\Model\Wishlist; + +/** + * Prepares the wishlist output as associative array + */ +class WishlistDataMapper +{ + /** + * Mapping the review data + * + * @param Wishlist $wishlist + * + * @return array + */ + public function map(Wishlist $wishlist): array + { + return [ + 'id' => $wishlist->getId(), + 'sharing_code' => $wishlist->getSharingCode(), + 'updated_at' => $wishlist->getUpdatedAt(), + 'items_count' => $wishlist->getItemsCount(), + 'name' => $wishlist->getName(), + 'model' => $wishlist, + ]; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php new file mode 100644 index 0000000000000..3489585cd17d7 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/AddProductsToWishlist.php @@ -0,0 +1,158 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\AddProductsToWishlist as AddProductsToWishlistModel; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Adding products to wishlist resolver + */ +class AddProductsToWishlist implements ResolverInterface +{ + /** + * @var AddProductsToWishlistModel + */ + private $addProductsToWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param AddProductsToWishlistModel $addProductsToWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + AddProductsToWishlistModel $addProductsToWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->addProductsToWishlist = $addProductsToWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if (null === $customerId || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $this->getWishlistItems($args['wishlistItems']); + $wishlistOutput = $this->addProductsToWishlist->execute($wishlist, $wishlistItems); + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'user_errors' => array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php index a84ce0e965b6d..cad574ef56ed2 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/CustomerWishlistResolver.php @@ -9,9 +9,11 @@ use Magento\Framework\GraphQl\Config\Element\Field; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; use Magento\Wishlist\Model\WishlistFactory; /** @@ -24,12 +26,21 @@ class CustomerWishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -42,6 +53,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + if (false === $context->getExtensionAttributes()->getIsCustomer()) { throw new GraphQlAuthorizationException(__('The current customer isn\'t authorized.')); } diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php new file mode 100644 index 0000000000000..a59c5ccdb0f70 --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/RemoveProductsFromWishlist.php @@ -0,0 +1,144 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\RemoveProductsFromWishlist as RemoveProductsFromWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Removing products from wishlist resolver + */ +class RemoveProductsFromWishlist implements ResolverInterface +{ + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var RemoveProductsFromWishlistModel + */ + private $removeProductsFromWishlist; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @param WishlistFactory $wishlistFactory + * @param WishlistResourceModel $wishlistResource + * @param WishlistConfig $wishlistConfig + * @param WishlistDataMapper $wishlistDataMapper + * @param RemoveProductsFromWishlistModel $removeProductsFromWishlist + */ + public function __construct( + WishlistFactory $wishlistFactory, + WishlistResourceModel $wishlistResource, + WishlistConfig $wishlistConfig, + WishlistDataMapper $wishlistDataMapper, + RemoveProductsFromWishlistModel $removeProductsFromWishlist + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistConfig = $wishlistConfig; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistDataMapper = $wishlistDataMapper; + $this->removeProductsFromWishlist = $removeProductsFromWishlist; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if ($customerId === null || 0 === $customerId) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItemsIds = $args['wishlistItemsIds']; + $wishlistOutput = $this->removeProductsFromWishlist->execute($wishlist, $wishlistItemsIds); + + if (!empty($wishlistItemsIds)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'user_errors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if ($wishlistId !== null && $wishlistId > 0) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php new file mode 100644 index 0000000000000..c6ede66fc2b1b --- /dev/null +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/UpdateProductsInWishlist.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\WishlistGraphQl\Model\Resolver; + +use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; +use Magento\Framework\GraphQl\Query\ResolverInterface; +use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; +use Magento\Wishlist\Model\Wishlist\Data\Error; +use Magento\Wishlist\Model\Wishlist\Data\WishlistItemFactory; +use Magento\Wishlist\Model\Wishlist\UpdateProductsInWishlist as UpdateProductsInWishlistModel; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\WishlistGraphQl\Mapper\WishlistDataMapper; + +/** + * Update wishlist items resolver + */ +class UpdateProductsInWishlist implements ResolverInterface +{ + /** + * @var UpdateProductsInWishlistModel + */ + private $updateProductsInWishlist; + + /** + * @var WishlistDataMapper + */ + private $wishlistDataMapper; + + /** + * @var WishlistConfig + */ + private $wishlistConfig; + + /** + * @var WishlistResourceModel + */ + private $wishlistResource; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @param WishlistResourceModel $wishlistResource + * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig + * @param UpdateProductsInWishlistModel $updateProductsInWishlist + * @param WishlistDataMapper $wishlistDataMapper + */ + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig, + UpdateProductsInWishlistModel $updateProductsInWishlist, + WishlistDataMapper $wishlistDataMapper + ) { + $this->wishlistResource = $wishlistResource; + $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; + $this->updateProductsInWishlist = $updateProductsInWishlist; + $this->wishlistDataMapper = $wishlistDataMapper; + } + + /** + * @inheritdoc + */ + public function resolve( + Field $field, + $context, + ResolveInfo $info, + array $value = null, + array $args = null + ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + + $customerId = $context->getUserId(); + + /* Guest checking */ + if (null === $customerId || $customerId === 0) { + throw new GraphQlAuthorizationException(__('The current user cannot perform operations on wishlist')); + } + + $wishlistId = ((int) $args['wishlistId']) ?: null; + $wishlist = $this->getWishlist($wishlistId, $customerId); + + if (null === $wishlist->getId() || $customerId !== (int) $wishlist->getCustomerId()) { + throw new GraphQlInputException(__('The wishlist was not found.')); + } + + $wishlistItems = $args['wishlistItems']; + $wishlistItems = $this->getWishlistItems($wishlistItems); + $wishlistOutput = $this->updateProductsInWishlist->execute($wishlist, $wishlistItems); + + if (count($wishlistOutput->getErrors()) !== count($wishlistItems)) { + $this->wishlistResource->save($wishlist); + } + + return [ + 'wishlist' => $this->wishlistDataMapper->map($wishlistOutput->getWishlist()), + 'user_errors' => \array_map( + function (Error $error) { + return [ + 'code' => $error->getCode(), + 'message' => $error->getMessage(), + ]; + }, + $wishlistOutput->getErrors() + ) + ]; + } + + /** + * Get DTO wishlist items + * + * @param array $wishlistItemsData + * + * @return array + */ + private function getWishlistItems(array $wishlistItemsData): array + { + $wishlistItems = []; + + foreach ($wishlistItemsData as $wishlistItemData) { + $wishlistItems[] = (new WishlistItemFactory())->create($wishlistItemData); + } + + return $wishlistItems; + } + + /** + * Get customer wishlist + * + * @param int|null $wishlistId + * @param int|null $customerId + * + * @return Wishlist + */ + private function getWishlist(?int $wishlistId, ?int $customerId): Wishlist + { + $wishlist = $this->wishlistFactory->create(); + + if (null !== $wishlistId && 0 < $wishlistId) { + $this->wishlistResource->load($wishlist, $wishlistId); + } elseif ($customerId !== null) { + $wishlist->loadByCustomerId($customerId, true); + } + + return $wishlist; + } +} diff --git a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php index 792928ab61aaf..09c0a8a935a6c 100644 --- a/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php +++ b/app/code/Magento/WishlistGraphQl/Model/Resolver/WishlistResolver.php @@ -8,10 +8,12 @@ namespace Magento\WishlistGraphQl\Model\Resolver; use Magento\Framework\GraphQl\Config\Element\Field; +use Magento\Framework\GraphQl\Exception\GraphQlInputException; use Magento\Framework\GraphQl\Query\ResolverInterface; use Magento\Framework\GraphQl\Schema\Type\ResolveInfo; use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResourceModel; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config as WishlistConfig; use Magento\Wishlist\Model\WishlistFactory; use Magento\Framework\GraphQl\Exception\GraphQlAuthorizationException; @@ -30,14 +32,24 @@ class WishlistResolver implements ResolverInterface */ private $wishlistFactory; + /** + * @var WishlistConfig + */ + private $wishlistConfig; + /** * @param WishlistResourceModel $wishlistResource * @param WishlistFactory $wishlistFactory + * @param WishlistConfig $wishlistConfig */ - public function __construct(WishlistResourceModel $wishlistResource, WishlistFactory $wishlistFactory) - { + public function __construct( + WishlistResourceModel $wishlistResource, + WishlistFactory $wishlistFactory, + WishlistConfig $wishlistConfig + ) { $this->wishlistResource = $wishlistResource; $this->wishlistFactory = $wishlistFactory; + $this->wishlistConfig = $wishlistConfig; } /** @@ -50,6 +62,10 @@ public function resolve( array $value = null, array $args = null ) { + if (!$this->wishlistConfig->isEnabled()) { + throw new GraphQlInputException(__('The wishlist is not currently available.')); + } + $customerId = $context->getUserId(); /* Guest checking */ diff --git a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php index 8385d3ca852a4..017462b4c94c6 100644 --- a/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php +++ b/app/code/Magento/WishlistGraphQl/Test/Unit/CustomerWishlistResolverTest.php @@ -14,6 +14,7 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\GraphQl\Model\Query\ContextExtensionInterface; use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\Wishlist\Config; use Magento\Wishlist\Model\WishlistFactory; use Magento\WishlistGraphQl\Model\Resolver\CustomerWishlistResolver; use PHPUnit\Framework\MockObject\MockObject; @@ -48,6 +49,11 @@ class CustomerWishlistResolverTest extends TestCase */ private $resolver; + /** + * @var Config|MockObject + */ + private $wishlistConfigMock; + /** * Build the Testing Environment */ @@ -74,9 +80,12 @@ protected function setUp(): void ->setMethods(['loadByCustomerId', 'getId', 'getSharingCode', 'getUpdatedAt', 'getItemsCount']) ->getMock(); + $this->wishlistConfigMock = $this->createMock(Config::class); + $objectManager = new ObjectManager($this); $this->resolver = $objectManager->getObject(CustomerWishlistResolver::class, [ - 'wishlistFactory' => $this->wishlistFactoryMock + 'wishlistFactory' => $this->wishlistFactoryMock, + 'wishlistConfig' => $this->wishlistConfigMock ]); } @@ -85,6 +94,8 @@ protected function setUp(): void */ public function testThrowExceptionWhenUserNotAuthorized(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(false); @@ -107,6 +118,8 @@ public function testThrowExceptionWhenUserNotAuthorized(): void */ public function testFactoryCreatesWishlistByAuthorizedCustomerId(): void { + $this->wishlistConfigMock->method('isEnabled')->willReturn(true); + // Given $this->extensionAttributesMock->method('getIsCustomer') ->willReturn(true); diff --git a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls index deaa66921ba7c..430e77cc45e96 100644 --- a/app/code/Magento/WishlistGraphQl/etc/schema.graphqls +++ b/app/code/Magento/WishlistGraphQl/etc/schema.graphqls @@ -6,7 +6,7 @@ type Query { } type Customer { - wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "The wishlist query returns the contents of a customer's wish lists") @cache(cacheable: false) + wishlist: Wishlist! @resolver(class:"\\Magento\\WishlistGraphQl\\Model\\Resolver\\CustomerWishlistResolver") @doc(description: "Contains the contents of a customer's wish lists") @cache(cacheable: false) } type WishlistOutput @doc(description: "Deprecated: `Wishlist` type should be used instead") { @@ -32,3 +32,50 @@ type WishlistItem { added_at: String @doc(description: "The time when the customer added the item to the wish list"), product: ProductInterface @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\ProductResolver") } + +type Mutation { + addProductsToWishlist(wishlistId: ID!, wishlistItems: [WishlistItemInput!]!): AddProductsToWishlistOutput @doc(description: "Adds one or more products to the specified wish list. This mutation supports all product types") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\AddProductsToWishlist") + removeProductsFromWishlist(wishlistId: ID!, wishlistItemsIds: [ID!]!): RemoveProductsFromWishlistOutput @doc(description: "Removes one or more products from the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\RemoveProductsFromWishlist") + updateProductsInWishlist(wishlistId: ID!, wishlistItems: [WishlistItemUpdateInput!]!): UpdateProductsInWishlistOutput @doc(description: "Updates one or more products in the specified wish list") @resolver(class: "\\Magento\\WishlistGraphQl\\Model\\Resolver\\UpdateProductsInWishlist") +} + +input WishlistItemInput @doc(description: "Defines the items to add to a wish list") { + sku: String @doc(description: "The SKU of the product to add. For complex product types, specify the child product SKU") + quantity: Float @doc(description: "The amount or number of items to add") + parent_sku: String @doc(description: "For complex product types, the SKU of the parent product") + selected_options: [ID!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type AddProductsToWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully added") + user_errors:[WishListUserInputError!]! @doc(description: "An array of errors encountered while adding products to a wish list") +} + +type RemoveProductsFromWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with after items were successfully deleted") + user_errors:[WishListUserInputError!]! @doc(description:"An array of errors encountered while deleting products from a wish list") +} + +input WishlistItemUpdateInput @doc(description: "Defines updates to items in a wish list") { + wishlist_item_id: ID @doc(description: "The ID of the wishlist item to update") + quantity: Float @doc(description: "The new amount or number of this item") + description: String @doc(description: "Describes the update") + selected_options: [ID!] @doc(description: "An array of strings corresponding to options the customer selected") + entered_options: [EnteredOptionInput!] @doc(description: "An array of options that the customer entered") +} + +type UpdateProductsInWishlistOutput @doc(description: "Contains the customer's wish list and any errors encountered") { + wishlist: Wishlist! @doc(description: "Contains the wish list with all items that were successfully updated") + user_errors: [WishListUserInputError!]! @doc(description:"An array of errors encountered while updating products in a wish list") +} + +type WishListUserInputError @doc(description:"An error encountered while performing operations with WishList.") { + message: String! @doc(description: "A localized error message") + code: WishListUserInputErrorType! @doc(description: "Wishlist-specific error code") +} + +enum WishListUserInputErrorType { + PRODUCT_NOT_FOUND + UNDEFINED +} diff --git a/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less b/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less index 6ce041fc19ac8..f0c98c891b5ba 100644 --- a/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less +++ b/app/design/adminhtml/Magento/backend/Magento_CatalogPermissions/web/css/source/_module.less @@ -17,3 +17,7 @@ } } } + +.warning-enable-permissions { + color: #f00; +} diff --git a/app/design/adminhtml/Magento/backend/web/css/styles-old.less b/app/design/adminhtml/Magento/backend/web/css/styles-old.less index 247316ab0361b..3087e7762a2d8 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles-old.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles-old.less @@ -5105,7 +5105,7 @@ .adminhtml-locks-index { .admin__scope-old { .grid .col-name { - &:extend(.col-570 all); + &:extend(.col-570-max all); } } } diff --git a/app/design/adminhtml/Magento/backend/web/css/styles.less b/app/design/adminhtml/Magento/backend/web/css/styles.less index 80d0923ced75a..02f8edc2b493b 100644 --- a/app/design/adminhtml/Magento/backend/web/css/styles.less +++ b/app/design/adminhtml/Magento/backend/web/css/styles.less @@ -53,6 +53,10 @@ td.col-date.col-date-min-width.col-created_at { min-width: 14rem; } +.colorRed { + color:red; +} + // ToDo UI: Temporary. Should be changed @import 'source/components/_calendar-temp.less'; @import 'source/components/_rules-temp.less'; diff --git a/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less b/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less index 70ae82045b3d1..bdd4ba3ec4fb5 100644 --- a/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less +++ b/app/design/adminhtml/Magento/backend/web/mui/styles/_table.less @@ -414,7 +414,7 @@ td.col-type { } tbody tr:nth-child(odd) td { - &:extend(.data-table tbody tr:nth-child(odd) td); + &:extend(.data-table tbody tr:nth-child(odd) td all); } tfoot tr:last-child td { @@ -601,13 +601,13 @@ td.col-type { } .label { - &:extend(.grid-actions .export .label); + &:extend(.grid-actions .export .label all); padding: 0; width: auto; } .action- { - &:extend(.grid-actions .export .action-); + &:extend(.grid-actions .export .action- all); vertical-align: top; } } diff --git a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less index f57420deb621d..4b48bbe99ced2 100644 --- a/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Catalog/web/css/source/_module.less @@ -457,11 +457,26 @@ .action { &.delete { &:extend(.abs-remove-button-for-blocks all); - line-height: unset; position: absolute; right: 0; top: -1px; - width: auto; + } + } + + .block-wishlist { + .action { + &.delete { + line-height: unset; + width: auto; + } + } + } + + .block-compare { + .action { + &.delete { + right: initial; + } } } @@ -814,6 +829,7 @@ &:extend(.abs-remove-button-for-blocks all); left: -6px; position: absolute; + right: 0; top: 0; } diff --git a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less index 09759d95c4b10..8434812f20719 100644 --- a/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/blank/Magento_Newsletter/web/css/source/_module.less @@ -82,6 +82,10 @@ .field { margin-right: 5px; + &.newsletter { + max-width: 220px; + } + .control { width: 100%; } diff --git a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js index 87632a6962cc5..cae30c83d95bc 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/requirejs-config.js @@ -5,7 +5,6 @@ var config = { deps: [ - 'Magento_Theme/js/responsive', 'Magento_Theme/js/theme' ] }; diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js deleted file mode 100644 index 011417f54ad9a..0000000000000 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/responsive.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -define([ - 'jquery', - 'matchMedia', - 'mage/tabs', - 'domReady!' -], function ($, mediaCheck) { - 'use strict'; - - mediaCheck({ - media: '(min-width: 768px)', - - /** - * Switch to Desktop Version. - */ - entry: function () { - var galleryElement; - - (function () { - - var productInfoMain = $('.product-info-main'), - productInfoAdditional = $('#product-info-additional'); - - if (productInfoAdditional.length) { - productInfoAdditional.addClass('hidden'); - productInfoMain.removeClass('responsive'); - } - - })(); - - galleryElement = $('[data-role=media-gallery]'); - - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('enable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', true); - galleryElement.gallery('option', 'showNav', false); - galleryElement.gallery('option', 'showThumbs', true); - } - }, - - /** - * Switch to Mobile Version. - */ - exit: function () { - var galleryElement; - - $('.action.toggle.checkout.progress').on('click.gotoCheckoutProgress', function () { - var myWrapper = '#checkout-progress-wrapper'; - - scrollTo(myWrapper + ' .title'); - $(myWrapper + ' .title').addClass('active'); - $(myWrapper + ' .content').show(); - }); - - $('body').on('click.checkoutProgress', '#checkout-progress-wrapper .title', function () { - $(this).toggleClass('active'); - $('#checkout-progress-wrapper .content').toggle(); - }); - - galleryElement = $('[data-role=media-gallery]'); - - setTimeout(function () { - if (galleryElement.length && galleryElement.data('mageZoom')) { - galleryElement.zoom('disable'); - } - - if (galleryElement.length && galleryElement.data('mageGallery')) { - galleryElement.gallery('option', 'disableLinks', false); - galleryElement.gallery('option', 'showNav', true); - galleryElement.gallery('option', 'showThumbs', false); - } - }, 2000); - } - }); -}); diff --git a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js index ab8a6063f29a7..e4edd3bd8662c 100644 --- a/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js +++ b/app/design/frontend/Magento/blank/Magento_Theme/web/js/theme.js @@ -12,21 +12,9 @@ define([ ], function ($, keyboardHandler) { 'use strict'; - if ($('body').hasClass('checkout-cart-index')) { - if ($('#co-shipping-method-form .fieldset.rates').length > 0 && - $('#co-shipping-method-form .fieldset.rates :checked').length === 0 - ) { - $('#block-shipping').on('collapsiblecreate', function () { - $('#block-shipping').collapsible('forceActivate'); - }); - } - } - $('.cart-summary').mage('sticky', { container: '#maincontent' }); - $('.panel.header > .header.links').clone().appendTo('#store\\.links'); - keyboardHandler.apply(); }); diff --git a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less index d0b7aa1523ad6..e205b20efd17c 100644 --- a/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Catalog/web/css/source/_module.less @@ -998,6 +998,15 @@ } } } + + .block-compare { + .action { + &.delete { + left: 0; + right: initial; + } + } + } } } @@ -1005,6 +1014,7 @@ .compare.wrapper { display: none; } + .catalog-product_compare-index { .columns { .column { diff --git a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less index 87990c3e48280..5d9746317af55 100644 --- a/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less +++ b/app/design/frontend/Magento/luma/Magento_Checkout/web/css/source/module/_cart.less @@ -424,7 +424,14 @@ .cart-container { .form-cart { .actions.main { - text-align: center; + .lib-vendor-prefix-display(); + .lib-vendor-prefix-flex-direction(column); + .lib-vendor-box-align(center); + + .clear, + .continue { + .lib-css(margin, 0 0 @indent__m 0); + } } } } diff --git a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less index a72f31d72ce48..21ed451a69d10 100644 --- a/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Newsletter/web/css/source/_module.less @@ -81,6 +81,10 @@ .block.newsletter { max-width: 44%; width: max-content; + + .field.newsletter { + max-width: 220px; + } .form.subscribe { > .field, diff --git a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less index e8adcc2f0e4f3..104dd6c4d5b92 100644 --- a/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less +++ b/app/design/frontend/Magento/luma/Magento_Rma/web/css/source/_module.less @@ -194,3 +194,17 @@ } } } + +#registrant-options { + .item { + .control { + table { + .col.qty { + .input-qty { + display: none; + } + } + } + } + } +} diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html index 4442c172a08e5..5b58659bcf48c 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_new_guest.html @@ -10,7 +10,7 @@ "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", "layout handle=\"sales_email_order_creditmemo_items\" creditmemo=$creditmemo order=$order":"Credit Memo Items Grid", -"var billing.name":"Guest Customer Name (Billing)", +"var order_data.customer_name":"Guest Customer Name (Billing)", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var formattedShippingAddress|raw":"Shipping Address", @@ -28,7 +28,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html index 9b09760c1fa4a..91f790715eda3 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/creditmemo_update_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var comment|escape|nl2br":"Credit Memo Comment", "var creditmemo.increment_id":"Credit Memo Id", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", "var store_email":"Store Email", @@ -19,7 +19,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html index 6e35fd2609dff..036b3b6d96dcb 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Invoice for your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "layout handle=\"sales_email_order_invoice_items\" invoice=$invoice order=$order":"Invoice Items Grid", @@ -28,7 +28,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html index f9e1498763cba..759e9766f335a 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/invoice_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name invoice" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Invoice Comment", "var invoice.increment_id":"Invoice Id", "var order.increment_id":"Order Id", @@ -19,7 +19,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html index 024f6daf76ace..e51b952281ed5 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_new_guest.html @@ -8,7 +8,7 @@ <!--@vars { "var formattedBillingAddress|raw":"Billing Address", "var order_data.email_customer_note|escape|nl2br":"Email Order Note", -"var order.billing_address.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var created_at_formatted":"Order Created At (datetime)", "var order.increment_id":"Order Id", "layout handle=\"sales_email_order_items\" order=$order":"Order Items Grid", @@ -27,7 +27,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$order.billing_address.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} {{trans "Once your package ships we will send you a tracking number."}} diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html index 5f23898b50018..3f8e8ace3428e 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/order_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name order" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -18,7 +18,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html index 18684fb052b4e..d1d9d21f1ee9a 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_new_guest.html @@ -7,7 +7,7 @@ <!--@subject {{trans "Your %store_name order has shipped" store_name=$store.frontend_name}} @--> <!--@vars { "var formattedBillingAddress|raw":"Billing Address", -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var order.increment_id":"Order Id", "var payment_html|raw":"Payment Details", "var comment|escape|nl2br":"Shipment Comment", @@ -30,7 +30,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Thank you for your order from %store_name." store_name=$store.frontend_name}} </p> diff --git a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html index 5887ff73c6398..37478644ddf9c 100644 --- a/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html +++ b/app/design/frontend/Magento/luma/Magento_Sales/email/shipment_update_guest.html @@ -6,7 +6,7 @@ --> <!--@subject {{trans "Update to your %store_name shipment" store_name=$store.frontend_name}} @--> <!--@vars { -"var billing.name":"Guest Customer Name", +"var order_data.customer_name":"Guest Customer Name", "var comment|escape|nl2br":"Order Comment", "var order.increment_id":"Order Id", "var order_data.frontend_status_label":"Order Status", @@ -19,7 +19,7 @@ <table> <tr class="email-intro"> <td> - <p class="greeting">{{trans "%name," name=$billing.name}}</p> + <p class="greeting">{{trans "%name," name=$order_data.customer_name}}</p> <p> {{trans "Your order #%increment_id has been updated with a status of <strong>%order_status</strong>." diff --git a/app/design/frontend/Magento/luma/web/css/source/_extends.less b/app/design/frontend/Magento/luma/web/css/source/_extends.less index ce86b690f6252..8ae1776daf239 100644 --- a/app/design/frontend/Magento/luma/web/css/source/_extends.less +++ b/app/design/frontend/Magento/luma/web/css/source/_extends.less @@ -1570,10 +1570,16 @@ margin-bottom: @indent__base; .actions.main { - .continue, - .clear { + .continue { display: none; } + + .clear { + .lib-button-as-link( + @_margin: 0 @indent__base 0 0 + ); + font-weight: @font-weight__regular; + } } } } diff --git a/app/etc/NonComposerComponentRegistration.php b/app/etc/NonComposerComponentRegistration.php index a7377ebfca3af..831f1aa27382e 100644 --- a/app/etc/NonComposerComponentRegistration.php +++ b/app/etc/NonComposerComponentRegistration.php @@ -3,6 +3,7 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); //Register components (via a list of glob patterns) namespace Magento\NonComposerComponentRegistration; @@ -11,23 +12,23 @@ /** * Include files from a list of glob patterns - * - * @throws RuntimeException - * @return void */ -$main = function () -{ +(static function (): void { $globPatterns = require __DIR__ . '/registration_globlist.php'; - $baseDir = dirname(dirname(__DIR__)) . '/'; + $baseDir = \dirname(__DIR__, 2) . '/'; foreach ($globPatterns as $globPattern) { // Sorting is disabled intentionally for performance improvement - $files = glob($baseDir . $globPattern, GLOB_NOSORT); + $files = \glob($baseDir . $globPattern, GLOB_NOSORT); if ($files === false) { throw new RuntimeException("glob(): error with '$baseDir$globPattern'"); } - array_map(function ($file) { require_once $file; }, $files); - } -}; -$main(); + \array_map( + static function (string $file): void { + require_once $file; + }, + $files + ); + } +})(); diff --git a/app/etc/di.xml b/app/etc/di.xml index ba635d0662755..fed2e336046f9 100644 --- a/app/etc/di.xml +++ b/app/etc/di.xml @@ -209,6 +209,8 @@ <preference for="Magento\Framework\MessageQueue\QueueFactoryInterface" type="Magento\Framework\MessageQueue\QueueFactory" /> <preference for="Magento\Framework\Search\Request\IndexScopeResolverInterface" type="Magento\Framework\Indexer\ScopeResolver\IndexScopeResolver"/> <preference for="Magento\Framework\HTTP\ClientInterface" type="Magento\Framework\HTTP\Client\Curl" /> + <preference for="Magento\Framework\Interception\ConfigLoaderInterface" type="Magento\Framework\Interception\PluginListGenerator" /> + <preference for="Magento\Framework\Interception\ConfigWriterInterface" type="Magento\Framework\Interception\PluginListGenerator" /> <type name="Magento\Framework\Model\ResourceModel\Db\TransactionManager" shared="false" /> <type name="Magento\Framework\Acl\Data\Cache"> <arguments> @@ -431,6 +433,16 @@ </argument> </arguments> </type> + <type name="Magento\Framework\Interception\PluginListGenerator"> + <arguments> + <argument name="reader" xsi:type="object">Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy</argument> + <argument name="logger" xsi:type="object">\Psr\Log\LoggerInterface\Proxy</argument> + <argument name="scopePriorityScheme" xsi:type="array"> + <item name="primary" xsi:type="string">primary</item> + <item name="first" xsi:type="string">global</item> + </argument> + </arguments> + </type> <type name="Magento\Framework\App\ResourceConnection"> <arguments> <argument name="connectionFactory" xsi:type="object">Magento\Framework\App\ResourceConnection\ConnectionFactory</argument> @@ -1820,4 +1832,12 @@ </argument> </arguments> </type> + <type name="Magento\Framework\View\TemplateEngine\Php"> + <arguments> + <argument name="blockVariables" xsi:type="array"> + <item name="secureRenderer" xsi:type="object">Magento\Framework\View\Helper\SecureHtmlRenderer\Proxy</item> + <item name="escaper" xsi:type="object">Magento\Framework\Escaper</item> + </argument> + </arguments> + </type> </config> diff --git a/composer.json b/composer.json index 25e6c6c5435bf..25be12b5bb72f 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "colinmollenhour/credis": "1.11.1", "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.9", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "guzzlehttp/guzzle": "^6.3.3", "laminas/laminas-captcha": "^2.7.1", "laminas/laminas-code": "~3.4.1", @@ -64,13 +64,13 @@ "laminas/laminas-uri": "^2.5.1", "laminas/laminas-validator": "^2.6.0", "laminas/laminas-view": "~2.11.2", - "magento/composer": "1.6.x-dev", + "magento/composer": "1.6.0", "magento/magento-composer-installer": ">=0.1.11", "magento/zendframework1": "~1.14.2", "monolog/monolog": "^1.17", "paragonie/sodium_compat": "^1.6", "pelago/emogrifier": "^3.1.0", - "php-amqplib/php-amqplib": "~2.7.0||~2.10.0", + "php-amqplib/php-amqplib": "~2.10.0", "phpseclib/mcrypt_compat": "1.0.8", "phpseclib/phpseclib": "2.0.*", "ramsey/uuid": "~3.8.0", @@ -88,7 +88,7 @@ "friendsofphp/php-cs-fixer": "~2.16.0", "lusitanian/oauth": "~0.8.10", "magento/magento-coding-standard": "*", - "magento/magento2-functional-testing-framework": "3.0.0-RC4", + "magento/magento2-functional-testing-framework": "^3.0", "pdepend/pdepend": "~2.7.1", "phpcompatibility/php-compatibility": "^9.3", "phpmd/phpmd": "^2.8.0", @@ -194,6 +194,7 @@ "magento/module-login-as-customer": "*", "magento/module-login-as-customer-admin-ui": "*", "magento/module-login-as-customer-api": "*", + "magento/module-login-as-customer-assistance": "*", "magento/module-login-as-customer-frontend-ui": "*", "magento/module-login-as-customer-log": "*", "magento/module-login-as-customer-quote": "*", @@ -205,6 +206,20 @@ "magento/module-media-content-cms": "*", "magento/module-media-gallery": "*", "magento/module-media-gallery-api": "*", + "magento/module-media-gallery-ui": "*", + "magento/module-media-gallery-ui-api": "*", + "magento/module-media-gallery-integration": "*", + "magento/module-media-gallery-synchronization": "*", + "magento/module-media-gallery-synchronization-api": "*", + "magento/module-media-content-synchronization": "*", + "magento/module-media-content-synchronization-api": "*", + "magento/module-media-content-synchronization-catalog": "*", + "magento/module-media-content-synchronization-cms": "*", + "magento/module-media-gallery-metadata": "*", + "magento/module-media-gallery-metadata-api": "*", + "magento/module-media-gallery-catalog-ui": "*", + "magento/module-media-gallery-cms-ui": "*", + "magento/module-media-gallery-catalog-integration": "*", "magento/module-media-gallery-catalog": "*", "magento/module-media-storage": "*", "magento/module-message-queue": "*", @@ -228,12 +243,16 @@ "magento/module-product-video": "*", "magento/module-quote": "*", "magento/module-quote-analytics": "*", + "magento/module-quote-bundle-options": "*", + "magento/module-quote-configurable-options": "*", + "magento/module-quote-downloadable-links": "*", "magento/module-quote-graph-ql": "*", "magento/module-related-product-graph-ql": "*", "magento/module-release-notification": "*", "magento/module-reports": "*", "magento/module-require-js": "*", "magento/module-review": "*", + "magento/module-review-graph-ql": "*", "magento/module-review-analytics": "*", "magento/module-robots": "*", "magento/module-rss": "*", diff --git a/composer.lock b/composer.lock index 39282cb149dc6..c2eed9d87cc00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "4abc523fda743ab847f07f9905bb2731", + "content-hash": "0b51badfd1978bb34febd90226af9e27", "packages": [ { "name": "colinmollenhour/cache-backend-file", @@ -210,16 +210,16 @@ }, { "name": "composer/composer", - "version": "1.10.6", + "version": "1.10.9", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "be81b9c4735362c26876bdbfd3b5bc7e7f711c88" + "reference": "83c3250093d5491600a822e176b107a945baf95a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/be81b9c4735362c26876bdbfd3b5bc7e7f711c88", - "reference": "be81b9c4735362c26876bdbfd3b5bc7e7f711c88", + "url": "https://api.github.com/repos/composer/composer/zipball/83c3250093d5491600a822e176b107a945baf95a", + "reference": "83c3250093d5491600a822e176b107a945baf95a", "shasum": "" }, "require": { @@ -227,7 +227,7 @@ "composer/semver": "^1.0", "composer/spdx-licenses": "^1.2", "composer/xdebug-handler": "^1.1", - "justinrainbow/json-schema": "^3.0 || ^4.0 || ^5.0", + "justinrainbow/json-schema": "^5.2.10", "php": "^5.3.2 || ^7.0", "psr/log": "^1.0", "seld/jsonlint": "^1.4", @@ -238,12 +238,11 @@ "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0" }, "conflict": { - "symfony/console": "2.8.38", - "symfony/phpunit-bridge": "3.4.40" + "symfony/console": "2.8.38" }, "require-dev": { "phpspec/prophecy": "^1.10", - "symfony/phpunit-bridge": "^3.4" + "symfony/phpunit-bridge": "^4.2" }, "suggest": { "ext-openssl": "Enabling the openssl extension allows you to access https URLs for repositories and packages", @@ -287,7 +286,21 @@ "dependency", "package" ], - "time": "2020-05-06T08:28:10+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-07-16T10:57:00+00:00" }, { "name": "composer/semver", @@ -352,16 +365,16 @@ }, { "name": "composer/spdx-licenses", - "version": "1.5.3", + "version": "1.5.4", "source": { "type": "git", "url": "https://github.com/composer/spdx-licenses.git", - "reference": "0c3e51e1880ca149682332770e25977c70cf9dae" + "reference": "6946f785871e2314c60b4524851f3702ea4f2223" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/0c3e51e1880ca149682332770e25977c70cf9dae", - "reference": "0c3e51e1880ca149682332770e25977c70cf9dae", + "url": "https://api.github.com/repos/composer/spdx-licenses/zipball/6946f785871e2314c60b4524851f3702ea4f2223", + "reference": "6946f785871e2314c60b4524851f3702ea4f2223", "shasum": "" }, "require": { @@ -408,20 +421,34 @@ "spdx", "validator" ], - "time": "2020-02-14T07:44:31+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-07-15T15:35:07+00:00" }, { "name": "composer/xdebug-handler", - "version": "1.4.1", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7" + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/1ab9842d69e64fb3a01be6b656501032d1b78cb7", - "reference": "1ab9842d69e64fb3a01be6b656501032d1b78cb7", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", + "reference": "fa2aaf99e2087f013a14f7432c1cd2dd7d8f1f51", "shasum": "" }, "require": { @@ -452,7 +479,21 @@ "Xdebug", "performance" ], - "time": "2020-03-01T12:26:26+00:00" + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2020-06-04T11:16:35+00:00" }, { "name": "container-interop/container-interop", @@ -652,16 +693,16 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.5.3", + "version": "6.5.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e" + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aab4ebd862aa7d04f01a4b51849d657db56d882e", - "reference": "aab4ebd862aa7d04f01a4b51849d657db56d882e", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", + "reference": "9d4290de1cfd701f38099ef7e183b64b4b7b0c5e", "shasum": "" }, "require": { @@ -669,7 +710,7 @@ "guzzlehttp/promises": "^1.0", "guzzlehttp/psr7": "^1.6.1", "php": ">=5.5", - "symfony/polyfill-intl-idn": "^1.11" + "symfony/polyfill-intl-idn": "^1.17.0" }, "require-dev": { "ext-curl": "*", @@ -715,7 +756,7 @@ "rest", "web service" ], - "time": "2020-04-18T10:38:46+00:00" + "time": "2020-06-16T21:01:06+00:00" }, { "name": "guzzlehttp/promises", @@ -841,16 +882,16 @@ }, { "name": "justinrainbow/json-schema", - "version": "5.2.9", + "version": "5.2.10", "source": { "type": "git", "url": "https://github.com/justinrainbow/json-schema.git", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4" + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/44c6787311242a979fa15c704327c20e7221a0e4", - "reference": "44c6787311242a979fa15c704327c20e7221a0e4", + "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", + "reference": "2ba9c8c862ecd5510ed16c6340aa9f6eadb4f31b", "shasum": "" }, "require": { @@ -903,7 +944,7 @@ "json", "schema" ], - "time": "2019-09-25T14:49:45+00:00" + "time": "2020-05-27T16:41:55+00:00" }, { "name": "laminas/laminas-captcha", @@ -1305,6 +1346,12 @@ "BSD-3-Clause" ], "description": "Replace zendframework and zfcampus packages with their Laminas Project equivalents.", + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-05-20T13:45:39+00:00" }, { @@ -1678,16 +1725,16 @@ }, { "name": "laminas/laminas-form", - "version": "2.14.5", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-form.git", - "reference": "3e22e09751cf6ae031be87a44e092e7925ce5b7b" + "reference": "359cd372c565e18a17f32ccfeacdf21bba091ce2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-form/zipball/3e22e09751cf6ae031be87a44e092e7925ce5b7b", - "reference": "3e22e09751cf6ae031be87a44e092e7925ce5b7b", + "url": "https://api.github.com/repos/laminas/laminas-form/zipball/359cd372c565e18a17f32ccfeacdf21bba091ce2", + "reference": "359cd372c565e18a17f32ccfeacdf21bba091ce2", "shasum": "" }, "require": { @@ -1730,8 +1777,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.14.x-dev", - "dev-develop": "2.15.x-dev" + "dev-master": "2.15.x-dev", + "dev-develop": "2.16.x-dev" }, "laminas": { "component": "Laminas\\Form", @@ -1756,20 +1803,26 @@ "form", "laminas" ], - "time": "2020-03-29T12:46:16+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-07-14T13:53:27+00:00" }, { "name": "laminas/laminas-http", - "version": "2.11.2", + "version": "2.12.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-http.git", - "reference": "8c66963b933c80da59433da56a44dfa979f3ec88" + "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-http/zipball/8c66963b933c80da59433da56a44dfa979f3ec88", - "reference": "8c66963b933c80da59433da56a44dfa979f3ec88", + "url": "https://api.github.com/repos/laminas/laminas-http/zipball/48bd06ffa3a6875e2b77d6852405eb7b1589d575", + "reference": "48bd06ffa3a6875e2b77d6852405eb7b1589d575", "shasum": "" }, "require": { @@ -1781,7 +1834,7 @@ "php": "^5.6 || ^7.0" }, "replace": { - "zendframework/zend-http": "self.version" + "zendframework/zend-http": "^2.11.2" }, "require-dev": { "laminas/laminas-coding-standard": "~1.0.0", @@ -1794,8 +1847,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.11.x-dev", - "dev-develop": "2.12.x-dev" + "dev-master": "2.12.x-dev", + "dev-develop": "2.13.x-dev" } }, "autoload": { @@ -1814,7 +1867,13 @@ "http client", "laminas" ], - "time": "2019-12-31T17:02:36+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-06-23T15:14:37+00:00" }, { "name": "laminas/laminas-hydrator", @@ -2200,16 +2259,16 @@ }, { "name": "laminas/laminas-mail", - "version": "2.10.1", + "version": "2.11.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-mail.git", - "reference": "cfe0711446c8d9c392e9fc664c9ccc180fa89005" + "reference": "4c5545637eea3dc745668ddff1028692ed004c4b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/cfe0711446c8d9c392e9fc664c9ccc180fa89005", - "reference": "cfe0711446c8d9c392e9fc664c9ccc180fa89005", + "url": "https://api.github.com/repos/laminas/laminas-mail/zipball/4c5545637eea3dc745668ddff1028692ed004c4b", + "reference": "4c5545637eea3dc745668ddff1028692ed004c4b", "shasum": "" }, "require": { @@ -2239,8 +2298,8 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.10.x-dev", - "dev-develop": "2.11.x-dev" + "dev-master": "2.11.x-dev", + "dev-develop": "2.12.x-dev" }, "laminas": { "component": "Laminas\\Mail", @@ -2262,7 +2321,13 @@ "laminas", "mail" ], - "time": "2020-04-21T16:42:19+00:00" + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], + "time": "2020-06-30T20:17:23+00:00" }, { "name": "laminas/laminas-math", @@ -3254,20 +3319,26 @@ "laminas", "zf" ], + "funding": [ + { + "url": "https://funding.communitybridge.org/projects/laminas-project", + "type": "community_bridge" + } + ], "time": "2020-05-20T16:45:56+00:00" }, { "name": "magento/composer", - "version": "1.6.x-dev", + "version": "1.6.0", "source": { "type": "git", "url": "https://github.com/magento/composer.git", - "reference": "f3e4bec8fc73a97a6cbc391b1b93d4c32566763d" + "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/composer/zipball/f3e4bec8fc73a97a6cbc391b1b93d4c32566763d", - "reference": "f3e4bec8fc73a97a6cbc391b1b93d4c32566763d", + "url": "https://api.github.com/repos/magento/composer/zipball/fcc66f535d631788f2ba160ff547357086d9b2c9", + "reference": "fcc66f535d631788f2ba160ff547357086d9b2c9", "shasum": "" }, "require": { @@ -3290,7 +3361,7 @@ "AFL-3.0" ], "description": "Magento composer library helps to instantiate Composer application and run composer commands.", - "time": "2020-05-08T01:07:09+00:00" + "time": "2020-06-15T17:52:31+00:00" }, { "name": "magento/magento-composer-installer", @@ -3493,6 +3564,16 @@ "logging", "psr-3" ], + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], "time": "2020-05-22T07:31:27+00:00" }, { @@ -3820,16 +3901,16 @@ }, { "name": "phpseclib/phpseclib", - "version": "2.0.27", + "version": "2.0.28", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "34620af4df7d1988d8f0d7e91f6c8a3bf931d8dc" + "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/34620af4df7d1988d8f0d7e91f6c8a3bf931d8dc", - "reference": "34620af4df7d1988d8f0d7e91f6c8a3bf931d8dc", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", + "reference": "d1ca58cf33cb21046d702ae3a7b14fdacd9f3260", "shasum": "" }, "require": { @@ -3908,7 +3989,21 @@ "x.509", "x509" ], - "time": "2020-04-04T23:17:33+00:00" + "funding": [ + { + "url": "https://github.com/terrafrost", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpseclib", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib", + "type": "tidelift" + } + ], + "time": "2020-07-08T09:08:33+00:00" }, { "name": "psr/container", @@ -4271,20 +4366,30 @@ "parser", "validator" ], + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], "time": "2020-04-30T19:05:18+00:00" }, { "name": "seld/phar-utils", - "version": "1.1.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/Seldaek/phar-utils.git", - "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0" + "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8800503d56b9867d43d9c303b9cbcc26016e82f0", - "reference": "8800503d56b9867d43d9c303b9cbcc26016e82f0", + "url": "https://api.github.com/repos/Seldaek/phar-utils/zipball/8674b1d84ffb47cc59a101f5d5a3b61e87d23796", + "reference": "8674b1d84ffb47cc59a101f5d5a3b61e87d23796", "shasum": "" }, "require": { @@ -4315,26 +4420,27 @@ "keywords": [ "phar" ], - "time": "2020-02-14T15:25:33+00:00" + "time": "2020-07-07T18:42:57+00:00" }, { "name": "symfony/console", - "version": "v4.4.8", + "version": "v4.4.10", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7" + "reference": "326b064d804043005526f5a0494cfb49edb59bb0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", - "reference": "10bb3ee3c97308869d53b3e3d03f6ac23ff985f7", + "url": "https://api.github.com/repos/symfony/console/zipball/326b064d804043005526f5a0494cfb49edb59bb0", + "reference": "326b064d804043005526f5a0494cfb49edb59bb0", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php73": "^1.8", + "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1|^2" }, "conflict": { @@ -4391,11 +4497,25 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2020-03-30T11:41:10+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:06:45+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.0", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", @@ -4444,24 +4564,38 @@ ], "description": "Symfony CssSelector Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v4.4.8", + "version": "v4.4.10", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed" + "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abc8e3618bfdb55e44c8c6a00abd333f831bbfed", - "reference": "abc8e3618bfdb55e44c8c6a00abd333f831bbfed", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a5370aaa7807c7a439b21386661ffccf3dff2866", + "reference": "a5370aaa7807c7a439b21386661ffccf3dff2866", "shasum": "" }, "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/event-dispatcher-contracts": "^1.1" }, "conflict": { @@ -4514,24 +4648,38 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:54:36+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T08:37:50+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v1.1.7", + "version": "v1.1.9", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18" + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/c43ab685673fb6c8d84220c77897b1d6cdbe1d18", - "reference": "c43ab685673fb6c8d84220c77897b1d6cdbe1d18", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/84e23fdcd2517bf37aecbd16967e83f0caee25a7", + "reference": "84e23fdcd2517bf37aecbd16967e83f0caee25a7", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=7.1.3" }, "suggest": { "psr/event-dispatcher": "", @@ -4541,6 +4689,10 @@ "extra": { "branch-alias": { "dev-master": "1.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -4572,11 +4724,25 @@ "interoperability", "standards" ], - "time": "2019-09-17T09:54:03+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-06T13:19:58+00:00" }, { "name": "symfony/filesystem", - "version": "v5.1.0", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", @@ -4622,11 +4788,25 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-30T20:35:19+00:00" }, { "name": "symfony/finder", - "version": "v5.1.0", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", @@ -4671,20 +4851,34 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.17.0", + "version": "v1.18.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9" + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/e94c8b1bbe2bc77507a1056cdb06451c75b427f9", - "reference": "e94c8b1bbe2bc77507a1056cdb06451c75b427f9", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", + "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", "shasum": "" }, "require": { @@ -4696,7 +4890,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4729,25 +4927,40 @@ "polyfill", "portable" ], - "time": "2020-05-12T16:14:59+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.17.0", + "version": "v1.18.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "3bff59ea7047e925be6b7f2059d60af31bb46d6a" + "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3bff59ea7047e925be6b7f2059d60af31bb46d6a", - "reference": "3bff59ea7047e925be6b7f2059d60af31bb46d6a", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", + "reference": "bc6549d068d0160e0f10f7a5a23c7d1406b95ebe", "shasum": "" }, "require": { "php": ">=5.3.3", - "symfony/polyfill-mbstring": "^1.3", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php70": "^1.10", "symfony/polyfill-php72": "^1.10" }, "suggest": { @@ -4756,7 +4969,11 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { @@ -4776,6 +4993,10 @@ "name": "Laurent Bassin", "email": "laurent@bassin.info" }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" @@ -4791,40 +5012,61 @@ "portable", "shim" ], - "time": "2020-05-12T16:47:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.17.0", + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "fa79b11539418b02fc5e1897267673ba2c19419c" + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fa79b11539418b02fc5e1897267673ba2c19419c", - "reference": "fa79b11539418b02fc5e1897267673ba2c19419c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", "shasum": "" }, "require": { "php": ">=5.3.3" }, "suggest": { - "ext-mbstring": "For best performance" + "ext-intl": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" }, "files": [ "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -4841,43 +5083,65 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill for the Mbstring extension", + "description": "Symfony polyfill for intl's Normalizer class and related functions", "homepage": "https://symfony.com", "keywords": [ "compatibility", - "mbstring", + "intl", + "normalizer", "polyfill", "portable", "shim" ], - "time": "2020-05-12T16:47:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/polyfill-php72", - "version": "v1.17.0", + "name": "symfony/polyfill-mbstring", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "f048e612a3905f34931127360bdd2def19a5e582" + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/f048e612a3905f34931127360bdd2def19a5e582", - "reference": "f048e612a3905f34931127360bdd2def19a5e582", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", + "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "suggest": { + "ext-mbstring": "For best performance" + }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php72\\": "" + "Symfony\\Polyfill\\Mbstring\\": "" }, "files": [ "bootstrap.php" @@ -4897,42 +5161,62 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "description": "Symfony polyfill for the Mbstring extension", "homepage": "https://symfony.com", "keywords": [ "compatibility", + "mbstring", "polyfill", "portable", "shim" ], - "time": "2020-05-12T16:47:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/polyfill-php73", - "version": "v1.17.0", + "name": "symfony/polyfill-php70", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc" + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a760d8964ff79ab9bf057613a5808284ec852ccc", - "reference": "a760d8964ff79ab9bf057613a5808284ec852ccc", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", + "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", "shasum": "" }, "require": { + "paragonie/random_compat": "~1.0|~2.0|~9.99", "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.17-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" + "Symfony\\Polyfill\\Php70\\": "" }, "files": [ "bootstrap.php" @@ -4955,7 +5239,7 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ "compatibility", @@ -4963,37 +5247,55 @@ "portable", "shim" ], - "time": "2020-05-12T16:47:27+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/process", - "version": "v4.4.8", + "name": "symfony/polyfill-php72", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "4b6a9a4013baa65d409153cbb5a895bf093dc7f4" + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "639447d008615574653fb3bc60d1986d7172eaae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/4b6a9a4013baa65d409153cbb5a895bf093dc7f4", - "reference": "4b6a9a4013baa65d409153cbb5a895bf093dc7f4", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", + "reference": "639447d008615574653fb3bc60d1986d7172eaae", "shasum": "" }, "require": { - "php": "^7.1.3" + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.4-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Component\\Process\\": "" + "Symfony\\Polyfill\\Php72\\": "" }, - "exclude-from-classmap": [ - "/Tests/" + "files": [ + "bootstrap.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -5002,49 +5304,75 @@ ], "authors": [ { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "name": "Nicolas Grekas", + "email": "p@tchwork.com" }, { "name": "Symfony Community", "homepage": "https://symfony.com/contributors" } ], - "description": "Symfony Process Component", + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", "homepage": "https://symfony.com", - "time": "2020-04-15T15:56:18+00:00" + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "symfony/service-contracts", - "version": "v2.0.1", + "name": "symfony/polyfill-php73", + "version": "v1.18.0", "source": { "type": "git", - "url": "https://github.com/symfony/service-contracts.git", - "reference": "144c5e51266b281231e947b51223ba14acf1a749" + "url": "https://github.com/symfony/polyfill-php73.git", + "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/144c5e51266b281231e947b51223ba14acf1a749", - "reference": "144c5e51266b281231e947b51223ba14acf1a749", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", "shasum": "" }, "require": { - "php": "^7.2.5", - "psr/container": "^1.0" - }, - "suggest": { - "symfony/service-implementation": "" + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { "psr-4": { - "Symfony\\Contracts\\Service\\": "" - } + "Symfony\\Polyfill\\Php73\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -5060,20 +5388,251 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Generic abstractions related to writing services", + "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" + "compatibility", + "polyfill", + "portable", + "shim" ], - "time": "2019-11-18T17:27:11+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" }, { - "name": "tedivm/jshrink", + "name": "symfony/polyfill-php80", + "version": "v1.18.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "shasum": "" + }, + "require": { + "php": ">=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.18-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-14T12:35:20+00:00" + }, + { + "name": "symfony/process", + "version": "v4.4.10", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c714958428a85c86ab97e3a0c96db4c4f381b7f5", + "reference": "c714958428a85c86ab97e3a0c96db4c4f381b7f5", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.4-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-30T20:06:45+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/58c7475e5457c5492c26cc740cc0ad7464be9442", + "reference": "58c7475e5457c5492c26cc740cc0ad7464be9442", + "shasum": "" + }, + "require": { + "php": ">=7.2.5", + "psr/container": "^1.0" + }, + "suggest": { + "symfony/service-implementation": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-07-06T13:23:11+00:00" + }, + { + "name": "tedivm/jshrink", "version": "v1.3.3", "source": { "type": "git", @@ -5219,16 +5778,16 @@ }, { "name": "webonyx/graphql-php", - "version": "v0.13.8", + "version": "v0.13.9", "source": { "type": "git", "url": "https://github.com/webonyx/graphql-php.git", - "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8" + "reference": "d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/6829ae58f4c59121df1f86915fb9917a2ec595e8", - "reference": "6829ae58f4c59121df1f86915fb9917a2ec595e8", + "url": "https://api.github.com/repos/webonyx/graphql-php/zipball/d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3", + "reference": "d9a94fddcad0a35d4bced212b8a44ad1bc59bdf3", "shasum": "" }, "require": { @@ -5267,7 +5826,13 @@ "api", "graphql" ], - "time": "2019-08-25T10:32:47+00:00" + "funding": [ + { + "url": "https://opencollective.com/webonyx-graphql-php", + "type": "open_collective" + } + ], + "time": "2020-07-02T05:49:25+00:00" }, { "name": "wikimedia/less.php", @@ -5488,16 +6053,16 @@ }, { "name": "aws/aws-sdk-php", - "version": "3.140.2", + "version": "3.147.1", "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "7e37960c1103ee211932be51b2282b41c948a5f0" + "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/7e37960c1103ee211932be51b2282b41c948a5f0", - "reference": "7e37960c1103ee211932be51b2282b41c948a5f0", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8a561a4a1645ccdd06413a4f2defe55d35e0eecc", + "reference": "8a561a4a1645ccdd06413a4f2defe55d35e0eecc", "shasum": "" }, "require": { @@ -5520,6 +6085,7 @@ "ext-pcntl": "*", "ext-sockets": "*", "nette/neon": "^2.3", + "paragonie/random_compat": ">= 2", "phpunit/phpunit": "^4.8.35|^5.4.3", "psr/cache": "^1.0", "psr/simple-cache": "^1.0", @@ -5568,7 +6134,7 @@ "s3", "sdk" ], - "time": "2020-06-05T18:12:25+00:00" + "time": "2020-07-20T18:18:31+00:00" }, { "name": "beberlei/assert", @@ -5786,16 +6352,16 @@ }, { "name": "codeception/codeception", - "version": "4.1.5", + "version": "4.1.6", "source": { "type": "git", "url": "https://github.com/Codeception/Codeception.git", - "reference": "24f2345329b1059f1208f65581fc632a4a6e5a55" + "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Codeception/zipball/24f2345329b1059f1208f65581fc632a4a6e5a55", - "reference": "24f2345329b1059f1208f65581fc632a4a6e5a55", + "url": "https://api.github.com/repos/Codeception/Codeception/zipball/5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", + "reference": "5515b6a6c6f1e1c909aaff2e5f3a15c177dfd1a9", "shasum": "" }, "require": { @@ -5867,7 +6433,13 @@ "functional testing", "unit testing" ], - "time": "2020-05-24T13:58:47+00:00" + "funding": [ + { + "url": "https://opencollective.com/codeception", + "type": "open_collective" + } + ], + "time": "2020-06-07T16:31:51+00:00" }, { "name": "codeception/lib-asserts", @@ -6110,16 +6682,16 @@ }, { "name": "codeception/stub", - "version": "3.6.1", + "version": "3.7.0", "source": { "type": "git", "url": "https://github.com/Codeception/Stub.git", - "reference": "a3ba01414cbee76a1bced9f9b6b169cc8d203880" + "reference": "468dd5fe659f131fc997f5196aad87512f9b1304" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Codeception/Stub/zipball/a3ba01414cbee76a1bced9f9b6b169cc8d203880", - "reference": "a3ba01414cbee76a1bced9f9b6b169cc8d203880", + "url": "https://api.github.com/repos/Codeception/Stub/zipball/468dd5fe659f131fc997f5196aad87512f9b1304", + "reference": "468dd5fe659f131fc997f5196aad87512f9b1304", "shasum": "" }, "require": { @@ -6136,7 +6708,7 @@ "MIT" ], "description": "Flexible Stub wrapper for PHPUnit's Mock Builder", - "time": "2020-02-07T18:42:28+00:00" + "time": "2020-07-03T15:54:43+00:00" }, { "name": "csharpru/vault-php", @@ -6361,16 +6933,16 @@ }, { "name": "doctrine/cache", - "version": "1.10.1", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "35a4a70cd94e09e2259dfae7488afc6b474ecbd3" + "reference": "13e3381b25847283a91948d04640543941309727" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/35a4a70cd94e09e2259dfae7488afc6b474ecbd3", - "reference": "35a4a70cd94e09e2259dfae7488afc6b474ecbd3", + "url": "https://api.github.com/repos/doctrine/cache/zipball/13e3381b25847283a91948d04640543941309727", + "reference": "13e3381b25847283a91948d04640543941309727", "shasum": "" }, "require": { @@ -6439,7 +7011,21 @@ "redis", "xcache" ], - "time": "2020-05-27T16:24:54+00:00" + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Fcache", + "type": "tidelift" + } + ], + "time": "2020-07-07T18:54:01+00:00" }, { "name": "doctrine/inflector", @@ -6562,6 +7148,20 @@ "constructor", "instantiate" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", + "type": "tidelift" + } + ], "time": "2020-05-29T17:27:14+00:00" }, { @@ -6624,20 +7224,34 @@ "parser", "php" ], + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], "time": "2020-05-25T17:44:05+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.16.3", + "version": "v2.16.4", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "83baf823a33a1cbd5416c8626935cf3f843c10b0" + "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/83baf823a33a1cbd5416c8626935cf3f843c10b0", - "reference": "83baf823a33a1cbd5416c8626935cf3f843c10b0", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/1023c3458137ab052f6ff1e09621a721bfdeca13", + "reference": "1023c3458137ab052f6ff1e09621a721bfdeca13", "shasum": "" }, "require": { @@ -6669,12 +7283,12 @@ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.1", "phpunit/phpunit": "^5.7.27 || ^6.5.14 || ^7.1", "phpunitgoodpractices/traits": "^1.8", - "symfony/phpunit-bridge": "^4.3 || ^5.0", + "symfony/phpunit-bridge": "^5.1", "symfony/yaml": "^3.0 || ^4.0 || ^5.0" }, "suggest": { "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "ext-mbstring": "For handling non-UTF8 characters.", "php-cs-fixer/phpunit-constraint-isidenticalstring": "For IsIdenticalString constraint.", "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "For XmlMatchesXsd constraint.", "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." @@ -6715,7 +7329,13 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2020-04-15T18:51:10+00:00" + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2020-06-27T23:57:46+00:00" }, { "name": "jms/metadata", @@ -6973,6 +7593,12 @@ "sftp", "storage" ], + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], "time": "2020-05-18T15:13:39+00:00" }, { @@ -7083,16 +7709,16 @@ }, { "name": "magento/magento2-functional-testing-framework", - "version": "3.0.0-RC4", + "version": "3.0.0", "source": { "type": "git", "url": "https://github.com/magento/magento2-functional-testing-framework.git", - "reference": "34781ccc7385993b1e5bc9182e6ddddde7f2769f" + "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/34781ccc7385993b1e5bc9182e6ddddde7f2769f", - "reference": "34781ccc7385993b1e5bc9182e6ddddde7f2769f", + "url": "https://api.github.com/repos/magento/magento2-functional-testing-framework/zipball/8d98efa7434a30ab9e82ef128c430ef8e3a50699", + "reference": "8d98efa7434a30ab9e82ef128c430ef8e3a50699", "shasum": "" }, "require": { @@ -7117,6 +7743,7 @@ "spomky-labs/otphp": "^10.0", "symfony/console": "^4.4", "symfony/finder": "^5.0", + "symfony/http-foundation": "^5.0", "symfony/mime": "^5.0", "symfony/process": "^4.4", "vlucas/phpdotenv": "^2.4", @@ -7168,7 +7795,7 @@ "magento", "testing" ], - "time": "2020-06-08T18:17:54+00:00" + "time": "2020-07-09T21:26:19+00:00" }, { "name": "mikey179/vfsstream", @@ -7321,20 +7948,20 @@ }, { "name": "myclabs/deep-copy", - "version": "1.9.5", + "version": "1.10.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef" + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/b2c28789e80a97badd14145fda39b545d83ca3ef", - "reference": "b2c28789e80a97badd14145fda39b545d83ca3ef", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", "shasum": "" }, "require": { - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "replace": { "myclabs/deep-copy": "self.version" @@ -7365,7 +7992,13 @@ "object", "object graph" ], - "time": "2020-01-17T21:11:47+00:00" + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2020-06-29T13:22:24+00:00" }, { "name": "paragonie/constant_time_encoding", @@ -7802,25 +8435,25 @@ }, { "name": "phpdocumentor/reflection-common", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b" + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/6568f4687e5b41b054365f9ae03fcb1ed5f2069b", - "reference": "6568f4687e5b41b054365f9ae03fcb1ed5f2069b", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", "shasum": "" }, "require": { - "php": ">=7.1" + "php": "^7.2 || ^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.x-dev" + "dev-2.x": "2.x-dev" } }, "autoload": { @@ -7847,32 +8480,31 @@ "reflection", "static analysis" ], - "time": "2020-04-27T09:25:28+00:00" + "time": "2020-06-27T09:03:43+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.1.0", + "version": "5.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e" + "reference": "3170448f5769fe19f456173d833734e0ff1b84df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", - "reference": "cd72d394ca794d3466a3b2fc09d5a6c1dc86b47e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/3170448f5769fe19f456173d833734e0ff1b84df", + "reference": "3170448f5769fe19f456173d833734e0ff1b84df", "shasum": "" }, "require": { - "ext-filter": "^7.1", - "php": "^7.2", - "phpdocumentor/reflection-common": "^2.0", - "phpdocumentor/type-resolver": "^1.0", - "webmozart/assert": "^1" + "ext-filter": "*", + "php": "^7.2 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.3", + "webmozart/assert": "^1.9.1" }, "require-dev": { - "doctrine/instantiator": "^1", - "mockery/mockery": "^1" + "mockery/mockery": "~1.3.2" }, "type": "library", "extra": { @@ -7900,34 +8532,33 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2020-02-22T12:28:44+00:00" + "time": "2020-07-20T20:05:34+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "1.1.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95" + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/7462d5f123dfc080dfdf26897032a6513644fc95", - "reference": "7462d5f123dfc080dfdf26897032a6513644fc95", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e878a14a65245fbe78f8080eba03b47c3b705651", + "reference": "e878a14a65245fbe78f8080eba03b47c3b705651", "shasum": "" }, "require": { - "php": "^7.2", + "php": "^7.2 || ^8.0", "phpdocumentor/reflection-common": "^2.0" }, "require-dev": { - "ext-tokenizer": "^7.2", - "mockery/mockery": "~1" + "ext-tokenizer": "*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-1.x": "1.x-dev" } }, "autoload": { @@ -7946,7 +8577,7 @@ } ], "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", - "time": "2020-02-18T18:59:58+00:00" + "time": "2020-06-27T10:12:23+00:00" }, { "name": "phpmd/phpmd", @@ -8020,24 +8651,24 @@ }, { "name": "phpoption/phpoption", - "version": "1.7.3", + "version": "1.7.5", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae" + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/4acfd6a4b33a509d8c88f50e5222f734b6aeebae", - "reference": "4acfd6a4b33a509d8c88f50e5222f734b6aeebae", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525", + "reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525", "shasum": "" }, "require": { "php": "^5.5.9 || ^7.0 || ^8.0" }, "require-dev": { - "bamarni/composer-bin-plugin": "^1.3", - "phpunit/phpunit": "^4.8.35 || ^5.0 || ^6.0 || ^7.0" + "bamarni/composer-bin-plugin": "^1.4.1", + "phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0" }, "type": "library", "extra": { @@ -8071,37 +8702,47 @@ "php", "type" ], - "time": "2020-03-21T18:07:53+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2020-07-20T17:29:33+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.10.3", + "version": "1.11.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "451c3cd1418cf640de218914901e51b064abb093" + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/451c3cd1418cf640de218914901e51b064abb093", - "reference": "451c3cd1418cf640de218914901e51b064abb093", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", + "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", - "sebastian/comparator": "^1.2.3|^2.0|^3.0|^4.0", - "sebastian/recursion-context": "^1.0|^2.0|^3.0|^4.0" + "doctrine/instantiator": "^1.2", + "php": "^7.2", + "phpdocumentor/reflection-docblock": "^5.0", + "sebastian/comparator": "^3.0 || ^4.0", + "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { - "phpspec/phpspec": "^2.5 || ^3.2", - "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" + "phpspec/phpspec": "^6.0", + "phpunit/phpunit": "^8.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.10.x-dev" + "dev-master": "1.11.x-dev" } }, "autoload": { @@ -8134,7 +8775,7 @@ "spy", "stub" ], - "time": "2020-03-05T15:02:03+00:00" + "time": "2020-07-08T12:44:21+00:00" }, { "name": "phpstan/phpstan", @@ -8176,20 +8817,34 @@ "MIT" ], "description": "PHPStan - PHP Static Analysis Tool", + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], "time": "2020-05-05T12:55:44+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "8.0.1", + "version": "8.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "31e94ccc084025d6abee0585df533eb3a792b96a" + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/31e94ccc084025d6abee0585df533eb3a792b96a", - "reference": "31e94ccc084025d6abee0585df533eb3a792b96a", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ca6647ffddd2add025ab3f21644a441d7c146cdc", + "reference": "ca6647ffddd2add025ab3f21644a441d7c146cdc", "shasum": "" }, "require": { @@ -8240,24 +8895,30 @@ "testing", "xunit" ], - "time": "2020-02-19T13:41:19+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-05-23T08:02:54+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.1", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4" + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4", - "reference": "4ac5b3e13df14829daa60a2eb4fdd2f2b7d33cf4", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", + "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -8290,24 +8951,30 @@ "filesystem", "iterator" ], - "time": "2020-04-18T05:02:12+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-11T05:18:21+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a" + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7579d5a1ba7f3ac11c80004d205877911315ae7a", - "reference": "7579d5a1ba7f3ac11c80004d205877911315ae7a", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f6eedfed1085dd1f4c599629459a0277d25f9a66", + "reference": "f6eedfed1085dd1f4c599629459a0277d25f9a66", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "ext-pcntl": "*", @@ -8343,26 +9010,35 @@ "keywords": [ "process" ], - "time": "2020-02-07T06:06:11+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:53:53+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346" + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/526dc996cc0ebdfa428cd2dfccd79b7b53fee346", - "reference": "526dc996cc0ebdfa428cd2dfccd79b7b53fee346", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", + "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, - "type": "library", + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "type": "library", "extra": { "branch-alias": { "dev-master": "2.0-dev" @@ -8389,7 +9065,13 @@ "keywords": [ "template" ], - "time": "2020-02-01T07:43:44+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T11:55:37+00:00" }, { "name": "phpunit/php-timer", @@ -8438,25 +9120,31 @@ "keywords": [ "timer" ], + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-04-20T06:00:37+00:00" }, { "name": "phpunit/php-token-stream", - "version": "4.0.1", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c" + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c", - "reference": "cdc0db5aed8fbfaf475fbd95bfd7bab83c7a779c", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5672711b6b07b14d5ab694e700c62eeb82fcf374", + "reference": "5672711b6b07b14d5ab694e700c62eeb82fcf374", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -8487,7 +9175,13 @@ "keywords": [ "tokenizer" ], - "time": "2020-05-06T09:56:31+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-27T06:36:25+00:00" }, { "name": "phpunit/phpunit", @@ -8575,6 +9269,16 @@ "testing", "xunit" ], + "funding": [ + { + "url": "https://phpunit.de/donate.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], "time": "2020-05-22T13:54:05+00:00" }, { @@ -8673,20 +9377,20 @@ }, { "name": "sebastian/code-unit", - "version": "1.0.2", + "version": "1.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "ac958085bc19fcd1d36425c781ef4cbb5b06e2a5" + "reference": "c1e2df332c905079980b119c4db103117e5e5c90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/ac958085bc19fcd1d36425c781ef4cbb5b06e2a5", - "reference": "ac958085bc19fcd1d36425c781ef4cbb5b06e2a5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", + "reference": "c1e2df332c905079980b119c4db103117e5e5c90", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -8715,24 +9419,30 @@ ], "description": "Collection of value objects that represent the PHP code units", "homepage": "https://github.com/sebastianbergmann/code-unit", - "time": "2020-04-30T05:58:10+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:50:45+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e" + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5b5dbe0044085ac41df47e79d34911a15b96d82e", - "reference": "5b5dbe0044085ac41df47e79d34911a15b96d82e", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", + "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -8760,24 +9470,30 @@ ], "description": "Looks up which function or method a line of code belongs to", "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", - "time": "2020-02-07T06:20:13+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:04:00+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.0", + "version": "4.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8" + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85b3435da967696ed618ff745f32be3ff4a2b8e8", - "reference": "85b3435da967696ed618ff745f32be3ff4a2b8e8", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", + "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", "shasum": "" }, "require": { - "php": "^7.3", + "php": "^7.3 || ^8.0", "sebastian/diff": "^4.0", "sebastian/exporter": "^4.0" }, @@ -8824,24 +9540,30 @@ "compare", "equality" ], - "time": "2020-02-07T06:08:51+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:05:46+00:00" }, { "name": "sebastian/diff", - "version": "4.0.1", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3e523c576f29dacecff309f35e4cc5a5c168e78a" + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3e523c576f29dacecff309f35e4cc5a5c168e78a", - "reference": "3e523c576f29dacecff309f35e4cc5a5c168e78a", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", + "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0", @@ -8880,24 +9602,30 @@ "unidiff", "unified diff" ], - "time": "2020-05-08T05:01:12+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-30T04:46:02+00:00" }, { "name": "sebastian/environment", - "version": "5.1.0", + "version": "5.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c" + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/c753f04d68cd489b6973cf9b4e505e191af3b05c", - "reference": "c753f04d68cd489b6973cf9b4e505e191af3b05c", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", + "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -8933,29 +9661,35 @@ "environment", "hhvm" ], - "time": "2020-04-14T13:36:52+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:07:24+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "80c26562e964016538f832f305b2286e1ec29566" + "reference": "571d721db4aec847a0e59690b954af33ebf9f023" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/80c26562e964016538f832f305b2286e1ec29566", - "reference": "80c26562e964016538f832f305b2286e1ec29566", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", + "reference": "571d721db4aec847a0e59690b954af33ebf9f023", "shasum": "" }, "require": { - "php": "^7.3", + "php": "^7.3 || ^8.0", "sebastian/recursion-context": "^4.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.2" }, "type": "library", "extra": { @@ -9000,7 +9734,13 @@ "export", "exporter" ], - "time": "2020-02-07T06:10:52+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:08:55+00:00" }, { "name": "sebastian/finder-facade", @@ -9104,20 +9844,20 @@ }, { "name": "sebastian/object-enumerator", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "e67516b175550abad905dc952f43285957ef4363" + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67516b175550abad905dc952f43285957ef4363", - "reference": "e67516b175550abad905dc952f43285957ef4363", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", + "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", "shasum": "" }, "require": { - "php": "^7.3", + "php": "^7.3 || ^8.0", "sebastian/object-reflector": "^2.0", "sebastian/recursion-context": "^4.0" }, @@ -9147,24 +9887,30 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2020-02-07T06:12:23+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:11:32+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.0", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7" + "reference": "127a46f6b057441b201253526f81d5406d6c7840" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", - "reference": "f4fd0835cabb0d4a6546d9fe291e5740037aa1e7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", + "reference": "127a46f6b057441b201253526f81d5406d6c7840", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9192,7 +9938,13 @@ ], "description": "Allows reflection of object attributes, including inherited and non-public ones", "homepage": "https://github.com/sebastianbergmann/object-reflector/", - "time": "2020-02-07T06:19:40+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:12:55+00:00" }, { "name": "sebastian/phpcpd", @@ -9247,20 +9999,20 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.0", + "version": "4.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cdd86616411fc3062368b720b0425de10bd3d579" + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cdd86616411fc3062368b720b0425de10bd3d579", - "reference": "cdd86616411fc3062368b720b0425de10bd3d579", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", + "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9296,24 +10048,30 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2020-02-07T06:18:20+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:14:17+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.0", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98" + "reference": "0653718a5a629b065e91f774595267f8dc32e213" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", - "reference": "8c98bf0dfa1f9256d0468b9803a1e1df31b6fa98", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", + "reference": "0653718a5a629b065e91f774595267f8dc32e213", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { "phpunit/phpunit": "^9.0" @@ -9341,32 +10099,38 @@ ], "description": "Provides a list of PHP built-in functions that operate on resources", "homepage": "https://www.github.com/sebastianbergmann/resource-operations", - "time": "2020-02-07T06:13:02+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:16:22+00:00" }, { "name": "sebastian/type", - "version": "2.0.0", + "version": "2.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "9e8f42f740afdea51f5f4e8cec2035580e797ee1" + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/9e8f42f740afdea51f5f4e8cec2035580e797ee1", - "reference": "9e8f42f740afdea51f5f4e8cec2035580e797ee1", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", + "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^9.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.2-dev" } }, "autoload": { @@ -9387,24 +10151,30 @@ ], "description": "Collection of value objects that represent the types of the PHP type system", "homepage": "https://github.com/sebastianbergmann/type", - "time": "2020-02-07T06:13:43+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-05T08:31:53+00:00" }, { "name": "sebastian/version", - "version": "3.0.0", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "0411bde656dce64202b39c2f4473993a9081d39e" + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/0411bde656dce64202b39c2f4473993a9081d39e", - "reference": "0411bde656dce64202b39c2f4473993a9081d39e", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", + "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", "shasum": "" }, "require": { - "php": "^7.3" + "php": "^7.3 || ^8.0" }, "type": "library", "extra": { @@ -9430,7 +10200,13 @@ ], "description": "Library that helps with managing the version number of Git-hosted PHP projects", "homepage": "https://github.com/sebastianbergmann/version", - "time": "2020-01-21T06:36:37+00:00" + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-06-26T12:18:43+00:00" }, { "name": "spomky-labs/otphp", @@ -9556,22 +10332,24 @@ }, { "name": "symfony/config", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "db1674e1a261148429f123871f30d211992294e7" + "reference": "b8623ef3d99fe62a34baf7a111b576216965f880" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/db1674e1a261148429f123871f30d211992294e7", - "reference": "db1674e1a261148429f123871f30d211992294e7", + "url": "https://api.github.com/repos/symfony/config/zipball/b8623ef3d99fe62a34baf7a111b576216965f880", + "reference": "b8623ef3d99fe62a34baf7a111b576216965f880", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", "symfony/filesystem": "^4.4|^5.0", - "symfony/polyfill-ctype": "~1.8" + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.15" }, "conflict": { "symfony/finder": "<4.4" @@ -9589,7 +10367,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -9616,29 +10394,45 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2020-04-15T15:59:10+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-23T13:08:13+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "92d8b3bd896a87cdd8aba0a3dd041bc072e8cfba" + "reference": "6508423eded583fc07e88a0172803e1a62f0310c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/92d8b3bd896a87cdd8aba0a3dd041bc072e8cfba", - "reference": "92d8b3bd896a87cdd8aba0a3dd041bc072e8cfba", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/6508423eded583fc07e88a0172803e1a62f0310c", + "reference": "6508423eded583fc07e88a0172803e1a62f0310c", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "psr/container": "^1.0", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1.6|^2" }, "conflict": { - "symfony/config": "<5.0", + "symfony/config": "<5.1", "symfony/finder": "<4.4", "symfony/proxy-manager-bridge": "<4.4", "symfony/yaml": "<4.4" @@ -9648,7 +10442,7 @@ "symfony/service-implementation": "1.0" }, "require-dev": { - "symfony/config": "^5.0", + "symfony/config": "^5.1", "symfony/expression-language": "^4.4|^5.0", "symfony/yaml": "^4.4|^5.0" }, @@ -9662,7 +10456,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -9689,20 +10483,34 @@ ], "description": "Symfony DependencyInjection Component", "homepage": "https://symfony.com", - "time": "2020-04-28T17:58:55+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-12T08:11:32+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.1.2", + "version": "v2.1.3", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337" + "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", - "reference": "dd99cb3a0aff6cadd2a8d7d7ed72c2161e218337", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5e20b83385a77593259c9f8beb2c43cd03b2ac14", + "reference": "5e20b83385a77593259c9f8beb2c43cd03b2ac14", "shasum": "" }, "require": { @@ -9712,6 +10520,10 @@ "extra": { "branch-alias": { "dev-master": "2.1-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" } }, "autoload": { @@ -9735,20 +10547,34 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "time": "2020-05-27T08:34:37+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-06T08:49:21+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.1.0", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e0d853bddc2b2cfb0d67b0b4496c03fffe1d37fa" + "reference": "f93055171b847915225bd5b0a5792888419d8d75" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e0d853bddc2b2cfb0d67b0b4496c03fffe1d37fa", - "reference": "e0d853bddc2b2cfb0d67b0b4496c03fffe1d37fa", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f93055171b847915225bd5b0a5792888419d8d75", + "reference": "f93055171b847915225bd5b0a5792888419d8d75", "shasum": "" }, "require": { @@ -9796,20 +10622,34 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2020-05-24T12:18:07+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-15T06:52:54+00:00" }, { "name": "symfony/mime", - "version": "v5.1.0", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "56261f89385f9d13cf843a5101ac72131190bc91" + "reference": "c0c418f05e727606e85b482a8591519c4712cf45" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/56261f89385f9d13cf843a5101ac72131190bc91", - "reference": "56261f89385f9d13cf843a5101ac72131190bc91", + "url": "https://api.github.com/repos/symfony/mime/zipball/c0c418f05e727606e85b482a8591519c4712cf45", + "reference": "c0c418f05e727606e85b482a8591519c4712cf45", "shasum": "" }, "require": { @@ -9859,29 +10699,45 @@ "mime", "mime-type" ], - "time": "2020-05-25T12:33:44+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-06-09T15:07:35+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1" + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/3707e3caeff2b797c0bfaadd5eba723dd44e6bf1", - "reference": "3707e3caeff2b797c0bfaadd5eba723dd44e6bf1", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/663f5dd5e14057d1954fe721f9709d35837f2447", + "reference": "663f5dd5e14057d1954fe721f9709d35837f2447", "shasum": "" }, "require": { - "php": "^7.2.5" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1", + "symfony/polyfill-php80": "^1.15" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -9913,151 +10769,44 @@ "configuration", "options" ], - "time": "2020-04-06T10:40:56+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.17.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "82225c2d7d23d7e70515496d249c0152679b468e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/82225c2d7d23d7e70515496d249c0152679b468e", - "reference": "82225c2d7d23d7e70515496d249c0152679b468e", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "~1.0|~2.0|~9.99", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.17-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2020-05-12T16:47:27+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.17.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "5e30b2799bc1ad68f7feb62b60a73743589438dd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/5e30b2799bc1ad68f7feb62b60a73743589438dd", - "reference": "5e30b2799bc1ad68f7feb62b60a73743589438dd", - "shasum": "" - }, - "require": { - "php": ">=7.0.8" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.17-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" + "url": "https://symfony.com/sponsor", + "type": "custom" }, { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" + "url": "https://github.com/fabpot", + "type": "github" }, { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" } ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "time": "2020-05-12T16:47:27+00:00" + "time": "2020-05-23T13:08:13+00:00" }, { "name": "symfony/stopwatch", - "version": "v5.0.8", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73" + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/a1d86d30d4522423afc998f32404efa34fcf5a73", - "reference": "a1d86d30d4522423afc998f32404efa34fcf5a73", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/0f7c58cf81dbb5dd67d423a89d577524a2ec0323", + "reference": "0f7c58cf81dbb5dd67d423a89d577524a2ec0323", "shasum": "" }, "require": { - "php": "^7.2.5", + "php": ">=7.2.5", "symfony/service-contracts": "^1.0|^2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } }, "autoload": { @@ -10084,11 +10833,25 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2020-03-27T16:56:45+00:00" + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-05-20T17:43:50+00:00" }, { "name": "symfony/yaml", - "version": "v5.1.0", + "version": "v5.1.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", @@ -10147,20 +10910,34 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], "time": "2020-05-20T17:43:50+00:00" }, { "name": "thecodingmachine/safe", - "version": "v1.1.1", + "version": "v1.1.3", "source": { "type": "git", "url": "https://github.com/thecodingmachine/safe.git", - "reference": "04f9ffae372a9816d4472dfb7bcf6126b623a9df" + "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/04f9ffae372a9816d4472dfb7bcf6126b623a9df", - "reference": "04f9ffae372a9816d4472dfb7bcf6126b623a9df", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/9f277171e296a3c8629c04ac93ec95ff0f208ccb", + "reference": "9f277171e296a3c8629c04ac93ec95ff0f208ccb", "shasum": "" }, "require": { @@ -10280,7 +11057,7 @@ "MIT" ], "description": "PHP core functions that throw exceptions instead of returning FALSE on error", - "time": "2020-05-04T15:25:36+00:00" + "time": "2020-07-10T09:34:29+00:00" }, { "name": "theseer/fdomdocument", @@ -10324,23 +11101,23 @@ }, { "name": "theseer/tokenizer", - "version": "1.1.3", + "version": "1.2.0", "source": { "type": "git", "url": "https://github.com/theseer/tokenizer.git", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9" + "reference": "75a63c33a8577608444246075ea0af0d052e452a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theseer/tokenizer/zipball/11336f6f84e16a720dae9d8e6ed5019efa85a0f9", - "reference": "11336f6f84e16a720dae9d8e6ed5019efa85a0f9", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", + "reference": "75a63c33a8577608444246075ea0af0d052e452a", "shasum": "" }, "require": { "ext-dom": "*", "ext-tokenizer": "*", "ext-xmlwriter": "*", - "php": "^7.0" + "php": "^7.2 || ^8.0" }, "type": "library", "autoload": { @@ -10360,30 +11137,36 @@ } ], "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", - "time": "2019-06-13T22:48:21+00:00" + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2020-07-12T23:59:07+00:00" }, { "name": "vlucas/phpdotenv", - "version": "v2.6.4", + "version": "v2.6.6", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "67d472b1794c986381a8950e4958e1adb779d561" + "reference": "e1d57f62db3db00d9139078cbedf262280701479" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/67d472b1794c986381a8950e4958e1adb779d561", - "reference": "67d472b1794c986381a8950e4958e1adb779d561", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/e1d57f62db3db00d9139078cbedf262280701479", + "reference": "e1d57f62db3db00d9139078cbedf262280701479", "shasum": "" }, "require": { "php": "^5.3.9 || ^7.0 || ^8.0", - "symfony/polyfill-ctype": "^1.9" + "symfony/polyfill-ctype": "^1.17" }, "require-dev": { "ext-filter": "*", "ext-pcre": "*", - "phpunit/phpunit": "^4.8.35 || ^5.0" + "phpunit/phpunit": "^4.8.35 || ^5.7.27" }, "suggest": { "ext-filter": "Required to use the boolean validator.", @@ -10422,27 +11205,38 @@ "env", "environment" ], - "time": "2020-05-02T13:38:00+00:00" + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2020-07-14T17:54:18+00:00" }, { "name": "webmozart/assert", - "version": "1.8.0", + "version": "1.9.1", "source": { "type": "git", "url": "https://github.com/webmozart/assert.git", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6" + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozart/assert/zipball/ab2cb0b3b559010b75981b1bdce728da3ee90ad6", - "reference": "ab2cb0b3b559010b75981b1bdce728da3ee90ad6", + "url": "https://api.github.com/repos/webmozart/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", + "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0", + "php": "^5.3.3 || ^7.0 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { + "phpstan/phpstan": "<0.12.20", "vimeo/psalm": "<3.9.1" }, "require-dev": { @@ -10470,7 +11264,7 @@ "check", "validate" ], - "time": "2020-04-18T12:12:48+00:00" + "time": "2020-07-08T17:02:28+00:00" }, { "name": "weew/helpers-array", @@ -10512,10 +11306,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "magento/composer": 20, - "magento/magento2-functional-testing-framework": 5 - }, + "stability-flags": [], "prefer-stable": true, "prefer-lowest": false, "platform": { @@ -10537,5 +11328,6 @@ "ext-zip": "*", "lib-libxml": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "1.1.0" } diff --git a/dev/tests/acceptance/tests/_data/gif.gif b/dev/tests/acceptance/tests/_data/gif.gif index 0b082504ab982..f0937bc132829 100644 Binary files a/dev/tests/acceptance/tests/_data/gif.gif and b/dev/tests/acceptance/tests/_data/gif.gif differ diff --git a/dev/tests/acceptance/tests/_data/magento3.jpg b/dev/tests/acceptance/tests/_data/magento3.jpg index 79ed12ec0aea4..6dc8cd69e41c1 100644 Binary files a/dev/tests/acceptance/tests/_data/magento3.jpg and b/dev/tests/acceptance/tests/_data/magento3.jpg differ diff --git a/dev/tests/acceptance/tests/_data/png.png b/dev/tests/acceptance/tests/_data/png.png index c83255dcf558d..4ec47267e8125 100644 Binary files a/dev/tests/acceptance/tests/_data/png.png and b/dev/tests/acceptance/tests/_data/png.png differ diff --git a/dev/tests/acceptance/tests/functional/Magento/ConfigurableProductCatalogSearch/etc/csp_whitelist.xml b/dev/tests/acceptance/tests/functional/Magento/ConfigurableProductCatalogSearch/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..35af3809cb225 --- /dev/null +++ b/dev/tests/acceptance/tests/functional/Magento/ConfigurableProductCatalogSearch/etc/csp_whitelist.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp:etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="signifyd_cdn" type="host">cdn-scripts.signifyd.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml index f4af91e02b2a1..b0b114890041b 100644 --- a/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml +++ b/dev/tests/api-functional/_files/Magento/TestModuleOverrideConfig3/Test/Api/_files/overrides.xml @@ -43,4 +43,44 @@ </dataSet> </method> </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesInterface"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testInterfaceInheritance"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesAbstractClass"> + <method name="testAbstractInheritance"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_2" remove="true"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="first_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + </dataSet> + <dataSet name="second_data_set"> + <magentoConfigFixture scopeType="store" scopeCode="default" path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoApiDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipAbstractClass"> + <method name="testAbstractSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipInterface"> + <method name="testInterfaceSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="second_data_set" skip="true"/> + </method> + </test> </overrides> diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php index 94eb5ddec8604..3de18a932f2cd 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/TestCase/GraphQlAbstract.php @@ -171,6 +171,11 @@ protected function assertResponseFields($actualResponse, $assertionMap) $expectedValue, "Value of '{$responseField}' field must not be NULL" ); + self::assertArrayHasKey( + $responseField, + $actualResponse, + "Response array does not contain key '{$responseField}'" + ); self::assertEquals( $expectedValue, $actualResponse[$responseField], diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php index 9fa5d2868fd11..06605d156933d 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config.php @@ -11,6 +11,11 @@ use Magento\Framework\Config\ConverterInterface; use Magento\Framework\Config\SchemaLocatorInterface; use Magento\Framework\View\File\CollectorInterface; +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ApiConfigFixture; +use Magento\TestFramework\Annotation\ApiDataFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; use Magento\TestFramework\WebapiWorkaround\Override\Config\Converter; use Magento\TestFramework\WebapiWorkaround\Override\Config\FileCollector; use Magento\TestFramework\WebapiWorkaround\Override\Config\SchemaLocator; @@ -21,12 +26,23 @@ */ class Config extends IntegrationConfig { + /** + * @inheritdoc + */ + protected const FIXTURE_TYPES = [ + ApiDataFixture::ANNOTATION, + ApiConfigFixture::ANNOTATION, + DataFixture::ANNOTATION, + DataFixtureBeforeTransaction::ANNOTATION, + AdminConfigFixture::ANNOTATION, + ]; + /** * @inheritdoc */ protected function getConverter(): ConverterInterface { - return ObjectManager::getInstance()->create(Converter::class); + return ObjectManager::getInstance()->create(Converter::class, ['types' => $this::FIXTURE_TYPES]); } /** diff --git a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php index ce83a611020a8..c14e535187296 100644 --- a/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php +++ b/dev/tests/api-functional/framework/Magento/TestFramework/WebapiWorkaround/Override/Config/Converter.php @@ -8,7 +8,6 @@ namespace Magento\TestFramework\WebapiWorkaround\Override\Config; use Magento\TestFramework\Annotation\AdminConfigFixture; -use Magento\TestFramework\Annotation\ApiConfigFixture; use Magento\TestFramework\Annotation\ApiDataFixture; use Magento\TestFramework\Annotation\ConfigFixture; use Magento\TestFramework\Annotation\DataFixture; @@ -20,14 +19,6 @@ */ class Converter extends IntegrationConverter { - protected const FIXTURE_TYPES = [ - ApiDataFixture::ANNOTATION, - ApiConfigFixture::ANNOTATION, - DataFixture::ANNOTATION, - DataFixtureBeforeTransaction::ANNOTATION, - AdminConfigFixture::ANNOTATION, - ]; - /** * Fill node attributes values * diff --git a/dev/tests/api-functional/phpunit_graphql.xml.dist b/dev/tests/api-functional/phpunit_graphql.xml.dist index 2f6ad1f9a37d4..e63008a10ee51 100644 --- a/dev/tests/api-functional/phpunit_graphql.xml.dist +++ b/dev/tests/api-functional/phpunit_graphql.xml.dist @@ -8,7 +8,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" @@ -18,11 +18,11 @@ > <!-- Test suites definition --> <testsuites> - <testsuite name="Magento GraphQL web API functional tests"> - <file>testsuite/Magento/WebApiTest.php</file> - </testsuite> + <testsuite name="Magento GraphQL web API functional tests"> + <file>testsuite/Magento/WebApiTest.php</file> + </testsuite> <testsuite name="Magento GraphQL web API functional tests real suite"> - <directory suffix="Test.php">testsuite/Magento/GraphQl</directory> + <directory>testsuite/Magento/GraphQl</directory> </testsuite> </testsuites> diff --git a/dev/tests/api-functional/phpunit_rest.xml.dist b/dev/tests/api-functional/phpunit_rest.xml.dist index 065c2bd11c48c..b949e6c6cffe2 100644 --- a/dev/tests/api-functional/phpunit_rest.xml.dist +++ b/dev/tests/api-functional/phpunit_rest.xml.dist @@ -8,7 +8,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" @@ -22,8 +22,8 @@ <file>testsuite/Magento/WebApiTest.php</file> </testsuite> <testsuite name="Magento REST web API functional tests real suite"> - <directory suffix="Test.php">testsuite</directory> - <directory suffix="Test.php">../../../app/code/*/*/Test/Api</directory> + <directory>testsuite</directory> + <directory>../../../app/code/*/*/Test/Api</directory> <exclude>testsuite/Magento/GraphQl</exclude> </testsuite> </testsuites> diff --git a/dev/tests/api-functional/phpunit_soap.xml.dist b/dev/tests/api-functional/phpunit_soap.xml.dist index 5e90b8965d34c..8fc7ad8cebdc4 100644 --- a/dev/tests/api-functional/phpunit_soap.xml.dist +++ b/dev/tests/api-functional/phpunit_soap.xml.dist @@ -8,7 +8,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.2/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" @@ -22,9 +22,9 @@ <file>testsuite/Magento/WebApiTest.php</file> </testsuite> <testsuite name="Magento SOAP web API functional tests real suite"> - <directory suffix="Test.php">testsuite</directory> + <directory>testsuite</directory> <!-- <exclude>testsuite/Magento/GraphQl</exclude> --> - <directory suffix="Test.php">../../../app/code/*/*/Test/Api</directory> + <directory>../../../app/code/*/*/Test/Api</directory> </testsuite> </testsuites> diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php index aba065a956d4f..21b93645fd15a 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/CategoryRepositoryTest.php @@ -7,14 +7,14 @@ namespace Magento\Catalog\Api; use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\RoleFactory; use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\RulesFactory; +use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; use Magento\Integration\Api\AdminTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; -use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator; -use Magento\Authorization\Model\RoleFactory; -use Magento\Authorization\Model\RulesFactory; /** * Test repository web API. @@ -218,6 +218,35 @@ public function testUpdate() $this->deleteCategory($categoryId); } + /** + * @magentoApiDataFixture Magento/Catalog/_files/category.php + */ + public function testUpdateWithDefaultSortByAttribute() + { + $categoryId = 333; + $categoryData = [ + 'name' => 'Update Category Test With default_sort_by Attribute', + 'is_active' => true, + "available_sort_by" => [], + 'custom_attributes' => [ + [ + 'attribute_code' => 'default_sort_by', + 'value' => ["name"], + ], + ], + ]; + $result = $this->updateCategory($categoryId, $categoryData); + $this->assertEquals($categoryId, $result['id']); + /** @var \Magento\Catalog\Model\Category $model */ + $model = Bootstrap::getObjectManager()->get(\Magento\Catalog\Model\Category::class); + $category = $model->load($categoryId); + $this->assertTrue((bool)$category->getIsActive(), 'Category "is_active" must equal to true'); + $this->assertEquals("Update Category Test With default_sort_by Attribute", $category->getName()); + $this->assertEquals("name", $category->getDefaultSortBy()); + // delete category to clean up auto-generated url rewrites + $this->deleteCategory($categoryId); + } + protected function getSimpleCategoryData($categoryData = []) { return [ diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php index 64f51b93cde50..2b628c05ae736 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionManagementInterfaceTest.php @@ -7,14 +7,21 @@ use Magento\Eav\Api\Data\AttributeOptionInterface; use Magento\Eav\Api\Data\AttributeOptionLabelInterface; +use Magento\Framework\Webapi\Rest\Request; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Class to test Eav Option Management functionality + */ class ProductAttributeOptionManagementInterfaceTest extends WebapiAbstract { const SERVICE_NAME = 'catalogProductAttributeOptionManagementV1'; const SERVICE_VERSION = 'V1'; const RESOURCE_PATH = '/V1/products/attributes'; + /** + * Test to get attribute options + */ public function testGetItems() { $testAttributeCode = 'quantity_and_stock_status'; @@ -29,64 +36,56 @@ public function testGetItems() ], ]; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $testAttributeCode . '/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'getItems', - ], - ]; - - $response = $this->_webApiCall($serviceInfo, ['attributeCode' => $testAttributeCode]); + $response = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_GET, + 'getItems', + ['attributeCode' => $testAttributeCode] + ); $this->assertIsArray($response); $this->assertEquals($expectedOptions, $response); } /** + * Test to add attribute option + * + * @param array $optionData * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php * @dataProvider addDataProvider */ - public function testAdd($optionData) + public function testAdd(array $optionData) { $testAttributeCode = 'select_attribute'; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $testAttributeCode . '/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'add', - ], - ]; - - $response = $this->_webApiCall( - $serviceInfo, + $response = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_POST, + 'add', [ 'attributeCode' => $testAttributeCode, 'option' => $optionData, ] ); - $this->assertNotNull($response); - $updatedData = $this->getAttributeOptions($testAttributeCode); - $lastOption = array_pop($updatedData); - $this->assertEquals( - $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], - $lastOption['label'] - ); + $this->assertTrue(is_numeric($response)); + /* Check new option labels by stores */ + $expectedStoreLabels = [ + 'all' => $optionData[AttributeOptionLabelInterface::LABEL], + 'default' => $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], + ]; + foreach ($expectedStoreLabels as $store => $label) { + $option = $this->getAttributeOption($testAttributeCode, $label, $store); + $this->assertNotNull($option); + $this->assertEquals($response, $option['value']); + } } /** + * Data provider for adding attribute option + * * @return array */ - public function addDataProvider() + public function addDataProvider(): array { $optionPayload = [ AttributeOptionInterface::LABEL => 'new color', @@ -114,62 +113,111 @@ public function addDataProvider() 'option_with_value_node_that_is_a_number' => [ array_merge($optionPayload, [AttributeOptionInterface::VALUE => '123']) ], - ]; } /** + * Test to delete attribute option + * * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php */ public function testDelete() { $attributeCode = 'select_attribute'; - //get option Id $optionList = $this->getAttributeOptions($attributeCode); $this->assertGreaterThan(0, count($optionList)); $lastOption = array_pop($optionList); $this->assertNotEmpty($lastOption['value']); $optionId = $lastOption['value']; - $serviceInfo = [ - 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $attributeCode . '/options/' . $optionId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, - ], - 'soap' => [ - 'service' => self::SERVICE_NAME, - 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'delete', - ], - ]; - $this->assertTrue($this->_webApiCall( - $serviceInfo, + $response = $this->webApiCallAttributeOptions( + $attributeCode, + Request::HTTP_METHOD_DELETE, + 'delete', [ 'attributeCode' => $attributeCode, 'optionId' => $optionId, - ] - )); + ], + $optionId + ); + $this->assertTrue($response); $updatedOptions = $this->getAttributeOptions($attributeCode); $this->assertEquals($optionList, $updatedOptions); } /** - * @param $testAttributeCode + * Perform Web API call to the system under test + * + * @param string $attributeCode + * @param string $httpMethod + * @param string $soapMethod + * @param array $arguments + * @param null $storeCode + * @param null $optionId * @return array|bool|float|int|string */ - private function getAttributeOptions($testAttributeCode) - { + private function webApiCallAttributeOptions( + string $attributeCode, + string $httpMethod, + string $soapMethod, + array $arguments = [], + $optionId = null, + $storeCode = null + ) { $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . '/' . $testAttributeCode . '/options', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'resourcePath' => self::RESOURCE_PATH . '/' . $attributeCode . '/options' + . ($optionId ? '/' .$optionId : ''), + 'httpMethod' => $httpMethod, ], 'soap' => [ 'service' => self::SERVICE_NAME, 'serviceVersion' => self::SERVICE_VERSION, - 'operation' => self::SERVICE_NAME . 'getItems', + 'operation' => self::SERVICE_NAME . $soapMethod, ], ]; - return $this->_webApiCall($serviceInfo, ['attributeCode' => $testAttributeCode]); + + return $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * @param string $testAttributeCode + * @param string|null $storeCode + * @return array|bool|float|int|string + */ + private function getAttributeOptions(string $testAttributeCode, ?string $storeCode = null) + { + return $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_GET, + 'getItems', + ['attributeCode' => $testAttributeCode], + null, + $storeCode + ); + } + + /** + * @param string $attributeCode + * @param string $optionLabel + * @param string|null $storeCode + * @return array|null + */ + private function getAttributeOption( + string $attributeCode, + string $optionLabel, + ?string $storeCode = null + ): ?array { + $attributeOptions = $this->getAttributeOptions($attributeCode, $storeCode); + $option = null; + /** @var array $attributeOption */ + foreach ($attributeOptions as $attributeOption) { + if ($attributeOption['label'] === $optionLabel) { + $option = $attributeOption; + break; + } + } + + return $option; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionUpdateInterfaceTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionUpdateInterfaceTest.php new file mode 100644 index 0000000000000..dc3648f68b10c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/ProductAttributeOptionUpdateInterfaceTest.php @@ -0,0 +1,234 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Catalog\Api; + +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Api\Data\AttributeOptionLabelInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * Class to test update Product Attribute Options + */ +class ProductAttributeOptionUpdateInterfaceTest extends WebapiAbstract +{ + private const SERVICE_NAME_UPDATE = 'catalogProductAttributeOptionUpdateV1'; + private const SERVICE_NAME = 'catalogProductAttributeOptionManagementV1'; + private const SERVICE_VERSION = 'V1'; + private const RESOURCE_PATH = '/V1/products/attributes'; + + /** + * Test to update attribute option + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + */ + public function testUpdate() + { + $testAttributeCode = 'select_attribute'; + $optionData = [ + AttributeOptionInterface::LABEL => 'Fixture Option Changed', + AttributeOptionInterface::VALUE => 'option_value', + AttributeOptionInterface::STORE_LABELS => [ + [ + AttributeOptionLabelInterface::LABEL => 'Store Label Changed', + AttributeOptionLabelInterface::STORE_ID => 1, + ], + ], + ]; + + $existOptionLabel = 'Fixture Option'; + $existAttributeOption = $this->getAttributeOption($testAttributeCode, $existOptionLabel, 'all'); + $optionId = $existAttributeOption['value']; + + $response = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_PUT, + 'update', + [ + 'attributeCode' => $testAttributeCode, + 'optionId' => $optionId, + 'option' => $optionData, + ], + $optionId + ); + + $this->assertTrue($response); + + /* Check update option labels by stores */ + $expectedStoreLabels = [ + 'all' => $optionData[AttributeOptionLabelInterface::LABEL], + 'default' => $optionData[AttributeOptionInterface::STORE_LABELS][0][AttributeOptionLabelInterface::LABEL], + ]; + foreach ($expectedStoreLabels as $store => $label) { + $this->assertNotNull($this->getAttributeOption($testAttributeCode, $label, $store)); + } + } + + /** + * Test to update option with already exist exception + * + * Test to except case when the two options has a same label + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + */ + public function testUpdateWithAlreadyExistsException() + { + $this->expectExceptionMessage("Admin store attribute option label '%1' is already exists."); + $testAttributeCode = 'select_attribute'; + + $newOptionData = [ + AttributeOptionInterface::LABEL => 'New Option', + AttributeOptionInterface::VALUE => 'new_option_value', + ]; + $newOptionId = $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_POST, + 'add', + [ + 'attributeCode' => $testAttributeCode, + 'option' => $newOptionData, + ] + ); + + $editOptionData = [ + AttributeOptionInterface::LABEL => 'Fixture Option', + AttributeOptionInterface::VALUE => $newOptionId, + ]; + $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_PUT, + 'update', + [ + 'attributeCode' => $testAttributeCode, + 'optionId' => $newOptionId, + 'option' => $editOptionData, + ], + $newOptionId + ); + } + + /** + * Test to update option with not exist exception + * + * @magentoApiDataFixture Magento/Catalog/Model/Product/Attribute/_files/select_attribute.php + */ + public function testUpdateWithNotExistsException() + { + $this->expectExceptionMessage("The '%1' attribute doesn't include an option id '%2'."); + $testAttributeCode = 'select_attribute'; + + $newOptionData = [ + AttributeOptionInterface::LABEL => 'New Option', + AttributeOptionInterface::VALUE => 'new_option_value' + ]; + $newOptionId = (int)$this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_POST, + 'add', + [ + 'attributeCode' => $testAttributeCode, + 'option' => $newOptionData, + ] + ); + + $newOptionId++; + $editOptionData = [ + AttributeOptionInterface::LABEL => 'New Option Changed', + AttributeOptionInterface::VALUE => $newOptionId + ]; + $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_PUT, + 'update', + [ + 'attributeCode' => $testAttributeCode, + 'optionId' => $newOptionId, + 'option' => $editOptionData, + ], + $newOptionId + ); + } + + /** + * Perform Web API call to the system under test + * + * @param string $attributeCode + * @param string $httpMethod + * @param string $soapMethod + * @param array $arguments + * @param null $storeCode + * @param null $optionId + * @return array|bool|float|int|string + */ + private function webApiCallAttributeOptions( + string $attributeCode, + string $httpMethod, + string $soapMethod, + array $arguments = [], + $optionId = null, + $storeCode = null + ) { + $resourcePath = self::RESOURCE_PATH . "/{$attributeCode}/options"; + if ($optionId) { + $resourcePath .= '/' . $optionId; + } + $serviceName = $soapMethod === 'update' ? self::SERVICE_NAME_UPDATE : self::SERVICE_NAME; + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => $resourcePath, + 'httpMethod' => $httpMethod, + ], + 'soap' => [ + 'service' => $serviceName, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => $serviceName . $soapMethod, + ], + ]; + + return $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * @param string $attributeCode + * @param string $optionLabel + * @param string|null $storeCode + * @return array|null + */ + private function getAttributeOption( + string $attributeCode, + string $optionLabel, + ?string $storeCode = null + ): ?array { + $attributeOptions = $this->getAttributeOptions($attributeCode, $storeCode); + $option = null; + /** @var array $attributeOption */ + foreach ($attributeOptions as $attributeOption) { + if ($attributeOption['label'] === $optionLabel) { + $option = $attributeOption; + break; + } + } + + return $option; + } + + /** + * @param string $testAttributeCode + * @param string|null $storeCode + * @return array|bool|float|int|string + */ + private function getAttributeOptions(string $testAttributeCode, ?string $storeCode = null) + { + return $this->webApiCallAttributeOptions( + $testAttributeCode, + Request::HTTP_METHOD_GET, + 'getItems', + ['attributeCode' => $testAttributeCode], + null, + $storeCode + ); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php index ef374dc1873cf..90fe075f91e30 100644 --- a/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Catalog/Api/SpecialPriceStorageTest.php @@ -68,6 +68,35 @@ public function testGet() $this->assertEquals($product->getSpecialPrice(), $response[0]['price']); } + /** + * Test get method when special price is 0. + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testGetZeroValue() + { + $specialPrice = 0; + $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $product = $productRepository->get(self::SIMPLE_PRODUCT_SKU, true); + $product->setData('special_price', $specialPrice); + $productRepository->save($product); + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/products/special-price-information', + 'httpMethod' => Request::HTTP_METHOD_POST + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Get', + ], + ]; + $response = $this->_webApiCall($serviceInfo, ['skus' => [self::SIMPLE_PRODUCT_SKU]]); + $this->assertNotEmpty($response); + $this->assertEquals($specialPrice, $response[0]['price']); + } + /** * Test update method. * diff --git a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php index 4c024008e6853..069944c8c35a9 100644 --- a/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/ConfigurableProduct/Api/ProductRepositoryTest.php @@ -6,6 +6,7 @@ namespace Magento\ConfigurableProduct\Api; use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Entity\Attribute; use Magento\Eav\Model\Config; use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection; @@ -13,10 +14,12 @@ use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Webapi\Rest\Request; use Magento\TestFramework\Helper\Bootstrap; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; use Magento\TestFramework\TestCase\WebapiAbstract; /** * Class ProductRepositoryTest for testing ConfigurableProduct integration + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ProductRepositoryTest extends WebapiAbstract { @@ -28,17 +31,22 @@ class ProductRepositoryTest extends WebapiAbstract /** * @var Config */ - protected $eavConfig; + private $eavConfig; /** * @var ObjectManagerInterface */ - protected $objectManager; + private $objectManager; /** * @var Attribute */ - protected $configurableAttribute; + private $configurableAttribute; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; /** * @inheritdoc @@ -47,6 +55,7 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->eavConfig = $this->objectManager->get(Config::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); } /** @@ -164,6 +173,65 @@ public function testCreateConfigurableProduct() $this->assertEquals([$productId1, $productId2], $resultConfigurableProductLinks); } + /** + * Create configurable with simple which has zero attribute value + * + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_attribute_with_source_model.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * @return void + */ + public function testCreateConfigurableProductWithZeroOptionValue(): void + { + $attributeCode = 'test_configurable_with_sm'; + $attributeValue = 0; + + $product = $this->productRepository->get('simple'); + $product->setCustomAttribute($attributeCode, $attributeValue); + $this->productRepository->save($product); + + $configurableAttribute = $this->eavConfig->getAttribute('catalog_product', $attributeCode); + + $productData = [ + 'sku' => self::CONFIGURABLE_PRODUCT_SKU, + 'name' => self::CONFIGURABLE_PRODUCT_SKU, + 'type_id' => Configurable::TYPE_CODE, + 'attribute_set_id' => 4, + 'extension_attributes' => [ + 'configurable_product_options' => [ + [ + 'attribute_id' => $configurableAttribute->getId(), + 'label' => 'Test configurable with source model', + 'values' => [ + ['value_index' => '0'], + ], + ], + ], + 'configurable_product_links' => [$product->getId()], + ], + ]; + + $response = $this->createProduct($productData); + + $this->assertArrayHasKey(ProductInterface::SKU, $response); + $this->assertEquals(self::CONFIGURABLE_PRODUCT_SKU, $response[ProductInterface::SKU]); + + $this->assertArrayHasKey(ProductInterface::TYPE_ID, $response); + $this->assertEquals('configurable', $response[ProductInterface::TYPE_ID]); + + $this->assertArrayHasKey(ProductInterface::EXTENSION_ATTRIBUTES_KEY, $response); + $this->assertArrayHasKey( + 'configurable_product_options', + $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY] + ); + $configurableProductOption = + current($response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]['configurable_product_options']); + + $this->assertArrayHasKey('attribute_id', $configurableProductOption); + $this->assertEquals($configurableAttribute->getId(), $configurableProductOption['attribute_id']); + $this->assertArrayHasKey('values', $configurableProductOption); + $this->assertEquals($attributeValue, $configurableProductOption['values'][0]['value_index']); + } + /** * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php */ 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 a00af2d6eb076..e1fb9e29105b9 100644 --- a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerRepositoryTest.php @@ -6,23 +6,26 @@ namespace Magento\Customer\Api; -use Magento\Customer\Api\Data\CustomerInterface as Customer; use Magento\Customer\Api\Data\AddressInterface as Address; +use Magento\Customer\Api\Data\CustomerInterface as Customer; +use Magento\Customer\Api\Data\CustomerInterfaceFactory; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Api\DataObjectHelper; use Magento\Framework\Api\FilterBuilder; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\Api\SortOrder; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Api\CustomerTokenServiceInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Customer as CustomerHelper; use Magento\TestFramework\TestCase\WebapiAbstract; -use Magento\Framework\Webapi\Exception as HTTPExceptionCodes; -use Magento\Framework\Exception\NoSuchEntityException; /** - * Test class for Magento\Customer\Api\CustomerRepositoryInterface + * Test for \Magento\Customer\Api\CustomerRepositoryInterface. * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ @@ -31,13 +34,8 @@ class CustomerRepositoryTest extends WebapiAbstract const SERVICE_VERSION = 'V1'; const SERVICE_NAME = 'customerCustomerRepositoryV1'; const RESOURCE_PATH = '/V1/customers'; - const RESOURCE_PATH_CUSTOMER_TOKEN = "/V1/integration/customer/token"; - /** - * Sample values for testing - */ - const ATTRIBUTE_CODE = 'attribute_code'; - const ATTRIBUTE_VALUE = 'attribute_value'; + private const STUB_INVALID_CUSTOMER_GROUP_ID = 777; /** * @var CustomerRepositoryInterface @@ -45,12 +43,12 @@ class CustomerRepositoryTest extends WebapiAbstract private $customerRepository; /** - * @var \Magento\Framework\Api\DataObjectHelper + * @var DataObjectHelper */ private $dataObjectHelper; /** - * @var \Magento\Customer\Api\Data\CustomerInterfaceFactory + * @var CustomerInterfaceFactory */ private $customerDataFactory; @@ -70,7 +68,7 @@ class CustomerRepositoryTest extends WebapiAbstract private $filterGroupBuilder; /** - * @var \Magento\Customer\Model\CustomerRegistry + * @var CustomerRegistry */ private $customerRegistry; @@ -131,7 +129,7 @@ protected function tearDown(): void $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -165,24 +163,23 @@ public function testInvalidCustomerUpdate() $customerTokenService = Bootstrap::getObjectManager()->create( \Magento\Integration\Api\CustomerTokenServiceInterface::class ); - $token = $customerTokenService->createCustomerAccessToken($firstCustomerData[Customer::EMAIL], 'test@123'); + $token = $customerTokenService->createCustomerAccessToken( + $firstCustomerData[Customer::EMAIL], + 'test@123' + ); //Create second customer and update lastname. $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); $customerData[Customer::LASTNAME] = $lastName . 'Updated'; $newCustomerDataObject = $this->customerDataFactory->create(); - $this->dataObjectHelper->populateWithArray( - $newCustomerDataObject, - $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $this->dataObjectHelper->populateWithArray($newCustomerDataObject, $customerData, Customer::class); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, 'token' => $token, ], 'soap' => [ @@ -195,7 +192,7 @@ public function testInvalidCustomerUpdate() $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; $this->_webApiCall($serviceInfo, $requestData); @@ -209,7 +206,7 @@ public function testDeleteCustomer() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $customerData[Customer::ID], - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -228,16 +225,21 @@ public function testDeleteCustomer() //Verify if the customer is deleted $this->expectException(\Magento\Framework\Exception\NoSuchEntityException::class); $this->expectExceptionMessage(sprintf("No such entity with customerId = %s", $customerData[Customer::ID])); - $this->_getCustomerData($customerData[Customer::ID]); + $this->getCustomerData($customerData[Customer::ID]); } - public function testDeleteCustomerInvalidCustomerId() + /** + * Test delete customer with invalid id + * + * @return void + */ + public function testDeleteCustomerInvalidCustomerId(): void { $invalidId = -1; $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/' . $invalidId, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_DELETE, + 'httpMethod' => Request::HTTP_METHOD_DELETE, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -266,23 +268,25 @@ public function testDeleteCustomerInvalidCustomerId() } } - public function testUpdateCustomer() + /** + * Test customer update + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testUpdateCustomer(): void { - $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); - $lastName = $existingCustomerDataObject->getLastname(); - $customerData[Customer::LASTNAME] = $lastName . 'Updated'; - $newCustomerDataObject = $this->customerDataFactory->create(); - $this->dataObjectHelper->populateWithArray( - $newCustomerDataObject, - $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $customerId = 1; + $updatedLastname = 'Updated lastname'; + $customer = $this->getCustomerData($customerId); + $customerData = $this->dataObjectProcessor->buildOutputDataArray($customer, Customer::class); + $customerData[Customer::LASTNAME] = $updatedLastname; $serviceInfo = [ 'rest' => [ - 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -290,17 +294,18 @@ public function testUpdateCustomer() 'operation' => self::SERVICE_NAME . 'Save', ], ]; - $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( - $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class - ); - $requestData = ['customer' => $newCustomerDataObject]; + + $requestData['customer'] = TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP + ? $customerData + : [Customer::LASTNAME => $updatedLastname]; + $response = $this->_webApiCall($serviceInfo, $requestData); - $this->assertTrue($response !== null); + $this->assertNotNull($response); //Verify if the customer is updated - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); - $this->assertEquals($lastName . "Updated", $existingCustomerDataObject->getLastname()); + $existingCustomerDataObject = $this->getCustomerData($customerId); + $this->assertEquals($updatedLastname, $existingCustomerDataObject->getLastname()); + $this->assertEquals($customerData[Customer::FIRSTNAME], $existingCustomerDataObject->getFirstname()); } /** @@ -309,20 +314,20 @@ public function testUpdateCustomer() public function testUpdateCustomerNoWebsiteId() { $customerData = $this->customerHelper->createSampleCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); $customerData[Customer::LASTNAME] = $lastName . 'Updated'; $newCustomerDataObject = $this->customerDataFactory->create(); $this->dataObjectHelper->populateWithArray( $newCustomerDataObject, $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/{$customerData[Customer::ID]}", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -332,32 +337,28 @@ public function testUpdateCustomerNoWebsiteId() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); unset($newCustomerDataObject['website_id']); $requestData = ['customer' => $newCustomerDataObject]; - $expectedMessage = '"Associate to Website" is a required value.'; try { - $this->_webApiCall($serviceInfo, $requestData); - $this->fail("Expected exception."); + $response = $this->_webApiCall($serviceInfo, $requestData); + $this->assertEquals($customerData['website_id'], $response['website_id']); } catch (\SoapFault $e) { - $this->assertStringContainsString( - $expectedMessage, - $e->getMessage(), - "SoapFault does not contain expected message." - ); - } catch (\Exception $e) { - $errorObj = $this->customerHelper->processRestExceptionResult($e); - $this->assertEquals($expectedMessage, $errorObj['message'], 'Invalid message: "' . $e->getMessage() . '"'); - $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $this->assertStringContainsString('"Associate to Website" is a required value.', $e->getMessage()); } } - public function testUpdateCustomerException() + /** + * Test customer exception update + * + * @return void + */ + public function testUpdateCustomerException(): void { $customerData = $this->_createCustomer(); - $existingCustomerDataObject = $this->_getCustomerData($customerData[Customer::ID]); + $existingCustomerDataObject = $this->getCustomerData($customerData[Customer::ID]); $lastName = $existingCustomerDataObject->getLastname(); //Set non-existent id = -1 @@ -367,13 +368,13 @@ public function testUpdateCustomerException() $this->dataObjectHelper->populateWithArray( $newCustomerDataObject, $customerData, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . "/-1", - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_PUT, + 'httpMethod' => Request::HTTP_METHOD_PUT, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -383,7 +384,7 @@ public function testUpdateCustomerException() ]; $newCustomerDataObject = $this->dataObjectProcessor->buildOutputDataArray( $newCustomerDataObject, - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); $requestData = ['customer' => $newCustomerDataObject]; @@ -406,14 +407,99 @@ public function testUpdateCustomerException() } } + /** + * Test customer update with invalid customer group id + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testUpdateCustomerWithInvalidGroupId(): void + { + $customerId = 1; + $customerData = $this->dataObjectProcessor->buildOutputDataArray( + $this->getCustomerData($customerId), + Customer::class + ); + $customerData[Customer::GROUP_ID] = self::STUB_INVALID_CUSTOMER_GROUP_ID; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH . '/' . $customerId, + 'httpMethod' => Request::HTTP_METHOD_PUT, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + + $requestData['customer'] = $customerData; + $expectedMessage = 'The specified customer group id does not exist.'; + + try { + $this->_webApiCall($serviceInfo, $requestData); + $this->fail('Expected exception was not raised'); + } catch (\SoapFault $e) { + $this->assertStringContainsString($expectedMessage, $e->getMessage()); + } catch (\Exception $e) { + $errorObj = $this->processRestExceptionResult($e); + $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $this->assertEquals($expectedMessage, $errorObj['message']); + } + } + + /** + * Test customer create with invalid customer group id + * + * @return void + */ + public function testCreateCustomerWithInvalidGroupId(): void + { + $customerData = $this->dataObjectProcessor->buildOutputDataArray( + $this->customerHelper->createSampleCustomerDataObject(), + Customer::class + ); + $customerData[Customer::GROUP_ID] = self::STUB_INVALID_CUSTOMER_GROUP_ID; + + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::SERVICE_NAME . 'Save', + ], + ]; + + $requestData = ['customer' => $customerData]; + $expectedMessage = 'The specified customer group id does not exist.'; + + try { + $this->_webApiCall($serviceInfo, $requestData); + $this->fail('Expected exception was not raised'); + } catch (\SoapFault $e) { + $this->assertStringContainsString($expectedMessage, $e->getMessage()); + } catch (\Exception $e) { + $errorObj = $this->processRestExceptionResult($e); + $this->assertEquals(HTTPExceptionCodes::HTTP_BAD_REQUEST, $e->getCode()); + $this->assertEquals($expectedMessage, $errorObj['message']); + } + } + /** * Test creating a customer with absent required address fields + * + * @return void */ - public function testCreateCustomerWithoutAddressRequiresException() + public function testCreateCustomerWithoutAddressRequiresException(): void { $customerDataArray = $this->dataObjectProcessor->buildOutputDataArray( $this->customerHelper->createSampleCustomerDataObject(), - \Magento\Customer\Api\Data\CustomerInterface::class + Customer::class ); foreach ($customerDataArray[Customer::KEY_ADDRESSES] as & $address) { @@ -423,7 +509,7 @@ public function testCreateCustomerWithoutAddressRequiresException() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -571,7 +657,7 @@ public function testSearchCustomersUsingGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo); @@ -588,7 +674,7 @@ public function testSearchCustomersUsingGETEmptyFilter() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; try { @@ -640,7 +726,7 @@ public function testSearchCustomersMultipleFiltersWithSort() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -682,7 +768,7 @@ public function testSearchCustomersMultipleFiltersWithSortUsingGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo); @@ -716,7 +802,7 @@ public function testSearchCustomersNonExistentMultipleFilters() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -755,7 +841,7 @@ public function testSearchCustomersNonExistentMultipleFiltersGET() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search?' . $searchQueryString, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], ]; $searchResults = $this->_webApiCall($serviceInfo, $requestData); @@ -793,7 +879,7 @@ public function testSearchCustomersMultipleFilterGroups() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH . '/search' . '?' . http_build_query($requestData), - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET, + 'httpMethod' => Request::HTTP_METHOD_GET, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -886,11 +972,11 @@ public function testRevokeAllAccessTokensForCustomer() * Retrieve customer data by Id * * @param int $customerId - * @return \Magento\Customer\Api\Data\CustomerInterface + * @return Customer */ - protected function _getCustomerData($customerId) + private function getCustomerData($customerId): Customer { - $customerData = $this->customerRepository->getById($customerId); + $customerData = $this->customerRepository->getById($customerId); $this->customerRegistry->remove($customerId); return $customerData; } diff --git a/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php new file mode 100644 index 0000000000000..9c7abcd6c8364 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Customer/Api/CustomerSharingOptionsTest.php @@ -0,0 +1,213 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Api; + +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\Registry; +use Magento\Framework\Webapi\Rest\Request; +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; +use Magento\TestFramework\TestCase\WebapiAbstract; + +/** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + */ +class CustomerSharingOptionsTest extends WebapiAbstract +{ + const RESOURCE_PATH = '/V1/customers/me'; + const REPO_SERVICE = 'customerCustomerRepositoryV1'; + const SERVICE_VERSION = 'V1'; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + + /** + * @var CustomerHelper + */ + private $customerHelper; + + /** + * @var TokenModel + */ + private $token; + + /** + * @var CustomerInterface + */ + private $customerData; + + /** + * @var CustomerTokenServiceInterface + */ + private $tokenService; + + /** + * Execute per test initialization. + */ + public function setUp(): void + { + $this->customerRegistry = Bootstrap::getObjectManager()->get( + \Magento\Customer\Model\CustomerRegistry::class + ); + + $this->customerRepository = Bootstrap::getObjectManager()->get( + CustomerRepositoryInterface::class, + ['customerRegistry' => $this->customerRegistry] + ); + + $this->customerHelper = new CustomerHelper(); + $this->customerData = $this->customerHelper->createSampleCustomer(); + $this->tokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + + // get token + $this->resetTokenForCustomerSampleData(); + } + + /** + * Ensure that fixture customer and his addresses are deleted. + */ + public function tearDown(): void + { + $this->customerRepository = null; + + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + parent::tearDown(); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataWebsiteScopeDataProvider + * + * @magentoConfigFixture default_store customer/account_share/scope 1 + */ + public function testGetCustomerDataWebsiteScope(string $storeCode, bool $expectingException) + { + $this->_markTestAsRestOnly('SOAP is difficult to generate exception messages, inconsistencies in WSDL'); + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + * @dataProvider getCustomerDataGlobalScopeDataProvider + * + * @magentoConfigFixture customer/account_share/scope 0 + */ + public function testGetCustomerDataGlobalScope(string $storeCode, bool $expectingException) + { + $this->processGetCustomerData($storeCode, $expectingException); + } + + /** + * @param string $storeCode + * @param bool $expectingException + */ + private function processGetCustomerData(string $storeCode, bool $expectingException) + { + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => self::RESOURCE_PATH, + 'httpMethod' => Request::HTTP_METHOD_GET, + 'token' => $this->token, + ], + 'soap' => [ + 'service' => self::REPO_SERVICE, + 'serviceVersion' => self::SERVICE_VERSION, + 'operation' => self::REPO_SERVICE . 'GetSelf', + 'token' => $this->token + ] + ]; + $arguments = []; + if (TESTS_WEB_API_ADAPTER === 'soap') { + $arguments['customerId'] = 0; + } + + if ($expectingException) { + self::expectException(\Exception::class); + self::expectExceptionMessage("The consumer isn't authorized to access %resources."); + } + + $this->_webApiCall($serviceInfo, $arguments, null, $storeCode); + } + + /** + * Data provider for testGetCustomerDataWebsiteScope. + * + * @return array + */ + public function getCustomerDataWebsiteScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => true + ] + ]; + } + + /** + * Data provider for testGetCustomerDataGlobalScope. + * + * @return array + */ + public function getCustomerDataGlobalScopeDataProvider(): array + { + return [ + 'Default Store View' => [ + 'store_code' => 'default', + 'exception' => false + ], + 'Custom Store View' => [ + 'store_code' => 'fixture_second_store', + 'exception' => false + ] + ]; + } + + /** + * Sets the test's access token for the created customer sample data + */ + private function resetTokenForCustomerSampleData() + { + $this->resetTokenForCustomer($this->customerData[CustomerInterface::EMAIL], 'test@123'); + } + + /** + * Sets the test's access token for a particular username and password. + * + * @param string $username + * @param string $password + */ + private function resetTokenForCustomer($username, $password) + { + $this->token = $this->tokenService->createCustomerAccessToken($username, $password); + $this->customerRegistry->remove($this->customerRepository->get($username)->getId()); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php index 1361f10427fab..00bbb3f435cae 100644 --- a/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Downloadable/Api/ProductRepositoryTest.php @@ -4,10 +4,13 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Downloadable\Api; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; /** @@ -15,22 +18,27 @@ */ class ProductRepositoryTest extends WebapiAbstract { - const SERVICE_NAME = 'catalogProductRepositoryV1'; - const SERVICE_VERSION = 'V1'; - const RESOURCE_PATH = '/V1/products'; - const PRODUCT_SKU = 'sku-test-product-downloadable'; + private const SERVICE_NAME = 'catalogProductRepositoryV1'; + private const SERVICE_VERSION = 'V1'; + private const RESOURCE_PATH = '/V1/products'; + private const PRODUCT_SKU = 'sku-test-product-downloadable'; + + private const PRODUCT_SAMPLES = 'downloadable_product_samples'; + private const PRODUCT_LINKS = 'downloadable_product_links'; /** * @var string */ - protected $testImagePath; + private $testImagePath; + /** + * @inheritdoc + */ protected function setUp(): void { - parent::setUp(); - $this->testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; + $objectManager = Bootstrap::getObjectManager(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->testImagePath = __DIR__ . DIRECTORY_SEPARATOR . '_files' . DIRECTORY_SEPARATOR . 'test_image.jpg'; /** @var DomainManagerInterface $domainManager */ $domainManager = $objectManager->get(DomainManagerInterface::class); @@ -45,7 +53,7 @@ protected function tearDown(): void $this->deleteProductBySku(self::PRODUCT_SKU); parent::tearDown(); - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $objectManager = Bootstrap::getObjectManager(); /** @var DomainManagerInterface $domainManager */ $domainManager = $objectManager->get(DomainManagerInterface::class); @@ -296,6 +304,35 @@ public function testUpdateDownloadableProductLinks() $this->assertCount(2, $resultSamples); } + /** + * Update downloadable product extension attribute and check data + * + * @return void + */ + public function testUpdateDownloadableProductData(): void + { + $productResponce = $this->createDownloadableProduct(); + $stockItemData = $productResponce[ProductInterface::EXTENSION_ATTRIBUTES_KEY]['stock_item']; + + $stockItemData = TESTS_WEB_API_ADAPTER === self::ADAPTER_SOAP + ? $stockItemData['manage_stock'] = false + : ['stock_item' => ['manage_stock' => false]]; + + $productData = [ + ProductInterface::SKU => self::PRODUCT_SKU, + ProductInterface::EXTENSION_ATTRIBUTES_KEY => $stockItemData, + ]; + + $response = $this->saveProduct($productData); + + $this->assertArrayHasKey(ProductInterface::EXTENSION_ATTRIBUTES_KEY, $response); + $this->assertArrayHasKey(self::PRODUCT_SAMPLES, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]); + $this->assertArrayHasKey(self::PRODUCT_LINKS, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY]); + + $this->assertCount(2, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::PRODUCT_SAMPLES]); + $this->assertCount(2, $response[ProductInterface::EXTENSION_ATTRIBUTES_KEY][self::PRODUCT_LINKS]); + } + /** * Update downloadable product, update two links and change file content * @SuppressWarnings(PHPMD.ExcessiveMethodLength) diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..fc0fdcf71525f --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartSingleMutationTest.php @@ -0,0 +1,351 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Bundle; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test adding bundled products to cart using the unified mutation mutation + */ +class AddBundleProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var QuoteResource + */ + private $quoteResource; + + /** + * @var Quote + */ + private $quote; + + /** + * @var QuoteIdToMaskedQuoteIdInterface + */ + private $quoteIdToMaskedId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->quoteResource = $objectManager->get(QuoteResource::class); + $this->quote = $objectManager->create(Quote::class); + $this->quoteIdToMaskedId = $objectManager->get(QuoteIdToMaskedQuoteIdInterface::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddBundleProductToCart() + { + $sku = 'bundle-product'; + + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $product = $this->productRepository->get($sku); + + /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var $option \Magento\Bundle\Model\Option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var \Magento\Catalog\Model\Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + + $bundleOptionIdV2 = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, 1); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: 1 + selected_options: [ + "{$bundleOptionIdV2}" + ] + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('addProductsToCart', $response); + self::assertArrayHasKey('cart', $response['addProductsToCart']); + $cart = $response['addProductsToCart']['cart']; + $bundleItem = current($cart['items']); + self::assertEquals($sku, $bundleItem['product']['sku']); + $bundleItemOption = current($bundleItem['bundle_options']); + self::assertEquals($optionId, $bundleItemOption['id']); + self::assertEquals($option->getTitle(), $bundleItemOption['label']); + self::assertEquals($option->getType(), $bundleItemOption['type']); + $value = current($bundleItemOption['values']); + self::assertEquals($selection->getSelectionId(), $value['id']); + self::assertEquals((float) $selection->getSelectionPriceValue(), $value['price']); + self::assertEquals(1, $value['quantity']); + } + + /** + * @param int $optionId + * @param int $selectionId + * @param int $quantity + * @return string + */ + private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $quantity): string + { + return base64_encode("bundle/$optionId/$selectionId/$quantity"); + } + + public function dataProviderTestUpdateBundleItemQuantity(): array + { + return [ + [2], + [0], + ]; + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + * @expectedExceptionMessage Please select all required options + */ + public function testAddBundleToCartWithWrongBundleOptions() + { + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $bundleOptionIdV2 = $this->generateBundleOptionIdV2((int) 1, (int) 1, 1); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "bundle-product" + quantity: 1 + selected_options: [ + "{$bundleOptionIdV2}" + ] + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + user_errors { + message + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + self::assertEquals( + "Please select all required options.", + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddBundleItemWithCustomOptionQuantity() + { + + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + $response = $this->graphQlQuery($this->getProductQuery("bundle-product")); + $bundleItem = $response['products']['items'][0]; + $sku = $bundleItem['sku']; + $bundleOptions = $bundleItem['items']; + + $uId0 = $bundleOptions[0]['options'][0]['uid']; + $uId1 = $bundleOptions[1]['options'][0]['uid']; + $response = $this->graphQlMutation( + $this->getMutationsQuery($maskedQuoteId, $uId0, $uId1, $sku) + ); + $bundleOptions = $response['addProductsToCart']['cart']['items'][0]['bundle_options']; + $this->assertEquals(5, $bundleOptions[0]['values'][0]['quantity']); + $this->assertEquals(1, $bundleOptions[1]['values'][0]['quantity']); + } + + /** + * Returns GraphQL query for retrieving a product with customizable options + * + * @param string $sku + * @return string + */ + private function getProductQuery(string $sku): string + { + return <<<QUERY +{ + products(search: "{$sku}") { + items { + sku + ... on BundleProduct { + items { + sku + option_id + required + type + title + options { + uid + label + product { + sku + } + can_change_quantity + id + price + + quantity + } + } + } + } + } +} +QUERY; + } + + private function getMutationsQuery( + string $maskedQuoteId, + string $optionUid0, + string $optionUid1, + string $sku + ): string { + return <<<QUERY +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: 2 + selected_options: [ + "{$optionUid1}", "{$optionUid0}" + ], + entered_options: [{ + uid: "{$optionUid0}" + value: "5" + }, + { + uid: "{$optionUid1}" + value: "5" + }] + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + user_errors { + message + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php index 0acd6bb333426..f705195050843 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Bundle/AddBundleProductToCartTest.php @@ -80,7 +80,7 @@ public function testAddBundleProductToCart() $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); $query = <<<QUERY -mutation { +mutation { addBundleProductsToCart(input:{ cart_id:"{$maskedQuoteId}" cart_items:[ @@ -223,7 +223,7 @@ public function testAddBundleToCartWithoutOptions() $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); $query = <<<QUERY -mutation { +mutation { addBundleProductsToCart(input:{ cart_id:"{$maskedQuoteId}" cart_items:[ @@ -268,6 +268,107 @@ public function testAddBundleToCartWithoutOptions() } } } +QUERY; + + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Bundle/_files/product_with_multiple_options_radio_select.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddBundleToCartWithRadioAndSelectErr() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Option type (select, radio) should have only one element.'); + + $sku = 'bundle-product'; + + $this->quoteResource->load( + $this->quote, + 'test_order_1', + 'reserved_order_id' + ); + + $product = $this->productRepository->get($sku); + + /** @var $typeInstance \Magento\Bundle\Model\Product\Type */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var $option \Magento\Bundle\Model\Option */ + $options = $typeInstance->getOptionsCollection($product); + + $selectionIds = []; + $optionIds = []; + foreach ($options as $option) { + $type = $option->getType(); + + /** @var \Magento\Catalog\Model\Product $selection */ + $selections = $typeInstance->getSelectionsCollection([$option->getId()], $product); + $optionIds[$type] = $option->getId(); + + foreach ($selections->getItems() as $selection) { + $selectionIds[$type][] = $selection->getSelectionId(); + } + } + + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$this->quote->getId()); + + $query = <<<QUERY +mutation { + addBundleProductsToCart(input:{ + cart_id:"{$maskedQuoteId}" + cart_items:[ + { + data:{ + sku:"{$sku}" + quantity:1 + } + bundle_options:[ + { + id:{$optionIds['select']} + quantity:1 + value:[ + "{$selectionIds['select'][0]}" + "{$selectionIds['select'][1]}" + ] + }, + { + id:{$optionIds['radio']} + quantity:1 + value:[ + "{$selectionIds['radio'][0]}" + "{$selectionIds['radio'][1]}" + ] + } + ] + } + ] + }) { + cart { + items { + id + quantity + product { + sku + } + ... on BundleCartItem { + bundle_options { + id + label + type + values { + id + label + price + quantity + } + } + } + } + } + } +} QUERY; $this->graphQlMutation($query); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php index 4da588794b2a9..a3daf89631c17 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesFilterTest.php @@ -189,7 +189,7 @@ public function testQueryChildCategoriesWithProducts() $expectedBaseCategoryProducts = [ ['sku' => 'simple', 'name' => 'Simple Product'], ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => '12345', 'name' => 'Simple Product Two'] + ['sku' => '12345', 'name' => 'Simple Product Two'], ]; $this->assertCategoryProducts($baseCategory, $expectedBaseCategoryProducts); //Check base category children @@ -648,15 +648,6 @@ public function filterMultipleCategoriesDataProvider(): array 'in', '["category-1-2", "movable"]', [ - [ - 'id' => '7', - 'name' => 'Movable', - 'url_key' => 'movable', - 'url_path' => 'movable', - 'children_count' => '0', - 'path' => '1/2/7', - 'position' => '3' - ], [ 'id' => '13', 'name' => 'Category 1.2', @@ -665,6 +656,15 @@ public function filterMultipleCategoriesDataProvider(): array 'children_count' => '0', 'path' => '1/2/3/13', 'position' => '2' + ], + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' ] ] ], diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php index c7fbcbd38c7e4..bbc84a82737bd 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoriesQuery/CategoriesPaginationTest.php @@ -155,7 +155,7 @@ public function testPaging() $lastPageQuery = sprintf($baseQuery, $page1Result['categories']['page_info']['total_pages']); $lastPageResult = $this->graphQlQuery($lastPageQuery); $this->assertCount(1, $lastPageResult['categories']['items']); - $this->assertEquals('Category 1.2', $lastPageResult['categories']['items'][0]['name']); + $this->assertEquals('Category 12', $lastPageResult['categories']['items'][0]['name']); } /** diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php index 00eb235cb4dc3..43612575a7dcb 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/CategoryListTest.php @@ -215,7 +215,7 @@ public function testQueryChildCategoriesWithProducts() $this->assertEquals('Its a description of Test Category 1.2', $secondChildCategory['description']); $firstChildCategoryExpectedProducts = [ ['sku' => 'simple-4', 'name' => 'Simple Product Three'], - ['sku' => 'simple', 'name' => 'Simple Product'], + ['sku' => 'simple', 'name' => 'Simple Product'] ]; $this->assertCategoryProducts($secondChildCategory, $firstChildCategoryExpectedProducts); $firstChildCategoryChildren = []; @@ -629,15 +629,6 @@ public function filterMultipleCategoriesDataProvider(): array 'in', '["category-1-2", "movable"]', [ - [ - 'id' => '7', - 'name' => 'Movable', - 'url_key' => 'movable', - 'url_path' => 'movable', - 'children_count' => '0', - 'path' => '1/2/7', - 'position' => '3' - ], [ 'id' => '13', 'name' => 'Category 1.2', @@ -646,6 +637,15 @@ public function filterMultipleCategoriesDataProvider(): array 'children_count' => '0', 'path' => '1/2/3/13', 'position' => '2' + ], + [ + 'id' => '7', + 'name' => 'Movable', + 'url_key' => 'movable', + 'url_path' => 'movable', + 'children_count' => '0', + 'path' => '1/2/7', + 'position' => '3' ] ] ], @@ -714,4 +714,60 @@ private function assertCategoryChildren(array $category, array $expectedChildren $this->assertResponseFields($category['children'][$i], $expectedChild); } } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryInlineFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ... on CategoryTree { + id + name + url_key + url_path + children_count + path + position + } + } +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/categories.php + */ + public function testFilterCategoryNamedFragment() + { + $query = <<<QUERY +{ + categoryList(filters: {ids: {eq: "6"}}){ + ...Cat + } +} + +fragment Cat on CategoryTree { + id + name + url_key + url_path + children_count + path + position +} +QUERY; + $result = $this->graphQlQuery($query); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['categoryList']); + $this->assertEquals($result['categoryList'][0]['name'], 'Category 2'); + $this->assertEquals($result['categoryList'][0]['url_path'], 'category-2'); + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/Options/Uid/CustomizableOptionsUidTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/Options/Uid/CustomizableOptionsUidTest.php new file mode 100644 index 0000000000000..c5a44d5ff68b3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/Options/Uid/CustomizableOptionsUidTest.php @@ -0,0 +1,156 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog\Options\Uid; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for product custom options uid + */ +class CustomizableOptionsUidTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_full_option_set.php + */ + public function testQueryUidForCustomizableOptions() + { + $productSku = 'simple'; + $query = $this->getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + self::assertNotEmpty($responseProduct['options']); + + foreach ($responseProduct['options'] as $option) { + if (isset($option['entered_option'])) { + $enteredOption = $option['entered_option']; + $uid = $this->getUidForEnteredValue($option['option_id']); + + self::assertEquals($uid, $enteredOption['uid']); + } elseif (isset($option['selected_option'])) { + $this->assertNotEmpty($option['selected_option']); + + foreach ($option['selected_option'] as $selectedOption) { + $uid = $this->getUidForSelectedValue($option['option_id'], $selectedOption['option_type_id']); + self::assertEquals($uid, $selectedOption['uid']); + } + } + } + } + + /** + * Get uid for entered option + * + * @param int $optionId + * + * @return string + */ + private function getUidForEnteredValue(int $optionId): string + { + return base64_encode('custom-option/' . $optionId); + } + + /** + * Get uid for selected option + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function getUidForSelectedValue(int $optionId, int $optionValueId): string + { + return base64_encode('custom-option/' . $optionId . '/' . $optionValueId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<<QUERY +query { + products(filter: { sku: { eq: "$sku" } }) { + items { + sku + + ... on CustomizableProductInterface { + options { + option_id + title + + ... on CustomizableRadioOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableDropDownOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableMultipleOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableCheckboxOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableAreaOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFieldOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFileOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableDateOption { + option_id + entered_option: value { + uid + } + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php new file mode 100644 index 0000000000000..97c6c41ad6397 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeStoreOptionsTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Exception; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class ProductAttributeStoreOptionsTest extends GraphQlAbstract +{ + /** + * Test that custom attribute option labels are returned respecting store + * + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php + * @throws LocalizedException + */ + public function testAttributeStoreLabels(): void + { + $this->attributeLabelTest('Option Default Store'); + $this->attributeLabelTest('Option Test Store', ['Store' => 'test']); + } + + /** + * @param $expectedLabel + * @param array $headers + * @throws LocalizedException + * @throws Exception + */ + private function attributeLabelTest($expectedLabel, array $headers = []): void + { + /** @var Config $eavConfig */ + $eavConfig = Bootstrap::getObjectManager()->get(Config::class); + $attributeCode = 'test_configurable'; + $attribute = $eavConfig->getAttribute('catalog_product', $attributeCode); + + /** @var AttributeOptionInterface[] $options */ + $options = $attribute->getOptions(); + array_shift($options); + $optionValues = []; + + foreach ($options as $option) { + $optionValues[] = [ + 'value' => $option->getValue(), + ]; + } + + $expectedOptions = [ + [ + 'label' => $expectedLabel, + 'value' => $optionValues[0]['value'] + ] + ]; + + $query = <<<QUERY +{ + products(search:"Simple", + pageSize: 3 + currentPage: 1 + ) + { + aggregations + { + attribute_code + options + { + label + value + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertNotEmpty($response['products']['aggregations']); + $actualAttributes = $response['products']['aggregations']; + $actualAttributeOptions = []; + + foreach ($actualAttributes as $actualAttribute) { + if ($actualAttribute['attribute_code'] === $attributeCode) { + $actualAttributeOptions = $actualAttribute['options']; + } + } + + $this->assertNotEmpty($actualAttributeOptions); + + foreach ($actualAttributeOptions as $key => $actualAttributeOption) { + if ($actualAttributeOption['value'] === $expectedOptions[$key]['value']) { + $this->assertEquals($actualAttributeOption['label'], $expectedOptions[$key]['label']); + } + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php index a34d5e21704af..b4b57b3817d3d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductAttributeTypeTest.php @@ -52,7 +52,7 @@ public function testAttributeTypeResolver() attribute_type entity_type input_type - } + } } } QUERY; @@ -125,7 +125,7 @@ public function testComplexAttributeTypeResolver() attribute_type entity_type input_type - } + } } } QUERY; @@ -199,8 +199,8 @@ public function testUnDefinedAttributeType() { attribute_code attribute_type - entity_type - } + entity_type + } } } QUERY; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php new file mode 100644 index 0000000000000..b19b8d519e857 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductDeleteTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Catalog; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test that product is not present in GQL after it was deleted + */ +class ProductDeleteTest extends GraphQlAbstract +{ + /** + * @var \Magento\TestFramework\ObjectManager + */ + private $objectManager; + + protected function setUp(): void + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_all_fields.php + */ + public function testQuerySimpleProductAfterDelete() + { + $productSku = 'simple'; + + $query = <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) + { + items { + attribute_set_id + } + } +} +QUERY; + // get customer ID token + /** @var \Magento\Integration\Api\CustomerTokenServiceInterface $customerTokenService */ + $customerTokenService = $this->objectManager->create( + \Magento\Integration\Api\CustomerTokenServiceInterface::class + ); + $customerToken = $customerTokenService->createCustomerAccessToken('customer@example.com', 'password'); + + $headerMap = ['Authorization' => 'Bearer ' . $customerToken]; + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('items', $response['products']); + $this->assertCount(1, $response['products']['items']); + + // Delete the product and verify it is actually not accessible via the storefront anymore + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + + $registry = ObjectManager::getInstance()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $productRepository->deleteById($productSku); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + + $response = $this->graphQlQuery($query, [], '', $headerMap); + $this->assertArrayHasKey('products', $response); + $this->assertArrayHasKey('items', $response['products']); + $this->assertCount(0, $response['products']['items']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php index 1a95a3d6f4925..226f240a247ec 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductSearchTest.php @@ -313,7 +313,7 @@ public function testFilterProductsByDropDownCustomAttribute() $product1 = $productRepository->get('simple'); $product2 = $productRepository->get('12345'); $product3 = $productRepository->get('simple-4'); - $filteredProducts = [$product1, $product2, $product3 ]; + $filteredProducts = [$product3, $product2, $product1]; $countOfFilteredProducts = count($filteredProducts); $this->reIndexAndCleanCache(); $response = $this->graphQlQuery($query); @@ -570,8 +570,8 @@ public function testSearchAndFilterByCustomAttribute() ], [ 'count' => 1, - 'label' => '40-*', - 'value' => '40_*', + 'label' => '40-50', + 'value' => '40_50', ], ], @@ -898,7 +898,7 @@ public function testFilterByMultipleProductUrlKeys() $product1 = $productRepository->get('simple'); $product2 = $productRepository->get('12345'); $product3 = $productRepository->get('simple-4'); - $filteredProducts = [$product1, $product2, $product3]; + $filteredProducts = [$product3, $product2, $product1]; $urlKey =[]; foreach ($filteredProducts as $product) { $urlKey[] = $product->getUrlKey(); @@ -1431,8 +1431,8 @@ public function testFilterProductsForExactMatchingName() 'count' => 1, ], [ - 'label' => '20-*', - 'value' => '20_*', + 'label' => '20-30', + 'value' => '20_30', 'count' => 1, ] ] diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php index 99fdfb2cf1b00..87f8b62ed84a1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Catalog/ProductViewTest.php @@ -232,7 +232,7 @@ public function testQueryAllFieldsSimpleProduct() special_from_date special_price special_to_date - swatch_image + swatch_image tier_price tier_prices { @@ -578,8 +578,8 @@ public function testProductLinks() */ public function testProductPrices() { - $firstProductSku = 'simple-249'; - $secondProductSku = 'simple-156'; + $firstProductSku = 'simple-156'; + $secondProductSku = 'simple-249'; $query = <<<QUERY { products(filter: {price: {from: "150.0", to: "250.0"}}) @@ -665,7 +665,8 @@ private function assertMediaGalleryEntries($product, $actualResponse) { $mediaGalleryEntries = $product->getMediaGalleryEntries(); $this->assertCount(1, $mediaGalleryEntries, "Precondition failed, incorrect number of media gallery entries."); - $this->assertIsArray([$actualResponse['media_gallery_entries']], + $this->assertIsArray( + [$actualResponse['media_gallery_entries']], "Media galleries field must be of an array type." ); $this->assertCount(1, $actualResponse['media_gallery_entries'], "There must be 1 record in media gallery."); @@ -701,10 +702,10 @@ private function assertMediaGalleryEntries($product, $actualResponse) */ private function assertCustomAttribute($actualResponse) { - $customAttribute = null; + $customAttribute = 'customAttributeValue'; $this->assertEquals($customAttribute, $actualResponse['attribute_code_custom']); } - + /** * @param ProductInterface $product * @param $actualResponse @@ -1049,7 +1050,7 @@ public function testProductInNonAnchoredSubCategories() { $query = <<<QUERY { - products(filter: + products(filter: { sku: {in:["12345"]} } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php new file mode 100644 index 0000000000000..a3f98c4cd81ba --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/PriceTiersTest.php @@ -0,0 +1,161 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +class PriceTiersTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var GetCustomerAuthenticationHeader + */ + private $getCustomerAuthenticationHeader; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->getCustomerAuthenticationHeader = $this->objectManager->get(GetCustomerAuthenticationHeader::class); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testAllGroups() + { + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + + $response = $this->graphQlQuery($query); + + $itemTiers = $response['products']['items'][0]['price_tiers']; + $this->assertCount(5, $itemTiers); + $this->assertEquals(8, $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(5, $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(6, $this->getValueForQuantity(3.2, $itemTiers)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php + */ + public function testLoggedInCustomer() + { + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + $response = $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthenticationHeader->execute('customer@example.com', 'password') + ); + + $itemTiers = $response['products']['items'][0]['price_tiers']; + $this->assertCount(3, $itemTiers); + $this->assertEquals(9.25, $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(8.25, $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(7.25, $this->getValueForQuantity(5, $itemTiers)); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php + */ + public function testSecondStoreViewWithCurrencyRate() + { + $storeViewCode = 'fixture_second_store'; + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $rate = $storeRepository->get($storeViewCode)->getCurrentCurrencyRate(); + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + $headers = array_merge( + $this->getCustomerAuthenticationHeader->execute('customer@example.com', 'password'), + $this->getHeaderStore($storeViewCode) + ); + + $response = $this->graphQlQuery( + $query, + [], + '', + $headers + ); + + $itemTiers = $response['products']['items'][0]['price_tiers']; + $this->assertCount(3, $itemTiers); + $this->assertEquals(round(9.25 * $rate, 2), $this->getValueForQuantity(2, $itemTiers)); + $this->assertEquals(round(8.25 * $rate, 2), $this->getValueForQuantity(3, $itemTiers)); + $this->assertEquals(round(7.25 * $rate, 2), $this->getValueForQuantity(5, $itemTiers)); + } + + /** + * Get the tier price value for the given product quantity + * + * @param float $quantity + * @param array $tiers + * @return float + */ + private function getValueForQuantity(float $quantity, array $tiers): float + { + $filteredResult = array_values(array_filter($tiers, function ($tier) use ($quantity) { + if ((float)$tier['quantity'] == $quantity) { + return $tier; + } + })); + + return (float)$filteredResult[0]['final_price']['value']; + } + + /** + * Get a query which user filter for product sku and returns price_tiers + * + * @param string $productSku + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + price_tiers { + final_price { + currency + value + } + discount { + amount_off + percent_off + } + quantity + } + } + } +} +QUERY; + } + + /** + * Get array that would be used in request header + * + * @param string $storeViewCode + * @return array + */ + private function getHeaderStore(string $storeViewCode): array + { + return ['Store' => $storeViewCode]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/SpecialPriceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/SpecialPriceTest.php new file mode 100644 index 0000000000000..931bb3f3c5d32 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CatalogCustomer/SpecialPriceTest.php @@ -0,0 +1,94 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\CatalogCustomer; + +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +class SpecialPriceTest extends GraphQlAbstract +{ + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/product_special_price.php + */ + public function testSpecialPrice() + { + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + + $response = $this->graphQlQuery($query); + + $specialPrice = (float)$response['products']['items'][0]['special_price']; + $this->assertEquals(5.99, $specialPrice); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_store_with_second_currency.php + * @magentoApiDataFixture Magento/Catalog/_files/product_special_price.php + */ + public function testSpecialPriceWithCurrencyRate() + { + $storeViewCode = 'fixture_second_store'; + $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $rate = $storeRepository->get($storeViewCode)->getCurrentCurrencyRate(); + $productSku = 'simple'; + $query = $this->getProductSearchQuery($productSku); + $headers = $this->getHeaderStore($storeViewCode); + + $response = $this->graphQlQuery( + $query, + [], + '', + $headers + ); + + $specialPrice = (float)$response['products']['items'][0]['special_price']; + $this->assertEquals(round(5.99 * $rate, 2), $specialPrice); + } + + /** + * Get a query which user filter for product sku and returns special_price + * + * @param string $productSku + * @return string + */ + private function getProductSearchQuery(string $productSku): string + { + return <<<QUERY +{ + products(filter: {sku: {eq: "{$productSku}"}}) { + items { + special_price + } + } +} +QUERY; + } + + /** + * Get array that would be used in request header + * + * @param string $storeViewCode + * @return array + */ + private function getHeaderStore(string $storeViewCode): array + { + return ['Store' => $storeViewCode]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..a2b7b54fb875a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/AddConfigurableProductToCartSingleMutationTest.php @@ -0,0 +1,297 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct; + +use Exception; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add configurable product to cart testcases + */ +class AddConfigurableProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddConfigurableProductToCart() + { + $product = $this->getConfigurableProductInfo(); + $quantity = 2; + $parentSku = $product['sku']; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][1]['value_index']; + + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $product['sku'], + 2, + $selectedConfigurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + $cartItem = current($response['addProductsToCart']['cart']['items']); + self::assertEquals($quantity, $cartItem['quantity']); + self::assertEquals($parentSku, $cartItem['product']['sku']); + self::assertArrayHasKey('configurable_options', $cartItem); + + $option = current($cartItem['configurable_options']); + self::assertEquals($attributeId, $option['id']); + self::assertEquals($valueIndex, $option['value_id']); + self::assertArrayHasKey('option_label', $option); + self::assertArrayHasKey('value_label', $option); + } + + /** + * Generates UID for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * @return string + */ + private function generateSuperAttributesUIDQuery(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + } + + /** + * @magentoApiDataFixture Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddConfigurableProductWithWrongSuperAttributes() + { + $product = $this->getConfigurableProductInfo(); + $quantity = 2; + $parentSku = $product['sku']; + + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery(0, 0); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + $quantity, + $selectedConfigurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + self::assertEquals( + 'You need to choose options for your item.', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddProductIfQuantityIsNotAvailable() + { + $product = $this->getConfigurableProductInfo(); + $parentSku = $product['sku']; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][1]['value_index']; + + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + 2000, + $selectedConfigurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + self::assertEquals( + 'The requested qty is not available', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_sku.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddNonExistentConfigurableProductParentToCart() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $parentSku = 'configurable_no_exist'; + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + 1, + '' + ); + + $response = $this->graphQlMutation($query); + + self::assertEquals( + 'Could not find a product with SKU "configurable_no_exist"', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable_zero_qty_first_child.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testOutOfStockVariationToCart() + { + $product = $this->getConfigurableProductInfo(); + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; + $parentSku = $product['sku']; + + $configurableOptionsQuery = $this->generateSuperAttributesUIDQuery($attributeId, $valueIndex); + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getQuery( + $maskedQuoteId, + $parentSku, + 1, + $configurableOptionsQuery + ); + + $response = $this->graphQlMutation($query); + + $expectedErrorMessages = [ + 'There are no source items with the in stock status', + 'This product is out of stock.' + ]; + $this->assertContains( + $response['addProductsToCart']['user_errors'][0]['message'], + $expectedErrorMessages + ); + } + + /** + * @param string $maskedQuoteId + * @param string $parentSku + * @param int $quantity + * @param string $selectedOptionsQuery + * @return string + */ + private function getQuery( + string $maskedQuoteId, + string $parentSku, + int $quantity, + string $selectedOptionsQuery + ): string { + return <<<QUERY +mutation { + addProductsToCart( + cartId:"{$maskedQuoteId}" + cartItems: [ + { + sku: "{$parentSku}" + quantity: $quantity + {$selectedOptionsQuery} + } + ] + ) { + cart { + items { + id + quantity + product { + sku + } + ... on ConfigurableCartItem { + configurable_options { + id + option_label + value_id + value_label + } + } + } + }, + user_errors { + message + } + } +} +QUERY; + } + + /** + * Returns information about testable configurable product retrieved from GraphQl query + * + * @return array + * @throws Exception + */ + private function getConfigurableProductInfo(): array + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + return current($searchResponse['products']['items']); + } + + /** + * Returns GraphQl query for fetching configurable product information + * + * @param string $term + * @return string + */ + private function getFetchProductQuery(string $term): string + { + return <<<QUERY +{ + products( + search:"{$term}" + pageSize:1 + ) { + items { + sku + ... on ConfigurableProduct { + configurable_options { + attribute_id + attribute_code + id + label + position + product_id + use_default + values { + default_label + label + store_label + use_default_value + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/Options/Uid/CustomizableValueUidTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/Options/Uid/CustomizableValueUidTest.php new file mode 100644 index 0000000000000..070d917b85330 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/ConfigurableProduct/Options/Uid/CustomizableValueUidTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\ConfigurableProduct\Options\Uid; + +use Magento\Catalog\Model\Product; +use Magento\Eav\Model\ResourceModel\Entity\Attribute; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for downloadable product links uid + */ +class CustomizableValueUidTest extends GraphQlAbstract +{ + /** + * @var Attribute + */ + private $eavAttribute; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->eavAttribute = $objectManager->get(Attribute::class); + } + + /** + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_one_simple.php + */ + public function testQueryUidForConfigurableSuperAttributes() + { + $productSku = 'configurable'; + $query = $this->getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + self::assertNotEmpty($responseProduct['variants']); + + foreach ($responseProduct['variants'] as $variant) { + self::assertNotEmpty($variant['attributes']); + + foreach ($variant['attributes'] as $attribute) { + $attributeId = (int) $this->eavAttribute->getIdByCode(Product::ENTITY, $attribute['code']); + $uid = $this->getUidByOptionIds($attributeId, $attribute['value_index']); + self::assertEquals($uid, $attribute['uid']); + } + } + } + + /** + * Get Uid + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function getUidByOptionIds(int $optionId, int $optionValueId): string + { + return base64_encode('configurable/' . $optionId . '/' . $optionValueId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<<QUERY +query { + products(filter: { sku: { eq: "$sku" } }) { + items { + ... on ConfigurableProduct { + variants { + attributes { + uid + code + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php new file mode 100644 index 0000000000000..25c808a549e80 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/CorsHeadersTest.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\GraphQl\Model\Cors\Configuration; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class CorsHeadersTest extends GraphQlAbstract +{ + /** + * @var Config $config + */ + private $resourceConfig; + /** + * @var ReinitableConfigInterface + */ + private $reinitConfig; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = ObjectManager::getInstance(); + + $this->resourceConfig = $objectManager->get(Config::class); + $this->reinitConfig = $objectManager->get(ReinitableConfigInterface::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->reinitConfig->reinit(); + } + + public function testNoCorsHeadersWhenCorsIsDisabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 0); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + public function testCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, 'Origin'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, '1'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, 'GET,POST'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, 'magento.local'); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, '86400'); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertEquals('Origin', $headers['Access-Control-Allow-Headers']); + self::assertEquals('1', $headers['Access-Control-Allow-Credentials']); + self::assertEquals('GET,POST', $headers['Access-Control-Allow-Methods']); + self::assertEquals('magento.local', $headers['Access-Control-Allow-Origin']); + self::assertEquals('86400', $headers['Access-Control-Max-Age']); + } + + public function testEmptyCorsHeadersWhenCorsIsEnabled(): void + { + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_HEADERS_ENABLED, 1); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_HEADERS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOW_CREDENTIALS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_METHODS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_ALLOWED_ORIGINS, ''); + $this->resourceConfig->saveConfig(Configuration::XML_PATH_CORS_MAX_AGE, ''); + $this->reinitConfig->reinit(); + + $headers = $this->getHeadersFromIntrospectionQuery(); + + self::assertArrayNotHasKey('Access-Control-Max-Age', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Credentials', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Headers', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Methods', $headers); + self::assertArrayNotHasKey('Access-Control-Allow-Origin', $headers); + } + + private function getHeadersFromIntrospectionQuery(): array + { + $query + = <<<QUERY + query IntrospectionQuery { + __schema { + types { + name + } + } + } +QUERY; + + return $this->graphQlQueryWithResponseHeaders($query)['headers'] ?? []; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php index 3560a6ba48dd5..4f2b8f7566d31 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerTest.php @@ -123,7 +123,7 @@ public function testCreateCustomerIfInputDataIsEmpty() mutation { createCustomer( input: { - + } ) { customer { @@ -339,7 +339,9 @@ public function testCreateCustomerSubscribed() public function testCreateCustomerIfCustomerWithProvidedEmailAlreadyExists() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('A customer with the same email address already exists in an associated website.'); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); $existedEmail = 'customer@example.com'; $password = 'test123#'; diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php new file mode 100644 index 0000000000000..10d17d5f7d1b3 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/CreateCustomerV2Test.php @@ -0,0 +1,390 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for create customer (V2) + */ +class CreateCustomerV2Test extends GraphQlAbstract +{ + /** + * @var Registry + */ + private $registry; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + protected function setUp(): void + { + parent::setUp(); + + $this->registry = Bootstrap::getObjectManager()->get(Registry::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertNull($response['createCustomerV2']['customer']['id']); + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + } + + /** + * @throws \Exception + */ + public function testCreateCustomerAccountWithoutPassword() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $response = $this->graphQlMutation($query); + + $this->assertEquals($newFirstname, $response['createCustomerV2']['customer']['firstname']); + $this->assertEquals($newLastname, $response['createCustomerV2']['customer']['lastname']); + $this->assertEquals($newEmail, $response['createCustomerV2']['customer']['email']); + $this->assertTrue($response['createCustomerV2']['customer']['is_subscribed']); + } + + /** + */ + public function testCreateCustomerIfInputDataIsEmpty() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('CustomerCreateInput.email of required type String! was not provided.'); + $this->expectExceptionMessage('CustomerCreateInput.firstname of required type String! was not provided.'); + $this->expectExceptionMessage('CustomerCreateInput.lastname of required type String! was not provided.'); + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + */ + public function testCreateCustomerIfEmailMissed() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Field CustomerCreateInput.email of required type String! was not provided'); + + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @dataProvider invalidEmailAddressDataProvider + * + * @param string $email + * @throws \Exception + */ + public function testCreateCustomerIfEmailIsNotValid(string $email) + { + $firstname = 'Richard'; + $lastname = 'Rowe'; + $password = 'test123#'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$firstname}" + lastname: "{$lastname}" + email: "{$email}" + password: "{$password}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->expectExceptionMessage('"' . $email . '" is not a valid email address.'); + $this->graphQlMutation($query); + } + + /** + * @return array + */ + public function invalidEmailAddressDataProvider(): array + { + return [ + ['plainaddress'], + ['jØrgen@somedomain.com'], + ['#@%^%#$@#$@#.com'], + ['@example.com'], + ['Joe Smith <email@example.com>'], + ['email.example.com'], + ['email@example@example.com'], + ['email@example.com (Joe Smith)'], + ['email@example'], + ['“email”@example.com'], + ]; + } + + /** + */ + public function testCreateCustomerIfPassedAttributeDosNotExistsInCustomerInput() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Field "test123" is not defined by type CustomerCreateInput.'); + + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + test123: "123test123" + email: "{$newEmail}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + */ + public function testCreateCustomerIfNameEmpty() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Required parameters are missing: First Name'); + + $newEmail = 'customer_created' . rand(1, 2000000) . '@example.com'; + $newFirstname = ''; + $newLastname = 'Rowe'; + $currentPassword = 'test123#'; + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + email: "{$newEmail}" + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + password: "{$currentPassword}" + is_subscribed: true + } + ) { + customer { + id + firstname + lastname + email + is_subscribed + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoConfigFixture default_store newsletter/general/active 0 + */ + public function testCreateCustomerSubscribed() + { + $newFirstname = 'Richard'; + $newLastname = 'Rowe'; + $newEmail = 'new_customer@example.com'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + firstname: "{$newFirstname}" + lastname: "{$newLastname}" + email: "{$newEmail}" + is_subscribed: true + } + ) { + customer { + email + is_subscribed + } + } +} +QUERY; + + $response = $this->graphQlMutation($query); + + $this->assertFalse($response['createCustomerV2']['customer']['is_subscribed']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCreateCustomerIfCustomerWithProvidedEmailAlreadyExists() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); + + $existedEmail = 'customer@example.com'; + $password = 'test123#'; + $firstname = 'John'; + $lastname = 'Smith'; + + $query = <<<QUERY +mutation { + createCustomerV2( + input: { + email: "{$existedEmail}" + password: "{$password}" + firstname: "{$firstname}" + lastname: "{$lastname}" + } + ) { + customer { + firstname + lastname + email + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + protected function tearDown(): void + { + $newEmail = 'new_customer@example.com'; + try { + $customer = $this->customerRepository->get($newEmail); + } catch (\Exception $exception) { + return; + } + + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $this->customerRepository->delete($customer); + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + parent::tearDown(); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerEmailTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerEmailTest.php new file mode 100644 index 0000000000000..ca21aa7d9c45b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerEmailTest.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\CustomerGraphQl\Model\Customer\UpdateCustomerAccount; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for update customer's email + */ +class UpdateCustomerEmailTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + /** + * @var UpdateCustomerAccount + */ + private $updateCustomerAccount; + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * Setting up tests + */ + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->customerRepository = Bootstrap::getObjectManager()->get(CustomerRepositoryInterface::class); + $this->updateCustomerAccount = Bootstrap::getObjectManager()->get(UpdateCustomerAccount::class); + $this->storeRepository = Bootstrap::getObjectManager()->get(StoreRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerEmail(): void + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $newEmail = 'newcustomer@example.com'; + + $query = <<<QUERY +mutation { + updateCustomerEmail( + email: "{$newEmail}" + password: "{$currentPassword}" + ) { + customer { + email + } + } +} +QUERY; + + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertEquals($newEmail, $response['updateCustomerEmail']['customer']['email']); + +/* $this->updateCustomerAccount->execute( + $this->customerRepository->get($newEmail), + ['email' => $currentEmail, 'password' => $currentPassword], + $this->storeRepository->getById(1) + );*/ + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerEmailIfPasswordIsWrong(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid login or password.'); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $newEmail = 'newcustomer@example.com'; + $wrongPassword = 'wrongpassword'; + + $query = <<<QUERY +mutation { + updateCustomerEmail( + email: "{$newEmail}" + password: "{$wrongPassword}" + ) { + customer { + email + } + } +} +QUERY; + + $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + */ + public function testUpdateEmailIfEmailAlreadyExists() + { + $this->expectException(Exception::class); + $this->expectExceptionMessage( + 'A customer with the same email address already exists in an associated website.' + ); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $existedEmail = 'customer_two@example.com'; + + $query = <<<QUERY +mutation { + updateCustomerEmail( + email: "{$existedEmail}" + password: "{$currentPassword}" + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * Get customer authorization headers + * + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php index c42450d86fd58..8d6bae35de49b 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerTest.php @@ -368,6 +368,32 @@ public function testEmptyCustomerLastName() $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerWithIncorrectGender() + { + $gender = 5; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('"' . $gender . '" is not a valid gender value.'); + + $query = <<<QUERY +mutation { + updateCustomer( + input: { + gender: {$gender} + } + ) { + customer { + gender + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + /** * @magentoApiDataFixture Magento/Customer/_files/customer.php */ diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2Test.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2Test.php new file mode 100644 index 0000000000000..8b3d1a565add4 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Customer/UpdateCustomerV2Test.php @@ -0,0 +1,273 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Customer; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Tests for new update customer endpoint + */ +class UpdateCustomerV2Test extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var LockCustomer + */ + private $lockCustomer; + + protected function setUp(): void + { + parent::setUp(); + + $this->customerTokenService = Bootstrap::getObjectManager()->get(CustomerTokenServiceInterface::class); + $this->lockCustomer = Bootstrap::getObjectManager()->get(LockCustomer::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomer(): void + { + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $newPrefix = 'Dr'; + $newFirstname = 'Richard'; + $newMiddlename = 'Riley'; + $newLastname = 'Rowe'; + $newSuffix = 'III'; + $newDob = '3/11/1972'; + $newTaxVat = 'GQL1234567'; + $newGender = 2; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + prefix: "{$newPrefix}" + firstname: "{$newFirstname}" + middlename: "{$newMiddlename}" + lastname: "{$newLastname}" + suffix: "{$newSuffix}" + date_of_birth: "{$newDob}" + taxvat: "{$newTaxVat}" + gender: {$newGender} + } + ) { + customer { + prefix + firstname + middlename + lastname + suffix + date_of_birth + taxvat + email + gender + } + } +} +QUERY; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->getCustomerAuthHeaders($currentEmail, $currentPassword) + ); + + $this->assertEquals($newPrefix, $response['updateCustomerV2']['customer']['prefix']); + $this->assertEquals($newFirstname, $response['updateCustomerV2']['customer']['firstname']); + $this->assertEquals($newMiddlename, $response['updateCustomerV2']['customer']['middlename']); + $this->assertEquals($newLastname, $response['updateCustomerV2']['customer']['lastname']); + $this->assertEquals($newSuffix, $response['updateCustomerV2']['customer']['suffix']); + $this->assertEquals($newDob, $response['updateCustomerV2']['customer']['date_of_birth']); + $this->assertEquals($newTaxVat, $response['updateCustomerV2']['customer']['taxvat']); + $this->assertEquals($newGender, $response['updateCustomerV2']['customer']['gender']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerIfInputDataIsEmpty(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('"input" value should be specified'); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + */ + public function testUpdateCustomerIfUserIsNotAuthorized(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('The current customer isn\'t authorized.'); + + $newFirstname = 'Richard'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + firstname: "{$newFirstname}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerIfAccountIsLocked(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('The account is locked.'); + + $this->lockCustomer->execute(1); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $newFirstname = 'Richard'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + firstname: "{$newFirstname}" + } + ) { + customer { + firstname + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmptyCustomerName(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Required parameters are missing: First Name'); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + firstname: "" + } + ) { + customer { + email + } + } +} +QUERY; + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders($currentEmail, $currentPassword)); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testEmptyCustomerLastName(): void + { + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + lastname: "" + } + ) { + customer { + lastname + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Required parameters are missing: Last Name'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testUpdateCustomerIfDobIsInvalid(): void + { + $invalidDob = 'bla-bla-bla'; + + $query = <<<QUERY +mutation { + updateCustomerV2( + input: { + date_of_birth: "{$invalidDob}" + } + ) { + customer { + date_of_birth + } + } +} +QUERY; + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Invalid date'); + + $this->graphQlMutation($query, [], '', $this->getCustomerAuthHeaders('customer@example.com', 'password')); + } + + /** + * @param string $email + * @param string $password + * @return array + * @throws AuthenticationException + */ + private function getCustomerAuthHeaders(string $email, string $password): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/Options/Uid/DownloadableLinksValueUidTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/Options/Uid/DownloadableLinksValueUidTest.php new file mode 100644 index 0000000000000..e2daadc5e743d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/DownloadableProduct/Options/Uid/DownloadableLinksValueUidTest.php @@ -0,0 +1,73 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\DownloadableProduct\Options\Uid; + +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for downloadable product links uid + */ +class DownloadableLinksValueUidTest extends GraphQlAbstract +{ + /** + * @magentoApiDataFixture Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php + */ + public function testQueryUidForDownloadableLinks() + { + $productSku = 'downloadable-product'; + $query = $this->getQuery($productSku); + $response = $this->graphQlQuery($query); + $responseProduct = $response['products']['items'][0]; + + self::assertNotEmpty($responseProduct['downloadable_product_links']); + + foreach ($responseProduct['downloadable_product_links'] as $productLink) { + $uid = $this->getUidByLinkId((int) $productLink['id']); + self::assertEquals($uid, $productLink['uid']); + } + } + + /** + * Get uid by link id + * + * @param int $linkId + * + * @return string + */ + private function getUidByLinkId(int $linkId): string + { + return base64_encode('downloadable/' . $linkId); + } + + /** + * Get query + * + * @param string $sku + * + * @return string + */ + private function getQuery(string $sku): string + { + return <<<QUERY +query { + products(filter: { sku: { eq: "$sku" } }) { + items { + sku + + ... on DownloadableProduct { + downloadable_product_links { + id + uid + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php new file mode 100644 index 0000000000000..8b51d37b50a27 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GetCustomerAuthenticationHeader.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl; + +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; + +/** + * Get authentication header for customer + */ +class GetCustomerAuthenticationHeader +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @param CustomerTokenServiceInterface $customerTokenService + */ + public function __construct(CustomerTokenServiceInterface $customerTokenService) + { + $this->customerTokenService = $customerTokenService; + } + + /** + * Get header to perform customer authenticated request + * + * @param string $email + * @param string $password + * @return string[] + * @throws AuthenticationException + */ + public function execute(string $email = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($email, $password); + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php new file mode 100644 index 0000000000000..fa0909d556b3a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GiftMessage/Cart/Item/GiftMessageTest.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\GiftMessage\Cart\Item; + +use Exception; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\GraphQl\Quote\GetMaskedQuoteIdByReservedOrderId; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class GiftMessageTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItemNotAllow() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws NoSuchEntityException + * @throws Exception + */ + public function testGiftMessageCartForItem() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_guest_order_with_gift_message'); + foreach ($this->requestCartResult($maskedQuoteId)['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertArrayHasKey('to', $item['gift_message']); + self::assertArrayHasKey('from', $item['gift_message']); + self::assertArrayHasKey('message', $item['gift_message']); + } + } + + /** + * @param string $quoteId + * + * @return array|bool|float|int|string + * @throws Exception + */ + private function requestCartResult(string $quoteId) + { + $query = <<<QUERY +{ + cart(cart_id: "$quoteId") { + items { + product { + name + } + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } +} +QUERY; + return $this->graphQlQuery($query); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php index e6db0b9e808ef..8cb0a6db972b4 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/GroupedProduct/GroupedProductViewTest.php @@ -7,12 +7,29 @@ namespace Magento\GraphQl\GroupedProduct; +use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; +/** + * Class to test GraphQl response with grouped products + */ class GroupedProductViewTest extends GraphQlAbstract { + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + } /** * @magentoApiDataFixture Magento/GroupedProduct/_files/product_grouped.php @@ -20,17 +37,16 @@ class GroupedProductViewTest extends GraphQlAbstract public function testAllFieldsGroupedProduct() { $productSku = 'grouped-product'; - $query - = <<<QUERY + $query = <<<QUERY { products(filter: {sku: {eq: "{$productSku}"}}) { - items { + items { id attribute_set_id created_at name sku - type_id + type_id ... on GroupedProduct { items{ qty @@ -39,9 +55,14 @@ public function testAllFieldsGroupedProduct() sku name type_id - url_key + url_key } } + product_links{ + linked_product_sku + position + link_type + } } } } @@ -49,47 +70,77 @@ public function testAllFieldsGroupedProduct() QUERY; $response = $this->graphQlQuery($query); - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - $groupedProduct = $productRepository->get($productSku, false, null, true); + $groupedProduct = $this->productRepository->get($productSku, false, null, true); - $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]); + $this->assertNotEmpty( + $response['products']['items'][0]['items'], + "Precondition failed: 'Grouped product items' must not be empty" + ); + $this->assertGroupedProductItems($groupedProduct, $response['products']['items'][0]['items']); + $this->assertNotEmpty( + $response['products']['items'][0]['product_links'], + "Precondition failed: 'Linked product items' must not be empty" + ); + $this->assertProductLinks($groupedProduct, $response['products']['items'][0]['product_links']); } - private function assertGroupedProductItems($product, $actualResponse) + /** + * @param ProductInterface $product + * @param array $items + */ + private function assertGroupedProductItems(ProductInterface $product, array $items): void { - $this->assertNotEmpty( - $actualResponse['items'], - "Precondition failed: 'grouped product items' must not be empty" - ); - $this->assertCount(2, $actualResponse['items']); + $this->assertCount(2, $items); $groupedProductLinks = $product->getProductLinks(); - foreach ($actualResponse['items'] as $itemIndex => $bundleItems) { - $this->assertNotEmpty($bundleItems); + foreach ($items as $itemIndex => $bundleItem) { + $this->assertNotEmpty($bundleItem); $associatedProductSku = $groupedProductLinks[$itemIndex]->getLinkedProductSku(); - - $productsRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); - /** @var \Magento\Catalog\Model\Product $associatedProduct */ - $associatedProduct = $productsRepository->get($associatedProductSku); + $associatedProduct = $this->productRepository->get($associatedProductSku); $this->assertEquals( $groupedProductLinks[$itemIndex]->getExtensionAttributes()->getQty(), - $actualResponse['items'][$itemIndex]['qty'] + $bundleItem['qty'] ); $this->assertEquals( $groupedProductLinks[$itemIndex]->getPosition(), - $actualResponse['items'][$itemIndex]['position'] + $bundleItem['position'] ); $this->assertResponseFields( - $actualResponse['items'][$itemIndex]['product'], + $bundleItem['product'], [ - 'sku' => $associatedProductSku, - 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), - 'url_key'=> $associatedProduct->getUrlKey(), - 'name' => $associatedProduct->getName() + 'sku' => $associatedProductSku, + 'type_id' => $groupedProductLinks[$itemIndex]->getLinkedProductType(), + 'url_key'=> $associatedProduct->getUrlKey(), + 'name' => $associatedProduct->getName() ] ); } } + + /** + * @param ProductInterface $product + * @param array $links + * @return void + */ + private function assertProductLinks(ProductInterface $product, array $links): void + { + $this->assertCount(2, $links); + $productLinks = $product->getProductLinks(); + foreach ($links as $itemIndex => $linkedItem) { + $this->assertNotEmpty($linkedItem); + $this->assertEquals( + $productLinks[$itemIndex]->getPosition(), + $linkedItem['position'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkedProductSku(), + $linkedItem['linked_product_sku'] + ); + $this->assertEquals( + $productLinks[$itemIndex]->getLinkType(), + $linkedItem['link_type'] + ); + } + } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..956316c1fa0fa --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddDownloadableProductToCartSingleMutationTest.php @@ -0,0 +1,196 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManager\ObjectManager; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test cases for adding downloadable product with custom options to cart using the single add to cart mutation. + */ +class AddDownloadableProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var GetCustomOptionsWithUIDForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * @var GetCartItemOptionsFromUID + */ + private $getCartItemOptionsFromUID; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $this->objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCartItemOptionsFromUID = $this->objectManager->get(GetCartItemOptionsFromUID::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = + $this->objectManager->get(GetCustomOptionsWithUIDForQueryBySku::class); + } + + /** + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddDownloadableProductWithOptions() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 1; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $decodedItemOptions = $this->getCartItemOptionsFromUID->execute($itemOptions); + + /* The type field is only required for assertions, it should not be present in query */ + foreach ($itemOptions['entered_options'] as &$enteredOption) { + if (isset($enteredOption['type'])) { + unset($enteredOption['type']); + } + } + + /* Add downloadable product link data to the "selected_options" */ + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + + $query = $this->getQuery($maskedQuoteId, $qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addProductsToCart']['cart']); + self::assertCount($qty, $response['addProductsToCart']['cart']); + self::assertEquals($linkId, $response['addProductsToCart']['cart']['items'][0]['links'][0]['id']); + + $customizableOptionsOutput = + $response['addProductsToCart']['cart']['items'][0]['customizable_options']; + + foreach ($customizableOptionsOutput as $customizableOptionOutput) { + $customizableOptionOutputValues = []; + foreach ($customizableOptionOutput['values'] as $customizableOptionOutputValue) { + $customizableOptionOutputValues[] = $customizableOptionOutputValue['value']; + } + if (count($customizableOptionOutputValues) === 1) { + $customizableOptionOutputValues = $customizableOptionOutputValues[0]; + } + + self::assertEquals( + $decodedItemOptions[$customizableOptionOutput['id']], + $customizableOptionOutputValues + ); + } + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * @return array + */ + private function getProductsLinks(string $sku) : array + { + $result = []; + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'link_type' => null, //deprecated field + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } + + /** + * Generates UID for downloadable links + * + * @param int $linkId + * @return string + */ + private function generateProductLinkSelectedOptions(int $linkId): string + { + return base64_encode("downloadable/$linkId"); + } + + /** + * Returns GraphQl query string + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getQuery( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] + ) { + cart { + items { + quantity + ... on DownloadableCartItem { + links { + id + } + customizable_options { + label + id + values { + value + } + } + } + } + }, + user_errors { + message + } + } +} +MUTATION; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartEndToEndTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartEndToEndTest.php new file mode 100644 index 0000000000000..d8f7aedfdd583 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartEndToEndTest.php @@ -0,0 +1,260 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Get customizable options of simple product via the corresponding GraphQl query and add the product + * with customizable options to the shopping cart + */ +class AddSimpleProductToCartEndToEndTest extends GraphQlAbstract +{ + /** + * @var GetCustomOptionsWithUIDForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetCartItemOptionsFromUID + */ + private $getCartItemOptionsFromUID; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCartItemOptionsFromUID = $objectManager->get(GetCartItemOptionsFromUID::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = $objectManager->get( + GetCustomOptionsWithUIDForQueryBySku::class + ); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $sku = 'simple'; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $qty = 1; + + $productOptionsData = $this->getProductOptionsViaQuery($sku); + + $itemOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($productOptionsData['received_options']) + ); + + $query = $this->getAddToCartMutation($maskedQuoteId, $qty, $sku, trim($itemOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('customizable_options', $response['addProductsToCart']['cart']['items'][0]); + + foreach ($response['addProductsToCart']['cart']['items'][0]['customizable_options'] as $option) { + self::assertEquals($productOptionsData['expected_options'][$option['id']], $option['values'][0]['value']); + } + } + + /** + * Get product data with customizable options using GraphQl query + * + * @param string $sku + * @return array + * @throws \Exception + */ + private function getProductOptionsViaQuery(string $sku): array + { + $query = $this->getProductQuery($sku); + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('options', $response['products']['items'][0]); + + $expectedItemOptions = []; + $receivedItemOptions = [ + 'entered_options' => [], + 'selected_options' => [] + ]; + + foreach ($response['products']['items'][0]['options'] as $option) { + if (isset($option['entered_option'])) { + /* The date normalization is required since the attribute might value is formatted by the system */ + if ($option['title'] === 'date option') { + $value = '2012-12-12 00:00:00'; + $expectedItemOptions[$option['option_id']] = date('M d, Y', strtotime($value)); + } else { + $value = 'test'; + $expectedItemOptions[$option['option_id']] = $value; + } + $value = $option['title'] === 'date option' ? '2012-12-12 00:00:00' : 'test'; + + $receivedItemOptions['entered_options'][] = [ + 'uid' => $option['entered_option']['uid'], + 'value' => $value + ]; + + } elseif (isset($option['selected_option'])) { + $receivedItemOptions['selected_options'][] = reset($option['selected_option'])['uid']; + $expectedItemOptions[$option['option_id']] = reset($option['selected_option'])['option_type_id']; + } + } + + return [ + 'expected_options' => $expectedItemOptions, + 'received_options' => $receivedItemOptions + ]; + } + + /** + * Returns GraphQL query for retrieving a product with customizable options + * + * @param string $sku + * @return string + */ + private function getProductQuery(string $sku): string + { + return <<<QUERY +query { + products(search: "$sku") { + items { + sku + + ... on CustomizableProductInterface { + options { + option_id + title + + ... on CustomizableRadioOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableDropDownOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableMultipleOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableCheckboxOption { + option_id + selected_option: value { + option_type_id + uid + } + } + + ... on CustomizableAreaOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFieldOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableFileOption { + option_id + entered_option: value { + uid + } + } + + ... on CustomizableDateOption { + option_id + entered_option: value { + uid + } + } + } + } + } + } +} +QUERY; + } + + /** + * Returns GraphQl mutation for adding item to cart + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getAddToCartMutation( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] + ) { + cart { + items { + quantity + ... on SimpleCartItem { + customizable_options { + label + id + values { + value + } + } + } + } + }, + user_errors { + message + } + } +} +MUTATION; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php new file mode 100644 index 0000000000000..4e50f6ff3a2ca --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/AddSimpleProductToCartSingleMutationTest.php @@ -0,0 +1,259 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Add simple product with custom options to cart using the unified mutation for adding different product types + */ +class AddSimpleProductToCartSingleMutationTest extends GraphQlAbstract +{ + /** + * @var GetCustomOptionsWithUIDForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * @var GetMaskedQuoteIdByReservedOrderId + */ + private $getMaskedQuoteIdByReservedOrderId; + + /** + * @var GetCartItemOptionsFromUID + */ + private $getCartItemOptionsFromUID; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getMaskedQuoteIdByReservedOrderId = $objectManager->get(GetMaskedQuoteIdByReservedOrderId::class); + $this->getCartItemOptionsFromUID = $objectManager->get(GetCartItemOptionsFromUID::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = $objectManager->get( + GetCustomOptionsWithUIDForQueryBySku::class + ); + } + + /** + * Test adding a simple product to the shopping cart with all supported + * customizable options assigned + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddSimpleProductWithOptions() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $sku = 'simple'; + $qty = 1; + + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $decodedItemOptions = $this->getCartItemOptionsFromUID->execute($itemOptions); + + /* The type field is only required for assertions, it should not be present in query */ + foreach ($itemOptions['entered_options'] as &$enteredOption) { + if (isset($enteredOption['type'])) { + unset($enteredOption['type']); + } + } + + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + + $query = $this->getAddToCartMutation($maskedQuoteId, $qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('items', $response['addProductsToCart']['cart']); + self::assertCount($qty, $response['addProductsToCart']['cart']['items']); + $customizableOptionsOutput = + $response['addProductsToCart']['cart']['items'][0]['customizable_options']; + + foreach ($customizableOptionsOutput as $customizableOptionOutput) { + $customizableOptionOutputValues = []; + foreach ($customizableOptionOutput['values'] as $customizableOptionOutputValue) { + $customizableOptionOutputValues[] = $customizableOptionOutputValue['value']; + } + if (count($customizableOptionOutputValues) === 1) { + $customizableOptionOutputValues = $customizableOptionOutputValues[0]; + } + + self::assertEquals( + $decodedItemOptions[$customizableOptionOutput['id']], + $customizableOptionOutputValues + ); + } + } + + /** + * @param string $sku + * @param string $message + * + * @dataProvider wrongSkuDataProvider + * + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddProductWithWrongSku(string $sku, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + + $query = $this->getAddToCartMutation($maskedQuoteId, 1, $sku, ''); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('user_errors', $response['addProductsToCart']); + self::assertCount(1, $response['addProductsToCart']['user_errors']); + self::assertEquals( + $message, + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * The test covers the case when upon adding available_qty + 1 to the shopping cart, the cart is being + * cleared + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_without_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddToCartWithQtyPlusOne() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $sku = 'simple-2'; + + $query = $this->getAddToCartMutation($maskedQuoteId, 100, $sku, ''); + $response = $this->graphQlMutation($query); + + self::assertEquals(100, $response['addProductsToCart']['cart']['total_quantity']); + + $query = $this->getAddToCartMutation($maskedQuoteId, 1, $sku, ''); + $response = $this->graphQlMutation($query); + + self::assertArrayHasKey('user_errors', $response['addProductsToCart']); + self::assertEquals( + 'The requested qty is not available', + $response['addProductsToCart']['user_errors'][0]['message'] + ); + self::assertEquals(100, $response['addProductsToCart']['cart']['total_quantity']); + } + + /** + * @param int $quantity + * @param string $message + * + * @dataProvider wrongQuantityDataProvider + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_without_custom_options.php + * @magentoApiDataFixture Magento/Checkout/_files/active_quote.php + */ + public function testAddProductWithWrongQuantity(int $quantity, string $message) + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_order_1'); + $sku = 'simple-2'; + + $query = $this->getAddToCartMutation($maskedQuoteId, $quantity, $sku, ''); + $response = $this->graphQlMutation($query); + self::assertArrayHasKey('user_errors', $response['addProductsToCart']); + self::assertCount(1, $response['addProductsToCart']['user_errors']); + + self::assertEquals( + $message, + $response['addProductsToCart']['user_errors'][0]['message'] + ); + } + + /** + * @return array + */ + public function wrongSkuDataProvider(): array + { + return [ + 'Non-existent SKU' => [ + 'non-existent', + 'Could not find a product with SKU "non-existent"' + ], + 'Empty SKU' => [ + '', + 'Could not find a product with SKU ""' + ] + ]; + } + + /** + * @return array + */ + public function wrongQuantityDataProvider(): array + { + return [ + 'More quantity than in stock' => [ + 101, + 'The requested qty is not available' + ], + 'Quantity equals zero' => [ + 0, + 'The product quantity should be greater than 0' + ] + ]; + } + + /** + * Returns GraphQl query string + * + * @param string $maskedQuoteId + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * @return string + */ + private function getAddToCartMutation( + string $maskedQuoteId, + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToCart( + cartId: "{$maskedQuoteId}", + cartItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] + ) { + cart { + total_quantity + items { + quantity + ... on SimpleCartItem { + customizable_options { + label + id + values { + value + } + } + } + } + }, + user_errors { + message + } + } +} +MUTATION; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php index 21a8d6ae94312..ff8d4f4280c10 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetPaymentMethodAndPlaceOrderTest.php @@ -281,6 +281,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query, [], '', $this->getHeaderMap()); } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php index 3e06b89c77fb7..08554cbd8fac1 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Customer/SetShippingAddressOnCartTest.php @@ -149,7 +149,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() public function testSetNewShippingAddressOnCartWithVirtualProduct() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The Cart includes virtual product(s) only, so a shipping address is not used.'); + $this->expectExceptionMessage('Shipping address is not allowed on cart: cart contains no items for shipment.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); @@ -1745,6 +1745,57 @@ public function testSetNewShippingAddressWithDefaultValueOfSaveInAddressBookAndP } } + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + */ + public function testSetShippingAddressOnCartWithNullCustomerAddressId() + { + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$maskedQuoteId" + shipping_addresses: [ + { + customer_address_id: null + } + ] + } + ) { + cart { + shipping_addresses { + firstname + lastname + company + street + city + postcode + telephone + country { + label + code + } + region { + code + label + } + __typename + } + } + } +} +QUERY; + $this->expectExceptionMessage( + 'The shipping address must contain either "customer_address_id" or "address".' + ); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + /** * Verify the all the whitelisted fields for a New Address Object * diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartItemOptionsFromUID.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartItemOptionsFromUID.php new file mode 100644 index 0000000000000..44b44e0ccac05 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCartItemOptionsFromUID.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +/** + * Extracts cart item options from UID + */ +class GetCartItemOptionsFromUID +{ + /** + * Gets an array of encoded item options with UID, extracts and decodes the values + * + * @param array $encodedCustomOptions + * @return array + */ + public function execute(array $encodedCustomOptions): array + { + $customOptions = []; + + foreach ($encodedCustomOptions['selected_options'] as $selectedOption) { + [$optionType, $optionId, $optionValueId] = explode('/', base64_decode($selectedOption)); + if ($optionType == 'custom-option') { + if (isset($customOptions[$optionId])) { + $customOptions[$optionId] = [$customOptions[$optionId], $optionValueId]; + } else { + $customOptions[$optionId] = $optionValueId; + } + } + } + + foreach ($encodedCustomOptions['entered_options'] as $enteredOption) { + /* The date normalization is required since the attribute might value is formatted by the system */ + if ($enteredOption['type'] === 'date') { + $enteredOption['value'] = date('M d, Y', strtotime($enteredOption['value'])); + } + [$optionType, $optionId] = explode('/', base64_decode($enteredOption['uid'])); + if ($optionType == 'custom-option') { + $customOptions[$optionId] = $enteredOption['value']; + } + } + + return $customOptions; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsWithUIDForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsWithUIDForQueryBySku.php new file mode 100644 index 0000000000000..870617555e8b2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/GetCustomOptionsWithUIDForQueryBySku.php @@ -0,0 +1,107 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Quote; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Generate an array with test values for customizable options with UID + */ +class GetCustomOptionsWithUIDForQueryBySku +{ + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionRepository; + + /** + * @param ProductCustomOptionRepositoryInterface $productCustomOptionRepository + */ + public function __construct(ProductCustomOptionRepositoryInterface $productCustomOptionRepository) + { + $this->productCustomOptionRepository = $productCustomOptionRepository; + } + + /** + * Returns array of custom options for the product + * + * @param string $sku + * @return array + */ + public function execute(string $sku): array + { + $customOptions = $this->productCustomOptionRepository->getList($sku); + $selectedOptions = []; + $enteredOptions = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + + switch ($optionType) { + case 'field': + case 'area': + $enteredOptions[] = [ + 'type' => 'field', + 'uid' => $this->encodeEnteredOption((int) $customOption->getOptionId()), + 'value' => 'test' + ]; + break; + case 'date': + $enteredOptions[] = [ + 'type' => 'date', + 'uid' => $this->encodeEnteredOption((int) $customOption->getOptionId()), + 'value' => '2012-12-12 00:00:00' + ]; + break; + case 'drop_down': + $optionSelectValues = $customOption->getValues(); + $selectedOptions[] = $this->encodeSelectedOption( + (int) $customOption->getOptionId(), + (int) reset($optionSelectValues)->getOptionTypeId() + ); + break; + case 'multiple': + foreach ($customOption->getValues() as $optionValue) { + $selectedOptions[] = $this->encodeSelectedOption( + (int) $customOption->getOptionId(), + (int) $optionValue->getOptionTypeId() + ); + } + break; + } + } + + return [ + 'selected_options' => $selectedOptions, + 'entered_options' => $enteredOptions + ]; + } + + /** + * Returns UID of the selected custom option + * + * @param int $optionId + * @param int $optionValueId + * @return string + */ + private function encodeSelectedOption(int $optionId, int $optionValueId): string + { + return base64_encode("custom-option/$optionId/$optionValueId"); + } + + /** + * Returns UID of the entered custom option + * + * @param int $optionId + * @return string + */ + private function encodeEnteredOption(int $optionId): string + { + return base64_encode("custom-option/$optionId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php index dbc10700794fa..78691d8cbd889 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetPaymentMethodAndPlaceOrderTest.php @@ -218,6 +218,50 @@ public function testSetDisabledPaymentOnCart() $this->graphQlMutation($query); } + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWitMissingCartId() + { + $methodCode = Checkmo::PAYMENT_METHOD_CHECKMO_CODE; + $maskedQuoteId = ""; + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"cart_id\" is missing" + ); + $this->graphQlMutation($query); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/create_empty_cart.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/guest/set_guest_email.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoApiDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + */ + public function testPlaceOrderWithMissingPaymentMethod() + { + $methodCode = ""; + $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); + + $query = $this->getQuery($maskedQuoteId, $methodCode); + + $this->expectExceptionMessage( + "Required parameter \"code\" for \"payment_method\" is missing." + ); + $this->graphQlMutation($query); + } + /** * @param string $maskedQuoteId * @param string $methodCode diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php index b4136d06bf67c..b7ddd085f932e 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/SetShippingAddressOnCartTest.php @@ -97,7 +97,7 @@ public function testSetNewShippingAddressOnCartWithSimpleProduct() public function testSetNewShippingAddressOnCartWithVirtualProduct() { $this->expectException(\Exception::class); - $this->expectExceptionMessage('The Cart includes virtual product(s) only, so a shipping address is not used.'); + $this->expectExceptionMessage('Shipping address is not allowed on cart: cart contains no items for shipment.'); $maskedQuoteId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php index a17bc1aa3821a..0a22f3ca9721c 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Quote/Guest/UpdateCartItemsTest.php @@ -7,8 +7,9 @@ namespace Magento\GraphQl\Quote\Guest; -use Magento\Quote\Model\QuoteFactory; +use Exception; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\QuoteIdToMaskedQuoteIdInterface; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -273,6 +274,81 @@ private function getCartQuery(string $maskedQuoteId) } } } +QUERY; + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 0 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItemNotAllow() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertNull($item['gift_message']); + } + } + + /** + * @magentoConfigFixture default_store sales/gift_options/allow_items 1 + * @magentoApiDataFixture Magento/GiftMessage/_files/guest/quote_with_item_message.php + * @throws Exception + */ + public function testUpdateGiftMessageCartForItem() + { + $query = $this->getUpdateGiftMessageQuery(); + foreach ($this->graphQlMutation($query)['updateCartItems']['cart']['items'] as $item) { + self::assertArrayHasKey('gift_message', $item); + self::assertSame('Alex', $item['gift_message']['to']); + self::assertSame('Mike', $item['gift_message']['from']); + self::assertSame('Best regards.', $item['gift_message']['message']); + } + } + + private function getUpdateGiftMessageQuery() + { + $quote = $this->quoteFactory->create(); + $this->quoteResource->load($quote, 'test_guest_order_with_gift_message', 'reserved_order_id'); + $maskedQuoteId = $this->quoteIdToMaskedId->execute((int)$quote->getId()); + $itemId = (int)$quote->getItemByProduct($this->productRepository->get('simple'))->getId(); + + return <<<QUERY +mutation { + updateCartItems( + input: { + cart_id: "$maskedQuoteId", + cart_items: [ + { + cart_item_id: $itemId + quantity: 3 + gift_message: { + to: "Alex" + from: "Mike" + message: "Best regards." + } + } + ] + } + ) { + cart { + items { + id + product { + name + } + quantity + ... on SimpleCartItem { + gift_message { + to + from + message + } + } + } + } + } +} QUERY; } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/CreateProductReviewsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/CreateProductReviewsTest.php new file mode 100644 index 0000000000000..f9df1dac5df34 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/CreateProductReviewsTest.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Review; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Registry; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory as ReviewCollectionFactory; +use Magento\Review\Model\Review; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for adding product reviews mutation + */ +class CreateProductReviewsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var ReviewCollectionFactory + */ + private $reviewCollectionFactory; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->customerRepository = $objectManager->get(CustomerRepositoryInterface::class); + $this->reviewCollectionFactory = $objectManager->get(ReviewCollectionFactory::class); + $this->registry = $objectManager->get(Registry::class); + } + + /** + * Test adding a product review as guest and logged in customer + * + * @param string $customerName + * @param bool $isGuest + * + * @magentoApiDataFixture Magento/Review/_files/set_position_and_add_store_to_all_ratings.php + * @magentoApiDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @dataProvider customerDataProvider + */ + public function testCustomerAddProductReviews(string $customerName, bool $isGuest) + { + $productSku = 'simple_product'; + $query = $this->getQuery($productSku, $customerName); + $headers = []; + + if (!$isGuest) { + $headers = $this->getHeaderMap(); + } + + $response = $this->graphQlMutation($query, [], '', $headers); + + $expectedResult = [ + 'nickname' => $customerName, + 'summary' => 'Summary Test', + 'text' => 'Text Test', + 'average_rating' => 66.67, + 'ratings_breakdown' => [ + [ + 'name' => 'Price', + 'value' => 3 + ], [ + 'name' => 'Quality', + 'value' => 2 + ], [ + 'name' => 'Value', + 'value' => 5 + ] + ] + ]; + self::assertArrayHasKey('createProductReview', $response); + self::assertArrayHasKey('review', $response['createProductReview']); + self::assertEquals($expectedResult, $response['createProductReview']['review']); + } + + /** + * @magentoConfigFixture default_store catalog/review/allow_guest 0 + */ + public function testAddProductReviewGuestIsNotAllowed() + { + $productSku = 'simple_product'; + $customerName = 'John Doe'; + $query = $this->getQuery($productSku, $customerName); + self::expectExceptionMessage('Guest customers aren\'t allowed to add product reviews.'); + $this->graphQlMutation($query); + } + + /** + * Removing the recently added product reviews + */ + public function tearDown(): void + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $productId = 1; + /** @var Collection $reviewsCollection */ + $reviewsCollection = $this->reviewCollectionFactory->create(); + $reviewsCollection->addEntityFilter(Review::ENTITY_PRODUCT_CODE, $productId); + /** @var Review $review */ + foreach ($reviewsCollection as $review) { + $review->delete(); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } + + /** + * @return array + */ + public function customerDataProvider(): array + { + return [ + 'Guest Customer' => ['John Doe', true], + 'Logged In Customer' => ['John', false], + ]; + } + + /** + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Get mutation query + * + * @param string $sku + * @param string $customerName + * + * @return string + */ + private function getQuery(string $sku, string $customerName): string + { + return <<<QUERY +mutation { + createProductReview( + input: { + sku: "$sku", + nickname: "$customerName", + summary: "Summary Test", + text: "Text Test", + ratings: [ + { + id: "Mw==", + value_id: "MTM=" + }, { + id: "MQ==", + value_id: "Mg==" + }, { + id: "Mg==", + value_id: "MTA=" + } + ] + } +) { + review { + nickname + summary + text + average_rating + ratings_breakdown { + name + value + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php new file mode 100644 index 0000000000000..f09a1827961f0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Review/GetProductReviewsTest.php @@ -0,0 +1,284 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Review; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Registry; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\Review\Model\ResourceModel\Review\Collection; +use Magento\Review\Model\ResourceModel\Review\CollectionFactory as ReviewCollectionFactory; +use Magento\Review\Model\Review; +use Magento\Review\Model\Review\SummaryFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for product reviews queries + */ +class GetProductReviewsTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ReviewCollectionFactory + */ + private $reviewCollectionFactory; + + /** + * @var Registry + */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->reviewCollectionFactory = $objectManager->get(ReviewCollectionFactory::class); + $this->registry = $objectManager->get(Registry::class); + } + + /** + * @magentoApiDataFixture Magento/Review/_files/set_position_and_add_store_to_all_ratings.php + */ + public function testProductReviewRatingsMetadata() + { + $query + = <<<QUERY +{ + productReviewRatingsMetadata { + items { + id + name + values { + value_id + value + } + } + } +} +QUERY; + $expectedRatingItems = [ + [ + 'id' => 'Mw==', + 'name' => 'Price', + 'values' => [ + [ + 'value_id' => 'MTE=', + 'value' => "1" + ],[ + 'value_id' => 'MTI=', + 'value' => "2" + ],[ + 'value_id' => 'MTM=', + 'value' => "3" + ],[ + 'value_id' => 'MTQ=', + 'value' => "4" + ],[ + 'value_id' => 'MTU=', + 'value' => "5" + ] + ] + ], [ + 'id' => 'MQ==', + 'name' => 'Quality', + 'values' => [ + [ + 'value_id' => 'MQ==', + 'value' => "1" + ],[ + 'value_id' => 'Mg==', + 'value' => "2" + ],[ + 'value_id' => 'Mw==', + 'value' => "3" + ],[ + 'value_id' => 'NA==', + 'value' => "4" + ],[ + 'value_id' => 'NQ==', + 'value' => "5" + ] + ] + ], [ + 'id' => 'Mg==', + 'name' => 'Value', + 'values' => [ + [ + 'value_id' => 'Ng==', + 'value' => "1" + ],[ + 'value_id' => 'Nw==', + 'value' => "2" + ],[ + 'value_id' => 'OA==', + 'value' => "3" + ],[ + 'value_id' => 'OQ==', + 'value' => "4" + ],[ + 'value_id' => 'MTA=', + 'value' => "5" + ] + ] + ] + ]; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('productReviewRatingsMetadata', $response); + self::assertArrayHasKey('items', $response['productReviewRatingsMetadata']); + self::assertNotEmpty($response['productReviewRatingsMetadata']['items']); + self::assertEquals($expectedRatingItems, $response['productReviewRatingsMetadata']['items']); + } + + /** + * @magentoApiDataFixture Magento/Review/_files/different_reviews.php + */ + public function testProductReviewRatings() + { + $productSku = 'simple'; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = ObjectManager::getInstance()->get(ProductRepositoryInterface::class); + $product = $productRepository->get($productSku, false, null, true); + $summaryFactory = ObjectManager::getInstance()->get(SummaryFactory::class); + $storeId = ObjectManager::getInstance()->get(StoreManagerInterface::class)->getStore()->getId(); + $summary = $summaryFactory->create()->setStoreId($storeId)->load($product->getId()); + $query + = <<<QUERY +{ + products(filter: { + sku: { + eq: "$productSku" + } + }) { + items { + rating_summary + review_count + reviews { + items { + nickname + summary + text + average_rating + product { + sku + name + } + ratings_breakdown { + name + value + } + } + } + } + } +} +QUERY; + $response = $this->graphQlQuery($query); + self::assertArrayHasKey('products', $response); + self::assertArrayHasKey('items', $response['products']); + self::assertNotEmpty($response['products']['items']); + + $items = $response['products']['items']; + self::assertEquals($summary->getData('rating_summary'), $items[0]['rating_summary']); + self::assertEquals($summary->getData('reviews_count'), $items[0]['review_count']); + self::assertArrayHasKey('items', $items[0]['reviews']); + self::assertNotEmpty($items[0]['reviews']['items']); + } + + /** + * @magentoApiDataFixture Magento/Review/_files/customer_review_with_rating.php + */ + public function testCustomerReviewsAddedToProduct() + { + $query = <<<QUERY +{ + customer { + reviews { + items { + nickname + summary + text + average_rating + ratings_breakdown { + name + value + } + } + } + } +} +QUERY; + $expectedFirstItem = [ + 'nickname' => 'Nickname', + 'summary' => 'Review Summary', + 'text' => 'Review text', + 'average_rating' => 40, + 'ratings_breakdown' => [ + [ + 'name' => 'Quality', + 'value' => 2 + ],[ + 'name' => 'Value', + 'value' => 2 + ] + ] + ]; + $response = $this->graphQlQuery($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('customer', $response); + self::assertArrayHasKey('reviews', $response['customer']); + self::assertArrayHasKey('items', $response['customer']['reviews']); + self::assertNotEmpty($response['customer']['reviews']['items']); + self::assertEquals($expectedFirstItem, $response['customer']['reviews']['items'][0]); + } + + /** + * Removing the recently added product reviews + */ + public function tearDown(): void + { + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', true); + $productId = 1; + /** @var Collection $reviewsCollection */ + $reviewsCollection = $this->reviewCollectionFactory->create(); + $reviewsCollection->addEntityFilter(Review::ENTITY_PRODUCT_CODE, $productId); + /** @var Review $review */ + foreach ($reviewsCollection as $review) { + $review->delete(); + } + $this->registry->unregister('isSecureArea'); + $this->registry->register('isSecureArea', false); + + parent::tearDown(); + } + + /** + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CreditmemoTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CreditmemoTest.php new file mode 100644 index 0000000000000..cca2b5a66407c --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CreditmemoTest.php @@ -0,0 +1,650 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\GraphQl\Sales\Fixtures\CustomerPlaceOrder; +use Magento\Sales\Api\CreditmemoRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\CreditmemoFactory; +use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; +use Magento\Sales\Model\ResourceModel\Order\Creditmemo\Collection; +use Magento\Sales\Model\Service\CreditmemoService; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for credit memo functionality + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CreditmemoTest extends GraphQlAbstract +{ + /** + * @var GetCustomerAuthenticationHeader + */ + private $customerAuthenticationHeader; + + /** @var CreditmemoFactory */ + private $creditMemoFactory; + + /** @var Order */ + private $order; + + /** @var OrderCollection */ + private $orderCollection; + + /** @var CreditmemoService */ + private $creditMemoService; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** + * Set up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get( + GetCustomerAuthenticationHeader::class + ); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + $this->creditMemoFactory = $objectManager->get(CreditmemoFactory::class); + $this->order = $objectManager->create(Order::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->orderCollection = $objectManager->get(OrderCollection::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->creditMemoService = $objectManager->get(CreditmemoService::class); + } + + protected function tearDown(): void + { + $this->cleanUpCreditMemos(); + $this->deleteOrder(); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_creditmemo_with_two_items.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreditMemoForLoggedInCustomerQuery(): void + { + $response = $this->getCustomerOrderWithCreditMemoQuery(); + + $expectedCreditMemoData = [ + [ + 'comments' => [ + ['message' => 'some_comment'], + ['message' => 'some_other_comment'] + ], + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10 + ], + 'discounts' => [], + 'quantity_refunded' => 1 + ], + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10 + ], + 'discounts' => [], + 'quantity_refunded' => 1 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 20 + ], + 'grand_total' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 10, + 'currency' => 'EUR' + ], + 'total_shipping' => [ + 'value' => 0 + ], + 'total_tax' => [ + 'value' => 0 + ], + 'shipping_handling' => [ + 'amount_including_tax' => [ + 'value' => 0 + ], + 'amount_excluding_tax' => [ + 'value' => 0 + ], + 'total_amount' => [ + 'value' => 0 + ], + 'taxes' => [], + 'discounts' => [], + ], + 'adjustment' => [ + 'value' => 1.23 + ] + ] + ] + ]; + + $firstOrderItem = current($response['customer']['orders']['items'] ?? []); + $this->assertArrayHasKey('credit_memos', $firstOrderItem); + $creditMemos = $firstOrderItem['credit_memos']; + $this->assertResponseFields($creditMemos, $expectedCreditMemoData); + } + + /** + * Test customer refund details from order for bundle product with a partial refund + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreditMemoForBundledProductsWithPartialRefund() + { + //Place order with bundled product + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $placeOrderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => 'bundle-product-two-dropdown-options', 'quantity' => 2] + ); + $orderNumber = $placeOrderResponse['placeOrder']['order']['order_number']; + $this->prepareInvoice($orderNumber, 2); + + $order = $this->order->loadByIncrementId($orderNumber); + /** @var Order\Item $orderItem */ + $orderItem = current($order->getAllItems()); + $orderItem->setQtyRefunded(1); + $order->addItem($orderItem); + $order->save(); + // Create a credit memo + $creditMemo = $this->creditMemoFactory->createByOrder($order, $order->getData()); + $creditMemo->setOrder($order); + $creditMemo->setState(1); + $creditMemo->setSubtotal(15); + $creditMemo->setBaseSubTotal(15); + $creditMemo->setShippingAmount(10); + $creditMemo->setBaseGrandTotal(23); + $creditMemo->setGrandTotal(23); + $creditMemo->setAdjustment(-2.00); + $creditMemo->addComment("Test comment for partial refund", false, true); + $creditMemo->save(); + + $this->creditMemoService->refund($creditMemo, true); + $response = $this->getCustomerOrderWithCreditMemoQuery(); + $expectedInvoicesData = [ + [ + 'items' => [ + [ + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15 + ], + 'discounts' => [], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1, 'currency' => 'USD'] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2, 'currency' => 'USD'] + ] + ] + ] + ], + 'quantity_invoiced' => 2 + ], + + ] + ] + ]; + $expectedCreditMemoData = [ + [ + 'comments' => [ + ['message' => 'Test comment for partial refund'] + ], + 'items' => [ + [ + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15 + ], + 'discounts' => [], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1, 'currency' => 'USD'] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2, 'currency' => 'USD'] + ] + ] + ] + ], + 'quantity_refunded' => 1 + ], + + ], + 'total' => [ + 'subtotal' => [ + 'value' => 15 + ], + 'grand_total' => [ + 'value' => 23, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 23, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 10 + ], + 'total_tax' => [ + 'value' => 0 + ], + 'shipping_handling' => [ + 'amount_including_tax' => [ + 'value' => 10 + ], + 'amount_excluding_tax' => [ + 'value' => 10 + ], + 'total_amount' => [ + 'value' => 10 + ], + 'taxes' => [], + 'discounts' => [], + ], + 'adjustment' => [ + 'value' => 2 + ] + ] + ] + ]; + $firstOrderItem = current($response['customer']['orders']['items'] ?? []); + + $this->assertArrayHasKey('invoices', $firstOrderItem); + $invoices = $firstOrderItem['invoices']; + $this->assertResponseFields($invoices, $expectedInvoicesData); + + $this->assertArrayHasKey('credit_memos', $firstOrderItem); + $creditMemos = $firstOrderItem['credit_memos']; + $this->assertResponseFields($creditMemos, $expectedCreditMemoData); + } + + /** + * Test customer order with credit memo details for bundle products with taxes and discounts + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testCreditMemoForBundleProductWithTaxesAndDiscounts() + { + //Place order with bundled product + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $placeOrderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => 'bundle-product-two-dropdown-options', 'quantity' => 2] + ); + $orderNumber = $placeOrderResponse['placeOrder']['order']['order_number']; + $this->prepareInvoice($orderNumber, 2); + $order = $this->order->loadByIncrementId($orderNumber); + /** @var Order\Item $orderItem */ + $orderItem = current($order->getAllItems()); + $orderItem->setQtyRefunded(1); + $order->addItem($orderItem); + $order->save(); + + $creditMemo = $this->creditMemoFactory->createByOrder($order, $order->getData()); + $creditMemo->setOrder($order); + $creditMemo->setState(1); + $creditMemo->setSubtotal(15); + $creditMemo->setBaseSubTotal(15); + $creditMemo->setShippingAmount(10); + $creditMemo->setTaxAmount(1.69); + $creditMemo->setBaseGrandTotal(24.19); + $creditMemo->setGrandTotal(24.19); + $creditMemo->setAdjustment(0.00); + $creditMemo->setDiscountAmount(-2.5); + $creditMemo->setDiscountDescription('Discount Label for 10% off'); + $creditMemo->addComment("Test comment for refund with taxes and discount", false, true); + $creditMemo->save(); + + $this->creditMemoService->refund($creditMemo, true); + $response = $this->getCustomerOrderWithCreditMemoQuery(); + $expectedCreditMemoData = [ + [ + 'comments' => [ + ['message' => 'Test comment for refund with taxes and discount'] + ], + 'items' => [ + [ + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15 + ], + 'discounts' => [ + [ + 'amount' => [ + 'value' => 3, + 'currency' => "USD" + ], + 'label' => 'Discount Label for 10% off' + ] + ], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1, 'currency' => 'USD'] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2, 'currency' => 'USD'] + ] + ] + ] + ], + 'quantity_refunded' => 1 + ], + + ], + 'total' => [ + 'subtotal' => [ + 'value' => 15 + ], + 'grand_total' => [ + 'value' => 24.19, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 24.19, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 10 + ], + 'total_tax' => [ + 'value'=> 1.69 + ], + 'shipping_handling' => [ + 'amount_including_tax' => [ + 'value' => 10.75 + ], + 'amount_excluding_tax' => [ + 'value' => 10 + ], + 'total_amount' => [ + 'value' => 10 + ], + 'taxes'=> [ + 0 => [ + 'amount' => ['value' => 0.67], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts' => [ + [ + 'amount'=> ['value'=> 1] + ] + ], + ], + 'adjustment' => [ + 'value' => 0 + ] + ] + ] + ]; + $firstOrderItem = current($response['customer']['orders']['items'] ?? []); + $this->assertArrayHasKey('credit_memos', $firstOrderItem); + + $creditMemos = $firstOrderItem['credit_memos']; + $this->assertResponseFields($creditMemos, $expectedCreditMemoData); + } + + /** + * Prepare invoice for the order + * + * @param string $orderNumber + * @param int|null $qty + */ + private function prepareInvoice(string $orderNumber, int $qty = null) + { + /** @var \Magento\Sales\Model\Order $order */ + $order = Bootstrap::getObjectManager() + ->create(\Magento\Sales\Model\Order::class)->loadByIncrementId($orderNumber); + $orderItem = current($order->getItems()); + $orderService = Bootstrap::getObjectManager()->create( + \Magento\Sales\Api\InvoiceManagementInterface::class + ); + $invoice = $orderService->prepareInvoice($order, [$orderItem->getId() => $qty]); + $invoice->register(); + $order = $invoice->getOrder(); + $order->setIsInProcess(true); + $transactionSave = Bootstrap::getObjectManager() + ->create(\Magento\Framework\DB\Transaction::class); + $transactionSave->addObject($invoice)->addObject($order)->save(); + } + + /** + * @return void + */ + private function deleteOrder(): void + { + /** @var \Magento\Framework\Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(OrderCollection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * @return void + */ + private function cleanUpCreditMemos(): void + { + /** @var \Magento\Framework\Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + $creditmemoRepository = Bootstrap::getObjectManager()->get(CreditmemoRepositoryInterface::class); + $creditmemoCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($creditmemoCollection as $creditmemo) { + $creditmemoRepository->delete($creditmemo); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } + + /** + * Get CustomerOrder with credit memo details + * + * @return array + * @throws AuthenticationException + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function getCustomerOrderWithCreditMemoQuery(): array + { + $query = + <<<QUERY +query { + customer { + orders { + items { + invoices { + items { + product_name + product_sku + product_sale_price { + value + } + ... on BundleInvoiceItem { + bundle_options { + label + values { + product_sku + product_name + quantity + price { + value + currency + } + } + } + } + discounts { amount{value currency} label } + quantity_invoiced + discounts { amount{value currency} label } + } + } + credit_memos { + comments { + message + } + items { + product_name + product_sku + product_sale_price { + value + } + ... on BundleCreditMemoItem { + bundle_options { + label + values { + product_sku + product_name + quantity + price { + value + currency + } + } + } + } + discounts { amount{value currency} label } + quantity_refunded + } + total { + subtotal { + value + } + base_grand_total { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + } + total_tax { + value + } + shipping_handling { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + taxes {amount{value} title rate} + discounts {amount{value}} + } + adjustment { + value + } + } + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrders/OrderShipmentsTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrders/OrderShipmentsTest.php new file mode 100644 index 0000000000000..c9f507b1f94e8 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/CustomerOrders/OrderShipmentsTest.php @@ -0,0 +1,328 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales\CustomerOrders; + +use Magento\Framework\DB\Transaction; +use Magento\Framework\Registry; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\GraphQl\Sales\Fixtures\CustomerPlaceOrder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Shipment; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\Sales\Model\ResourceModel\Order\Collection as OrderCollection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +class OrderShipmentsTest extends GraphQlAbstract +{ + /** + * @var GetCustomerAuthenticationHeader + */ + private $getCustomerAuthHeader; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + protected function setUp(): void + { + $this->getCustomerAuthHeader = Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = Bootstrap::getObjectManager()->get(OrderRepositoryInterface::class); + } + + protected function tearDown(): void + { + $this->cleanupOrders(); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php + */ + public function testGetOrderShipment() + { + $query = $this->getQuery('100000555'); + $authHeader = $this->getCustomerAuthHeader->execute('customer_uk_address@test.com', 'password'); + $orderModel = $this->fetchOrderModel('100000555'); + + $result = $this->graphQlQuery($query, [], '', $authHeader); + $this->assertArrayNotHasKey('errors', $result); + $this->assertCount(1, $result['customer']['orders']['items']); + + $order = $result['customer']['orders']['items'][0]; + $this->assertEquals('Flat Rate', $order['carrier']); + $this->assertEquals('Flat Rate - Fixed', $order['shipping_method']); + $this->assertArrayHasKey('shipments', $order); + /** @var Shipment $orderShipmentModel */ + $orderShipmentModel = $orderModel->getShipmentsCollection()->getFirstItem(); + $shipment = $order['shipments'][0]; + $this->assertEquals(base64_encode($orderShipmentModel->getIncrementId()), $shipment['id']); + $this->assertEquals($orderShipmentModel->getIncrementId(), $shipment['number']); + //Check Tracking + $this->assertCount(1, $shipment['tracking']); + $tracking = $shipment['tracking'][0]; + $this->assertEquals('ups', $tracking['carrier']); + $this->assertEquals('United Parcel Service', $tracking['title']); + $this->assertEquals('1234567890', $tracking['number']); + //Check Items + $this->assertCount(2, $shipment['items']); + foreach ($orderShipmentModel->getItems() as $expectedItem) { + $sku = $expectedItem->getSku(); + $findItem = array_filter($shipment['items'], function ($item) use ($sku) { + return $item['product_sku'] === $sku; + }); + $this->assertCount(1, $findItem); + $actualItem = reset($findItem); + $expectedEncodedId = base64_encode($expectedItem->getEntityId()); + $this->assertEquals($expectedEncodedId, $actualItem['id']); + $this->assertEquals($expectedItem->getSku(), $actualItem['product_sku']); + $this->assertEquals($expectedItem->getName(), $actualItem['product_name']); + $this->assertEquals($expectedItem->getPrice(), $actualItem['product_sale_price']['value']); + $this->assertEquals('USD', $actualItem['product_sale_price']['currency']); + $this->assertEquals('1', $actualItem['quantity_shipped']); + //Check correct order_item + $this->assertNotEmpty($actualItem['order_item']); + $this->assertEquals($expectedItem->getSku(), $actualItem['order_item']['product_sku']); + } + //Check comments + $this->assertCount(1, $shipment['comments']); + $this->assertEquals('This comment is visible to the customer', $shipment['comments'][0]['message']); + $this->assertNotEmpty($shipment['comments'][0]['timestamp']); + } + + /** + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php + */ + public function testGetOrderShipmentsMultiple() + { + $query = $this->getQuery('100000555'); + $authHeader = $this->getCustomerAuthHeader->execute('customer_uk_address@test.com', 'password'); + + $result = $this->graphQlQuery($query, [], '', $authHeader); + $this->assertArrayNotHasKey('errors', $result); + $order = $result['customer']['orders']['items'][0]; + $shipments = $order['shipments']; + $this->assertCount(2, $shipments); + $this->assertEquals('0000000098', $shipments[0]['number']); + $this->assertCount(1, $shipments[0]['items']); + $this->assertEquals('0000000099', $shipments[1]['number']); + $this->assertCount(1, $shipments[1]['items']); + } + + /** + * @magentoConfigFixture default_store carriers/ups/active 1 + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php + */ + public function testOrderShipmentWithUpsCarrier() + { + $query = $this->getQuery('100000001'); + $authHeader = $this->getCustomerAuthHeader->execute('customer@example.com', 'password'); + + $result = $this->graphQlQuery($query, [], '', $authHeader); + + $this->assertArrayNotHasKey('errors', $result); + $this->assertEquals('UPS Next Day Air', $result['customer']['orders']['items'][0]['shipping_method']); + $this->assertEquals('United Parcel Service', $result['customer']['orders']['items'][0]['carrier']); + $shipments = $result['customer']['orders']['items'][0]['shipments']; + $expectedTracking = [ + 'title' => 'United Parcel Service', + 'carrier' => 'ups', + 'number' => '987654321' + ]; + $this->assertEquals($expectedTracking, $shipments[0]['tracking'][0]); + } + + /** + * @magentoConfigFixture default_store carriers/ups/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + */ + public function testOrderShipmentWithBundleProduct() + { + //Place order with bundled product + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $placeOrderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => 'bundle-product-two-dropdown-options'] + ); + $orderNumber = $placeOrderResponse['placeOrder']['order']['order_number']; + $this->shipOrder($orderNumber); + + $result = $this->graphQlQuery( + $this->getQuery(), + [], + '', + $this->getCustomerAuthHeader->execute('customer@example.com', 'password') + ); + $this->assertArrayNotHasKey('errors', $result); + + $shipments = $result['customer']['orders']['items'][0]['shipments']; + $shipmentBundleItem = $shipments[0]['items'][0]; + + $shipmentItemAssertionMap = [ + 'order_item' => [ + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2' + ], + 'product_name' => 'Bundle Product With Two dropdown options', + 'product_sku' => 'bundle-product-two-dropdown-options-simple1-simple2', + 'product_sale_price' => [ + 'value' => 15, + 'currency' => 'USD' + ], + 'bundle_options' => [ + [ + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_name' => 'Simple Product1', + 'product_sku' => 'simple1', + 'quantity' => 1, + 'price' => ['value' => 1] + ] + ] + ], + [ + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_name' => 'Simple Product2', + 'product_sku' => 'simple2', + 'quantity' => 2, + 'price' => ['value' => 2] + ] + ] + ] + ] + ]; + + $this->assertResponseFields($shipmentBundleItem, $shipmentItemAssertionMap); + } + + /** + * Get query that fetch orders and shipment information + * + * @param string|null $orderId + * @return string + */ + private function getQuery(string $orderId = null) + { + $filter = $orderId ? "(filter:{number:{eq:\"$orderId\"}})" : ""; + return <<<QUERY +{ + customer { + orders {$filter}{ + items { + number + status + items { + product_sku + } + carrier + shipping_method + shipments { + id + number + tracking { + title + carrier + number + } + items { + id + order_item { + product_sku + } + product_name + product_sku + product_sale_price { + value + currency + } + ... on BundleShipmentItem { + bundle_options { + label + values { + product_name + product_sku + quantity + price { + value + } + } + } + } + quantity_shipped + } + comments { + timestamp + message + } + } + } + } + } +} +QUERY; + } + + /** + * Get model instance for order by number + * + * @param string $orderNumber + * @return Order + */ + private function fetchOrderModel(string $orderNumber): Order + { + /** @var Order $order */ + $order = Bootstrap::getObjectManager()->get(Order::class); + $order->loadByIncrementId($orderNumber); + return $order; + } + + /** + * Create shipment for order + * + * @param string $orderNumber + */ + private function shipOrder(string $orderNumber): void + { + $order = $this->fetchOrderModel($orderNumber); + $order->setIsInProcess(true); + /** @var Transaction $transaction */ + $transaction = Bootstrap::getObjectManager()->create(Transaction::class); + + $items = []; + foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); + } + + $shipment = Bootstrap::getObjectManager()->get(ShipmentFactory::class)->create($order, $items); + $shipment->register(); + $transaction->addObject($shipment)->addObject($order)->save(); + } + + /** + * Clean up orders + */ + private function cleanupOrders() + { + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(OrderCollection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php new file mode 100644 index 0000000000000..0386d414b8682 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/Fixtures/CustomerPlaceOrder.php @@ -0,0 +1,367 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales\Fixtures; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\TestCase\GraphQl\Client; + +class CustomerPlaceOrder +{ + /** + * @var Client + */ + private $gqlClient; + + /** + * @var CustomerTokenServiceInterface + */ + private $tokenService; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var string + */ + private $authHeader; + + /** + * @var string + */ + private $cartId; + + /** + * @var array + */ + private $customerLogin; + + /** + * @param Client $gqlClient + * @param CustomerTokenServiceInterface $tokenService + * @param ProductRepositoryInterface $productRepository + */ + public function __construct( + Client $gqlClient, + CustomerTokenServiceInterface $tokenService, + ProductRepositoryInterface $productRepository + ) { + $this->gqlClient = $gqlClient; + $this->tokenService = $tokenService; + $this->productRepository = $productRepository; + } + + /** + * Place order for a bundled product + * + * @param array $customerLogin + * @param array $productData + * @return array + */ + public function placeOrderWithBundleProduct(array $customerLogin, array $productData): array + { + $this->customerLogin = $customerLogin; + $this->createCustomerCart(); + $this->addBundleProduct($productData); + $this->setBillingAddress(); + $shippingMethod = $this->setShippingAddress(); + $paymentMethod = $this->setShippingMethod($shippingMethod); + $this->setPaymentMethod($paymentMethod); + return $this->doPlaceOrder(); + } + + /** + * Make GraphQl POST request + * + * @param string $query + * @param array $additionalHeaders + * @return array + */ + private function makeRequest(string $query, array $additionalHeaders = []): array + { + $headers = array_merge([$this->getAuthHeader()], $additionalHeaders); + return $this->gqlClient->post($query, [], '', $headers); + } + + /** + * Get header for authenticated requests + * + * @return string + * @throws \Magento\Framework\Exception\AuthenticationException + */ + private function getAuthHeader(): string + { + if (empty($this->authHeader)) { + $customerToken = $this->tokenService + ->createCustomerAccessToken($this->customerLogin['email'], $this->customerLogin['password']); + $this->authHeader = "Authorization: Bearer {$customerToken}"; + } + return $this->authHeader; + } + + /** + * Get cart id + * + * @return string + */ + private function getCartId(): string + { + if (empty($this->cartId)) { + $this->cartId = $this->createCustomerCart(); + } + return $this->cartId; + } + + /** + * Create empty cart for the customer + * + * @return array + */ + private function createCustomerCart(): string + { + //Create empty cart + $createEmptyCart = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $result = $this->makeRequest($createEmptyCart); + return $result['createEmptyCart']; + } + + /** + * Add a bundle product to the cart + * + * @param array $productData + * @return array + * @throws \Magento\Framework\Exception\NoSuchEntityException + */ + private function addBundleProduct(array $productData) + { + $productSku = $productData['sku']; + $qty = $productData['quantity'] ?? 1; + /** @var Product $bundleProduct */ + $bundleProduct = $this->productRepository->get($productSku); + /** @var \Magento\Bundle\Model\Product\Type $typeInstance */ + $typeInstance = $bundleProduct->getTypeInstance(); + $optionId1 = (int)$typeInstance->getOptionsCollection($bundleProduct)->getFirstItem()->getId(); + $optionId2 = (int)$typeInstance->getOptionsCollection($bundleProduct)->getLastItem()->getId(); + $selectionId1 = (int)$typeInstance->getSelectionsCollection([$optionId1], $bundleProduct) + ->getFirstItem() + ->getSelectionId(); + $selectionId2 = (int)$typeInstance->getSelectionsCollection([$optionId2], $bundleProduct) + ->getLastItem() + ->getSelectionId(); + + $addProduct = <<<QUERY +mutation { + addBundleProductsToCart(input:{ + cart_id:"{$this->getCartId()}" + cart_items:[ + { + data:{ + sku:"{$productSku}" + quantity:{$qty} + } + bundle_options:[ + { + id:{$optionId1} + quantity:1 + value:["{$selectionId1}"] + } + { + id:$optionId2 + quantity:2 + value:["{$selectionId2}"] + } + ] + } + ] + }) { + cart { + items {quantity product {sku}} + } + } +} +QUERY; + return $this->makeRequest($addProduct); + } + + /** + * Set the billing address on the cart + * + * @return array + */ + private function setBillingAddress(): array + { + $setBillingAddress = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$this->getCartId()}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + return $this->makeRequest($setBillingAddress); + } + + /** + * Set the shipping address on the cart and return an available shipping method + * + * @return array + */ + private function setShippingAddress(): array + { + $setShippingAddress = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "{$this->getCartId()}" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $result = $this->makeRequest($setShippingAddress); + $shippingMethod = $result['setShippingAddressesOnCart'] + ['cart']['shipping_addresses'][0]['available_shipping_methods'][0]; + return $shippingMethod; + } + + /** + * Set the shipping method on the cart and return an available payment method + * + * @param array $shippingMethod + * @return array + */ + private function setShippingMethod(array $shippingMethod): array + { + $setShippingMethod = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$this->getCartId()}", + shipping_methods: [ + { + carrier_code: "{$shippingMethod['carrier_code']}" + method_code: "{$shippingMethod['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $result = $this->makeRequest($setShippingMethod); + $paymentMethod = $result['setShippingMethodsOnCart']['cart']['available_payment_methods'][0]; + return $paymentMethod; + } + + /** + * Set the payment method on the cart + * + * @param array $paymentMethod + * @return array + */ + private function setPaymentMethod(array $paymentMethod): array + { + $setPaymentMethod = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$this->getCartId()}" + payment_method: { + code: "{$paymentMethod['code']}" + } + } + ) { + cart { + selected_payment_method { + code + } + } + } +} +QUERY; + return $this->makeRequest($setPaymentMethod); + } + + /** + * Place the order + * + * @return array + */ + private function doPlaceOrder(): array + { + $placeOrder = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$this->getCartId()}" + } + ) { + order { + order_number + } + } +} +QUERY; + return $this->makeRequest($placeOrder); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php new file mode 100644 index 0000000000000..8b18d4bd07d1b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/InvoiceTest.php @@ -0,0 +1,880 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Framework\Registry; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\GraphQl\GetCustomerAuthenticationHeader; + +/** + * Tests the Invoice query + */ +class InvoiceTest extends GraphQlAbstract +{ + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var OrderRepositoryInterface */ + private $orderRepository; + + protected function setUp(): void + { + $this->customerAuthenticationHeader + = Bootstrap::getObjectManager()->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = Bootstrap::getObjectManager()->get(OrderRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testSingleInvoiceForLoggedInCustomerQuery() + { + $response = $this->getCustomerInvoicesBasedOnOrderNumber('100000001'); + $expectedOrdersData = [ + 'status' => 'Processing', + 'grand_total' => 100.00 + ]; + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1, + 'discounts' => [] + ], + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1, + 'discounts' => [] + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'taxes' => [], + 'discounts' => [] + ], + 'taxes' => [], + 'discounts' => [], + 'base_grand_total' => [ + 'value' => 100, + 'currency' => 'EUR' + ], + 'total_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + ] + ] + ]; + $this->assertOrdersData($response, $expectedOrdersData); + $invoices = $response[0]['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testMultipleInvoiceForLoggedInCustomerQuery() + { + $response = $this->getCustomerInvoicesBasedOnOrderNumber('100000002'); + $expectedOrdersData = [ + 'status' => 'Processing', + 'grand_total' => 60.00 + ]; + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Related Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 3, + 'discounts'=> [] + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 30, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 50, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 50, + 'currency' => 'EUR' + ], + 'total_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 25, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 20, + 'currency' => 'USD' + ], + 'taxes' => [], + 'discounts' => [], + ], + 'taxes' => [], + 'discounts' => [], + ] + ], + [ + 'items' => [ + [ + 'product_name' => 'Simple Product With Related Product', + 'product_sku' => 'simple_with_cross', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1, + 'discounts' => [] + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'base_grand_total' => [ + 'value' => 0, + 'currency' => 'EUR' + ], + 'total_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'shipping_handling' => [ + 'total_amount' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_including_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'amount_excluding_tax' => [ + 'value' => 0, + 'currency' => 'USD' + ], + 'taxes' => [], + 'discounts' => [], + ], + 'taxes' => [], + 'discounts' => [], + ] + ] + ]; + $this->assertOrdersData($response, $expectedOrdersData); + $invoices = $response[0]['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Sales/_files/customers_with_invoices.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testMultipleCustomersWithInvoicesQuery() + { + $query = + <<<QUERY +{ + customer + { + orders { + items { + status + total { + grand_total { + value + currency + } + } + invoices { + items{ + product_name + product_sku + product_sale_price { + value + currency + } + quantity_invoiced + } + total { + subtotal { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + currency + } + } + } +} +} +} +} +QUERY; + + $currentEmail = 'customer@search.example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $expectedOrdersData = [ + 'status' => 'Processing', + 'grand_total' => 100.00 + ]; + + $expectedInvoiceData = [ + [ + 'items' => [ + [ + 'product_name' => 'Simple Product', + 'product_sku' => 'simple', + 'product_sale_price' => [ + 'value' => 10, + 'currency' => 'USD' + ], + 'quantity_invoiced' => 1 + ] + ], + 'total' => [ + 'subtotal' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'grand_total' => [ + 'value' => 100, + 'currency' => 'USD' + ], + 'total_shipping' => [ + 'value' => 0, + 'currency' => 'USD' + ] + ] + ] + ]; + $this->assertOrdersData($response['customer']['orders']['items'], $expectedOrdersData); + $invoices = $response['customer']['orders']['items'][0]['invoices']; + $this->assertResponseFields($invoices, $expectedInvoiceData); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + */ + public function testInvoiceForCustomerWithTaxesAndDiscounts() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderNumber = $this->placeOrder($cartId); + $this->prepareInvoice($orderNumber, 2); + $customerOrderResponse = $this->getCustomerInvoicesBasedOnOrderNumber($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $invoice = $customerOrderItem['invoices'][0]; + $this->assertEquals(3, $invoice['total']['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $invoice['total']['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $invoice['total']['discounts'][0]['label'] + ); + $this->assertTotalsAndShippingWithTaxesAndDiscounts($customerOrderItem['invoices'][0]['total']); + $this->deleteOrder(); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + */ + public function testPartialInvoiceForCustomerWithTaxesAndDiscounts() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderNumber = $this->placeOrder($cartId); + $this->prepareInvoice($orderNumber, 1); + $customerOrderResponse = $this->getCustomerInvoicesBasedOnOrderNumber($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $invoice = $customerOrderItem['invoices'][0]; + $invoiceItem = $invoice['items'][0]; + $this->assertEquals(1, $invoiceItem['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $invoiceItem['discounts'][0]['amount']['currency']); + $this->assertEquals('Discount Label for 10% off', $invoiceItem['discounts'][0]['label']); + $this->assertEquals(2, $invoice['total']['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $invoice['total']['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $invoice['total']['discounts'][0]['label'] + ); + $this->assertTotalsAndShippingWithTaxesAndDiscountsForOneQty($customerOrderItem['invoices'][0]['total']); + $this->deleteOrder(); + } + + /** + * Prepare invoice for the order + * + * @param string $orderNumber + * @param int|null $qty + */ + private function prepareInvoice(string $orderNumber, int $qty = null) + { + /** @var \Magento\Sales\Model\Order $order */ + $order = Bootstrap::getObjectManager() + ->create(\Magento\Sales\Model\Order::class)->loadByIncrementId($orderNumber); + $orderItem = current($order->getItems()); + $orderService = Bootstrap::getObjectManager()->create( + \Magento\Sales\Api\InvoiceManagementInterface::class + ); + $invoice = $orderService->prepareInvoice($order, [$orderItem->getId() => $qty]); + $invoice->register(); + $order = $invoice->getOrder(); + $order->setIsInProcess(true); + $transactionSave = Bootstrap::getObjectManager() + ->create(\Magento\Framework\DB\Transaction::class); + $transactionSave->addObject($invoice)->addObject($order)->save(); + } + + /** + * Check order totals an shipping amounts with taxes + * + * @param array $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.03, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 29.1, 'currency' =>'USD'], + 'grand_total' => ['value' => 29.1, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.03, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75, 'currency' =>'USD'], + 'amount_excluding_tax' => ['value' => 10, 'currency' =>'USD'], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.68], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts'=> [ + 0 => ['amount'=>['value' => 1, 'currency'=> 'USD']] + ], + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Check order totals an shipping amounts with taxes + * + * @param array $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithTaxesAndDiscountsForOneQty(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(1.36, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 19.43, 'currency' =>'USD'], + 'grand_total' => ['value' => 19.43, 'currency' =>'USD'], + 'total_tax' => ['value' => 1.36, 'currency' =>'USD'], + 'subtotal' => ['value' => 10, 'currency' =>'USD'], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75, 'currency' =>'USD'], + 'amount_excluding_tax' => ['value' => 10, 'currency' =>'USD'], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.68], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts'=> [['amount'=>['value' => 1, 'currency'=> 'USD']] + ], + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Create an empty cart with GraphQl mutation + * + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['createEmptyCart']; + } + + /** + * Add product to cart with GraphQl query + * + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cart_items: [ + { + data: { + quantity: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart {items{quantity product {sku}}}} +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set billing address on cart with GraphQL mutation + * + * @param string $cartId + * @return void + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set shipping address on cart with GraphQl query + * + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + return $availableShippingMethod; + } + + /** + * Set shipping method on cart with GraphQl mutation + * + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + + /** + * Set payment method on cart with GrpahQl mutation + * + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart {selected_payment_method {code}} + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Place order using GraphQl mutation + * + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_number + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['placeOrder']['order']['order_number']; + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerInvoicesBasedOnOrderNumber($orderNumber): array + { + $query = + <<<QUERY +{ + customer { + email + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + status + total { + grand_total{value currency} + } + invoices { + items{ + product_name product_sku product_sale_price{value currency}quantity_invoiced + discounts {amount{value currency} label} + } + total { + base_grand_total{value currency} + grand_total{value currency} + total_tax{value currency} + subtotal { value currency } + taxes {amount{value currency} title rate} + discounts {amount{value currency} label} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value currency} + amount_excluding_tax{value currency} + total_amount{value currency} + taxes {amount{value} title rate} + discounts {amount{value currency}} + } + } + } + } + } + } + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + return $response['customer']['orders']['items']; + } + + private function assertOrdersData($response, $expectedOrdersData): void + { + $actualData = $response[0]; + $this->assertEquals( + $expectedOrdersData['grand_total'], + $actualData['total']['grand_total']['value'], + "grand_total is different than the expected for order" + ); + $this->assertEquals( + $expectedOrdersData['status'], + $actualData['status'], + "status is different than the expected for order" + ); + } + + /** + * Clean up orders + * + * @return void + */ + private function deleteOrder(): void + { + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php index 7bece410a06f8..0baee2797bf5d 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/ReorderTest.php @@ -108,7 +108,7 @@ public function testSimpleProductOutOfStock() /** @var \Magento\Catalog\Api\ProductRepositoryInterface $repository */ $productRepository = Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $productSku = 'simple'; + $productSku = 'simple-2'; /** @var \Magento\Catalog\Api\Data\ProductInterface $product */ $product = $productRepository->get($productSku); diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php new file mode 100644 index 0000000000000..299bccc5a1277 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersByOrderNumberTest.php @@ -0,0 +1,1394 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\Registry; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Class RetrieveOrdersTest + */ +class RetrieveOrdersByOrderNumberTest extends GraphQlAbstract +{ + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + protected function setUp():void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerOrdersSimpleProductQuery() + { + $orderNumber = '100000002'; + $response = $this->getCustomerOrderQueryOnSimpleProducts($orderNumber); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertNotEmpty($response['customer']['orders']['items']); + $customerOrderItemsInResponse = $response['customer']['orders']['items'][0]; + $this->assertArrayHasKey('items', $customerOrderItemsInResponse); + $this->assertNotEmpty($customerOrderItemsInResponse['items']); + $this->assertNotEmpty($response["customer"]["orders"]["items"][0]["billing_address"]); + $this->assertNotEmpty($response["customer"]["orders"]["items"][0]["shipping_address"]); + $this->assertNotEmpty($response["customer"]["orders"]["items"][0]["payment_methods"]); + + $searchCriteria = $this->searchCriteriaBuilder->addFilter('increment_id', '100000002') + ->create(); + /** @var \Magento\Sales\Api\Data\OrderInterface[] $orders */ + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + foreach ($orders as $order) { + $orderNumber = $order->getIncrementId(); + $this->assertNotEmpty($customerOrderItemsInResponse['id']); + $this->assertEquals($orderNumber, $customerOrderItemsInResponse['number']); + $this->assertEquals('Processing', $customerOrderItemsInResponse['status']); + } + $expectedOrderItems = [ + 'quantity_ordered'=> 2, + 'product_sku'=> 'simple', + 'product_name'=> 'Simple Product', + 'product_sale_price'=> ['currency'=> 'USD', 'value'=> 10] + ]; + $actualOrderItemsFromResponse = $customerOrderItemsInResponse['items'][0]; + $this->assertEquals($expectedOrderItems, $actualOrderItemsFromResponse); + $actualOrderTotalFromResponse = $response['customer']['orders']['items'][0]['total']; + $expectedOrderTotal = [ + 'base_grand_total' => ['value'=> 120,'currency' =>'USD'], + 'grand_total' => ['value'=> 120,'currency' =>'USD'], + 'subtotal' => ['value'=> 120,'currency' =>'USD'] + ]; + $this->assertEquals($expectedOrderTotal, $actualOrderTotalFromResponse, 'Totals do not match'); + } + + /** + * Verify the customer order with tax, discount with shipping tax class set for calculation setting + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrdersSimpleProductWithTaxesAndDiscounts() + { + $quantity = 4; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $billingAssertionMap = [ + 'firstname' => 'John', + 'lastname' => 'Smith', + 'city' => 'Texas City', + 'company' => 'Test company', + 'country_code' => 'US', + 'postcode' => '78717', + 'region' => 'Texas', + 'region_id' => '57', + 'street' => [ + 0 => 'test street 1', + 1 => 'test street 2', + ], + 'telephone' => '5123456677' + ]; + $this->assertResponseFields($customerOrderResponse[0]["billing_address"], $billingAssertionMap); + $shippingAssertionMap = [ + 'firstname' => 'test shipFirst', + 'lastname' => 'test shipLast', + 'city' => 'Montgomery', + 'company' => 'test company', + 'country_code' => 'US', + 'postcode' => '36013', + 'street' => [ + 0 => 'test street 1', + 1 => 'test street 2', + ], + 'region_id' => '1', + 'region' => 'Alabama', + 'telephone' => '3347665522' + ]; + $this->assertResponseFields($customerOrderResponse[0]["shipping_address"], $shippingAssertionMap); + $paymentMethodAssertionMap = [ + [ + 'name' => 'Check / Money order', + 'type' => 'checkmo', + 'additional_data' => [] + ] + ]; + $this->assertResponseFields($customerOrderResponse[0]["payment_methods"], $paymentMethodAssertionMap); + // Asserting discounts on order item level + $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsWithTaxesAndDiscounts($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 58.05, 'currency' =>'USD'], + 'grand_total' => ['value' => 58.05, 'currency' =>'USD'], + 'subtotal' => ['value' => 40, 'currency' =>'USD'], + 'total_tax' => ['value' => 4.05, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 21.5], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2, 'currency' =>'USD']] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Verify the customer order with tax, discount with shipping tax class set for calculation setting + * + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrdersSimpleProductWithTaxesAndDiscountsWithTwoRules() + { + $quantity = 4; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + // Asserting discounts on order item level + $this->assertEquals(4, $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['value']); + $this->assertEquals('USD', $customerOrderResponse[0]['items'][0]['discounts'][0]['amount']['currency']); + $this->assertEquals( + 'Discount Label for 10% off', + $customerOrderResponse[0]['items'][0]['discounts'][0]['label'] + ); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsWithTaxesAndDiscountsWithTwoRules($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsWithTaxesAndDiscountsWithTwoRules(array $customerOrderItemTotal): void + { + $this->assertCount(2, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(4.05, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + $secondTaxData = $customerOrderItemTotal['taxes'][1]; + $this->assertEquals('USD', $secondTaxData['amount']['currency']); + $this->assertEquals(2.97, $secondTaxData['amount']['value']); + $this->assertEquals('US-AL-*-Rate-1', $secondTaxData['title']); + $this->assertEquals(5.5, $secondTaxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'grand_total' => ['value' => 61.02, 'currency' =>'USD'], + 'subtotal' => ['value' => 40, 'currency' =>'USD'], + 'total_tax' => ['value' => 7.02, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 22.6], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2, 'currency' =>'USD']] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ], + 1 => [ + 'amount'=>['value' => 0.99], + 'title' => 'US-AL-*-Rate-1', + 'rate' => 5.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 6, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetMatchingCustomerOrders() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{match:"100"}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + product_type + product_sale_price{currency value} + product_url_key + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(6, $response['customer']['orders']['total_count']); + $this->assertCount(6, $response['customer']['orders']['items']); + $customerOrderItems = $response['customer']['orders']['items']; + $expectedOrderNumbers = ['100000002', '100000004', '100000005','100000006', '100000007', '100000008']; + $actualOrdersFromResponse = []; + foreach ($customerOrderItems as $order) { + array_push($actualOrdersFromResponse, $order['number']); + } + $this->assertEquals($expectedOrderNumbers, $actualOrdersFromResponse, 'Order numbers do not match'); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetMatchingOrdersForLowerQueryLength() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{match:"0"}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + } + } + } +} +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + //character length should not trigger an exception + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(6, $response['customer']['orders']['total_count']); + $this->assertCount($response['customer']['orders']['total_count'], $response['customer']['orders']['items']); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testGetMultipleCustomerOrdersQueryWithDefaultPagination() + { + $orderNumbers = ['100000007', '100000008']; + $query = <<<QUERY +{ + customer + { + orders(filter:{number:{in:["{$orderNumbers[0]}","{$orderNumbers[1]}"]}}){ + total_count + page_info{ + total_pages + current_page + page_size + } + items + { + id + number + status + order_date + items{ + quantity_ordered + product_sku + product_name + product_type + product_sale_price{currency value} + } + total{ + base_grand_total {value currency} + grand_total {value currency} + subtotal {value currency} + total_shipping{value} + total_tax{value currency} + taxes {amount {currency value} title rate} + total_shipping{value} + shipping_handling{ + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + taxes {amount{value} title rate} + } + } + } + } + } +} +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(2, $response['customer']['orders']['total_count']); + $this->assertArrayHasKey('page_info', $response['customer']['orders']); + $pageInfo = $response['customer']['orders']['page_info']; + $this->assertEquals(1, $pageInfo['current_page']); + $this->assertEquals(20, $pageInfo['page_size']); + $this->assertEquals(1, $pageInfo['total_pages']); + $this->assertNotEmpty($response['customer']['orders']['items']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + $this->assertCount(2, $response['customer']['orders']['items']); + + $searchCriteria = $this->searchCriteriaBuilder + ->addFilter('increment_id', $orderNumbers, 'in') + ->create(); + $orders = $this->orderRepository->getList($searchCriteria)->getItems(); + $key = 0; + foreach ($orders as $order) { + $orderId = base64_encode($order->getEntityId()); + $orderNumber = $order->getIncrementId(); + $orderItemInResponse = $customerOrderItemsInResponse[$key]; + $this->assertNotEmpty($orderItemInResponse['id']); + $this->assertEquals($orderId, $orderItemInResponse['id']); + $this->assertEquals($orderNumber, $orderItemInResponse['number']); + $this->assertEquals('Processing', $orderItemInResponse['status']); + $this->assertEquals(5, $orderItemInResponse['total']['shipping_handling']['total_amount']['value']); + $this->assertEquals(5, $orderItemInResponse['total']['total_shipping']['value']); + $this->assertEquals(5, $orderItemInResponse['total']['total_tax']['value']); + $key++; + } + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerOrdersUnauthorizedCustomer() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"100000001"}}){ + total_count + items + { + id + number + status + order_date + } + } + } +} +QUERY; + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The current customer isn\'t authorized.'); + $this->graphQlQuery($query); + } + + /** + * @magentoApiDataFixture Magento/Customer/_files/two_customers.php + * @magentoApiDataFixture Magento/Sales/_files/two_orders_for_two_diff_customers.php + */ + public function testGetCustomerOrdersWithWrongCustomer() + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"100000001"}}){ + total_count + items + { + id + number + status + order_date + } + } + } +} +QUERY; + $currentEmail = 'customer_two@example.com'; + $currentPassword = 'password'; + $responseWithWrongCustomer = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertEquals(0, $responseWithWrongCustomer['customer']['orders']['total_count']); + $this->assertEmpty($responseWithWrongCustomer['customer']['orders']['items']); + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $responseWithCorrectCustomer = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertEquals(1, $responseWithCorrectCustomer['customer']['orders']['total_count']); + $this->assertNotEmpty($responseWithCorrectCustomer['customer']['orders']['items']); + } + + /** + * @param String $orderNumber + * @throws AuthenticationException + * @dataProvider dataProviderIncorrectOrder + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/orders_with_customer.php + */ + public function testGetCustomerNonExistingOrderQuery(string $orderNumber) + { + $query = + <<<QUERY +{ + customer { + orders(filter: {number: {eq: "{$orderNumber}"}}) { + items { + number + items { + product_sku + } + total { + base_grand_total { + value + currency + } + grand_total { + value + currency + } + total_shipping { + value + } + shipping_handling { + amount_including_tax { + value + } + amount_excluding_tax { + value + } + total_amount { + value + } + taxes { + amount { + value + } + title + rate + } + } + subtotal { + value + currency + } + taxes { + amount { + value + currency + } + title + rate + } + discounts { + amount { + value + currency + } + label + } + } + } + page_info { + current_page + page_size + total_pages + } + total_count + } + } +} + +QUERY; + + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $this->assertArrayNotHasKey('errors', $response); + $this->assertArrayHasKey('customer', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertCount(0, $response['customer']['orders']['items']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals(0, $response['customer']['orders']['total_count']); + $this->assertArrayHasKey('page_info', $response['customer']['orders']); + $this->assertEquals( + ['current_page' => 1, 'page_size' => 20, 'total_pages' => 0], + $response['customer']['orders']['page_info'] + ); + } + + /** + * @return array + */ + public function dataProviderIncorrectOrder(): array + { + return [ + 'correctFormatNonExistingOrder' => [ + '200000009', + ], + 'alphaFormatNonExistingOrder' => [ + '200AA00B9', + ], + 'longerFormatNonExistingOrder' => [ + 'X0000-0033331', + ], + ]; + } + + /** + * @param String $orderNumber + * @param String $store + * @param int $expectedCount + * @throws AuthenticationException + * @dataProvider dataProviderMultiStores + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php + */ + public function testGetCustomerOrdersTwoStoreViewQuery(string $orderNumber, string $store, int $expectedCount) + { + $query = + <<<QUERY +{ + customer { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + page_info {current_page page_size total_pages} + total_count + items { + number + items{ product_sku } + total { + base_grand_total{value currency} + grand_total{value currency} + subtotal { value currency } + shipping_handling + { + total_amount{value currency} + } + } + } + } + } + } +QUERY; + + $headers = array_merge( + $this->customerAuthenticationHeader->execute('customer@example.com', 'password'), + ['Store' => $store] + ); + $response = $this->graphQlQuery($query, [], '', $headers); + $this->assertArrayHasKey('customer', $response); + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $this->assertCount($expectedCount, $response['customer']['orders']['items']); + $this->assertArrayHasKey('total_count', $response['customer']['orders']); + $this->assertEquals($expectedCount, (int)$response['customer']['orders']['total_count']); + $this->assertTotals($response, $expectedCount); + } + + /** + * @param array $response + * @param int $expectedCount + */ + private function assertTotals(array $response, int $expectedCount): void + { + $assertionMap = [ + 'base_grand_total' => ['value' => 100, 'currency' =>'USD'], + 'grand_total' => ['value' => 100, 'currency' =>'USD'], + 'subtotal' => ['value' => 110, 'currency' =>'USD'], + 'shipping_handling' => [ + 'total_amount' => ['value' => 10, 'currency' =>'USD'] + ] + ]; + if ($expectedCount === 0) { + $this->assertEmpty($response['customer']['orders']['items']); + } else { + $customerOrderItemTotal = $response['customer']['orders']['items'][0]['total']; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + } + + /** + * @return array + */ + public function dataProviderMultiStores(): array + { + return [ + 'firstStoreFirstOrder' => [ + '100000001', 'default', 1 + ], + 'secondStoreSecondOrder' => [ + '100000002', 'fixture_second_store', 1 + ], + 'firstStoreSecondOrder' => [ + '100000002', 'default', 0 + ], + 'secondStoreFirstOrder' => [ + '100000001', 'fixture_second_store', 0 + ], + ]; + } + + /** + * Verify that the customer order has the tax information on shipping and totals + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testCustomerOrderWithTaxesExcludedOnShipping() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsAndShippingWithExcludedTaxSetting($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * Assert totals and shipping amounts with taxes excluded + * + * @param $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithExcludedTaxSetting($customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.25, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'discounts' => [], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75], + 'amount_excluding_tax' => ['value' => 10], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.75], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ], + 'discounts' =>[] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Verify that the customer order has the tax information on shipping and totals + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Catalog/_files/product_simple_with_url_key.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php + */ + public function testCustomerOrderWithTaxesIncludedOnShippingAndTotals() + { + $quantity = 2; + $sku = 'simple1'; + $cartId = $this->createEmptyCart(); + $this->addProductToCart($cartId, $quantity, $sku); + + $this->setBillingAddress($cartId); + $shippingMethod = $this->setShippingAddress($cartId); + + $paymentMethod = $this->setShippingMethod($cartId, $shippingMethod); + $this->setPaymentMethod($cartId, $paymentMethod); + + $orderNumber = $this->placeOrder($cartId); + $customerOrderResponse = $this->getCustomerOrderQuery($orderNumber); + $customerOrderItem = $customerOrderResponse[0]; + $this->assertTotalsAndShippingWithTaxes($customerOrderItem['total']); + $this->deleteOrder(); + } + + /** + * Check order totals an shipping amounts with taxes + * + * @param array $customerOrderItemTotal + */ + private function assertTotalsAndShippingWithTaxes(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(2.25, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + unset($customerOrderItemTotal['shipping_handling']['discounts']); + $assertionMap = [ + 'base_grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'grand_total' => ['value' => 32.25, 'currency' =>'USD'], + 'total_tax' => ['value' => 2.25, 'currency' =>'USD'], + 'subtotal' => ['value' => 20, 'currency' =>'USD'], + 'total_shipping' => ['value' => 10, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 10.75], + 'amount_excluding_tax' => ['value' => 10], + 'total_amount' => ['value' => 10, 'currency' =>'USD'], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 0.75], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Create an empty cart with GraphQl mutation + * + * @return string + */ + private function createEmptyCart(): string + { + $query = <<<QUERY +mutation { + createEmptyCart +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['createEmptyCart']; + } + + /** + * Add product to cart with GraphQl query + * + * @param string $cartId + * @param float $qty + * @param string $sku + * @return void + */ + private function addProductToCart(string $cartId, float $qty, string $sku): void + { + $query = <<<QUERY +mutation { + addSimpleProductsToCart( + input: { + cart_id: "{$cartId}" + cart_items: [ + { + data: { + quantity: {$qty} + sku: "{$sku}" + } + } + ] + } + ) { + cart {items{quantity product {sku}}}} +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set billing address on cart with GraphQL mutation + * + * @param string $cartId + * @return void + */ + private function setBillingAddress(string $cartId): void + { + $query = <<<QUERY +mutation { + setBillingAddressOnCart( + input: { + cart_id: "{$cartId}" + billing_address: { + address: { + firstname: "John" + lastname: "Smith" + company: "Test company" + street: ["test street 1", "test street 2"] + city: "Texas City" + postcode: "78717" + telephone: "5123456677" + region: "TX" + country_code: "US" + } + } + } + ) { + cart { + billing_address { + __typename + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Set shipping address on cart with GraphQl query + * + * @param string $cartId + * @return array + */ + private function setShippingAddress(string $cartId): array + { + $query = <<<QUERY +mutation { + setShippingAddressesOnCart( + input: { + cart_id: "$cartId" + shipping_addresses: [ + { + address: { + firstname: "test shipFirst" + lastname: "test shipLast" + company: "test company" + street: ["test street 1", "test street 2"] + city: "Montgomery" + region: "AL" + postcode: "36013" + country_code: "US" + telephone: "3347665522" + } + } + ] + } + ) { + cart { + shipping_addresses { + available_shipping_methods { + carrier_code + method_code + amount {value} + } + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + $shippingAddress = current($response['setShippingAddressesOnCart']['cart']['shipping_addresses']); + $availableShippingMethod = current($shippingAddress['available_shipping_methods']); + return $availableShippingMethod; + } + + /** + * Set shipping method on cart with GraphQl mutation + * + * @param string $cartId + * @param array $method + * @return array + */ + private function setShippingMethod(string $cartId, array $method): array + { + $query = <<<QUERY +mutation { + setShippingMethodsOnCart(input: { + cart_id: "{$cartId}", + shipping_methods: [ + { + carrier_code: "{$method['carrier_code']}" + method_code: "{$method['method_code']}" + } + ] + }) { + cart { + available_payment_methods { + code + title + } + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $availablePaymentMethod = current($response['setShippingMethodsOnCart']['cart']['available_payment_methods']); + return $availablePaymentMethod; + } + + /** + * Set payment method on cart with GrpahQl mutation + * + * @param string $cartId + * @param array $method + * @return void + */ + private function setPaymentMethod(string $cartId, array $method): void + { + $query = <<<QUERY +mutation { + setPaymentMethodOnCart( + input: { + cart_id: "{$cartId}" + payment_method: { + code: "{$method['code']}" + } + } + ) { + cart {selected_payment_method {code}} + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + } + + /** + * Place order using GraphQl mutation + * + * @param string $cartId + * @return string + */ + private function placeOrder(string $cartId): string + { + $query = <<<QUERY +mutation { + placeOrder( + input: { + cart_id: "{$cartId}" + } + ) { + order { + order_number + } + } +} +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlMutation( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + return $response['placeOrder']['order']['order_number']; + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerOrderQuery($orderNumber): array + { + $query = + <<<QUERY +{ + customer { + email + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + id + number + order_date + status + payment_methods + { + name + type + additional_data + { + name + value + } + } + shipping_address { + ... address + } + billing_address { + ... address + } + items{product_name product_sku quantity_ordered discounts {amount{value currency} label}} + total { + base_grand_total{value currency} + grand_total{value currency} + total_tax{value currency} + subtotal { value currency } + taxes {amount{value currency} title rate} + discounts {amount{value currency} label} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value currency} + taxes {amount{value} title rate} + discounts {amount{value currency}} + } + + } + } + } + } + } + + fragment address on OrderAddress { + firstname + lastname + city + company + country_code + fax + middlename + postcode + street + region + region_id + telephone + vat_id + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + return $response['customer']['orders']['items']; + } + + /** + * Get customer order query + * + * @param string $orderNumber + * @return array + */ + private function getCustomerOrderQueryOnSimpleProducts($orderNumber): array + { + $query = + <<<QUERY +{ + customer + { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items + { + id + number + status + order_date + payment_methods + { + name + type + additional_data + { + name + value + } + } + shipping_address { + ... address + } + billing_address { + ... address + } + items{ + quantity_ordered + product_sku + product_name + product_sale_price{currency value} + } + total { + base_grand_total { + value + currency + } + grand_total { + value + currency + } + subtotal { + value + currency + } + } + } + } + } +} + +fragment address on OrderAddress { + firstname + lastname + city + company + country_code + fax + middlename + postcode + street + region + region_id + telephone + vat_id + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + return $response; + } + + /** + * Clean up orders + * + * @return void + */ + private function deleteOrder(): void + { + /** @var Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php new file mode 100644 index 0000000000000..b4c9bd4962cc2 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Sales/RetrieveOrdersWithBundleProductByOrderNumberTest.php @@ -0,0 +1,310 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Sales; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Exception\AuthenticationException; +use Magento\GraphQl\GetCustomerAuthenticationHeader; +use Magento\GraphQl\Sales\Fixtures\CustomerPlaceOrder; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\ResourceModel\Order\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test for orders with bundle product + */ +class RetrieveOrdersWithBundleProductByOrderNumberTest extends GraphQlAbstract +{ + /** @var OrderRepositoryInterface */ + private $orderRepository; + + /** @var SearchCriteriaBuilder */ + private $searchCriteriaBuilder; + + /** @var GetCustomerAuthenticationHeader */ + private $customerAuthenticationHeader; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + protected function setUp():void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->customerAuthenticationHeader = $objectManager->get(GetCustomerAuthenticationHeader::class); + $this->orderRepository = $objectManager->get(OrderRepositoryInterface::class); + $this->searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + protected function tearDown(): void + { + $this->deleteOrder(); + } + + /** + * Test customer order details with bundle product with child items + * + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + */ + public function testGetCustomerOrderBundleProduct() + { + //Place order with bundled product + $qty = 1; + $bundleSku = 'bundle-product-two-dropdown-options'; + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $orderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => $bundleSku, 'quantity' => $qty] + ); + $orderNumber = $orderResponse['placeOrder']['order']['order_number']; + //End place order with bundled product + + $customerOrderResponse = $this->getCustomerOrderQueryBundleProduct($orderNumber); + $customerOrderItems = $customerOrderResponse[0]; + $this->assertEquals("Pending", $customerOrderItems['status']); + $bundledItemInTheOrder = $customerOrderItems['items'][0]; + $this->assertEquals( + 'bundle-product-two-dropdown-options-simple1-simple2', + $bundledItemInTheOrder['product_sku'] + ); + $priceOfBundledItemInOrder = $bundledItemInTheOrder['product_sale_price']['value']; + $this->assertEquals(15, $priceOfBundledItemInOrder); + $this->assertArrayHasKey('bundle_options', $bundledItemInTheOrder); + $bundleOptionsFromResponse = $bundledItemInTheOrder['bundle_options']; + $this->assertNotEmpty($bundleOptionsFromResponse); + $this->assertEquals(2, count($bundleOptionsFromResponse)); + $expectedBundleOptions = + [ + [ '__typename' => 'ItemSelectedBundleOption', + 'label' => 'Drop Down Option 1', + 'values' => [ + [ + 'product_sku' => 'simple1', + 'product_name' => 'Simple Product1', + 'quantity'=> 1, + 'price' => [ + 'value' => 1, + 'currency' => 'USD' + ] + ] + ] + ], + [ '__typename' => 'ItemSelectedBundleOption', + 'label' => 'Drop Down Option 2', + 'values' => [ + [ + 'product_sku' => 'simple2', + 'product_name' => 'Simple Product2', + 'quantity'=> 2, + 'price' => [ + 'value' => 2, + 'currency' => 'USD' + ] + ] + ] + ], + ]; + $this->assertEquals($expectedBundleOptions, $bundleOptionsFromResponse); + } + + /** + * Test customer order details with bundle products + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/bundle_product_two_dropdown_options.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_rule_for_region_1.php + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php + * @magentoApiDataFixture Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php + */ + public function testGetCustomerOrderBundleProductWithTaxesAndDiscounts() + { + //Place order with bundled product + $qty = 4; + $bundleSku = 'bundle-product-two-dropdown-options'; + /** @var CustomerPlaceOrder $bundleProductOrderFixture */ + $bundleProductOrderFixture = Bootstrap::getObjectManager()->create(CustomerPlaceOrder::class); + $orderResponse = $bundleProductOrderFixture->placeOrderWithBundleProduct( + ['email' => 'customer@example.com', 'password' => 'password'], + ['sku' => $bundleSku, 'quantity' => $qty] + ); + $orderNumber = $orderResponse['placeOrder']['order']['order_number']; + //End place order with bundled product + + $customerOrderResponse = $this->getCustomerOrderQueryBundleProduct($orderNumber); + $customerOrderItems = $customerOrderResponse[0]; + $this->assertEquals("Pending", $customerOrderItems['status']); + + $bundledItemInTheOrder = $customerOrderItems['items'][0]; + $this->assertEquals( + 'bundle-product-two-dropdown-options-simple1-simple2', + $bundledItemInTheOrder['product_sku'] + ); + $this->assertEquals(6, $bundledItemInTheOrder['discounts'][0]['amount']['value']); + $this->assertEquals( + 'Discount Label for 10% off', + $bundledItemInTheOrder["discounts"][0]['label'] + ); + $this->assertArrayHasKey('bundle_options', $bundledItemInTheOrder); + $childItemsInTheOrder = $bundledItemInTheOrder['bundle_options']; + $this->assertNotEmpty($childItemsInTheOrder); + $this->assertCount(2, $childItemsInTheOrder); + $this->assertEquals('Drop Down Option 1', $childItemsInTheOrder[0]['label']); + $this->assertEquals('Drop Down Option 2', $childItemsInTheOrder[1]['label']); + + $this->assertEquals('simple1', $childItemsInTheOrder[0]['values'][0]['product_sku']); + $this->assertEquals('simple2', $childItemsInTheOrder[1]['values'][0]['product_sku']); + $this->assertTotalsOnBundleProductWithTaxesAndDiscounts($customerOrderItems['total']); + } + + /** + * @param array $customerOrderItemTotal + */ + private function assertTotalsOnBundleProductWithTaxesAndDiscounts(array $customerOrderItemTotal): void + { + $this->assertCount(1, $customerOrderItemTotal['taxes']); + $taxData = $customerOrderItemTotal['taxes'][0]; + $this->assertEquals('USD', $taxData['amount']['currency']); + $this->assertEquals(5.4, $taxData['amount']['value']); + $this->assertEquals('US-TEST-*-Rate-1', $taxData['title']); + $this->assertEquals(7.5, $taxData['rate']); + + unset($customerOrderItemTotal['taxes']); + $assertionMap = [ + 'base_grand_total' => ['value' => 77.4, 'currency' =>'USD'], + 'grand_total' => ['value' => 77.4, 'currency' =>'USD'], + 'subtotal' => ['value' => 60, 'currency' =>'USD'], + 'total_tax' => ['value' => 5.4, 'currency' =>'USD'], + 'total_shipping' => ['value' => 20, 'currency' =>'USD'], + 'shipping_handling' => [ + 'amount_including_tax' => ['value' => 21.5], + 'amount_excluding_tax' => ['value' => 20], + 'total_amount' => ['value' => 20], + 'discounts' => [ + 0 => ['amount'=>['value'=> 2]] + ], + 'taxes'=> [ + 0 => [ + 'amount'=>['value' => 1.35], + 'title' => 'US-TEST-*-Rate-1', + 'rate' => 7.5 + ] + ] + ], + 'discounts' => [ + 0 => ['amount' => [ 'value' => 8, 'currency' =>'USD'], + 'label' => 'Discount Label for 10% off' + ] + ] + ]; + $this->assertResponseFields($customerOrderItemTotal, $assertionMap); + } + + /** + * Get customer order query for bundle order items + * + * @param $orderNumber + * @return mixed + * @throws AuthenticationException + */ + private function getCustomerOrderQueryBundleProduct($orderNumber) + { + $query = + <<<QUERY +{ + customer { + orders(filter:{number:{eq:"{$orderNumber}"}}) { + total_count + items { + id + number + order_date + status + items{ + __typename + product_sku + product_name + product_url_key + product_sale_price{value} + quantity_ordered + discounts{amount{value} label} + ... on BundleOrderItem{ + bundle_options{ + __typename + label + values { + product_sku + product_name + quantity + price { + value + currency + } + } + } + } + } + total { + base_grand_total{value currency} + grand_total{value currency} + subtotal {value currency } + total_tax{value currency} + taxes {amount{value currency} title rate} + total_shipping{value currency} + shipping_handling + { + amount_including_tax{value} + amount_excluding_tax{value} + total_amount{value} + discounts{amount{value}} + taxes {amount{value} title rate} + } + discounts {amount{value currency} label} + } + } + } + } + } +QUERY; + $currentEmail = 'customer@example.com'; + $currentPassword = 'password'; + $response = $this->graphQlQuery( + $query, + [], + '', + $this->customerAuthenticationHeader->execute($currentEmail, $currentPassword) + ); + + $this->assertArrayHasKey('orders', $response['customer']); + $this->assertArrayHasKey('items', $response['customer']['orders']); + $customerOrderItemsInResponse = $response['customer']['orders']['items']; + return $customerOrderItemsInResponse; + } + + /** + * @return void + */ + private function deleteOrder(): void + { + /** @var \Magento\Framework\Registry $registry */ + $registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', true); + + /** @var $order \Magento\Sales\Model\Order */ + $orderCollection = Bootstrap::getObjectManager()->create(Collection::class); + foreach ($orderCollection as $order) { + $this->orderRepository->delete($order); + } + $registry->unregister('isSecureArea'); + $registry->register('isSecureArea', false); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php new file mode 100644 index 0000000000000..eaf76e4559557 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/AvailableStoreConfigTest.php @@ -0,0 +1,171 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Store; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\Data\StoreConfigInterface; +use Magento\Store\Api\StoreConfigManagerInterface; +use Magento\Store\Model\ResourceModel\Store as StoreResource; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test the GraphQL endpoint's AvailableStores query + */ +class AvailableStoreConfigTest extends GraphQlAbstract +{ + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var StoreConfigManagerInterface + */ + private $storeConfigManager; + + /** + * @var StoreResource + */ + private $storeResource; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->storeConfigManager = $this->objectManager->get(StoreConfigManagerInterface::class); + $this->storeResource = $this->objectManager->get(StoreResource::class); + } + + /** + * @magentoApiDataFixture Magento/Store/_files/store.php + * @magentoApiDataFixture Magento/Store/_files/inactive_store.php + */ + public function testDefaultWebsiteAvailableStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(); + + $expectedAvailableStores = []; + $expectedAvailableStoreCodes = [ + 'default', + 'test' + ]; + + foreach ($storeConfigs as $storeConfig) { + if (in_array($storeConfig->getCode(), $expectedAvailableStoreCodes)) { + $expectedAvailableStores[] = $storeConfig; + } + } + + $query + = <<<QUERY +{ + availableStores { + id, + code, + website_id, + locale, + base_currency_code, + default_display_currency_code, + timezone, + weight_unit, + base_url, + base_link_url, + base_static_url, + base_media_url, + secure_base_url, + secure_base_link_url, + secure_base_static_url, + secure_base_media_url, + store_name + } +} +QUERY; + $response = $this->graphQlQuery($query); + + $this->assertArrayHasKey('availableStores', $response); + foreach ($expectedAvailableStores as $key => $storeConfig) { + $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); + } + } + + /** + * @magentoApiDataFixture Magento/Store/_files/second_website_with_two_stores.php + */ + public function testNonDefaultWebsiteAvailableStoreConfigs(): void + { + $storeConfigs = $this->storeConfigManager->getStoreConfigs(['fixture_second_store', 'fixture_third_store']); + + $query + = <<<QUERY +{ + availableStores { + id, + code, + website_id, + locale, + base_currency_code, + default_display_currency_code, + timezone, + weight_unit, + base_url, + base_link_url, + base_static_url, + base_media_url, + secure_base_url, + secure_base_link_url, + secure_base_static_url, + secure_base_media_url, + store_name + } +} +QUERY; + $headerMap = ['Store' => 'fixture_second_store']; + $response = $this->graphQlQuery($query, [], '', $headerMap); + + $this->assertArrayHasKey('availableStores', $response); + foreach ($storeConfigs as $key => $storeConfig) { + $this->validateStoreConfig($storeConfig, $response['availableStores'][$key]); + } + } + + /** + * Validate Store Config Data + * + * @param StoreConfigInterface $storeConfig + * @param array $responseConfig + */ + private function validateStoreConfig(StoreConfigInterface $storeConfig, array $responseConfig): void + { + $store = $this->objectManager->get(Store::class); + $this->storeResource->load($store, $storeConfig->getCode(), 'code'); + $this->assertEquals($storeConfig->getId(), $responseConfig['id']); + $this->assertEquals($storeConfig->getCode(), $responseConfig['code']); + $this->assertEquals($storeConfig->getLocale(), $responseConfig['locale']); + $this->assertEquals($storeConfig->getBaseCurrencyCode(), $responseConfig['base_currency_code']); + $this->assertEquals( + $storeConfig->getDefaultDisplayCurrencyCode(), + $responseConfig['default_display_currency_code'] + ); + $this->assertEquals($storeConfig->getTimezone(), $responseConfig['timezone']); + $this->assertEquals($storeConfig->getWeightUnit(), $responseConfig['weight_unit']); + $this->assertEquals($storeConfig->getBaseUrl(), $responseConfig['base_url']); + $this->assertEquals($storeConfig->getBaseLinkUrl(), $responseConfig['base_link_url']); + $this->assertEquals($storeConfig->getBaseStaticUrl(), $responseConfig['base_static_url']); + $this->assertEquals($storeConfig->getBaseMediaUrl(), $responseConfig['base_media_url']); + $this->assertEquals($storeConfig->getSecureBaseUrl(), $responseConfig['secure_base_url']); + $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $responseConfig['secure_base_link_url']); + $this->assertEquals($storeConfig->getSecureBaseStaticUrl(), $responseConfig['secure_base_static_url']); + $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $responseConfig['secure_base_media_url']); + $this->assertEquals($store->getName(), $responseConfig['store_name']); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php index 48619d1392309..cc8a60cf0937a 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Store/StoreConfigResolverTest.php @@ -7,10 +7,12 @@ namespace Magento\GraphQl\Store; +use Magento\Framework\Exception\NoSuchEntityException; use Magento\Store\Api\Data\StoreConfigInterface; use Magento\Store\Api\StoreConfigManagerInterface; use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Api\StoreResolverInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\ObjectManager; use Magento\TestFramework\TestCase\GraphQlAbstract; @@ -20,34 +22,37 @@ class StoreConfigResolverTest extends GraphQlAbstract { - /** @var ObjectManager */ + /** @var ObjectManager */ private $objectManager; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->objectManager = Bootstrap::getObjectManager(); } /** * @magentoApiDataFixture Magento/Store/_files/store.php - * @magentoConfigFixture default_store store/information/name Test Store + * @throws NoSuchEntityException */ - public function testGetStoreConfig() + public function testGetStoreConfig(): void { - /** @var StoreConfigManagerInterface $storeConfigsManager */ - $storeConfigsManager = $this->objectManager->get(StoreConfigManagerInterface::class); + /** @var StoreConfigManagerInterface $storeConfigManager */ + $storeConfigManager = $this->objectManager->get(StoreConfigManagerInterface::class); /** @var StoreResolverInterface $storeResolver */ $storeResolver = $this->objectManager->get(StoreResolverInterface::class); /** @var StoreRepositoryInterface $storeRepository */ $storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); $storeId = $storeResolver->getCurrentStoreId(); $store = $storeRepository->getById($storeId); - /** @var StoreConfigInterface $storeConfig */ - $storeConfig = current($storeConfigsManager->getStoreConfigs([$store->getCode()])); + /** @var StoreConfigInterface $defaultStoreConfig */ + $defaultStoreConfig = current($storeConfigManager->getStoreConfigs([$store->getCode()])); $query = <<<QUERY { - storeConfig{ + storeConfig { id, code, website_id, @@ -70,27 +75,39 @@ public function testGetStoreConfig() QUERY; $response = $this->graphQlQuery($query); $this->assertArrayHasKey('storeConfig', $response); - $this->assertEquals($storeConfig->getId(), $response['storeConfig']['id']); - $this->assertEquals($storeConfig->getCode(), $response['storeConfig']['code']); - $this->assertEquals($storeConfig->getLocale(), $response['storeConfig']['locale']); - $this->assertEquals($storeConfig->getBaseCurrencyCode(), $response['storeConfig']['base_currency_code']); + $this->validateStoreConfig($defaultStoreConfig, $response['storeConfig'], $store->getName()); + } + + /** + * Validate Store Config Data + * + * @param StoreConfigInterface $storeConfig + * @param array $responseConfig + * @param string $storeName + */ + private function validateStoreConfig( + StoreConfigInterface $storeConfig, + array $responseConfig, + string $storeName + ): void { + $this->assertEquals($storeConfig->getId(), $responseConfig['id']); + $this->assertEquals($storeConfig->getCode(), $responseConfig['code']); + $this->assertEquals($storeConfig->getLocale(), $responseConfig['locale']); + $this->assertEquals($storeConfig->getBaseCurrencyCode(), $responseConfig['base_currency_code']); $this->assertEquals( $storeConfig->getDefaultDisplayCurrencyCode(), - $response['storeConfig']['default_display_currency_code'] - ); - $this->assertEquals($storeConfig->getTimezone(), $response['storeConfig']['timezone']); - $this->assertEquals($storeConfig->getWeightUnit(), $response['storeConfig']['weight_unit']); - $this->assertEquals($storeConfig->getBaseUrl(), $response['storeConfig']['base_url']); - $this->assertEquals($storeConfig->getBaseLinkUrl(), $response['storeConfig']['base_link_url']); - $this->assertEquals($storeConfig->getBaseStaticUrl(), $response['storeConfig']['base_static_url']); - $this->assertEquals($storeConfig->getBaseMediaUrl(), $response['storeConfig']['base_media_url']); - $this->assertEquals($storeConfig->getSecureBaseUrl(), $response['storeConfig']['secure_base_url']); - $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $response['storeConfig']['secure_base_link_url']); - $this->assertEquals( - $storeConfig->getSecureBaseStaticUrl(), - $response['storeConfig']['secure_base_static_url'] + $responseConfig['default_display_currency_code'] ); - $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $response['storeConfig']['secure_base_media_url']); - $this->assertEquals('Test Store', $response['storeConfig']['store_name']); + $this->assertEquals($storeConfig->getTimezone(), $responseConfig['timezone']); + $this->assertEquals($storeConfig->getWeightUnit(), $responseConfig['weight_unit']); + $this->assertEquals($storeConfig->getBaseUrl(), $responseConfig['base_url']); + $this->assertEquals($storeConfig->getBaseLinkUrl(), $responseConfig['base_link_url']); + $this->assertEquals($storeConfig->getBaseStaticUrl(), $responseConfig['base_static_url']); + $this->assertEquals($storeConfig->getBaseMediaUrl(), $responseConfig['base_media_url']); + $this->assertEquals($storeConfig->getSecureBaseUrl(), $responseConfig['secure_base_url']); + $this->assertEquals($storeConfig->getSecureBaseLinkUrl(), $responseConfig['secure_base_link_url']); + $this->assertEquals($storeConfig->getSecureBaseStaticUrl(), $responseConfig['secure_base_static_url']); + $this->assertEquals($storeConfig->getSecureBaseMediaUrl(), $responseConfig['secure_base_media_url']); + $this->assertEquals($storeName, $responseConfig['store_name']); } } diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php new file mode 100644 index 0000000000000..a81ec701b22a8 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddBundleProductToWishlistTest.php @@ -0,0 +1,176 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Product\Type; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a bundle product to wishlist + */ +class AddBundleProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var mixed + */ + private $productRepository; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + $this->productRepository = $objectManager->get(ProductRepositoryInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Bundle/_files/product_1.php + * + * @throws Exception + */ + public function testAddBundleProductWithOptions(): void + { + $sku = 'bundle-product'; + $product = $this->productRepository->get($sku); + $customerId = 1; + $qty = 2; + $optionQty = 1; + + /** @var Type $typeInstance */ + $typeInstance = $product->getTypeInstance(); + $typeInstance->setStoreFilter($product->getStoreId(), $product); + /** @var Option $option */ + $option = $typeInstance->getOptionsCollection($product)->getFirstItem(); + /** @var Product $selection */ + $selection = $typeInstance->getSelectionsCollection([$option->getId()], $product)->getFirstItem(); + $optionId = $option->getId(); + $selectionId = $selection->getSelectionId(); + $bundleOptions = $this->generateBundleOptionIdV2((int) $optionId, (int) $selectionId, $optionQty); + + $query = $this->getQuery($sku, $qty, $bundleOptions); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $item */ + $item = $wishlist->getItemCollection()->getFirstItem(); + + $this->assertArrayHasKey('addProductsToWishlist', $response); + $this->assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $response = $response['addProductsToWishlist']['wishlist']; + $this->assertEquals($wishlist->getItemsCount(), $response['items_count']); + $this->assertEquals($wishlist->getSharingCode(), $response['sharing_code']); + $this->assertEquals($wishlist->getUpdatedAt(), $response['updated_at']); + $this->assertEquals($item->getData('qty'), $response['items'][0]['qty']); + $this->assertEquals($item->getDescription(), $response['items'][0]['description']); + $this->assertEquals($item->getAddedAt(), $response['items'][0]['added_at']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $sku + * @param int $qty + * @param string $bundleOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $sku, + int $qty, + string $bundleOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + selected_options: [ + "{$bundleOptions}" + ] + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * @param int $optionId + * @param int $selectionId + * + * @param int $quantity + * + * @return string + */ + private function generateBundleOptionIdV2(int $optionId, int $selectionId, int $quantity): string + { + return base64_encode("bundle/$optionId/$selectionId/$quantity"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php new file mode 100644 index 0000000000000..d8d44541f899d --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddConfigurableProductToWishlistTest.php @@ -0,0 +1,222 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a configurable product to wishlist + */ +class AddConfigurableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $objectManager->get(WishlistFactory::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/ConfigurableProduct/_files/product_configurable.php + * + * @throws Exception + */ + public function testAddDownloadableProductWithOptions(): void + { + $product = $this->getConfigurableProductInfo(); + $customerId = 1; + $qty = 2; + $attributeId = (int) $product['configurable_options'][0]['attribute_id']; + $valueIndex = $product['configurable_options'][0]['values'][0]['value_index']; + $childSku = $product['variants'][0]['product']['sku']; + $parentSku = $product['sku']; + $selectedConfigurableOptionsQuery = $this->generateSuperAttributesIdV2Query($attributeId, $valueIndex); + + $query = $this->getQuery($parentSku, $childSku, $qty, $selectedConfigurableOptionsQuery); + + $response = $this->graphQlMutation($query, [], '', $this->getHeadersMap()); + $wishlist = $this->wishlistFactory->create()->loadByCustomerId($customerId, true); + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + + self::assertArrayHasKey('addProductsToWishlist', $response); + self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $wishlistResponse = $response['addProductsToWishlist']['wishlist']; + self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); + self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); + self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); + self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeadersMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param string $parentSku + * @param string $childSku + * @param int $qty + * @param string $customizableOptions + * @param int $wishlistId + * + * @return string + */ + private function getQuery( + string $parentSku, + string $childSku, + int $qty, + string $customizableOptions, + int $wishlistId = 0 + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + sku: "{$childSku}" + parent_sku: "{$parentSku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * Generates Id_v2 for super configurable product super attributes + * + * @param int $attributeId + * @param int $valueIndex + * + * @return string + */ + private function generateSuperAttributesIdV2Query(int $attributeId, int $valueIndex): string + { + return 'selected_options: ["' . base64_encode("configurable/$attributeId/$valueIndex") . '"]'; + } + + /** + * Returns information about testable configurable product retrieved from GraphQl query + * + * @return array + * + * @throws Exception + */ + private function getConfigurableProductInfo(): array + { + $searchResponse = $this->graphQlQuery($this->getFetchProductQuery('configurable')); + + return current($searchResponse['products']['items']); + } + + /** + * Returns GraphQl query for fetching configurable product information + * + * @param string $term + * + * @return string + */ + private function getFetchProductQuery(string $term): string + { + return <<<QUERY +{ + products( + search:"{$term}" + pageSize:1 + ) { + items { + sku + ... on ConfigurableProduct { + variants { + product { + sku + } + } + configurable_options { + attribute_id + attribute_code + id + label + position + product_id + use_default + values { + default_label + label + store_label + use_default_value + value_index + } + } + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php new file mode 100644 index 0000000000000..489a960056f1b --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/AddDownloadableProductToWishlistTest.php @@ -0,0 +1,216 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; +use Magento\Wishlist\Model\Item; +use Magento\Wishlist\Model\WishlistFactory; + +/** + * Test coverage for adding a downloadable product to wishlist + */ +class AddDownloadableProductToWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var WishlistFactory + */ + private $wishlistFactory; + + /** + * @var GetCustomOptionsWithIDV2ForQueryBySku + */ + private $getCustomOptionsWithIDV2ForQueryBySku; + + /** + * Set Up + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $this->objectManager->get(CustomerTokenServiceInterface::class); + $this->wishlistFactory = $this->objectManager->get(WishlistFactory::class); + $this->getCustomOptionsWithIDV2ForQueryBySku = + $this->objectManager->get(GetCustomOptionsWithIDV2ForQueryBySku::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + */ + public function testAddDownloadableProductWithOptions(): void + { + $customerId = 1; + $sku = 'downloadable-product-with-purchased-separately-links'; + $qty = 2; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ); + $query = $this->getQuery($qty, $sku, trim($productOptionsQuery, '{}')); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + $wishlist = $this->wishlistFactory->create(); + $wishlist->loadByCustomerId($customerId, true); + /** @var Item $wishlistItem */ + $wishlistItem = $wishlist->getItemCollection()->getFirstItem(); + + self::assertArrayHasKey('addProductsToWishlist', $response); + self::assertArrayHasKey('wishlist', $response['addProductsToWishlist']); + $wishlistResponse = $response['addProductsToWishlist']['wishlist']; + self::assertEquals($wishlist->getItemsCount(), $wishlistResponse['items_count']); + self::assertEquals($wishlist->getSharingCode(), $wishlistResponse['sharing_code']); + self::assertEquals($wishlist->getUpdatedAt(), $wishlistResponse['updated_at']); + self::assertEquals($wishlistItem->getId(), $wishlistResponse['items'][0]['id']); + self::assertEquals($wishlistItem->getData('qty'), $wishlistResponse['items'][0]['qty']); + self::assertEquals($wishlistItem->getDescription(), $wishlistResponse['items'][0]['description']); + self::assertEquals($wishlistItem->getAddedAt(), $wishlistResponse['items'][0]['added_at']); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Downloadable/_files/product_downloadable_with_custom_options.php + */ + public function testAddDownloadableProductOnDisabledWishlist(): void + { + $qty = 2; + $sku = 'downloadable-product-with-purchased-separately-links'; + $links = $this->getProductsLinks($sku); + $linkId = key($links); + $itemOptions = $this->getCustomOptionsWithIDV2ForQueryBySku->execute($sku); + $itemOptions['selected_options'][] = $this->generateProductLinkSelectedOptions($linkId); + $productOptionsQuery = trim(preg_replace( + '/"([^"]+)"\s*:\s*/', + '$1:', + json_encode($itemOptions) + ), '{}'); + $query = $this->getQuery($qty, $sku, $productOptionsQuery); + $this->expectExceptionMessage('The wishlist is not currently available.'); + $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + } + + /** + * Function returns array of all product's links + * + * @param string $sku + * + * @return array + */ + private function getProductsLinks(string $sku): array + { + $result = []; + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $product = $productRepository->get($sku, false, null, true); + + foreach ($product->getDownloadableLinks() as $linkObject) { + $result[$linkObject->getLinkId()] = [ + 'title' => $linkObject->getTitle(), + 'price' => $linkObject->getPrice(), + ]; + } + + return $result; + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $qty + * @param string $sku + * @param string $customizableOptions + * + * @return string + */ + private function getQuery( + int $qty, + string $sku, + string $customizableOptions + ): string { + return <<<MUTATION +mutation { + addProductsToWishlist( + wishlistId: 0, + wishlistItems: [ + { + sku: "{$sku}" + quantity: {$qty} + {$customizableOptions} + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + updated_at + items { + id + description + qty + added_at + } + } + } +} +MUTATION; + } + + /** + * Generates Id_v2 for downloadable links + * + * @param int $linkId + * + * @return string + */ + private function generateProductLinkSelectedOptions(int $linkId): string + { + return base64_encode("downloadable/$linkId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php index 2208f904320d9..0a8e1757a2ce2 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/CustomerWishlistTest.php @@ -32,6 +32,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testCustomerWishlist(): void @@ -74,6 +75,7 @@ public function testCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Customer/_files/customer.php */ public function testCustomerAlwaysHasWishlist(): void @@ -100,6 +102,7 @@ public function testCustomerAlwaysHasWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGuestCannotGetWishlist() { @@ -121,6 +124,35 @@ public function testGuestCannotGetWishlist() $this->graphQlQuery($query); } + /** + * @magentoConfigFixture default_store wishlist/general/active 0 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + */ + public function testCustomerCannotGetWishlistWhenDisabled() + { + $this->expectException(\Exception::class); + $this->expectExceptionMessage('The wishlist is not currently available.'); + + $query = + <<<QUERY +{ + customer { + wishlist { + items_count + sharing_code + updated_at + } + } +} +QUERY; + $this->graphQlQuery( + $query, + [], + '', + $this->getCustomerAuthHeaders('customer@example.com', 'password') + ); + } + /** * @param string $email * @param string $password diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php new file mode 100644 index 0000000000000..ebe99289b8934 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/DeleteProductsFromWishlistTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for deleting a product from wishlist + */ +class DeleteProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testDeleteWishlistItemFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlist = $wishlist['customer']['wishlist']; + $wishlistItems = $wishlist['items']; + self::assertEquals(1, $wishlist['items_count']); + + $query = $this->getQuery((int) $wishlistId, (int) $wishlistItems[0]['id']); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('removeProductsFromWishlist', $response); + self::assertArrayHasKey('wishlist', $response['removeProductsFromWishlist']); + $wishlistResponse = $response['removeProductsFromWishlist']['wishlist']; + self::assertEquals(0, $wishlistResponse['items_count']); + self::assertEmpty($wishlistResponse['items']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId + ): string { + return <<<MUTATION +mutation { + removeProductsFromWishlist( + wishlistId: {$wishlistId}, + wishlistItemsIds: [{$wishlistItemId}] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + items { + id + description + qty + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items { + id + qty + description + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php new file mode 100644 index 0000000000000..fcba7458f317a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/GetCustomOptionsWithIDV2ForQueryBySku.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Magento\Catalog\Api\ProductCustomOptionRepositoryInterface; + +/** + * Generate an array with test values for customizable options with encoded id_v2 value + */ +class GetCustomOptionsWithIDV2ForQueryBySku +{ + /** + * @var ProductCustomOptionRepositoryInterface + */ + private $productCustomOptionRepository; + + /** + * @param ProductCustomOptionRepositoryInterface $productCustomOptionRepository + */ + public function __construct(ProductCustomOptionRepositoryInterface $productCustomOptionRepository) + { + $this->productCustomOptionRepository = $productCustomOptionRepository; + } + + /** + * Returns array of custom options for the product + * + * @param string $sku + * + * @return array + */ + public function execute(string $sku): array + { + $customOptions = $this->productCustomOptionRepository->getList($sku); + $selectedOptions = []; + $enteredOptions = []; + + foreach ($customOptions as $customOption) { + $optionType = $customOption->getType(); + + if ($optionType === 'field' || $optionType === 'area' || $optionType === 'date') { + $enteredOptions[] = [ + 'uid' => $this->encodeEnteredOption((int)$customOption->getOptionId()), + 'value' => '2012-12-12' + ]; + } elseif ($optionType === 'drop_down') { + $optionSelectValues = $customOption->getValues(); + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)reset($optionSelectValues)->getOptionTypeId() + ); + } elseif ($optionType === 'multiple') { + foreach ($customOption->getValues() as $optionValue) { + $selectedOptions[] = $this->encodeSelectedOption( + (int)$customOption->getOptionId(), + (int)$optionValue->getOptionTypeId() + ); + } + } + } + + return [ + 'selected_options' => $selectedOptions, + 'entered_options' => $enteredOptions + ]; + } + + /** + * Returns id_v2 of the selected custom option + * + * @param int $optionId + * @param int $optionValueId + * + * @return string + */ + private function encodeSelectedOption(int $optionId, int $optionValueId): string + { + return base64_encode("custom-option/$optionId/$optionValueId"); + } + + /** + * Returns id_v2 of the entered custom option + * + * @param int $optionId + * + * @return string + */ + private function encodeEnteredOption(int $optionId): string + { + return base64_encode("custom-option/$optionId"); + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php new file mode 100644 index 0000000000000..9a9cd424e54ca --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/UpdateProductsFromWishlistTest.php @@ -0,0 +1,159 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\GraphQl\Wishlist; + +use Exception; +use Magento\Framework\Exception\AuthenticationException; +use Magento\Integration\Api\CustomerTokenServiceInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\GraphQlAbstract; + +/** + * Test coverage for updating a product from wishlist + */ +class UpdateProductsFromWishlistTest extends GraphQlAbstract +{ + /** + * @var CustomerTokenServiceInterface + */ + private $customerTokenService; + + /** + * Set Up + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->customerTokenService = $objectManager->get(CustomerTokenServiceInterface::class); + } + + /** + * @magentoConfigFixture default_store wishlist/general/active 1 + * @magentoApiDataFixture Magento/Customer/_files/customer.php + * @magentoApiDataFixture Magento/Wishlist/_files/wishlist_with_simple_product.php + */ + public function testUpdateSimpleProductFromWishlist(): void + { + $wishlist = $this->getWishlist(); + $qty = 5; + $description = 'New Description'; + $wishlistId = $wishlist['customer']['wishlist']['id']; + $wishlistItem = $wishlist['customer']['wishlist']['items'][0]; + self::assertNotEquals($description, $wishlistItem['description']); + self::assertNotEquals($qty, $wishlistItem['qty']); + + $query = $this->getQuery((int) $wishlistId, (int) $wishlistItem['id'], $qty, $description); + $response = $this->graphQlMutation($query, [], '', $this->getHeaderMap()); + + self::assertArrayHasKey('updateProductsInWishlist', $response); + self::assertArrayHasKey('wishlist', $response['updateProductsInWishlist']); + $wishlistResponse = $response['updateProductsInWishlist']['wishlist']; + self::assertEquals($qty, $wishlistResponse['items'][0]['qty']); + self::assertEquals($description, $wishlistResponse['items'][0]['description']); + } + + /** + * Authentication header map + * + * @param string $username + * @param string $password + * + * @return array + * + * @throws AuthenticationException + */ + private function getHeaderMap(string $username = 'customer@example.com', string $password = 'password'): array + { + $customerToken = $this->customerTokenService->createCustomerAccessToken($username, $password); + + return ['Authorization' => 'Bearer ' . $customerToken]; + } + + /** + * Returns GraphQl mutation string + * + * @param int $wishlistId + * @param int $wishlistItemId + * @param int $qty + * @param string $description + * + * @return string + */ + private function getQuery( + int $wishlistId, + int $wishlistItemId, + int $qty, + string $description + ): string { + return <<<MUTATION +mutation { + updateProductsInWishlist( + wishlistId: {$wishlistId}, + wishlistItems: [ + { + wishlist_item_id: "{$wishlistItemId}" + quantity: {$qty} + description: "{$description}" + } + ] +) { + user_errors { + code + message + } + wishlist { + id + sharing_code + items_count + items { + id + description + qty + } + } + } +} +MUTATION; + } + + /** + * Get wishlist result + * + * @return array + * + * @throws Exception + */ + public function getWishlist(): array + { + return $this->graphQlQuery($this->getCustomerWishlistQuery(), [], '', $this->getHeaderMap()); + } + + /** + * Get customer wishlist query + * + * @return string + */ + private function getCustomerWishlistQuery(): string + { + return <<<QUERY +query { + customer { + wishlist { + id + items_count + items { + id + qty + description + } + } + } +} +QUERY; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php index bb353938239bc..88c59d6dd8428 100644 --- a/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php +++ b/dev/tests/api-functional/testsuite/Magento/GraphQl/Wishlist/WishlistTest.php @@ -39,6 +39,7 @@ protected function setUp(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 * @magentoApiDataFixture Magento/Wishlist/_files/wishlist.php */ public function testGetCustomerWishlist(): void @@ -94,6 +95,7 @@ public function testGetCustomerWishlist(): void } /** + * @magentoConfigFixture default_store wishlist/general/active 1 */ public function testGetGuestWishlist() { diff --git a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php index 91a044f189b4c..0e277ac942263 100644 --- a/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Integration/Model/CustomerTokenServiceTest.php @@ -7,7 +7,7 @@ namespace Magento\Integration\Model; use Magento\Customer\Api\AccountManagementInterface; -use Magento\Framework\Exception\InputException; +use Magento\Framework\Webapi\Rest\Request; use Magento\Integration\Model\Oauth\Token as TokenModel; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; @@ -76,9 +76,15 @@ protected function setUp(): void } /** + * Create customer access token + * + * @dataProvider storesDataProvider * @magentoApiDataFixture Magento/Customer/_files/customer.php + * + * @param string|null $store + * @return void */ - public function testCreateCustomerAccessToken() + public function testCreateCustomerAccessToken(?string $store): void { $userName = 'customer@example.com'; $password = 'password'; @@ -86,15 +92,28 @@ public function testCreateCustomerAccessToken() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $userName, 'password' => $password]; - $accessToken = $this->_webApiCall($serviceInfo, $requestData); + $accessToken = $this->_webApiCall($serviceInfo, $requestData, null, $store); $this->assertToken($accessToken, $userName, $password); } + /** + * DataProvider for testCreateCustomerAccessToken + * + * @return array + */ + public function storesDataProvider(): array + { + return [ + 'default store' => [null], + 'all store view' => ['all'], + ]; + } + /** * @dataProvider validationDataProvider */ @@ -105,7 +124,7 @@ public function testCreateCustomerAccessTokenEmptyOrNullCredentials($username, $ $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $username, 'password' => $password]; @@ -128,7 +147,7 @@ public function testCreateCustomerAccessTokenInvalidCustomer() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $requestData = ['username' => $customerUserName, 'password' => $password]; @@ -195,7 +214,7 @@ public function testThrottlingMaxAttempts() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ @@ -238,7 +257,7 @@ public function testThrottlingAccountLockout() $serviceInfo = [ 'rest' => [ 'resourcePath' => self::RESOURCE_PATH_CUSTOMER_TOKEN, - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'httpMethod' => Request::HTTP_METHOD_POST, ], ]; $invalidCredentials = [ diff --git a/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php new file mode 100644 index 0000000000000..dc59a571aa136 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Quote/Api/GuestShipmentEstimationWithExtensionAttributesTest.php @@ -0,0 +1,121 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +namespace Magento\Quote\Api; + +use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\TestCase\WebapiAbstract; +use Magento\Quote\Api\Data\AddressInterface; + +class GuestShipmentEstimationWithExtensionAttributesTest extends WebapiAbstract +{ + const SERVICE_VERSION = 'V1'; + const SERVICE_NAME = 'quoteGuestShipmentEstimationV1'; + const RESOURCE_PATH = '/V1/guest-carts/'; + + /** + * @var ObjectManager + */ + private $objectManager; + + protected function setUp(): void + { + $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + } + + /** + * @magentoAppIsolation enabled + * @magentoDbIsolation disabled + * @magentoApiDataFixture Magento/SalesRule/_files/cart_rule_free_shipping.php + * @magentoApiDataFixture Magento/Sales/_files/quote.php + */ + public function testEstimateByExtendedAddress(): void + { + /** @var \Magento\Quote\Model\Quote $quote */ + $quote = $this->objectManager->create(\Magento\Quote\Model\Quote::class); + $quote->load('test01', 'reserved_order_id'); + $cartId = $quote->getId(); + if (!$cartId) { + $this->fail('quote fixture failed'); + } + + /** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ + $quoteIdMask = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(); + $quoteIdMask->load($cartId, 'quote_id'); + //Use masked cart Id + $cartId = $quoteIdMask->getMaskedId(); + $serviceInfo = [ + 'rest' => [ + 'resourcePath' => '/V1/guest-carts/' . $cartId . '/estimate-shipping-methods', + 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + ], + 'soap' => [ + 'service' => self::SERVICE_NAME, + 'serviceVersion' => 'V1', + 'operation' => self::SERVICE_NAME . 'EstimateByExtendedAddress', + ], + ]; + if (TESTS_WEB_API_ADAPTER == self::ADAPTER_SOAP) { + /** @var \Magento\Quote\Model\Quote\Address $address */ + $address = $quote->getShippingAddress(); + + $data = [ + AddressInterface::KEY_ID => (int)$address->getId(), + AddressInterface::KEY_REGION => $address->getRegion(), + AddressInterface::KEY_REGION_ID => $address->getRegionId(), + AddressInterface::KEY_REGION_CODE => $address->getRegionCode(), + AddressInterface::KEY_COUNTRY_ID => $address->getCountryId(), + AddressInterface::KEY_STREET => $address->getStreet(), + AddressInterface::KEY_COMPANY => $address->getCompany(), + AddressInterface::KEY_TELEPHONE => $address->getTelephone(), + AddressInterface::KEY_POSTCODE => $address->getPostcode(), + AddressInterface::KEY_CITY => $address->getCity(), + AddressInterface::KEY_FIRSTNAME => $address->getFirstname(), + AddressInterface::KEY_LASTNAME => $address->getLastname(), + AddressInterface::KEY_CUSTOMER_ID => $address->getCustomerId(), + AddressInterface::KEY_EMAIL => $address->getEmail(), + AddressInterface::SAME_AS_BILLING => $address->getSameAsBilling(), + AddressInterface::CUSTOMER_ADDRESS_ID => $address->getCustomerAddressId(), + AddressInterface::SAVE_IN_ADDRESS_BOOK => $address->getSaveInAddressBook(), + ExtensibleDataInterface::EXTENSION_ATTRIBUTES_KEY => [ + 'discounts' => [] + ] + ]; + + $requestData = [ + 'cartId' => $cartId, + 'address' => $data + ]; + } else { + + $requestData = [ + 'address' => [ + 'country_id' => "US", + 'postcode' => null, + 'region' => null, + 'region_id' => null, + 'extension_attributes' => [ + 'discounts' => [] + ] + ] + ]; + } + + // Cart must be anonymous (see fixture) + $this->assertEmpty($quote->getCustomerId()); + + $result = $this->_webApiCall($serviceInfo, $requestData); + + $this->assertNotEmpty($result); + $this->assertEquals(1, count($result)); + foreach ($result as $rate) { + $this->assertEquals("flatrate", $rate['carrier_code']); + $this->assertEquals(0, $rate['amount']); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php index 1096c0dca6530..c5b06285f1fe1 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderAddressUpdateTest.php @@ -3,13 +3,14 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Sales\Service\V1; use Magento\Sales\Api\Data\OrderAddressInterface as OrderAddress; use Magento\TestFramework\TestCase\WebapiAbstract; /** - * Class OrderAddressUpdateTest + * Test for address update */ class OrderAddressUpdateTest extends WebapiAbstract { @@ -28,7 +29,7 @@ public function testOrderAddressUpdate() $order = $objectManager->get(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); $address = [ - OrderAddress::REGION => 'CA', + OrderAddress::REGION => 'California', OrderAddress::POSTCODE => '11111', OrderAddress::LASTNAME => 'lastname', OrderAddress::STREET => ['street'], @@ -75,7 +76,7 @@ public function testOrderAddressUpdate() $billingAddress = $actualOrder->getBillingAddress(); $validate = [ - OrderAddress::REGION => 'CA', + OrderAddress::REGION => 'California', OrderAddress::POSTCODE => '11111', OrderAddress::LASTNAME => 'lastname', OrderAddress::STREET => 'street', diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php index 021698f874e55..e28cca72e8fb8 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderGetTest.php @@ -76,7 +76,7 @@ public function testOrderGet(): void 'city' => 'Los Angeles', 'email' => 'customer@null.com', 'postcode' => '11111', - 'region' => 'CA' + 'region' => 'California' ]; $result = $this->makeServiceCall(self::ORDER_INCREMENT_ID); diff --git a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php index e5df8c18cda0c..e7ee1acda7982 100644 --- a/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Sales/Service/V1/OrderHoldTest.php @@ -4,27 +4,64 @@ * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Service\V1; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Webapi\Rest\Request; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\TestCase\WebapiAbstract; +/** + * Test for hold order. + */ class OrderHoldTest extends WebapiAbstract { - const SERVICE_VERSION = 'V1'; + private const SERVICE_VERSION = 'V1'; - const SERVICE_NAME = 'salesOrderManagementV1'; + private const SERVICE_NAME = 'salesOrderManagementV1'; + + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + } /** - * @magentoApiDataFixture Magento/Sales/_files/order.php + * Test hold order and check order items product options after. + * + * @magentoApiDataFixture Magento/Sales/_files/order_with_two_configurable_variations.php + * + * @return void */ - public function testOrderHold() + public function testOrderHold(): void { - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $order = $objectManager->get(\Magento\Sales\Model\Order::class)->loadByIncrementId('100000001'); + $order = $this->objectManager->get(Order::class) + ->loadByIncrementId('100000001'); + $orderId = $order->getId(); + $orderItemsProductOptions = $this->getOrderItemsProductOptions($order); + $serviceInfo = [ 'rest' => [ - 'resourcePath' => '/V1/orders/' . $order->getId() . '/hold', - 'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_POST, + 'resourcePath' => '/V1/orders/' . $orderId . '/hold', + 'httpMethod' => Request::HTTP_METHOD_POST, ], 'soap' => [ 'service' => self::SERVICE_NAME, @@ -32,8 +69,29 @@ public function testOrderHold() 'operation' => self::SERVICE_NAME . 'hold', ], ]; - $requestData = ['id' => $order->getId()]; + $requestData = ['id' => $orderId]; $result = $this->_webApiCall($serviceInfo, $requestData); $this->assertTrue($result); + + $this->assertEquals( + $orderItemsProductOptions, + $this->getOrderItemsProductOptions($this->orderRepository->get($orderId)) + ); + } + + /** + * Return order items product options + * + * @param OrderInterface $order + * @return array + */ + private function getOrderItemsProductOptions(OrderInterface $order): array + { + $result = []; + foreach ($order->getItems() as $orderItem) { + $result[] = $orderItem->getProductOptions(); + } + + return $result; } } diff --git a/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php index 6c8d3f90cf65c..8a68e24c8a21c 100644 --- a/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Search/Api/SearchTest.php @@ -24,19 +24,24 @@ class SearchTest extends WebapiAbstract */ private $product; + /** + * @inheritDoc + */ protected function setUp(): void { $productSku = 'simple'; $objectManager = Bootstrap::getObjectManager(); - $productRepository = $objectManager->create(ProductRepositoryInterface::class); + $productRepository = $objectManager->get(ProductRepositoryInterface::class); $this->product = $productRepository->get($productSku); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * Tests that webapi call returns response when search criteria is valid. + * + * @magentoApiDataFixture Magento/Catalog/_files/products.php */ - public function testExistingProductSearch() + public function testExistingProductSearch(): void { $productName = $this->product->getName(); @@ -47,14 +52,16 @@ public function testExistingProductSearch() self::assertArrayHasKey('search_criteria', $response); self::assertArrayHasKey('items', $response); - self::assertGreaterThan(0, count($response['items'])); + self::assertGreaterThan(1, count($response['items'])); self::assertGreaterThan(0, $response['items'][0]['id']); } /** - * @magentoApiDataFixture Magento/Catalog/_files/product_simple.php + * Tests that response is empty if invalid data is provided. + * + * @magentoApiDataFixture Magento/Catalog/_files/products.php */ - public function testNonExistentProductSearch() + public function testNonExistentProductSearch(): void { $searchCriteria = $this->buildSearchCriteria('nonExistentProduct'); $serviceInfo = $this->buildServiceInfo($searchCriteria); diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php new file mode 100644 index 0000000000000..326ec789da45a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class FixturesAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php new file mode 100644 index 0000000000000..e0049895577cc --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +/** + * Test interface for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface FixturesInterface +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php new file mode 100644 index 0000000000000..ca811c222132e --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php @@ -0,0 +1,229 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Config\Model\ConfigStorage; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that fixtures override config inherited from abstract class and interface. + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class FixturesTest extends FixturesAbstractClass implements FixturesInterface +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var ConfigStorage + */ + private $configStorage; + + /** + * @var FixtureCallStorage + */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->configStorage = $this->objectManager->get(ConfigStorage::class); + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoConfigFixture default_store test_section/test_group/field_3 new_value + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * @dataProvider interfaceDataProvider + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testInterfaceInheritance( + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @magentoConfigFixture default_store test_section/test_group/field_2 new_value + * @magentoApiDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @dataProvider abstractDataProvider + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testAbstractInheritance( + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @return array + */ + public function interfaceDataProvider(): array + { + return [ + 'first_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => 'overridden config fixture value for method', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_3' => [ + 'value' => 'new_value', + 'exists_in_db' => true, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 1, + 'fixture3_first_module.php' => 1, + ], + ], + 'second_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => 'overridden config fixture value for method', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_3' => [ + 'value' => '3rd field website scope default value', + 'exists_in_db' => false, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 1, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * @return array + */ + public function abstractDataProvider(): array + { + return [ + 'first_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for class', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => '2nd field default value', + 'exists_in_db' => false, + ], + 'test_section/test_group/field_3' => [ + 'value' => 'overridden config fixture value for data set from abstract', + 'exists_in_db' => true, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 1, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 0, + 'fixture3_first_module.php' => 1, + ], + ], + 'second_data_set' => [ + 'store_configs' => [ + 'test_section/test_group/field_1' => [ + 'value' => 'overridden config fixture value for data set from abstract', + 'exists_in_db' => true, + ], + 'test_section/test_group/field_2' => [ + 'value' => '2nd field default value', + 'exists_in_db' => false, + ], + 'test_section/test_group/field_3' => [ + 'value' => '3rd field website scope default value', + 'exists_in_db' => false, + ], + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 0, + 'fixture2_first_module.php' => 0, + 'fixture1_second_module.php' => 1, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * Asserts config field values. + * + * @param array $configs + * @param string $scope + * @return void + */ + private function assertConfigFieldValues( + array $configs, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ): void { + foreach ($configs as $path => $expected) { + $this->assertEquals($expected['value'], $this->config->getValue($path, $scope, 'default')); + if ($expected['exists_in_db']) { + $this->assertEquals( + $expected['value'], + $this->configStorage->getValueFromDb($path, ScopeInterface::SCOPE_STORES, 'default') + ); + } else { + $this->assertFalse( + $this->configStorage->checkIsRecordExist($path, ScopeInterface::SCOPE_STORES, 'default') + ); + } + } + } + + /** + * Asserts count of used fixtures. + * + * @param array $fixtures + * @return void + */ + private function assertUsedFixturesCount(array $fixtures): void + { + foreach ($fixtures as $fixture => $count) { + $this->assertEquals($count, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php new file mode 100644 index 0000000000000..445aa0c501c0a --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class SkipAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php new file mode 100644 index 0000000000000..99a9332460211 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Test interface for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface SkipInterface +{ + +} diff --git a/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php new file mode 100644 index 0000000000000..e5eb1e3a419f7 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Class checks that test method can be skipped using inherited from abstract class/interface override config + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class SkipTest extends SkipAbstractClass implements SkipInterface +{ + /** + * @return void + */ + public function testAbstractSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from abstract class'); + } + + /** + * @return void + */ + public function testInterfaceSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from interface'); + } + + /** + * @dataProvider skipDataProvider + * + * @param string $message + * @return void + */ + public function testSkipDataSet(string $message): void + { + $this->fail($message); + } + + /** + * @return array + */ + public function skipDataProvider(): array + { + return [ + 'first_data_set' => ['This test should be skipped in data set node inherited from abstract class'], + 'second_data_set' => ['This test should be skipped in data set node inherited from interface'], + ]; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php index 4d89e3a0b582a..5e278e6058dc9 100644 --- a/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/JoinDirectivesTest.php @@ -50,6 +50,7 @@ protected function setUp(): void * * @magentoApiDataFixture Magento/SalesRule/_files/rules_rollback.php * @magentoApiDataFixture Magento/Sales/_files/quote.php + * @magentoAppIsolation enabled */ public function testGetList() { @@ -87,6 +88,7 @@ public function testGetList() /** * @magentoApiDataFixture Magento/Sales/_files/invoice.php + * @magentoAppIsolation enabled */ public function testAutoGeneratedGetList() { @@ -131,6 +133,7 @@ public function testAutoGeneratedGetList() * Test get list of orders with extension attributes. * * @magentoApiDataFixture Magento/Sales/_files/order.php + * @magentoAppIsolation enabled */ public function testGetOrdertList() { diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/RestSessionCookieTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/RestSessionCookieTest.php new file mode 100644 index 0000000000000..36dc7a9afeba0 --- /dev/null +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/RestSessionCookieTest.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Webapi; + +use Magento\Framework\Module\Manager; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Class for RestSessionCookieTest + */ +class RestSessionCookieTest extends \Magento\TestFramework\TestCase\WebapiAbstract +{ + + private $moduleManager; + private $objectManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->moduleManager = $this->objectManager->get(Manager::class); + if ($this->moduleManager->isEnabled('Magento_B2b')) { + $this->markTestSkipped('Skipped, because this logic is rewritten on B2B.'); + } + } + + /** + * Check for non exist cookie PHPSESSID + */ + public function testRestSessionNoCookie() + { + $this->_markTestAsRestOnly(); + /** @var $curlClient CurlClientWithCookies */ + + $curlClient = $this->objectManager + ->get(\Magento\TestFramework\TestCase\HttpClient\CurlClientWithCookies::class); + $phpSessionCookieName = + [ + 'cookie_name' => 'PHPSESSID', + ]; + + $response = $curlClient->get('/rest/V1/directory/countries', []); + + $cookie = $this->findCookie($phpSessionCookieName['cookie_name'], $response['cookies']); + $this->assertNull($cookie); + } + + /** + * Find cookie with given name in the list of cookies + * + * @param string $cookieName + * @param array $cookies + * @return $cookie|null + * @SuppressWarnings(PHPMD.UnusedLocalVariable) + */ + private function findCookie($cookieName, $cookies) + { + foreach ($cookies as $cookieIndex => $cookie) { + if ($cookie['name'] === $cookieName) { + return $cookie; + } + } + return null; + } +} diff --git a/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php b/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php index dadc2caef7a13..c43cb81683aac 100644 --- a/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php +++ b/dev/tests/api-functional/testsuite/Magento/Webapi/WsdlGenerationFromDataObjectTest.php @@ -116,7 +116,7 @@ protected function _getWsdlContent($wsdlUrl) $responseDom->loadXML($responseContent), "Valid XML is always expected as a response for WSDL request." ); - return $responseContent; + return $responseDom->saveXML(); } /** @@ -207,7 +207,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="id" minOccurs="1" maxOccurs="1" type="xsd:int"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:min/> <inf:max/> @@ -231,7 +231,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="entityId" minOccurs="1" maxOccurs="1" type="xsd:int"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:min/> <inf:max/> @@ -266,7 +266,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="result" minOccurs="1" maxOccurs="1" type="tns:TestModule5V2EntityAllSoapAndRest"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:callInfo> <inf:callName>testModule5AllSoapAndRestV2Item</inf:callName> @@ -290,7 +290,7 @@ protected function _checkComplexTypesDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="result" minOccurs="1" maxOccurs="1" type="tns:TestModule5V1EntityAllSoapAndRest"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:callInfo> <inf:callName>testModule5AllSoapAndRestV1Item</inf:callName> @@ -331,7 +331,7 @@ protected function _checkReferencedTypeDeclaration($wsdlContent) <xsd:sequence> <xsd:element name="price" minOccurs="1" maxOccurs="1" type="xsd:int"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:min/> <inf:max/> @@ -835,7 +835,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) <xsd:sequence> <xsd:element name="key" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:maxLength/> </xsd:appinfo> @@ -843,7 +843,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) </xsd:element> <xsd:element name="value" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_soapUrl}"> <inf:maxLength/> </xsd:appinfo> @@ -865,7 +865,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) <xsd:sequence> <xsd:element name="message" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_baseUrl}/soap/{$this->_storeCode}?services=testModule5AllSoapAndRestV2"> <inf:maxLength/> </xsd:appinfo> @@ -888,7 +888,7 @@ protected function _checkFaultsComplexTypeSection($wsdlContent) <xsd:sequence> <xsd:element name="message" minOccurs="1" maxOccurs="1" type="xsd:string"> <xsd:annotation> - <xsd:documentation></xsd:documentation> + <xsd:documentation/> <xsd:appinfo xmlns:inf="{$this->_baseUrl}/soap/{$this->_storeCode}?services=testModule5AllSoapAndRestV1%2CtestModule5AllSoapAndRestV2"> <inf:maxLength/> </xsd:appinfo> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml index e9eb1fe21aa4f..7c5d66427aecb 100644 --- a/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml +++ b/dev/tests/integration/_files/Magento/TestModuleCspConfig/etc/csp_whitelist.xml @@ -11,6 +11,7 @@ <values> <value id="mage-base" type="host">https://magento.com</value> <value id="hash" type="hash" algorithm="sha256">B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=</value> + <value id="hash2" type="hash" algorithm="sha256">B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF9=</value> </values> </policy> <policy id="media-src"> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Aware.php b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Aware.php new file mode 100644 index 0000000000000..567c308330ba3 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Aware.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleCspUtil\Controller\Csp; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\App\Action\Action; +use Magento\Framework\Controller\ResultFactory; + +/** + * CSP Aware controller. + */ +class Aware extends Action implements CspAwareActionInterface +{ + /** + * @inheritDoc + */ + public function execute() + { + return $this->resultFactory->create(ResultFactory::TYPE_PAGE); + } + + /** + * @inheritDoc + */ + public function modifyCsp(array $appliedPolicies): array + { + $policies = []; + foreach ($appliedPolicies as $policy) { + if ($policy instanceof FetchPolicy + && in_array('http://controller.magento.com', $policy->getHostSources(), true) + ) { + $policies[] = new FetchPolicy( + 'script-src', + false, + ['https://controller.magento.com'], + [], + true, + false, + false, + [], + ['H4RRnauTM2X2Xg/z9zkno1crqhsaY3uKKu97uwmnXXE=' => 'sha256'] + ); + } + } + + return $policies; + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Helper.php b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Helper.php new file mode 100644 index 0000000000000..8dde67de73dfa --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/Controller/Csp/Helper.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleCspUtil\Controller\Csp; + +use Magento\Framework\App\Action\Action; +use Magento\Framework\Controller\ResultFactory; +use Magento\Framework\View\Result\PageFactory; + +/** + * .phtml templates utilizes CSP helper. + */ +class Helper extends Action +{ + /** + * @inheritDoc + */ + public function execute() + { + return $this->resultFactory->create(ResultFactory::TYPE_PAGE); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/composer.json b/dev/tests/integration/_files/Magento/TestModuleCspUtil/composer.json new file mode 100644 index 0000000000000..aece82306d9d5 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-csp-util", + "description": "test csp module", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleCspUtil" + ] + ] + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/csp_whitelist.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/csp_whitelist.xml new file mode 100644 index 0000000000000..c39e74edafd5e --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/csp_whitelist.xml @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp/etc/csp_whitelist.xsd"> + <policies> + <policy id="script-src"> + <values> + <value id="devdocs-base" type="host">https://devdocs.magento.com</value> + </values> + </policy> + </policies> +</csp_whitelist> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/frontend/routes.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/frontend/routes.xml new file mode 100644 index 0000000000000..f78ddb740ec6c --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/frontend/routes.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="standard"> + <route id="csputil" frontName="csputil"> + <module name="Magento_TestModuleCspUtil" /> + </route> + </router> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/module.xml new file mode 100644 index 0000000000000..1e9bc9b6fa9bc --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleCspUtil" active="true" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/registration.php b/dev/tests/integration/_files/Magento/TestModuleCspUtil/registration.php new file mode 100644 index 0000000000000..570aed9fe5ce6 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleCspUtil') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleCspUtil', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_aware.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_aware.xml new file mode 100644 index 0000000000000..89550403fa6bf --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_aware.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <block class="Magento\Framework\View\Element\Template" + name="csp_helper" + cacheable="false" + template="Magento_TestModuleCspUtil::helper.phtml" /> + </referenceContainer> + </body> +</page> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_helper.xml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_helper.xml new file mode 100644 index 0000000000000..89550403fa6bf --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/layout/csputil_csp_helper.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <block class="Magento\Framework\View\Element\Template" + name="csp_helper" + cacheable="false" + template="Magento_TestModuleCspUtil::helper.phtml" /> + </referenceContainer> + </body> +</page> diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/helper.phtml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/helper.phtml new file mode 100644 index 0000000000000..fa349062aafcb --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/helper.phtml @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Csp\Api\InlineUtilInterface $csp */ +?> +<h1>Hello there!</h1> +<?= /* @noEscape */ $csp->renderTag('script', ['src' => 'http://my.magento.com/static/script.js']); ?> +<?= /* @noEscape */ $csp->renderTag('script', [], "\n let myVar = 1;\n") ?> + diff --git a/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/secure.phtml b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/secure.phtml new file mode 100644 index 0000000000000..f3ed9365d18b4 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleCspUtil/view/frontend/templates/secure.phtml @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> +<?= /* @NoEscape */ $secureRenderer->renderTag('script', ['type' => 'text/javascript'], 'let var = 1; console.log("var = " + var);', false) ?> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml index 8c0badac4b1d1..c0873b9968132 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/adminhtml/system.xml @@ -12,6 +12,8 @@ <field id="field_1" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> <field id="field_2" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> <field id="field_3" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_4" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> + <field id="field_5" translate="label" type="text" sortOrder="10" showInDefault="1" showInWebsite="1" showInStore="1"/> </group> </section> </system> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml index 3b2f2a1ddde1e..8604428274194 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig/etc/config.xml @@ -12,6 +12,8 @@ <field_1>1st field default value</field_1> <field_2>2nd field default value</field_2> <field_3>3rd field default value</field_3> + <field_4>4th field default value</field_4> + <field_5>5th field default value</field_5> </test_group> </test_section> </default> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml index 45bc6115e3704..aadddfcd2827a 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig2/Test/Integration/_files/overrides.xml @@ -204,4 +204,9 @@ <dataSet name="first_data_set" skip="true"/> </method> </test> + <global> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php" /> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_4" value="4th field globally overridden value"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_5" newValue="5th field globally replaced value"/> + </global> </overrides> diff --git a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml index bbb3fad88e5cd..45c45a79eeafa 100644 --- a/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml +++ b/dev/tests/integration/_files/Magento/TestModuleOverrideConfig3/Test/Integration/_files/overrides.xml @@ -53,4 +53,58 @@ </dataSet> </method> </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesInterface"> + <magentoAdminConfigFixture path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_1" value="overridden config fixture value for class"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php"/> + <method name="testInterfaceInheritance"> + <magentoAdminConfigFixture path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_2" newValue="overridden config fixture value for method"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture2_second_module_rollback.php" /> + <dataSet name="second_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_3" remove="true"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_3" remove="true"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php" remove="true"/> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Fixtures\FixturesAbstractClass"> + <method name="testAbstractInheritance"> + <magentoAdminConfigFixture path="test_section/test_group/field_2" remove="true"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_2" remove="true"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php" remove="true"/> + <dataSet name="first_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_3" value="overridden config fixture value for data set from abstract"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php"/> + </dataSet> + <dataSet name="second_data_set"> + <magentoAdminConfigFixture path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoConfigFixture scopeType="store" scopeCode="current" path="test_section/test_group/field_1" newValue="overridden config fixture value for data set from abstract"/> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoDataFixture path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module.php" /> + <magentoDataFixtureBeforeTransaction path="Magento/TestModuleOverrideConfig/_files/fixture1_first_module_rollback.php" newPath="Magento/TestModuleOverrideConfig2/_files/fixture1_second_module_rollback.php" /> + </dataSet> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipAbstractClass"> + <method name="testAbstractSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="first_data_set" skip="true"/> + </method> + </test> + <test class="Magento\TestModuleOverrideConfig\Inheritance\Skip\SkipInterface"> + <method name="testInterfaceSkip" skip="true"/> + <method name="testSkipDataSet"> + <dataSet name="second_data_set" skip="true"/> + </method> + </test> </overrides> diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/Controller/Secure/Helper.php b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/Controller/Secure/Helper.php new file mode 100644 index 0000000000000..4657f10374514 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/Controller/Secure/Helper.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleSecureHtmlRenderer\Controller\Secure; + +use Magento\Framework\App\Action\Action; +use Magento\Framework\Controller\ResultFactory; + +/** + * .phtml template utilizing secure-html helper. + */ +class Helper extends Action +{ + /** + * @inheritDoc + */ + public function execute() + { + return $this->resultFactory->create(ResultFactory::TYPE_PAGE); + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/composer.json b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/composer.json new file mode 100644 index 0000000000000..316d3c8428d08 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/composer.json @@ -0,0 +1,21 @@ +{ + "name": "magento/module-secure-html-renderer", + "description": "test secure html renderer module", + "config": { + "sort-packages": true + }, + "require": { + "php": "~7.1.3||~7.2.0||~7.3.0", + "magento/framework": "*", + "magento/module-integration": "*" + }, + "type": "magento2-module", + "extra": { + "map": [ + [ + "*", + "Magento/TestModuleSecureHtmlRenderer" + ] + ] + } +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/frontend/routes.xml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/frontend/routes.xml new file mode 100644 index 0000000000000..8cfe6080149b8 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/frontend/routes.xml @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd"> + <router id="standard"> + <route id="securehtml" frontName="securehtml"> + <module name="Magento_TestModuleSecureHtmlRenderer" /> + </route> + </router> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/module.xml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/module.xml new file mode 100644 index 0000000000000..653ce176d4e0e --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/etc/module.xml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd"> + <module name="Magento_TestModuleSecureHtmlRenderer" active="true" /> +</config> diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/registration.php b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/registration.php new file mode 100644 index 0000000000000..4fff392257f8a --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/registration.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Framework\Component\ComponentRegistrar; + +$registrar = new ComponentRegistrar(); +if ($registrar->getPath(ComponentRegistrar::MODULE, 'Magento_TestModuleSecureHtmlRenderer') === null) { + ComponentRegistrar::register(ComponentRegistrar::MODULE, 'Magento_TestModuleSecureHtmlRenderer', __DIR__); +} diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/layout/securehtml_secure_helper.xml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/layout/securehtml_secure_helper.xml new file mode 100644 index 0000000000000..0ea70a15a3299 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/layout/securehtml_secure_helper.xml @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd"> + <body> + <referenceContainer name="content"> + <block class="Magento\Framework\View\Element\Template" + name="secure_helper" + cacheable="false" + template="Magento_TestModuleSecureHtmlRenderer::helper.phtml" /> + </referenceContainer> + </body> +</page> diff --git a/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/templates/helper.phtml b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/templates/helper.phtml new file mode 100644 index 0000000000000..a7a5eb9555cc3 --- /dev/null +++ b/dev/tests/integration/_files/Magento/TestModuleSecureHtmlRenderer/view/frontend/templates/helper.phtml @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\View\Helper\SecureHtmlRenderer $secureRenderer */ +?> +<h1 <?= /* @noEscape */$secureRenderer->renderEventListener('onclick', 'alert()') ?>>Hello there!</h1> +<?= /* @noEscape */ $secureRenderer->renderTag('script', ['src' => 'http://my.magento.com/static/script.js'], false); ?> +<?= /* @noEscape */ $secureRenderer->renderTag('script', [], "\n let myVar = 1;\n", false) ?> +<?= /* @noEscape */ $secureRenderer->renderTag('div', [], 'I am just <a> text', true) ?> + diff --git a/dev/tests/integration/bin/magento b/dev/tests/integration/bin/magento index 303fbfb217d2b..8226f5c711708 100755 --- a/dev/tests/integration/bin/magento +++ b/dev/tests/integration/bin/magento @@ -5,6 +5,9 @@ * See COPYING.txt for license details. */ +use Magento\Framework\Console\Cli; +use Magento\TestFramework\Console\CliProxy; + if (PHP_SAPI !== 'cli') { echo 'bin/magento must be run as a CLI application'; exit(1); @@ -21,7 +24,8 @@ if (isset($_SERVER['INTEGRATION_TEST_PARAMS'])) { } try { - require $_SERVER['MAGE_DIRS']['base']['path'] . '/app/bootstrap.php'; + require $_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER'] ?? + ($_SERVER['MAGE_DIRS']['base']['path'] . '/app/bootstrap.php'); } catch (\Exception $e) { echo 'Autoload error: ' . $e->getMessage(); exit(1); @@ -29,7 +33,11 @@ try { try { $handler = new \Magento\Framework\App\ErrorHandler(); set_error_handler([$handler, 'handler']); - $application = new Magento\Framework\Console\Cli('Magento CLI'); + if (isset($_SERVER['INTEGRATION_TESTS_CLI_AUTOLOADER'])) { + $application = new CliProxy('Magento CLI'); + } else { + $application = new Cli('Magento CLI'); + } $application->run(); } catch (\Exception $e) { while ($e) { diff --git a/dev/tests/integration/etc/di/preferences/ce.php b/dev/tests/integration/etc/di/preferences/ce.php index 68de1cc009d68..9374fb4dfe5df 100644 --- a/dev/tests/integration/etc/di/preferences/ce.php +++ b/dev/tests/integration/etc/di/preferences/ce.php @@ -30,11 +30,5 @@ \Magento\Framework\Lock\Backend\Database::class => \Magento\TestFramework\Lock\Backend\DummyLocker::class, \Magento\Framework\Session\SessionStartChecker::class => \Magento\TestFramework\Session\SessionStartChecker::class, - \Magento\Framework\HTTP\AsyncClientInterface::class => \Magento\TestFramework\HTTP\AsyncClientInterfaceMock::class, - \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class => - \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class, - \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => - \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class, - \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => - \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + \Magento\Framework\HTTP\AsyncClientInterface::class => \Magento\TestFramework\HTTP\AsyncClientInterfaceMock::class ]; diff --git a/dev/tests/integration/etc/install-config-mysql.travis.php.dist b/dev/tests/integration/etc/install-config-mysql.travis.php.dist deleted file mode 100644 index 8c41b0a0f2626..0000000000000 --- a/dev/tests/integration/etc/install-config-mysql.travis.php.dist +++ /dev/null @@ -1,23 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -return [ - 'db-host' => '127.0.0.1', - 'db-user' => 'root', - 'db-password' => '', - 'db-name' => 'magento_integration_tests', - 'db-prefix' => 'trv_', - 'backend-frontname' => 'backend', - 'admin-user' => \Magento\TestFramework\Bootstrap::ADMIN_NAME, - 'admin-password' => \Magento\TestFramework\Bootstrap::ADMIN_PASSWORD, - 'admin-email' => \Magento\TestFramework\Bootstrap::ADMIN_EMAIL, - 'admin-firstname' => \Magento\TestFramework\Bootstrap::ADMIN_FIRSTNAME, - 'admin-lastname' => \Magento\TestFramework\Bootstrap::ADMIN_LASTNAME, - 'amqp-host' => 'localhost', - 'amqp-port' => '5672', - 'amqp-user' => 'guest', - 'amqp-password' => 'guest', -]; diff --git a/dev/tests/integration/framework/Magento/TestFramework/Application.php b/dev/tests/integration/framework/Magento/TestFramework/Application.php index f0ce2e24545eb..62f1101bae847 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Application.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Application.php @@ -704,6 +704,7 @@ protected function getCustomDirs() $customDirs = [ DirectoryList::CONFIG => [$path => "{$this->installDir}/etc"], DirectoryList::VAR_DIR => [$path => $var], + DirectoryList::VAR_EXPORT => [$path => "{$var}/export"], DirectoryList::MEDIA => [$path => "{$this->installDir}/pub/media"], DirectoryList::STATIC_VIEW => [$path => "{$this->installDir}/pub/static"], DirectoryList::TMP_MATERIALIZATION_DIR => [$path => "{$var}/view_preprocessed/pub/static"], diff --git a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php index d2aa20a005ec4..35f449a404410 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/DataProvider/Type/File.php @@ -8,7 +8,7 @@ namespace Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type; use Magento\Catalog\Api\Data\ProductCustomOptionInterface; -use Magento\Catalog\Model\Product\Option; +use Magento\TestFramework\Catalog\Model\Product\Option\DataProvider\Type\AbstractBase; /** * Data provider for options from file group with type "file". @@ -20,44 +20,41 @@ class File extends AbstractBase */ public function getDataForCreateOptions(): array { - return $this->injectFileExtension( - array_merge_recursive( - parent::getDataForCreateOptions(), - [ - "type_{$this->getType()}_option_file_extension" => [ - [ - 'record_id' => 0, - 'sort_order' => 1, - 'is_require' => 1, - 'sku' => 'test-option-title-1', - 'max_characters' => 30, - 'title' => 'Test option title 1', - 'type' => $this->getType(), - 'price' => 10, - 'price_type' => 'fixed', - 'file_extension' => 'gif', - 'image_size_x' => 10, - 'image_size_y' => 20, - ], + return array_merge_recursive( + parent::getDataForCreateOptions(), + [ + "type_{$this->getType()}_option_file_extension" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'max_characters' => 30, + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + 'file_extension' => 'gif', + 'image_size_x' => 10, + 'image_size_y' => 20, ], - "type_{$this->getType()}_option_maximum_file_size" => [ - [ - 'record_id' => 0, - 'sort_order' => 1, - 'is_require' => 1, - 'sku' => 'test-option-title-1', - 'title' => 'Test option title 1', - 'type' => $this->getType(), - 'price' => 10, - 'price_type' => 'fixed', - 'file_extension' => 'gif', - 'image_size_x' => 10, - 'image_size_y' => 20, - ], + ], + "type_{$this->getType()}_option_maximum_file_size" => [ + [ + 'record_id' => 0, + 'sort_order' => 1, + 'is_require' => 1, + 'sku' => 'test-option-title-1', + 'title' => 'Test option title 1', + 'type' => $this->getType(), + 'price' => 10, + 'price_type' => 'fixed', + 'file_extension' => 'gif', + 'image_size_x' => 10, + 'image_size_y' => 20, ], - ] - ), - 'png' + ], + ] ); } @@ -66,24 +63,21 @@ public function getDataForCreateOptions(): array */ public function getDataForUpdateOptions(): array { - return $this->injectFileExtension( - array_merge_recursive( - parent::getDataForUpdateOptions(), - [ - "type_{$this->getType()}_option_file_extension" => [ - [ - 'file_extension' => 'jpg', - ], + return array_merge_recursive( + parent::getDataForUpdateOptions(), + [ + "type_{$this->getType()}_option_file_extension" => [ + [ + 'file_extension' => 'jpg', ], - "type_{$this->getType()}_option_maximum_file_size" => [ - [ - 'image_size_x' => 300, - 'image_size_y' => 815, - ], + ], + "type_{$this->getType()}_option_maximum_file_size" => [ + [ + 'image_size_x' => 300, + 'image_size_y' => 815, ], - ] - ), - '' + ], + ] ); } @@ -94,24 +88,4 @@ protected function getType(): string { return ProductCustomOptionInterface::OPTION_TYPE_FILE; } - - /** - * Add 'file_extension' value to each option. - * - * @param array $data - * @param string $extension - * @return array - */ - private function injectFileExtension(array $data, string $extension): array - { - foreach ($data as &$caseData) { - foreach ($caseData as &$option) { - if (!isset($option[Option::KEY_FILE_EXTENSION])) { - $option[Option::KEY_FILE_EXTENSION] = $extension; - } - } - } - - return $data; - } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/Type/File/ValidatorFileMock.php similarity index 84% rename from dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php rename to dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/Type/File/ValidatorFileMock.php index 9b5650b1826c3..b7df1205f2ba3 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/ValidatorFileMock.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Catalog/Model/Product/Option/Type/File/ValidatorFileMock.php @@ -5,19 +5,22 @@ */ declare(strict_types=1); -namespace Magento\Checkout\_files; +namespace Magento\TestFramework\Catalog\Model\Product\Option\Type\File; use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** * Creates mock for ValidatorFile to replace real instance in fixtures. */ -class ValidatorFileMock extends \PHPUnit\Framework\TestCase +class ValidatorFileMock extends TestCase { /** * Returns mock. + * * @param array|null $fileData - * @return ValidatorFile|\PHPUnit_Framework_MockObject_MockObject + * @return ValidatorFile|MockObject */ public function getInstance($fileData = null) { diff --git a/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php new file mode 100644 index 0000000000000..497f234dfa84b --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Console/CliProxy.php @@ -0,0 +1,126 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Console; + +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * Provides the ability to inject additional DI configuration to call a CLI command + */ +class CliProxy implements \Magento\Framework\ObjectManager\NoninterceptableInterface +{ + /** + * @var Cli + */ + private $subject; + + /** + * @param string $name + * @param string $version + * @throws \ReflectionException + * @throws LocalizedException + */ + public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') + { + $this->subject = new Cli($name, $version); + $this->injectDiConfiguration($this->subject); + } + + /** + * Runs the current application. + * + * @see \Magento\Framework\Console\Cli::doRun + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null + * @throws \Exception + */ + public function doRun(InputInterface $input, OutputInterface $output) + { + return $this->getSubject()->doRun($input, $output); + } + + /** + * Runs the current application. + * + * @see \Symfony\Component\Console\Application::run + * @param InputInterface|null $input + * @param OutputInterface|null $output + * @return int + * @throws \Exception + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + return $this->getSubject()->run($input, $output); + } + + /** + * Get subject + * + * @return Cli + */ + private function getSubject(): Cli + { + return $this->subject; + } + + /** + * Inject additional DI configuration + * + * @param Cli $cli + * @return bool + * @throws LocalizedException + * @throws \ReflectionException + */ + private function injectDiConfiguration(Cli $cli): bool + { + $diPreferences = $this->getDiPreferences(); + if ($diPreferences) { + $object = new \ReflectionObject($cli); + + $attribute = $object->getProperty('objectManager'); + $attribute->setAccessible(true); + + /** @var ObjectManagerInterface $objectManager */ + $objectManager = $attribute->getValue($cli); + $objectManager->configure($diPreferences); + + $attribute->setAccessible(false); + } + + return true; + } + + /** + * Get additional DI preferences + * + * @return array|array[] + * @throws LocalizedException + * @SuppressWarnings(PHPMD.Superglobals) + */ + private function getDiPreferences(): array + { + $diPreferences = []; + $diPreferencesPath = $_SERVER['TESTS_BASE_DIR'] . '/etc/di/preferences/cli/'; + + $preferenceFiles = glob($diPreferencesPath . '*.php'); + + foreach ($preferenceFiles as $file) { + if (!is_readable($file)) { + throw new LocalizedException(__("'%1' is not readable file.", $file)); + } + $diPreferences = array_replace($diPreferences, include $file); + } + + return $diPreferences ? ['preferences' => $diPreferences] : $diPreferences; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Interception/PluginList.php b/dev/tests/integration/framework/Magento/TestFramework/Interception/PluginList.php index 88c9086f8270b..0b5fc407d438b 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Interception/PluginList.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Interception/PluginList.php @@ -5,6 +5,8 @@ */ namespace Magento\TestFramework\Interception; +use Magento\Framework\Interception\ConfigLoaderInterface; +use Magento\Framework\Interception\PluginListGenerator; use Magento\Framework\Serialize\SerializerInterface; /** @@ -31,6 +33,8 @@ class PluginList extends \Magento\Framework\Interception\PluginList\PluginList * @param array $scopePriorityScheme * @param string|null $cacheId * @param SerializerInterface|null $serializer + * @param ConfigLoaderInterface|null $configLoader + * @param PluginListGenerator|null $pluginListGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -44,7 +48,9 @@ public function __construct( \Magento\Framework\ObjectManager\DefinitionInterface $classDefinitions, array $scopePriorityScheme, $cacheId = 'plugins', - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ConfigLoaderInterface $configLoader = null, + PluginListGenerator $pluginListGenerator = null ) { parent::__construct( $reader, @@ -57,7 +63,9 @@ public function __construct( $classDefinitions, $scopePriorityScheme, $cacheId, - $serializer + $serializer, + $configLoader, + $pluginListGenerator ); $this->_originScopeScheme = $this->_scopePriorityScheme; } diff --git a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php index bf8ca0dc51b18..fff16a7edc1ba 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php +++ b/dev/tests/integration/framework/Magento/TestFramework/TestCase/AbstractBackendController.php @@ -58,7 +58,14 @@ protected function setUp(): void parent::setUp(); $this->_objectManager->get(\Magento\Backend\Model\UrlInterface::class)->turnOffSecretKey(); - + /** + * Authorization can be created on test bootstrap... + * If it will be created on test bootstrap we will have invalid RoleLocator object. + * As tests by default are run not from adminhtml area... + */ + \Magento\TestFramework\ObjectManager::getInstance()->removeSharedInstance( + \Magento\Framework\Authorization::class + ); $this->_auth = $this->_objectManager->get(\Magento\Backend\Model\Auth::class); $this->_session = $this->_auth->getAuthStorage(); $credentials = $this->_getAdminCredentials(); diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php index 73786707b417b..aed6c53c22702 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Cleanup/StaticProperties.php @@ -46,6 +46,8 @@ class StaticProperties \Magento\TestFramework\Annotation\AppIsolation::class, \Magento\TestFramework\Workaround\Cleanup\StaticProperties::class, \Magento\Framework\Phrase::class, + \Magento\TestFramework\Workaround\Override\Fixture\ResolverInterface::class, + \Magento\TestFramework\Workaround\Override\ConfigInterface::class, ]; private const CACHE_NAME = 'integration_test_static_properties'; @@ -79,7 +81,7 @@ public function __construct() */ protected static function _isClassCleanable(\ReflectionClass $reflectionClass) { - // do not process blacklisted classes from integration framework + // do not process skipped classes from integration framework foreach (self::$_classesToSkip as $notCleanableClass) { if ($reflectionClass->getName() == $notCleanableClass || is_subclass_of( $reflectionClass->getName(), diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php index 4a0ad01a909e3..f34eec274873d 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config.php @@ -15,10 +15,15 @@ use Magento\Framework\View\File\Collector\Decorator\ModuleDependency; use Magento\Framework\View\File\Collector\Decorator\ModuleOutput; use Magento\Framework\View\File\CollectorInterface; +use Magento\TestFramework\Annotation\AdminConfigFixture; +use Magento\TestFramework\Annotation\ConfigFixture; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Annotation\DataFixtureBeforeTransaction; use Magento\TestFramework\Workaround\Override\Config\Converter; use Magento\TestFramework\Workaround\Override\Config\Dom; use Magento\TestFramework\Workaround\Override\Config\FileCollector; use Magento\TestFramework\Workaround\Override\Config\FileResolver; +use Magento\TestFramework\Workaround\Override\Config\RelationsCollector; use Magento\TestFramework\Workaround\Override\Config\SchemaLocator; use Magento\TestFramework\Workaround\Override\Config\ValidationState; use PHPUnit\Framework\TestCase; @@ -31,7 +36,17 @@ class Config implements ConfigInterface { /** - * @var self + * List of allowed fixture types + */ + protected const FIXTURE_TYPES = [ + DataFixture::ANNOTATION, + DataFixtureBeforeTransaction::ANNOTATION, + ConfigFixture::ANNOTATION, + AdminConfigFixture::ANNOTATION, + ]; + + /** + * @var ConfigInterface */ private static $instance; @@ -40,6 +55,11 @@ class Config implements ConfigInterface */ private $config; + /** + * @var array + */ + private $inheritedConfig; + /** * Self instance getter. * @@ -54,6 +74,22 @@ public static function getInstance(): ConfigInterface return self::$instance; } + /** + * Get config from global node + * + * @param string|null $fixtureType + * @return array + */ + public function getGlobalConfig(?string $fixtureType = null): array + { + $result = $this->config['global'] ?? []; + if ($fixtureType) { + $result = $result[$fixtureType] ?? []; + } + + return $result; + } + /** * Self instance setter. * @@ -126,7 +162,7 @@ public function getSkipConfiguration(TestCase $test): array */ public function hasSkippedTest(string $className): bool { - $classConfig = $this->config[$className] ?? []; + $classConfig = $this->getInheritedClassConfig($className); return $this->isSkippedByConfig($classConfig); } @@ -136,12 +172,11 @@ public function hasSkippedTest(string $className): bool */ public function getClassConfig(TestCase $test, ?string $fixtureType = null): array { - $result = $this->config[$this->getOriginalClassName($test)] ?? []; - if ($fixtureType) { - $result = $result[$fixtureType] ?? []; - } + $config = $this->getInheritedClassConfig($this->getOriginalClassName($test)); - return $result; + return $fixtureType + ? $config[$fixtureType] ?? [] + : $config; } /** @@ -223,7 +258,7 @@ protected function getValidationState(): ValidationStateInterface */ protected function getConverter(): ConverterInterface { - return ObjectManager::getInstance()->create(Converter::class); + return ObjectManager::getInstance()->create(Converter::class, ['types' => $this::FIXTURE_TYPES]); } /** @@ -295,4 +330,70 @@ private function prepareSkipConfig(array $config): array 'skipMessage' => $config['skipMessage'] ?: 'Skipped according to override configurations', ]; } + + /** + * Returns class relation collector. + * + * @return RelationsCollector + */ + private function getRelationsCollector(): RelationsCollector + { + return ObjectManager::getInstance()->get(RelationsCollector::class); + } + + /** + * Returns config for test including config from parents. + * + * @param string $originalClassName + * @return array + */ + private function getInheritedClassConfig(string $originalClassName): array + { + if (empty($this->inheritedConfig[$originalClassName])) { + $classConfig = $this->config[$originalClassName] ?? []; + foreach ($this->getRelationsCollector()->getParents($originalClassName) as $parent) { + $parentConfig = $this->config[$parent] ?? []; + $classConfig = $this->mergeConfiguration($classConfig, $parentConfig); + } + $this->inheritedConfig[$originalClassName] = $classConfig; + } + + return $this->inheritedConfig[$originalClassName]; + } + + /** + * Merges test configurations. + * + * @param array $mainConfig + * @param array $parentConfig + * @return array + */ + private function mergeConfiguration(array $mainConfig, array $parentConfig): array + { + $merged = $mainConfig; + + foreach ($parentConfig as $key => &$value) { + if (is_array($value)) { + $merged[$key] = $merged[$key] ?? []; + if (in_array($key, $this::FIXTURE_TYPES, true)) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $merged[$key] = array_merge($merged[$key], $value); + } else { + $merged[$key] = $this->mergeConfiguration($merged[$key], $value); + } + } elseif ($key === 'skip') { + $merged['skip_from_config'] = $merged['skip_from_config'] ?? false; + $merged['skip'] = $merged['skip'] ?? false; + $merged['skipMessage'] = $merged['skipMessage'] ?? null; + + if (!$merged['skip_from_config'] && $parentConfig['skip_from_config']) { + $merged[$key] = $value; + $merged['skipMessage'] = $parentConfig['skipMessage']; + $merged['skip_from_config'] = $parentConfig['skip_from_config']; + } + } + } + + return $merged; + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php index 22d88279e8a9a..3f4b4687da793 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/Converter.php @@ -18,12 +18,18 @@ */ class Converter implements ConverterInterface { - protected const FIXTURE_TYPES = [ - DataFixture::ANNOTATION, - DataFixtureBeforeTransaction::ANNOTATION, - ConfigFixture::ANNOTATION, - AdminConfigFixture::ANNOTATION, - ]; + /** + * @var array + */ + private $supportedFixtureTypes; + + /** + * @param array $types + */ + public function __construct(array $types = []) + { + $this->supportedFixtureTypes = $types; + } /** @var \DOMXPath */ private $xpath; @@ -34,7 +40,7 @@ class Converter implements ConverterInterface public function convert($source) { $this->xpath = new \DOMXPath($source); - $config = []; + $config = $this->getGlobalConfig($this->xpath); foreach ($this->xpath->query('//test') as $testOverride) { $className = ltrim($testOverride->getAttribute('class'), '\\'); $config[$className] = $this->getTestConfigByFixtureType($testOverride); @@ -67,6 +73,7 @@ public function convert($source) */ private function fillSkipSection(\DOMElement $node, array $config): array { + $config['skip_from_config'] = !empty($node->getAttribute('skip')); $config['skip'] = $node->getAttribute('skip') === 'true'; $config['skipMessage'] = $node->getAttribute('skipMessage') ?: null; @@ -81,7 +88,7 @@ private function fillSkipSection(\DOMElement $node, array $config): array */ private function getTestConfigByFixtureType(\DOMElement $node): array { - foreach ($this::FIXTURE_TYPES as $fixtureType) { + foreach ($this->supportedFixtureTypes as $fixtureType) { $currentTestNodePath = sprintf("//test[@class ='%s']/%s", $node->getAttribute('class'), $fixtureType); foreach ($this->xpath->query($currentTestNodePath) as $classDataFixture) { $config[$fixtureType][] = $this->fillAttributes($classDataFixture); @@ -182,4 +189,36 @@ protected function fillAdminConfigFixtureAttributes(\DOMElement $fixture): array 'remove' => $fixture->getAttribute('remove'), ]; } + /** + * Get global configurations + * + * @param \DOMXPath $xpath + * @return array + */ + private function getGlobalConfig(\DOMXPath $xpath): array + { + foreach ($xpath->query('//global') as $globalOverride) { + $config = $this->fillGlobalConfigByFixtureType($globalOverride); + } + + return $config ?? []; + } + + /** + * Fill global configurations node + * + * @param \DOMElement $node + * @return array + */ + private function fillGlobalConfigByFixtureType(\DOMElement $node): array + { + $config = []; + foreach ($this->supportedFixtureTypes as $fixtureType) { + foreach ($node->getElementsByTagName($fixtureType) as $fixture) { + $config['global'][$fixtureType][] = $this->fillAttributes($fixture); + } + } + + return $config; + } } diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php new file mode 100644 index 0000000000000..2a17e7dba4904 --- /dev/null +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Config/RelationsCollector.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Workaround\Override\Config; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\ObjectManager\Relations\Runtime; +use Magento\Framework\ObjectManager\RelationsInterface; +use PHPUnit\Framework\TestCase; + +/** + * Class collects test class parents and interfaces. + */ +class RelationsCollector +{ + /** + * @var RelationsInterface + */ + private $relations; + + /** + * @var array + */ + private $internalParents = []; + + /** + * Returns filtered list of parent classes and interfaces for given class name. + * + * @param string $className + * @return array + */ + public function getParents(string $className): array + { + return array_diff($this->getRelations($className), $this->getInternalParents()); + } + + /** + * Returns list of parent classes and interfaces for given class name. + * + * @param string $className + * @return array + */ + private function getRelations(string $className): array + { + $result = $this->getRelationsReader()->getParents($className); + + foreach ($result as $parent) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge + $result = array_merge($result, $this->getRelations($parent)); + } + + return $result; + } + + /** + * Returns class relations reader. + * + * @return RelationsInterface + */ + private function getRelationsReader(): RelationsInterface + { + if (empty($this->relations)) { + $this->relations = ObjectManager::getInstance()->create(Runtime::class); + } + + return $this->relations; + } + + /** + * Returns list of classes that should not be in list of parent classes. + * + * @return array + */ + private function getInternalParents(): array + { + if (empty($this->internalParents)) { + $this->internalParents = $this->getRelations(TestCase::class); + $this->internalParents[] = TestCase::class; + } + + return $this->internalParents; + } +} diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php index 556f4e22d6f45..0f0579c49a94c 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Applier/Base.php @@ -12,6 +12,9 @@ */ abstract class Base implements ApplierInterface { + /** @var array */ + private $globalConfig; + /** @var array */ private $classConfig; @@ -21,6 +24,27 @@ abstract class Base implements ApplierInterface /** @var array */ private $dataSetConfig; + /** + * Get global node config + * + * @return array + */ + public function getGlobalConfig(): array + { + return $this->globalConfig; + } + + /** + * Set global node config + * + * @param array $globalConfig + * @return void + */ + public function setGlobalConfig(array $globalConfig): void + { + $this->globalConfig = $globalConfig; + } + /** * Get class node config * @@ -92,6 +116,7 @@ public function setDataSetConfig(array $dataSetConfig): void protected function getPrioritizedConfig(): array { return [ + $this->getGlobalConfig(), $this->getClassConfig(), $this->getMethodConfig(), $this->getDataSetConfig(), diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php index 351ecb60ae34d..33bf1011c5b7b 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/Override/Fixture/Resolver.php @@ -127,7 +127,11 @@ public function requireDataFixture(string $path): void */ public function applyConfigFixtures(TestCase $test, array $fixtures, string $fixtureType): array { - return $this->getApplier($test, $fixtureType)->apply($fixtures); + $skipConfig = $this->config->getSkipConfiguration($test); + + return $skipConfig['skip'] + ? [] + : $this->getApplier($test, $fixtureType)->apply($fixtures); } /** @@ -136,10 +140,14 @@ public function applyConfigFixtures(TestCase $test, array $fixtures, string $fix public function applyDataFixtures(TestCase $test, array $fixtures, string $fixtureType): array { $result = []; - $fixtures = $this->getApplier($test, $fixtureType)->apply($fixtures); + $skipConfig = $this->config->getSkipConfiguration($test); + + if (!$skipConfig['skip']) { + $fixtures = $this->getApplier($test, $fixtureType)->apply($fixtures); - foreach ($fixtures as $fixture) { - $result[] = $this->processFixturePath($test, $fixture); + foreach ($fixtures as $fixture) { + $result[] = $this->processFixturePath($test, $fixture); + } } return $result; @@ -195,6 +203,7 @@ private function getApplier(TestCase $test, string $fixtureType): ApplierInterfa } /** @var Base $applier */ $applier = $this->appliersList[$fixtureType]; + $applier->setGlobalConfig($this->config->getGlobalConfig($fixtureType)); $applier->setClassConfig($this->config->getClassConfig($test, $fixtureType)); $applier->setMethodConfig($this->config->getMethodConfig($test, $fixtureType)); $applier->setDataSetConfig( diff --git a/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd b/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd index 3e18c4bb7daca..424381b5cb2b9 100644 --- a/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd +++ b/dev/tests/integration/framework/Magento/TestFramework/Workaround/etc/overrides.xsd @@ -10,6 +10,7 @@ <xs:complexType> <xs:sequence minOccurs="0" maxOccurs="unbounded"> <xs:element name="test" type="test" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="global" type="global" minOccurs="0" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> </xs:element> @@ -77,4 +78,12 @@ <xs:attribute name="newValue" type="xs:string"/> <xs:attribute name="remove" type="xs:boolean"/> </xs:complexType> + <xs:complexType name="global"> + <xs:sequence minOccurs="0" maxOccurs="unbounded"> + <xs:element name="magentoDataFixture" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoDataFixtureBeforeTransaction" type="dataFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoConfigFixture" type="configFixture" minOccurs="0" maxOccurs="unbounded" /> + <xs:element name="magentoAdminConfigFixture" type="adminConfigFixture" minOccurs="0" maxOccurs="unbounded" /> + </xs:sequence> + </xs:complexType> </xs:schema> diff --git a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist index 1a93397caaa4a..d15c5f1818784 100644 --- a/dev/tests/integration/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/integration/framework/tests/unit/phpunit.xml.dist @@ -6,7 +6,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" @@ -15,7 +15,7 @@ <!-- Test suites definition --> <testsuites> <testsuite name="Unit Tests for Integration Tests Framework"> - <directory suffix="Test.php">testsuite</directory> + <directory>testsuite</directory> </testsuite> </testsuites> <php> diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php index 524e6933dfe06..4a6461a32df9d 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/ConfigFixtureTest.php @@ -484,6 +484,7 @@ private function processApply(array $existingFixtures, array $config): array */ private function setConfig(array $config): void { + $this->object->setGlobalConfig([]); $this->object->setClassConfig([]); $this->object->setDataSetConfig([]); $this->object->setMethodConfig($config); diff --git a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php index 6dd5df493353a..921c78e7bd482 100644 --- a/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php +++ b/dev/tests/integration/framework/tests/unit/testsuite/Magento/Test/Workaround/Override/Fixture/Applier/DataFixtureTest.php @@ -34,8 +34,11 @@ protected function setUp(): void public function testGetPrioritizedConfig(): void { $this->object = $this->getMockBuilder(DataFixture::class) - ->setMethods(['getClassConfig', 'getMethodConfig', 'getDataSetConfig']) + ->setMethods(['getGlobalConfig','getClassConfig', 'getMethodConfig', 'getDataSetConfig']) ->getMock(); + $this->object->expects($this->once()) + ->method('getGlobalConfig') + ->willReturn(['global_config']); $this->object->expects($this->once()) ->method('getClassConfig') ->willReturn(['class_config']); @@ -46,6 +49,7 @@ public function testGetPrioritizedConfig(): void ->method('getDataSetConfig') ->willReturn(['data_set_config']); $expectedResult = [ + ['global_config'], ['class_config'], ['method_config'], ['data_set_config'], @@ -271,6 +275,7 @@ private function processApply(array $existingFixtures, array $config): array */ private function setConfig(array $config): void { + $this->object->setGlobalConfig([]); $this->object->setClassConfig([]); $this->object->setDataSetConfig([]); $this->object->setMethodConfig($config); diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/BulkManagementTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/BulkManagementTest.php index 8c72977f6d8c8..3014fe37acb78 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/BulkManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/BulkManagementTest.php @@ -94,9 +94,6 @@ public function testRetryBulk() ->create() ->addFieldToFilter('bulk_uuid', ['eq' => $bulkUuid]) ->getItems(); - foreach ($operations as $operation) { - $operation->setId(null); - } $this->publisherMock->expects($this->once()) ->method('publish') ->with($topicName, array_values($operations)); diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php index 7ef6aa94768de..4976c8098103b 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/MassScheduleTest.php @@ -26,6 +26,8 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * + * @magentoDbIsolation disabled */ class MassScheduleTest extends \PHPUnit\Framework\TestCase { @@ -64,6 +66,9 @@ class MassScheduleTest extends \PHPUnit\Framework\TestCase */ private $skus = []; + /** @var string */ + private $logFilePath; + /** * @var Registry */ diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php index 7633a161253cd..4be795dd654cd 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/Model/OperationManagementTest.php @@ -8,7 +8,11 @@ use Magento\AsynchronousOperations\Api\Data\OperationInterface; use Magento\AsynchronousOperations\Api\Data\OperationInterfaceFactory; +use Magento\AsynchronousOperations\Model\BulkStatus; +use Magento\AsynchronousOperations\Model\OperationManagement; use Magento\Framework\EntityManager\EntityManager; +use Magento\Framework\App\ResourceConnection; +use Magento\TestFramework\Helper\Bootstrap; class OperationManagementTest extends \PHPUnit\Framework\TestCase { @@ -32,21 +36,18 @@ class OperationManagementTest extends \PHPUnit\Framework\TestCase */ private $entityManager; + /** + * @var ResourceConnection + */ + private $connection; + protected function setUp(): void { - $this->model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\AsynchronousOperations\Model\OperationManagement::class - ); - $this->bulkStatusManagement = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\AsynchronousOperations\Model\BulkStatus::class - ); - - $this->operationFactory = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - OperationInterfaceFactory::class - ); - $this->entityManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - EntityManager::class - ); + $this->connection = Bootstrap::getObjectManager()->get(ResourceConnection::class); + $this->model = Bootstrap::getObjectManager()->get(OperationManagement::class); + $this->bulkStatusManagement = Bootstrap::getObjectManager()->get(BulkStatus::class); + $this->operationFactory = Bootstrap::getObjectManager()->get(OperationInterfaceFactory::class); + $this->entityManager = Bootstrap::getObjectManager()->get(EntityManager::class); } /** @@ -62,13 +63,22 @@ public function testGetBulkStatus() $operation = array_shift($operations); $operationId = $operation->getId(); - $this->assertTrue($this->model->changeOperationStatus($operationId, OperationInterface::STATUS_TYPE_OPEN)); + $this->assertTrue($this->model->changeOperationStatus( + 'bulk-uuid-5', + $operationId, + OperationInterface::STATUS_TYPE_OPEN + )); + + $table = $this->connection->getTableName('magento_operation'); + $connection = $this->connection->getConnection(); + $select = $connection->select() + ->from($table) + ->where("bulk_uuid = ?", 'bulk-uuid-5') + ->where("operation_key = ?", $operationId); + $updatedOperation = $connection->fetchRow($select); - /** @var OperationInterface $updatedOperation */ - $updatedOperation = $this->operationFactory->create(); - $this->entityManager->load($updatedOperation, $operationId); - $this->assertEquals(OperationInterface::STATUS_TYPE_OPEN, $updatedOperation->getStatus()); - $this->assertNull($updatedOperation->getResultMessage()); - $this->assertNull($updatedOperation->getSerializedData()); + $this->assertEquals(OperationInterface::STATUS_TYPE_OPEN, $updatedOperation['status']); + $this->assertNull($updatedOperation['result_message']); + $this->assertNull($updatedOperation['serialized_data']); } } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php index 9e215667903d3..576927184ba8a 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/bulk.php @@ -60,6 +60,7 @@ 'status' => OperationInterface::STATUS_TYPE_COMPLETE, 'error_code' => null, 'result_message' => null, + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-3', @@ -68,6 +69,7 @@ 'status' => OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, 'error_code' => 1111, 'result_message' => 'Something went wrong during your request', + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-4', @@ -76,6 +78,7 @@ 'status' => OperationInterface::STATUS_TYPE_COMPLETE, 'error_code' => null, 'result_message' => null, + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-5', @@ -84,6 +87,7 @@ 'status' => OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, 'error_code' => 1111, 'result_message' => 'Something went wrong during your request', + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-5', @@ -92,6 +96,7 @@ 'status' => OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, 'error_code' => 2222, 'result_message' => 'Entity with ID=4 does not exist', + 'operation_key' => 1 ], ]; @@ -102,8 +107,8 @@ } $operationQuery = "INSERT INTO {$operationTable}" - . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`)" - . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message);"; + . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`, `operation_key`)" + . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message, :operation_key);"; foreach ($operations as $operation) { $connection->query($operationQuery, $operation); } diff --git a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php index a3547566c4245..1e27df71d5709 100644 --- a/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php +++ b/dev/tests/integration/testsuite/Magento/AsynchronousOperations/_files/operation_searchable.php @@ -42,6 +42,7 @@ 'status' => OperationInterface::STATUS_TYPE_COMPLETE, 'error_code' => null, 'result_message' => null, + 'operation_key' => 0 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -50,6 +51,7 @@ 'status' => OperationInterface::STATUS_TYPE_NOT_RETRIABLY_FAILED, 'error_code' => 1111, 'result_message' => 'Something went wrong during your request', + 'operation_key' => 1 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -58,6 +60,7 @@ 'status' => OperationInterface::STATUS_TYPE_RETRIABLY_FAILED, 'error_code' => 2222, 'result_message' => 'Entity with ID=4 does not exist', + 'operation_key' => 2 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -66,6 +69,7 @@ 'status' => OperationInterface::STATUS_TYPE_OPEN, 'error_code' => null, 'result_message' => '', + 'operation_key' => 3 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -74,6 +78,7 @@ 'status' => OperationInterface::STATUS_TYPE_OPEN, 'error_code' => null, 'result_message' => '', + 'operation_key' => 4 ], [ 'bulk_uuid' => 'bulk-uuid-searchable-6', @@ -82,6 +87,7 @@ 'status' => OperationInterface::STATUS_TYPE_REJECTED, 'error_code' => null, 'result_message' => '', + 'operation_key' => 5 ], ]; @@ -92,8 +98,8 @@ } $operationQuery = "INSERT INTO {$operationTable}" - . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`)" - . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message);"; + . " (`bulk_uuid`, `topic_name`, `serialized_data`, `status`, `error_code`, `result_message`, `operation_key`)" + . " VALUES (:bulk_uuid, :topic_name, :serialized_data, :status, :error_code, :result_message, :operation_key);"; foreach ($operations as $operation) { $connection->query($operationQuery, $operation); } diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Button/SplitButtonTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Button/SplitButtonTest.php new file mode 100644 index 0000000000000..473caf1d6737e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/Button/SplitButtonTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backend\Block\Widget\Button; + +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Testing SplitButton widget + * + * @magentoAppArea adminhtml + */ +class SplitButtonTest extends TestCase +{ + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->layout = $objectManager->get(LayoutInterface::class); + } + + /** + * Create the block. + * + * @return SplitButton + */ + private function createBlock(): SplitButton + { + /** @var SplitButton $block */ + $block = $this->layout->createBlock(SplitButton::class, 'button_block'); + $block->setLayout($this->layout); + + return $block; + } + + /** + * Test resulting button HTML. + * + * @return void + */ + public function testToHtml(): void + { + $block = $this->createBlock(); + $block->addData( + [ + 'title' => 'A button', + 'label' => 'A button', + 'has_split' => true, + 'button_class' => 'aclass', + 'id' => 'split-button', + 'disabled' => false, + 'class' => 'aclass', + 'data_attribute' => ['bind' => ['var' => 'val']], + 'options' => [ + [ + 'disabled' => false, + 'title' => 'An option', + 'label' => 'An option', + 'onclick' => $onclick = 'console.log("option")', + 'style' => 'width: 100px' + ] + ] + ] + ); + + $html = $block->toHtml(); + $this->assertStringContainsString('<button ', $html); + $this->assertStringContainsString('<span>A button</span>', $html); + $this->assertStringNotContainsString('onclick=', $html); + $this->assertStringNotContainsString('style=', $html); + $this->assertMatchesRegularExpression('/\<script.*?\>.*?' . preg_quote($onclick) . '.*?\<\/script\>/ims', $html); + $this->assertStringContainsString('width', $html); + $this->assertStringContainsString('100px', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/ButtonTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/ButtonTest.php new file mode 100644 index 0000000000000..e48a6bae2ff80 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/Widget/ButtonTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Backend\Block\Widget; + +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; +use Magento\Framework\View\LayoutInterface; + +/** + * Test for the button widget. + * + * @magentoAppArea adminhtml + */ +class ButtonTest extends TestCase +{ + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->layout = $objectManager->get(LayoutInterface::class); + } + + /** + * Create the block. + * + * @return Button + */ + private function createBlock(): Button + { + /** @var Button $block */ + $block = $this->layout->createBlock(Button::class, 'button_block'); + $block->setLayout($this->layout); + + return $block; + } + + /** + * Test resulting button HTML. + * + * @return void + */ + public function testToHtml(): void + { + $block = $this->createBlock(); + $block->addData( + [ + 'type' => 'button', + 'onclick' => 'console.log("Button pressed!")', + 'disabled' => false, + 'title' => 'A button', + 'label' => 'A button', + 'class' => 'button', + 'id' => 'button', + 'element_name' => 'some-name', + 'value' => 'Press a button', + 'data-style' => 'width: 100px', + 'style' => 'height: 200px' + ] + ); + + $html = $block->toHtml(); + $this->assertStringContainsString('<button ', $html); + $this->assertStringContainsString('<span>A button</span>', $html); + $this->assertStringNotContainsString('onclick=', $html); + $this->assertStringNotContainsString('style=', $html); + $this->assertMatchesRegularExpression('/\<script.*?\>.*?' .preg_quote($block->getOnClick()) .'.*?\<\/script\>/ims', $html); + $this->assertStringContainsString('height', $html); + $this->assertStringContainsString('200px', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php b/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php index cdcecabe00f8c..ef984c8289d99 100644 --- a/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php +++ b/dev/tests/integration/testsuite/Magento/Backend/Block/WidgetTest.php @@ -27,7 +27,8 @@ public function testGetButtonHtml() $widget = $layout->createBlock(\Magento\Backend\Block\Widget::class); $this->assertMatchesRegularExpression( - '/<button.*onclick\=\"this.form.submit\(\)\".*\>[\s\S]*Button Label[\s\S]*<\/button>/iu', + '/\<button.*\>[\s\S]*Button Label[\s\S]*<\/button>' + . '.*?\<script.*?\>.*?this\.form\.submit\(\).*?\<\/script\>/is', $widget->getButtonHtml('Button Label', 'this.form.submit()') ); } @@ -49,12 +50,14 @@ public function testGetButtonHtmlForTwoButtonsInOneBlock() $widget = $layout->createBlock(\Magento\Backend\Block\Widget::class); $this->assertMatchesRegularExpression( - '/<button.*onclick\=\"this.form.submit\(\)\".*\>[\s\S]*Button Label[\s\S]*<\/button>/iu', + '/<button.*\>[\s\S]*Button Label[\s\S]*<\/button>' + . '.*?\<script.*?\>.*?this\.form\.submit\(\).*?\<\/script\>/ius', $widget->getButtonHtml('Button Label', 'this.form.submit()') ); $this->assertMatchesRegularExpression( - '/<button.*onclick\=\"this.form.submit\(\)\".*\>[\s\S]*Button Label2[\s\S]*<\/button>/iu', + '/<button.*\>[\s\S]*Button Label2[\s\S]*<\/button>' + . '.*?\<script.*?\>.*?this\.form\.submit\(\).*?\<\/script\>/ius', $widget->getButtonHtml('Button Label2', 'this.form.submit()') ); } diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php b/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php index 369cbcf8ead33..980c5fe8a6e0a 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Block/Adminhtml/Catalog/Product/Edit/Tab/Bundle/Option/Search/GridTest.php @@ -27,7 +27,7 @@ public function testToHtmlHasOnClick() $html = $block->toHtml(); - $regexpTemplate = '/<button [^>]* onclick="temp_id[^"]*\\.%s/i'; + $regexpTemplate = '/\<script.*?\>.*?temp_id[^"]*\\.%s/is'; $jsFuncs = ['doFilter', 'resetFilter']; foreach ($jsFuncs as $func) { $regexp = sprintf($regexpTemplate, $func); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php index 572a526da07da..bf369ed28167b 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/Model/Product/BundlePriceAbstract.php @@ -3,13 +3,12 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); namespace Magento\Bundle\Model\Product; /** * Abstract class for testing bundle prices - * + * @codingStandardsIgnoreStart * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ abstract class BundlePriceAbstract extends \PHPUnit\Framework\TestCase @@ -31,14 +30,6 @@ abstract class BundlePriceAbstract extends \PHPUnit\Framework\TestCase */ protected $productCollectionFactory; - /** - * @var \Magento\CatalogRule\Model\RuleFactory - */ - private $ruleFactory; - - /** - * @inheritdoc - */ protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -52,19 +43,15 @@ protected function setUp(): void true, \Magento\Store\Model\ScopeInterface::SCOPE_STORE ); - $this->ruleFactory = $this->objectManager->get(\Magento\CatalogRule\Model\RuleFactory::class); } /** - * Get test cases. - * + * Get test cases * @return array */ abstract public function getTestCases(); /** - * Prepare fixture. - * * @param array $strategyModifiers * @param string $productSku * @return void @@ -75,14 +62,11 @@ abstract public function getTestCases(); */ protected function prepareFixture($strategyModifiers, $productSku) { - $this->ruleFactory->create()->clearPriceRulesData(); - $bundleProduct = $this->productRepository->get($productSku); foreach ($strategyModifiers as $modifier) { if (method_exists($this, $modifier['modifierName'])) { array_unshift($modifier['data'], $bundleProduct); - // phpcs:ignore Magento2.Functions.DiscouragedFunction $bundleProduct = call_user_func_array([$this, $modifier['modifierName']], $modifier['data']); } else { throw new \Magento\Framework\Exception\InputException( @@ -130,8 +114,6 @@ protected function addSimpleProduct(\Magento\Catalog\Model\Product $bundleProduc } /** - * Add custom option. - * * @param \Magento\Catalog\Model\Product $bundleProduct * @param array $optionsData * @return \Magento\Catalog\Model\Product @@ -161,3 +143,4 @@ protected function addCustomOption(\Magento\Catalog\Model\Product $bundleProduct return $bundleProduct; } } +// @codingStandardsIgnoreEnd diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php new file mode 100644 index 0000000000000..245656f536463 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$productIds = range(10, 12, 1); +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product With Two dropdown options') + ->setSku('bundle-product-two-dropdown-options') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceView(1) + ->setPriceType(1) + ->setPrice(10.0) + ->setShipmentType(0) + ->setBundleOptionsData( + [ + // "Drop-down" option + [ + 'title' => 'Drop Down Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 0, + 'position' => 1, + 'delete' => '', + ], + [ + 'title' => 'Drop Down Option 2', + 'default_title' => 'Option 2', + 'type' => 'select', + 'required' => 0, + 'position' => 2, + 'delete' => '', + ] + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_price_value' => 1.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 2.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_price_value' => 1.00, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_price_value' => 2.00, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ] + ], + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + $link->setPrice($linkData['selection_price_value']); + if (isset($linkData['selection_can_change_qty'])) { + $link->setCanChangeQuantity($linkData['selection_can_change_qty']); + } + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$productRepository->save($product, true); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php new file mode 100644 index 0000000000000..7088621f14c74 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_two_dropdown_options_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('bundle-product-two-dropdown-options', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price.php new file mode 100644 index 0000000000000..7e0ba34b8ea9f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Bundle\Model\Product\Price; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Type\AbstractType; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Bundle\Model\PrepareBundleLinks; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var PrepareBundleLinks $prepareBundleLinks */ +$prepareBundleLinks = $objectManager->get(PrepareBundleLinks::class); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$defaultWebsiteId = $storeManager->getWebsite('base')->getId(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Api\Data\ProductInterface $bundleProduct */ +$bundleProduct = $productFactory->create(); +$bundleProduct->setTypeId(Type::TYPE_BUNDLE) + ->setAttributeSetId($bundleProduct->getDefaultAttributeSetId()) + ->setWebsiteIds([$defaultWebsiteId]) + ->setName('Bundle Product With Dynamic Price') + ->setSku('bundle_product_with_dynamic_price') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 0, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + ) + ->setSkuType(0) + ->setPriceView(0) + ->setPriceType(Price::PRICE_TYPE_DYNAMIC) + ->setPrice(null) + ->setWeightType(0) + ->setShipmentType(AbstractType::SHIPMENT_TOGETHER); + +$bundleOptionsData = [ + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + ], + [ + 'title' => 'Option 2', + 'default_title' => 'Option 2', + 'type' => 'select', + 'required' => 1, + ], +]; +$bundleSelectionsData = [ + [ + [ + 'sku' => 'simple1', + 'selection_qty' => 1, + ], + ], + [ + [ + 'sku' => 'simple2', + 'selection_qty' => 1, + ], + ] +]; +$bundleProduct = $prepareBundleLinks->execute($bundleProduct, $bundleOptionsData, $bundleSelectionsData); +$productRepository->save($bundleProduct); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price_rollback.php new file mode 100644 index 0000000000000..85b7d8377ab9e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/bundle_product_with_dynamic_price_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle_product_with_dynamic_price', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $e) { + //product already deleted. +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php index fa957a0bfd3f8..1da7f821bb36e 100644 --- a/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/multiple_products.php @@ -28,7 +28,7 @@ ->setAttributeSetId($product->getDefaultAttributeSetId()) ->setName('Simple Product') ->setSku('simple1') - ->setTaxClassId(0) + ->setTaxClassId(2) ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') @@ -57,7 +57,7 @@ ->setAttributeSetId($product2->getDefaultAttributeSetId()) ->setName('Simple Product2') ->setSku('simple2') - ->setTaxClassId(0) + ->setTaxClassId(2) ->setDescription('description') ->setShortDescription('short description') ->setOptionsContainer('container1') diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php new file mode 100644 index 0000000000000..a623c583fb599 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity.php @@ -0,0 +1,137 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$productIds = range(10, 12, 1); +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setId(3) + ->setAttributeSetId(4) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setWebsiteIds([1]) + ->setPriceType(1) + ->setPrice(10.0) + ->setShipmentType(0) + ->setPriceView(1) + ->setBundleOptionsData( + [ + // Required "Drop-down" option + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + 'position' => 1, + 'delete' => '', + ], + // Required "Radio Buttons" option + [ + 'title' => 'Option 2', + 'default_title' => 'Option 2', + 'type' => 'radio', + 'required' => 1, + 'position' => 2, + 'delete' => '', + ], + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 0, + 'delete' => '', + 'option_id' => 2 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 0, + 'delete' => '', + 'option_id' => 2 + ] + ] + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + if (isset($linkData['selection_can_change_qty'])) { + $link->setCanChangeQuantity($linkData['selection_can_change_qty']); + } + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$product->save(); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity_rollback.php new file mode 100644 index 0000000000000..9d702b4506551 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_and_custom_quantity_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\TestFramework\Helper\Bootstrap; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +/** @var \Magento\Framework\Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle-product'); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select.php new file mode 100644 index 0000000000000..74182d830dc6d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select.php @@ -0,0 +1,177 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products.php'); + +$objectManager = Bootstrap::getObjectManager(); + +$productIds = range(10, 12, 1); +foreach ($productIds as $productId) { + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = $objectManager->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); +} + +/** @var $product Product */ +$product = $objectManager->create(Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_BUNDLE) + ->setId(3) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Bundle Product') + ->setSku('bundle-product') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setPriceType(1) + ->setPrice(10.0) + ->setShipmentType(0) + ->setBundleOptionsData( + [ + // Required "Drop-down" option + [ + 'title' => 'Option 1', + 'default_title' => 'Option 1', + 'type' => 'select', + 'required' => 1, + 'position' => 1, + 'delete' => '', + ], + // Required "Radio Buttons" option + [ + 'title' => 'Option 2', + 'default_title' => 'Option 2', + 'type' => 'radio', + 'required' => 1, + 'position' => 2, + 'delete' => '', + ] + ] + )->setBundleSelectionsData( + [ + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 1 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'selection_can_change_qty' => 1, + 'delete' => '', + 'option_id' => 2 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 3 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 3 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 4 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 4 + ] + ], + [ + [ + 'product_id' => 10, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 5 + ], + [ + 'product_id' => 11, + 'selection_qty' => 1, + 'delete' => '', + 'option_id' => 5 + ] + ] + ] + ); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +if ($product->getBundleOptionsData()) { + $options = []; + foreach ($product->getBundleOptionsData() as $key => $optionData) { + if (!(bool)$optionData['delete']) { + $option = $objectManager->create(\Magento\Bundle\Api\Data\OptionInterfaceFactory::class) + ->create(['data' => $optionData]); + $option->setSku($product->getSku()); + $option->setOptionId(null); + + $links = []; + $bundleLinks = $product->getBundleSelectionsData(); + if (!empty($bundleLinks[$key])) { + foreach ($bundleLinks[$key] as $linkData) { + if (!(bool)$linkData['delete']) { + $link = $objectManager->create(\Magento\Bundle\Api\Data\LinkInterfaceFactory::class) + ->create(['data' => $linkData]); + $linkProduct = $productRepository->getById($linkData['product_id']); + $link->setSku($linkProduct->getSku()); + $link->setQty($linkData['selection_qty']); + $links[] = $link; + } + } + $option->setProductLinks($links); + $options[] = $option; + } + } + } + $extension = $product->getExtensionAttributes(); + $extension->setBundleProductOptions($options); + $product->setExtensionAttributes($extension); +} +$product->save(); diff --git a/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select_rollback.php b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select_rollback.php new file mode 100644 index 0000000000000..57b4eb2e6cc91 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Bundle/_files/product_with_multiple_options_radio_select_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiple_products_rollback.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = $objectManager->get(\Magento\Framework\Registry::class); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $product = $productRepository->get('bundle-product'); + $productRepository->delete($product); +} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php index c0acf3344f60f..33c42d794bd78 100644 --- a/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Captcha/Observer/ResetAttemptForFrontendObserverTest.php @@ -34,7 +34,7 @@ protected function setUp(): void /** * @magentoDataFixture Magento/Captcha/_files/failed_logins_frontend.php */ - public function testSuccesfulLoginRemovesFailedAttempts() + public function testSuccessfulLoginRemovesFailedAttempts() { $customerEmail = 'mageuser@dummy.com'; $customerFactory = $this->objectManager->get(CustomerFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/AbstractTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/AbstractTest.php index a80b229bbbd15..fd1ff22dc6525 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/AbstractTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/AbstractTest.php @@ -3,15 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Catalog\Block\Product; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Pricing\Price\FinalPrice; +use Magento\Framework\Pricing\Render; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Framework\View\DesignInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; + /** * Test class for \Magento\Catalog\Block\Product\Abstract. * * @magentoDataFixture Magento/Catalog/_files/product_with_image.php * @magentoAppArea frontend */ -class AbstractTest extends \PHPUnit\Framework\TestCase +class AbstractTest extends TestCase { /** * Stub class name for class under test @@ -19,17 +32,17 @@ class AbstractTest extends \PHPUnit\Framework\TestCase const STUB_CLASS = 'Magento_Catalog_Block_Product_AbstractProduct_Stub'; /** - * @var \Magento\Catalog\Block\Product\AbstractProduct + * @var AbstractProduct */ protected $block; /** - * @var \Magento\Catalog\Model\Product + * @var ProductInterface */ protected $product; /** - * @var \Magento\Catalog\Api\ProductRepositoryInterface + * @var ProductRepositoryInterface */ protected $productRepository; @@ -40,43 +53,51 @@ class AbstractTest extends \PHPUnit\Framework\TestCase */ protected static $isStubClass = false; + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var SerializerInterface + */ + private $json; + + /** + * @inheritdoc + */ + protected function setUp(): void { if (!self::$isStubClass) { $this->getMockForAbstractClass( - \Magento\Catalog\Block\Product\AbstractProduct::class, + AbstractProduct::class, [], self::STUB_CLASS, false ); self::$isStubClass = true; } - - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - $objectManager->get(\Magento\Framework\App\State::class)->setAreaCode('frontend'); - $objectManager->get(\Magento\Framework\View\DesignInterface::class)->setDefaultDesignTheme(); - $this->block = $objectManager->get( - \Magento\Framework\View\LayoutInterface::class - )->createBlock(self::STUB_CLASS); - $this->productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); - - $this->product = $this->productRepository->get('simple'); - $this->product->addData( - [ - 'image' => '/m/a/magento_image.jpg', - 'small_image' => '/m/a/magento_image.jpg', - 'thumbnail' => '/m/a/magento_image.jpg', - ] - ); - $this->block->setProduct($this->product); + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->get(DesignInterface::class)->setDefaultDesignTheme(); + $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->block = $this->layout->createBlock(self::STUB_CLASS); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->json = $this->objectManager->get(SerializerInterface::class); } /** * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php * @magentoAppIsolation enabled + * @return void */ - public function testGetAddToCartUrl() + public function testGetAddToCartUrlWithProductRequiredOptions(): void { $product = $this->productRepository->get('simple'); $url = $this->block->getAddToCartUrl($product); @@ -84,18 +105,38 @@ public function testGetAddToCartUrl() $this->assertStringMatchesFormat('%ssimple-product.html%s', $url); } - public function testGetSubmitUrl() + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetAddToCartUrlWithSimpleProduct(): void + { + $product = $this->productRepository->get('simple-1'); + $url = $this->block->getAddToCartUrl($product); + $this->assertStringEndsWith(sprintf('product/%s/', $product->getId()), $url); + $this->assertStringContainsString('checkout/cart/add', $url); + } + + /** + * @return void + */ + public function testGetSubmitUrl(): void { + $this->product = $this->productRepository->get('simple'); /* by default same as add to cart */ $this->assertStringEndsWith('?options=cart', $this->block->getSubmitUrl($this->product)); $this->block->setData('submit_route_data', ['route' => 'catalog/product/view']); $this->assertStringEndsWith('catalog/product/view/', $this->block->getSubmitUrl($this->product)); } - public function testGetAddToWishlistParams() + /** + * @return void + */ + public function testGetAddToWishlistParams(): void { + $this->product = $this->productRepository->get('simple'); $json = $this->block->getAddToWishlistParams($this->product); - $params = (array)json_decode($json); + $params = (array)$this->json->unserialize($json); $data = (array)$params['data']; $this->assertEquals($this->product->getId(), $data['product']); $this->assertArrayHasKey('uenc', $data); @@ -105,53 +146,70 @@ public function testGetAddToWishlistParams() ); } - public function testGetAddToCompareUrl() + /** + * @return void + */ + public function testGetAddToCompareUrl(): void { $this->assertStringMatchesFormat('%scatalog/product_compare/add/', $this->block->getAddToCompareUrl()); } - public function testGetMinimalQty() + /** + * @return void + */ + public function testGetMinimalQty(): void { + $this->product = $this->productRepository->get('simple'); $this->assertGreaterThan(0, $this->block->getMinimalQty($this->product)); } - public function testGetReviewsSummaryHtml() + /** + * @return void + */ + public function testGetReviewsSummaryHtml(): void { - $this->block->setLayout( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Framework\View\LayoutInterface::class) - ); + $this->product = $this->productRepository->get('simple'); $html = $this->block->getReviewsSummaryHtml($this->product, false, true); $this->assertNotEmpty($html); $this->assertStringContainsString('review', $html); } - public function testGetProduct() + /** + * @return void + */ + public function testGetProduct(): void { + $this->product = $this->productRepository->get('simple'); + $this->block->setProduct($this->product); $this->assertSame($this->product, $this->block->getProduct()); } /** * @magentoDataFixture Magento/CatalogUrlRewrite/_files/product_simple.php * @magentoAppIsolation enabled + * @return void */ - public function testGetProductUrl() + public function testGetProductUrl(): void { $product = $this->productRepository->get('simple'); $this->assertStringEndsWith('simple-product.html', $this->block->getProductUrl($product)); } - public function testHasProductUrl() + /** + * @return void + */ + public function testHasProductUrl(): void { + $this->product = $this->productRepository->get('simple'); $this->assertTrue($this->block->hasProductUrl($this->product)); } - public function testLayoutDependColumnCount() + /** + * @return void + */ + public function testLayoutDependColumnCount(): void { - $this->block->setLayout( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Framework\View\LayoutInterface::class) - ); + $this->block->setLayout($this->layout); $this->assertEquals(3, $this->block->getColumnCount()); /* default column count */ @@ -161,8 +219,35 @@ public function testLayoutDependColumnCount() $this->assertFalse($this->block->getColumnCountLayoutDepend('test')); } - public function testGetCanShowProductPrice() + /** + * @return void + */ + public function testGetCanShowProductPrice(): void { + $this->product = $this->productRepository->get('simple'); $this->assertTrue($this->block->getCanShowProductPrice($this->product)); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testGetProductPriceHtml(): void + { + $product = $this->productRepository->get('simple-1'); + $this->assertEmpty($this->block->getProductPriceHtml($product, FinalPrice::PRICE_CODE)); + $this->layout->createBlock( + Render::class, + 'product.price.render.default', + [ + 'data' => [ + 'price_render_handle' => 'catalog_product_prices', + 'use_link_for_as_low_as' => true, + ], + ] + ); + $finalPriceHtml = $this->block->getProductPriceHtml($product, FinalPrice::PRICE_CODE); + $this->assertStringContainsString('price-' . FinalPrice::PRICE_CODE, $finalPriceHtml); + $this->assertStringContainsString('product-price-' . $product->getId(), $finalPriceHtml); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php index 699df30c7bf3d..5badbef361b62 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/AbstractLinksTest.php @@ -66,6 +66,12 @@ abstract class AbstractLinksTest extends TestCase /** @var string */ protected $linkType; + /** @var string */ + protected $titleName; + + /** @var string */ + protected $titleXpath = "//strong[@id = 'block-%s-heading'][contains(text(), '%s')]"; + /** * @inheritdoc */ @@ -297,7 +303,7 @@ protected function linkProducts(string $sku, array $productLinks): void * * @return array */ - protected function prepareWebsiteIdsProducts(): array + protected function prepareProductsWebsiteIds(): array { $websiteId = $this->storeManager->getWebsite('test')->getId(); $defaultWebsiteId = $this->storeManager->getWebsite('base')->getId(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php index 2c61743ae6aa5..11ce7b1328df8 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/RelatedTest.php @@ -132,7 +132,7 @@ public function testPositionRelatedProducts(): void */ public function testMultipleWebsitesRelatedProducts(array $data): void { - $this->updateProducts($this->prepareWebsiteIdsProducts()); + $this->updateProducts($this->prepareProductsWebsiteIds()); $productLinks = array_replace_recursive($this->existingProducts, $data['productLinks']); $this->linkProducts('simple-1', $productLinks); $this->product = $this->productRepository->get( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php index fd9d4e7e68fff..4d24a5aabafdb 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/ProductList/UpsellTest.php @@ -120,7 +120,7 @@ public function testPositionUpsellProducts(): void */ public function testMultipleWebsitesUpsellProducts(array $data): void { - $this->updateProducts($this->prepareWebsiteIdsProducts()); + $this->updateProducts($this->prepareProductsWebsiteIds()); $productLinks = array_replace_recursive($this->existingProducts, $data['productLinks']); $this->linkProducts('simple-1', $productLinks); $this->product = $this->productRepository->get( diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/OptionsTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/OptionsTest.php index 28357919ed566..57782fc17c9f5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/OptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Block/Product/View/OptionsTest.php @@ -3,12 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -declare(strict_types=1); - namespace Magento\Catalog\Block\Product\View; -use Magento\CatalogRule\Model\Indexer\IndexBuilder; - /** * Test class for \Magento\Catalog\Block\Product\View\Options. */ @@ -34,19 +30,12 @@ class OptionsTest extends \PHPUnit\Framework\TestCase */ protected $productRepository; - /** - * @var IndexBuilder - */ - private $indexBuilder; - protected function setUp(): void { $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); $this->productRepository = $this->objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - $this->indexBuilder = $this->objectManager->create(IndexBuilder::class); - try { $this->product = $this->productRepository->get('simple'); } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { @@ -124,7 +113,9 @@ private function getExpectedJsonConfig() { return [ 0 => [ - 'prices' => ['oldPrice' => ['amount' => 10, 'adjustments' => []], + 'prices' => + ['oldPrice' => + ['amount' => 10, 'adjustments' => []], 'basePrice' => ['amount' => 10], 'finalPrice' => ['amount' => 10] ], @@ -132,7 +123,9 @@ private function getExpectedJsonConfig() 'name' => 'drop_down option 1', ], 1 => [ - 'prices' => ['oldPrice' => ['amount' => 40, 'adjustments' => []], + 'prices' => + ['oldPrice' => + ['amount' => 40, 'adjustments' => []], 'basePrice' => ['amount' => 40], 'finalPrice' => ['amount' => 40], ], @@ -141,47 +134,4 @@ private function getExpectedJsonConfig() ], ]; } - - /** - * Test option prices with catalog price rules applied. - * - * @magentoDbIsolation disabled - * @magentoDataFixture Magento/CatalogRule/_files/two_rules.php - * @magentoDataFixture Magento/Catalog/_files/product_with_dropdown_option.php - */ - public function testGetJsonConfigWithCatalogRules() - { - $this->indexBuilder->reindexFull(); - sleep(1); - $config = json_decode($this->block->getJsonConfig(), true); - $configValues = array_values($config); - $this->assertEquals($this->getExpectedJsonConfigWithCatalogRules(), array_values($configValues[0])); - } - - /** - * Expected data for testGetJsonConfigWithCatalogRules - * - * @return array - */ - private function getExpectedJsonConfigWithCatalogRules() - { - return [ - 0 => [ - 'prices' => ['oldPrice' => ['amount' => 10, 'adjustments' => []], - 'basePrice' => ['amount' => 9.5], - 'finalPrice' => ['amount' => 9.5], - ], - 'type' => 'fixed', - 'name' => 'drop_down option 1', - ], - 1 => [ - 'prices' => ['oldPrice' => ['amount' => 40, 'adjustments' => []], - 'basePrice' => ['amount' => 38], - 'finalPrice' => ['amount' => 38], - ], - 'type' => 'percent', - 'name' => 'drop_down option 2', - ], - ]; - } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php index 7389799c00362..6245e4e9f8de7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/CategoryTest.php @@ -67,6 +67,12 @@ class CategoryTest extends AbstractBackendController */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); parent::setUp(); /** @var ProductResource $productResource */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php index 6bf521f098fa0..c53ee2170d4b4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/Product/Action/AttributeTest.php @@ -5,16 +5,26 @@ */ namespace Magento\Catalog\Controller\Adminhtml\Product\Action; +use Magento\Backend\Model\Session; +use Magento\Catalog\Block\Product\ListProduct; +use Magento\Catalog\Helper\Product\Edit\Action\Attribute; +use Magento\Catalog\Model\CategoryFactory; use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\Message\MessageInterface; use Magento\Catalog\Model\ProductRepository; use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\UrlInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\MessageQueue\EnvironmentPreconditionException; +use Magento\TestFramework\MessageQueue\PreconditionFailedException; use Magento\TestFramework\MessageQueue\PublisherConsumerController; +use Magento\TestFramework\TestCase\AbstractBackendController; /** * @magentoAppArea adminhtml + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendController +class AttributeTest extends AbstractBackendController { /** @var PublisherConsumerController */ private $publisherConsumerController; @@ -22,7 +32,9 @@ class AttributeTest extends \Magento\TestFramework\TestCase\AbstractBackendContr protected function setUp(): void { - $this->publisherConsumerController = Bootstrap::getObjectManager()->create( + parent::setUp(); + + $this->publisherConsumerController = $this->_objectManager->create( PublisherConsumerController::class, [ 'consumers' => $this->consumers, @@ -34,15 +46,13 @@ protected function setUp(): void try { $this->publisherConsumerController->startConsumers(); - } catch (\Magento\TestFramework\MessageQueue\EnvironmentPreconditionException $e) { + } catch (EnvironmentPreconditionException $e) { $this->markTestSkipped($e->getMessage()); - } catch (\Magento\TestFramework\MessageQueue\PreconditionFailedException $e) { + } catch (PreconditionFailedException $e) { $this->fail( $e->getMessage() ); } - - parent::setUp(); } protected function tearDown(): void @@ -59,10 +69,8 @@ protected function tearDown(): void */ public function testSaveActionRedirectsSuccessfully() { - $objectManager = Bootstrap::getObjectManager(); - - /** @var $session \Magento\Backend\Model\Session */ - $session = $objectManager->get(\Magento\Backend\Model\Session::class); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); $session->setProductIds([1]); $this->getRequest()->setMethod(HttpRequest::METHOD_POST); @@ -70,10 +78,10 @@ public function testSaveActionRedirectsSuccessfully() $this->assertEquals(302, $this->getResponse()->getHttpResponseCode()); /** @var \Magento\Backend\Model\UrlInterface $urlBuilder */ - $urlBuilder = $objectManager->get(\Magento\Framework\UrlInterface::class); + $urlBuilder = $this->_objectManager->get(UrlInterface::class); - /** @var \Magento\Catalog\Helper\Product\Edit\Action\Attribute $attributeHelper */ - $attributeHelper = $objectManager->get(\Magento\Catalog\Helper\Product\Edit\Action\Attribute::class); + /** @var Attribute $attributeHelper */ + $attributeHelper = $this->_objectManager->get(Attribute::class); $expectedUrl = $urlBuilder->getUrl( 'catalog/product/index', ['store' => $attributeHelper->getSelectedStoreId()] @@ -98,18 +106,15 @@ public function testSaveActionRedirectsSuccessfully() */ public function testSaveActionChangeVisibility($attributes) { - $objectManager = Bootstrap::getObjectManager(); /** @var ProductRepository $repository */ - $repository = Bootstrap::getObjectManager()->create( - ProductRepository::class - ); + $repository = $this->_objectManager->create(ProductRepository::class); $product = $repository->get('simple'); $product->setOrigData(); $product->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE); $product->save(); - /** @var $session \Magento\Backend\Model\Session */ - $session = $objectManager->get(\Magento\Backend\Model\Session::class); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); $session->setProductIds([$product->getId()]); $this->getRequest()->setParam('attributes', $attributes); $this->getRequest()->setMethod(HttpRequest::METHOD_POST); @@ -117,13 +122,9 @@ public function testSaveActionChangeVisibility($attributes) $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); /** @var \Magento\Catalog\Model\Category $category */ - $categoryFactory = Bootstrap::getObjectManager()->get( - \Magento\Catalog\Model\CategoryFactory::class - ); - /** @var \Magento\Catalog\Block\Product\ListProduct $listProduct */ - $listProduct = Bootstrap::getObjectManager()->get( - \Magento\Catalog\Block\Product\ListProduct::class - ); + $categoryFactory = $this->_objectManager->get(CategoryFactory::class); + /** @var ListProduct $listProduct */ + $listProduct = $this->_objectManager->get(ListProduct::class); $this->publisherConsumerController->waitForAsynchronousResult( function () use ($repository) { @@ -159,10 +160,8 @@ function () use ($repository) { */ public function testValidateActionWithMassUpdate($attributes) { - $objectManager = Bootstrap::getObjectManager(); - - /** @var $session \Magento\Backend\Model\Session */ - $session = $objectManager->get(\Magento\Backend\Model\Session::class); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); $session->setProductIds([1, 2]); $this->getRequest()->setParam('attributes', $attributes); @@ -214,4 +213,34 @@ public function saveActionVisibilityAttrDataProvider() ['arguments' => ['visibility' => Visibility::VISIBILITY_IN_CATALOG]] ]; } + + /** + * Assert that custom layout update can not be change for existing entity. + * + * @return void + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + */ + public function testSaveActionCantChangeCustomLayoutUpdate(): void + { + /** @var ProductRepository $repository */ + $repository = $this->_objectManager->get(ProductRepository::class); + $product = $repository->get('simple'); + + $product->setOrigData('custom_layout_update', 'test'); + $product->setData('custom_layout_update', 'test'); + $product->save(); + /** @var $session Session */ + $session = $this->_objectManager->get(Session::class); + $session->setProductIds([$product->getId()]); + $this->getRequest()->setParam('attributes', ['custom_layout_update' => 'test2']); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + + $this->dispatch('backend/catalog/product_action_attribute/save/store/0'); + + $this->assertSessionMessages( + $this->equalTo(['Custom layout update text cannot be changed, only removed']), + MessageInterface::TYPE_ERROR + ); + $this->assertEquals('test', $product->getData('custom_layout_update')); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php index 65e7e94f4aa24..7032199e9fc4c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Adminhtml/ProductTest.php @@ -49,6 +49,12 @@ class ProductTest extends \Magento\TestFramework\TestCase\AbstractBackendControl */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); parent::setUp(); $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); @@ -412,6 +418,7 @@ private function getProductData(array $tierPrice) $repo = $this->repositoryFactory->create(); $product = $repo->get('tier_prices')->getData(); $product['tier_price'] = $tierPrice; + $product['entity_id'] = null; /** @phpstan-ignore-next-line */ unset($product['entity_id']); return $product; diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php index 07cc43921d59f..3f023a75d0f92 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/CategoryTest.php @@ -53,6 +53,12 @@ protected function setUp(): void parent::setUp(); $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); $this->registry = $this->objectManager->get(Registry::class); $this->layout = $this->objectManager->get(LayoutInterface::class); $this->session = $this->objectManager->get(Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php index 5458de89e9b82..e8f9607530fba 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/Product/ViewTest.php @@ -13,6 +13,7 @@ use Magento\Catalog\Model\Product; use Magento\Catalog\Model\Product\Visibility; use Magento\Eav\Model\Entity\Type; +use Magento\Framework\App\Cache\Manager; use Magento\Framework\App\Http; use Magento\Framework\Registry; use Magento\Store\Model\StoreManagerInterface; @@ -268,6 +269,30 @@ public function testProductWithoutWebsite(): void $this->assert404NotFound(); } + /** + * Test that 404 page has product tag if product is not visible + * + * @magentoDataFixture Magento/Quote/_files/is_not_salable_product.php + * @magentoCache full_page enabled + * @return void + */ + public function test404NotFoundPageCacheTags(): void + { + $cache = $this->_objectManager->get(Manager::class); + $cache->clean(['full_page']); + $product = $this->productRepository->get('simple-99'); + $this->dispatch(sprintf('catalog/product/view/id/%s/', $product->getId())); + $this->assert404NotFound(); + $pTag = Product::CACHE_TAG . '_' . $product->getId(); + $hTags = $this->getResponse()->getHeader('X-Magento-Tags'); + $tags = $hTags && $hTags->getFieldValue() ? explode(',', $hTags->getFieldValue()) : []; + $this->assertContains( + $pTag, + $tags, + "Failed asserting that X-Magento-Tags: {$hTags->getFieldValue()} contains \"$pTag\"" + ); + } + /** * @param string|ProductInterface $product * @param array $data diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php index 4494ccf1eb3fe..3f9f788dc28c7 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Controller/ProductTest.php @@ -42,6 +42,12 @@ protected function setUp(): void if (defined('HHVM_VERSION')) { $this->markTestSkipped('Randomly fails due to known HHVM bug (DOMText mixed with DOMElement)'); } + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); parent::setUp(); $this->registry = $this->_objectManager->get(Registry::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php index 506556dbe95b3..da6aa44df3e6a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Attribute/Backend/AbstractLayoutUpdateTest.php @@ -55,6 +55,12 @@ private function recreateCategory(): void */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); $this->categoryFactory = Bootstrap::getObjectManager()->get(CategoryFactory::class); $this->recreateCategory(); $this->attribute = $this->category->getAttributes()['custom_layout_update_file']->getBackend(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php index f9237e89817f1..1d846fc154fc0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Category/DataProviderTest.php @@ -3,15 +3,20 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category; +use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; +use Magento\Catalog\Model\CategoryFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager; use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\Registry; use PHPUnit\Framework\TestCase; -use Magento\Catalog\Model\Category; -use Magento\Catalog\Model\CategoryFactory; -use Magento\Catalog\Model\Category\Attribute\Backend\LayoutUpdate; /** * @magentoDbIsolation enabled @@ -40,6 +45,16 @@ class DataProviderTest extends TestCase */ private $fakeFiles; + /** + * @var ScopeConfigInterface + */ + private $scopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * Create subject instance. * @@ -58,22 +73,30 @@ private function createDataProvider(): DataProvider } /** - * {@inheritDoc} + * @inheritDoc */ protected function setUp(): void { - parent::setUp(); $objectManager = Bootstrap::getObjectManager(); + $objectManager->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); + parent::setUp(); $this->dataProvider = $this->createDataProvider(); $this->registry = $objectManager->get(Registry::class); $this->categoryFactory = $objectManager->get(CategoryFactory::class); $this->fakeFiles = $objectManager->get(CategoryLayoutUpdateManager::class); + $this->scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); } /** * @return void */ - public function testGetMetaRequiredAttributes() + public function testGetMetaRequiredAttributes(): void { $requiredAttributes = [ 'general' => ['name'], @@ -221,4 +244,48 @@ public function testCustomLayoutMeta(): void sort($list); $this->assertEquals($expectedList, $list); } + + /** + * Check if existing category page layout will remain unaffected by category page layout default value setting + * + * @return void + */ + public function testExistingCategoryLayoutUnaffectedByDefaults(): void + { + /** @var Category $category */ + $category = $this->categoryFactory->create(); + $category->load(2); + + $this->registry->register('category', $category); + $meta = $this->dataProvider->getMeta(); + $categoryPageLayout = $meta["design"]["children"]["page_layout"]["arguments"]["data"]["config"]["default"]; + $this->registry->unregister('category'); + + $this->assertNull($categoryPageLayout); + } + + /** + * Check if category page layout default value setting will apply to the new category during it's creation + * + * @throws NoSuchEntityException + */ + public function testNewCategoryLayoutMatchesDefault(): void + { + $categoryDefaultPageLayout = $this->scopeConfig->getValue( + 'web/default_layouts/default_category_layout', + ScopeInterface::SCOPE_STORE, + $this->storeManager->getStore()->getId() + ); + + /** @var Category $category */ + $category = $this->categoryFactory->create(); + $category->setName('Net Test Category'); + + $this->registry->register('category', $category); + $meta = $this->dataProvider->getMeta(); + $categoryPageLayout = $meta["design"]["children"]["page_layout"]["arguments"]["data"]["config"]["default"]; + $this->registry->unregister('category'); + + $this->assertEquals($categoryDefaultPageLayout, $categoryPageLayout); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php index bfacdb85bbcce..7fd7627c738d6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryRepositoryTest.php @@ -53,6 +53,12 @@ class CategoryRepositoryTest extends TestCase */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Category\Attribute\LayoutUpdateManager::class + => \Magento\TestFramework\Catalog\Model\CategoryLayoutUpdateManager::class + ] + ]); $this->repositoryFactory = Bootstrap::getObjectManager()->get(CategoryRepositoryInterfaceFactory::class); $this->layoutManager = Bootstrap::getObjectManager()->get(CategoryLayoutUpdateManager::class); $this->productCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php index 13437554febd3..0d2f9d63c5d7f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/CategoryTest.php @@ -49,7 +49,7 @@ class CategoryTest extends TestCase */ protected $objectManager; - /** @var CategoryRepository */ + /** @var CategoryResource */ private $categoryResource; /** @var CategoryRepositoryInterface */ @@ -355,6 +355,17 @@ public function testDeleteChildren(): void $this->assertEquals($this->_model->getId(), null); } + /** + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + * @magentoDataFixture Magento/Catalog/_files/categories_no_products.php + */ + public function testChildrenCountAfterDeleteParentCategory(): void + { + $this->categoryRepository->deleteByIdentifier(3); + $this->assertEquals(8, $this->categoryResource->getChildrenCount(1)); + } + /** * @magentoDataFixture Magento/Catalog/_files/category.php */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php index 51b1d4fdb7fe0..e3b5bc8d5fd0d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Indexer/Product/Flat/Action/RelationTest.php @@ -15,6 +15,8 @@ /** * Test relation customization + * + * @magentoDbIsolation disabled */ class RelationTest extends \Magento\TestFramework\Indexer\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php new file mode 100644 index 0000000000000..0d8b0a825d24c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/Product/Price/SpecialPriceStorageTest.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\Product\Price; + +use Magento\Catalog\Api\Data\SpecialPriceInterface; +use Magento\Catalog\Api\Data\SpecialPriceInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test special price storage model + */ +class SpecialPriceStorageTest extends TestCase +{ + /** + * @var SpecialPriceStorage + */ + private $model; + /** + * @var SpecialPriceInterfaceFactory + */ + private $specialPriceFactory; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->model = $objectManager->get(SpecialPriceStorage::class); + $this->specialPriceFactory = $objectManager->get(SpecialPriceInterfaceFactory::class); + } + + /** + * Test that price update validation works correctly + * + * @magentoDataFixture Magento/Catalog/_files/category_product.php + */ + public function testUpdateValidationResult() + { + $date = new \Datetime('+2 days'); + $date->setTime(0, 0); + /** @var SpecialPriceInterface $price */ + $price = $this->specialPriceFactory->create(); + $price->setSku('invalid') + ->setStoreId(0) + ->setPrice(5.0) + ->setPriceFrom($date->format('Y-m-d H:i:s')) + ->setPriceTo( + $date->modify('+1 day') + ->format('Y-m-d H:i:s') + ); + $result = $this->model->update([$price]); + $this->assertCount(1, $result); + $this->assertStringContainsString( + 'The product that was requested doesn\'t exist.', + (string) $result[0]->getMessage() + ); + $price->setSku('simple333'); + $result = $this->model->update([$price]); + $this->assertCount(0, $result); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php index af7a027367fff..8908561702dd0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ProductRepositoryTest.php @@ -87,6 +87,12 @@ protected function setUp(): void { parent::setUp(); + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); $this->objectManager = Bootstrap::getObjectManager(); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $this->productRepository->cleanCache(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/CollectionTest.php new file mode 100644 index 0000000000000..607e5b6de4541 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Attribute/CollectionTest.php @@ -0,0 +1,80 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Model\ResourceModel\Attribute; + +use Magento\Catalog\Model\ResourceModel\Product\Attribute\CollectionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Tests \Magento\Catalog\Model\ResourceModel\Product\Attribute\Collection + */ +class CollectionTest extends TestCase +{ + /** + * @var CollectionFactory . + */ + private $attributesCollectionFactory; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->attributesCollectionFactory = $objectManager->get(CollectionFactory::class); + } + + /** + * @magentoAppArea adminhtml + * @dataProvider attributesCollectionGetCurrentPageDataProvider + * + * @param array|null $condition + * @param int $currentPage + * @param int $expectedCurrentPage + * @return void + */ + public function testAttributesCollectionGetCurrentPage( + ?array $condition, + int $currentPage, + int $expectedCurrentPage + ): void { + $attributeCollection = $this->attributesCollectionFactory->create(); + $attributeCollection->setCurPage($currentPage)->setPageSize(20); + + if ($condition !== null) { + $attributeCollection->addFieldToFilter('is_global', $condition); + } + + $this->assertEquals($expectedCurrentPage, (int)$attributeCollection->getCurPage()); + } + + /** + * @return array[] + */ + public function attributesCollectionGetCurrentPageDataProvider(): array + { + return [ + [ + 'condition' => null, + 'currentPage' => 1, + 'expectedCurrentPage' => 1, + ], + [ + 'condition' => ['eq' => 0], + 'currentPage' => 1, + 'expectedCurrentPage' => 1, + ], + [ + 'condition' => ['eq' => 0], + 'currentPage' => 15, + 'expectedCurrentPage' => 1, + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php index 19e62d7a50606..263a1c41ac8df 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Model/ResourceModel/Eav/AttributeTest.php @@ -5,21 +5,48 @@ */ namespace Magento\Catalog\Model\ResourceModel\Eav; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + /** * Test for \Magento\Catalog\Model\ResourceModel\Eav\Attribute. */ class AttributeTest extends \PHPUnit\Framework\TestCase { /** - * @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Attribute */ - protected $_model; + private $model; + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var int|string + */ + private $catalogProductEntityType; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class - ); + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(Attribute::class); + $this->attributeRepository = $this->objectManager->get(AttributeRepositoryInterface::class); + $this->catalogProductEntityType = $this->objectManager->get(Config::class) + ->getEntityType('catalog_product') + ->getId(); } /** @@ -29,18 +56,28 @@ protected function setUp(): void */ public function testCRUD() { - $this->_model->setAttributeCode( - 'test' - )->setEntityTypeId( - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Eav\Model\Config::class - )->getEntityType( - 'catalog_product' - )->getId() - )->setFrontendLabel( - 'test' - )->setIsUserDefined(1); - $crud = new \Magento\TestFramework\Entity($this->_model, ['frontend_label' => uniqid()]); + $this->model->setAttributeCode('test') + ->setEntityTypeId($this->catalogProductEntityType) + ->setFrontendLabel('test') + ->setIsUserDefined(1); + $crud = new \Magento\TestFramework\Entity($this->model, [AttributeInterface::FRONTEND_LABEL => uniqid()]); $crud->testCrud(); } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_attribute.php + * + * @return void + */ + public function testAttributeSaveWithChangedEntityType(): void + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + $this->expectExceptionMessage('Do not change entity type.'); + + $attribute = $this->attributeRepository->get($this->catalogProductEntityType, 'test_attribute_code_333'); + $attribute->setEntityTypeId(1); + $attribute->save(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/CombinationWithDifferentTypePricesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationAbstract.php similarity index 55% rename from dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/CombinationWithDifferentTypePricesTest.php rename to dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationAbstract.php index 7d366811952ca..fce502b2dfea0 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/CombinationWithDifferentTypePricesTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationAbstract.php @@ -5,12 +5,16 @@ */ declare(strict_types=1); -namespace Magento\Catalog\Pricing\Render; +namespace Magento\Catalog\Pricing\Render\PriceTypes; +use Magento\Catalog\Api\Data\ProductCustomOptionInterface; +use Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Config\Source\ProductPriceOptionsInterface; +use Magento\Catalog\Model\Product\Option; use Magento\CatalogRule\Api\CatalogRuleRepositoryInterface; use Magento\CatalogRule\Api\Data\RuleInterface; use Magento\CatalogRule\Api\Data\RuleInterfaceFactory; @@ -19,45 +23,50 @@ use Magento\Customer\Model\Session; use Magento\Framework\Registry; use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Helper\Xpath; use Magento\TestFramework\ObjectManager; use PHPUnit\Framework\TestCase; /** - * Assertions related to check product price rendering with combination of different price types. + * Base class for combination of different price types tests. * - * @magentoDbIsolation disabled - * @magentoAppArea frontend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CombinationWithDifferentTypePricesTest extends TestCase +abstract class CombinationAbstract extends TestCase { /** * @var ObjectManager */ - private $objectManager; + protected $objectManager; /** * @var Page */ - private $page; + protected $page; /** - * @var Registry + * @var IndexBuilder */ - private $registry; + protected $indexBuilder; /** - * @var IndexBuilder + * @var Session */ - private $indexBuilder; + protected $customerSession; /** - * @var Session + * @var StoreManagerInterface + */ + protected $storeManager; + + /** + * @var Registry */ - private $customerSession; + private $registry; /** * @var WebsiteRepositoryInterface @@ -89,6 +98,11 @@ class CombinationWithDifferentTypePricesTest extends TestCase */ private $productTierPriceExtensionFactory; + /** + * @var ProductCustomOptionInterfaceFactory + */ + private $productCustomOptionFactory; + /** * @inheritdoc */ @@ -96,17 +110,19 @@ protected function setUp(): void { parent::setUp(); $this->objectManager = Bootstrap::getObjectManager(); - $this->page = $this->objectManager->create(Page::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); $this->registry = $this->objectManager->get(Registry::class); $this->indexBuilder = $this->objectManager->get(IndexBuilder::class); $this->customerSession = $this->objectManager->get(Session::class); $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); $this->catalogRuleFactory = $this->objectManager->get(RuleInterfaceFactory::class); $this->catalogRuleRepository = $this->objectManager->get(CatalogRuleRepositoryInterface::class); $this->productTierPriceFactory = $this->objectManager->get(ProductTierPriceInterfaceFactory::class); $this->productTierPriceExtensionFactory = $this->objectManager->get(ProductTierPriceExtensionFactory::class); - $this->productRepository->cleanCache(); + $this->productCustomOptionFactory = $this->objectManager->get(ProductCustomOptionInterfaceFactory::class); } /** @@ -116,29 +132,7 @@ protected function tearDown(): void { parent::tearDown(); $this->registry->unregister('product'); - } - - /** - * Assert that product price rendered with expected special and regular prices if - * product has special price which lower than regular and tier prices. - * - * @magentoDataFixture Magento/Catalog/_files/product_special_price.php - * - * @dataProvider tierPricesForAllCustomerGroupsDataProvider - * - * @param float $specialPrice - * @param float $regularPrice - * @param array $tierPrices - * @param array|null $tierMessageConfig - * @return void - */ - public function testRenderSpecialPriceInCombinationWithTierPrice( - float $specialPrice, - float $regularPrice, - array $tierPrices, - ?array $tierMessageConfig - ): void { - $this->assertRenderedPrices($specialPrice, $regularPrice, $tierPrices, $tierMessageConfig); + $this->registry->unregister('current_product'); } /** @@ -150,79 +144,48 @@ public function tierPricesForAllCustomerGroupsDataProvider(): array { return [ 'fixed_tier_price_with_qty_1' => [ - 5.99, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 9], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 9]], + 'message_config' => null, ], - null ], 'fixed_tier_price_with_qty_2' => [ - 5.99, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'value' => 5], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'value' => 5]], + 'message_config' => ['qty' => 2, 'price' => 5.00, 'percent' => 17], ], - ['qty' => 2, 'price' => 5.00, 'percent' => 17], ], 'percent_tier_price_with_qty_2' => [ - 5.99, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70]], + 'message_config' => ['qty' => 2, 'price' => 3.00, 'percent' => 50], ], - ['qty' => 2, 'price' => 3.00, 'percent' => 50], ], 'fixed_tier_price_with_qty_1_is_lower_than_special' => [ - 5, - 10, - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 5], + 'special_price' => 5, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 5]], + 'message_config' => null, ], - null ], 'percent_tier_price_with_qty_1_is_lower_than_special' => [ - 3, - 10, - [ - ['customer_group_id' => Group::NOT_LOGGED_IN_ID, 'qty' => 1, 'percent_value' => 70], + 'special_price' => 3, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::NOT_LOGGED_IN_ID, 'qty' => 1, 'percent_value' => 70]], + 'message_config' => null, ], - null ], ]; } - /** - * Assert that product price rendered with expected special and regular prices if - * product has special price which lower than regular and tier prices and customer is logged. - * - * @magentoDataFixture Magento/Catalog/_files/product_special_price.php - * @magentoDataFixture Magento/Customer/_files/customer.php - * - * @magentoAppIsolation enabled - * - * @dataProvider tierPricesForLoggedCustomerGroupDataProvider - * - * @param float $specialPrice - * @param float $regularPrice - * @param array $tierPrices - * @param array|null $tierMessageConfig - * @return void - */ - public function testRenderSpecialPriceInCombinationWithTierPriceForLoggedInUser( - float $specialPrice, - float $regularPrice, - array $tierPrices, - ?array $tierMessageConfig - ): void { - try { - $this->customerSession->setCustomerId(1); - $this->assertRenderedPrices($specialPrice, $regularPrice, $tierPrices, $tierMessageConfig); - } finally { - $this->customerSession->setCustomerId(null); - } - } - /** * Data provider with tier prices which are for logged customers group. * @@ -232,52 +195,24 @@ public function tierPricesForLoggedCustomerGroupDataProvider(): array { return [ 'fixed_tier_price_with_qty_1' => [ - 5.99, - 10, - [ - ['customer_group_id' => 1, 'qty' => 1, 'value' => 9], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => 1, 'qty' => 1, 'value' => 9]], + 'message_config' => null, ], - null ], 'percent_tier_price_with_qty_1' => [ - 5.99, - 10, - [ - ['customer_group_id' => 1, 'qty' => 1, 'percent_value' => 30], + 'special_price' => 5.99, + 'regular_price' => 10, + 'tier_data' => [ + 'prices' => [['customer_group_id' => 1, 'qty' => 1, 'percent_value' => 30]], + 'message_config' => null, ], - null ], ]; } - /** - * Assert that product price rendered with expected special and regular prices if - * product has catalog rule price with different type of prices. - * - * @magentoDataFixture Magento/Catalog/_files/product_special_price.php - * @magentoDataFixture Magento/CatalogRule/_files/delete_catalog_rule_data.php - * - * @dataProvider catalogRulesDataProvider - * - * @param float $specialPrice - * @param float $regularPrice - * @param array $catalogRules - * @param array $tierPrices - * @param array|null $tierMessageConfig - * @return void - */ - public function testRenderCatalogRulePriceInCombinationWithDifferentPriceTypes( - float $specialPrice, - float $regularPrice, - array $catalogRules, - array $tierPrices, - ?array $tierMessageConfig - ): void { - $this->createCatalogRulesForProduct($catalogRules); - $this->indexBuilder->reindexFull(); - $this->assertRenderedPrices($specialPrice, $regularPrice, $tierPrices, $tierMessageConfig); - } - /** * Data provider with expect special and regular price, catalog rule data and tier price. * @@ -287,84 +222,99 @@ public function catalogRulesDataProvider(): array { return [ 'fixed_catalog_rule_price_more_than_special_price' => [ - 5.99, - 10, - [ + 'special_price' => 5.99, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 2], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'fixed_catalog_rule_price_lower_than_special_price' => [ - 2, - 10, - [ + 'special_price' => 2, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 8], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'fixed_catalog_rule_price_more_than_tier_price' => [ - 4, - 10, - [ + 'special_price' => 4, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 6], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 2, 'percent_value' => 70]], + 'message_config' => ['qty' => 2, 'price' => 3.00, 'percent' => 25], ], - ['qty' => 2, 'price' => 3.00, 'percent' => 25], ], 'fixed_catalog_rule_price_lower_than_tier_price' => [ - 2, - 10, - [ + 'special_price' => 2, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 7], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 2], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 2]], + 'message_config' => null, ], - null ], 'adjust_percent_catalog_rule_price_lower_than_special_price' => [ - 4.50, - 10, - [ + 'special_price' => 4.50, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 45, RuleInterface::SIMPLE_ACTION => 'to_percent'], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'adjust_percent_catalog_rule_price_lower_than_tier_price' => [ - 3, - 10, - [ + 'special_price' => 3, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 30, RuleInterface::SIMPLE_ACTION => 'to_percent'], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3.50], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3.50]], + 'message_config' => null, ], - null ], 'percent_catalog_rule_price_lower_than_special_price' => [ - 2, - 10, - [ + 'special_price' => 2, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 2, RuleInterface::SIMPLE_ACTION => 'to_fixed'], ], - [], - null + 'tier_data' => ['prices' => [], 'message_config' => null], ], 'percent_catalog_rule_price_lower_than_tier_price' => [ - 1, - 10, - [ + 'special_price' => 1, + 'regular_price' => 10, + 'catalog_rules' => [ [RuleInterface::DISCOUNT_AMOUNT => 1, RuleInterface::SIMPLE_ACTION => 'to_fixed'], ], - [ - ['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3], + 'tier_data' => [ + 'prices' => [['customer_group_id' => Group::CUST_GROUP_ALL, 'qty' => 1, 'value' => 3]], + 'message_config' => null, ], - null + ], + ]; + } + + /** + * Data provider with percent customizable option prices. + * + * @return array + */ + public function percentCustomOptionsDataProvider(): array + { + return [ + 'percent_option_for_product_without_special_price' => [ + 'option_price' => 5, + 'product_prices' => ['special_price' => null], + ], + 'percent_option_for_product_with_special_price' => [ + 'option_price' => 3, + 'product_prices' => ['special_price' => 5.99], ], ]; } @@ -377,7 +327,7 @@ public function catalogRulesDataProvider(): array * @param float $regularPrice * @return void */ - private function checkPrices(string $priceHtml, float $specialPrice, float $regularPrice): void + protected function checkPrices(string $priceHtml, float $specialPrice, float $regularPrice): void { $this->assertEquals( 1, @@ -403,7 +353,7 @@ private function checkPrices(string $priceHtml, float $specialPrice, float $regu * @param array $tierMessageConfig * @return void */ - private function checkTierPriceMessage(string $priceHtml, array $tierMessageConfig): void + protected function checkTierPriceMessage(string $priceHtml, array $tierMessageConfig): void { $this->assertEquals( 1, @@ -418,38 +368,40 @@ private function checkTierPriceMessage(string $priceHtml, array $tierMessageConf * @param ProductInterface $product * @return string */ - private function getPriceHtml(ProductInterface $product): string + protected function getPriceHtml(ProductInterface $product): string { - $this->registerProduct($product); - $this->page->addHandle([ - 'default', - 'catalog_product_view', - ]); - $this->page->getLayout()->generateXml(); - $priceHtml = ''; - $availableChildNames = [ - 'product.info.price', - 'product.price.tier' - ]; - foreach ($this->page->getLayout()->getChildNames('product.info.main') as $childName) { - if (in_array($childName, $availableChildNames, true)) { - $priceHtml .= $this->page->getLayout()->renderElement($childName, false); - } - } + $this->preparePageLayout($product); + $priceHtml = $this->page->getLayout()->renderElement('product.info.price', false); + $priceHtml .= $this->page->getLayout()->renderElement('product.price.tier', false); return $priceHtml; } + /** + * Render custom options price render template with product. + * + * @param ProductInterface $product + * @return string + */ + protected function getCustomOptionsPriceHtml(ProductInterface $product): string + { + $this->preparePageLayout($product); + + return $this->page->getLayout()->renderElement('product.info.options', false); + } + /** * Add product to the registry. * * @param ProductInterface $product * @return void */ - private function registerProduct(ProductInterface $product): void + protected function registerProduct(ProductInterface $product): void { $this->registry->unregister('product'); $this->registry->register('product', $product); + $this->registry->unregister('current_product'); + $this->registry->register('current_product', $product); } /** @@ -457,10 +409,14 @@ private function registerProduct(ProductInterface $product): void * * @param ProductInterface $product * @param array $tierPrices + * @param int $websiteId * @return ProductInterface */ - private function createTierPricesForProduct(ProductInterface $product, array $tierPrices): ProductInterface - { + protected function createTierPricesForProduct( + ProductInterface $product, + array $tierPrices, + int $websiteId + ): ProductInterface { if (empty($tierPrices)) { return $product; } @@ -468,7 +424,7 @@ private function createTierPricesForProduct(ProductInterface $product, array $ti $createdTierPrices = []; foreach ($tierPrices as $tierPrice) { $tierPriceExtensionAttribute = $this->productTierPriceExtensionFactory->create(); - $tierPriceExtensionAttribute->setWebsiteId(0); + $tierPriceExtensionAttribute->setWebsiteId($websiteId); if (isset($tierPrice['percent_value'])) { $tierPriceExtensionAttribute->setPercentageValue($tierPrice['percent_value']); @@ -487,10 +443,35 @@ private function createTierPricesForProduct(ProductInterface $product, array $ti } /** + * Add custom option to product with data. + * + * @param ProductInterface $product + * @return void + */ + protected function addOptionToProduct(ProductInterface $product): void + { + $optionData = [ + Option::KEY_PRODUCT_SKU => $product->getSku(), + Option::KEY_TITLE => 'Test option field title', + Option::KEY_TYPE => ProductCustomOptionInterface::OPTION_TYPE_FIELD, + Option::KEY_IS_REQUIRE => 0, + Option::KEY_PRICE => 50, + Option::KEY_PRICE_TYPE => ProductPriceOptionsInterface::VALUE_PERCENT, + Option::KEY_SKU => 'test-option-field-title', + ]; + $option = $this->productCustomOptionFactory->create(['data' => $optionData]); + $option->setProductSku($product->getSku()); + $product->setOptions([$option]); + $product->setHasOptions(true); + } + + /** + * Returns xpath for special price. + * * @param float $specialPrice * @return string */ - private function getSpecialPriceXpath(float $specialPrice): string + protected function getSpecialPriceXpath(float $specialPrice): string { $pathsForSearch = [ "//div[contains(@class, 'price-box') and contains(@class, 'price-final_price')]", @@ -502,10 +483,12 @@ private function getSpecialPriceXpath(float $specialPrice): string } /** + * Returns xpath for regular price. + * * @param float $regularPrice * @return string */ - private function getRegularPriceXpath(float $regularPrice): string + protected function getRegularPriceXpath(float $regularPrice): string { $pathsForSearch = [ "//div[contains(@class, 'price-box') and contains(@class, 'price-final_price')]", @@ -518,28 +501,29 @@ private function getRegularPriceXpath(float $regularPrice): string } /** + * Returns xpath for regular price label. + * * @return string */ - private function getRegularPriceLabelXpath(): string + protected function getRegularPriceLabelXpath(): string { $pathsForSearch = [ "//div[contains(@class, 'price-box') and contains(@class, 'price-final_price')]", "//span[contains(@class, 'old-price')]", "//span[contains(@class, 'price-container')]", - "//span[text()='Regular Price']", + sprintf("//span[normalize-space(text())='%s']", __('Regular Price')), ]; return implode('', $pathsForSearch); } /** - * Return tier price message xpath. Message must contain expected quantity, - * price and discount percent. + * Return tier price message xpath. Message must contain expected quantity, price and discount percent. * * @param array $expectedMessage * @return string */ - private function getTierPriceMessageXpath(array $expectedMessage): string + protected function getTierPriceMessageXpath(array $expectedMessage): string { [$qty, $price, $percent] = array_values($expectedMessage); $liPaths = [ @@ -557,36 +541,60 @@ private function getTierPriceMessageXpath(array $expectedMessage): string /** * Process test with combination of special and tier price. * + * @param string $sku * @param float $specialPrice * @param float $regularPrice - * @param array $tierPrices - * @param array|null $tierMessageConfig + * @param array $tierData + * @param int $websiteId * @return void */ - private function assertRenderedPrices( + public function assertRenderedPrices( + string $sku, float $specialPrice, float $regularPrice, - array $tierPrices, - ?array $tierMessageConfig + array $tierData, + int $websiteId = 0 ): void { - $product = $this->productRepository->get('simple', false, null, true); - $product = $this->createTierPricesForProduct($product, $tierPrices); + $product = $this->getProduct($sku); + $product = $this->createTierPricesForProduct($product, $tierData['prices'], $websiteId); $priceHtml = $this->getPriceHtml($product); $this->checkPrices($priceHtml, $specialPrice, $regularPrice); - if (null !== $tierMessageConfig) { - $this->checkTierPriceMessage($priceHtml, $tierMessageConfig); + if (null !== $tierData['message_config']) { + $this->checkTierPriceMessage($priceHtml, $tierData['message_config']); } } + /** + * Process test with combination of special and custom option price. + * + * @param string $sku + * @param float $optionPrice + * @param array $productPrices + * @return void + */ + public function assertRenderedCustomOptionPrices( + string $sku, + float $optionPrice, + array $productPrices + ): void { + $product = $this->getProduct($sku); + $product->addData($productPrices); + $this->addOptionToProduct($product); + $this->productRepository->save($product); + $priceHtml = $this->getCustomOptionsPriceHtml($this->getProduct($sku)); + $this->assertStringContainsString(sprintf('data-price-amount="%s"', $optionPrice), $priceHtml); + } + /** * Create provided catalog rules. * * @param array $catalogRules + * @param string $websiteCode * @return void */ - private function createCatalogRulesForProduct(array $catalogRules): void + protected function createCatalogRulesForProduct(array $catalogRules, string $websiteCode): void { - $baseWebsite = $this->websiteRepository->get('base'); + $baseWebsite = $this->websiteRepository->get($websiteCode); $staticRuleData = [ RuleInterface::IS_ACTIVE => 1, RuleInterface::NAME => 'Test rule name.', @@ -605,4 +613,36 @@ private function createCatalogRulesForProduct(array $catalogRules): void $this->catalogRuleRepository->save($catalogRule); } } + + /** + * Loads product by sku. + * + * @param string $sku + * @return ProductInterface + */ + protected function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get( + $sku, + false, + null, + true + ); + } + + /** + * Prepares product page layout. + * + * @param ProductInterface $product + * @return void + */ + private function preparePageLayout(ProductInterface $product): void + { + $this->registerProduct($product); + $this->page->addHandle([ + 'default', + 'catalog_product_view', + ]); + $this->page->getLayout()->generateXml(); + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationTest.php new file mode 100644 index 0000000000000..f30e0492fc23e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/CombinationTest.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\Pricing\Render\PriceTypes; + +/** + * Assertions related to check product price rendering with combination of different price types. + * + * @magentoDbIsolation disabled + * @magentoAppArea frontend + * @magentoAppIsolation enabled + */ +class CombinationTest extends CombinationAbstract +{ + /** + * Assert that product price rendered with expected special and regular prices if + * product has special price which lower than regular and tier prices. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * + * @dataProvider tierPricesForAllCustomerGroupsDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPrice( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + $this->assertRenderedPrices('simple', $specialPrice, $regularPrice, $tierData); + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has special price which lower than regular and tier prices and customer is logged. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @magentoAppIsolation enabled + * + * @dataProvider tierPricesForLoggedCustomerGroupDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPriceForLoggedInUser( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + try { + $this->customerSession->setCustomerId(1); + $this->assertRenderedPrices('simple', $specialPrice, $regularPrice, $tierData); + } finally { + $this->customerSession->setCustomerId(null); + } + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has catalog rule price with different type of prices. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * @magentoDataFixture Magento/CatalogRule/_files/delete_catalog_rule_data.php + * + * @dataProvider catalogRulesDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $catalogRules + * @param array $tierData + * @return void + */ + public function testRenderCatalogRulePriceInCombinationWithDifferentPriceTypes( + float $specialPrice, + float $regularPrice, + array $catalogRules, + array $tierData + ): void { + $this->createCatalogRulesForProduct($catalogRules, 'base'); + $this->indexBuilder->reindexFull(); + $this->assertRenderedPrices('simple', $specialPrice, $regularPrice, $tierData); + } + + /** + * Assert that product price rendered with expected custom option price if product has special price. + * + * @magentoDataFixture Magento/Catalog/_files/product_special_price.php + * + * @dataProvider percentCustomOptionsDataProvider + * + * @param float $optionPrice + * @param array $productPrices + * @return void + */ + public function testRenderSpecialPriceInCombinationWithCustomOptionPrice( + float $optionPrice, + array $productPrices + ): void { + $this->assertRenderedCustomOptionPrices('simple', $optionPrice, $productPrices); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/MultiWebsiteCombinationTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/MultiWebsiteCombinationTest.php new file mode 100644 index 0000000000000..852acf63a3f64 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/Pricing/Render/PriceTypes/MultiWebsiteCombinationTest.php @@ -0,0 +1,188 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Catalog\Pricing\Render\PriceTypes; + +use Magento\Framework\View\Result\PageFactory; +use Magento\TestFramework\Store\ExecuteInStoreContext; + +/** + * Assertions related to check product price rendering with combination of different price types on second website. + * + * @magentoDbIsolation disabled + * @magentoAppArea frontend + */ +class MultiWebsiteCombinationTest extends CombinationAbstract +{ + /** + * @var ExecuteInStoreContext + */ + private $executeInStoreContext; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has special price which lower than regular and tier prices on second website. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * @dataProvider tierPricesForAllCustomerGroupsDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPrice( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedPrices'], + 'second-website-price-product', + $specialPrice, + $regularPrice, + $tierData, + (int)$this->storeManager->getStore('fixture_second_store')->getWebsiteId() + ); + $this->assertRenderedPricesOnDefaultStore('second-website-price-product'); + } + + /** + * Assert that product price rendered with expected special and regular prices on second website if + * product has special price which lower than regular and tier prices and customer is logged. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @magentoAppIsolation enabled + * + * @dataProvider tierPricesForLoggedCustomerGroupDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $tierData + * @return void + */ + public function testRenderSpecialPriceInCombinationWithTierPriceForLoggedInUser( + float $specialPrice, + float $regularPrice, + array $tierData + ): void { + try { + $this->customerSession->setCustomerId(1); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedPrices'], + 'second-website-price-product', + $specialPrice, + $regularPrice, + $tierData, + (int)$this->storeManager->getStore('fixture_second_store')->getWebsiteId() + ); + $this->assertRenderedPricesOnDefaultStore('second-website-price-product'); + } finally { + $this->customerSession->setCustomerId(null); + } + } + + /** + * Assert that product price rendered with expected special and regular prices if + * product has catalog rule price with different type of prices on second website. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * @magentoDataFixture Magento/CatalogRule/_files/delete_catalog_rule_data.php + * + * @dataProvider catalogRulesDataProvider + * + * @param float $specialPrice + * @param float $regularPrice + * @param array $catalogRules + * @param array $tierData + * @return void + */ + public function testRenderCatalogRulePriceInCombinationWithDifferentPriceTypes( + float $specialPrice, + float $regularPrice, + array $catalogRules, + array $tierData + ): void { + $this->createCatalogRulesForProduct($catalogRules, 'test'); + $this->indexBuilder->reindexFull(); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedPrices'], + 'second-website-price-product', + $specialPrice, + $regularPrice, + $tierData, + (int)$this->storeManager->getStore('fixture_second_store')->getWebsiteId() + ); + $this->assertRenderedPricesOnDefaultStore('second-website-price-product'); + } + + /** + * Assert that product price rendered with expected custom option price if product has special price. + * + * @magentoDataFixture Magento/Catalog/_files/product_with_price_on_second_website.php + * + * @dataProvider percentCustomOptionsDataProvider + * + * @param float $optionPrice + * @param array $productPrices + * @return void + */ + public function testRenderSpecialPriceInCombinationWithCustomOptionPrice( + float $optionPrice, + array $productPrices + ): void { + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertRenderedCustomOptionPrices'], + 'second-website-price-product', + $optionPrice, + $productPrices + ); + $this->assertRenderedCustomOptionPricesOnDefaultStore('second-website-price-product'); + } + + /** + * Checks price data for product on default store. + * + * @param string $sku + * @return void + */ + private function assertRenderedPricesOnDefaultStore(string $sku): void + { + //Reset layout page to get new block html + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $defaultStoreTierData = ['prices' => [], 'message_config' => null]; + $this->assertRenderedPrices($sku, 15, 20, $defaultStoreTierData); + } + + /** + * Checks custom option price data for product on default store. + * + * @param string $sku + * @return void + */ + private function assertRenderedCustomOptionPricesOnDefaultStore(string $sku): void + { + //Reset layout page to get new block html + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $this->assertRenderedCustomOptionPrices($sku, 7.5, []); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php index a95a981cb8006..1115a48c79ef4 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/AbstractEavTest.php @@ -220,11 +220,13 @@ protected function getOptionValueByLabel(string $attributeCode, string $label): /** * Returns product for testing. * + * @param bool $forceReload * @return ProductInterface + * @throws \Magento\Framework\Exception\NoSuchEntityException */ - protected function getProduct(): ProductInterface + protected function getProduct($forceReload = false): ProductInterface { - return $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID); + return $this->productRepository->get('simple', false, Store::DEFAULT_STORE_ID, $forceReload); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php index fbf752cc9e239..b5005ba9fc76a 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/Eav/DefaultAttributesTest.php @@ -33,7 +33,8 @@ public function testModifyMeta(): void public function testModifyData(): void { $expectedData = include __DIR__ . '/../_files/eav_expected_data_output.php'; - $this->callModifyDataAndAssert($this->getProduct(), $expectedData); + // force load: ProductRepositoryInterface::getList does not add stock item, prices, categories to product + $this->callModifyDataAndAssert($this->getProduct(true), $expectedData); } /** diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php index 1c709ffcacec7..72d96334e0335 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/EavTest.php @@ -9,6 +9,7 @@ use Magento\Eav\Api\AttributeSetRepositoryInterface; use Magento\Eav\Model\AttributeSetRepository; +use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\TestFramework\Eav\Model\GetAttributeGroupByName; use Magento\TestFramework\Eav\Model\ResourceModel\GetEntityIdByAttributeId; @@ -34,6 +35,9 @@ class EavTest extends AbstractEavTest */ private $setRepository; + /** @var ScopeConfigInterface */ + private $config; + /** * @inheritdoc */ @@ -43,6 +47,7 @@ protected function setUp(): void $this->attributeGroupByName = $this->objectManager->get(GetAttributeGroupByName::class); $this->getEntityIdByAttributeId = $this->objectManager->get(GetEntityIdByAttributeId::class); $this->setRepository = $this->objectManager->get(AttributeSetRepositoryInterface::class); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); } /** @@ -217,4 +222,92 @@ private function prepareAttributeSet(array $additional): void $set->organizeData(array_merge($data, $additional)); $this->setRepository->save($set); } + + /** + * @magentoDataFixture Magento/Catalog/_files/attribute_page_layout_default.php + * @dataProvider testModifyMetaNewProductPageLayoutDefaultProvider + * @return void + */ + public function testModifyMetaNewProductPageLayoutDefault($attributesMeta): void + { + $defaultLayout = $this->config->getValue('web/default_layouts/default_product_layout'); + if ($defaultLayout) { + $attributesMeta = array_merge($attributesMeta, ['default' => $defaultLayout]); + } + $expectedMeta = $this->addMetaNesting( + $attributesMeta, + 'design', + 'page_layout' + ); + $this->callModifyMetaAndAssert($this->getNewProduct(), $expectedMeta); + } + + /** + * @return array + */ + public function testModifyMetaNewProductPageLayoutDefaultProvider(): array + { + return [ + 'attributes_meta' => [ + [ + 'dataType' => 'select', + 'formElement' => 'select', + 'visible' => '1', + 'required' => false, + 'label' => 'Layout', + 'code' => 'page_layout', + 'source' => 'design', + 'scopeLabel' => '[STORE VIEW]', + 'globalScope' => false, + 'sortOrder' => '__placeholder__', + 'options' => + [ + 0 => + [ + 'value' => '', + 'label' => 'No layout updates', + '__disableTmpl' => true, + ], + 1 => + [ + 'label' => 'Empty', + 'value' => 'empty', + '__disableTmpl' => true, + ], + 2 => + [ + 'label' => '1 column', + 'value' => '1column', + '__disableTmpl' => true, + ], + 3 => + [ + 'label' => '2 columns with left bar', + 'value' => '2columns-left', + '__disableTmpl' => true, + ], + 4 => + [ + 'label' => '2 columns with right bar', + 'value' => '2columns-right', + '__disableTmpl' => true, + ], + 5 => + [ + 'label' => '3 columns', + 'value' => '3columns', + '__disableTmpl' => true, + ], + ], + 'componentType' => 'field', + 'disabled' => true, + 'validation' => + [ + 'required' => false, + ], + 'serviceDisabled' => true, + ] + ] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php index 38fcc4554d391..6645c1fe7751f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/Ui/DataProvider/Product/Form/Modifier/LayoutUpdateTest.php @@ -58,6 +58,12 @@ class LayoutUpdateTest extends TestCase */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Catalog\Model\Product\Attribute\LayoutUpdateManager::class => + \Magento\TestFramework\Catalog\Model\ProductLayoutUpdateManager::class + ] + ]); $this->locator = $this->getMockForAbstractClass(LocatorInterface::class); $store = Bootstrap::getObjectManager()->create(StoreInterface::class); $this->locator->method('getStore')->willReturn($store); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php new file mode 100644 index 0000000000000..c8222ac565dc7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +$attribute->loadByCode($entityType, 'page_layout'); +$attribute->setData('default_value', '1column'); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php new file mode 100644 index 0000000000000..f762574a2efd1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/attribute_page_layout_default_rollback.php @@ -0,0 +1,21 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$installer = $objectManager->create(CategorySetup::class); +$attribute = $objectManager->create(AttributeFactory::class)->create(); +$attributeRepository = $objectManager->create(ProductAttributeRepositoryInterface::class); +$entityType = $installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); +$attribute->loadByCode($entityType, 'page_layout'); +$attribute->setData('default_value', null); +$attributeRepository->save($attribute); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php index 29b4a05c4dcbe..6b85b27929c2c 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute.php @@ -4,9 +4,15 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ -$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +/** @var Attribute $attribute */ + +use Magento\Catalog\Model\Category\AttributeFactory; +use Magento\Catalog\Model\Category\Attribute; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var AttributeFactory $attributeFactory */ +$attributeFactory = Bootstrap::getObjectManager()->get(AttributeFactory::class); +$attribute = $attributeFactory->create(); $attribute->setAttributeCode('test_attribute_code_666') ->setEntityTypeId(3) ->setIsGlobal(1) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php index 34114703de344..2cae71c35b916 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_attribute_rollback.php @@ -4,15 +4,22 @@ * See COPYING.txt for license details. */ -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +/** @var Registry $registry */ + +use Magento\Catalog\Model\Category\AttributeFactory; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */ -$attribute = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); +/** @var AttributeFactory $attributeFactory */ +$attributeFactory = $objectManager->get(AttributeFactory::class); +$attribute = $attributeFactory->create(); $attribute->loadByCode(3, 'test_attribute_code_666'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php index d4f2c803187dc..0198b82df2629 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/category_product_rollback.php @@ -22,3 +22,5 @@ if ($category->getId()) { $category->delete(); } +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php index 2acb7fe99e192..c2c3782c8cd23 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/configurable_products_with_custom_attribute_layered_navigation.php @@ -5,13 +5,14 @@ */ declare(strict_types=1); -use Magento\Eav\Api\AttributeRepositoryInterface; -use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_products.php'); +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; + $eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ @@ -28,11 +29,5 @@ /** @var AttributeRepositoryInterface $attributeRepository */ $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); + CacheCleaner::cleanAll(); -/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ -$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); -$indexerCollection->load(); -/** @var \Magento\Indexer\Model\Indexer $indexer */ -foreach ($indexerCollection->getItems() as $indexer) { - $indexer->reindexAll(); -} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php index dcdbed7562fdb..559a1109cd420 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/multiple_products.php @@ -13,7 +13,7 @@ ->setName('Simple Product1') ->setSku('simple1') ->setTaxClassId('none') - ->setDescription('description') + ->setDescription('description uniqueword') ->setShortDescription('short description') ->setOptionsContainer('container1') ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer.php new file mode 100644 index 0000000000000..17bf50bc76352 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Model\Product\Compare\ListCompare; +use Magento\Catalog\Model\Product\Compare\ListCompareFactory; +use Magento\Customer\Model\Session; +use Magento\Customer\Model\Visitor; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); + +try { + $session->loginById(1); + /** @var Visitor $visitor */ + $visitor = $objectManager->get(Visitor::class); + $visitor->setVisitorId(1); + /** @var ListCompare $compareList */ + $compareList = $objectManager->get(ListCompareFactory::class)->create(); + $compareList->addProduct(6); +} finally { + $session->logout(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer_rollback.php new file mode 100644 index 0000000000000..dc948702b7f53 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_in_compare_list_with_customer_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php index 514c6563622c9..a7e4f702e5630 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple.php @@ -95,7 +95,7 @@ ->setPrice(10) ->setWeight(1) ->setShortDescription("Short description") - ->setTaxClassId(0) + ->setTaxClassId(2) ->setTierPrices($tierPrices) ->setDescription('Description with <b>html tag</b>') ->setExtensionAttributes($productExtensionAttributesWebsiteIds) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_disabled.php new file mode 100644 index 0000000000000..85209b8569645 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_disabled.php @@ -0,0 +1,209 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\Data\ProductExtensionInterfaceFactory; + +\Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); + +/** @var \Magento\TestFramework\ObjectManager $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var \Magento\Catalog\Api\CategoryLinkManagementInterface $categoryLinkManagement */ +$categoryLinkManagement = $objectManager->get(\Magento\Catalog\Api\CategoryLinkManagementInterface::class); + +$tierPrices = []; +/** @var \Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory $tierPriceFactory */ +$tierPriceFactory = $objectManager->get(\Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory::class); +/** @var $tpExtensionAttributes */ +$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +/** @var $productExtensionAttributes */ +$productExtensionAttributesFactory = $objectManager->get(ProductExtensionInterfaceFactory::class); + +$adminWebsite = $objectManager->get(\Magento\Store\Api\WebsiteRepositoryInterface::class)->get('admin'); +$tierPriceExtensionAttributes1 = $tpExtensionAttributesFactory->create() + ->setWebsiteId($adminWebsite->getId()); +$productExtensionAttributesWebsiteIds = $productExtensionAttributesFactory->create( + ['website_ids' => $adminWebsite->getId()] +); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 2, + 'value' => 8 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::CUST_GROUP_ALL, + 'qty' => 5, + 'value' => 5 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 3, + 'value' => 5 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 3.2, + 'value' => 6, + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes1); + +$tierPriceExtensionAttributes2 = $tpExtensionAttributesFactory->create() + ->setWebsiteId($adminWebsite->getId()) + ->setPercentageValue(50); + +$tierPrices[] = $tierPriceFactory->create( + [ + 'data' => [ + 'customer_group_id' => \Magento\Customer\Model\Group::NOT_LOGGED_IN_ID, + 'qty' => 10 + ] + ] +)->setExtensionAttributes($tierPriceExtensionAttributes2); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(1) + ->setAttributeSetId(4) + ->setWebsiteIds([1]) + ->setName('Simple Product') + ->setSku('simple') + ->setPrice(10) + ->setWeight(1) + ->setShortDescription("Short description") + ->setTaxClassId(0) + ->setTierPrices($tierPrices) + ->setDescription('Description with <b>html tag</b>') + ->setExtensionAttributes($productExtensionAttributesWebsiteIds) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_DISABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ] + )->setCanSaveCustomOptions(true) + ->setHasOptions(true); + +$oldOptions = [ + [ + 'previous_group' => 'text', + 'title' => 'Test Field', + 'type' => 'field', + 'is_require' => 1, + 'sort_order' => 0, + 'price' => 1, + 'price_type' => 'fixed', + 'sku' => '1-text', + 'max_characters' => 100, + ], + [ + 'previous_group' => 'date', + 'title' => 'Test Date and Time', + 'type' => 'date_time', + 'is_require' => 1, + 'sort_order' => 0, + 'price' => 2, + 'price_type' => 'fixed', + 'sku' => '2-date', + ], + [ + 'previous_group' => 'select', + 'title' => 'Test Select', + 'type' => 'drop_down', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '3-1-select', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '3-2-select', + ], + ] + ], + [ + 'previous_group' => 'select', + 'title' => 'Test Radio', + 'type' => 'radio', + 'is_require' => 1, + 'sort_order' => 0, + 'values' => [ + [ + 'option_type_id' => null, + 'title' => 'Option 1', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '4-1-radio', + ], + [ + 'option_type_id' => null, + 'title' => 'Option 2', + 'price' => 3, + 'price_type' => 'fixed', + 'sku' => '4-2-radio', + ], + ] + ] +]; + +$options = []; + +/** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory $customOptionFactory */ +$customOptionFactory = $objectManager->create(\Magento\Catalog\Api\Data\ProductCustomOptionInterfaceFactory::class); + +foreach ($oldOptions as $option) { + /** @var \Magento\Catalog\Api\Data\ProductCustomOptionInterface $option */ + $option = $customOptionFactory->create(['data' => $option]); + $option->setProductSku($product->getSku()); + + $options[] = $option; +} + +$product->setOptions($options); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +$productRepository->save($product); + +$categoryLinkManagement->assignProductToCategories( + $product->getSku(), + [2] +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php index 4ed783100fa98..7c8ce4c63034d 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_custom_attribute.php @@ -20,6 +20,9 @@ $entityTypeId = $entityModel->setType(\Magento\Catalog\Model\Product::ENTITY)->getTypeId(); $groupId = $installer->getDefaultAttributeGroupId($entityTypeId, $attributeSetId); +/** @var \Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple', true); + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = $objectManager->create(\Magento\Catalog\Model\ResourceModel\Eav\Attribute::class); $attribute->setAttributeCode( @@ -30,6 +33,8 @@ 'text' )->setFrontendLabel( 'custom_attributes_frontend_label' +)->setAttributeSetId( + $product->getDefaultAttributeSetId() )->setAttributeGroupId( $groupId )->setIsFilterable( @@ -40,8 +45,6 @@ $attribute->getBackendTypeByInput($attribute->getFrontendInput()) )->save(); -$product = $productRepository->get('simple', true); - $product->setCustomAttribute($attribute->getAttributeCode(), 'customAttributeValue'); $productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php index d8222d0ce5c49..0dbcb998da836 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_simple_with_url_key.php @@ -13,6 +13,7 @@ ->setSku('simple1') ->setPrice(10) ->setDescription('Description with <b>html tag</b>') + ->setTaxClassId(2) ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setCategoryIds([2]) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website.php new file mode 100644 index 0000000000000..fd746578fcbf7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->saveConfig(Data::XML_PATH_PRICE_SCOPE, Store::PRICE_SCOPE_WEBSITE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +/** @var SwitchPriceAttributeScopeOnConfigChange $observer */ +$observer = $objectManager->get(Observer::class); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$websiteId = $websiteRepository->get('test')->getId(); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$secondStoreId = $storeManager->getStore('fixture_second_store')->getId(); +/** @var $product \Magento\Catalog\Model\Product */ +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$defaultWebsiteId, $websiteId]) + ->setName('Second website price product') + ->setSku('second-website-price-product') + ->setPrice(20) + ->setSpecialPrice(15) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_in_stock' => 1 + ] + ); +$productRepository->save($product); + +try { + $currentStoreCode = $storeManager->getStore()->getCode(); + $storeManager->setCurrentStore('fixture_second_store'); + $product = $productRepository->get('second-website-price-product', false, $secondStoreId, true); + $product->setPrice(10) + ->setSpecialPrice(5.99); + $productRepository->save($product); +} finally { + $storeManager->setCurrentStore($currentStoreCode); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php new file mode 100644 index 0000000000000..ce8d54d02a9ab --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/product_with_price_on_second_website_rollback.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\Event\Observer; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->deleteConfig(Data::XML_PATH_PRICE_SCOPE, 'default', 0); +$observer = $objectManager->get(Observer::class); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +try { + $productRepository->deleteById('second-website-price-product'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php index 5a1dd30c6b492..8c1de09d2d8e2 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_for_relevance_sorting_rollback.php @@ -4,6 +4,10 @@ * See COPYING.txt for license details. */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Framework/Search/_files/products_rollback.php'); + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); /** @var \Magento\Framework\Registry $registry */ $registry = $objectManager->get(\Magento\Framework\Registry::class); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php index 93c4fa854c7f3..aa8bd2ca9c89b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_list.php @@ -29,6 +29,7 @@ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\Product::class); $product + ->setId(153) ->setTypeId('simple') ->setAttributeSetId(4) ->setWebsiteIds([1]) @@ -49,6 +50,7 @@ $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Model\Product::class); $product + ->setId(156) ->setTypeId('simple') ->setAttributeSetId(4) ->setWebsiteIds([1]) diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php index 7bee46bc2078f..29812aa942ab5 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute.php @@ -7,6 +7,7 @@ use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; $eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); $attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); @@ -59,6 +60,7 @@ /* Assign attribute to attribute set */ $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); + CacheCleaner::cleanAll(); } $eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php new file mode 100644 index 0000000000000..c2ebfa4389ab2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options.php @@ -0,0 +1,162 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$attribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + /** @var $store \Magento\Store\Model\Store */ + $store = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Store::class); + $store = $store->load('test', 'code'); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => [ + Store::DEFAULT_STORE_ID => 'Option Admin Store', + Store::DISTRO_STORE_ID => 'Option Default Store', + $store->getId() => 'Option Test Store' + ], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); +} + +$eavConfig->clear(); + +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(10) + ->setAttributeSetId(4) + ->setName('Simple Product1') + ->setSku('simple1') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_IN_CART) + ->setPrice(10) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('5.99') + ->save(); + +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->isObjectNew(true); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) + ->setId(11) + ->setAttributeSetId(4) + ->setName('Simple Product2') + ->setSku('simple2') + ->setTaxClassId('none') + ->setDescription('description') + ->setShortDescription('short description') + ->setOptionsContainer('container1') + ->setMsrpDisplayActualPriceType(\Magento\Msrp\Model\Product\Attribute\Source\Type::TYPE_ON_GESTURE) + ->setPrice(20) + ->setWeight(1) + ->setMetaTitle('meta title') + ->setMetaKeyword('meta keyword') + ->setMetaDescription('meta description') + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) + ->setWebsiteIds([1]) + ->setCategoryIds([]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 50, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) + ->setSpecialPrice('15.99') + ->save(); + +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->isObjectNew(true); +$category->setId( + 333 +)->setCreatedAt( + '2014-06-23 09:50:07' +)->setName( + 'Category 1' +)->setParentId( + 2 +)->setPath( + '1/2/333' +)->setLevel( + 2 +)->setAvailableSortBy( + ['position', 'name'] +)->setDefaultSortBy( + 'name' +)->setIsActive( + true +)->setPosition( + 1 +)->setPostedProducts( + [10 => 10, 11 => 11] +)->save(); + +/** @var \Magento\Indexer\Model\Indexer\Collection $indexerCollection */ +$indexerCollection = Bootstrap::getObjectManager()->get(\Magento\Indexer\Model\Indexer\Collection::class); +$indexerCollection->load(); + +/** @var \Magento\Indexer\Model\Indexer $indexer */ +foreach ($indexerCollection->getItems() as $indexer) { + $indexer->reindexAll(); +} diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php new file mode 100644 index 0000000000000..6793051b5787b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_attribute_store_options_rollback.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +foreach (['simple1', 'simple2'] as $sku) { + try { + $product = $productRepository->get($sku, false, null, true); + $productRepository->delete($product); + } catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { + //Product already removed + } +} + +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +/** @var $category \Magento\Catalog\Model\Category */ +$category = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Category::class); +$category->load(333); +if ($category->getId()) { + $category->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php index 379bf33ac4e3d..4dd088e148d75 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute.php @@ -5,15 +5,9 @@ */ declare(strict_types=1); -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute; -use Magento\Catalog\Setup\CategorySetup; -use Magento\Eav\Model\Config; -use Magento\Framework\Exception\NoSuchEntityException; -use Magento\Indexer\Model\Indexer; -use Magento\Indexer\Model\Indexer\Collection as IndexerCollection; use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; use Magento\TestFramework\Eav\Model\GetAttributeSetByName; @@ -24,94 +18,119 @@ /** @var GetAttributeSetByName $getAttributeSetByName */ $getAttributeSetByName = $objectManager->get(GetAttributeSetByName::class); $attributeSet = $getAttributeSetByName->execute('second_attribute_set'); -/** @var Config $eavConfig */ -$eavConfig = $objectManager->get(Config::class); -/** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->create(AttributeRepositoryInterface::class); -/** @var CategorySetup $installer */ -$installer = $objectManager->create(CategorySetup::class); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + $eavConfig->clear(); -$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); +$attribute1 = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + if (!$attribute->getId()) { - /** @var $attribute Attribute */ - $attribute->setData([ - 'attribute_code' => 'test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 0, - 'is_visible_in_advanced_search' => 0, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default_value' => 'option_0' - ]); + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute->setData( + [ + 'attribute_code' => 'test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 1'], 'option_1' => ['Option 2']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default_value' => 'option_0' + ] + ); + $attributeRepository->save($attribute); /* Assign attribute to attribute set */ $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $attribute->getId()); } - // create a second attribute -/** @var Attribute $secondAttribute */ -$secondAttribute = $eavConfig->getAttribute('catalog_product', ' second_test_configurable'); -if (!$secondAttribute->getId()) { - $secondAttribute->setData([ - 'attribute_code' => 'second_test_configurable', - 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), - 'is_global' => 1, - 'is_user_defined' => 1, - 'frontend_input' => 'select', - 'is_unique' => 0, - 'is_required' => 0, - 'is_searchable' => 0, - 'is_visible_in_advanced_search' => 0, - 'is_comparable' => 1, - 'is_filterable' => 1, - 'is_filterable_in_search' => 1, - 'is_used_for_promo_rules' => 0, - 'is_html_allowed_on_front' => 1, - 'is_visible_on_front' => 1, - 'used_in_product_listing' => 1, - 'used_for_sort_by' => 1, - 'frontend_label' => ['Second Test Configurable'], - 'backend_type' => 'int', - 'option' => [ - 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], - 'order' => ['option_0' => 1, 'option_1' => 2], - ], - 'default' => ['option_0'] - ]); - $attributeRepository->save($secondAttribute); +if (!$attribute1->getId()) { + + /** @var $attribute1 \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute1 = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $attribute1->setData( + [ + 'attribute_code' => 'second_test_configurable', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 1, + 'is_visible_in_advanced_search' => 1, + 'is_comparable' => 1, + 'is_filterable' => 1, + 'is_filterable_in_search' => 1, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 1, + 'frontend_label' => ['Second Test Configurable'], + 'backend_type' => 'int', + 'option' => [ + 'value' => ['option_0' => ['Option 3'], 'option_1' => ['Option 4']], + 'order' => ['option_0' => 1, 'option_1' => 2], + ], + 'default' => ['option_0'] + ] + ); + + $attributeRepository->save($attribute1); /* Assign attribute to attribute set */ $installer->addAttributeToGroup( 'catalog_product', $attributeSet->getId(), $attributeSet->getDefaultGroupId(), - $secondAttribute->getId() + $attribute1->getId() ); } $eavConfig->clear(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var \Magento\Framework\ObjectManagerInterface $objectManager */ +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +/** @var $productRepository \Magento\Catalog\Api\ProductRepositoryInterface */ +$productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); $productsWithNewAttributeSet = ['simple', '12345', 'simple-4']; foreach ($productsWithNewAttributeSet as $sku) { @@ -125,14 +144,8 @@ 'is_in_stock' => 1] ); $productRepository->save($product); - } catch (NoSuchEntityException $e) { + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { } } - -/** @var IndexerCollection $indexerCollection */ -$indexerCollection = $objectManager->get(IndexerCollection::class)->load(); -/** @var Indexer $indexer */ -foreach ($indexerCollection->getItems() as $indexer) { - $indexer->reindexAll(); -} +CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php index f291127fe855d..6e1b20da18f18 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_custom_attribute_rollback.php @@ -5,44 +5,43 @@ */ declare(strict_types=1); -use Magento\Eav\Api\Data\AttributeInterface; -use Magento\Eav\Model\Config; -use Magento\Eav\Model\Entity\Attribute\Set as AttributeSet; -use Magento\Eav\Model\Entity\Type; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection as AttributeSetCollection; -use Magento\Framework\App\ObjectManager; use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Eav/_files/empty_attribute_set_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/categories_rollback.php'); -/** @var ObjectManager $objectManager */ -$objectManager = Bootstrap::getObjectManager(); -$eavConfig = $objectManager->get(Config::class); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); $attributesToDelete = ['test_configurable', 'second_test_configurable']; /** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->get(AttributeRepositoryInterface::class); +$attributeRepository = Bootstrap::getObjectManager()->get(AttributeRepositoryInterface::class); foreach ($attributesToDelete as $attributeCode) { - /** @var AttributeInterface $attribute */ + /** @var \Magento\Eav\Api\Data\AttributeInterface $attribute */ $attribute = $attributeRepository->get('catalog_product', $attributeCode); $attributeRepository->delete($attribute); } +/** @var $product \Magento\Catalog\Model\Product */ +$objectManager = Bootstrap::getObjectManager(); + +$entityType = $objectManager->create(\Magento\Eav\Model\Entity\Type::class)->loadByCode('catalog_product'); // remove attribute set -$entityType = $objectManager->create(Type::class)->loadByCode('catalog_product'); -/** @var AttributeSetCollection $attributeSetCollection */ + +/** @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attributeSetCollection */ $attributeSetCollection = $objectManager->create( - AttributeSetCollection::class + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection::class ); $attributeSetCollection->addFilter('attribute_set_name', 'second_attribute_set'); $attributeSetCollection->addFilter('entity_type_id', $entityType->getId()); -$attributeSetCollection->setOrder('attribute_set_id'); +$attributeSetCollection->setOrder('attribute_set_id'); // descending is default value $attributeSetCollection->setPageSize(1); $attributeSetCollection->load(); -/** @var AttributeSet $attributeSet */ +/** @var \Magento\Eav\Model\Entity\Attribute\Set $attributeSet */ $attributeSet = $attributeSetCollection->fetchItem(); $attributeSet->delete(); + +CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php index 81aad017d9619..42df6330d0dcf 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute.php @@ -4,54 +4,51 @@ * See COPYING.txt for license details. */ -use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\Product; -use Magento\Catalog\Model\Product\Attribute\Source\Status; -use Magento\Catalog\Model\Product\Type as ProductType; -use Magento\Catalog\Model\Product\Visibility; -use Magento\Catalog\Model\ResourceModel\Eav\Attribute as EavAttribute; -use Magento\Catalog\Setup\CategorySetup; -use Magento\Eav\Model\Config as EavConfig; -use Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection as OptionCollection; -use Magento\Indexer\Model\Indexer; -use Magento\Indexer\Model\Indexer\Collection as IndexerCollection; use Magento\TestFramework\Helper\Bootstrap; use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\CacheCleaner; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +/** + * Create multiselect attribute + */ Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiselect_attribute.php'); + /** Create product with options and multiselect attribute */ -$objectManager = Bootstrap::getObjectManager(); -/** @var CategorySetup $installer */ -$installer = $objectManager->create(CategorySetup::class); +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Setup\CategorySetup::class +); -/** @var OptionCollection $options */ -$options = $objectManager->create(OptionCollection::class); -$eavConfig = $objectManager->get(EavConfig::class); +/** @var $options \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection */ +$options = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Eav\Model\ResourceModel\Entity\Attribute\Option\Collection::class +); +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); -/** @var $attribute EavAttribute */ +/** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ $attribute = $eavConfig->getAttribute('catalog_product', 'multiselect_attribute'); $eavConfig->clear(); $attribute->setIsSearchable(1) ->setIsVisibleInAdvancedSearch(1) - ->setIsFilterable(false) - ->setIsFilterableInSearch(false) - ->setIsVisibleOnFront(0); + ->setIsFilterable(true) + ->setIsFilterableInSearch(true) + ->setIsVisibleOnFront(1); /** @var AttributeRepositoryInterface $attributeRepository */ -$attributeRepository = $objectManager->create(AttributeRepositoryInterface::class); +$attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); $attributeRepository->save($attribute); $options->setAttributeFilter($attribute->getId()); $optionIds = $options->getAllIds(); -/** @var ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); -/** @var Product $product */ -$product = $objectManager->create(Product::class); -$product->setTypeId(ProductType::TYPE_SIMPLE) +/** @var $product \Magento\Catalog\Model\Product */ +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId($optionIds[0] * 10) ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) ->setWebsiteIds([1]) @@ -59,45 +56,39 @@ ->setSku('simple_ms_1') ->setPrice(10) ->setDescription('Hello " &" Bring the water bottle when you can!') - ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setMultiselectAttribute([$optionIds[1],$optionIds[2]]) - ->setStatus(Status::STATUS_ENABLED) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); -$product = $objectManager->create(Product::class); -$product->setTypeId(ProductType::TYPE_SIMPLE) +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId($optionIds[1] * 10) ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) ->setWebsiteIds([1]) ->setName('With Multiselect 2 and 3') ->setSku('simple_ms_2') ->setPrice(10) - ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) - ->setStatus(Status::STATUS_ENABLED) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); -$product = $objectManager->create(Product::class); -$product->setTypeId(ProductType::TYPE_SIMPLE) +$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); +$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) ->setId($optionIds[2] * 10) ->setAttributeSetId($installer->getAttributeSetId('catalog_product', 'Default')) ->setWebsiteIds([1]) ->setName('With Multiselect 1 and 3') ->setSku('simple_ms_2') ->setPrice(10) - ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) ->setMultiselectAttribute([$optionIds[2], $optionIds[3]]) - ->setStatus(Status::STATUS_ENABLED) + ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); $productRepository->save($product); -/** @var IndexerCollection $indexerCollection */ -$indexerCollection = $objectManager->get(IndexerCollection::class); -$indexerCollection->load(); -/** @var Indexer $indexer */ -foreach ($indexerCollection->getItems() as $indexer) { - $indexer->reindexAll(); -} +CacheCleaner::cleanAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php index 5bc32e97db955..0e8d1f6f1022e 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_layered_navigation_with_multiselect_attribute_rollback.php @@ -3,29 +3,37 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -use Magento\Framework\Indexer\IndexerRegistry; + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiselect_attribute_rollback.php'); + /** * Remove all products as strategy of isolation process */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$registry = $objectManager->get('Magento\Framework\Registry'); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ -$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Model\Product') - ->getCollection(); -foreach ($productCollection as $product) { - $product->delete(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, 'simple_ms_%', 'like'); + +/** @var ProductSearchResultsInterface $products */ +$products = $productRepository->getList($searchCriteriaBuilder->create()); +/** @var ProductInterface $product */ +foreach ($products->getItems() as $product) { + $productRepository->delete($product); } $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); - -\Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(IndexerRegistry::class) - ->get(Magento\CatalogInventory\Model\Indexer\Stock\Processor::INDEXER_ID) - ->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php index 3056bf6cc5384..dd7081eaf508b 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model.php @@ -8,7 +8,6 @@ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/multiselect_attribute_with_source_model.php'); -Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/ValidatorFileMock.php'); /** Create product with options and multiselect attribute */ diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php index 658b6d8e8908a..786a8f1d90a50 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_multiselect_attribute_with_source_model_rollback.php @@ -3,26 +3,36 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductSearchResultsInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture( 'Magento/Catalog/_files/multiselect_attribute_with_source_model_rollback.php' ); + /** * Remove all products as strategy of isolation process */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get('Magento\Framework\Registry'); +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$registry = $objectManager->get('Magento\Framework\Registry'); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var $productCollection \Magento\Catalog\Model\ResourceModel\Product */ -$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create('Magento\Catalog\Model\Product') - ->getCollection(); +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$searchCriteriaBuilder->addFilter(ProductInterface::SKU, 'simple_mssm_%', 'like'); -foreach ($productCollection as $product) { - $product->delete(); +/** @var ProductSearchResultsInterface $products */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$products = $productRepository->getList($searchCriteriaBuilder->create()); +/** @var ProductInterface $product */ +foreach ($products->getItems() as $product) { + $productRepository->delete($product); } $registry->unregister('isSecureArea'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php index 6d74d85c0c819..58994e51a7a9f 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores.php @@ -3,40 +3,59 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductFactory; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores.php'); -$website = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\Website::class); -/** @var $website \Magento\Store\Model\Website */ -$websiteId = $website->load('test', 'code')->getId(); +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsiteId = $websiteRepository->get('base')->getId(); +$secondWebsiteId = $websiteRepository->get('test')->getId(); +$defaultCategoryId = $objectManager->get(DefaultCategory::class)->getId(); +$stockData = ['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]; -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setAttributeSetId(4) - ->setWebsiteIds([$websiteId]) +$product = $productFactory->create(); +$attributeSetId = $product->getDefaultAttributeSetId(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([$secondWebsiteId]) ->setName('Simple Product on second website') ->setSku('simple-2') ->setPrice(10) ->setDescription('Description with <b>html tag</b>') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setCategoryIds([2]) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$defaultCategoryId]) + ->setStockData($stockData); +$productRepository->save($product); -/** @var $product \Magento\Catalog\Model\Product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->setTypeId(\Magento\Catalog\Model\Product\Type::TYPE_SIMPLE) - ->setAttributeSetId(4) - ->setWebsiteIds([1]) +$secondProduct = $productFactory->create(); +$secondProduct->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([$baseWebsiteId]) ->setName('Simple Product') ->setSku('simple-1') + ->setUrlKey('simple_product_uniq_key_1') ->setPrice(10) ->setDescription('Description with <b>html tag</b>') - ->setVisibility(\Magento\Catalog\Model\Product\Visibility::VISIBILITY_BOTH) - ->setStatus(\Magento\Catalog\Model\Product\Attribute\Source\Status::STATUS_ENABLED) - ->setCategoryIds([2]) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]) - ->save(); + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$defaultCategoryId]) + ->setStockData($stockData); +$productRepository->save($secondProduct); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php index dcd849279ae39..f152438fe93f6 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/products_with_websites_and_stores_rollback.php @@ -3,30 +3,33 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -use Magento\TestFramework\Workaround\Override\Fixture\Resolver; - -Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); +declare(strict_types=1); -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; -/** @var \Magento\Framework\Registry $registry */ -$registry = $objectManager->get(\Magento\Framework\Registry::class); +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ -$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); - try { foreach (['simple-2', 'simple-1'] as $sku) { - $product = $productRepository->get($sku, false, null, true); - $productRepository->delete($product); + $productRepository->deleteById($sku); } -} catch (\Magento\Framework\Exception\NoSuchEntityException $exception) { +} catch (NoSuchEntityException $exception) { //Product already removed } $registry->unregister('isSecureArea'); $registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_two_stores_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/second_product_simple_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_product_simple_rollback.php index 27e60d29805ff..b045bfe4f6977 100644 --- a/dev/tests/integration/testsuite/Magento/Catalog/_files/second_product_simple_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/second_product_simple_rollback.php @@ -3,19 +3,27 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); -/** @var \Magento\Framework\Registry $registry */ -$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); -/** @var \Magento\Catalog\Model\Product $product */ -$product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); -$product->load(6); - -if ($product->getId()) { - $product->delete(); +try { + $productRepository->deleteById('simple2'); +} catch (NoSuchEntityException $e) { + //Product already removed } $registry->unregister('isSecureArea'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols.php new file mode 100644 index 0000000000000..235aedc66f3f6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Catalog\Api\ProductRepositoryInterface; + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$baseWebsiteId = $websiteRepository->get('base')->getId(); + +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsiteId]) + ->setName('Простий продукт') + ->setSku('Продукт') + ->setDescription('Повний опис продукту') + ->setShortDescription('Короткий опис') + ->setPrice(10) + ->setTaxClassId(0) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + [ + 'qty' => 100, + 'is_in_stock' => 1, + 'manage_stock' => 1, + ] + ); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols_rollback.php new file mode 100644 index 0000000000000..3b33077988f35 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_cyrillic_symbols_rollback.php @@ -0,0 +1,30 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +/** @var Registry $registry */ +$registry = Bootstrap::getObjectManager()->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); + +try { + $product = $productRepository->get('Продукт', false, null, true); + $productRepository->delete($product); +} catch (NoSuchEntityException $exception) { + //Product already removed +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled.php new file mode 100644 index 0000000000000..60dcfc4ea0d24 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); +/** @var DefaultCategory $defaultCategory */ +$defaultCategory = $objectManager->get(DefaultCategory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::ATTRIBUTE_SET_ID => $product->getDefaultAttributeSetId(), + ProductInterface::SKU => 'product_disabled', + ProductInterface::NAME => 'Product with category', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_DISABLED, + 'website_ids' => [$defaultWebsiteId], + 'stock_data' => [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ], + 'category_ids' => [$defaultCategory->getId()], +]; +$product->setData($productData); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled_rollback.php new file mode 100644 index 0000000000000..afd874f1b38b1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_disabled_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('product_disabled'); +} catch (NoSuchEntityException $e) { + // product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty.php new file mode 100644 index 0000000000000..e8666ad9a13cd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory ProductInterfaceFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setStatus(Status::STATUS_ENABLED) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product min and max sale qty') + ->setSku('simple_product_min_max_sale_qty') + ->setPrice(10) + ->setWeight(1) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + 'min_sale_qty' => 5, + 'max_sale_qty' => 20, + ] + ) + ->setCanSaveCustomOptions(true) + ->setHasOptions(true); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty_rollback.php new file mode 100644 index 0000000000000..bc06240d2e9a3 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_min_max_sale_qty_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('simple_product_min_max_sale_qty'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments.php new file mode 100644 index 0000000000000..bf425e2e57874 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductInterfaceFactory ProductInterfaceFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +/** @var ProductRepositoryInterface $productRepositoryFactory */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); + +$product = $productFactory->create(); +$product->setTypeId(Type::TYPE_SIMPLE) + ->setStatus(Status::STATUS_ENABLED) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([1]) + ->setName('Simple Product with qty increments') + ->setSku('simple_product_with_qty_increments') + ->setPrice(10) + ->setWeight(1) + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStockData( + [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + 'enable_qty_increments' => 1, + 'qty_increments' => 3, + ] + ) + ->setCanSaveCustomOptions(true) + ->setHasOptions(true); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments_rollback.php new file mode 100644 index 0000000000000..d6cd1212daeb8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_qty_increments_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('simple_product_with_qty_increments'); +} catch (NoSuchEntityException $e) { + //product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php new file mode 100644 index 0000000000000..36550b1696ced --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups.php @@ -0,0 +1,71 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductTierPriceInterfaceFactory; +use Magento\Catalog\Api\Data\ProductTierPriceExtensionFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$tierPriceFactory = $objectManager->get(ProductTierPriceInterfaceFactory::class); +$tpExtensionAttributesFactory = $objectManager->get(ProductTierPriceExtensionFactory::class); +$product = $productRepository->get('simple', false, null, true); +$adminWebsite = $objectManager->get(WebsiteRepositoryInterface::class)->get('admin'); +$tierPriceExtensionAttributes = $tpExtensionAttributesFactory->create()->setWebsiteId($adminWebsite->getId()); +$pricesForCustomerGroupsInput = [ + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 9.25 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 8.25 + ], + [ + 'customer_group_id' => 1, + 'percentage_value'=> null, + 'qty'=> 5, + 'value'=> 7.25 + ], + [ + 'customer_group_id' => 2, + 'percentage_value'=> null, + 'qty'=> 2, + 'value'=> 9 + ], + [ + 'customer_group_id' => 2, + 'percentage_value'=> null, + 'qty'=> 3, + 'value'=> 8 + ], + [ + 'customer_group_id' => 2, + 'percentage_value'=> null, + 'qty'=> 5, + 'value'=> 7 + ] +]; +$productTierPrices = []; +foreach ($pricesForCustomerGroupsInput as $price) { + $productTierPrices[] = $tierPriceFactory->create( + [ + 'data' => $price + ] + )->setExtensionAttributes($tierPriceExtensionAttributes); +} +$product->setTierPrices($productTierPrices); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups_rollback.php new file mode 100644 index 0000000000000..328c1e229da5c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/simple_product_with_tier_prices_for_multiple_groups_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product.php new file mode 100644 index 0000000000000..10a06c3b8a239 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +$defaultWebsiteId = $websiteRepository->get('base')->getId(); +/** @var DefaultCategory $defaultCategory */ +$defaultCategory = $objectManager->get(DefaultCategory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var ProductInterfaceFactory $productFactory */ +$productFactory = $objectManager->get(ProductInterfaceFactory::class); +$product = $productFactory->create(); +$productData = [ + ProductInterface::TYPE_ID => Type::TYPE_SIMPLE, + ProductInterface::ATTRIBUTE_SET_ID => $product->getDefaultAttributeSetId(), + ProductInterface::SKU => 'taxable_product', + ProductInterface::NAME => 'Taxable Product', + ProductInterface::PRICE => 10, + ProductInterface::VISIBILITY => Visibility::VISIBILITY_BOTH, + ProductInterface::STATUS => Status::STATUS_ENABLED, + 'website_ids' => [$defaultWebsiteId], + 'stock_data' => [ + 'use_config_manage_stock' => 1, + 'qty' => 100, + 'is_qty_decimal' => 0, + 'is_in_stock' => 1, + ], + 'category_ids' => [$defaultCategory->getId()], + 'tax_class_id' => 2, //Taxable Goods +]; +$product->setData($productData); + +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product_rollback.php b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product_rollback.php new file mode 100644 index 0000000000000..9d58556fc987e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Catalog/_files/taxable_simple_product_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +try { + $productRepository->deleteById('taxable_product'); +} catch (NoSuchEntityException $e) { + // product already deleted +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php index cfd07f57a4cd8..c2f571097f8e9 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/AbstractProductExportImportTestCase.php @@ -9,6 +9,8 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\ImportExport\Model\Export\Adapter\AbstractAdapter; use Magento\Store\Model\Store; +use Magento\TestFramework\Annotation\DataFixture; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; /** * Abstract class for testing product export and import scenarios @@ -68,6 +70,11 @@ abstract class AbstractProductExportImportTestCase extends \PHPUnit\Framework\Te */ private $writer; + /** + * @var string + */ + private $csvFile; + /** * @inheritdoc */ @@ -87,6 +94,11 @@ protected function setUp(): void protected function tearDown(): void { $this->executeFixtures($this->fixtures, true); + + if ($this->csvFile !== null) { + $directoryWrite = $this->fileSystem->getDirectoryWrite(DirectoryList::VAR_DIR); + $directoryWrite->delete($this->csvFile); + } } /** @@ -104,6 +116,7 @@ protected function tearDown(): void */ public function testImportExport(array $fixtures, array $skus, array $skippedAttributes = []): void { + $this->csvFile = null; $this->fixtures = $fixtures; $this->executeFixtures($fixtures); $this->modifyData($skus); @@ -242,6 +255,7 @@ protected function executeImportDeleteTest(array $skus, string $csvFile = null): */ protected function executeFixtures(array $fixtures, bool $rollback = false) { + Resolver::getInstance()->setCurrentFixtureType(DataFixture::ANNOTATION); foreach ($fixtures as $fixture) { $fixturePath = $this->resolveFixturePath($fixture, $rollback); include $fixturePath; @@ -378,6 +392,7 @@ protected function executeImportReplaceTest( private function exportProducts(\Magento\CatalogImportExport\Model\Export\Product $exportProduct = null) { $csvfile = uniqid('importexport_') . '.csv'; + $this->csvFile = $csvfile; $exportProduct = $exportProduct ?: $this->objectManager->create( \Magento\CatalogImportExport\Model\Export\Product::class diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php index 4d08d71793cbb..4502501da4f4f 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest.php @@ -295,6 +295,59 @@ public function testSaveStockItemQty() unset($stockItems, $stockItem); } + /** + * Test that is_in_stock set to 0 when item quantity is 0 + * + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testSaveIsInStockByZeroQty(): void + { + /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ + $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + \Magento\Catalog\Api\ProductRepositoryInterface::class + ); + $id1 = $productRepository->get('simple1')->getId(); + $id2 = $productRepository->get('simple2')->getId(); + $id3 = $productRepository->get('simple3')->getId(); + $existingProductIds = [$id1, $id2, $id3]; + + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->create(\Magento\Framework\Filesystem::class); + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + \Magento\ImportExport\Model\Import\Source\Csv::class, + [ + 'file' => __DIR__ . '/_files/products_to_import_zero_qty.csv', + 'directory' => $directory + ] + ); + $errors = $this->_model->setParameters( + ['behavior' => \Magento\ImportExport\Model\Import::BEHAVIOR_APPEND, 'entity' => 'catalog_product'] + )->setSource( + $source + )->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + + $this->_model->importData(); + + /** @var $stockItmBeforeImport \Magento\CatalogInventory\Model\Stock\Item */ + foreach ($existingProductIds as $productId) { + /** @var $stockRegistry StockRegistry */ + $stockRegistry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( + StockRegistry::class + ); + + $stockItemAfterImport = $stockRegistry->getStockItem($productId, 1); + + $this->assertEquals(0, $stockItemAfterImport->getIsInStock()); + unset($stockItemAfterImport); + } + } + /** * Test if stock state properly changed after import * @@ -3127,4 +3180,98 @@ public function testCheckDoubleImportOfProducts() $productsAfterSecondImport = $this->productRepository->getList($searchCriteria)->getItems(); $this->assertCount(3, $productsAfterSecondImport); } + + /** + * Checks that product related links added for all bunches properly after products import + */ + public function testImportProductsWithLinksInDifferentBunches() + { + $this->importedProducts = [ + 'simple1', + 'simple2', + 'simple3', + 'simple4', + 'simple5', + 'simple6', + ]; + $importExportData = $this->getMockBuilder(Data::class) + ->disableOriginalConstructor() + ->getMock(); + $importExportData->expects($this->atLeastOnce()) + ->method('getBunchSize') + ->willReturn(5); + $this->_model = $this->objectManager->create( + \Magento\CatalogImportExport\Model\Import\Product::class, + ['importExportData' => $importExportData] + ); + $linksData = [ + 'related' => [ + 'simple1' => '2', + 'simple2' => '1' + ] + ]; + $pathToFile = __DIR__ . '/_files/products_to_import_with_related.csv'; + $filesystem = $this->objectManager->create(Filesystem::class); + + $directory = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = $this->objectManager->create( + Csv::class, + [ + 'file' => $pathToFile, + 'directory' => $directory + ] + ); + $errors = $this->_model->setSource($source) + ->setParameters( + [ + 'behavior' => Import::BEHAVIOR_APPEND, + 'entity' => 'catalog_product' + ] + ) + ->validateData(); + + $this->assertTrue($errors->getErrorsCount() == 0); + $this->_model->importData(); + + $resource = $this->objectManager->get(ProductResource::class); + $productId = $resource->getIdBySku('simple6'); + /** @var Product $product */ + $product = $this->objectManager->create(Product::class); + $product->load($productId); + $productLinks = [ + 'related' => $product->getRelatedProducts() + ]; + $importedProductLinks = []; + foreach ($productLinks as $linkType => $linkedProducts) { + foreach ($linkedProducts as $linkedProductData) { + $importedProductLinks[$linkType][$linkedProductData->getSku()] = $linkedProductData->getPosition(); + } + } + $this->assertEquals($linksData, $importedProductLinks); + } + + /** + * Tests that image name does not have to be prefixed by slash + * + * @magentoDataFixture mediaImportImageFixture + * @magentoDataFixture Magento/Store/_files/core_fixturestore.php + * @magentoDataFixture Magento/Catalog/_files/product_with_image.php + */ + public function testUpdateImageByNameNotPrefixedWithSlash() + { + $expectedLabelForDefaultStoreView = 'image label updated'; + $expectedImageFile = '/m/a/magento_image.jpg'; + $secondStoreCode = 'fixturestore'; + $productSku = 'simple'; + $this->importDataForMediaTest('import_image_name_without_slash.csv'); + $product = $this->getProductBySku($productSku); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(1, $imageItems); + $imageItem = array_shift($imageItems); + $this->assertEquals($expectedImageFile, $imageItem->getFile()); + $this->assertEquals($expectedLabelForDefaultStoreView, $imageItem->getLabel()); + $product = $this->getProductBySku($productSku, $secondStoreCode); + $imageItems = $product->getMediaGalleryImages()->getItems(); + $this->assertCount(0, $imageItems); + } } diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv new file mode 100644 index 0000000000000..415501daf89d8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/import_image_name_without_slash.csv @@ -0,0 +1,3 @@ +"sku","store_view_code","base_image","base_image_label","hide_from_product_page" +"simple",,"m/a/magento_image.jpg","image label updated", +"simple","fixturestore",,,"m/a/magento_image.jpg" diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv new file mode 100644 index 0000000000000..3627cdc24ec41 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_with_related.csv @@ -0,0 +1,7 @@ +sku,product_type,store_view_code,name,price,qty,attribute_set_code,related_skus,related_position +simple1,simple,,simple 1,25,10,Default,, +simple2,simple,,simple 2,34,10,Default,, +simple3,simple,,simple 3,58,10,Default,"simple1,simple2","1,2" +simple4,simple,,simple 4,67,10,Default,"simple1,simple2","2,1" +simple5,simple,,simple 5,58,10,Default,"simple1,simple2","1,2" +simple6,simple,,simple 6,67,10,Default,"simple1,simple2","2,1" \ No newline at end of file diff --git a/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_zero_qty.csv b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_zero_qty.csv new file mode 100644 index 0000000000000..632d60cf7daa0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/_files/products_to_import_zero_qty.csv @@ -0,0 +1,4 @@ +sku,qty,is_in_stock +simple1,0,1 +simple2,0,1 +simple3,0,1 diff --git a/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php b/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php index 76a4ff9714ebd..6d679a5aea7d4 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogSearch/Block/ResultTest.php @@ -12,6 +12,7 @@ use Magento\Framework\View\LayoutInterface; use Magento\Search\Model\QueryFactory; use Magento\TestFramework\Helper\Bootstrap; +use Magento\Search\ViewModel\ConfigProvider; class ResultTest extends \PHPUnit\Framework\TestCase { @@ -25,6 +26,11 @@ class ResultTest extends \PHPUnit\Framework\TestCase */ private $layout; + /** + * @var ConfigProvider + */ + private $configProvider; + /** * @inheritdoc */ @@ -32,9 +38,15 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->layout = $this->objectManager->get(LayoutInterface::class); + $this->configProvider = $this->objectManager->get(ConfigProvider::class); } - public function testSetListOrders() + /** + * Set list orders test + * + * @return void + */ + public function testSetListOrders(): void { $this->layout->addBlock(Text::class, 'head'); // The tested block is using head block @@ -62,6 +74,7 @@ public function testEscapeSearchText(string $searchValue, string $expectedOutput $searchResultBlock = $this->layout->createBlock(Result::class); /** @var Template $searchBlock */ $searchBlock = $this->layout->createBlock(Template::class); + $searchBlock->setData(['configProvider' => $this->configProvider]); $searchBlock->setTemplate('Magento_Search::form.mini.phtml'); /** @var RequestInterface $request */ $request = $this->objectManager->get(RequestInterface::class); diff --git a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php index d87a7ffd48c09..f8837f8d9c5d6 100644 --- a/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php +++ b/dev/tests/integration/testsuite/Magento/CatalogUrlRewrite/Model/CategoryUrlRewriteTest.php @@ -9,18 +9,23 @@ use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\CategoryRepositoryInterface; -use Magento\Catalog\Api\Data\CategoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\CategoryFactory; -use Magento\Catalog\Model\ResourceModel\Category as CategoryResource; +use Magento\Catalog\Model\ResourceModel\CategoryFactory as CategoryResourceFactory; use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap; use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap; use Magento\CatalogUrlRewrite\Model\ResourceModel\Category\Product; -use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Api\StoreRepositoryInterface; use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Helper\Bootstrap; use Magento\UrlRewrite\Model\Exception\UrlAlreadyExistsException; use Magento\UrlRewrite\Model\OptionProvider; use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollection; +use Magento\UrlRewrite\Model\ResourceModel\UrlRewriteCollectionFactory; use Magento\UrlRewrite\Service\V1\Data\UrlRewrite; +use PHPUnit\Framework\TestCase; /** * Class for category url rewrites tests @@ -29,22 +34,34 @@ * @magentoConfigFixture default/catalog/seo/generate_category_product_rewrites 1 * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class CategoryUrlRewriteTest extends AbstractUrlRewriteTest +class CategoryUrlRewriteTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CategoryFactory */ + private $categoryFactory; + + /** @var UrlRewriteCollectionFactory */ + private $urlRewriteCollectionFactory; + /** @var CategoryRepositoryInterface */ private $categoryRepository; - /** @var CategoryResource */ - private $categoryResource; + /** @var CategoryResourceFactory */ + private $categoryResourceFactory; /** @var CategoryLinkManagementInterface */ - private $categoryLinkManagement; + private $categoryLinkManagment; - /** @var CategoryFactory */ - private $categoryFactory; + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var StoreRepositoryInterface */ + private $storeRepository; - /** @var string */ - private $suffix; + /** @var ScopeConfigInterface */ + private $config; /** * @inheritdoc @@ -53,19 +70,18 @@ protected function setUp(): void { parent::setUp(); - $this->categoryRepository = $this->objectManager->create(CategoryRepositoryInterface::class); - $this->categoryResource = $this->objectManager->get(CategoryResource::class); - $this->categoryLinkManagement = $this->objectManager->create(CategoryLinkManagementInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); $this->categoryFactory = $this->objectManager->get(CategoryFactory::class); - $this->suffix = $this->config->getValue( - CategoryUrlPathGenerator::XML_PATH_CATEGORY_URL_SUFFIX, - ScopeInterface::SCOPE_STORE - ); + $this->urlRewriteCollectionFactory = $this->objectManager->get(UrlRewriteCollectionFactory::class); + $this->categoryRepository = $this->objectManager->create(CategoryRepositoryInterface::class); + $this->categoryResourceFactory = $this->objectManager->get(CategoryResourceFactory::class); + $this->categoryLinkManagment = $this->objectManager->create(CategoryLinkManagementInterface::class); + $this->productRepository = $this->objectManager->create(ProductRepositoryInterface::class); + $this->storeRepository = $this->objectManager->create(StoreRepositoryInterface::class); + $this->config = $this->objectManager->get(ScopeConfigInterface::class); } /** - * Test url rewrite after category save - * * @magentoDataFixture Magento/Catalog/_files/category_with_position.php * @dataProvider categoryProvider * @param array $data @@ -73,18 +89,25 @@ protected function setUp(): void */ public function testUrlRewriteOnCategorySave(array $data): void { - $categoryModel = $this->saveCategory($data['data']); + $categoryModel = $this->categoryFactory->create(); + $categoryModel->isObjectNew(true); + $categoryModel->setData($data['data']); + $categoryResource = $this->categoryResourceFactory->create(); + $categoryResource->save($categoryModel); $this->assertNotNull($categoryModel->getId(), 'The category was not created'); - $urlRewriteCollection = $this->getEntityRewriteCollection($categoryModel->getId()); - $this->assertRewrites( - $urlRewriteCollection, - $this->prepareData($data['expected_data'], (int)$categoryModel->getId()) - ); + $urlRewriteCollection = $this->getCategoryRewriteCollection($categoryModel->getId()); + foreach ($urlRewriteCollection as $item) { + foreach ($data['expected_data'] as $field => $expectedItem) { + $this->assertEquals( + sprintf($expectedItem, $categoryModel->getId()), + $item[$field], + 'The expected data does not match actual value' + ); + } + } } /** - * Provider. categoryProvider - * * @return array */ public function categoryProvider(): array @@ -100,10 +123,8 @@ public function categoryProvider(): array 'is_active' => true, ], 'expected_data' => [ - [ - 'request_path' => 'test-category%suffix%', - 'target_path' => 'catalog/category/view/id/%id%', - ], + 'request_path' => 'test-category.html', + 'target_path' => 'catalog/category/view/id/%s', ], ], ], @@ -117,10 +138,8 @@ public function categoryProvider(): array 'is_active' => true, ], 'expected_data' => [ - [ - 'request_path' => 'category-1/test-sub-category%suffix%', - 'target_path' => 'catalog/category/view/id/%id%', - ], + 'request_path' => 'category-1/test-sub-category.html', + 'target_path' => 'catalog/category/view/id/%s', ], ], ], @@ -128,8 +147,6 @@ public function categoryProvider(): array } /** - * Test category product url rewrite - * * @magentoDataFixture Magento/Catalog/_files/category_tree.php * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php * @dataProvider productRewriteProvider @@ -139,16 +156,12 @@ public function categoryProvider(): array public function testCategoryProductUrlRewrite(array $data): void { $category = $this->categoryRepository->get(402); - $this->categoryLinkManagement->assignProductToCategories('simple2', [$category->getId()]); - $productRewriteCollection = $this->getCategoryProductRewriteCollection( - array_keys($category->getParentCategories()) - ); - $this->assertRewrites($productRewriteCollection, $this->prepareData($data)); + $this->categoryLinkManagment->assignProductToCategories('simple2', [$category->getId()]); + $productRewriteCollection = $this->getProductRewriteCollection(array_keys($category->getParentCategories())); + $this->assertRewrites($productRewriteCollection, $data); } /** - * Provider. productRewriteProvider - * * @return array */ public function productRewriteProvider(): array @@ -157,15 +170,15 @@ public function productRewriteProvider(): array [ [ [ - 'request_path' => 'category-1/category-1-1/category-1-1-1/simple-product2%suffix%', + 'request_path' => 'category-1/category-1-1/category-1-1-1/simple-product2.html', 'target_path' => 'catalog/product/view/id/6/category/402', ], [ - 'request_path' => 'category-1/simple-product2%suffix%', + 'request_path' => 'category-1/simple-product2.html', 'target_path' => 'catalog/product/view/id/6/category/400', ], [ - 'request_path' => 'category-1/category-1-1/simple-product2%suffix%', + 'request_path' => 'category-1/category-1-1/simple-product2.html', 'target_path' => 'catalog/product/view/id/6/category/401', ], ], @@ -174,8 +187,6 @@ public function productRewriteProvider(): array } /** - * Test url rewrites after category save with existing url key - * * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_products.php * @magentoAppIsolation enabled * @dataProvider existingUrlProvider @@ -186,12 +197,13 @@ public function testUrlRewriteOnCategorySaveWithExistingUrlKey(array $data): voi { $this->expectException(UrlAlreadyExistsException::class); $this->expectExceptionMessage((string)__('URL key for specified store already exists.')); - $this->saveCategory($data); + $category = $this->categoryFactory->create(); + $category->setData($data); + $categoryResource = $this->categoryResourceFactory->create(); + $categoryResource->save($category); } /** - * Provider. existingUrlProvider - * * @return array */ public function existingUrlProvider(): array @@ -239,8 +251,6 @@ public function existingUrlProvider(): array } /** - * Test url rewrites after category move - * * @magentoDataFixture Magento/Catalog/_files/category_product.php * @magentoDataFixture Magento/Catalog/_files/catalog_category_with_slash.php * @dataProvider categoryMoveProvider @@ -252,12 +262,10 @@ public function testUrlRewriteOnCategoryMove(array $data): void $categoryId = $data['data']['id']; $category = $this->categoryRepository->get($categoryId); $category->move($data['data']['pid'], $data['data']['aid']); - $productRewriteCollection = $this->getCategoryProductRewriteCollection( - array_keys($category->getParentCategories()) - ); - $categoryRewriteCollection = $this->getEntityRewriteCollection($categoryId); - $this->assertRewrites($categoryRewriteCollection, $this->prepareData($data['expected_data']['category'])); - $this->assertRewrites($productRewriteCollection, $this->prepareData($data['expected_data']['product'])); + $productRewriteCollection = $this->getProductRewriteCollection(array_keys($category->getParentCategories())); + $categoryRewriteCollection = $this->getCategoryRewriteCollection($categoryId); + $this->assertRewrites($categoryRewriteCollection, $data['expected_data']['category']); + $this->assertRewrites($productRewriteCollection, $data['expected_data']['product']); } /** @@ -277,21 +285,21 @@ public function categoryMoveProvider(): array 'category' => [ [ 'request_path' => 'category-1.html', - 'target_path' => 'category-with-slash-symbol/category-1%suffix%', + 'target_path' => 'category-with-slash-symbol/category-1.html', 'redirect_type' => OptionProvider::PERMANENT, ], [ - 'request_path' => 'category-with-slash-symbol/category-1%suffix%', + 'request_path' => 'category-with-slash-symbol/category-1.html', 'target_path' => 'catalog/category/view/id/333', ], ], 'product' => [ [ - 'request_path' => 'category-with-slash-symbol/simple-product-three%suffix%', + 'request_path' => 'category-with-slash-symbol/simple-product-three.html', 'target_path' => 'catalog/product/view/id/333/category/3331', ], [ - 'request_path' => 'category-with-slash-symbol/category-1/simple-product-three%suffix%', + 'request_path' => 'category-with-slash-symbol/category-1/simple-product-three.html', 'target_path' => 'catalog/product/view/id/333/category/333', ], ], @@ -302,14 +310,14 @@ public function categoryMoveProvider(): array } /** - * Test url rewrites after category delete * @magentoDataFixture Magento/Catalog/_files/category.php + * @magentoAppArea adminhtml * @return void */ public function testUrlRewritesAfterCategoryDelete(): void { $categoryId = 333; - $categoryItemIds = $this->getEntityRewriteCollection($categoryId)->getAllIds(); + $categoryItemIds = $this->getCategoryRewriteCollection($categoryId)->getAllIds(); $this->categoryRepository->deleteByIdentifier($categoryId); $this->assertEmpty( array_intersect($this->getAllRewriteIds(), $categoryItemIds), @@ -318,8 +326,6 @@ public function testUrlRewritesAfterCategoryDelete(): void } /** - * Test url rewrites after category with products delete - * * @magentoAppArea adminhtml * @magentoDataFixture Magento/CatalogUrlRewrite/_files/categories_with_product_ids.php * @return void @@ -328,8 +334,8 @@ public function testUrlRewritesAfterCategoryWithProductsDelete(): void { $category = $this->categoryRepository->get(3); $childIds = explode(',', $category->getAllChildren()); - $productRewriteIds = $this->getCategoryProductRewriteCollection($childIds)->getAllIds(); - $categoryItemIds = $this->getEntityRewriteCollection($childIds)->getAllIds(); + $productRewriteIds = $this->getProductRewriteCollection($childIds)->getAllIds(); + $categoryItemIds = $this->getCategoryRewriteCollection($childIds)->getAllIds(); $this->categoryRepository->deleteByIdentifier($category->getId()); $allIds = $this->getAllRewriteIds(); $this->assertEmpty( @@ -343,8 +349,6 @@ public function testUrlRewritesAfterCategoryWithProductsDelete(): void } /** - * Test category url rewrite per Store Views - * * @magentoDataFixture Magento/Store/_files/second_store.php * @magentoDataFixture Magento/Catalog/_files/category.php * @return void @@ -360,12 +364,11 @@ public function testCategoryUrlRewritePerStoreViews(): void $categoryId = 333; $category = $this->categoryRepository->get($categoryId); $urlKeyFirstStore = $category->getUrlKey(); - $this->saveCategory( - ['store_id' => $secondStoreId, 'url_key' => $urlKeySecondStore], - $category - ); - $urlRewriteItems = $this->getEntityRewriteCollection($categoryId)->getItems(); - $this->assertTrue(count($urlRewriteItems) == 2); + $category->setStoreId($secondStoreId); + $category->setUrlKey($urlKeySecondStore); + $categoryResource = $this->categoryResourceFactory->create(); + $categoryResource->save($category); + $urlRewriteItems = $this->getCategoryRewriteCollection($categoryId)->getItems(); foreach ($urlRewriteItems as $item) { $item->getData('store_id') == $secondStoreId ? $this->assertEquals($urlKeySecondStore . $urlSuffix, $item->getRequestPath()) @@ -374,103 +377,74 @@ public function testCategoryUrlRewritePerStoreViews(): void } /** - * Test category url rewrite while reassign store view + * Get products url rewrites collection referred to categories * - * @magentoAppArea adminhtml - * @magentoDataFixture Magento/Store/_files/second_store_group_with_second_website.php - * @magentoDataFixture Magento/Catalog/_files/category.php - * @return void + * @param string|array $categoryId + * @return UrlRewriteCollection */ - public function testCategoryUrlRewriteMovingToOtherStoreView(): void + private function getProductRewriteCollection($categoryId): UrlRewriteCollection { - $categoryId = 333; - $store = $this->storeRepository->get('default'); - $storeId = $store->getId(); - $urlRewrites = [ - ['category-1-updated.html', 'category-1.html'], - ['category-1-most-recent.html', 'category-1-updated.html'], - ]; - foreach ($urlRewrites as $rewrite) { - /** @var \Magento\UrlRewrite\Model\UrlRewrite $urlRewrite */ - $urlRewrite = $this->objectManager->create(\Magento\UrlRewrite\Model\UrlRewrite::class); - $urlRewrite->setEntityType(\Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator::ENTITY_TYPE) - ->setEntityId($categoryId) - ->setRequestPath($rewrite[0]) - ->setTargetPath($rewrite[1]) - ->setRedirectType(\Magento\UrlRewrite\Model\OptionProvider::PERMANENT) - ->setStoreId($storeId); - $urlRewrite->save(); - } - - /** @var WebsiteRepositoryInterface $websiteRepo */ - $websiteRepo = $this->objectManager->get(WebsiteRepositoryInterface::class); - $website = $websiteRepo->get('test'); - $group = $website->getDefaultGroup(); - $group->setRootCategoryId(2); - $group->save(); - $groupId = $group->getId(); - $store->setStoreGroupId($groupId); - $store->save(); + $condition = is_array($categoryId) ? ['in' => $categoryId] : $categoryId; + $productRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $productRewriteCollection + ->join( + ['p' => Product::TABLE_NAME], + 'main_table.url_rewrite_id = p.url_rewrite_id', + 'category_id' + ) + ->addFieldToFilter('category_id', $condition) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataProductUrlRewriteDatabaseMap::ENTITY_TYPE]); - $urlRewriteItems = $this->getEntityRewriteCollection($categoryId)->getItems(); - $this->assertTrue(count($urlRewriteItems) === 3); - $expectedRewriteRequestPaths = ['category-1.html', 'category-1-updated.html', 'category-1-most-recent.html']; - foreach ($urlRewriteItems as $item) { - $this->assertTrue(in_array($item->getRequestPath(), $expectedRewriteRequestPaths)); - } + return $productRewriteCollection; } /** - * @inheritdoc + * Retrieve all rewrite ids + * + * @return array */ - protected function getUrlSuffix(): string + private function getAllRewriteIds(): array { - return $this->suffix; - } + $urlRewriteCollection = $this->urlRewriteCollectionFactory->create(); - /** - * @inheritdoc - */ - protected function getEntityType(): string - { - return DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE; + return $urlRewriteCollection->getAllIds(); } /** - * Save product with data using resource model directly + * Get category url rewrites collection * - * @param array $data - * @param CategoryInterface|null $category - * @return CategoryInterface + * @param string|array $categoryId + * @return UrlRewriteCollection */ - private function saveCategory(array $data, $category = null): CategoryInterface + private function getCategoryRewriteCollection($categoryId): UrlRewriteCollection { - $category = $category ?: $this->categoryFactory->create(); - $category->addData($data); - $this->categoryResource->save($category); + $condition = is_array($categoryId) ? ['in' => $categoryId] : $categoryId; + $categoryRewriteCollection = $this->urlRewriteCollectionFactory->create(); + $categoryRewriteCollection->addFieldToFilter(UrlRewrite::ENTITY_ID, $condition) + ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataCategoryUrlRewriteDatabaseMap::ENTITY_TYPE]); - return $category; + return $categoryRewriteCollection; } /** - * Get products url rewrites collection referred to categories + * Check that actual data contains of expected values * - * @param string|array $categoryId - * @return UrlRewriteCollection + * @param UrlRewriteCollection $collection + * @param array $expectedData + * @return void */ - private function getCategoryProductRewriteCollection($categoryId): UrlRewriteCollection + private function assertRewrites(UrlRewriteCollection $collection, array $expectedData): void { - $condition = is_array($categoryId) ? ['in' => $categoryId] : $categoryId; - $productRewriteCollection = $this->urlRewriteCollectionFactory->create(); - $productRewriteCollection - ->join( - ['p' => $this->categoryResource->getTable(Product::TABLE_NAME)], - 'main_table.url_rewrite_id = p.url_rewrite_id', - 'category_id' - ) - ->addFieldToFilter('category_id', $condition) - ->addFieldToFilter(UrlRewrite::ENTITY_TYPE, ['eq' => DataProductUrlRewriteDatabaseMap::ENTITY_TYPE]); - - return $productRewriteCollection; + $collectionItems = $collection->toArray()['items']; + foreach ($collectionItems as $item) { + $found = false; + foreach ($expectedData as $expectedItem) { + $found = array_intersect_assoc($item, $expectedItem) == $expectedItem; + if ($found) { + break; + } + } + $this->assertTrue($found, 'The actual data does not contains of expected values'); + } } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Block/Cart/CrosssellTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Block/Cart/CrosssellTest.php new file mode 100644 index 0000000000000..7a898b4310b3f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Block/Cart/CrosssellTest.php @@ -0,0 +1,353 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Block\Cart; + +use Magento\Catalog\Block\Product\ProductList\AbstractLinksTest; +use Magento\Catalog\ViewModel\Product\Listing\PreparePostData; +use Magento\Checkout\Model\Session; +use Magento\TestFramework\Helper\Xpath; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Store\ExecuteInStoreContext; + +/** + * Check the correct behavior of cross-sell products in the shopping cart + * + * @see \Magento\Checkout\Block\Cart\Crosssell + * @magentoDbIsolation disabled + * @magentoAppIsolation enabled + * @magentoAppArea frontend + */ +class CrosssellTest extends AbstractLinksTest +{ + private const MAX_ITEM_COUNT = 4; + + /** @var Session */ + private $checkoutSession; + + /** @var string */ + private $addToCartButtonXpath = "//div[contains(@class, 'actions-primary')]/button[@type='button']"; + + /** @var string */ + private $addToCartSubmitXpath = "//div[contains(@class, 'actions-primary')]" + . "/form[@data-product-sku='%s']/button[@type='submit']"; + + /** @var string */ + private $addToLinksXpath = "//div[contains(@class, 'actions-secondary')]"; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->block = $this->layout->createBlock(Crosssell::class); + $this->linkType = 'crosssell'; + $this->titleName = (string)__('More Choices:'); + $this->checkoutSession = $this->objectManager->get(Session::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * Checks for a simple cross-sell product when block code is generated + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testSimpleCrosssellProduct(): void + { + $relatedProduct = $this->productRepository->get('simple-1'); + $this->linkProducts('simple', ['simple-1' => ['position' => 2]]); + $this->setCheckoutSessionQuote('test_order_with_simple_product_without_address'); + $this->prepareBlock(); + $html = $this->block->toHtml(); + + $this->assertNotEmpty($html); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->titleXpath, $this->linkType, $this->titleName), $html), + 'Expected title is incorrect or missing!' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($this->addToCartSubmitXpath, $relatedProduct->getSku()), $html), + 'Expected add to cart button is incorrect or missing!' + ); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($this->addToLinksXpath, $html), + 'Expected add to links is incorrect or missing!' + ); + } + + /** + * Checks for a cross-sell product with required option when block code is generated + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/product_virtual_with_options.php + * @return void + */ + public function testCrosssellProductWithRequiredOption(): void + { + $this->linkProducts('simple', ['virtual' => ['position' => 1]]); + $this->setCheckoutSessionQuote('test_order_with_simple_product_without_address'); + $this->prepareBlock(); + $html = $this->block->toHtml(); + + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath($this->addToCartButtonXpath, $html), + 'Expected add to cart button is incorrect or missing!' + ); + } + + /** + * Test the display of cross-sell products in the block + * + * @dataProvider displayLinkedProductsProvider + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @param array $data + * @return void + */ + public function testDisplayCrosssellProducts(array $data): void + { + $this->updateProducts($data['updateProducts']); + $this->linkProducts('simple', $this->existingProducts); + $items = $this->getBlockItems('test_order_with_simple_product_without_address'); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Test the position and max count of cross-sell products in the block + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @return void + */ + public function testPositionCrosssellProducts(): void + { + $positionData = array_merge_recursive( + $this->getPositionData(), + [ + 'productLinks' => [ + 'simple-1' => ['position' => 5], + 'simple2' => ['position' => 4], + ], + 'expectedProductLinks' => [ + 'simple2', + ], + ] + ); + $this->linkProducts('simple', $positionData['productLinks']); + $items = $this->getBlockItems('test_order_with_simple_product_without_address'); + + $this->assertCount( + self::MAX_ITEM_COUNT, + $items, + 'Expected quantity of cross-sell products do not match the actual quantity!' + ); + $this->assertEquals( + $positionData['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Test the position and max count of cross-sell products in the block + * when set last added product in checkout session + * + * @dataProvider positionWithLastAddedProductProvider + * @magentoDataFixture Magento/Sales/_files/quote_with_multiple_products.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Catalog/_files/second_product_simple.php + * @param array $positionData + * @param array $expectedProductLinks + * @return void + */ + public function testPositionCrosssellProductsWithLastAddedProduct( + array $positionData, + array $expectedProductLinks + ): void { + foreach ($positionData as $sku => $productLinks) { + $this->linkProducts($sku, $productLinks); + } + $this->checkoutSession->setLastAddedProductId($this->productRepository->get('simple-tableRate-1')->getId()); + $items = $this->getBlockItems('tableRate'); + + $this->assertCount( + self::MAX_ITEM_COUNT, + $items, + 'Expected quantity of cross-sell products do not match the actual quantity!' + ); + $this->assertEquals( + $expectedProductLinks, + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Provide test data to verify the position of linked products of the last added product. + * + * @return array + */ + public function positionWithLastAddedProductProvider(): array + { + return [ + 'less_four_linked_products_to_last_added_product' => [ + 'positionData' => [ + 'simple-tableRate-1' => [ + 'simple-249' => ['position' => 2], + 'simple-156' => ['position' => 1], + ], + 'simple-tableRate-2' => [ + 'simple-1' => ['position' => 2], + 'simple2' => ['position' => 1], + 'wrong-simple' => ['position' => 3], + ], + ], + 'expectedProductLinks' => [ + 'simple-156', + 'simple-249', + 'simple2', + 'simple-1', + ], + ], + 'four_linked_products_to_last_added_product' => [ + 'positionData' => [ + 'simple-tableRate-1' => [ + 'wrong-simple' => ['position' => 3], + 'simple-249' => ['position' => 1], + 'simple-156' => ['position' => 2], + 'simple2' => ['position' => 4], + ], + 'simple-tableRate-2' => [ + 'simple-1' => ['position' => 1], + ], + ], + 'expectedProductLinks' => [ + 'simple-249', + 'simple-156', + 'wrong-simple', + 'simple2', + ], + ], + ]; + } + + /** + * Test the display of cross-sell products in the block on different websites + * + * @dataProvider multipleWebsitesLinkedProductsProvider + * @magentoDataFixture Magento/Catalog/_files/products_with_websites_and_stores.php + * @magentoDataFixture Magento/Sales/_files/quote_with_multiple_products.php + * @magentoDataFixture Magento/Catalog/_files/products_list.php + * @param array $data + * @return void + */ + public function testMultipleWebsitesCrosssellProducts(array $data): void + { + $this->updateProducts($this->prepareProductsWebsiteIds()); + $productLinks = array_merge($this->existingProducts, $data['productLinks']); + $this->linkProducts('simple-tableRate-1', $productLinks); + $items = $this->executeInStoreContext->execute($data['storeCode'], [$this, 'getBlockItems'], 'tableRate'); + + $this->assertEquals( + $data['expectedProductLinks'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Test the invisibility of cross-sell products in the block which added to cart + * + * @magentoDataFixture Magento/Sales/_files/quote_with_multiple_products.php + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @return void + */ + public function testInvisibilityCrosssellProductAddedToCart(): void + { + $productLinks = [ + 'simple-1' => ['position' => 1], + 'simple-tableRate-2' => ['position' => 2], + ]; + $this->linkProducts('simple-tableRate-1', $productLinks); + $items = $this->getBlockItems('tableRate'); + + $this->assertEquals( + ['simple-1'], + $this->getActualLinks($items), + 'Expected cross-sell products do not match actual cross-sell products!' + ); + } + + /** + * Get products of block when quote in checkout session + * + * @param string $reservedOrderId + * @return array + */ + public function getBlockItems(string $reservedOrderId): array + { + $this->setCheckoutSessionQuote($reservedOrderId); + + return $this->block->getItems(); + } + + /** + * @inheritdoc + */ + protected function prepareBlock(): void + { + parent::prepareBlock(); + + $this->block->setViewModel($this->objectManager->get(PreparePostData::class)); + } + + /** + * @inheritdoc + */ + protected function prepareProductsWebsiteIds(): array + { + $productsWebsiteIds = parent::prepareProductsWebsiteIds(); + $simple = $productsWebsiteIds['simple-1']; + unset($productsWebsiteIds['simple-1']); + + return array_merge($productsWebsiteIds, ['simple-tableRate-1' => $simple]); + } + + /** + * Set quoteId in checkoutSession object. + * + * @param string $reservedOrderId + * @return void + */ + private function setCheckoutSessionQuote(string $reservedOrderId): void + { + $this->checkoutSession->clearQuote(); + $quote = $this->objectManager->get(GetQuoteByReservedOrderId::class)->execute($reservedOrderId); + if ($quote !== null) { + $this->checkoutSession->setQuoteId($quote->getId()); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/AddTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/AddTest.php new file mode 100644 index 0000000000000..424fa13d74890 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/Cart/AddTest.php @@ -0,0 +1,231 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\Controller\Cart; + +use Laminas\Stdlib\Parameters; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\SessionFactory as CheckoutSessionFactory; +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Escaper; +use Magento\Framework\Message\MessageInterface; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Class add product to cart controller. + * + * @see \Magento\Checkout\Controller\Cart\Add + * @magentoAppArea frontend + * @magentoDbIsolation enabled + */ +class AddTest extends AbstractController +{ + /** @var SerializerInterface */ + private $json; + + /** @var CheckoutSessionFactory */ + private $checkoutSessionFactory; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** @var Escaper */ + private $escaper; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->_objectManager->get(SerializerInterface::class); + $this->checkoutSessionFactory = $this->_objectManager->get(CheckoutSessionFactory::class); + $this->productRepository = $this->_objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->executeInStoreContext = $this->_objectManager->get(ExecuteInStoreContext::class); + $this->escaper = $this->_objectManager->get(Escaper::class); + } + + /** + * Test with simple product and activated redirect to cart + * + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 1 + * + * @return void + */ + public function testMessageAtAddToCartWithRedirect(): void + { + $this->prepareReferer(); + $checkoutSession = $this->checkoutSessionFactory->create(); + $postData = [ + 'qty' => '1', + 'product' => '1', + 'custom_price' => 1, + 'isAjax' => 1, + ]; + $this->dispatchAddToCartRequest($postData); + $this->assertEquals( + $this->json->serialize(['backUrl' => 'http://localhost/checkout/cart/']), + $this->getResponse()->getBody() + ); + $this->assertSessionMessages( + $this->containsEqual((string)__('You added %1 to your shopping cart.', 'Simple Product')), + MessageInterface::TYPE_SUCCESS + ); + $this->assertCount(1, $checkoutSession->getQuote()->getItemsCollection()); + } + + /** + * Test with simple product and deactivated redirect to cart + * + * @magentoDataFixture Magento/Catalog/_files/products.php + * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 0 + * + * @return void + */ + public function testMessageAtAddToCartWithoutRedirect(): void + { + $this->prepareReferer(); + $checkoutSession = $this->checkoutSessionFactory->create(); + $postData = [ + 'qty' => '1', + 'product' => '1', + 'custom_price' => 1, + 'isAjax' => 1, + ]; + $this->dispatchAddToCartRequest($postData); + $this->assertFalse($this->getResponse()->isRedirect()); + $this->assertEquals('[]', $this->getResponse()->getBody()); + $message = (string)__( + 'You added %1 to your <a href="%2">shopping cart</a>.', + 'Simple Product', + 'http://localhost/checkout/cart/' + ); + $this->assertSessionMessages( + $this->containsEqual("\n" . $message), + MessageInterface::TYPE_SUCCESS + ); + $this->assertCount(1, $checkoutSession->getQuote()->getItemsCollection()); + } + + /** + * @dataProvider wrongParamsDataProvider + * + * @param array $params + * @return void + */ + public function testWithWrongParams(array $params): void + { + $this->prepareReferer(); + $this->dispatchAddToCartRequest($params); + $this->assertRedirect($this->stringContains('http://localhost/test')); + } + + /** + * @return array + */ + public function wrongParamsDataProvider(): array + { + return [ + 'empty_params' => ['params' => []], + 'with_not_existing_product_id' => ['params' => ['product' => 989]], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * + * @return void + */ + public function testAddProductFromUnavailableWebsite(): void + { + $this->prepareReferer(); + $product = $this->productRepository->get('simple-1'); + $postData = ['product' => $product->getId()]; + $this->executeInStoreContext->execute('fixture_second_store', [$this, 'dispatchAddToCartRequest'], $postData); + $this->assertRedirect($this->stringContains('http://localhost/test')); + $message = $this->escaper->escapeHtml( + (string)__('The product wasn\'t found. Verify the product and try again.') + ); + $this->assertSessionMessages($this->containsEqual($message), MessageInterface::TYPE_ERROR); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testAddProductWithUnavailableQty(): void + { + $product = $this->productRepository->get('simple-1'); + $postData = ['product' => $product->getId(), 'qty' => '1000']; + $this->dispatchAddToCartRequest($postData); + $message = (string)__('The requested qty is not available'); + $this->assertSessionMessages($this->containsEqual($message), MessageInterface::TYPE_ERROR); + $this->assertRedirect($this->stringContains($product->getProductUrl())); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/products_related_multiple.php + * + * @return void + */ + public function testAddProductWithRelated(): void + { + $this->prepareReferer(); + $checkoutSession = $this->checkoutSessionFactory->create(); + $product = $this->productRepository->get('simple_with_cross'); + $params = [ + 'product' => $product->getId(), + 'related_product' => implode(',', $product->getRelatedProductIds()), + ]; + $this->dispatchAddToCartRequest($params); + $this->assertCount(3, $checkoutSession->getQuote()->getItemsCollection()); + $message = (string)__( + 'You added %1 to your <a href="%2">shopping cart</a>.', + $product->getName(), + 'http://localhost/checkout/cart/' + ); + $this->assertSessionMessages( + $this->containsEqual("\n" . $message), + MessageInterface::TYPE_SUCCESS + ); + } + + /** + * Dispatch add product to cart request. + * + * @param array $postData + * @return void + */ + public function dispatchAddToCartRequest(array $postData = []): void + { + $this->getRequest()->setPostValue($postData); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('checkout/cart/add'); + } + + /** + * Prepare referer to test. + * + * @return void + */ + private function prepareReferer(): void + { + $parameters = $this->_objectManager->create(Parameters::class); + $parameters->set('HTTP_REFERER', 'http://localhost/test'); + $this->getRequest()->setServer($parameters); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php index a9714a17ffe4f..fd89229bb73be 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Controller/CartTest.php @@ -352,77 +352,6 @@ public function addAddProductDataProvider() ]; } - /** - * Test for \Magento\Checkout\Controller\Cart\Add::execute() with simple product and activated redirect to cart - * - * @magentoDataFixture Magento/Catalog/_files/products.php - * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 1 - * @magentoAppIsolation enabled - */ - public function testMessageAtAddToCartWithRedirect() - { - $formKey = $this->_objectManager->get(FormKey::class); - $postData = [ - 'qty' => '1', - 'product' => '1', - 'custom_price' => 1, - 'form_key' => $formKey->getFormKey(), - 'isAjax' => 1 - ]; - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); - $this->getRequest()->setPostValue($postData); - $this->getRequest()->setMethod('POST'); - - $this->dispatch('checkout/cart/add'); - - $this->assertEquals( - '{"backUrl":"http:\/\/localhost\/index.php\/checkout\/cart\/"}', - $this->getResponse()->getBody() - ); - - $this->assertSessionMessages( - $this->containsEqual( - 'You added Simple Product to your shopping cart.' - ), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - } - - /** - * Test for \Magento\Checkout\Controller\Cart\Add::execute() with simple product and deactivated redirect to cart - * - * @magentoDataFixture Magento/Catalog/_files/products.php - * @magentoConfigFixture current_store checkout/cart/redirect_to_cart 0 - * @magentoAppIsolation enabled - */ - public function testMessageAtAddToCartWithoutRedirect() - { - $formKey = $this->_objectManager->get(FormKey::class); - $postData = [ - 'qty' => '1', - 'product' => '1', - 'custom_price' => 1, - 'form_key' => $formKey->getFormKey(), - 'isAjax' => 1 - ]; - \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); - $this->getRequest()->setPostValue($postData); - $this->getRequest()->setMethod('POST'); - - $this->dispatch('checkout/cart/add'); - - $this->assertFalse($this->getResponse()->isRedirect()); - $this->assertEquals('[]', $this->getResponse()->getBody()); - - $this->assertSessionMessages( - $this->containsEqual( - "\n" . 'You added Simple Product to your ' . - '<a href="http://localhost/index.php/checkout/cart/">shopping cart</a>.' - ), - \Magento\Framework\Message\MessageInterface::TYPE_SUCCESS - ); - } - /** * @covers \Magento\Checkout\Controller\Cart\Addgroup::execute() * diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php index f534904e9db6b..c8f3bc891a413 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/CartTest.php @@ -3,51 +3,163 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Checkout\Model; +use Magento\Catalog\Api\Data\ProductInterfaceFactory; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Session as CheckoutSession; +use Magento\Checkout\Model\SessionFactory as CheckoutSessionFactory; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use PHPUnit\Framework\TestCase; -class CartTest extends \PHPUnit\Framework\TestCase +/** + * Test for checkout cart model. + * + * @see \Magento\Checkout\Model\Cart + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CartTest extends TestCase { + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CartFactory */ + private $cartFactory; + + /** @var ProductInterfaceFactory */ + private $productFactory; + + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** @var CheckoutSession */ + private $checkoutSession; + + /** @var CartInterface */ + private $quote; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + /** - * @var Cart + * @inheritdoc */ - private $cart; + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->cartFactory = $this->objectManager->get(CartFactory::class); + $this->productFactory = $this->objectManager->get(ProductInterfaceFactory::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + $this->checkoutSession = $this->objectManager->get(CheckoutSessionFactory::class)->create(); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + } /** - * @var ProductRepositoryInterface + * @inheritdoc */ - private $productRepository; - - protected function setUp(): void + protected function tearDown(): void { - $this->cart = Bootstrap::getObjectManager()->create(Cart::class); - $this->productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); } /** - * @magentoDataFixture Magento/Checkout/_files/simple_product.php * @magentoDataFixture Magento/Checkout/_files/set_product_min_in_cart.php - * @magentoDbIsolation enabled * @magentoAppIsolation enabled + * + * @return void */ - public function testAddProductWithLowerQty() + public function testAddProductWithLowerQty(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('The fewest you may purchase is 3'); + $cart = $this->cartFactory->create(); + $this->expectException(LocalizedException::class); + $this->expectExceptionMessage((string)__('The fewest you may purchase is %1', 3)); $product = $this->productRepository->get('simple'); - $this->cart->addProduct($product->getId(), ['qty' => 1]); + $cart->addProduct($product->getId(), ['qty' => 1]); } /** - * @magentoDataFixture Magento/Checkout/_files/simple_product.php * @magentoDataFixture Magento/Checkout/_files/set_product_min_in_cart.php - * @magentoDbIsolation enabled + * + * @return void + */ + public function testAddProductWithNoQty(): void + { + $cart = $this->cartFactory->create(); + $product = $this->productRepository->get('simple'); + $cart->addProduct($product->getId(), [])->save(); + $this->quote = $cart->getQuote(); + $this->assertCount(1, $cart->getItems()); + $this->assertEquals($product->getId(), $this->checkoutSession->getLastAddedProductId()); + } + + /** + * @return void + */ + public function testAddNotExistingProduct(): void + { + $product = $this->productFactory->create(); + $this->expectExceptionObject( + new LocalizedException(__('The product wasn\'t found. Verify the product and try again.')) + ); + $this->cartFactory->create()->addProduct($product); + } + + /** + * @return void + */ + public function testAddNotExistingProductId(): void + { + $this->expectExceptionObject( + new LocalizedException(__('The product wasn\'t found. Verify the product and try again.')) + ); + $this->cartFactory->create()->addProduct(989); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * + * @return void + */ + public function testAddProductFromUnavailableWebsite(): void + { + $product = $this->productRepository->get('simple'); + $this->expectExceptionObject( + new LocalizedException(__('The product wasn\'t found. Verify the product and try again.')) + ); + $this->executeInStoreContext + ->execute('fixture_second_store', [$this->cartFactory->create(), 'addProduct'], $product->getId()); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * + * @return void */ - public function testAddProductWithNoQty() + public function testAddProductWithInvalidRequest(): void { $product = $this->productRepository->get('simple'); - $this->cart->addProduct($product->getId(), []); + $message = __('We found an invalid request for adding product to quote.'); + $this->expectExceptionObject(new LocalizedException($message)); + $this->cartFactory->create()->addProduct($product->getId(), ''); } } diff --git a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php index 41bf18619332a..32968572b4ac8 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/Model/SessionTest.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Checkout\Model; use Magento\Catalog\Api\Data\ProductTierPriceInterface; @@ -10,22 +12,24 @@ use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Model\Session as CustomerSession; -use Magento\Framework\Api\FilterBuilder; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartRepositoryInterface; use Magento\Quote\Api\Data\CartInterface; -use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; /** * Checkout Session model test. * + * @see \Magento\Checkout\Model\Session + * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class SessionTest extends \PHPUnit\Framework\TestCase +class SessionTest extends TestCase { /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ private $objectManager; @@ -45,40 +49,78 @@ class SessionTest extends \PHPUnit\Framework\TestCase private $checkoutSession; /** - * @return void + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @var CartInterface + */ + private $quote; + + /** + * @inheritdoc */ protected function setUp(): void { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); $this->customerSession = $this->objectManager->get(CustomerSession::class); - $this->checkoutSession = $this->objectManager->create(Session::class); + $this->checkoutSession = $this->objectManager->get(Session::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + $this->customerSession->setCustomerId(null); + $this->checkoutSession->clearQuote(); + $this->checkoutSession->setCustomerData(null); + + parent::tearDown(); } /** * Tests that quote items and totals are correct when product becomes unavailable. * - * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Sales/_files/quote.php * @magentoAppIsolation enabled + * + * @return void */ - public function testGetQuoteWithUnavailableProduct() + public function testGetQuoteWithUnavailableProduct(): void { $reservedOrderId = 'test01'; $quoteGrandTotal = 10; - - $quote = $this->getQuote($reservedOrderId); + $quote = $this->getQuoteByReservedOrderId->execute($reservedOrderId); $this->assertEquals(1, $quote->getItemsCount()); $this->assertCount(1, $quote->getItems()); $this->assertEquals($quoteGrandTotal, $quote->getShippingAddress()->getBaseGrandTotal()); - - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple'); + $product = $this->productRepository->get('simple'); $product->setStatus(Status::STATUS_DISABLED); - $productRepository->save($product); + $this->productRepository->save($product); $this->checkoutSession->setQuoteId($quote->getId()); $quote = $this->checkoutSession->getQuote(); - $this->assertEquals(0, $quote->getItemsCount()); $this->assertEmpty($quote->getItems()); $this->assertEquals(0, $quote->getShippingAddress()->getBaseGrandTotal()); @@ -90,15 +132,15 @@ public function testGetQuoteWithUnavailableProduct() * Expected result - quote object should be loaded and customer data should be set to it. * * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php + * + * @return void */ - public function testGetQuoteNotInitializedCustomerSet() + public function testGetQuoteNotInitializedCustomerSet(): void { $customer = $this->customerRepository->getById(1); $this->checkoutSession->setCustomerData($customer); - - /** Execute SUT */ $quote = $this->checkoutSession->getQuote(); - $this->_validateCustomerDataInQuote($quote); + $this->validateCustomerDataInQuote($quote); } /** @@ -107,36 +149,29 @@ public function testGetQuoteNotInitializedCustomerSet() * Expected result - quote object should be loaded and customer data should be set to it. * * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php - * @magentoAppIsolation enabled + * + * @return void */ - public function testGetQuoteNotInitializedCustomerLoggedIn() + public function testGetQuoteNotInitializedCustomerLoggedIn(): void { $customer = $this->customerRepository->getById(1); $this->customerSession->setCustomerDataObject($customer); - - /** Execute SUT */ $quote = $this->checkoutSession->getQuote(); - $this->_validateCustomerDataInQuote($quote); + $this->validateCustomerDataInQuote($quote); } /** * @magentoDataFixture Magento/Sales/_files/quote_with_customer.php - * @magentoAppIsolation enabled + * + * @return void */ - public function testGetQuoteWithMismatchingSession() + public function testGetQuoteWithMismatchingSession(): void { - /** @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 + $quote = $this->getQuoteByReservedOrderId->execute('test01'); $this->checkoutSession->setQuoteId($quote->getId()); - - $sessionQuote = $this->checkoutSession->getQuote(); - $this->assertEmpty($sessionQuote->getCustomerId()); - $this->assertNotEquals($quote->getId(), $sessionQuote->getId()); + $this->quote = $this->checkoutSession->getQuote(); + $this->assertEmpty($this->quote->getCustomerId()); + $this->assertNotEquals($quote->getId(), $this->quote->getId()); } /** @@ -150,96 +185,90 @@ public function testGetQuoteWithMismatchingSession() * Quote which is set to checkout session should contain customer data * * @magentoDataFixture Magento/Customer/_files/customer.php - * @magentoAppIsolation enabled + * + * @return void */ - public function testLoadCustomerQuoteCustomerWithoutQuote() + public function testLoadCustomerQuoteCustomerWithoutQuote(): void { - $quote = $this->checkoutSession->getQuote(); - $this->assertEmpty($quote->getCustomerId(), 'Precondition failed: Customer data must not be set to quote'); - $this->assertEmpty($quote->getCustomerEmail(), 'Precondition failed: Customer data must not be set to quote'); - + $this->quote = $this->checkoutSession->getQuote(); + $this->assertEmpty( + $this->quote->getCustomerId(), + 'Precondition failed: Customer data must not be set to quote' + ); + $this->assertEmpty( + $this->quote->getCustomerEmail(), + 'Precondition failed: Customer data must not be set to quote' + ); $customer = $this->customerRepository->getById(1); $this->customerSession->setCustomerDataObject($customer); - - /** Ensure that customer data is still unavailable before SUT invocation */ - $quote = $this->checkoutSession->getQuote(); - $this->assertEmpty($quote->getCustomerEmail(), 'Precondition failed: Customer data must not be set to quote'); - - /** Execute SUT */ + $this->quote = $this->checkoutSession->getQuote(); + $this->assertEmpty( + $this->quote->getCustomerEmail(), + 'Precondition failed: Customer data must not be set to quote' + ); $this->checkoutSession->loadCustomerQuote(); - $quote = $this->checkoutSession->getQuote(); - $this->_validateCustomerDataInQuote($quote); + $this->quote = $this->checkoutSession->getQuote(); + $this->validateCustomerDataInQuote($this->quote); } /** * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Sales/_files/quote.php + * + * @return void */ - public function testGetQuoteWithProductWithTierPrice() + public function testGetQuoteWithProductWithTierPrice(): void { $reservedOrderId = 'test01'; $customerGroupId = 1; $tierPriceQty = 1; $tierPriceValue = 9; - - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple'); - $tierPrice = $this->objectManager->create(ProductTierPriceInterface::class) + $product = $this->productRepository->get('simple'); + $tierPrice = $this->objectManager->get(ProductTierPriceInterface::class) ->setCustomerGroupId($customerGroupId) ->setQty($tierPriceQty) ->setValue($tierPriceValue); $product->setTierPrices([$tierPrice]); - $productRepository->save($product); - - $quote = $this->getQuote($reservedOrderId); + $this->productRepository->save($product); + $quote = $this->getQuoteByReservedOrderId->execute($reservedOrderId); $this->checkoutSession->setQuoteId($quote->getId()); - $quote = $this->checkoutSession->getQuote(); $item = $quote->getItems()[0]; - /** @var \Magento\Catalog\Model\Product $quoteProduct */ $quoteProduct = $item->getProduct(); $this->assertEquals(10, $quoteProduct->getTierPrice($tierPriceQty)); - $customer = $this->customerRepository->getById(1); $this->customerSession->setCustomerDataAsLoggedIn($customer); - $quote = $this->checkoutSession->getQuote(); $item = $quote->getItems()[0]; - /** @var \Magento\Catalog\Model\Product $quoteProduct */ $quoteProduct = $item->getProduct(); $this->assertEquals($tierPriceValue, $quoteProduct->getTierPrice(1)); } /** - * Returns quote by reserved order id. + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php * - * @param string $reservedOrderId - * @return CartInterface + * @return void */ - private function getQuote(string $reservedOrderId): CartInterface + public function testMergeGuestQuoteWithCustomerQuote(): void { - $filterBuilder = $this->objectManager->create(FilterBuilder::class); - $filter = $filterBuilder->setField('reserved_order_id') - ->setConditionType('=') - ->setValue($reservedOrderId) - ->create(); - $searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilters([$filter]) - ->create(); - $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); - $searchResult = $quoteRepository->getList($searchCriteria); - /** @var CartInterface[] $items */ - $items = $searchResult->getItems(); - - return \array_values($items)[0]; + $guestQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $customerQuote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->checkoutSession->setQuoteId($guestQuote->getId()); + $this->customerSession->setCustomerId(1); + $updatedQuote = $this->checkoutSession->loadCustomerQuote()->getQuote(); + $this->assertNull($this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address')); + $this->assertEquals($customerQuote->getId(), $updatedQuote->getId()); + $this->assertCount(2, $updatedQuote->getItems()); } /** * Ensure that quote has customer data specified in customer fixture. * - * @param \Magento\Quote\Model\Quote $quote + * @param CartInterface $quote + * @return void */ - protected function _validateCustomerDataInQuote($quote) + private function validateCustomerDataInQuote(CartInterface $quote): void { $customerIdFromFixture = 1; $customerEmailFromFixture = 'customer@example.com'; diff --git a/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php b/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php new file mode 100644 index 0000000000000..7fb57ca0f4090 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/ViewModel/CartTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Checkout\ViewModel; + +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for clear shopping cart config + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class CartTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Cart + */ + private $cart; + + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = $this->objectManager = Bootstrap::getObjectManager(); + $this->cart = $objectManager->get(Cart::class); + $this->mutableScopeConfig = $objectManager->get(MutableScopeConfigInterface::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testConfigClearShoppingCartEnabledWithWebsiteScopes() + { + // Assert not active by default + $this->assertFalse($this->cart->isClearShoppingCartEnabled()); + + // Enable Clear Shopping Cart in default website scope + $this->setClearShoppingCartEnabled( + true, + ScopeInterface::SCOPE_WEBSITE + ); + + // Assert now active in default website scope + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + + $defaultStore = $this->storeManager->getStore(); + $defaultWebsite = $defaultStore->getWebsite(); + $defaultWebsiteCode = $defaultWebsite->getCode(); + + $secondStore = $this->storeManager->getStore('fixture_second_store'); + $secondWebsite = $secondStore->getWebsite(); + $secondWebsiteCode = $secondWebsite->getCode(); + + // Change current store context to that of second website + $this->storeManager->setCurrentStore($secondStore); + + // Assert not active by default in second website + $this->assertFalse($this->cart->isClearShoppingCartEnabled()); + + // Enable Clear Shopping Cart in second website scope + $this->setClearShoppingCartEnabled( + true, + ScopeInterface::SCOPE_WEBSITE, + $secondWebsiteCode + ); + + // Assert now active in second website scope + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + + // Disable Clear Shopping Cart in default website scope + $this->setClearShoppingCartEnabled( + false, + ScopeInterface::SCOPE_WEBSITE, + $defaultWebsiteCode + ); + + // Assert still active in second website + $this->assertTrue($this->cart->isClearShoppingCartEnabled()); + } + + /** + * Set clear shopping cart enabled. + * + * @param bool $isActive + * @param string $scope + * @param string|null $scopeCode + */ + private function setClearShoppingCartEnabled(bool $isActive, string $scope, $scopeCode = null) + { + $this->mutableScopeConfig->setValue( + 'checkout/cart/enable_clear_shopping_cart', + $isActive ? '1' : '0', + $scope, + $scopeCode + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website.php new file mode 100644 index 0000000000000..a705335a3f68b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_with_websites_and_stores.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $customerRepository->get('customer@example.com'); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId($storeManager->getStore('fixture_second_store')->getId()) + ->setIsActive(true) + ->setIsMultiShipping(0) + ->setCustomer($customer) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_on_second_website') + ->addProduct($productRepository->get('simple-2'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website_rollback.php new file mode 100644 index 0000000000000..2cc43f9171f4b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_on_second_website_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('test_order_on_second_website'); +if ($quote !== null) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance() + ->requireDataFixture('Magento/Catalog/_files/products_with_websites_and_stores_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php index 66d984301d14f..f0ba56f7179aa 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/customer_quote_with_items_simple_product_options.php @@ -10,7 +10,7 @@ use Magento\Catalog\Model\Product\Option; use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; use Magento\Catalog\Model\Product\Option\Value; -use Magento\Checkout\_files\ValidatorFileMock; +use Magento\TestFramework\Catalog\Model\Product\Option\Type\File\ValidatorFileMock; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Framework\DataObject; use Magento\Quote\Api\CartRepositoryInterface; @@ -83,7 +83,7 @@ $itemsOptions[$dropDownValue->getTitle()] = $options; } -$validatorFileMock = (new ValidatorFileMock())->getInstance(); +$validatorFileMock = $objectManager->get(ValidatorFileMock::class)->getInstance(); $objectManager->addSharedInstance($validatorFileMock, ValidatorFile::class); $quote->setStoreId($storeManager->getStore()->getId()) diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price.php new file mode 100644 index 0000000000000..b6d51dfe0fdc7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price.php @@ -0,0 +1,54 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Bundle\Model\Option; +use Magento\Bundle\Model\Product\Type; +use Magento\Bundle\Model\Selection; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; +use Magento\Checkout\Model\Cart; +use Magento\Checkout\Model\Session; +use Magento\Framework\DataObject; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Bundle/_files/bundle_product_with_dynamic_price.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +/** @var Product $product */ +$product = $productRepository->get('bundle_product_with_dynamic_price'); + +/** @var $typeInstance Type */ +//Load options +$typeInstance = $product->getTypeInstance(); +$typeInstance->setStoreFilter($product->getStoreId(), $product); +$optionCollection = $typeInstance->getOptionsCollection($product); + +$bundleOptions = []; +$bundleOptionsQty = []; +/** @var $option Option */ +foreach ($optionCollection as $option) { + $selectionCollection = $typeInstance->getSelectionsCollection([$option->getId()], $product); + /** @var $selection Selection */ + $selection = $selectionCollection->getFirstItem(); + $bundleOptions[$option->getId()] = $selection->getSelectionId(); + $bundleOptionsQty[$option->getId()] = 1; +} + +$requestInfo = new DataObject( + ['qty' => 1, 'bundle_option' => $bundleOptions, 'bundle_option_qty' => $bundleOptionsQty] +); + +/** @var Cart $cart */ +$cart = $objectManager->create(Cart::class); +$cart->addProduct($product, $requestInfo); +$cart->getQuote()->setReservedOrderId('quote_with_bundle_product_with_dynamic_price'); +$cart->save(); + +$objectManager->removeSharedInstance(Session::class); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price_rollback.php new file mode 100644 index 0000000000000..8507b6f1a2619 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\Registry; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$quote = $objectManager->create(Quote::class); +$quote->load('quote_with_bundle_product_with_dynamic_price', 'reserved_order_id')->delete(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Bundle/_files/bundle_product_with_dynamic_price_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address.php new file mode 100644 index 0000000000000..1e3813f3970bc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address.php @@ -0,0 +1,39 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $customerRepository->get('customer@example.com'); + +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId(1) + ->setIsActive(true) + ->setIsMultiShipping(0) + ->setCustomer($customer) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_with_customer_without_address') + ->addProduct($productRepository->get('simple2'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address_rollback.php new file mode 100644 index 0000000000000..bd06a8da059dd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_customer_without_address_rollback.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); +if ($quote !== null) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_items_and_custom_options_saved.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_items_and_custom_options_saved.php index 3abe6b21f110e..2293f0662c699 100644 --- a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_items_and_custom_options_saved.php +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_items_and_custom_options_saved.php @@ -4,7 +4,12 @@ * See COPYING.txt for license details. */ -use Magento\Checkout\_files\ValidatorFileMock; +use Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile; +use Magento\Framework\DataObject; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Catalog\Model\Product\Option\Type\File\ValidatorFileMock; use Magento\Quote\Model\QuoteFactory; use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; use Magento\TestFramework\Helper\Bootstrap; @@ -12,7 +17,6 @@ Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/quote_with_address.php'); Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_with_options.php'); -Resolver::getInstance()->requireDataFixture('Magento/Checkout/_files/ValidatorFileMock.php'); $objectManager = Bootstrap::getObjectManager(); /** @var QuoteFactory $quoteFactory */ @@ -45,18 +49,18 @@ $options[$option->getId()] = $value; } -$requestInfo = new \Magento\Framework\DataObject(['qty' => 1, 'options' => $options]); -$validatorFile = (new ValidatorFileMock())->getInstance(); -$objectManager->addSharedInstance($validatorFile, \Magento\Catalog\Model\Product\Option\Type\File\ValidatorFile::class); +$requestInfo = new DataObject(['qty' => 1, 'options' => $options]); +$validatorFile = $objectManager->get(ValidatorFileMock::class)->getInstance(); +$objectManager->addSharedInstance($validatorFile, ValidatorFile::class); $quote->setReservedOrderId('test_order_item_with_items_and_custom_options'); $quote->addProduct($product, $requestInfo); $quote->collectTotals(); -$objectManager->get(\Magento\Quote\Model\QuoteRepository::class)->save($quote); +$objectManager->get(QuoteRepository::class)->save($quote); -/** @var \Magento\Quote\Model\QuoteIdMask $quoteIdMask */ +/** @var QuoteIdMask $quoteIdMask */ $quoteIdMask = Bootstrap::getObjectManager() - ->create(\Magento\Quote\Model\QuoteIdMaskFactory::class) + ->create(QuoteIdMaskFactory::class) ->create(); $quoteIdMask->setQuoteId($quote->getId()); $quoteIdMask->setDataChanges(true); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product.php new file mode 100644 index 0000000000000..2321aa1d4bc71 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_products_not_visible_individually.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var AddressInterface $quoteShippingAddress */ +$quoteShippingAddress = $objectManager->get(AddressInterfaceFactory::class)->create(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$quoteShippingAddress->importCustomerAddressData($addressRepository->getById(1)); +$customer = $customerRepository->getById(1); + +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId(1) + ->setIsActive(true) + ->setIsMultiShipping(0) + ->assignCustomerWithAddressChange($customer) + ->setShippingAddress($quoteShippingAddress) + ->setBillingAddress($quoteShippingAddress) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_with_not_visible_product') + ->setEmail($customer->getEmail()) + ->addProduct($productRepository->get('simple_not_visible_1'), 1); + +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product_rollback.php new file mode 100644 index 0000000000000..4ed8bf5e11735 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_not_visible_individually_product_rollback.php @@ -0,0 +1,26 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('test_order_with_not_visible_product'); +if ($quote) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance() + ->requireDataFixture('Magento/Catalog/_files/simple_products_not_visible_individually_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer.php new file mode 100644 index 0000000000000..c32d299d427b5 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer.php @@ -0,0 +1,51 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Model\Type\Onepage; +use Magento\Customer\Api\AddressRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var AddressInterface $quoteShippingAddress */ +$quoteShippingAddress = $objectManager->get(AddressInterfaceFactory::class)->create(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +/** @var AddressRepositoryInterface $addressRepository */ +$addressRepository = $objectManager->get(AddressRepositoryInterface::class); +$quoteShippingAddress->importCustomerAddressData($addressRepository->getById(1)); +$customer = $customerRepository->getById(1); + +/** @var CartInterface $quote */ +$quote = $objectManager->get(CartInterfaceFactory::class)->create(); +$quote->setStoreId(1) + ->setIsActive(true) + ->setIsMultiShipping(0) + ->assignCustomerWithAddressChange($customer) + ->setShippingAddress($quoteShippingAddress) + ->setBillingAddress($quoteShippingAddress) + ->setCheckoutMethod(Onepage::METHOD_CUSTOMER) + ->setReservedOrderId('test_order_with_taxable_product') + ->setEmail($customer->getEmail()) + ->addProduct($productRepository->get('taxable_product'), 1); +$quoteRepository->save($quote); diff --git a/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php new file mode 100644 index 0000000000000..0051023e48060 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Checkout/_files/quote_with_taxable_product_and_customer_rollback.php @@ -0,0 +1,25 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var CartRepositoryInterface $quoteRepository */ +$quoteRepository = $objectManager->get(CartRepositoryInterface::class); +/** @var GetQuoteByReservedOrderId $getQuoteByReservedOrderId */ +$getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); +$quote = $getQuoteByReservedOrderId->execute('test_order_with_taxable_product'); +if ($quote) { + $quoteRepository->delete($quote); +} + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/taxable_simple_product_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_address_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php index 66b452d234366..ee99ec96bbf2c 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'Checkout Agreement (active)', 'content' => 'Checkout agreement content: <b>HTML</b>', @@ -15,4 +28,4 @@ 'is_html' => true, 'stores' => [0, 1], ]); -$agreement->save(); +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php index da65dcae7d8f4..10879d3d91306 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_active_with_html_content_rollback.php @@ -3,10 +3,23 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Checkout Agreement (active)', 'name'); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php index e60c754d66a3c..29b01163df514 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'Checkout Agreement (inactive)', 'content' => 'Checkout agreement content: TEXT', @@ -15,4 +28,4 @@ 'is_html' => false, 'stores' => [0, 1], ]); -$agreement->save(); +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php index 39ba6cf30be26..3fda82782ebc5 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/agreement_inactive_with_text_content_rollback.php @@ -4,10 +4,22 @@ * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Checkout Agreement (inactive)', 'name'); +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'Checkout Agreement (inactive)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php index 3be16338110a1..8d15bf6e9b74f 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text.php @@ -3,9 +3,22 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); + +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + $agreement->setData([ 'name' => 'First Checkout Agreement (active)', 'content' => 'Checkout agreement content: TEXT', @@ -16,8 +29,9 @@ 'mode' => 1, 'stores' => [0, 1], ]); -$agreement->save(); -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); +$agreementResource->save($agreement); + +$agreement = $objectManager->create(Agreement::class); $agreement->setData([ 'name' => 'Second Checkout Agreement (active)', 'content' => 'Checkout agreement content: TEXT', @@ -28,4 +42,5 @@ 'mode' => 1, 'stores' => [0, 1], ]); -$agreement->save(); + +$agreementResource->save($agreement); diff --git a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php index 9c594c0c22b65..f43f7a5ba9a51 100644 --- a/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php +++ b/dev/tests/integration/testsuite/Magento/CheckoutAgreements/_files/multi_agreements_active_with_text_rollback.php @@ -4,15 +4,28 @@ * See COPYING.txt for license details. */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -/** @var $agreement \Magento\CheckoutAgreements\Model\Agreement */ -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('First Checkout Agreement (active)', 'name'); +declare(strict_types=1); + +use Magento\CheckoutAgreements\Model\Agreement; +use Magento\CheckoutAgreements\Model\ResourceModel\Agreement as AgreementResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $agreement Agreement + * @var $agreementResource AgreementResource + */ +$agreement = $objectManager->create(Agreement::class); +$agreementResource = $objectManager->create(AgreementResource::class); + +$agreementResource->load($agreement, 'First Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } -$agreement = $objectManager->create(\Magento\CheckoutAgreements\Model\Agreement::class); -$agreement->load('Second Checkout Agreement (active)', 'name'); + +$agreement = $objectManager->create(Agreement::class); +$agreementResource->load($agreement, 'Second Checkout Agreement (active)', 'name'); if ($agreement->getId()) { - $agreement->delete(); + $agreementResource->delete($agreement); } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php index cb6ea60a5efdc..de1a78c87953c 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/Adminhtml/PageDesignTest.php @@ -66,6 +66,12 @@ class PageDesignTest extends AbstractBackendController */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); parent::setUp(); $this->aclBuilder = Bootstrap::getObjectManager()->get(Builder::class); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php index 8d82602b3ac1c..4d9178f1a0659 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Controller/PageTest.php @@ -4,17 +4,38 @@ * See COPYING.txt for license details. */ -/** - * Test class for \Magento\Cms\Controller\Page. - */ namespace Magento\Cms\Controller; use Magento\Cms\Api\GetPageByIdentifierInterface; +use Magento\Cms\Model\Page\CustomLayoutManagerInterface; use Magento\Framework\View\LayoutInterface; -use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Cms\Model\CustomLayoutManager; +use Magento\TestFramework\TestCase\AbstractController; -class PageTest extends \Magento\TestFramework\TestCase\AbstractController +/** + * Test for \Magento\Cms\Controller\Page\View class. + */ +class PageTest extends AbstractController { + /** + * @var GetPageByIdentifierInterface + */ + private $pageRetriever; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->_objectManager->configure([ + 'preferences' => [ + CustomLayoutManagerInterface::class => CustomLayoutManager::class, + ] + ]); + $this->pageRetriever = $this->_objectManager->get(GetPageByIdentifierInterface::class); + } + public function testViewAction() { $this->dispatch('/enable-cookies'); @@ -37,9 +58,7 @@ public function testViewRedirectWithTrailingSlash() public function testAddBreadcrumbs() { $this->dispatch('/enable-cookies'); - $layout = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\View\LayoutInterface::class - ); + $layout = $this->_objectManager->get(LayoutInterface::class); $breadcrumbsBlock = $layout->getBlock('breadcrumbs'); $this->assertStringContainsString($breadcrumbsBlock->toHtml(), $this->getResponse()->getBody()); } @@ -76,12 +95,10 @@ public static function cmsPageWithSystemRouteFixture() */ public function testCustomHandles(): void { - /** @var GetPageByIdentifierInterface $pageFinder */ - $pageFinder = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); - $page = $pageFinder->execute('test_custom_layout_page_3', 0); - $this->dispatch('/cms/page/view/page_id/' .$page->getId()); + $page = $this->pageRetriever->execute('test_custom_layout_page_3', 0); + $this->dispatch('/cms/page/view/page_id/' . $page->getId()); /** @var LayoutInterface $layout */ - $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $layout = $this->_objectManager->get(LayoutInterface::class); $handles = $layout->getUpdate()->getHandles(); $this->assertContains('cms_page_view_selectable_test_custom_layout_page_3_test_selected', $handles); } @@ -97,8 +114,37 @@ public function testHomePageCustomHandles(): void { $this->dispatch('/'); /** @var LayoutInterface $layout */ - $layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $layout = $this->_objectManager->get(LayoutInterface::class); $handles = $layout->getUpdate()->getHandles(); $this->assertContains('cms_page_view_selectable_home_page_custom_layout', $handles); } + + /** + * Tests page renders even with unavailable custom page layout. + * + * @magentoDataFixture Magento/Cms/Fixtures/page_list.php + * @dataProvider pageLayoutDataProvider + * @param string $pageIdentifier + * @return void + */ + public function testPageWithCustomLayout(string $pageIdentifier): void + { + $page = $this->pageRetriever->execute($pageIdentifier, 0); + $this->dispatch('/cms/page/view/page_id/' . $page->getId()); + $this->assertStringContainsString( + '<main id="maincontent" class="page-main">', + $this->getResponse()->getBody() + ); + } + + /** + * @return array + */ + public function pageLayoutDataProvider(): array + { + return [ + 'Page with 1column layout' => ['page-with-1column-layout'], + 'Page with unavailable layout' => ['page-with-unavailable-layout'] + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php index ae431f5c4cf1a..2fa0bf3a5bc13 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list.php @@ -14,15 +14,27 @@ $data = [ [ 'title' => 'simplePage', - 'is_active' => 1 + 'is_active' => 1, ], [ 'title' => 'simplePage01', - 'is_active' => 1 + 'is_active' => 1, ], [ 'title' => '01simplePage', - 'is_active' => 1 + 'is_active' => 1, + ], + [ + 'title' => 'Page with 1column layout', + 'is_active' => 1, + 'content' => '<h1>Test Page Content</h1>', + 'page_layout' => '1column', + ], + [ + 'title' => 'Page with unavailable layout', + 'content' => '<h1>Test Page Content</h1>', + 'is_active' => 1, + 'page_layout' => 'unavailable-layout', ], ]; diff --git a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php index 261cdba589653..00bec67bcfefc 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Fixtures/page_list_rollback.php @@ -16,7 +16,18 @@ /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ $searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); -$searchCriteria = $searchCriteriaBuilder->addFilter('title', ['simplePage', 'simplePage01', '01simplePage'], 'in') +$searchCriteria = $searchCriteriaBuilder + ->addFilter( + 'title', + [ + 'simplePage', + 'simplePage01', + '01simplePage', + 'Page with 1column layout', + 'Page with unavailable layout', + ], + 'in' + ) ->create(); $result = $pageRepository->getList($searchCriteria); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php index 0036c1722fd52..5542b779cde47 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/CustomLayoutRepositoryTest.php @@ -21,6 +21,8 @@ /** * Test the repository. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class CustomLayoutRepositoryTest extends TestCase { @@ -50,6 +52,12 @@ class CustomLayoutRepositoryTest extends TestCase protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); + $objectManager->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); $this->fakeManager = $objectManager->get(CustomLayoutManager::class); $this->repo = $objectManager->create(CustomLayoutRepositoryInterface::class, ['manager' => $this->fakeManager]); $this->pageFactory = $objectManager->get(PageFactory::class); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php index 17188238c5126..5197daa759e04 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/Page/DataProviderTest.php @@ -48,6 +48,12 @@ class DataProviderTest extends TestCase protected function setUp(): void { $objectManager = Bootstrap::getObjectManager(); + $objectManager->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); $this->repo = $objectManager->get(GetPageByIdentifierInterface::class); $this->filesFaker = $objectManager->get(CustomLayoutManager::class); $this->request = $objectManager->get(HttpRequest::class); diff --git a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php index 88d84eb4dc80a..53e514083d6ba 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Cms/Model/PageRepositoryTest.php @@ -34,6 +34,12 @@ class PageRepositoryTest extends TestCase */ protected function setUp(): void { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] + ]); $this->repo = Bootstrap::getObjectManager()->get(PageRepositoryInterface::class); $this->retriever = Bootstrap::getObjectManager()->get(GetPageByIdentifierInterface::class); } 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 5685f9f140a6d..a68a546c20bc6 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 @@ -167,7 +167,9 @@ public function testUploadFile(): void public function testUploadFileWithExcludedDirPath(): void { $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('We can\'t upload the file to current folder right now. Please try another folder.'); + $this->expectExceptionMessage( + 'We can\'t upload the file to current folder right now. Please try another folder.' + ); $fileName = 'magento_small_image.jpg'; $tmpDirectory = $this->filesystem->getDirectoryWrite(\Magento\Framework\App\Filesystem\DirectoryList::SYS_TMP); @@ -295,6 +297,58 @@ public function testGetThumbnailUrl(string $directory, string $filename, string $this->storage->deleteFile($path); } + /** + * Verify thumbnail generation for diferent sizes + * + * @param array $sizes + * @param bool $resized + * @dataProvider getThumbnailsSizes + */ + public function testResizeFile(array $sizes, bool $resized): void + { + $root = $this->storage->getCmsWysiwygImages()->getStorageRoot(); + $path = $root . '/' . 'testfile.png'; + $this->generateImage($path, $sizes['width'], $sizes['height']); + $this->storage->resizeFile($path); + + $thumbPath = $this->storage->getThumbnailPath($path); + list($imageWidth, $imageHeight) = getimagesize($thumbPath); + + $this->assertEquals( + $resized ? $this->storage->getResizeWidth() : $sizes['width'], + $imageWidth + ); + $this->assertLessThanOrEqual( + $resized ? $this->storage->getResizeHeight() : $sizes['height'], + $imageHeight + ); + + $this->storage->deleteFile($path); + } + + /** + * Provide sizes for resizeFile test + */ + public function getThumbnailsSizes(): array + { + return [ + [ + [ + 'width' => 1024, + 'height' => 768, + ], + true + ], + [ + [ + 'width' => 20, + 'height' => 20, + ], + false + ] + ]; + } + /** * Provide scenarios for testing getThumbnailUrl() * diff --git a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php index c7ea5f6380b32..0dacb4d5576b0 100644 --- a/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php +++ b/dev/tests/integration/testsuite/Magento/Cms/_files/pages_with_layout_xml.php @@ -13,6 +13,12 @@ use Magento\TestFramework\Helper\Bootstrap; $objectManager = Bootstrap::getObjectManager(); +$objectManager->configure([ + 'preferences' => [ + \Magento\Cms\Model\Page\CustomLayoutManagerInterface::class => + \Magento\TestFramework\Cms\Model\CustomLayoutManager::class + ] +]); $pageFactory = $objectManager->get(PageModelFactory::class); /** @var CustomLayoutManager $fakeManager */ diff --git a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php index 8aad6049125a8..660d59f3264ec 100644 --- a/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php +++ b/dev/tests/integration/testsuite/Magento/CmsUrlRewrite/Plugin/Cms/Model/Store/ViewTest.php @@ -25,7 +25,7 @@ use PHPUnit\Framework\TestCase; /** - * Test for plugin which is listening store resource model and on save replace cms page url rewrites + * Test for plugin which is listening store resource model and on save replace cms page url rewrites. * * @magentoAppArea adminhtml * @SuppressWarnings(PHPMD.CouplingBetweenObjects) @@ -84,9 +84,10 @@ protected function setUp(): void /** * Test of replacing cms page url rewrites on create and delete store * + * @magentoDataFixture Magento/Cms/_files/two_cms_page_with_same_url_for_different_stores.php * @magentoDataFixture Magento/Cms/_files/pages.php */ - public function testUrlRewritesChangesAfterStoreSave() + public function testUrlRewritesChangesAfterStoreSave(): void { $storeId = $this->createStore(); $this->assertUrlRewritesCount($storeId, 'page100', 1); @@ -98,16 +99,16 @@ public function testUrlRewritesChangesAfterStoreSave() } /** - * Assert url rewrites count by store id + * Assert url rewrites count by store id and request path * * @param int $storeId - * @param string $pageIdentifier + * @param string $requestPath * @param int $expectedCount */ - private function assertUrlRewritesCount(int $storeId, string $pageIdentifier, int $expectedCount): void + private function assertUrlRewritesCount(int $storeId, string $requestPath, int $expectedCount): void { $data = [ - UrlRewrite::REQUEST_PATH => $pageIdentifier, + UrlRewrite::REQUEST_PATH => $requestPath, UrlRewrite::STORE_ID => $storeId ]; $urlRewrites = $this->urlFinder->findAllByData($data); @@ -116,8 +117,6 @@ private function assertUrlRewritesCount(int $storeId, string $pageIdentifier, in /** * Create test store - * - * @return int */ private function createStore(): int { @@ -134,7 +133,6 @@ private function createStore(): int * Delete test store * * @param int $storeId - * @return void */ private function deleteStore(int $storeId): void { diff --git a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php index e7f714250f2c8..4906014ad1903 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Console/Command/ConfigShowCommandTest.php @@ -6,24 +6,23 @@ namespace Magento\Config\Console\Command; +use Magento\Config\Model\Config\Structure; use Magento\Framework\App\DeploymentConfig\FileReader; use Magento\Framework\App\DeploymentConfig\Writer; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Config\File\ConfigFilePool; use Magento\Framework\Console\Cli; use Magento\Framework\Filesystem; -use Magento\Framework\ObjectManagerInterface; use Magento\Store\Model\ScopeInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; -class ConfigShowCommandTest extends \PHPUnit\Framework\TestCase +/** + * Test for \Magento\Config\Console\Command\ConfigShowCommand. + */ +class ConfigShowCommandTest extends TestCase { - /** - * @var ObjectManagerInterface - */ - private $objectManager; - /** * @var CommandTester */ @@ -64,16 +63,22 @@ class ConfigShowCommandTest extends \PHPUnit\Framework\TestCase */ private $envConfig; + /** + * @var Structure + */ + private $structure; + /** * @inheritdoc */ protected function setUp(): void { - $this->objectManager = Bootstrap::getObjectManager(); - $this->configFilePool = $this->objectManager->get(ConfigFilePool::class); - $this->filesystem = $this->objectManager->get(Filesystem::class); - $this->reader = $this->objectManager->get(FileReader::class); - $this->writer = $this->objectManager->get(Writer::class); + $objectManager = Bootstrap::getObjectManager(); + $this->configFilePool = $objectManager->get(ConfigFilePool::class); + $this->filesystem = $objectManager->get(Filesystem::class); + $this->reader = $objectManager->get(FileReader::class); + $this->writer = $objectManager->get(Writer::class); + $this->structure = $objectManager->get(Structure::class); $this->config = $this->loadConfig(); $this->envConfig = $this->loadEnvConfig(); @@ -89,21 +94,27 @@ protected function setUp(): void $_ENV['CONFIG__WEBSITES__BASE__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.website_base.test'; $_ENV['CONFIG__STORES__DEFAULT__WEB__TEST2__TEST_VALUE_4'] = 'value4.env.store_default.test'; - $command = $this->objectManager->create(ConfigShowCommand::class); + $command = $objectManager->create(ConfigShowCommand::class); $this->commandTester = new CommandTester($command); } /** + * Test execute config show command + * * @param string $scope * @param string $scopeCode * @param int $resultCode * @param array $configs + * @return void + * * @magentoDbIsolation enabled * @magentoDataFixture Magento/Config/_files/config_data.php * @dataProvider executeDataProvider */ - public function testExecute($scope, $scopeCode, $resultCode, array $configs) + public function testExecute($scope, $scopeCode, $resultCode, array $configs): void { + $this->setConfigPaths(); + foreach ($configs as $inputPath => $configValue) { $arguments = [ ConfigShowCommand::INPUT_ARGUMENT_PATH => $inputPath @@ -130,6 +141,41 @@ public function testExecute($scope, $scopeCode, $resultCode, array $configs) } } + /** + * Set config paths to structure + * + * @return void + */ + private function setConfigPaths(): void + { + $reflection = new \ReflectionClass(Structure::class); + $mappedPaths = $reflection->getProperty('mappedPaths'); + $mappedPaths->setAccessible(true); + $mappedPaths->setValue($this->structure, $this->getConfigPaths()); + } + + /** + * Returns config paths + * + * @return array + */ + private function getConfigPaths(): array + { + $configs = [ + 'web/test/test_value_1', + 'web/test/test_value_2', + 'web/test2/test_value_3', + 'web/test2/test_value_4', + 'carriers/fedex/account', + 'paypal/fetch_reports/ftp_password', + 'web/test', + 'web/test2', + 'web', + ]; + + return array_flip($configs); + } + /** * @return array * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -240,7 +286,7 @@ public function executeDataProvider() Cli::RETURN_FAILURE, [ 'web/test/test_wrong_value' => [ - 'Configuration for path: "web/test/test_wrong_value" doesn\'t exist' + 'The "web/test/test_wrong_value" path doesn\'t exist. Verify and try again.' ], ] ], @@ -250,7 +296,7 @@ public function executeDataProvider() Cli::RETURN_FAILURE, [ 'web/test/test_wrong_value' => [ - 'Configuration for path: "web/test/test_wrong_value" doesn\'t exist' + 'The "web/test/test_wrong_value" path doesn\'t exist. Verify and try again.' ], ] ], diff --git a/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php b/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php index 1b7a504959d54..eedb93099b8c3 100644 --- a/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Config/Model/ConfigTest.php @@ -5,12 +5,18 @@ */ namespace Magento\Config\Model; +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Config\Model\ResourceModel\Config\Data\Collection; +use Magento\Config\Model\ResourceModel\Config\Data\CollectionFactory; +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** * @magentoAppArea adminhtml */ -class ConfigTest extends \PHPUnit\Framework\TestCase +class ConfigTest extends TestCase { /** * @covers \Magento\Config\Model\Config::save @@ -22,25 +28,25 @@ class ConfigTest extends \PHPUnit\Framework\TestCase public function testSaveWithSingleStoreModeEnabled($groups) { Bootstrap::getObjectManager()->get( - \Magento\Framework\Config\ScopeInterface::class + ScopeInterface::class )->setCurrentScope( - \Magento\Backend\App\Area\FrontNameResolver::AREA_CODE + FrontNameResolver::AREA_CODE ); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->setWebsite('base')->load(); $this->assertEmpty($_configData); - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configDataObject->setSection('dev')->setGroups($groups)->save(); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->load(); $this->assertArrayHasKey('dev/debug/template_hints_admin', $_configData); $this->assertArrayHasKey('dev/debug/template_hints_blocks', $_configData); - $_configDataObject = Bootstrap::getObjectManager()->create(\Magento\Config\Model\Config::class); + $_configDataObject = Bootstrap::getObjectManager()->create(Config::class); $_configData = $_configDataObject->setSection('dev')->setWebsite('base')->load(); $this->assertArrayNotHasKey('dev/debug/template_hints_admin', $_configData); $this->assertArrayNotHasKey('dev/debug/template_hints_blocks', $_configData); @@ -63,16 +69,16 @@ public function testSave($section, $groups, $expected) { $objectManager = Bootstrap::getObjectManager(); - /** @var $_configDataObject \Magento\Config\Model\Config */ - $_configDataObject = $objectManager->create(\Magento\Config\Model\Config::class); + /** @var $_configDataObject Config */ + $_configDataObject = $objectManager->create(Config::class); $_configDataObject->setSection($section)->setWebsite('base')->setGroups($groups)->save(); foreach ($expected as $group => $expectedData) { - $_configDataObject = $objectManager->create(\Magento\Config\Model\Config::class); + $_configDataObject = $objectManager->create(Config::class); $_configData = $_configDataObject->setSection($group)->setWebsite('base')->load(); if (array_key_exists('payment/payflow_link/pwd', $_configData)) { $_configData['payment/payflow_link/pwd'] = $objectManager->get( - \Magento\Framework\Encryption\EncryptorInterface::class + EncryptorInterface::class )->decrypt( $_configData['payment/payflow_link/pwd'] ); @@ -85,4 +91,102 @@ public function saveDataProvider() { return require __DIR__ . '/_files/config_section.php'; } + + /** + * @param string $website + * @param string $section + * @param array $override + * @param array $inherit + * @param array $expected + * @dataProvider saveWebsiteScopeDataProvider + */ + public function testSaveUseDefault( + string $website, + string $section, + array $override, + array $inherit, + array $expected + ): void { + $objectManager = Bootstrap::getObjectManager(); + /** @var Config $config*/ + $configFactory = $objectManager->create(ConfigFactory::class); + $config = $configFactory->create() + ->setSection($section) + ->setWebsite($website) + ->setGroups($override['groups']) + ->save(); + + $paths = array_keys($expected); + + $this->assertEquals( + $expected, + $this->getConfigValues($config->getScope(), $config->getScopeId(), $paths) + ); + + $config = $configFactory->create() + ->setSection($section) + ->setWebsite($website) + ->setGroups($inherit['groups']) + ->save(); + + $this->assertEmpty( + $this->getConfigValues($config->getScope(), $config->getScopeId(), $paths) + ); + } + + /** + * @return array + */ + public function saveWebsiteScopeDataProvider(): array + { + return [ + [ + 'website' => 'base', + 'section' => 'payment', + [ + 'groups' => [ + 'account' => [ + 'fields' => [ + 'merchant_country' => ['value' => 'GB'], + ], + ], + ] + ], + [ + 'groups' => [ + 'account' => [ + 'fields' => [ + 'merchant_country' => ['inherit' => 1], + ], + ], + ], + ], + 'expected' => [ + 'paypal/general/merchant_country' => 'GB', + ], + ] + ]; + } + + /** + * @param string $scope + * @param int $scopeId + * @param array $paths + * @return array + */ + private function getConfigValues(string $scope, int $scopeId, array $paths): array + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Collection $configCollection */ + $configCollectionFactory = $objectManager->create(CollectionFactory::class); + $configCollection = $configCollectionFactory->create(); + $configCollection->addFieldToFilter('scope', $scope); + $configCollection->addFieldToFilter('scope_id', $scopeId); + $configCollection->addFieldToFilter('path', ['in' => $paths]); + $result = []; + foreach ($configCollection as $data) { + $result[$data->getPath()] = $data->getValue(); + } + return $result; + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php new file mode 100644 index 0000000000000..b0a1c81857221 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Adminhtml/Product/Steps/AttributeValuesTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps; + +use Magento\Backend\Model\Auth\Session; +use Magento\ConfigurableProduct\Block\DataProviders\PermissionsData; +use Magento\Framework\View\Layout; +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; +use PHPUnit\Framework\TestCase; + +/** + * @magentoAppArea adminhtml + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + */ +class AttributeValuesTest extends TestCase +{ + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php + */ + public function testRestrictedUserNotAllowedToManageAttributes() + { + $user = Bootstrap::getObjectManager()->create( + User::class + )->loadByUsername( + 'admincatalog_user' + ); + + /** @var $session Session */ + $session = Bootstrap::getObjectManager()->get( + Session::class + ); + $session->setUser($user); + + /** @var $layout Layout */ + $layout = Bootstrap::getObjectManager()->get( + LayoutInterface::class + ); + + /** @var \Magento\ConfigurableProduct\Block\Adminhtml\Product\Steps\AttributeValues */ + $block = $layout->createBlock( + AttributeValues::class, + 'step2', + [ + 'data' => [ + 'config' => [ + 'form' => 'product_form.product_form', + 'modal' => 'configurableModal', + 'dataScope' => 'productFormConfigurable', + ], + 'permissions' => Bootstrap::getObjectManager()->get(PermissionsData::class) + ] + ] + ); + $isAllowedToManageAttributes = $block->getPermissions()->isAllowedToManageAttributes(); + $html = $block->toHtml(); + $this->assertFalse($isAllowedToManageAttributes); + $this->assertStringNotContainsString('<button class="action-create-new action-tertiary"', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableProductPriceTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableProductPriceTest.php index 327544911a45d..325ab7db23aaf 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableProductPriceTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableProductPriceTest.php @@ -14,13 +14,16 @@ use Magento\Framework\Registry; use Magento\Framework\Serialize\SerializerInterface; use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** * Check configurable product price displaying * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppIsolation enabled * @magentoAppArea frontend */ @@ -44,6 +47,12 @@ class ConfigurableProductPriceTest extends TestCase /** @var SerializerInterface */ private $json; + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -53,11 +62,13 @@ protected function setUp(): void $this->objectManager = Bootstrap::getObjectManager(); $this->registry = $this->objectManager->get(Registry::class); - $this->page = $this->objectManager->get(Page::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); $this->productRepository->cleanCache(); $this->productCustomOption = $this->objectManager->get(ProductCustomOptionInterface::class); $this->json = $this->objectManager->get(SerializerInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); } /** @@ -78,7 +89,20 @@ protected function tearDown(): void */ public function testConfigurablePrice(): void { - $this->assertPrice($this->processPriceView('configurable'), 10.00); + $this->assertPrice('configurable', 10.00); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testConfigurablePriceOnSecondWebsite(): void + { + $this->executeInStoreContext->execute('fixture_second_store', [$this, 'assertPrice'], 'configurable', 10.00); + $this->resetPageLayout(); + $this->assertPrice('configurable', 150.00); } /** @@ -88,7 +112,7 @@ public function testConfigurablePrice(): void */ public function testConfigurablePriceWithDisabledFirstChild(): void { - $this->assertPrice($this->processPriceView('configurable'), 20.00); + $this->assertPrice('configurable', 20.00); } /** @@ -98,7 +122,7 @@ public function testConfigurablePriceWithDisabledFirstChild(): void */ public function testConfigurablePriceWithOutOfStockFirstChild(): void { - $this->assertPrice($this->processPriceView('configurable'), 20.00); + $this->assertPrice('configurable', 20.00); } /** @@ -110,7 +134,7 @@ public function testConfigurablePriceWithOutOfStockFirstChild(): void */ public function testConfigurablePriceWithCatalogRule(): void { - $this->assertPrice($this->processPriceView('configurable'), 9.00); + $this->assertPrice('configurable', 9.00); } /** @@ -120,7 +144,7 @@ public function testConfigurablePriceWithCatalogRule(): void */ public function testConfigurablePriceWithCustomOption(): void { - $product = $this->productRepository->get('configurable'); + $product = $this->getProduct('configurable'); $this->registerProduct($product); $this->preparePageLayout(); $customOptionsBlock = $this->page->getLayout() @@ -162,6 +186,16 @@ private function preparePageLayout(): void $this->page->getLayout()->generateXml(); } + /** + * Reset layout page to get new block html. + * + * @return void + */ + private function resetPageLayout(): void + { + $this->page = $this->objectManager->get(PageFactory::class)->create(); + } + /** * Process view product final price block html. * @@ -170,7 +204,7 @@ private function preparePageLayout(): void */ private function processPriceView(string $sku): string { - $product = $this->productRepository->get($sku); + $product = $this->getProduct($sku); $this->registerProduct($product); $this->preparePageLayout(); @@ -180,12 +214,13 @@ private function processPriceView(string $sku): string /** * Assert that html contain price label and expected final price amount. * - * @param string $priceBlockHtml + * @param string $sku * @param float $expectedPrice * @return void */ - private function assertPrice(string $priceBlockHtml, float $expectedPrice): void + public function assertPrice(string $sku, float $expectedPrice): void { + $priceBlockHtml = $this->processPriceView($sku); $regexp = '/<span class="price-label">As low as<\/span>.*'; $regexp .= '<span.*data-price-amount="%s".*<span class="price">\$%.2f<\/span><\/span>/'; $this->assertMatchesRegularExpression( @@ -208,4 +243,15 @@ private function assertJsonConfig(string $config, string $expectedPrice, int $op $this->assertNotNull($price); $this->assertEquals($expectedPrice, $price); } + + /** + * Loads product by sku.s + * + * @param string $sku + * @return ProductInterface + */ + private function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get($sku, false, $this->storeManager->getStore()->getId(), true); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php index d577994cdc45b..bf910359b893b 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Block/Product/View/Type/ConfigurableViewOnCategoryPageTest.php @@ -7,11 +7,18 @@ namespace Magento\ConfigurableProduct\Block\Product\View\Type; +use Magento\Catalog\Api\CategoryRepositoryInterface; +use Magento\Catalog\Api\Data\ProductInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Block\Product\ListProduct; use Magento\Eav\Model\Entity\Collection\AbstractCollection; use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\View\LayoutInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** @@ -21,17 +28,30 @@ * @magentoAppIsolation enabled * @magentoAppArea frontend * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_out_of_stock_children.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class ConfigurableViewOnCategoryPageTest extends TestCase { /** @var ObjectManagerInterface */ private $objectManager; - /** @var LayoutInterface */ - private $layout; + /** @var ProductRepositoryInterface */ + private $productRepository; - /** @var ListProduct $listingBlock */ - private $listingBlock; + /** @var CategoryRepositoryInterface */ + private $categoryRepository; + + /** @var Page */ + private $page; + + /** @var Registry */ + private $registry; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; /** * @inheritdoc @@ -41,9 +61,22 @@ protected function setUp(): void parent::setUp(); $this->objectManager = Bootstrap::getObjectManager(); - $this->layout = $this->objectManager->get(LayoutInterface::class); - $this->listingBlock = $this->layout->createBlock(ListProduct::class); - $this->listingBlock->setCategoryId(333); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->categoryRepository = $this->objectManager->get(CategoryRepositoryInterface::class); + $this->page = $this->objectManager->get(PageFactory::class)->create(); + $this->registry = $this->objectManager->get(Registry::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('current_category'); + parent::tearDown(); } /** @@ -53,8 +86,8 @@ protected function setUp(): void */ public function testOutOfStockProductWithEnabledConfigView(): void { - $collection = $this->listingBlock->getLoadedProductCollection(); - $this->assertCollectionSize(1, $collection); + $this->preparePageLayout(); + $this->assertCollectionSize(1, $this->getListingBlock()->getLoadedProductCollection()); } /** @@ -64,8 +97,50 @@ public function testOutOfStockProductWithEnabledConfigView(): void */ public function testOutOfStockProductWithDisabledConfigView(): void { - $collection = $this->listingBlock->getLoadedProductCollection(); - $this->assertCollectionSize(0, $collection); + $this->preparePageLayout(); + $this->assertCollectionSize(0, $this->getListingBlock()->getLoadedProductCollection()); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_category.php + * + * @return void + */ + public function testCheckConfigurablePrice(): void + { + $this->assertProductPrice('configurable', 'As low as $10.00'); + } + + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php + * + * @return void + */ + public function testCheckConfigurablePriceOnSecondWebsite(): void + { + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertProductPrice'], + 'configurable', + __('As low as') . ' $10.00' + ); + $this->resetPageLayout(); + $this->assertProductPrice('configurable', __('As low as') . ' $150.00'); + } + + /** + * Checks product price. + * + * @param string $sku + * @param string $priceString + * @return void + */ + public function assertProductPrice(string $sku, string $priceString): void + { + $this->preparePageLayout(); + $this->assertCollectionSize(1, $this->getListingBlock()->getLoadedProductCollection()); + $priceHtml = $this->getListingBlock()->getProductPrice($this->getProduct($sku)); + $this->assertEquals($priceString, $this->clearPriceHtml($priceHtml)); } /** @@ -80,4 +155,64 @@ private function assertCollectionSize(int $expectedSize, AbstractCollection $col $this->assertEquals($expectedSize, $collection->getSize()); $this->assertCount($expectedSize, $collection->getItems()); } + + /** + * Prepare category page. + * + * @return void + */ + private function preparePageLayout(): void + { + $this->registry->unregister('current_category'); + $this->registry->register( + 'current_category', + $this->categoryRepository->get(333, $this->storeManager->getStore()->getId()) + ); + $this->page->addHandle(['default', 'catalog_category_view']); + $this->page->getLayout()->generateXml(); + } + + /** + * Reset layout page to get new block html. + * + * @return void + */ + private function resetPageLayout(): void + { + $this->page = $this->objectManager->get(PageFactory::class)->create(); + } + + /** + * Removes html tags and spaces from price html string. + * + * @param string $priceHtml + * @return string + */ + private function clearPriceHtml(string $priceHtml): string + { + return trim(preg_replace('/\s+/', ' ', strip_tags($priceHtml))); + } + + /** + * Returns product list block. + * + * @return null|ListProduct + */ + private function getListingBlock(): ?ListProduct + { + $block = $this->page->getLayout()->getBlock('category.products.list'); + + return $block ? $block : null; + } + + /** + * Loads product by sku. + * + * @param string $sku + * @return ProductInterface + */ + private function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get($sku, false, $this->storeManager->getStore()->getId(), true); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php index 52bb7a2f8ab6d..2ffba24e465e0 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/Product/Type/Configurable/PriceTest.php @@ -13,14 +13,17 @@ use Magento\Customer\Model\Group; use Magento\Framework\ObjectManagerInterface; use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Catalog\Model\Product\Price\GetPriceIndexDataByProductId; use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; use PHPUnit\Framework\TestCase; /** * Provides tests for configurable product pricing. * * @magentoDbIsolation disabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PriceTest extends TestCase { @@ -49,6 +52,16 @@ class PriceTest extends TestCase */ private $websiteRepository; + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var ExecuteInStoreContext + */ + private $executeInStoreContext; + /** * @inheritdoc */ @@ -60,6 +73,8 @@ protected function setUp(): void $this->productRepository->cleanCache(); $this->getPriceIndexDataByProductId = $this->objectManager->get(GetPriceIndexDataByProductId::class); $this->websiteRepository = $this->objectManager->get(WebsiteRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); } /** @@ -84,6 +99,46 @@ public function testGetFinalPrice(): void ); } + /** + * @magentoDataFixture Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php + * @return void + */ + public function testGetFinalPriceOnSecondWebsite(): void + { + $this->executeInStoreContext->execute('fixture_second_store', [$this, 'assertPrice'], 10); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertIndexTableData'], + 'configurable', + ['price' => 0, 'final_price' => 0, 'min_price' => 10, 'max_price' => 30, 'tier_price' => null] + ); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertIndexTableData'], + 'simple_option_1', + ['price' => 20, 'final_price' => 10, 'min_price' => 10, 'max_price' => 10, 'tier_price' => null] + ); + $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this, 'assertIndexTableData'], + 'simple_option_2', + ['price' => 40, 'final_price' => 30, 'min_price' => 30, 'max_price' => 30, 'tier_price' => null] + ); + $this->assertPrice(150); + $this->assertIndexTableData( + 'configurable', + ['price' => 0, 'final_price' => 0, 'min_price' => 150, 'max_price' => 150, 'tier_price' => null] + ); + $this->assertIndexTableData( + 'simple_option_1', + ['price' => 150, 'final_price' => 150, 'min_price' => 150, 'max_price' => 150, 'tier_price' => null] + ); + $this->assertIndexTableData( + 'simple_option_2', + ['price' => 150, 'final_price' => 150, 'min_price' => 150, 'max_price' => 150, 'tier_price' => null] + ); + } + /** * @magentoConfigFixture current_store tax/display/type 1 * @magentoDataFixture Magento/ConfigurableProduct/_files/tax_rule.php @@ -127,7 +182,7 @@ public function testGetFinalPriceIncludingExcludingTax(): void public function testGetFinalPriceWithSelectedSimpleProduct(): void { $product = $this->productRepository->get('configurable'); - $product->addCustomOption('simple_product', 20, $this->productRepository->get('simple_20')); + $product->addCustomOption('simple_product', 20, $this->getProduct('simple_20')); $this->assertPrice(20, $product); } @@ -137,7 +192,7 @@ public function testGetFinalPriceWithSelectedSimpleProduct(): void */ public function testGetFinalPriceWithCustomOptionAndSimpleTierPrice(): void { - $configurable = $this->productRepository->get('configurable'); + $configurable = $this->getProduct('configurable'); $this->assertIndexTableData( 'configurable', ['price' => 0, 'final_price' => 0, 'min_price' => 9, 'max_price' => 30, 'tier_price' => 15] @@ -167,12 +222,12 @@ public function testGetFinalPriceWithCustomOptionAndSimpleTierPrice(): void * @param array $expectedPrices * @return void */ - private function assertIndexTableData(string $sku, array $expectedPrices): void + public function assertIndexTableData(string $sku, array $expectedPrices): void { $data = $this->getPriceIndexDataByProductId->execute( - (int)$this->productRepository->get($sku)->getId(), + (int)$this->getProduct($sku)->getId(), Group::NOT_LOGGED_IN_ID, - (int)$this->websiteRepository->get('base')->getId() + (int)$this->storeManager->getStore()->getWebsiteId() ); $data = reset($data); foreach ($expectedPrices as $column => $price) { @@ -187,13 +242,24 @@ private function assertIndexTableData(string $sku, array $expectedPrices): void * @param ProductInterface|null $product * @return void */ - private function assertPrice(float $expectedPrice, ?ProductInterface $product = null): void + public function assertPrice(float $expectedPrice, ?ProductInterface $product = null): void { - $product = $product ?: $this->productRepository->get('configurable'); + $product = $product ?: $this->getProduct('configurable'); // final price is the lowest price of configurable variations $this->assertEquals( round($expectedPrice, 2), round($this->priceModel->getFinalPrice(1, $product), 2) ); } + + /** + * Loads product by sku. + * + * @param string $sku + * @return ProductInterface + */ + private function getProduct(string $sku): ProductInterface + { + return $this->productRepository->get($sku, false, $this->storeManager->getStore()->getId(), true); + } } diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php index 32eddb28151a7..ffa84ca740e62 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/Model/ResourceModel/Product/Indexer/Price/ConfigurableTest.php @@ -6,6 +6,7 @@ namespace Magento\ConfigurableProduct\Model\ResourceModel\Product\Indexer\Price; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; use Magento\Catalog\Model\Product\Attribute\Source\Status; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory; @@ -18,7 +19,7 @@ use Magento\Catalog\Api\Data\ProductInterface; /** - * Configurable test + * Test reindex of configurable products * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @magentoAppArea adminhtml @@ -64,7 +65,7 @@ protected function setUp(): void */ public function testGetProductFinalPriceIfOneOfChildIsDisabled(): void { - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->getById(10, false, null, true); @@ -75,7 +76,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabled(): void $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } @@ -93,7 +94,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabled(): void */ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore(): void { - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->get('simple_10', false, null, true); @@ -106,7 +107,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore(): void $this->productRepository->save($childProduct); $this->storeManager->setCurrentStore($currentStoreId); - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } @@ -122,7 +123,7 @@ public function testGetProductFinalPriceIfOneOfChildIsDisabledPerStore(): void */ public function testGetProductMinimalPriceIfOneOfChildIsOutOfStock(): void { - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(10, $configurableProduct->getMinimalPrice()); $childProduct = $this->productRepository->getById(10, false, null, true); @@ -130,25 +131,48 @@ public function testGetProductMinimalPriceIfOneOfChildIsOutOfStock(): void $stockItem->setIsInStock(Stock::STOCK_OUT_OF_STOCK); $this->stockRepository->save($stockItem); - $configurableProduct = $this->getConfigurableProductFromCollection(); + $configurableProduct = $this->getConfigurableProductFromCollection(1); $this->assertEquals(20, $configurableProduct->getMinimalPrice()); } + /** + * @magentoDataFixture Magento/Catalog/_files/enable_price_index_schedule.php + * @magentoDataFixture Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testReindexWithCorrectPriority() + { + $configurableProduct = $this->productRepository->get('configurable'); + $childProduct1 = $this->productRepository->get('simple_1'); + $childProduct2 = $this->productRepository->get('simple_2'); + $priceIndexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); + $priceIndexerProcessor->reindexList( + [$configurableProduct->getId(), $childProduct1->getId(), $childProduct2->getId()], + true + ); + + $configurableProduct = $this->getConfigurableProductFromCollection($configurableProduct->getId()); + $this->assertEquals($childProduct1->getPrice(), $configurableProduct->getMinimalPrice()); + } + /** * Retrieve configurable product. * Returns Configurable product that was created by Magento/ConfigurableProduct/_files/product_configurable.php * fixture * + * @param int $productId * @return ProductInterface */ - private function getConfigurableProductFromCollection(): ProductInterface + private function getConfigurableProductFromCollection(int $productId): ProductInterface { /** @var Collection $collection */ $collection = Bootstrap::getObjectManager()->get(CollectionFactory::class) ->create(); /** @var ProductInterface $configurableProduct */ $configurableProduct = $collection - ->addIdFilter([1]) + ->addIdFilter([$productId]) ->addMinimalPrice() ->load() ->getFirstItem(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first.php new file mode 100644 index 0000000000000..d882d6058f50c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$eavConfig = Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$firstAttribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_first'); + +$eavConfig->clear(); + +/** @var $installer \Magento\Catalog\Setup\CategorySetup */ +$installer = Bootstrap::getObjectManager()->create(\Magento\Catalog\Setup\CategorySetup::class); + +if (!$firstAttribute->getId()) { + + /** @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */ + $attribute = Bootstrap::getObjectManager()->create( + \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class + ); + + /** @var AttributeRepositoryInterface $attributeRepository */ + $attributeRepository = Bootstrap::getObjectManager()->create(AttributeRepositoryInterface::class); + + $firstAttribute->setData( + [ + 'attribute_code' => 'test_configurable_first', + 'entity_type_id' => $installer->getEntityTypeId('catalog_product'), + 'is_global' => 1, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 0, + 'used_in_product_listing' => 0, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Test Configurable First'], + 'backend_type' => 'int', + 'option' => [ + 'value' => [ + 'first_option_0' => ['First Option 1'], + 'first_option_1' => ['First Option 2'], + 'first_option_2' => ['First Option 3'], + 'first_option_3' => ['First Option 4'] + ], + 'order' => ['first_option_0' => 1, 'first_option_1' => 2, 'first_option_2' => 3, 'first_option_3' => 4], + ], + ] + ); + + $attributeRepository->save($firstAttribute); + + /* Assign attribute to attribute set */ + $installer->addAttributeToGroup('catalog_product', 'Default', 'General', $firstAttribute->getId()); +} + +$eavConfig->clear(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php new file mode 100644 index 0000000000000..9ce9b543c5485 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/** @var \Magento\Framework\Registry $registry */ +$registry = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Framework\Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$productCollection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Catalog\Model\ResourceModel\Product\Collection::class); +foreach ($productCollection as $product) { + $product->delete(); +} + +$eavConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get(\Magento\Eav\Model\Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable_first'); +if ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute + && $attribute->getId() +) { + $attribute->delete(); +} +$eavConfig->clear(); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php new file mode 100644 index 0000000000000..a0edde0becd9e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Api\Data\ProductExtensionFactory; +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Helper\DefaultCategory; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type as ProductType; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Model\ProductFactory; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\Store\Api\WebsiteRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_website_with_store_group_and_store.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductAttributeRepositoryInterface $productAttributeRepository */ +$productAttributeRepository = $objectManager->get(ProductAttributeRepositoryInterface::class); +/** @var WebsiteRepositoryInterface $websiteRepository */ +$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class); +/** @var ProductFactory $productFactory */ +$productFactory = $objectManager->get(ProductFactory::class); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var Factory $optionsFactory */ +$optionsFactory = $objectManager->get(Factory::class); +/** @var ProductExtensionFactory $extensionAttributesFactory */ +$extensionAttributesFactory = $objectManager->get(ProductExtensionFactory::class); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +/** @var SwitchPriceAttributeScopeOnConfigChange $observer */ +$observer = $objectManager->get(Observer::class); +/** @var DefaultCategory $categoryHelper */ +$categoryHelper = $objectManager->get(DefaultCategory::class); + +$attribute = $productAttributeRepository->get('test_configurable'); +$options = $attribute->getOptions(); +$baseWebsite = $websiteRepository->get('base'); +$secondWebsite = $websiteRepository->get('test'); +$attributeValues = []; +$associatedProductIds = []; +array_shift($options); + +foreach ($options as $option) { + $product = $productFactory->create(); + $product->setTypeId(ProductType::TYPE_SIMPLE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId(), $secondWebsite->getId()]) + ->setName('Configurable Option ' . $option->getLabel()) + ->setSku(strtolower(str_replace(' ', '_', 'simple ' . $option->getLabel()))) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setPrice(150) + ->setCategoryIds([$categoryHelper->getId(), 333]) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $product = $productRepository->save($product); + $associatedProductIds[] = $product->getId(); + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; +} +$configurableAttributesData = [ + [ + 'values' => $attributeValues, + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + ], +]; +$configurableOptions = $optionsFactory->create($configurableAttributesData); + +$product = $productFactory->create(); +$extensionConfigurableAttributes = $product->getExtensionAttributes() ?: $extensionAttributesFactory->create(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($product->getDefaultAttributeSetId()) + ->setWebsiteIds([$baseWebsite->getId(), $secondWebsite->getId()]) + ->setStatus(Status::STATUS_ENABLED) + ->setCategoryIds([$categoryHelper->getId(), 333]) + ->setSku('configurable') + ->setName('Configurable Product') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository->save($product); + +$configResource->saveConfig(Data::XML_PATH_PRICE_SCOPE, Store::PRICE_SCOPE_WEBSITE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +$secondStoreId = $storeManager->getStore('fixture_second_store')->getId(); + +try { + $currentStoreCode = $storeManager->getStore()->getCode(); + $storeManager->setCurrentStore('fixture_second_store'); + $firstChild = $productRepository->get('simple_option_1', false, $secondStoreId, true); + $firstChild->setPrice(20) + ->setSpecialPrice(10); + $productRepository->save($firstChild); + $secondChild = $productRepository->get('simple_option_2', false, $secondStoreId, true); + $secondChild->setPrice(40) + ->setSpecialPrice(30); + $productRepository->save($secondChild); +} finally { + $storeManager->setCurrentStore($currentStoreCode); +} diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php new file mode 100644 index 0000000000000..ceeeb13fb5a36 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/configurable_product_with_price_on_second_website_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Helper\Data; +use Magento\Catalog\Observer\SwitchPriceAttributeScopeOnConfigChange; +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ReinitableConfigInterface; +use Magento\Framework\Event\Observer; +use Magento\TestFramework\ConfigurableProduct\Model\DeleteConfigurableProduct; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DeleteConfigurableProduct $deleteConfigurableProduct */ +$deleteConfigurableProduct = $objectManager->get(DeleteConfigurableProduct::class); +$deleteConfigurableProduct->execute('configurable'); +/** @var Config $configResource */ +$configResource = $objectManager->get(Config::class); +$configResource->deleteConfig(Data::XML_PATH_PRICE_SCOPE, 'default', 0); +$objectManager->get(ReinitableConfigInterface::class)->reinit(); +$observer = $objectManager->get(Observer::class); +$objectManager->get(SwitchPriceAttributeScopeOnConfigChange::class)->execute($observer); + +Resolver::getInstance()->requireDataFixture( + 'Magento/Store/_files/second_website_with_store_group_and_store_rollback.php' +); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php' +); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php index 35439a24cd2db..f1f65602a9273 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable.php @@ -24,6 +24,7 @@ /** @var ProductRepositoryInterface $productRepository */ $productRepository = Bootstrap::getObjectManager() ->get(ProductRepositoryInterface::class); + /** @var $installer CategorySetup */ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); $eavConfig = Bootstrap::getObjectManager()->get(Config::class); @@ -35,7 +36,7 @@ $attributeValues = []; $attributeSetId = $installer->getAttributeSetId(Product::ENTITY, 'Default'); $associatedProductIds = []; -$productIds = [10, 20]; +$idsToReindex = $productIds = [10, 20]; array_shift($options); //remove the first option which is empty foreach ($options as $option) { diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php new file mode 100644 index 0000000000000..81a067195e902 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Indexer\Product\Price\Processor as PriceIndexerProcessor; +use Magento\Catalog\Model\Product; +use Magento\Catalog\Model\Product\Attribute\Source\Status; +use Magento\Catalog\Model\Product\Type; +use Magento\Catalog\Model\Product\Visibility; +use Magento\Catalog\Setup\CategorySetup; +use Magento\CatalogInventory\Model\Stock\Item; +use Magento\ConfigurableProduct\Helper\Product\Options\Factory; +use Magento\ConfigurableProduct\Model\Product\Type\Configurable; +use Magento\Eav\Api\Data\AttributeOptionInterface; +use Magento\Eav\Model\Config; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Bootstrap::getInstance()->reinitialize(); + +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); + +$installer = Bootstrap::getObjectManager()->create(CategorySetup::class); +$attributeSetId = $installer->getAttributeSetId('catalog_product', 'Default'); + +$eavConfig = Bootstrap::getObjectManager()->get(Config::class); +$attribute = $eavConfig->getAttribute('catalog_product', 'test_configurable'); + +$product = Bootstrap::getObjectManager()->create(Product::class); +$product->setTypeId(Configurable::TYPE_CODE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Product') + ->setSku('configurable') + ->setVisibility(Visibility::VISIBILITY_BOTH) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'is_in_stock' => 1]); +$productRepository = Bootstrap::getObjectManager()->get(ProductRepositoryInterface::class); +$product = $productRepository->save($product); + +$attributeValues = []; +$associatedProductIds = []; +/** @var AttributeOptionInterface[] $options */ +$options = $attribute->getOptions(); +array_shift($options); //remove the first option which is empty +$productNumber = 0; +foreach ($options as $option) { + $productNumber++; + + $childProduct = Bootstrap::getObjectManager()->create(Product::class); + $childProduct->setTypeId(Type::TYPE_SIMPLE) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productNumber) + ->setPrice($productNumber * 10) + ->setTestConfigurable($option->getValue()) + ->setVisibility(Visibility::VISIBILITY_NOT_VISIBLE) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData( + ['use_config_manage_stock' => 1,'qty' => $productNumber * 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1] + ); + $childProduct = $productRepository->save($childProduct); + + $stockItem = Bootstrap::getObjectManager()->create(Item::class); + $stockItem->load($childProduct->getId(), 'product_id'); + if (!$stockItem->getProductId()) { + $stockItem->setProductId($childProduct->getId()); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty($productNumber * 100); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $childProduct->getId(); +} + +$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor->reindexList($associatedProductIds); + +$optionsFactory = Bootstrap::getObjectManager()->create(Factory::class); +$configurableOptions = $optionsFactory->create( + [ + [ + 'attribute_id' => $attribute->getId(), + 'code' => $attribute->getAttributeCode(), + 'label' => $attribute->getStoreLabel(), + 'position' => '0', + 'values' => $attributeValues, + ], + ] +); +$extensionConfigurableAttributes = $product->getExtensionAttributes(); +$extensionConfigurableAttributes->setConfigurableProductOptions($configurableOptions); +$extensionConfigurableAttributes->setConfigurableProductLinks($associatedProductIds); +$product->setExtensionAttributes($extensionConfigurableAttributes); +$product = $productRepository->save($product); + +$indexerProcessor = Bootstrap::getObjectManager()->get(PriceIndexerProcessor::class); +$indexerProcessor->reindexRow($product->getId()); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples_rollback.php new file mode 100644 index 0000000000000..68621f78745e8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_assigned_simples_rollback.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\CatalogInventory\Model\Stock\Status; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); + +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +foreach (['simple_1', 'simple_2', 'configurable'] as $sku) { + try { + $product = $productRepository->get($sku, true); + + $stockStatus = $objectManager->create(Status::class); + $stockStatus->load($product->getEntityId(), 'product_id'); + $stockStatus->delete(); + + if ($product->getId()) { + $productRepository->delete($product); + } + } catch (\Magento\Framework\Exception\NoSuchEntityException $e) { + //Product already removed + } +} + +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_rollback.php'); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php index 879d19a0d0b96..91eb1d709b8f0 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight.php @@ -20,7 +20,7 @@ \Magento\TestFramework\Helper\Bootstrap::getInstance()->reinitialize(); -Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute.php'); +Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/configurable_attribute_first.php'); Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); /** @var ProductRepositoryInterface $productRepository */ @@ -31,7 +31,7 @@ $installer = Bootstrap::getObjectManager()->create(CategorySetup::class); /** @var Config $eavConfig */ $eavConfig = Bootstrap::getObjectManager()->get(Config::class); -$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable'); +$attribute = $eavConfig->getAttribute(Product::ENTITY, 'test_configurable_first'); /* Create simple products per each option value*/ /** @var AttributeOptionInterface[] $options */ $options = $attribute->getOptions(); @@ -46,113 +46,118 @@ 20 => Visibility::VISIBILITY_IN_CATALOG ]; +$i = 0; foreach ($options as $option) { - /** @var $product Product */ - $product = Bootstrap::getObjectManager()->create(Product::class); - $productId = array_shift($productIds); - $product->setTypeId(Type::TYPE_SIMPLE) - ->setId($productId) - ->setAttributeSetId($attributeSetId) - ->setWebsiteIds([1]) - ->setName('Configurable Option' . $option->getLabel()) - ->setSku('simple_' . $productId) - ->setPrice($productId) - ->setTestConfigurable($option->getValue()) - ->setVisibility($visibility[$productId]) - ->setStatus(Status::STATUS_ENABLED) - ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); - $eavAttributeValues = [ - 'category_ids' => [333] + if ($i < 2) { + /** @var $product Product */ + $product = Bootstrap::getObjectManager()->create(Product::class); + $productId = array_shift($productIds); + $product->setTypeId(Type::TYPE_SIMPLE) + ->setId($productId) + ->setAttributeSetId($attributeSetId) + ->setWebsiteIds([1]) + ->setName('Configurable Option' . $option->getLabel()) + ->setSku('simple_' . $productId) + ->setPrice($productId) + ->setTestConfigurable($option->getValue()) + ->setVisibility($visibility[$productId]) + ->setStatus(Status::STATUS_ENABLED) + ->setStockData(['use_config_manage_stock' => 1, 'qty' => 100, 'is_qty_decimal' => 0, 'is_in_stock' => 1]); + $eavAttributeValues = [ + 'category_ids' => [333], + $attribute->getAttributeCode() => $option->getValue() ]; - foreach ($eavAttributeValues as $eavCategoryAttributeCode => $eavCategoryAttributeValues) { - $product->setCustomAttribute($eavCategoryAttributeCode, $eavCategoryAttributeValues); - } - - $product = $productRepository->save($product); - - /** - * @var \Magento\TestFramework\ObjectManager $objectManager - */ - $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - - /** - * @var \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory $mediaGalleryEntryFactory - */ - - $mediaGalleryEntryFactory = $objectManager->get( - \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory::class - ); - - /** - * @var \Magento\Framework\Api\Data\ImageContentInterfaceFactory $imageContentFactory - */ - $imageContentFactory = $objectManager->get(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class); - $imageContent = $imageContentFactory->create(); - $testImagePath = __DIR__ .'/magento_image.jpg'; - $imageContent->setBase64EncodedData(base64_encode(file_get_contents($testImagePath))); - $imageContent->setType("image/jpeg"); - $imageContent->setName("1.jpg"); - - $video = $mediaGalleryEntryFactory->create(); - $video->setDisabled(false); - $video->setFile('1.jpg'); - $video->setLabel('Video Label'); - $video->setMediaType('external-video'); - $video->setPosition(2); - $video->setContent($imageContent); - - /** - * @var ProductAttributeMediaGalleryEntryExtensionFactory $mediaGalleryEntryExtensionFactory - */ - $mediaGalleryEntryExtensionFactory = $objectManager->get( - \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryExtensionFactory::class - ); - $mediaGalleryEntryExtension = $mediaGalleryEntryExtensionFactory->create(); - - /** - * @var \Magento\Framework\Api\Data\VideoContentInterfaceFactory $videoContentFactory - */ - $videoContentFactory = $objectManager->get( - \Magento\Framework\Api\Data\VideoContentInterfaceFactory::class - ); - $videoContent = $videoContentFactory->create(); - $videoContent->setMediaType('external-video'); - $videoContent->setVideoDescription('Video description'); - $videoContent->setVideoProvider('youtube'); - $videoContent->setVideoMetadata('Video Metadata'); - $videoContent->setVideoTitle('Video title'); - $videoContent->setVideoUrl('http://www.youtube.com/v/tH_2PFNmWoga'); - - $mediaGalleryEntryExtension->setVideoContent($videoContent); - $video->setExtensionAttributes($mediaGalleryEntryExtension); - - /** - * @var \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface $mediaGalleryManagement - */ - $mediaGalleryManagement = $objectManager->get( - \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface::class - ); - $mediaGalleryManagement->create('simple_' . $productId, $video); - - /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ - $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); - $stockItem->load($productId, 'product_id'); - - if (!$stockItem->getProductId()) { - $stockItem->setProductId($productId); + foreach ($eavAttributeValues as $eavCategoryAttributeCode => $eavCategoryAttributeValues) { + $product->setCustomAttribute($eavCategoryAttributeCode, $eavCategoryAttributeValues); + } + + $product = $productRepository->save($product); + + /** + * @var \Magento\TestFramework\ObjectManager $objectManager + */ + $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + + /** + * @var \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory $mediaGalleryEntryFactory + */ + + $mediaGalleryEntryFactory = $objectManager->get( + \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterfaceFactory::class + ); + + /** + * @var \Magento\Framework\Api\Data\ImageContentInterfaceFactory $imageContentFactory + */ + $imageContentFactory = $objectManager->get(\Magento\Framework\Api\Data\ImageContentInterfaceFactory::class); + $imageContent = $imageContentFactory->create(); + $testImagePath = __DIR__ .'/magento_image.jpg'; + $imageContent->setBase64EncodedData(base64_encode(file_get_contents($testImagePath))); + $imageContent->setType("image/jpeg"); + $imageContent->setName("1.jpg"); + + $video = $mediaGalleryEntryFactory->create(); + $video->setDisabled(false); + $video->setFile('1.jpg'); + $video->setLabel('Video Label'); + $video->setMediaType('external-video'); + $video->setPosition(2); + $video->setContent($imageContent); + + /** + * @var ProductAttributeMediaGalleryEntryExtensionFactory $mediaGalleryEntryExtensionFactory + */ + $mediaGalleryEntryExtensionFactory = $objectManager->get( + \Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryExtensionFactory::class + ); + $mediaGalleryEntryExtension = $mediaGalleryEntryExtensionFactory->create(); + + /** + * @var \Magento\Framework\Api\Data\VideoContentInterfaceFactory $videoContentFactory + */ + $videoContentFactory = $objectManager->get( + \Magento\Framework\Api\Data\VideoContentInterfaceFactory::class + ); + $videoContent = $videoContentFactory->create(); + $videoContent->setMediaType('external-video'); + $videoContent->setVideoDescription('Video description'); + $videoContent->setVideoProvider('youtube'); + $videoContent->setVideoMetadata('Video Metadata'); + $videoContent->setVideoTitle('Video title'); + $videoContent->setVideoUrl('http://www.youtube.com/v/tH_2PFNmWoga'); + + $mediaGalleryEntryExtension->setVideoContent($videoContent); + $video->setExtensionAttributes($mediaGalleryEntryExtension); + + /** + * @var \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface $mediaGalleryManagement + */ + $mediaGalleryManagement = $objectManager->get( + \Magento\Catalog\Api\ProductAttributeMediaGalleryManagementInterface::class + ); + $mediaGalleryManagement->create('simple_' . $productId, $video); + + /** @var \Magento\CatalogInventory\Model\Stock\Item $stockItem */ + $stockItem = Bootstrap::getObjectManager()->create(\Magento\CatalogInventory\Model\Stock\Item::class); + $stockItem->load($productId, 'product_id'); + + if (!$stockItem->getProductId()) { + $stockItem->setProductId($productId); + } + $stockItem->setUseConfigManageStock(1); + $stockItem->setQty(1000); + $stockItem->setIsQtyDecimal(0); + $stockItem->setIsInStock(1); + $stockItem->save(); + + $attributeValues[] = [ + 'label' => 'test', + 'attribute_id' => $attribute->getId(), + 'value_index' => $option->getValue(), + ]; + $associatedProductIds[] = $product->getId(); + $i++; } - $stockItem->setUseConfigManageStock(1); - $stockItem->setQty(1000); - $stockItem->setIsQtyDecimal(0); - $stockItem->setIsInStock(1); - $stockItem->save(); - - $attributeValues[] = [ - 'label' => 'test', - 'attribute_id' => $attribute->getId(), - 'value_index' => $option->getValue(), - ]; - $associatedProductIds[] = $product->getId(); } /** @var $product Product */ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php index ba3fad88c1fba..570679853fb87 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_configurable_with_category_and_weight_rollback.php @@ -3,7 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/ConfigurableProduct/_files/product_configurable_rollback.php'); -Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/category_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/ConfigurableProduct/_files/configurable_attribute_first_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php index 4e581ee0e995a..1419bec32431c 100644 --- a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/product_simple_77.php @@ -75,7 +75,14 @@ ] )->setCanSaveCustomOptions(true) ->setHasOptions(true) - ->setCustomAttribute('test_configurable', 42); + ->setCustomAttribute( + 'test_configurable', + Bootstrap::getObjectManager() + ->create(\Magento\Eav\Api\AttributeRepositoryInterface::class) + ->get('catalog_product', 'test_configurable') + ->getOptions()[1] + ->getValue() + ); $oldOptions = [ [ diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php new file mode 100644 index 0000000000000..7fd64c95f9942 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions.php @@ -0,0 +1,40 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Authorization\Model\Acl\Role\Group; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\UserContextInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; + +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->setName('role_catalog_permissions'); +$role->setData('role_name', $role->getName()); +$role->setRoleType(Group::ROLE_TYPE); +$role->setUserType((string)UserContextInterface::USER_TYPE_ADMIN); +$role->save(); + +/** @var $rule Rules */ +$rule = Bootstrap::getObjectManager()->create(Rules::class); +$rule->setRoleId($role->getId())->setResources(['Magento_Catalog::catalog'])->saveRel(); + +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->setData( + [ + 'firstname' => 'firstname', + 'lastname' => 'lastname', + 'email' => 'admincatalog@example.com', + 'username' => 'admincatalog_user', + 'password' => 'admincatalog_password1', + 'is_active' => 1, + ] +); +$user->setRoleId($role->getId())->save(); diff --git a/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php new file mode 100644 index 0000000000000..743503d1bd388 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ConfigurableProduct/_files/restricted_admin_with_catalog_permissions_rollback.php @@ -0,0 +1,28 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Authorization\Model\Role; +use Magento\Authorization\Model\RoleFactory; +use Magento\Authorization\Model\Rules; +use Magento\Authorization\Model\RulesFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\User\Model\User; + +// Deleting the user and the role. +/** @var User $user */ +$user = Bootstrap::getObjectManager()->create(User::class); +$user->loadByUsername('admincatalog_user')->delete(); +/** @var Role $role */ +$role = Bootstrap::getObjectManager()->get(RoleFactory::class)->create(); +$role->load('role_catalog_permissions', 'role_name'); +if ($role->getId()) { + /** @var Rules $rules */ + $rules = Bootstrap::getObjectManager()->get(RulesFactory::class)->create(); + $rules->load($role->getId(), 'role_id'); + $rules->delete(); + $role->delete(); +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/CachedBlockTest.php b/dev/tests/integration/testsuite/Magento/Csp/CachedBlockTest.php new file mode 100644 index 0000000000000..1b5325a07bdd1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/CachedBlockTest.php @@ -0,0 +1,82 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp; + +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Collector\DynamicCollectorMock; +use Magento\Framework\Math\Random; +use Magento\Framework\View\LayoutInterface; +use PHPUnit\Framework\TestCase; +use Magento\Framework\View\Element\Template; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test that inline util works fine with cached blocks. + */ +class CachedBlockTest extends TestCase +{ + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var DynamicCollectorMock + */ + private $dynamicCollected; + + /** + * @var Random + */ + private $random; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + DynamicCollector::class => DynamicCollectorMock::class + ] + ]); + $this->layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $this->dynamicCollected = Bootstrap::getObjectManager()->get(DynamicCollector::class); + $this->random = Bootstrap::getObjectManager()->get(Random::class); + } + + /** + * Validate policies preserved when reading block from cache. + * + * @return void + * + * @magentoAppArea frontend + * @magentoCache block_html enabled + */ + public function testCachedPolicies(): void + { + /** @var Template $block */ + $block = $this->layout->createBlock( + Template::class, + 'test-block', + ['data' => ['cache_lifetime' => 3600, 'cache_key' => $this->random->getRandomString(32)]] + ); + $block->setTemplate('Magento_TestModuleCspUtil::secure.phtml'); + //Clearing previously added just in case. + $this->dynamicCollected->consumeAdded(); + + $block->toHtml(); + $dynamic = $this->dynamicCollected->consumeAdded(); + $this->assertNotEmpty($dynamic); + + //From cache + $block->toHtml(); + $cached = $this->dynamicCollected->consumeAdded(); + $this->assertEquals($dynamic, $cached); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/CspAwareActionTest.php b/dev/tests/integration/testsuite/Magento/Csp/CspAwareActionTest.php new file mode 100644 index 0000000000000..883851c77a46f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/CspAwareActionTest.php @@ -0,0 +1,43 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp; + +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test that controllers can modify CSPs for a page. + * + * @magentoAppArea frontend + */ +class CspAwareActionTest extends AbstractController +{ + /** + * Check that a CSP aware action can modify CSPs after ALL other policies had been gathered. + * + * @return void + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/policies/storefront/script/policy_id script-src + * @magentoConfigFixture default_store csp/policies/storefront/script/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/script/hosts/example http://controller.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/script/self 0 + * @magentoConfigFixture default_store csp/policies/storefront/script/inline 0 + */ + public function testAwareAction(): void + { + $this->getRequest()->setMethod('GET'); + $this->dispatch('csputil/csp/aware'); + $header = $this->getResponse()->getHeader('Content-Security-Policy'); + $this->assertNotEmpty($header); + + $this->assertStringContainsString( + 'script-src https://controller.magento.com' + .' \'self\' \'sha256-H4RRnauTM2X2Xg/z9zkno1crqhsaY3uKKu97uwmnXXE=\'', + $header->getFieldValue() + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/CspTest.php b/dev/tests/integration/testsuite/Magento/Csp/CspTest.php index e66c6af36e42c..8905c953a00e3 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/CspTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/CspTest.php @@ -23,10 +23,6 @@ class CspTest extends AbstractController */ private function searchInResponse($response, string $search): bool { - if (mb_stripos(mb_strtolower($response->getBody()), mb_strtolower($search)) !== false) { - return true; - } - foreach ($response->getHeaders() as $header) { if (mb_stripos(mb_strtolower($header->toString()), mb_strtolower($search)) !== false) { return true; @@ -67,7 +63,7 @@ public function testStorefrontPolicies(): void $this->assertFalse($this->searchInResponse($response, '\'none\'')); $this->assertTrue($this->searchInResponse($response, 'script-src')); $this->assertTrue($this->searchInResponse($response, '\'unsafe-inline\'')); - $this->assertFalse($this->searchInResponse($response, 'font-src')); + $this->assertTrue($this->searchInResponse($response, 'font-src')); //Policies configured in cps_whitelist.xml files $this->assertTrue($this->searchInResponse($response, 'object-src')); $this->assertTrue($this->searchInResponse($response, 'media-src')); @@ -104,7 +100,7 @@ public function testAdminPolicies(): void $this->assertFalse($this->searchInResponse($response, '\'none\'')); $this->assertTrue($this->searchInResponse($response, 'script-src')); $this->assertTrue($this->searchInResponse($response, '\'unsafe-inline\'')); - $this->assertFalse($this->searchInResponse($response, 'font-src')); + $this->assertTrue($this->searchInResponse($response, 'font-src')); } /** diff --git a/dev/tests/integration/testsuite/Magento/Csp/CspUtilTest.php b/dev/tests/integration/testsuite/Magento/Csp/CspUtilTest.php new file mode 100644 index 0000000000000..93d019f572e63 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/CspUtilTest.php @@ -0,0 +1,42 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp; + +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test CSP util use cases. + * + * @magentoAppArea frontend + */ +class CspUtilTest extends AbstractController +{ + /** + * Test that CSP helper for templates works. + * + * @return void + * @magentoConfigFixture default_store csp/mode/storefront/report_only 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/inline 0 + */ + public function testPhtmlHelper(): void + { + $this->getRequest()->setMethod('GET'); + $this->dispatch('csputil/csp/helper'); + $content = $this->getResponse()->getContent(); + + $this->assertStringContainsString( + '<script src="http://my.magento.com/static/script.js"/>', + $content + ); + $this->assertStringContainsString("<script>\n let myVar = 1;\n</script>", $content); + $header = $this->getResponse()->getHeader('Content-Security-Policy'); + $this->assertNotEmpty($header); + $this->assertStringContainsString('http://my.magento.com', $header->getFieldValue()); + $this->assertStringContainsString('\'sha256-H4RRnauTM2X2Xg/z9zkno1crqhsaY3uKKu97uwmnXXE=\'', $header->getFieldValue()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Helper/InlineUtilTest.php b/dev/tests/integration/testsuite/Magento/Csp/Helper/InlineUtilTest.php new file mode 100644 index 0000000000000..0f31f0beccda9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Helper/InlineUtilTest.php @@ -0,0 +1,308 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Helper; + +use Magento\Csp\Api\Data\PolicyInterface; +use Magento\Csp\Model\Collector\DynamicCollector; +use Magento\Csp\Model\Collector\DynamicCollectorMock; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Cover CSP util use cases. + */ +class InlineUtilTest extends TestCase +{ + /** + * @var InlineUtil + */ + private $util; + + /** + * @var SecureHtmlRenderer + */ + private $secureHtmlRenderer; + + /** + * @var DynamicCollectorMock + */ + private $dynamicCollector; + + /** + * @inheritDoc + */ + public function setUp(): void + { + Bootstrap::getObjectManager()->configure([ + 'preferences' => [ + DynamicCollector::class => DynamicCollectorMock::class + ] + ]); + $this->util = Bootstrap::getObjectManager()->get(InlineUtil::class); + $this->secureHtmlRenderer = Bootstrap::getObjectManager()->get(SecureHtmlRenderer::class); + $this->dynamicCollector = Bootstrap::getObjectManager()->get(DynamicCollector::class); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->util = null; + $this->secureHtmlRenderer = null; + $this->dynamicCollector->consumeAdded(); + $this->dynamicCollector = null; + } + + /** + * Test tag rendering. + * + * @param string $tagName + * @param array $attributes + * @param string|null $content + * @param string $result Expected result. + * @param PolicyInterface[] $policiesExpected + * @return void + * @dataProvider getTags + */ + public function testRenderTag( + string $tagName, + array $attributes, + ?string $content, + string $result, + array $policiesExpected + ): void { + $this->assertEquals($result, $this->util->renderTag($tagName, $attributes, $content)); + $this->assertEquals($policiesExpected, $this->dynamicCollector->consumeAdded()); + } + + /** + * Test data for tag rendering test. + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function getTags(): array + { + return [ + 'remote-script' => [ + 'script', + ['src' => 'http://magento.com/static/some-script.js'], + null, + '<script src="http://magento.com/static/some-script.js"/>', + [new FetchPolicy('script-src', false, ['http://magento.com'])] + ], + 'inline-script' => [ + 'script', + ['type' => 'text/javascript'], + "\n let someVar = 25;\n document.getElementById('test').innerText = someVar;\n", + "<script type=\"text/javascript\">\n let someVar = 25;" + ."\n document.getElementById('test').innerText = someVar;\n</script>", + [ + new FetchPolicy( + 'script-src', + false, + [], + [], + false, + false, + false, + [], + ['U+SKpEef030N2YgyKKdIBIvPy8Fmd42N/JcTZgQV+DA=' => 'sha256'] + ) + ] + ], + 'remote-style' => [ + 'link', + ['rel' => 'stylesheet', 'type' => 'text/css', 'href' => 'http://magento.com/static/style.css'], + null, + '<link rel="stylesheet" type="text/css"' + . ' href="http://magento.com/static/style.css"/>', + [new FetchPolicy('style-src', false, ['http://magento.com'])] + ], + 'inline-style' => [ + 'style', + [], + "\n h1 {color: red;}\n p {color: green;}\n", + "<style>\n h1 {color: red;}\n p {color: green;}\n</style>", + [ + new FetchPolicy( + 'style-src', + false, + [], + [], + false, + false, + false, + [], + ['KISO7smrk+XdGrEsiPvVjX6qx4wNef/UKjNb26RaKGM=' => 'sha256'] + ) + ] + ], + 'remote-image' => [ + 'img', + ['src' => 'http://magento.com/static/my.jpg'], + null, + '<img src="http://magento.com/static/my.jpg"/>', + [new FetchPolicy('img-src', false, ['http://magento.com'])] + ], + 'remote-font' => [ + 'style', + ['type' => 'text/css'], + "\n @font-face {\n font-family: \"MyCustomFont\";" + ."\n src: url(\"http://magento.com/static/font.ttf\");\n }\n" + ." @font-face {\n font-family: \"MyCustomFont2\";" + ."\n src: url('https://magento.com/static/font-2.ttf')," + ."\n url(static/font.ttf)," + ."\n url(https://devdocs.magento.com/static/another-font.woff)," + ."\n url(http://devdocs.magento.com/static/font.woff);\n }\n", + "<style type=\"text/css\">" + ."\n @font-face {\n font-family: \"MyCustomFont\";" + ."\n src: url(\"http://magento.com/static/font.ttf\");\n }\n" + ." @font-face {\n font-family: \"MyCustomFont2\";" + ."\n src: url('https://magento.com/static/font-2.ttf')," + ."\n url(static/font.ttf)," + ."\n url(https://devdocs.magento.com/static/another-font.woff)," + ."\n url(http://devdocs.magento.com/static/font.woff);\n }\n" + ."</style>", + [ + new FetchPolicy( + 'style-src', + false, + [ + 'http://magento.com', + 'https://magento.com', + 'https://devdocs.magento.com', + 'http://devdocs.magento.com' + ] + ), + new FetchPolicy( + 'style-src', + false, + [], + [], + false, + false, + false, + [], + ['TP6Ulnz1kstJ8PYUKvowgJm0phHhtqJnJCnWxKLXkf0=' => 'sha256'] + ) + ] + ], + 'cross-origin-form' => [ + 'form', + ['action' => 'https://magento.com/submit', 'method' => 'post'], + "\n <input type=\"text\" name=\"test\" /><input type=\"submit\" value=\"Submit\" />\n", + "<form action=\"https://magento.com/submit\" method=\"post\">" + ."\n <input type=\"text\" name=\"test\" /><input type=\"submit\" value=\"Submit\" />\n" + ."</form>", + [new FetchPolicy('form-action', false, ['https://magento.com'])] + ], + 'cross-origin-iframe' => [ + 'iframe', + ['src' => 'http://magento.com/some-page'], + null, + '<iframe src="http://magento.com/some-page"/>', + [new FetchPolicy('frame-src', false, ['http://magento.com'])] + ], + 'remote-track' => [ + 'track', + ['src' => 'http://magento.com/static/track.vtt', 'kind' => 'subtitles'], + null, + '<track src="http://magento.com/static/track.vtt" kind="subtitles"/>', + [new FetchPolicy('media-src', false, ['http://magento.com'])] + ], + 'remote-source' => [ + 'source', + ['src' => 'http://magento.com/static/track.ogg', 'type' => 'audio/ogg'], + null, + '<source src="http://magento.com/static/track.ogg" type="audio/ogg"/>', + [new FetchPolicy('media-src', false, ['http://magento.com'])] + ], + 'remote-video' => [ + 'video', + ['src' => 'https://magento.com/static/video.mp4'], + null, + '<video src="https://magento.com/static/video.mp4"/>', + [new FetchPolicy('media-src', false, ['https://magento.com'])] + ], + 'remote-audio' => [ + 'audio', + ['src' => 'https://magento.com/static/audio.mp3'], + null, + '<audio src="https://magento.com/static/audio.mp3"/>', + [new FetchPolicy('media-src', false, ['https://magento.com'])] + ], + 'remote-object' => [ + 'object', + ['data' => 'http://magento.com/static/flash.swf'], + null, + '<object data="http://magento.com/static/flash.swf"/>', + [new FetchPolicy('object-src', false, ['http://magento.com'])] + ], + 'remote-embed' => [ + 'embed', + ['src' => 'http://magento.com/static/flash.swf'], + null, + '<embed src="http://magento.com/static/flash.swf"/>', + [new FetchPolicy('object-src', false, ['http://magento.com'])] + ], + 'remote-applet' => [ + 'applet', + ['code' => 'SomeApplet.class', 'archive' => 'https://magento.com/applet/my-applet.jar'], + null, + '<applet code="SomeApplet.class" ' + . 'archive="https://magento.com/applet/my-applet.jar"/>', + [new FetchPolicy('object-src', false, ['https://magento.com'])] + ] + ]; + } + + /** + * Test that inline event listeners are rendered properly. + * + * @return void + */ + public function testRenderEventListener(): void + { + $result = $this->util->renderEventListener('onclick', 'alert()'); + $this->assertEquals('onclick="alert()"', $result); + $this->assertEquals( + [new FetchPolicy('script-src', false, [], [], false, true)], + $this->dynamicCollector->consumeAdded() + ); + } + + /** + * Check that CSP logic was added to SecureHtmlRenderer + * + * @return void + */ + public function testSecureHtmlRenderer(): void + { + $scriptTag = $this->secureHtmlRenderer->renderTag( + 'script', + ['src' => 'https://test.magento.com/static/script.js'] + ); + $eventListener = $this->secureHtmlRenderer->renderEventListener('onclick', 'alert()'); + + $this->assertEquals( + '<script src="https://test.magento.com/static/script.js"/>', + $scriptTag + ); + $this->assertEquals( + 'onclick="alert()"', + $eventListener + ); + $policies = $this->dynamicCollector->consumeAdded(); + $this->assertTrue(in_array(new FetchPolicy('script-src', false, ['https://test.magento.com']), $policies)); + $this->assertTrue(in_array(new FetchPolicy('script-src', false, [], [], false, true), $policies)); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php index e88d5d723ef46..2d8cbbeedeab9 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ConfigCollectorTest.php @@ -45,7 +45,7 @@ private function getExpectedPolicies(): array 'child-src', false, ['http://magento.com', 'http://devdocs.magento.com'], - ['http'], + ['http', 'https', 'blob'], true, true, false, @@ -86,59 +86,74 @@ private function getExpectedPolicies(): array * Test initiating policies from config. * * @magentoAppArea frontend - * @magentoConfigFixture default_store csp/policies/storefront/default_src/policy_id default-src - * @magentoConfigFixture default_store csp/policies/storefront/default_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example http://magento.com - * @magentoConfigFixture default_store csp/policies/storefront/default_src/hosts/example2 http://devdocs.magento.com - * @magentoConfigFixture default_store csp/policies/storefront/default_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/policy_id child-src - * @magentoConfigFixture default_store csp/policies/storefront/child_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/hosts/example http://magento.com - * @magentoConfigFixture default_store csp/policies/storefront/child_src/hosts/example2 http://devdocs.magento.com - * @magentoConfigFixture default_store csp/policies/storefront/child_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/inline 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src/schemes/scheme1 http - * @magentoConfigFixture default_store csp/policies/storefront/child_src/dynamic 1 - * @magentoConfigFixture default_store csp/policies/storefront/child_src2/policy_id child-src - * @magentoConfigFixture default_store csp/policies/storefront/child_src2/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/child_src2/eval 1 - * @magentoConfigFixture default_store csp/policies/storefront/connect_src/policy_id connect-src - * @magentoConfigFixture default_store csp/policies/storefront/connect_src/none 1 - * @magentoConfigFixture default_store csp/policies/storefront/font_src/policy_id font-src - * @magentoConfigFixture default_store csp/policies/storefront/font_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/font_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/policy_id frame-src - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/frame_src/dynamic 1 - * @magentoConfigFixture default_store csp/policies/storefront/img_src/policy_id img-src - * @magentoConfigFixture default_store csp/policies/storefront/img_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/img_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/policy_id manifest-src - * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/manifest_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/media_src/policy_id media-src - * @magentoConfigFixture default_store csp/policies/storefront/media_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/media_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/object_src/policy_id object-src - * @magentoConfigFixture default_store csp/policies/storefront/object_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/object_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/script_src/policy_id script-src - * @magentoConfigFixture default_store csp/policies/storefront/script_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/script_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/script_src/event_handlers 1 + * @magentoConfigFixture default_store csp/policies/storefront/default/policy_id default-src + * @magentoConfigFixture default_store csp/policies/storefront/default/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/default/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/default/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/default/eval 0 + * @magentoConfigFixture default_store csp/policies/storefront/default/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/children/policy_id child-src + * @magentoConfigFixture default_store csp/policies/storefront/children/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/children/hosts/example http://magento.com + * @magentoConfigFixture default_store csp/policies/storefront/children/hosts/example2 http://devdocs.magento.com + * @magentoConfigFixture default_store csp/policies/storefront/children/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/children/inline 1 + * @magentoConfigFixture default_store csp/policies/storefront/children/schemes/scheme1 http + * @magentoConfigFixture default_store csp/policies/storefront/children/dynamic 1 + * @magentoConfigFixture default_store csp/policies/storefront/children-2/policy_id child-src + * @magentoConfigFixture default_store csp/policies/storefront/children-2/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/children-2/eval 1 + * @magentoConfigFixture default_store csp/policies/storefront/connections/policy_id connect-src + * @magentoConfigFixture default_store csp/policies/storefront/connections/none 1 + * @magentoConfigFixture default_store csp/policies/storefront/connections/self 0 + * @magentoConfigFixture default_store csp/policies/storefront/connections/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/fonts/policy_id font-src + * @magentoConfigFixture default_store csp/policies/storefront/fonts/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/fonts/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/fonts/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/frames/policy_id frame-src + * @magentoConfigFixture default_store csp/policies/storefront/frames/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/frames/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/frames/dynamic 1 + * @magentoConfigFixture default_store csp/policies/storefront/frames/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/images/policy_id img-src + * @magentoConfigFixture default_store csp/policies/storefront/images/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/images/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/images/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/manifests/policy_id manifest-src + * @magentoConfigFixture default_store csp/policies/storefront/manifests/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/manifests/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/manifests/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/media/policy_id media-src + * @magentoConfigFixture default_store csp/policies/storefront/media/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/media/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/media/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/objects/policy_id object-src + * @magentoConfigFixture default_store csp/policies/storefront/objects/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/objects/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/objects/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/policy_id script-src + * @magentoConfigFixture default_store csp/policies/storefront/scripts/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/eval 0 + * @magentoConfigFixture default_store csp/policies/storefront/scripts/event_handlers 1 * @magentoConfigFixture default_store csp/policies/storefront/base_uri/policy_id base-uri * @magentoConfigFixture default_store csp/policies/storefront/base_uri/none 0 * @magentoConfigFixture default_store csp/policies/storefront/base_uri/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/style_src/policy_id style-src - * @magentoConfigFixture default_store csp/policies/storefront/style_src/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/style_src/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/form_action/policy_id form-action - * @magentoConfigFixture default_store csp/policies/storefront/form_action/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/form_action/self 1 - * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/policy_id frame-ancestors - * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/none 0 - * @magentoConfigFixture default_store csp/policies/storefront/frame_ancestors/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/styles/policy_id style-src + * @magentoConfigFixture default_store csp/policies/storefront/styles/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/styles/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/styles/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/forms/policy_id form-action + * @magentoConfigFixture default_store csp/policies/storefront/forms/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/forms/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/forms/inline 0 + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/policy_id frame-ancestors + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/none 0 + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/self 1 + * @magentoConfigFixture default_store csp/policies/storefront/frame-ancestors/inline 0 * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/policy_id plugin-types * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/types/fl application/x-shockwave-flash * @magentoConfigFixture default_store csp/policies/storefront/plugin_types/types/applet application/x-java-applet @@ -155,32 +170,30 @@ private function getExpectedPolicies(): array * @magentoConfigFixture default_store csp/policies/storefront/sandbox/navigation 1 * @magentoConfigFixture default_store csp/policies/storefront/sandbox/navigation_by_user 1 * @magentoConfigFixture default_store csp/policies/storefront/mixed_content/policy_id block-all-mixed-content + * @magentoConfigFixture default_store csp/policies/storefront/base/policy_id base-uri + * @magentoConfigFixture default_store csp/policies/storefront/base/inline 0 * @magentoConfigFixture default_store csp/policies/storefront/upgrade/policy_id upgrade-insecure-requests * @return void */ public function testCollecting(): void { $policies = $this->collector->collect([new FlagPolicy('upgrade-insecure-requests')]); - $checked = []; $expectedPolicies = $this->getExpectedPolicies(); - - //Policies were collected $this->assertNotEmpty($policies); - //Default policies are being kept - /** @var PolicyInterface $defaultPolicy */ $defaultPolicy = array_shift($policies); $this->assertEquals('upgrade-insecure-requests', $defaultPolicy->getId()); - //Comparing collected with configured - /** @var PolicyInterface $policy */ + $expectedPolicyKeys = array_keys($expectedPolicies); + $checkedKeys = []; + foreach ($policies as $policy) { $id = $policy->getId(); + $this->assertTrue(in_array($id, $expectedPolicyKeys)); if ($id === 'child-src' && $policy->isEvalAllowed()) { $id = 'child-src2'; } - $this->assertEquals($expectedPolicies[$id], $policy); - $checked[] = $id; + $this->assertEquals($expectedPolicies[$id]->getValue(), $policy->getValue()); + $checkedKeys[] = $id; } - $expectedIds = array_keys($expectedPolicies); - $this->assertEquals(sort($expectedIds), sort($checked)); + $this->assertEmpty(array_diff($expectedPolicyKeys, $checkedKeys)); } } diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ControllerCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ControllerCollectorTest.php new file mode 100644 index 0000000000000..c3a5e58e7f9be --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/ControllerCollectorTest.php @@ -0,0 +1,85 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\CspAwareActionInterface; +use Magento\Csp\Model\Policy\FetchPolicy; +use Magento\Csp\Model\Policy\FlagPolicy; +use Magento\Framework\Exception\NotFoundException; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test collecting policies from a CSP-aware controllers. + */ +class ControllerCollectorTest extends TestCase +{ + /** + * @var ControllerCollector + */ + private $collector; + + /** + * @inheritDoc + */ + public function setUp(): void + { + $this->collector = Bootstrap::getObjectManager()->create(ControllerCollector::class); + } + + /** + * Test collection. + * + * @return void + */ + public function testCollect(): void + { + $controller = new class implements CspAwareActionInterface { + /** + * @inheritDoc + */ + public function execute() + { + throw new NotFoundException(__('Page not found.')); + } + + /** + * @inheritDoc + */ + public function modifyCsp(array $appliedPolicies): array + { + $processed = []; + foreach ($appliedPolicies as $policy) { + if ($policy instanceof FetchPolicy && $policy->getHostSources()) { + $policy = new FetchPolicy( + 'default-src', + false, + array_map( + function ($host) { + return str_replace('http://', 'https://', $host); + }, + $policy->getHostSources() + ) + ); + } + $processed[] = $policy; + } + $processed[] = new FlagPolicy(FlagPolicy::POLICIES[0]); + + return $processed; + } + }; + + $this->collector->setCurrentActionInstance($controller); + $collected = $this->collector->collect([new FetchPolicy('default-src', false, ['http://magento.com'])]); + $this->assertEquals( + [new FetchPolicy('default-src', false, ['https://magento.com']), new FlagPolicy(FlagPolicy::POLICIES[0])], + $collected + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php index 453d7bd0947af..67a15c24ea410 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/CspWhitelistXmlCollectorTest.php @@ -53,7 +53,13 @@ public function testCollecting(): void if ($policy->getId() === 'object-src') { $this->assertInstanceOf(FetchPolicy::class, $policy); $this->assertEquals(['http://magento.com', 'https://devdocs.magento.com'], $policy->getHostSources()); - $this->assertEquals(['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], $policy->getHashes()); + $this->assertEquals( + [ + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256', + 'B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF9=' => 'sha256' + ], + $policy->getHashes() + ); $objectSrcChecked = true; } elseif ($policy->getId() === 'media-src') { $this->assertInstanceOf(FetchPolicy::class, $policy); diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/DynamicCollectorMock.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/DynamicCollectorMock.php new file mode 100644 index 0000000000000..744df4c92018b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Collector/DynamicCollectorMock.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Csp\Model\Collector; + +use Magento\Csp\Api\Data\PolicyInterface; + +/** + * Helps with testing CSP policies. + */ +class DynamicCollectorMock extends DynamicCollector +{ + /** + * @var PolicyInterface[] + */ + private $added = []; + + /** + * @inheritDoc + */ + public function add(PolicyInterface $policy): void + { + $this->added[] = $policy; + + parent::add($policy); + } + + /** + * Collect added policies and start a new cycle. + * + * @return PolicyInterface[] + */ + public function consumeAdded(): array + { + $policies = $this->added; + $this->added = []; + + return $policies; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php b/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php index e9e9ed99ecd7c..388d35d1285b2 100644 --- a/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php +++ b/dev/tests/integration/testsuite/Magento/Csp/Model/Policy/Renderer/SimplePolicyHeaderRendererTest.php @@ -53,13 +53,7 @@ public function testRenderRestrictMode(): void $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy')); $this->assertEmpty($this->response->getHeader('Content-Security-Policy-Report-Only')); - $contentSecurityPolicyContent = []; - if ($header instanceof \ArrayIterator) { - foreach ($header as $item) { - $contentSecurityPolicyContent[] = $item->getFieldValue(); - } - } - $this->assertEquals(['default-src https://magento.com \'self\';'], $contentSecurityPolicyContent); + $this->assertEquals('default-src https://magento.com \'self\';', $header->getFieldValue()); } /** @@ -79,15 +73,9 @@ public function testRenderRestrictWithReportingMode(): void $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy')); $this->assertEmpty($this->response->getHeader('Content-Security-Policy-Report-Only')); - $contentSecurityPolicyContent = []; - if ($header instanceof \ArrayIterator) { - foreach ($header as $item) { - $contentSecurityPolicyContent[] = $item->getFieldValue(); - } - } $this->assertEquals( - ['default-src https://magento.com \'self\'; report-uri /csp-reports/; report-to report-endpoint;'], - $contentSecurityPolicyContent + 'default-src https://magento.com \'self\'; report-uri /csp-reports/; report-to report-endpoint;', + $header->getFieldValue() ); $this->assertNotEmpty($reportToHeader = $this->response->getHeader('Report-To')); $this->assertNotEmpty($reportData = json_decode("[{$reportToHeader->getFieldValue()}]", true)); @@ -111,7 +99,7 @@ public function testRenderReportMode(): void ['https://magento.com'], ['https'], true, - true, + false, true, ['5749837589457695'], ['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], @@ -124,13 +112,48 @@ public function testRenderReportMode(): void $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy-Report-Only')); $this->assertEmpty($this->response->getHeader('Content-Security-Policy')); $this->assertEquals( - 'default-src https://magento.com https: \'self\' \'unsafe-inline\' \'unsafe-eval\' \'strict-dynamic\'' + 'default-src https://magento.com https: \'self\' \'unsafe-eval\' \'strict-dynamic\'' . ' \'unsafe-hashes\' \'nonce-'.base64_encode($policy->getNonceValues()[0]).'\'' . ' \'sha256-B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=\';', $header->getFieldValue() ); } + /** + * Test rendering a fetch policy with inline allowed. + * + * @magentoAppArea frontend + * @magentoConfigFixture default_store csp/mode/storefront/report_only 1 + * @magentoConfigFixture default_store csp/mode/storefront/report_uri 0 + * + * @return void + */ + public function testFetchPolicyInlineAllowed(): void + { + $policy = new FetchPolicy( + 'script-src', + false, + ['https://magento.com'], + ['https'], + true, + true, + true, + ['5749837589457695'], + ['B2yPHKaXnvFWtRChIbabYmUBFZdVfKKXHbWtWidDVF8=' => 'sha256'], + false, + false + ); + + $this->renderer->render($policy, $this->response); + + $this->assertNotEmpty($header = $this->response->getHeader('Content-Security-Policy-Report-Only')); + $this->assertEmpty($this->response->getHeader('Content-Security-Policy')); + $this->assertEquals( + 'script-src https://magento.com https: \'self\' \'unsafe-inline\' \'unsafe-eval\';', + $header->getFieldValue() + ); + } + /** * Test policy rendering in report-only mode with report URL provided. * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/OrderButtonTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/OrderButtonTest.php new file mode 100644 index 0000000000000..1a093c741b1b2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/OrderButtonTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit; + +use Magento\Backend\Model\Search\AuthorizationMock; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\Authorization; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Class checks Create Order button visibility + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class OrderButtonTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var OrderButton */ + private $button; + + /** @var Registry */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->objectManager->addSharedInstance( + $this->objectManager->get(AuthorizationMock::class), + Authorization::class + ); + $this->button = $this->objectManager->get(OrderButton::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + + parent::tearDown(); + } + + /** + * @return void + */ + public function testGetButtonDataWithoutCustomer(): void + { + $this->assertEmpty($this->button->getButtonData()); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testGetButtonDataWithCustomer(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, 1); + $data = $this->button->getButtonData(); + $this->assertNotEmpty($data); + $this->assertEquals(__('Create Order'), $data['label']); + $this->assertStringContainsString('sales/order_create/start/customer_id/1/', $data['on_click']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CollectionTest.php new file mode 100644 index 0000000000000..b37870535d9da --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/CollectionTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; +use Magento\Customer\Controller\RegistryConstants; +use Magento\Framework\Data\Collection; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Model\QuoteRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Store\ExecuteInStoreContext; +use PHPUnit\Framework\TestCase; + +/** + * Class checks that shopping cart grid can be filtered + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart::_prepareCollection() + * + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + */ +class CollectionTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var ExecuteInStoreContext */ + private $executeInStoreContext; + + /** @var Registry */ + private $registry; + + /** @var LayoutInterface */ + private $layout; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->executeInStoreContext = $this->objectManager->get(ExecuteInStoreContext::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->layout = $this->objectManager->get(LayoutInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * @magentoDataFixture Magento/Checkout/_files/customer_quote_on_second_website.php + * + * @return void + */ + public function testCollectionOnDifferentStores(): void + { + $this->registry->unregister(RegistryConstants::CURRENT_CUSTOMER_ID); + $this->registry->register(RegistryConstants::CURRENT_CUSTOMER_ID, 1); + $collectionFirstWebsite = $this->executeInStoreContext->execute( + 'default', + [$this->layout->createBlock(Cart::class), 'getPreparedCollection'] + ); + $this->assertCollection($collectionFirstWebsite, 'Simple Product'); + $this->objectManager->removeSharedInstance(QuoteRepository::class); + $collectionSecondWebsite = $this->executeInStoreContext->execute( + 'fixture_second_store', + [$this->layout->createBlock(Cart::class), 'getPreparedCollection'] + ); + $this->assertCollection($collectionSecondWebsite, 'Simple Product on second website'); + } + + /** + * Check is collection match expected value + * + * @param Collection $collection + * @param string $itemName + * @return void + */ + private function assertCollection(Collection $collection, string $itemName): void + { + $this->assertCount(1, $collection, 'Collection size does not match expected value'); + $this->assertEquals($itemName, $collection->getFirstItem()->getName()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/StoreSwitcherTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/StoreSwitcherTest.php new file mode 100644 index 0000000000000..5aba76b37e74a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/Cart/StoreSwitcherTest.php @@ -0,0 +1,146 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Block\Adminhtml\Edit\Tab\Cart; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\View\Result\Page; +use Magento\Framework\View\Result\PageFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Helper\Xpath; +use PHPUnit\Framework\TestCase; + +/** + * Class checks store switcher appearance in the customer shopping cart block. + * + * @see \Magento\Customer\Block\Adminhtml\Edit\Tab\Cart + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class StoreSwitcherTest extends TestCase +{ + private const WEBSITE_FILTER_XPATH = "//select[@name='website_id' and @id='website_filter']"; + + private const WEBSITE_FILTER_OPTION_XPATH = "//select[@name='website_id' and @id='website_filter']/option"; + + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var PageFactory */ + private $pageFactory; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->pageFactory = $this->objectManager->get(PageFactory::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); + } + + /** + * @return void + */ + public function testStoreSwitcherDisplayed(): void + { + $html = $this->getBlockHtml('admin.customer.view.edit.cart'); + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_XPATH, $html), + 'Website Filter was not found on the page' + ); + $this->checkFilterOptions($html, [$this->storeManager->getWebsite('base')->getName()]); + } + + /** + * @magentoConfigFixture current_store general/single_store_mode/enabled 1 + * + * @return void + */ + public function testStoreSwitcherIsNotDisplayed(): void + { + $html = $this->getBlockHtml('admin.customer.view.edit.cart'); + $this->assertEmpty(Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_XPATH, $html)); + } + + /** + * @magentoDataFixture Magento/Store/_files/second_website_with_two_stores.php + * + * @return void + */ + public function testStoreSwitcherMultiWebsite(): void + { + $expectedWebsites = [ + $this->storeManager->getWebsite('base')->getName(), + $this->storeManager->getWebsite('test')->getName(), + ]; + $html = $this->getBlockHtml('admin.customer.view.edit.cart'); + $this->assertEquals(1, Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_XPATH, $html)); + $this->checkFilterOptions($html, $expectedWebsites); + } + + /** + * Check store switcher appearance + * + * @param string $html + * @param array $expectedOptions + * @return void + */ + private function checkFilterOptions(string $html, array $expectedOptions): void + { + $this->assertEquals( + count($expectedOptions), + Xpath::getElementsCountForXpath(self::WEBSITE_FILTER_OPTION_XPATH, $html), + 'Website filter options count does not match expected value' + ); + $optionPath = self::WEBSITE_FILTER_OPTION_XPATH . "[contains(text(), '%s')]"; + foreach ($expectedOptions as $option) { + $this->assertEquals( + 1, + Xpath::getElementsCountForXpath(sprintf($optionPath, $option), $html), + sprintf('Option for %s website was not found in filter options list', $option) + ); + } + } + + /** + * Get block html + * + * @param string $alias + * @return string + */ + private function getBlockHtml(string $alias): string + { + $page = $this->preparePage(); + $block = $page->getLayout()->getBlock($alias); + $this->assertNotFalse($block); + + return $block->toHtml(); + } + + /** + * Prepare page layout + * + * @return Page + */ + private function preparePage(): Page + { + $page = $this->pageFactory->create(); + $page->addHandle(['default', 'customer_index_cart']); + $page->getLayout()->generateXml(); + + return $page; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php index 11d51e1f2c814..d4ae576523b15 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Edit/Tab/View/WishlistTest.php @@ -18,7 +18,7 @@ * Tests for customer wish list tab. * * @magentoAppArea adminhtml - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled */ class WishlistTest extends TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php index f8ede749872f4..02d7c886ec2e2 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Adminhtml/Grid/Renderer/AbstractMultiactionTest.php @@ -99,12 +99,12 @@ protected function processRender(): void private function assertUrl(int $quoteItemId, array $action, string $html): void { $jsFunction = str_replace('url_', '', $action['url']); - $configureXPath = "//a[contains(@onclick, 'return cartControl.$jsFunction($quoteItemId)')" - . " and text()='{$action['caption']}' and @href='{$action['url']}']"; + $configureXPath = "//a[text()='{$action['caption']}' and @href='{$action['url']}']"; $this->assertEquals( 1, Xpath::getElementsCountForXpath($configureXPath, $html), sprintf('Expected %s link is incorrect or missing', $action['caption']) ); + $this->assertStringContainsString("return cartControl.$jsFunction($quoteItemId)", $html); } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php index e6d3c5aa39d15..1d06aa7201f64 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Block/Form/RegisterTest.php @@ -138,6 +138,22 @@ public function testFaxEnabled(): void $this->assertStringContainsString('title="Fax"', $block->toHtml()); } + /** + * @magentoDataFixture Magento/Customer/_files/attribute_city_store_label_address.php + */ + public function testCityWithStoreLabel(): void + { + /** @var \Magento\Customer\Block\Form\Register $block */ + $block = Bootstrap::getObjectManager()->create( + Register::class + )->setTemplate('Magento_Customer::form/register.phtml') + ->setShowAddressFields(true); + $this->setAttributeDataProvider($block); + + $this->assertStringNotContainsString('title="City"', $block->toHtml()); + $this->assertStringContainsString('title="Suburb"', $block->toHtml()); + } + /** * @inheritdoc */ diff --git a/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php b/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php index 664c7d9418401..1c9a41962997d 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Controller/Section/LoadTest.php @@ -16,7 +16,7 @@ /** * Load customer data test class. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend */ class LoadTest extends AbstractController diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php index e2b43fcbd2688..6f2cf2d76bd11 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AccountManagementTest.php @@ -13,6 +13,7 @@ use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\State\ExpiredException; use Magento\Framework\Reflection\DataObjectProcessor; +use Magento\Framework\Session\SessionManagerInterface; use Magento\Framework\Url as UrlBuilder; use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; @@ -155,8 +156,22 @@ public function testLoginWrongUsername() */ public function testChangePassword() { + /** @var SessionManagerInterface $session */ + $session = $this->objectManager->get(SessionManagerInterface::class); + $oldSessionId = $session->getSessionId(); + $session->setTestData('test'); $this->accountManagement->changePassword('customer@example.com', 'password', 'new_Password123'); + $this->assertTrue( + $oldSessionId !== $session->getSessionId(), + 'Customer session id wasn\'t regenerated after change password' + ); + + $session->destroy(); + $session->setSessionId($oldSessionId); + + $this->assertNull($session->getTestData(), 'Customer session data wasn\'t cleaned'); + $this->accountManagement->authenticate('customer@example.com', 'new_Password123'); } diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/AttributeTest.php new file mode 100644 index 0000000000000..f433324efcfa6 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/AttributeTest.php @@ -0,0 +1,97 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Customer\Model; + +use Magento\Eav\Api\AttributeRepositoryInterface; +use Magento\Eav\Api\Data\AttributeInterface; +use Magento\Eav\Model\Config; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * Test for \Magento\Customer\Model\Attribute. + */ +class AttributeTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Attribute + */ + private $model; + + /** + * @var AttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @var int|string + */ + private $customerEntityType; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(Attribute::class); + $this->attributeRepository = $this->objectManager->get(AttributeRepositoryInterface::class); + $this->customerEntityType = $this->objectManager->get(Config::class) + ->getEntityType('customer') + ->getId(); + } + + /** + * Test Create -> Read -> Update -> Delete attribute operations. + * + * @return void + */ + public function testCRUD(): void + { + $this->model->setAttributeCode('test') + ->setEntityTypeId($this->customerEntityType) + ->setFrontendLabel('test') + ->setIsUserDefined(1); + $crud = new \Magento\TestFramework\Entity($this->model, [AttributeInterface::FRONTEND_LABEL => uniqid()]); + $crud->testCrud(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_customer.php + * + * @return void + */ + public function testAttributeSaveWithChangedEntityType(): void + { + $this->expectException( + \Magento\Framework\Exception\LocalizedException::class + ); + $this->expectExceptionMessage('Do not change entity type.'); + + $attribute = $this->attributeRepository->get($this->customerEntityType, 'user_attribute'); + $attribute->setEntityTypeId(5); + $attribute->save(); + } + + /** + * @magentoDataFixture Magento/Customer/_files/attribute_user_defined_customer.php + * + * @return void + */ + public function testAttributeSaveWithoutChangedEntityType(): void + { + $attribute = $this->attributeRepository->get($this->customerEntityType, 'user_attribute'); + $attribute->setSortOrder(1250); + $attribute->save(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php index e63c3d2761c49..69afd17c674a6 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/EmailNotificationTest.php @@ -141,6 +141,36 @@ public function testRemindPasswordCustomTemplate(): void $this->assertMessage($expectedSender); } + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testChangeEmailCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_CHANGE_EMAIL_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $customer->setEmail('customer_update@example.com'); + $this->emailNotification->credentialsChanged($customer, 'customer@example.com'); + $expectedSender = ['name' => 'CustomerSupport', 'email' => 'support@example.com']; + $this->assertMessage($expectedSender); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testChangeEmailAndPasswordCustomTemplate(): void + { + $this->setEmailTemplateConfig(EmailNotification::XML_PATH_CHANGE_EMAIL_AND_PASSWORD_TEMPLATE); + $customer = $this->customerRepository->get('customer@example.com'); + $customer->setEmail('customer_update@example.com'); + $this->emailNotification->credentialsChanged($customer, 'customer@example.com', true); + $expectedSender = ['name' => 'CustomerSupport', 'email' => 'support@example.com']; + $this->assertMessage($expectedSender); + } + /** * Assert message. * diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/Metadata/Form/ImageTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/Metadata/Form/ImageTest.php new file mode 100644 index 0000000000000..48e45126d124b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/Metadata/Form/ImageTest.php @@ -0,0 +1,217 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Customer\Model\Metadata\Form; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Filesystem; +use Magento\Framework\Filesystem\Directory\WriteInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +class ImageTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var Filesystem + */ + private $filesystem; + + /** + * @var string + */ + private $fileName = 'magento.jpg'; + + /** + * @var string + */ + private $invalidFileName = '../../invalidFile.xyz'; + + /** + * @var string + */ + private $imageFixtureDir; + + /** + * @var string + */ + private $expectedFileName; + + /** + * @var WriteInterface + */ + private $mediaDirectory; + + /** + * @inheritDoc + */ + public function setUp(): void + { + $this->objectManager = Bootstrap::getObjectManager(); + $this->filesystem = $this->objectManager->get(Filesystem::class); + $this->mediaDirectory = $this->filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $this->imageFixtureDir = realpath(__DIR__ . '/../../../_files/image'); + $this->expectedFileName = '/m/a/' . $this->fileName; + } + + /** + * Test for processCustomerAddressValue method + * + * @magentoAppIsolation enabled + * @throws FileSystemException + * @throws \ReflectionException + */ + public function testProcessCustomerAddressValue() + { + $this->mediaDirectory->delete('customer_address'); + $this->mediaDirectory->create($this->mediaDirectory->getRelativePath('customer_address/tmp/')); + $tmpFilePath = $this->mediaDirectory->getAbsolutePath('customer_address/tmp/' . $this->fileName); + copy($this->imageFixtureDir . DIRECTORY_SEPARATOR . $this->fileName, $tmpFilePath); + + $imageFile = [ + 'name' => $this->fileName, + 'type' => 'image/jpeg', + 'tmp_name' => $this->fileName, + 'file' => $this->fileName, + 'error' => 0, + 'size' => 12500, + 'previewType' => 'image', + ]; + + $params = [ + 'entityTypeCode' => 'customer_address', + 'formCode' => 'customer_address_edit', + 'isAjax' => false, + 'value' => $imageFile + ]; + + $expectedPath = $this->mediaDirectory->getAbsolutePath('customer_address' . $this->expectedFileName); + + /** @var Image $image */ + $image = $this->objectManager->create(\Magento\Customer\Model\Metadata\Form\Image::class, $params); + $processCustomerAddressValueMethod = new \ReflectionMethod( + \Magento\Customer\Model\Metadata\Form\Image::class, + 'processCustomerAddressValue' + ); + $processCustomerAddressValueMethod->setAccessible(true); + $actual = $processCustomerAddressValueMethod->invoke($image, $imageFile); + $this->assertEquals($this->expectedFileName, $actual); + $this->assertFileExists($expectedPath); + $this->assertFileNotExists($tmpFilePath); + } + + /** + * Test for processCustomerValue method + * + * @magentoAppIsolation enabled + * @throws FileSystemException + * @throws \ReflectionException + */ + public function testProcessCustomerValue() + { + $this->mediaDirectory->delete('customer'); + $this->mediaDirectory->create($this->mediaDirectory->getRelativePath('customer/tmp/')); + $tmpFilePath = $this->mediaDirectory->getAbsolutePath('customer/tmp/' . $this->fileName); + copy($this->imageFixtureDir . DIRECTORY_SEPARATOR . $this->fileName, $tmpFilePath); + + $imageFile = [ + 'name' => $this->fileName, + 'type' => 'image/jpeg', + 'tmp_name' => $this->fileName, + 'file' => $this->fileName, + 'error' => 0, + 'size' => 12500, + 'previewType' => 'image', + ]; + + $params = [ + 'entityTypeCode' => 'customer', + 'formCode' => 'customer_edit', + 'isAjax' => false, + 'value' => $imageFile + ]; + + /** @var Image $image */ + $image = $this->objectManager->create(\Magento\Customer\Model\Metadata\Form\Image::class, $params); + $processCustomerAddressValueMethod = new \ReflectionMethod( + \Magento\Customer\Model\Metadata\Form\Image::class, + 'processCustomerValue' + ); + $processCustomerAddressValueMethod->setAccessible(true); + $result = $processCustomerAddressValueMethod->invoke($image, $imageFile); + $this->assertInstanceOf('Magento\Framework\Api\ImageContent', $result); + $this->assertFileNotExists($tmpFilePath); + } + + /** + * Test for processCustomerValue method with invalid value + * + * @magentoAppIsolation enabled + * + * @throws FileSystemException + * @throws \ReflectionException + */ + public function testProcessCustomerInvalidValue() + { + $this->expectException( + \Magento\Framework\Exception\ValidatorException::class + ); + + $this->mediaDirectory->delete('customer'); + $this->mediaDirectory->create($this->mediaDirectory->getRelativePath('customer/tmp/')); + $tmpFilePath = $this->mediaDirectory->getAbsolutePath('customer/tmp/' . $this->fileName); + copy($this->imageFixtureDir . DIRECTORY_SEPARATOR . $this->fileName, $tmpFilePath); + + $imageFile = [ + 'name' => $this->fileName, + 'type' => 'image/jpeg', + 'tmp_name' => $this->fileName, + 'file' => $this->invalidFileName, + 'error' => 0, + 'size' => 12500, + 'previewType' => 'image', + ]; + + $params = [ + 'entityTypeCode' => 'customer', + 'formCode' => 'customer_edit', + 'isAjax' => false, + 'value' => $imageFile + ]; + + /** @var Image $image */ + $image = $this->objectManager->create(\Magento\Customer\Model\Metadata\Form\Image::class, $params); + $processCustomerAddressValueMethod = new \ReflectionMethod( + \Magento\Customer\Model\Metadata\Form\Image::class, + 'processCustomerValue' + ); + $processCustomerAddressValueMethod->setAccessible(true); + $processCustomerAddressValueMethod->invoke($image, $imageFile); + } + + /** + * @inheritdoc + * @throws FileSystemException + */ + public static function tearDownAfterClass(): void + { + parent::tearDownAfterClass(); + $filesystem = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( + Filesystem::class + ); + /** @var WriteInterface $mediaDirectory */ + $mediaDirectory = $filesystem->getDirectoryWrite(DirectoryList::MEDIA); + $mediaDirectory->delete('customer'); + $mediaDirectory->delete('customer_address'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php index 00b5d2bc6f279..8651db95ae645 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Customer/Model/ResourceModel/CustomerRepositoryTest.php @@ -19,6 +19,11 @@ use Magento\Framework\Api\SortOrder; use Magento\Framework\Config\CacheInterface; use Magento\Framework\ObjectManagerInterface; +use Magento\Sales\Api\Data\InvoiceInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\InvoiceOrderInterface; +use Magento\Sales\Api\InvoiceRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\TestFramework\Helper\Bootstrap; use Magento\Customer\Api\Data\AddressInterface; use Magento\Framework\Api\SearchCriteriaBuilder; @@ -34,12 +39,18 @@ */ class CustomerRepositoryTest extends \PHPUnit\Framework\TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const CUSTOMER_ID = 1; + /** @var AccountManagementInterface */ private $accountManagement; /** @var CustomerRepositoryInterface */ private $customerRepository; + /** @var OrderRepositoryInterface */ + private $orderRepository; + /** @var ObjectManagerInterface */ private $objectManager; @@ -71,6 +82,7 @@ protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); $this->customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $this->orderRepository = $this->objectManager->create(OrderRepositoryInterface::class); $this->customerFactory = $this->objectManager->create(CustomerInterfaceFactory::class); $this->addressFactory = $this->objectManager->create(AddressInterfaceFactory::class); $this->regionFactory = $this->objectManager->create(RegionInterfaceFactory::class); @@ -625,4 +637,55 @@ public function testUpdateDefaultShippingAndDefaultBillingTest() 'Default shipping should not be overridden' ); } + + /** + * Test that UpgradeOrderCustomerEmailObserver is executed + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoDbIsolation enabled + */ + public function testUpgradeOrderCustomerEmailObserverWhenEmailIsModified() + { + $customer = $this->customerRepository->getById(self::CUSTOMER_ID); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + + $this->customerRepository->save($customer); + + /** @var SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + $customerOrders = $this->orderRepository->getList($searchCriteria); + + foreach ($customerOrders as $customerOrder) { + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $customerOrder->getCustomerEmail()); + } + } + + /** + * Test that UpgradeOrderCustomerEmailObserver is executed but does not update orders + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoDbIsolation enabled + */ + public function testUpgradeOrderCustomerEmailObserverWhenEmailIsNotModified(): void + { + $customer = $this->customerRepository->getById(self::CUSTOMER_ID); + + $this->customerRepository->save($customer); + + /** @var SearchCriteriaBuilder $searchBuilder */ + $searchBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); + $searchCriteria = $searchBuilder + ->addFilter(OrderInterface::CUSTOMER_ID, $customer->getId()) + ->create(); + + $customerOrders = $this->orderRepository->getList($searchCriteria); + + foreach ($customerOrders as $customerOrder) { + $this->assertEquals('customer@null.com', $customerOrder->getCustomerEmail()); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php new file mode 100644 index 0000000000000..8a4afc23aaea8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/attribute_city_store_label_address.php @@ -0,0 +1,19 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +//@codingStandardsIgnoreFile +/** @var \Magento\Customer\Model\Attribute $model */ +$model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Attribute::class); +/** @var \Magento\Store\Model\StoreManagerInterface $storeManager */ +$storeManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Store\Model\StoreManager::class); +$model->loadByCode('customer_address', 'city'); +$storeLabels = $model->getStoreLabels(); +$stores = $storeManager->getStores(); +/** @var \Magento\Store\Api\Data\WebsiteInterface $website */ +foreach ($stores as $store) { + $storeLabels[$store->getId()] = 'Suburb'; +} +$model->setStoreLabels($storeLabels); +$model->save(); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_uk_address.php b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_uk_address.php index a7ad0bb82719f..c024d18e40942 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_uk_address.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/customer_with_uk_address.php @@ -11,9 +11,10 @@ use Magento\Customer\Api\Data\AddressInterface; use Magento\Customer\Model\Address; use Magento\Customer\Model\AddressFactory; +use Magento\Customer\Model\AddressRegistry; use Magento\Customer\Model\CustomerFactory; use Magento\Customer\Model\CustomerRegistry; -use Magento\Customer\Model\AddressRegistry; +use Magento\Framework\Encryption\EncryptorInterface; use Magento\Store\Api\WebsiteRepositoryInterface; use Magento\Store\Model\Website; use Magento\Store\Model\WebsiteRepository; @@ -31,6 +32,9 @@ $websiteRepository = $objectManager->create(WebsiteRepositoryInterface::class); /** @var Website $mainWebsite */ $mainWebsite = $websiteRepository->get('base'); +/** @var EncryptorInterface $encryptor */ +$encryptor = $objectManager->get(EncryptorInterface::class); + $customer->setWebsiteId($mainWebsite->getId()) ->setEmail('customer_uk_address@test.com') ->setPassword('password') @@ -67,7 +71,7 @@ ); $customer->addAddress($customerAddress); $customer->isObjectNew(true); -$customerDataModel = $customerRepository->save($customer->getDataModel()); +$customerDataModel = $customerRepository->save($customer->getDataModel(), $encryptor->hash('password')); $addressId = $customerDataModel->getAddresses()[0]->getId(); $customerDataModel->setDefaultShipping($addressId); $customerDataModel->setDefaultBilling($addressId); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/image/magento.jpg b/dev/tests/integration/testsuite/Magento/Customer/_files/image/magento.jpg new file mode 100644 index 0000000000000..5704eccd795de Binary files /dev/null and b/dev/tests/integration/testsuite/Magento/Customer/_files/image/magento.jpg differ diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php index 3a39e62af0ccb..9c24e4b5ff3bd 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer.php @@ -3,39 +3,41 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//Create customer -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'CharlesTAlston@teleworm.us' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Charles' -)->setLastname( - 'Alston' -)->setGender( - '2' -); + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ +$customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('CharlesTAlston@teleworm.us') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Charles') + ->setLastname('Alston') + ->setGender('2'); + $customer->isObjectNew(true); // Create address -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); // default_billing and default_shipping information would not be saved, it is needed only for simple check $address->addData( [ @@ -54,14 +56,12 @@ // Assign customer and address $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); // Mark last address as default billing and default shipping for current customer $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); -/** @var $objectManager \Magento\TestFramework\ObjectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$objectManager->get(\Magento\Framework\Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); -$objectManager->get(\Magento\Framework\Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer', $customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php index b8a69def69d6b..46086e00244ee 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customer_with_addresses.php @@ -3,41 +3,44 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\ObjectManager; + +/** @var $objectManager ObjectManager */ +$objectManager = Bootstrap::getObjectManager(); + $customers = []; -//Create customer -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'BetsyParker@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Betsy' -)->setLastname( - 'Parker' -)->setGender( - 2 -); +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ +$customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(1) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('BetsyParker@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Betsy') + ->setLastname('Parker') + ->setGender(2); $customer->isObjectNew(true); // Create address -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); // default_billing and default_shipping information would not be saved, it is needed only for simple check $address->addData( [ @@ -56,46 +59,31 @@ // Assign customer and address $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); // Mark last address as default billing and default shipping for current customer $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 2 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'AnthonyNealy@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Anthony' -)->setLastname( - 'Nealy' -)->setGender( - 1 -); +$customer = $objectManager->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(2) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('AnthonyNealy@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Anthony') + ->setLastname('Nealy') + ->setGender(1); $customer->isObjectNew(true); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Anthony', @@ -112,7 +100,7 @@ ); $customer->addAddress($address); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Anthony', @@ -129,45 +117,30 @@ ); $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 1 -)->setEntityId( - 3 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'LoriBanks@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Lori' -)->setLastname( - 'Banks' -)->setGender( - 2 -); +$customer = $objectManager->create(Customer::class); +$customer->setWebsiteId(1) + ->setEntityId(3) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('LoriBanks@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Lori') + ->setLastname('Banks') + ->setGender(2); $customer->isObjectNew(true); -$address = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Customer\Model\Address::class); +$address = $objectManager->create(Address::class); $address->addData( [ 'firstname' => 'Lori', @@ -183,17 +156,13 @@ ] ); $customer->addAddress($address); -$customer->save(); +$customerResource->save($customer); $customer->setDefaultBilling($address->getId()); $customer->setDefaultShipping($address->getId()); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -/** @var $objectManager \Magento\TestFramework\ObjectManager */ -$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); -$objectManager->get(\Magento\Framework\Registry::class) - ->unregister('_fixture/Magento_ImportExport_Customers_Array'); -$objectManager->get(\Magento\Framework\Registry::class) - ->register('_fixture/Magento_ImportExport_Customers_Array', $customers); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customers_Array'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customers_Array', $customers); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php index 9b989779e4cbd..302ac055f61ca 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers.php @@ -4,107 +4,75 @@ * See COPYING.txt for license details. */ -use Magento\TestFramework\Helper\Bootstrap; -use Magento\Framework\ObjectManagerInterface; +declare(strict_types=1); + use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; /** @var $objectManager ObjectManagerInterface */ $objectManager = Bootstrap::getObjectManager(); $customers = []; + +/** + * @var $customer Customer + * @var $customerResource CustomerResource + */ $customer = $objectManager->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'customer@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Firstname' -)->setLastname( - 'Lastname' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('customer@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Firstname') + ->setLastname('Lastname') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; $customer = $objectManager->create(Customer::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'julie.worrell@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'Julie' -)->setLastname( - 'Worrell' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('julie.worrell@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('Julie') + ->setLastname('Worrell') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; $customer = $objectManager->create(Customer::class); -$customer->setWebsiteId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'david.lamar@example.com' -)->setPassword( - 'password' -)->setGroupId( - 1 -)->setStoreId( - 1 -)->setIsActive( - 1 -)->setFirstname( - 'David' -)->setLastname( - 'Lamar' -)->setDefaultBilling( - 1 -)->setDefaultShipping( - 1 -); +$customer->setWebsiteId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('david.lamar@example.com') + ->setPassword('password') + ->setGroupId(1) + ->setStoreId(1) + ->setIsActive(1) + ->setFirstname('David') + ->setLastname('Lamar') + ->setDefaultBilling(1) + ->setDefaultShipping(1); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); $customers[] = $customer; -$objectManager->get(Registry::class) - ->unregister('_fixture/Magento_ImportExport_Customer_Collection'); -$objectManager->get(Registry::class) - ->register('_fixture/Magento_ImportExport_Customer_Collection', $customers); +$objectManager->get(Registry::class)->unregister('_fixture/Magento_ImportExport_Customer_Collection'); +$objectManager->get(Registry::class)->register('_fixture/Magento_ImportExport_Customer_Collection', $customers); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php index 9a90061a6de76..ca32958e66639 100644 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php +++ b/dev/tests/integration/testsuite/Magento/Customer/_files/import_export/customers_for_address_import.php @@ -3,43 +3,39 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -//Create customer -/** @var Magento\Customer\Model\Customer $customer */ -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -); -$customer->setWebsiteId( - 0 -)->setEntityId( - 1 -)->setEntityTypeId( - 1 -)->setAttributeSetId( - 0 -)->setEmail( - 'BetsyParker@example.com' -)->setPassword( - 'password' -)->setGroupId( - 0 -)->setStoreId( - 0 -)->setIsActive( - 1 -)->setFirstname( - 'Betsy' -)->setLastname( - 'Parker' -)->setGender( - 2 -); + +declare(strict_types=1); + +use Magento\Customer\Model\Address; +use Magento\Customer\Model\Customer; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); + +/** + * @var Customer $customer + * @var CustomerResource $customerResource + */ +$customer = Bootstrap::getObjectManager()->create(Customer::class); +$customerResource = $objectManager->create(CustomerResource::class); + +$customer->setWebsiteId(0) + ->setEntityId(1) + ->setEntityTypeId(1) + ->setAttributeSetId(0) + ->setEmail('BetsyParker@example.com') + ->setPassword('password') + ->setGroupId(0) + ->setStoreId(0) + ->setIsActive(1) + ->setFirstname('Betsy') + ->setLastname('Parker') + ->setGender(2); $customer->isObjectNew(true); -$customer->save(); +$customerResource->save($customer); -// Create and set addresses -$addressFirst = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Address::class -); +$addressFirst = $objectManager->create(Address::class); $addressFirst->addData( [ 'entity_id' => 1, @@ -57,9 +53,7 @@ $customer->addAddress($addressFirst); $customer->setDefaultBilling($addressFirst->getId()); -$addressSecond = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Address::class -); +$addressSecond = $objectManager->create(Address::class); $addressSecond->addData( [ 'entity_id' => 2, @@ -76,4 +70,4 @@ $addressSecond->isObjectNew(true); $customer->addAddress($addressSecond); $customer->setDefaultShipping($addressSecond->getId()); -$customer->save(); +$customerResource->save($customer); diff --git a/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php b/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php deleted file mode 100644 index 2ea0e58fddaba..0000000000000 --- a/dev/tests/integration/testsuite/Magento/Customer/_files/sales_order.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ - -/** @var \Magento\Customer\Model\Customer $customer */ -$customer = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Customer::class -)->load( - 1 -); - -/** @var \Magento\Sales\Model\Order $order */ -$order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order::class -)->loadByIncrementId( - '100000001' -); -$order->setCustomerIsGuest(false)->setCustomerId($customer->getId())->setCustomerEmail($customer->getEmail()); -$order->save(); diff --git a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php index 832aabe6b6a78..0a5e6cdfe21bd 100644 --- a/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php +++ b/dev/tests/integration/testsuite/Magento/CustomerImportExport/Model/Import/AddressTest.php @@ -9,30 +9,41 @@ */ namespace Magento\CustomerImportExport\Model\Import; +use Magento\Catalog\Model\ResourceModel\Product; use Magento\Customer\Api\AddressRepositoryInterface; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Customer\Model\AddressRegistry; +use Magento\Customer\Model\Indexer\Processor; +use Magento\Customer\Model\ResourceModel\Address\Collection; use Magento\Framework\Api\SearchCriteriaBuilder; use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\Framework\Registry; +use Magento\Framework\Stdlib\DateTime; use Magento\ImportExport\Model\Import as ImportModel; use Magento\ImportExport\Model\Import\Adapter as ImportAdapter; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; use Magento\ImportExport\Model\Import\Source\Csv; +use Magento\ImportExport\Model\ResourceModel\Helper; use Magento\TestFramework\Helper\Bootstrap; use Magento\Framework\Indexer\StateInterface; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; use ReflectionClass; /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class AddressTest extends \PHPUnit\Framework\TestCase +class AddressTest extends TestCase { /** * Tested class name * * @var string */ - protected $_testClassName = \Magento\CustomerImportExport\Model\Import\Address::class; + protected $_testClassName = Address::class; /** * Fixture key from fixture @@ -92,7 +103,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase protected $customerResource; /** - * @var \Magento\Customer\Model\Indexer\Processor + * @var Processor */ private $indexerProcessor; @@ -101,7 +112,7 @@ class AddressTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - /** @var \Magento\Catalog\Model\ResourceModel\Product $productResource */ + /** @var Product $productResource */ $this->customerResource = Bootstrap::getObjectManager()->get( \Magento\Customer\Model\ResourceModel\Customer::class ); @@ -109,7 +120,7 @@ protected function setUp(): void $this->_testClassName ); $this->indexerProcessor = Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\Indexer\Processor::class + Processor::class ); } @@ -140,10 +151,10 @@ public function testSaveAddressEntities() */ protected function _addTestAddress(Address $entityAdapter) { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); - $customers = $objectManager->get(\Magento\Framework\Registry::class)->registry($this->_fixtureKey); + $customers = $objectManager->get(Registry::class)->registry($this->_fixtureKey); /** @var $customer \Magento\Customer\Model\Customer */ $customer = reset($customers); $customerId = $customer->getId(); @@ -153,14 +164,14 @@ protected function _addTestAddress(Address $entityAdapter) \Magento\Customer\Model\Address::class ); $tableName = $addressModel->getResource()->getEntityTable(); - $addressId = $objectManager->get(\Magento\ImportExport\Model\ResourceModel\Helper::class) + $addressId = $objectManager->get(Helper::class) ->getNextAutoincrement($tableName); $newEntityData = [ 'entity_id' => $addressId, 'parent_id' => $customerId, - 'created_at' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT), - 'updated_at' => (new \DateTime())->format(\Magento\Framework\Stdlib\DateTime::DATETIME_PHP_FORMAT), + 'created_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), + 'updated_at' => (new \DateTime())->format(DateTime::DATETIME_PHP_FORMAT), ]; // invoke _saveAddressEntities @@ -223,11 +234,11 @@ public function testSaveAddressAttributes() */ public function testSaveCustomerDefaults() { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); // get not default address - $customers = $objectManager->get(\Magento\Framework\Registry::class)->registry($this->_fixtureKey); + $customers = $objectManager->get(Registry::class)->registry($this->_fixtureKey); /** @var $notDefaultAddress \Magento\Customer\Model\Address */ $notDefaultAddress = null; /** @var $addressCustomer \Magento\Customer\Model\Customer */ @@ -300,7 +311,7 @@ public function testImportDataAddUpdate() // set fixture CSV file $sourceFile = __DIR__ . '/_files/address_import_update.csv'; - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); $filesystem = $objectManager->create(Filesystem::class); @@ -328,7 +339,7 @@ public function testImportDataAddUpdate() // get addresses $addressCollection = Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\ResourceModel\Address\Collection::class + Collection::class ); $addressCollection->addAttributeToSelect($requiredAttributes); $addresses = []; @@ -399,7 +410,7 @@ public function testImportDataDelete() // set fixture CSV file $sourceFile = __DIR__ . '/_files/address_import_delete.csv'; - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); $filesystem = $objectManager->create(Filesystem::class); $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); @@ -415,9 +426,9 @@ public function testImportDataDelete() $keyAttribute = 'postcode'; // get addresses - /** @var $addressCollection \Magento\Customer\Model\ResourceModel\Address\Collection */ + /** @var $addressCollection Collection */ $addressCollection = Bootstrap::getObjectManager()->create( - \Magento\Customer\Model\ResourceModel\Address\Collection::class + Collection::class ); $addressCollection->addAttributeToSelect($keyAttribute); $addresses = []; @@ -442,7 +453,7 @@ public function testImportDataDelete() */ public function testDifferentOptions(): void { - /** @var $objectManager \Magento\TestFramework\ObjectManager */ + /** @var $objectManager ObjectManager */ $objectManager = Bootstrap::getObjectManager(); /** @var Filesystem $filesystem */ $filesystem = $objectManager->create(Filesystem::class); @@ -472,9 +483,10 @@ public function testDifferentOptions(): void public function testCustomerIndexer(): void { $file = __DIR__ . '/_files/address_import_update.csv'; - $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); + $filesystem = Bootstrap::getObjectManager() + ->create(Filesystem::class); $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = new \Magento\ImportExport\Model\Import\Source\Csv($file, $directoryWrite); + $source = new Csv($file, $directoryWrite); $this->_entityAdapter ->setParameters(['behavior' => ImportModel::BEHAVIOR_ADD_UPDATE]) ->setSource($source) @@ -492,23 +504,15 @@ public function testCustomerIndexer(): void /** * Test import address with region for a country that does not have regions defined * + * @magentoAppIsolation enabled * @magentoDataFixture Magento/Customer/_files/import_export/customer_with_addresses.php */ public function testImportAddressWithOptionalRegion() { - $objectManager = Bootstrap::getObjectManager(); - $customerRepository = $objectManager->get(CustomerRepositoryInterface::class); - $customer = $customerRepository->get('BetsyParker@example.com'); + $customer = $this->getCustomer('BetsyParker@example.com'); $file = __DIR__ . '/_files/import_uk_address.csv'; - $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); - $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); - $source = new Csv($file, $directoryWrite); - $errors = $this->_entityAdapter - ->setParameters(['behavior' => ImportModel::BEHAVIOR_ADD_UPDATE]) - ->setSource($source) - ->validateData(); - $this->assertEmpty($errors->getAllErrors(), 'Import validation failed'); - $this->_entityAdapter->importData(); + $errors = $this->doImport($file); + $this->assertImportValidationPassed($errors); $address = $this->getAddresses( [ 'parent_id' => $customer->getId(), @@ -520,6 +524,55 @@ public function testImportAddressWithOptionalRegion() $this->assertEquals('Liverpool', $address[0]->getRegion()->getRegion()); } + /** + * Test update first name and last name + * + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/Customer/_files/import_export/customer_with_addresses.php + */ + public function testUpdateFirstAndLastName() + { + $customer = $this->getCustomer('BetsyParker@example.com'); + $addresses = $this->getAddresses( + [ + 'parent_id' => $customer->getId(), + ] + ); + $this->assertCount(1, $addresses); + $address = $addresses[0]; + $row = [ + '_website' => 'base', + '_email' => $customer->getEmail(), + '_entity_id' => $address->getId(), + 'firstname' => 'Mark', + 'lastname' => 'Antony', + ]; + $file = $this->generateImportFile([$row]); + $errors = $this->doImport($file); + $this->assertImportValidationPassed($errors); + $objectManager = Bootstrap::getObjectManager(); + //clear cache + $objectManager->get(AddressRegistry::class)->remove($address->getId()); + $addresses = $this->getAddresses( + [ + 'parent_id' => $customer->getId(), + 'entity_id' => $address->getId(), + ] + ); + $this->assertCount(1, $addresses); + $updatedAddress = $addresses[0]; + //assert that firstname and lastname were updated + $this->assertEquals($row['firstname'], $updatedAddress->getFirstname()); + $this->assertEquals($row['lastname'], $updatedAddress->getLastname()); + //assert other values have not changed + $this->assertEquals($address->getStreet(), $updatedAddress->getStreet()); + $this->assertEquals($address->getCity(), $updatedAddress->getCity()); + $this->assertEquals($address->getCountryId(), $updatedAddress->getCountryId()); + $this->assertEquals($address->getPostcode(), $updatedAddress->getPostcode()); + $this->assertEquals($address->getTelephone(), $updatedAddress->getTelephone()); + $this->assertEquals($address->getRegionId(), $updatedAddress->getRegionId()); + } + /** * Get Addresses by filter * @@ -538,4 +591,121 @@ private function getAddresses(array $filter): array } return $repository->getList($searchCriteriaBuilder->create())->getItems(); } + + /** + * @param string $email + * @return CustomerInterface + */ + private function getCustomer(string $email): CustomerInterface + { + $objectManager = Bootstrap::getObjectManager(); + $customerRepository = $objectManager->get(CustomerRepositoryInterface::class); + return $customerRepository->get($email); + } + + /** + * @param string $file + * @param string $behavior + * @param bool $validateOnly + * @return ProcessingErrorAggregatorInterface + */ + private function doImport( + string $file, + string $behavior = ImportModel::BEHAVIOR_ADD_UPDATE, + bool $validateOnly = false + ): ProcessingErrorAggregatorInterface { + $objectManager = Bootstrap::getObjectManager(); + /** @var Filesystem $filesystem */ + $filesystem = $objectManager->create(Filesystem::class); + $directoryWrite = $filesystem->getDirectoryWrite(DirectoryList::ROOT); + $source = ImportAdapter::findAdapterFor($file, $directoryWrite); + $errors = $this->_entityAdapter + ->setParameters(['behavior' => $behavior]) + ->setSource($source) + ->validateData(); + if (!$validateOnly && !$errors->getAllErrors()) { + $this->_entityAdapter->importData(); + } + return $errors; + } + + /** + * @param array $data + * @return string + */ + private function generateImportFile(array $data): string + { + $objectManager = Bootstrap::getObjectManager(); + /** @var Filesystem $filesystem */ + $filesystem = $objectManager->get(Filesystem::class); + $tmpDir = $filesystem->getDirectoryWrite(DirectoryList::TMP); + $tmpFilename = uniqid('test_import_address_') . '.csv'; + $stream = $tmpDir->openFile($tmpFilename, 'w+'); + $stream->lock(); + $stream->writeCsv($this->getFields()); + $emptyRow = array_fill_keys($this->getFields(), ''); + foreach ($data as $row) { + $row = array_replace($emptyRow, $row); + $stream->writeCsv($row); + } + $stream->unlock(); + $stream->close(); + return $tmpDir->getAbsolutePath($tmpFilename); + } + + /** + * @param ProcessingErrorAggregatorInterface $errors + */ + private function assertImportValidationPassed(ProcessingErrorAggregatorInterface $errors): void + { + if ($errors->getAllErrors()) { + $messages = []; + $messages[] = 'Import validation failed'; + $messages[] = ''; + foreach ($errors->getAllErrors() as $error) { + $messages[] = sprintf( + '%s: #%d [%s] %s: %s', + strtoupper($error->getErrorLevel()), + $error->getRowNumber(), + $error->getErrorCode(), + $error->getErrorMessage(), + $error->getErrorDescription() + ); + } + $this->fail(implode("\n", $messages)); + } + } + + /** + * @return array + */ + private function getFields(): array + { + return [ + '_website', + '_email', + '_entity_id', + 'city', + 'company', + 'country_id', + 'fax', + 'firstname', + 'lastname', + 'middlename', + 'postcode', + 'prefix', + 'region', + 'region_id', + 'street', + 'suffix', + 'telephone', + 'vat_id', + 'vat_is_valid', + 'vat_request_date', + 'vat_request_id', + 'vat_request_success', + '_address_default_billing_', + '_address_default_shipping_', + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php index e312d973aeb17..22e95a329361d 100644 --- a/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php +++ b/dev/tests/integration/testsuite/Magento/Downloadable/_files/downloadable_product_with_files_and_sample_url.php @@ -169,4 +169,4 @@ /** @var \Magento\Catalog\Api\ProductRepositoryInterface $productRepository */ $productRepository = $objectManager->get(\Magento\Catalog\Api\ProductRepositoryInterface::class); -$productRepository->save($product)->getData(); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php index a5c88fc7571a2..2bff6f5ce82f6 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Adapter/BatchDataMapper/ProductDataMapperTest.php @@ -54,7 +54,7 @@ protected function setUp(): void $this->model = $this->objectManager->create( ProductDataMapper::class, [ - 'additionalFieldsProvider' => $additionalFieldsProvider + 'additionalFieldsProvider' => $additionalFieldsProvider, ] ); $this->eavConfig = $this->objectManager->get(Config::class); @@ -83,24 +83,24 @@ public function testMapSelectAttributeWithDifferentStoreLabels(): void $defaultStoreMap = [ $productId => [ 'store_id' => $defaultStore->getId(), - 'select_attribute' => $attributeValue, + 'select_attribute' => (int)$attributeValue, 'select_attribute_value' => 'Table_default', - ] + ], ]; $secondStoreMap = [ $productId => [ 'store_id' => $secondStore->getId(), - 'select_attribute' => $attributeValue, + 'select_attribute' => (int)$attributeValue, 'select_attribute_value' => 'Table_fixture_second_store', - ] + ], ]; $data = [ $productId => [ - $attributeId => $attributeValue - ] + $attributeId => $attributeValue, + ], ]; - $this->assertEquals($defaultStoreMap, $this->model->map($data, $defaultStore->getId(), [])); - $this->assertEquals($secondStoreMap, $this->model->map($data, $secondStore->getId(), [])); + $this->assertSame($defaultStoreMap, $this->model->map($data, $defaultStore->getId(), [])); + $this->assertSame($secondStoreMap, $this->model->map($data, $secondStore->getId(), [])); } /** diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php new file mode 100644 index 0000000000000..c1fe6f11f6e6e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch/Model/Indexer/Fulltext/Plugin/Category/Product/AttributeTest.php @@ -0,0 +1,195 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Elasticsearch\Model\Indexer\Fulltext\Plugin\Category\Product; + +use Magento\AdvancedSearch\Model\Client\ClientInterface; +use Magento\Catalog\Api\Data\EavAttributeInterface; +use Magento\Catalog\Api\Data\ProductAttributeInterface; +use Magento\Catalog\Api\ProductAttributeRepositoryInterface; +use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory; +use Magento\Catalog\Setup\CategorySetup; +use Magento\CatalogSearch\Model\Indexer\Fulltext\Processor; +use Magento\Elasticsearch\Model\Adapter\Index\IndexNameResolver; +use Magento\Elasticsearch\SearchAdapter\ConnectionManager; +use Magento\Framework\Stdlib\ArrayManager; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Check Elasticsearch indexer mapping when working with attributes. + */ +class AttributeTest extends TestCase +{ + /** + * @var ClientInterface + */ + private $client; + + /** + * @var ArrayManager + */ + private $arrayManager; + + /** + * @var IndexNameResolver + */ + private $indexNameResolver; + + /** + * @var Processor + */ + private $indexerProcessor; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @var CategorySetup + */ + private $installer; + + /** + * @var AttributeFactory + */ + private $attributeFactory; + + /** + * @var ProductAttributeRepositoryInterface + */ + private $attributeRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $connectionManager = Bootstrap::getObjectManager()->get(ConnectionManager::class); + $this->client = $connectionManager->getConnection(); + $this->arrayManager = Bootstrap::getObjectManager()->get(ArrayManager::class); + $this->indexNameResolver = Bootstrap::getObjectManager()->get(IndexNameResolver::class); + $this->indexerProcessor = Bootstrap::getObjectManager()->get(Processor::class); + $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + $this->installer = Bootstrap::getObjectManager()->get(CategorySetup::class); + $this->attributeFactory = Bootstrap::getObjectManager()->get(AttributeFactory::class); + $this->attributeRepository = Bootstrap::getObjectManager()->get(ProductAttributeRepositoryInterface::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + parent::tearDown(); + + /** @var ProductAttributeInterface $attribute */ + $attribute = $this->attributeRepository->get('dropdown_attribute'); + $this->attributeRepository->delete($attribute); + } + + /** + * Check Elasticsearch indexer mapping is updated after creating attribute. + * + * @return void + * @magentoConfigFixture default/catalog/search/engine elasticsearch7 + * @magentoDataFixture Magento/CatalogSearch/_files/full_reindex.php + */ + public function testCheckElasticsearchMappingAfterUpdateAttributeToSearchable(): void + { + $mappedAttributesBefore = $this->getMappingProperties(); + $expectedResult = [ + 'dropdown_attribute' => [ + 'type' => 'integer', + 'index' => false, + ], + 'dropdown_attribute_value' => [ + 'type' => 'text', + 'copy_to' => ['_search'], + ], + ]; + + /** @var ProductAttributeInterface $dropDownAttribute */ + $dropDownAttribute = $this->attributeFactory->create(); + $dropDownAttribute->setData($this->getAttributeData()); + $this->attributeRepository->save($dropDownAttribute); + $this->assertTrue($this->indexerProcessor->getIndexer()->isValid()); + + $mappedAttributesAfter = $this->getMappingProperties(); + $this->assertEquals($expectedResult, array_diff_key($mappedAttributesAfter, $mappedAttributesBefore)); + + $dropDownAttribute->setData(EavAttributeInterface::IS_SEARCHABLE, true); + $this->attributeRepository->save($dropDownAttribute); + $this->assertTrue($this->indexerProcessor->getIndexer()->isInvalid()); + + $this->assertEquals($mappedAttributesAfter, $this->getMappingProperties()); + } + + /** + * Retrieve Elasticsearch indexer mapping. + * + * @return array + */ + private function getMappingProperties(): array + { + $storeId = $this->storeManager->getStore()->getId(); + $mappedIndexerId = $this->indexNameResolver->getIndexMapping(Processor::INDEXER_ID); + $indexName = $this->indexNameResolver->getIndexFromAlias($storeId, $mappedIndexerId); + $mappedAttributes = $this->client->getMapping(['index' => $indexName]); + $pathField = $this->arrayManager->findPath('properties', $mappedAttributes); + + return $this->arrayManager->get($pathField, $mappedAttributes, []); + } + + /** + * Retrieve drop-down attribute data. + * + * @return array + */ + private function getAttributeData(): array + { + $entityTypeId = $this->installer->getEntityTypeId(ProductAttributeInterface::ENTITY_TYPE_CODE); + + return [ + 'attribute_code' => 'dropdown_attribute', + 'entity_type_id' => $entityTypeId, + 'is_global' => 0, + 'is_user_defined' => 1, + 'frontend_input' => 'select', + 'is_unique' => 0, + 'is_required' => 0, + 'is_searchable' => 0, + 'is_visible_in_advanced_search' => 0, + 'is_comparable' => 0, + 'is_filterable' => 0, + 'is_filterable_in_search' => 0, + 'is_used_for_promo_rules' => 0, + 'is_html_allowed_on_front' => 1, + 'is_visible_on_front' => 1, + 'used_in_product_listing' => 1, + 'used_for_sort_by' => 0, + 'frontend_label' => ['Drop-Down Attribute'], + 'backend_type' => 'varchar', + 'option' => [ + 'value' => [ + 'option_1' => ['Option 1'], + 'option_2' => ['Option 2'], + 'option_3' => ['Option 3'], + ], + 'order' => [ + 'option_1' => 1, + 'option_2' => 2, + 'option_3' => 3, + ], + ], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Elasticsearch6/Controller/QuickSearchTest.php b/dev/tests/integration/testsuite/Magento/Elasticsearch6/Controller/QuickSearchTest.php index 200360b7340bd..1d640e62dc5d4 100644 --- a/dev/tests/integration/testsuite/Magento/Elasticsearch6/Controller/QuickSearchTest.php +++ b/dev/tests/integration/testsuite/Magento/Elasticsearch6/Controller/QuickSearchTest.php @@ -64,6 +64,6 @@ public function testQuickSearchWithImprovedPriceRangeCalculation() $this->storeManager->setCurrentStore($defaultStore); } - $this->assertContains('search product 1', $responseBody); + $this->assertStringContainsString('search product 1', $responseBody); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock index 064b5d5f992ab..85ee46cd823b8 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testFromCreateProject/composer.lock @@ -2352,10 +2352,6 @@ ".php_cs.dist", ".php_cs.dist" ], - [ - ".travis.yml", - ".travis.yml" - ], [ ".user.ini", ".user.ini" @@ -2556,10 +2552,6 @@ "dev/tools", "dev/tools" ], - [ - "dev/travis", - "dev/travis" - ], [ "generated", "generated" @@ -4775,7 +4767,7 @@ "shasum": "ed1da1137848560dde1a85f0f54dc2fac262359e" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog": "103.0.*", @@ -4815,7 +4807,7 @@ "shasum": "a9da3243900390ad163efc7969b07116d2eb793f" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog-search": "101.0.*", @@ -9408,7 +9400,7 @@ "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.6", "dotmailer/dotmailer-magento2-extension": "3.1.1", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", diff --git a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock index a6f208c9c0d8d..4c2f8692bf805 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Framework/Composer/_files/testSkeleton/composer.lock @@ -2352,10 +2352,6 @@ ".php_cs.dist", ".php_cs.dist" ], - [ - ".travis.yml", - ".travis.yml" - ], [ ".user.ini", ".user.ini" @@ -2556,10 +2552,6 @@ "dev/tools", "dev/tools" ], - [ - "dev/travis", - "dev/travis" - ], [ "generated", "generated" @@ -4775,7 +4767,7 @@ "shasum": "ed1da1137848560dde1a85f0f54dc2fac262359e" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog": "103.0.*", @@ -4815,7 +4807,7 @@ "shasum": "a9da3243900390ad163efc7969b07116d2eb793f" }, "require": { - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "magento/framework": "102.0.*", "magento/module-advanced-search": "100.3.*", "magento/module-catalog-search": "101.0.*", @@ -9408,7 +9400,7 @@ "colinmollenhour/php-redis-session-abstract": "~1.4.0", "composer/composer": "^1.6", "dotmailer/dotmailer-magento2-extension": "3.1.1", - "elasticsearch/elasticsearch": "~7.6", + "elasticsearch/elasticsearch": "~7.7.0", "ext-bcmath": "*", "ext-ctype": "*", "ext-curl": "*", diff --git a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php b/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php index cdbfa26111d0f..c6aeaf9e0f927 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Console/CliTest.php @@ -79,6 +79,7 @@ protected function tearDown(): void * Checks that settings from env.php config file are applied * to created application instance. * + * @magentoAppIsolation enabled * @param bool $isPub * @param array $params * @dataProvider documentRootIsPubProvider diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php index 8388f2e81c0aa..5dfab6fcc756c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/InterfaceTest.php @@ -9,6 +9,9 @@ */ namespace Magento\Framework\DB\Adapter; +/** + * @magentoDbIsolation disabled + */ class InterfaceTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php index 345302a374081..6e3391bd8959f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/Adapter/Pdo/MysqlTest.php @@ -10,6 +10,11 @@ use Magento\Framework\DB\Ddl\Table; use Magento\TestFramework\Helper\Bootstrap; +/** + * Class checks Mysql adapter behaviour + * + * @magentoDbIsolation disabled + */ class MysqlTest extends \PHPUnit\Framework\TestCase { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php b/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php index d4507237b0ad1..db5e90d46880c 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/TransactionTest.php @@ -67,10 +67,13 @@ public function testTransactionLevelDbIsolationEnabled() $this->assertEquals(1, $resourceConnection->getConnection('default')->getTransactionLevel()); } + /** + * @magentoDataFixture Magento/Framework/DB/_files/dummy_fixture.php + */ public function testTransactionLevelDbIsolationDefault() { $resourceConnection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->get(\Magento\Framework\App\ResourceConnection::class); - $this->assertEquals(0, $resourceConnection->getConnection('default')->getTransactionLevel()); + $this->assertEquals(1, $resourceConnection->getConnection('default')->getTransactionLevel()); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php b/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php new file mode 100644 index 0000000000000..2dc96aa234590 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/DB/_files/dummy_fixture.php @@ -0,0 +1,8 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +//this fixture should not do anything diff --git a/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php b/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php index 6cd211be6f14d..e663b8ccedceb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Filter/VariableResolver/LegacyResolverTest.php @@ -117,6 +117,7 @@ public function getThing() ['foo' => $dataClassStub, 'g' => ['h' => ['i' => 'abc']]], 'abca=123,b=321,' ], + 'disallow __callParent method' => ['foo.___callParent()',['foo' => $classStub], null], ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php index 79c8765dd4220..96e31a753adaa 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/Config/GraphQlReaderTest.php @@ -47,9 +47,11 @@ protected function setUp(): void $fileResolverMock = $this->getMockBuilder( \Magento\Framework\Config\FileResolverInterface::class )->disableOriginalConstructor()->getMock(); + $filePath1 = __DIR__ . '/../_files/schemaA.graphqls'; + $filePath2 = __DIR__ . '/../_files/schemaB.graphqls'; $fileList = [ - file_get_contents(__DIR__ . '/../_files/schemaA.graphqls'), - file_get_contents(__DIR__ . '/../_files/schemaB.graphqls') + $filePath1 => file_get_contents($filePath1), + $filePath2 => file_get_contents($filePath2) ]; $fileResolverMock->expects($this->any())->method('get')->willReturn($fileList); $graphQlReader = $this->objectManager->create( @@ -219,31 +221,25 @@ function ($a, $b) { } //Checks to make sure that the given description exists in the expectedOutput array $this->assertArrayHasKey( - - array_search( - 'Comment for empty PhysicalProductInterface', - array_column($expectedOutput, 'description') - ), - $expectedOutput - + array_search( + 'Comment for empty PhysicalProductInterface', + array_column($expectedOutput, 'description') + ), + $expectedOutput ); $this->assertArrayHasKey( - - array_search( - 'Comment for empty Enum', - array_column($expectedOutput, 'description') - ), - $expectedOutput - + array_search( + 'Comment for empty Enum', + array_column($expectedOutput, 'description') + ), + $expectedOutput ); $this->assertArrayHasKey( - - array_search( - 'Comment for SearchResultPageInfo', - array_column($expectedOutput, 'description') - ), - $expectedOutput - + array_search( + 'Comment for SearchResultPageInfo', + array_column($expectedOutput, 'description') + ), + $expectedOutput ); } } diff --git a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php index 5d4047b1456d5..f74d96bcc42a3 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/GraphQl/GraphQlConfigTest.php @@ -36,9 +36,11 @@ protected function setUp(): void $fileResolverMock = $this->getMockBuilder( \Magento\Framework\Config\FileResolverInterface::class )->disableOriginalConstructor()->getMock(); + $filePath1 = __DIR__ . '/_files/schemaC.graphqls'; + $filePath2 = __DIR__ . '/_files/schemaD.graphqls'; $fileList = [ - file_get_contents(__DIR__ . '/_files/schemaC.graphqls'), - file_get_contents(__DIR__ . '/_files/schemaD.graphqls') + $filePath1 => file_get_contents($filePath1), + $filePath2 => file_get_contents($filePath2) ]; $fileResolverMock->expects($this->any())->method('get')->willReturn($fileList); $graphQlReader = $objectManager->create( @@ -46,10 +48,12 @@ protected function setUp(): void ['fileResolver' => $fileResolverMock] ); $reader = $objectManager->create( + // phpstan:ignore \Magento\Framework\GraphQlSchemaStitching\Reader::class, ['readers' => ['graphql_reader' => $graphQlReader]] ); $data = $objectManager->create( + // phpstan:ignore \Magento\Framework\GraphQl\Config\Data ::class, ['reader' => $reader] ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php index 1f65bca8f5f1d..b9deeb3bb968f 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/AbstractPlugin.php @@ -5,6 +5,8 @@ */ namespace Magento\Framework\Interception; +use Magento\Framework\App\Filesystem\DirectoryList; + /** * Class GeneralTest * @@ -81,6 +83,10 @@ public function setUpInterceptionConfig($pluginConfig) $cacheManager->method('load')->willReturn(null); $definitions = new \Magento\Framework\ObjectManager\Definition\Runtime(); $relations = new \Magento\Framework\ObjectManager\Relations\Runtime(); + $configLoader = $this->createMock(ConfigLoaderInterface::class); + $logger = $this->createMock(\Psr\Log\LoggerInterface::class); + $directoryList = $this->createMock(DirectoryList::class); + $configWriter = $this->createMock(PluginListGenerator::class); $interceptionConfig = new Config\Config( $this->_configReader, $configScope, @@ -104,6 +110,10 @@ public function setUpInterceptionConfig($pluginConfig) \Magento\Framework\ObjectManager\DefinitionInterface::class => $definitions, \Magento\Framework\Interception\DefinitionInterface::class => $interceptionDefinitions, \Magento\Framework\Serialize\SerializerInterface::class => $json, + \Magento\Framework\Interception\ConfigLoaderInterface::class => $configLoader, + \Psr\Log\LoggerInterface::class => $logger, + \Magento\Framework\App\Filesystem\DirectoryList::class => $directoryList, + \Magento\Framework\App\ObjectManager\ConfigWriterInterface::class => $configWriter ]; $this->_objectManager = new \Magento\Framework\ObjectManager\ObjectManager( $factory, @@ -118,8 +128,8 @@ public function setUpInterceptionConfig($pluginConfig) 'preferences' => [ \Magento\Framework\Interception\PluginListInterface::class => \Magento\Framework\Interception\PluginList\PluginList::class, - \Magento\Framework\Interception\ChainInterface::class => - \Magento\Framework\Interception\Chain\Chain::class, + \Magento\Framework\Interception\ConfigWriterInterface::class => + \Magento\Framework\Interception\PluginListGenerator::class ], ] ); diff --git a/dev/tests/integration/testsuite/Magento/Framework/Interception/PluginListGeneratorTest.php b/dev/tests/integration/testsuite/Magento/Framework/Interception/PluginListGeneratorTest.php new file mode 100644 index 0000000000000..8f1771759cee0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/Interception/PluginListGeneratorTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Interception; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\TestFramework\Application; +use Magento\TestFramework\Helper\Bootstrap; + +class PluginListGeneratorTest extends \PHPUnit\Framework\TestCase +{ + /** + * Generated plugin list config for frontend scope + */ + const CACHE_ID = 'primary|global|frontend|plugin-list'; + + /** + * @var PluginListGenerator + */ + private $model; + + /** + * @var DirectoryList + */ + private $directoryList; + + /** + * @var DriverInterface + */ + private $file; + + /** + * @var Application + */ + private $application; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->application = Bootstrap::getInstance()->getBootstrap()->getApplication(); + $this->directoryList = new DirectoryList(BP, $this->getCustomDirs()); + $this->file = Bootstrap::getObjectManager()->create(DriverInterface::class); + $reader = Bootstrap::getObjectManager()->create( + // phpstan:ignore "Class Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy not found." + \Magento\Framework\ObjectManager\Config\Reader\Dom\Proxy::class + ); + $scopeConfig = Bootstrap::getObjectManager()->create(\Magento\Framework\Config\Scope::class); + $omConfig = Bootstrap::getObjectManager()->create( + \Magento\Framework\Interception\ObjectManager\Config\Developer::class + ); + $relations = Bootstrap::getObjectManager()->create( + \Magento\Framework\ObjectManager\Relations\Runtime::class + ); + $definitions = Bootstrap::getObjectManager()->create( + \Magento\Framework\Interception\Definition\Runtime::class + ); + $classDefinitions = Bootstrap::getObjectManager()->create( + \Magento\Framework\ObjectManager\Definition\Runtime::class + ); + // phpstan:ignore "Class Psr\Log\LoggerInterface\Proxy not found." + $logger = Bootstrap::getObjectManager()->create(\Psr\Log\LoggerInterface\Proxy::class); + $this->model = new PluginListGenerator( + $reader, + $scopeConfig, + $omConfig, + $relations, + $definitions, + $classDefinitions, + $logger, + $this->directoryList, + ['primary', 'global'] + ); + } + + /** + * Test plugin list configuration generation and load. + */ + public function testPluginListConfigGeneration() + { + $scopes = ['frontend']; + $this->model->write($scopes); + $configData = $this->model->load(self::CACHE_ID); + $this->assertNotEmpty($configData[0]); + $this->assertNotEmpty($configData[1]); + $this->assertNotEmpty($configData[2]); + $expected = [ + 1 => [ + 0 => 'genericHeaderPlugin', + 1 => 'asyncCssLoad', + 2 => 'response-http-page-cache' + ] + ]; + // Here in test is assumed that this class below has 3 plugins. But the amount of plugins and class itself + // may vary. If it is changed, please update these assertions. + $this->assertArrayHasKey( + 'Magento\\Framework\\App\\Response\\Http_sendResponse___self', + $configData[2], + 'Processed plugin does not exist in the processed plugins array.' + ); + $this->assertSame( + $expected, + $configData[2]['Magento\\Framework\\App\\Response\\Http_sendResponse___self'], + 'Plugin configurations are not equal' + ); + } + + /** + * Gets customized directory paths + * + * @return array + */ + private function getCustomDirs() + { + $path = DirectoryList::PATH; + $generated = "{$this->application->getTempDir()}/generated"; + + return [ + DirectoryList::GENERATED_METADATA => [$path => "{$generated}/metadata"], + ]; + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $filePath = $this->directoryList->getPath(DirectoryList::GENERATED_METADATA) + . '/' . self::CACHE_ID . '.' . 'php'; + + if (file_exists($filePath)) { + $this->file->deleteFile($filePath); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php index 306bda462820a..81ab34fae9b98 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Lock/Backend/CacheTest.php @@ -47,16 +47,10 @@ public function testParallelLock(): void { $identifier1 = \uniqid('lock_name_1_', true); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 2)); + $this->assertTrue($this->cacheInstance1->lock($identifier1)); - $this->assertFalse($this->cacheInstance1->lock($identifier1, 2)); - $this->assertFalse($this->cacheInstance2->lock($identifier1, 2)); - sleep(4); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); - - $this->assertTrue($this->cacheInstance2->lock($identifier1, -1)); - sleep(4); - $this->assertTrue($this->cacheInstance1->isLocked($identifier1)); + $this->assertFalse($this->cacheInstance1->lock($identifier1, 0)); + $this->assertFalse($this->cacheInstance2->lock($identifier1, 0)); } /** @@ -66,19 +60,17 @@ public function testParallelLock(): void */ public function testParallelLockExpired(): void { - $identifier1 = \uniqid('lock_name_1_', true); + $testLifeTime = 2; + \Closure::bind(function (Cache $class) use ($testLifeTime) { + $class->defaultLifetime = $testLifeTime; + }, null, $this->cacheInstance1)($this->cacheInstance1); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $identifier1 = \uniqid('lock_name_1_', true); - $this->assertTrue($this->cacheInstance1->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $this->assertTrue($this->cacheInstance1->lock($identifier1, 0)); + $this->assertTrue($this->cacheInstance2->lock($identifier1, $testLifeTime + 1)); - $this->assertTrue($this->cacheInstance2->lock($identifier1, 1)); - sleep(2); - $this->assertFalse($this->cacheInstance1->isLocked($identifier1)); + $this->cacheInstance2->unlock($identifier1); } /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php index c11004f503c40..e5fed191ea17e 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/MessageQueue/UseCase/WaitAndNotWaitMessagesTest.php @@ -66,14 +66,21 @@ protected function setUp(): void */ public function testWaitForMessages() { - $this->assertArrayHasKey('queue', $this->config); - $this->assertArrayHasKey('consumers_wait_for_messages', $this->config['queue']); - $this->assertEquals(1, $this->config['queue']['consumers_wait_for_messages']); + $this->publisherConsumerController->stopConsumers(); + + $config = $this->config; + $config['queue']['consumers_wait_for_messages'] = 1; + $this->writeConfig($config); + + $loadedConfig = $this->loadConfig(); + $this->assertArrayHasKey('queue', $loadedConfig); + $this->assertArrayHasKey('consumers_wait_for_messages', $loadedConfig['queue']); + $this->assertEquals(1, $loadedConfig['queue']['consumers_wait_for_messages']); foreach ($this->messages as $message) { $this->publishMessage($message); } - + $this->publisherConsumerController->startConsumers(); $this->waitForAsynchronousResult(count($this->messages), $this->logFilePath); foreach ($this->messages as $item) { 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 2797cad61084c..ba2225fbe5eac 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Mview/View/ChangelogTest.php @@ -9,6 +9,8 @@ /** * Test Class for \Magento\Framework\Mview\View\Changelog + * + * @magentoDbIsolation disabled */ class ChangelogTest extends \PHPUnit\Framework\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php index c620251ca9b67..7d3b9d2089cf9 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/CompiledTest.php @@ -14,6 +14,9 @@ use Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation; use Magento\Framework\ObjectManager\TestAsset\TestAssetInterface; +/** + * @magentoAppIsolation enabled + */ class CompiledTest extends AbstractFactoryRuntimeDefinitionsTestCases { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php index 7fa7e677e0d8d..c74c00de4ce53 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/ObjectManager/Factory/Dynamic/DeveloperTest.php @@ -15,6 +15,9 @@ use Magento\Framework\ObjectManager\TestAsset\InterfaceImplementation; use Magento\Framework\ObjectManager\TestAsset\TestAssetInterface; +/** + * @magentoAppIsolation enabled + */ class DeveloperTest extends AbstractFactoryRuntimeDefinitionsTestCases { /** diff --git a/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php b/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php index c58689f0cd8e7..d35d875ff8006 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/Session/SessionManagerTest.php @@ -225,22 +225,6 @@ public function testSetSessionId() $this->assertEquals('test', $this->model->getSessionId()); } - /** - * @magentoConfigFixture current_store web/session/use_frontend_sid 1 - */ - public function testSetSessionIdFromParam() - { - $this->initializeModel(); - $this->appState->expects($this->any()) - ->method('getAreaCode') - ->willReturn(\Magento\Framework\App\Area::AREA_FRONTEND); - $currentId = $this->model->getSessionId(); - $this->assertNotEquals('test_id', $this->model->getSessionId()); - $this->request->getQuery()->set(SidResolverInterface::SESSION_ID_QUERY_PARAM, 'test-id'); - $this->model->setSessionId($this->sidResolver->getSid($this->model)); - $this->assertEquals($currentId, $this->model->getSessionId()); - } - public function testGetSessionIdForHost() { $this->initializeModel(); diff --git a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php index 04f64ff93ab1e..785637a9470cb 100644 --- a/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php +++ b/dev/tests/integration/testsuite/Magento/Framework/UrlTest.php @@ -23,13 +23,6 @@ protected function setUp(): void $this->model = Bootstrap::getObjectManager()->create(\Magento\Framework\Url::class); } - public function testSetGetUseSession() - { - $this->assertFalse((bool)$this->model->getUseSession()); - $this->model->setUseSession(false); - $this->assertFalse($this->model->getUseSession()); - } - public function testSetRouteFrontName() { $value = 'route'; diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTemplateTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTemplateTest.php new file mode 100644 index 0000000000000..0d2b85b4ae20d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTemplateTest.php @@ -0,0 +1,48 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper; + +use Magento\TestFramework\TestCase\AbstractController; + +/** + * Test the secure HTML helper and templates. + * + * @magentoAppArea frontend + */ +class SecureHtmlRendererTemplateTest extends AbstractController +{ + /** + * Test using the helper inside templates. + * + * @return void + */ + public function testTemplateUsage(): void + { + $this->getRequest()->setMethod('GET'); + $this->dispatch('securehtml/secure/helper'); + $content = $this->getResponse()->getContent(); + + $this->assertStringContainsString( + '<h1 onclick="alert()">Hello there!</h1>', + $content + ); + $this->assertStringContainsString( + '<script src="http://my.magento.com/static/script.js"/>', + $content + ); + $this->assertStringContainsString( + "<script>\n let myVar = 1;\n</script>", + $content + ); + $this->assertStringContainsString( + '<div>I am just <a> text</div>', + $content + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTest.php b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTest.php new file mode 100644 index 0000000000000..8fcb464aec734 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Framework/View/Helper/SecureHtmlRendererTest.php @@ -0,0 +1,141 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper; + +use Magento\Framework\View\Helper\SecureHtmlRender\TagData; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for the secure HTML helper. + */ +class SecureHtmlRendererTest extends TestCase +{ + /** + * @var SecureHtmlRenderer + */ + private $helper; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + //Clearing the processors list to ensure stable results. + $this->helper = $objectManager->create(SecureHtmlRenderer::class, ['processors' => []]); + } + + /** + * Provides tags to render. + * + * @return array + */ + public function getTags(): array + { + return [ + [ + new TagData('div', ['style' => 'display: none;', 'width' => '20px'], 'some <text>', true), + '<div style="display: none;" width="20px">some <text></div>' + ], + [ + new TagData('div', [], 'some <b>HTML</b>', false), + '<div>some <b>HTML</b></div>' + ], + [ + new TagData('img', ['src' => 'https://magento.com/img.jpg'], null, true), + '<img src="https://magento.com/img.jpg"/>' + ] + ]; + } + + /** + * Test tag rendering. + * + * @param TagData $tagData + * @param string $expected Expected HTML. + * @return void + * @dataProvider getTags + */ + public function testRenderTag(TagData $tagData, string $expected): void + { + $this->assertEquals( + $expected, + $this->helper->renderTag( + $tagData->getTag(), + $tagData->getAttributes(), + $tagData->getContent(), + $tagData->isTextContent() + ) + ); + } + + /** + * Test rendering an event listener. + * + * @return void + */ + public function testRenderEventHandler(): void + { + $this->assertEquals( + 'onclick="alert(this.parent.getAttribute("data-title"))"', + $this->helper->renderEventListener('onclick', 'alert(this.parent.getAttribute("data-title"))') + ); + } + + /** + * Test rendering JS listeners as separate tags. + * + * @return void + */ + public function testRenderEventListenerAsTag(): void + { + $html = $this->helper->renderEventListenerAsTag('onclick', 'alert(1)', '#id'); + $this->assertStringContainsString('alert(1)', $html); + $this->assertStringContainsString('#id', $html); + $this->assertStringContainsString('click', $html); + } + + /** + * Check handler validation + * + * @return void + */ + public function testInvalidEventListener(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->helper->renderEventListenerAsTag('nonevent', '', ''); + } + + /** + * Test rendering "style" attribute as separate tag. + * + * @return void + */ + public function testRenderStyleAsTag(): void + { + $html = $this->helper->renderStyleAsTag('display: none; font-size: 3em; ', '#id'); + $this->assertStringContainsString('#id', $html); + $this->assertStringContainsString('display', $html); + $this->assertStringContainsString('none', $html); + $this->assertStringContainsString('fontSize', $html); + $this->assertStringContainsString('3em', $html); + } + + /** + * Check style validation + * + * @return void + */ + public function testInvalidStyle(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->helper->renderStyleAsTag('display;', ''); + } +} diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php new file mode 100644 index 0000000000000..4cbe088893b03 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message.php @@ -0,0 +1,65 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\GiftMessage\Model\Message; +use Magento\GiftMessage\Model\ResourceModel\Message as MessageResource; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMask as QuoteIdMaskResource; +use Magento\Quote\Model\ResourceModel\Quote\QuoteIdMaskFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products.php'); + +/** @var ObjectManagerInterface $objectManager */ +$objectManager = Bootstrap::getObjectManager(); + +/** @var QuoteResource $quote */ +$quote = $objectManager->create(QuoteResource::class); + +/** @var Quote $quoteModel */ +$quoteModel = $objectManager->create(Quote::class); +$quoteModel->setData(['store_id' => 1, 'is_active' => 1, 'is_multi_shipping' => 0]); +$quote->save($quoteModel); + +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); + +$quoteModel->setReservedOrderId('test_guest_order_with_gift_message') + ->addProduct($product, 1); +$quoteModel->collectTotals(); +$quote->save($quoteModel); + +/** @var MessageResource $message */ +$message = $objectManager->create(MessageResource::class); + +/** @var Message $message */ +$messageModel = $objectManager->create(Message::class); + +$messageModel->setSender('John Doe'); +$messageModel->setRecipient('Jane Roe'); +$messageModel->setMessage('Gift Message Text'); +$message->save($messageModel); + +$quoteModel->getItemByProduct($product)->setGiftMessageId($messageModel->getId()); +$quote->save($quoteModel); + +/** @var QuoteIdMaskResource $quoteIdMask */ +$quoteIdMask = Bootstrap::getObjectManager() + ->create(QuoteIdMaskFactory::class) + ->create(); + +/** @var QuoteIdMask $quoteIdMaskModel */ +$quoteIdMaskModel = $objectManager->create(QuoteIdMask::class); + +$quoteIdMaskModel->setQuoteId($quoteModel->getId()); +$quoteIdMaskModel->setDataChanges(true); +$quoteIdMask->save($quoteIdMaskModel); diff --git a/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php new file mode 100644 index 0000000000000..9c215cb432b45 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GiftMessage/_files/guest/quote_with_item_message_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product; +use Magento\Framework\Registry; +use Magento\GiftMessage\Model\Message; +use Magento\Quote\Model\Quote; +use Magento\TestFramework\Helper\Bootstrap; + +$registry = Bootstrap::getObjectManager()->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +$objectManager = Bootstrap::getObjectManager(); +$quote = $objectManager->create(Quote::class); +$quote->load('test_guest_order_with_gift_message', 'reserved_order_id'); +$message = $objectManager->create(Message::class); +$product = $objectManager->create(Product::class); +foreach ($quote->getAllItems() as $item) { + $message->load($item->getGiftMessageId()); + $message->delete(); + $sku = $item->getSku(); + $product->load($product->getIdBySku($sku)); + if ($product->getId()) { + $product->delete(); + } +} +$quote->delete(); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php new file mode 100644 index 0000000000000..394b13078010a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/address_data.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +return [ + 'region' => 'CA', + 'region_id' => '12', + 'postcode' => '11111', + 'lastname' => 'lastname', + 'firstname' => 'firstname', + 'street' => 'street', + 'city' => 'Los Angeles', + 'email' => 'admin@example.com', + 'telephone' => '11111111', + 'country_id' => 'US' +]; diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php new file mode 100644 index 0000000000000..def622b8f5025 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Shipment; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\Framework\DB\Transaction; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/customer_order_with_two_items.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->create(Transaction::class); + +/** @var Order $order */ +$order = $objectManager->create(Order::class)->loadByIncrementId('100000555'); + +$items = []; +$shipmentIds = ['0000000098', '0000000099']; +$i = 0; +foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); + /** @var Shipment $shipment */ + $shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); + $shipment->setIncrementId($shipmentIds[$i]); + $shipment->register(); + + $transaction->addObject($shipment)->addObject($order)->save(); + $i++; +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments_rollback.php new file mode 100644 index 0000000000000..5fc01f2ecc073 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_multiple_shipments_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/customer_order_with_two_items_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php new file mode 100644 index 0000000000000..22eac03f9a6a8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment.php @@ -0,0 +1,70 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Sales\Api\ShipmentCommentRepositoryInterface; +use Magento\Sales\Model\Order\Shipment\Comment; +use Magento\Sales\Model\Order\ShipmentFactory; +use Magento\Sales\Model\Order\Shipment\Track; +use Magento\Framework\DB\Transaction; +use Magento\Sales\Api\ShipmentTrackRepositoryInterface; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/customer_order_with_two_items.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->create(Transaction::class); + +/** @var Order $order */ +$order = $objectManager->create(Order::class)->loadByIncrementId('100000555'); + +$items = []; +foreach ($order->getItems() as $orderItem) { + $items[$orderItem->getId()] = $orderItem->getQtyOrdered(); +} +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $items); +$shipment->register(); + +$transaction->addObject($shipment)->addObject($order)->save(); + +//Add shipment comments +$shipmentCommentRepository = $objectManager->get(ShipmentCommentRepositoryInterface::class); +$comments = [ + [ + 'comment' => 'This comment is visible to the customer', + 'is_visible_on_front' => 1, + 'is_customer_notified' => 1, + ], + [ + 'comment' => 'This comment should not be visible to the customer', + 'is_visible_on_front' => 0, + 'is_customer_notified' => 0, + ], +]; + +foreach ($comments as $commentData) { + /** @var Comment $comment */ + $comment = $objectManager->create(Comment::class); + $comment->setParentId($shipment->getId()); + $comment->setComment($commentData['comment']); + $comment->setIsVisibleOnFront($commentData['is_visible_on_front']); + $comment->setIsCustomerNotified($commentData['is_customer_notified']); + $shipmentCommentRepository->save($comment); +} + +//Add tracking +/** @var ShipmentTrackRepositoryInterface $shipmentTrackRepository */ +$shipmentTrackRepository = $objectManager->get(ShipmentTrackRepositoryInterface::class); +/** @var Track $track */ +$track = $objectManager->create(Track::class); +$track->setOrderId($order->getId()); +$track->setParentId($shipment->getId()); +$track->setTitle('United Parcel Service'); +$track->setCarrierCode('ups'); +$track->setTrackNumber('1234567890'); +$shipmentTrackRepository->save($track); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment_rollback.php new file mode 100644 index 0000000000000..5fc01f2ecc073 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_simple_shipment_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/customer_order_with_two_items_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php new file mode 100644 index 0000000000000..848ee1ff0174b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Framework\DB\Transaction; +use Magento\Sales\Model\Order\ShipmentFactory; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_with_customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Transaction $transaction */ +$transaction = $objectManager->get(Transaction::class); +/** @var Order $order */ +$order = $objectManager->create(Order::class)->loadByIncrementId('100000001'); +//Set the shipping method +$order->setShippingDescription('UPS Next Day Air'); +$order->setShippingMethod('ups_01'); +$order->save(); + +//Create Shipment with UPS tracking and some items +$shipmentItems = []; +foreach ($order->getItems() as $orderItem) { + $shipmentItems[$orderItem->getId()] = $orderItem->getQtyOrdered(); +} +$tracking = [ + 'carrier_code' => 'ups', + 'title' => 'United Parcel Service', + 'number' => '987654321' +]; + +$shipment = $objectManager->get(ShipmentFactory::class)->create($order, $shipmentItems, [$tracking]); +$shipment->register(); +$transaction->addObject($shipment)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping_rollback.php new file mode 100644 index 0000000000000..bbb90e0326aec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/customer_order_with_ups_shipping_rollback.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_with_customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php new file mode 100644 index 0000000000000..f8dd55c6fdbeb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals.php @@ -0,0 +1,81 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple'); + +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setBaseRowTotal($product->getPrice()); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(100) + ->setBaseGrandTotal(100) + ->setOrderCurrencyCode("USD") + ->setBaseCurrencyCode('USD') + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php new file mode 100644 index 0000000000000..113f84dae385e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/order_with_totals_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php new file mode 100644 index 0000000000000..3caa1410c65e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer.php @@ -0,0 +1,163 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Model\Order; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order.php'); +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple'); +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); +$payment = $order->getPayment(); +$orderItems = $order->getItems(); +$orderItem = reset($orderItems); +$addressData = include __DIR__ . '/address_data.php'; +$orders = [ + [ + 'increment_id' => '100000002', + 'state' => \Magento\Sales\Model\Order::STATE_NEW, + 'status' => 'processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 120.00, + 'subtotal' => 120.00, + 'base_grand_total' => 120.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000003', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 130.00, + 'base_grand_total' => 130.00, + 'subtotal' => 130.00, + 'total_paid' => 130.00, + 'store_id' => 0, + 'website_id' => 0, + ], + [ + 'increment_id' => '100000004', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'closed', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 140.00, + 'base_grand_total' => 140.00, + 'subtotal' => 140.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000005', + 'state' => \Magento\Sales\Model\Order::STATE_COMPLETE, + 'status' => 'complete', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 150.00, + 'base_grand_total' => 150.00, + 'subtotal' => 150.00, + 'total_paid' => 150.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000006', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 160.00, + 'base_grand_total' => 160.00, + 'subtotal' => 160.00, + 'total_paid' => 160.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000007', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 180.00, + 'base_grand_total' => 180.00, + 'subtotal' => 170.00, + 'tax_amount' => 5.00, + 'shipping_amount'=> 5.00, + 'base_shipping_amount'=> 4.00, + 'store_id' => 1, + 'website_id' => 1, + ], + [ + 'increment_id' => '100000008', + 'state' => \Magento\Sales\Model\Order::STATE_PROCESSING, + 'status' => 'Processing', + 'order_currency_code' =>'USD', + 'base_currency_code' =>'USD', + 'grand_total' => 190.00, + 'base_grand_total' => 190.00, + 'subtotal' => 180.00, + 'tax_amount' => 5.00, + 'shipping_amount'=> 5.00, + 'base_shipping_amount'=> 4.00, + 'store_id' => 1, + 'website_id' => 1, + ] +]; + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +/** @var array $orderData */ +foreach ($orders as $orderData) { + $newPayment = clone $payment; + $newPayment->setId(null); + /** @var $order \Magento\Sales\Model\Order */ + $order = $objectManager->create( + \Magento\Sales\Model\Order::class + ); + + // Reset addresses + /** @var Order\Address $billingAddress */ + $billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType('billing'); + + $shippingAddress = clone $billingAddress; + $shippingAddress->setId(null)->setAddressType('shipping'); + + /** @var Order\Item $orderItem */ + $orderItem = $objectManager->create(Order\Item::class); + $orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + + $order->setData($orderData) + ->addItem($orderItem) + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@example.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setPayment($newPayment); + + $orderRepository->save($order); +} diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php new file mode 100644 index 0000000000000..dc455c3cb2c49 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/orders_with_customer_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php new file mode 100644 index 0000000000000..c10ca26e640f1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews.php @@ -0,0 +1,138 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Store\Model\Store; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store.php'); + +/** @var \Magento\Catalog\Model\Product $product */ + +$addressData = include __DIR__ . '/address_data.php'; + +$objectManager = Bootstrap::getObjectManager(); +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +/** @var Magento\Catalog\Model\Product $product */ +$product = $productRepository->get('simple'); + +$secondStore = Bootstrap::getObjectManager() + ->create(Store::class); + +$billingAddress = $objectManager->create(OrderAddress::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); +$customerIdFromFixture = 1; +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(Payment::class); +$payment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setState(Order::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setOrderCurrencyCode("USD") + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(10) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(false) + ->setCustomerId($customerIdFromFixture) + ->setCustomerEmail('customer@null.com') + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); + +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); + +/** @var Payment $payment */ +$secondPayment = $objectManager->create(Payment::class); +$secondPayment->setMethod('checkmo') + ->setAdditionalInformation('last_trans_id', '11122') + ->setAdditionalInformation( + 'metadata', + [ + 'type' => 'free', + 'fraudulent' => false, + ] + ); + +/** @var OrderItem $orderItem */ +$secondOrderItem = $objectManager->create(OrderItem::class); +$secondOrderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple') + ->setName($product->getName()) + ->setSku($product->getSku()); + +/** @var Order $order */ +$secondOrder = $objectManager->create(Order::class); +$secondOrder->setIncrementId('100000002') + ->setState(Order::STATE_PROCESSING) + ->setStatus($secondOrder->getConfig()->getStateDefaultStatus(Order::STATE_PROCESSING)) + ->setSubtotal(110) + ->setOrderCurrencyCode("USD") + ->setShippingAmount(10.0) + ->setBaseShippingAmount(10.0) + ->setTaxAmount(5.0) + ->setGrandTotal(100) + ->setBaseSubtotal(110) + ->setBaseGrandTotal(100) + ->setCustomerIsGuest(false) + ->setCustomerId($customerIdFromFixture) + ->setCustomerEmail('customer@null.com') + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($secondStore->load('fixture_second_store')->getId()) + ->addItem($secondOrderItem) + ->setPayment($secondPayment); +$orderRepository->save($secondOrder); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php new file mode 100644 index 0000000000000..fe98d8659d3c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Sales/_files/two_orders_with_order_items_two_storeviews_rollback.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php new file mode 100644 index 0000000000000..fbd710fc07c0c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//configuration setting for shipping tax class and shipping tax calculation and display +$configWriter->save('tax/classes/shipping_tax_class', '2'); +$configWriter->save('tax/calculation/shipping_includes_tax', '1'); +$configWriter->save('tax/sales_display/shipping', '3'); +$configWriter->save('tax/display/shipping', '3'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php new file mode 100644 index 0000000000000..21b0a4317fc78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_and_order_display_settings_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//Apply discount on prices to include tax +$configWriter->save('tax/classes/shipping_tax_class', '0'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '1'); +$configWriter->save('tax/display/shipping', '1'); +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php new file mode 100644 index 0000000000000..9e1ce11a01b0e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//configuration setting for shipping tax class and shipping tax calculation and display +$configWriter->save('tax/classes/shipping_tax_class', '2'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '3'); +$configWriter->save('tax/display/shipping', '3'); + +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php new file mode 100644 index 0000000000000..21b0a4317fc78 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_calculation_shipping_excludeTax_order_display_settings_rollback.php @@ -0,0 +1,23 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\App\Config\Storage\Writer; +use Magento\Framework\App\Config\Storage\WriterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Config\ScopeConfigInterface; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Writer $configWriter */ +$configWriter = $objectManager->get(WriterInterface::class); + +//Apply discount on prices to include tax +$configWriter->save('tax/classes/shipping_tax_class', '0'); +$configWriter->save('tax/calculation/shipping_includes_tax', '0'); +$configWriter->save('tax/sales_display/shipping', '1'); +$configWriter->save('tax/display/shipping', '1'); +$scopeConfig = $objectManager->get(ScopeConfigInterface::class); +$scopeConfig->clean(); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php new file mode 100644 index 0000000000000..2603e2056f19d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al.php @@ -0,0 +1,53 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\Data\TaxRateInterface; +use Magento\Tax\Api\Data\TaxRuleInterface; +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\Api\DataObjectHelper; + +$objectManager = Bootstrap::getObjectManager(); +/** @var DataObjectHelper $dataObjectHelper */ +$dataObjectHelper = Bootstrap::getObjectManager()->get(DataObjectHelper::class); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var Rate $rate */ +$rate = $rateFactory->create(); +$rateData = [ + Rate::KEY_COUNTRY_ID => 'US', + Rate::KEY_REGION_ID => '1', + Rate::KEY_POSTCODE => '*', + Rate::KEY_CODE => 'US-AL-*-Rate-1', + Rate::KEY_PERCENTAGE_RATE => '5.5', +]; +$dataObjectHelper->populateWithArray($rate, $rateData, TaxRateInterface::class); +$rateRepository->save($rate); + +$rule = $ruleFactory->create(); +$ruleData = [ + Rule::KEY_CODE=> 'GraphQl Test Rule AL', + Rule::KEY_PRIORITY => '0', + Rule::KEY_POSITION => '0', + Rule::KEY_CUSTOMER_TAX_CLASS_IDS => [3], + Rule::KEY_PRODUCT_TAX_CLASS_IDS => [2], + Rule::KEY_TAX_RATE_IDS => [$rate->getId()], +]; +$dataObjectHelper->populateWithArray($rule, $ruleData, TaxRuleInterface::class); +$ruleRepository->save($rule); diff --git a/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php new file mode 100644 index 0000000000000..22372f3a21022 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/GraphQl/Tax/_files/tax_rule_for_region_al_rollback.php @@ -0,0 +1,38 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Tax\Api\TaxRateRepositoryInterface; +use Magento\Tax\Api\TaxRuleRepositoryInterface; +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\Calculation\Rule; +use Magento\Tax\Model\Calculation\RuleFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Tax\Model\ResourceModel\Calculation\Rate as RateResource; +use Magento\Tax\Model\ResourceModel\Calculation\Rule as RuleResource; + +$objectManager = Bootstrap::getObjectManager(); +/** @var RateFactory $rateFactory */ +$rateFactory = $objectManager->get(RateFactory::class); +/** @var RuleFactory $ruleFactory */ +$ruleFactory = $objectManager->get(RuleFactory::class); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(TaxRateRepositoryInterface::class); +/** @var TaxRuleRepository $ruleRepository */ +$ruleRepository = $objectManager->get(TaxRuleRepositoryInterface::class); +/** @var RateResource $rateResource */ +$rateResource = $objectManager->get(RateResource::class); +/** @var RuleResource $ruleResource */ +$ruleResource = $objectManager->get(RuleResource::class); + +$rate = $rateFactory->create(); +$rateResource->load($rate, 'US-AL-*-Rate-1', Rate::KEY_CODE); +$rule = $ruleFactory->create(); +$ruleResource->load($rule, 'GraphQl Test Rule AL', Rule::KEY_CODE); +$ruleRepository->delete($rule); +$rateRepository->delete($rate); 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 index 2a460a8ce622a..53e90ebf76f66 100644 --- 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 @@ -16,13 +16,15 @@ /** * Test for \Magento\ImportExport\Controller\Adminhtml\Export\File\Delete class. + * + * @magentoAppArea adminhtml */ class DeleteTest extends AbstractBackendController { /** * @var WriteInterface */ - private $varDirectory; + protected $varDirectory; /** * @var string @@ -83,7 +85,7 @@ public function testExecute($file): void * @param $destinationFilePath * @return void */ - private function copyFile($destinationFilePath): void + protected function copyFile($destinationFilePath): void { //Refers to application root directory $rootDirectory = $this->fileSystem->getDirectoryWrite(DirectoryList::ROOT); diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php index 9d83b3d2ece98..1bd41b047163a 100644 --- a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Export/Adapter/CsvTest.php @@ -9,6 +9,7 @@ use Magento\Framework\App\Filesystem\DirectoryList; use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Import; use Magento\Framework\ObjectManagerInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; @@ -28,43 +29,53 @@ class CsvTest extends TestCase */ private $objectManager; - /** - * @var Csv - */ - private $csv; - /** * @inheritdoc */ protected function setUp(): void { - parent::setUp(); - $this->objectManager = Bootstrap::getObjectManager(); - $this->csv = $this->objectManager->create( - Csv::class, - ['destination' => $this->destination] - ); } /** * Test to destruct export adapter + * + * @dataProvider destructDataProvider + * + * @param string $destination + * @param bool $shouldBeDeleted + * @return void */ - public function testDestruct(): void + public function testDestruct(string $destination, bool $shouldBeDeleted): void { + $csv = $this->objectManager->create(Csv::class, ['destination' => $destination]); /** @var Filesystem $fileSystem */ $fileSystem = $this->objectManager->get(Filesystem::class); $directoryHandle = $fileSystem->getDirectoryRead(DirectoryList::VAR_DIR); /** Assert that the destination file is present after construct */ $this->assertFileExists( - $directoryHandle->getAbsolutePath($this->destination), + $directoryHandle->getAbsolutePath($destination), 'The destination file was\'t created after construct' ); - /** Assert that the destination file was removed after destruct */ - $this->csv = null; - $this->assertFileNotExists( - $directoryHandle->getAbsolutePath($this->destination), - 'The destination file was\'t removed after destruct' - ); + unset($csv); + + if ($shouldBeDeleted) { + $this->assertFileDoesNotExist($directoryHandle->getAbsolutePath($destination)); + } else { + $this->assertFileExists($directoryHandle->getAbsolutePath($destination)); + } + } + + /** + * DataProvider for testDestruct + * + * @return array + */ + public function destructDataProvider(): array + { + return [ + 'temporary file' => [$this->destination, true], + 'import history file' => [Import::IMPORT_HISTORY_DIR . $this->destination, false], + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php new file mode 100644 index 0000000000000..218221c35632c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/ImportExport/Model/Report/CsvTest.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\ImportExport\Model\Report; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Filesystem; +use Magento\ImportExport\Model\Import; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingError; +use Magento\ImportExport\Model\Import\ErrorProcessing\ProcessingErrorAggregatorInterface; +use Magento\TestFramework\Helper\Bootstrap; + +/** + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoAppArea adminhtml + */ +class CsvTest extends \PHPUnit\Framework\TestCase +{ + /** + * @var Filesystem\Directory\WriteInterface + */ + private $directory; + + /** + * @var Csv + */ + private $csvReport; + + /** + * @var string|null + */ + private $importFilePath; + + /** + * @var string|null + */ + private $reportPath; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $filesystem = Bootstrap::getObjectManager()->create(Filesystem::class); + $this->directory = $filesystem->getDirectoryWrite(DirectoryList::VAR_DIR); + + $this->csvReport = Bootstrap::getObjectManager()->create(Csv::class); + } + /** + * @inheritDoc + */ + protected function tearDown(): void + { + foreach ([$this->importFilePath, $this->reportPath] as $path) { + if ($path && $this->directory->isExist($path)) { + $this->directory->delete($path); + } + } + } + + /** + * @return void + */ + public function testCreateReport() + { + $importData = <<<fileContent +sku,store_view_code,name,price,product_type,attribute_set_code,weight +simple1,,"simple 1",10,simple,Default,-5 +fileContent; + $this->importFilePath = 'test_import.csv'; + $this->directory->writeFile($this->importFilePath, $importData); + + $errorAggregator = Bootstrap::getObjectManager()->create(ProcessingErrorAggregatorInterface::class); + $error = 'Value for \'weight\' attribute contains incorrect value'; + $errorAggregator->addError($error, ProcessingError::ERROR_LEVEL_CRITICAL, 1, 'weight', $error); + + $outputFileName = $this->csvReport->createReport( + $this->directory->getAbsolutePath($this->importFilePath), + $errorAggregator + ); + + $this->reportPath = Import::IMPORT_HISTORY_DIR . $outputFileName; + $this->assertTrue($this->directory->isExist($this->reportPath), 'Report was not generated'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php index 3531937b881e2..31ddbb5c0b46f 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/DeleteTest.php @@ -37,10 +37,10 @@ public function testRender() { $integration = $this->getFixtureIntegration(); $buttonHtml = $this->deleteButtonBlock->render($integration); - $this->assertStringContainsString('title="Remove"', $buttonHtml); - $this->assertStringContainsString( - 'onclick="this.setAttribute('data-url', ' - . ''http://localhost/index.php/backend/admin/integration/delete/id/' + self::assertStringContainsString('title="Remove"', $buttonHtml); + self::assertStringContainsString( + 'this.setAttribute(\'data-url\', ' + . '\'http://localhost/index.php/backend/admin/integration/delete/id/' . $integration->getId(), $buttonHtml ); @@ -52,14 +52,18 @@ public function testRenderDisabled() $integration = $this->getFixtureIntegration(); $integration->setSetupType(Integration::TYPE_CONFIG); $buttonHtml = $this->deleteButtonBlock->render($integration); - $this->assertStringContainsString('title="Uninstall the extension to remove this integration"', $buttonHtml); - $this->assertStringContainsString( - 'onclick="this.setAttribute('data-url', ' - . ''http://localhost/index.php/backend/admin/integration/delete/id/' + self::assertStringContainsString( + 'title="' .$this->deleteButtonBlock->escapeHtmlAttr('Uninstall the extension to remove this integration') + .'"', + $buttonHtml + ); + self::assertStringContainsString( + 'this.setAttribute(\'data-url\', ' + . '\'http://localhost/index.php/backend/admin/integration/delete/id/' . $integration->getId(), $buttonHtml ); - $this->assertStringContainsString('disabled="disabled"', $buttonHtml); + self::assertStringContainsString('disabled="disabled"', $buttonHtml); } /** diff --git a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php index 6b1322e58f130..522af5e08f1de 100644 --- a/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/Integration/Block/Adminhtml/Widget/Grid/Column/Renderer/Button/EditTest.php @@ -38,9 +38,9 @@ public function testRenderEdit() $integration = $this->getFixtureIntegration(); $buttonHtml = $this->editButtonBlock->render($integration); $this->assertStringContainsString('title="Edit"', $buttonHtml); - $this->assertStringContainsString('class="action edit"', $buttonHtml); + $this->assertStringContainsString('class="' .$this->editButtonBlock->escapeHtmlAttr('action edit') .'"', $buttonHtml); $this->assertStringContainsString( - 'onclick="window.location.href='http://localhost/index.php/backend/admin/integration/edit/id/' + 'window.location.href=\'http://localhost/index.php/backend/admin/integration/edit/id/' . $integration->getId(), $buttonHtml ); @@ -52,7 +52,7 @@ public function testRenderView() $integration->setSetupType(Integration::TYPE_CONFIG); $buttonHtml = $this->editButtonBlock->render($integration); $this->assertStringContainsString('title="View"', $buttonHtml); - $this->assertStringContainsString('class="action info"', $buttonHtml); + $this->assertStringContainsString('class="' .$this->editButtonBlock->escapeHtmlAttr('action info') .'"', $buttonHtml); } /** diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php index dd4fdde250c03..b6508e3b3dfda 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Bundle/PriceFilterTest.php @@ -53,7 +53,7 @@ public function testGetFilters(): void ['is_filterable' => '1'], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ], 'Category 1' ); diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php index 07882b68d62d5..e226881b9cfcc 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/Configurable/PriceFilterTest.php @@ -76,7 +76,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$60.00</span> and above', - 'value' => '60-', + 'value' => '60-70', 'count' => 1, ], ], @@ -94,7 +94,7 @@ public function getFiltersDataProvider(): array ], [ 'label' => '<span class="price">$50.00</span> and above', - 'value' => '50-', + 'value' => '50-60', 'count' => 1, ], ], diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php index 3b2673b18635a..97928463620f4 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Category/PriceFilterTest.php @@ -71,15 +71,15 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$50.00 and above', 'value' => '50-60', 'count' => 1], ], ], 'auto_calculation_variation_with_big_price_difference' => [ 'config' => ['catalog/layered_navigation/price_range_calculation' => 'auto'], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $99.99', 'value' => '-100', 'count' => 2], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1], + ['label' => '$0.00 - $99.99', 'value' => '0-100', 'count' => 2], + ['label' => '$300.00 and above', 'value' => '300-400', 'count' => 1], ], ], 'auto_calculation_variation_with_fixed_price_step' => [ @@ -88,7 +88,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$300.00 - $399.99', 'value' => '300-400', 'count' => 1], ['label' => '$400.00 - $499.99', 'value' => '400-500', 'count' => 1], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-600', 'count' => 1], ], ], 'improved_calculation_variation_with_small_price_difference' => [ @@ -98,8 +98,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 50.00], 'expectation' => [ - ['label' => '$0.00 - $49.99', 'value' => '-50', 'count' => 2], - ['label' => '$50.00 and above', 'value' => '50-', 'count' => 1], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-50', 'count' => 2], ], ], 'improved_calculation_variation_with_big_price_difference' => [ @@ -109,8 +109,8 @@ public function getFiltersDataProvider(): array ], 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 300.00], 'expectation' => [ - ['label' => '$0.00 - $299.99', 'value' => '-300', 'count' => 2.0], - ['label' => '$300.00 and above', 'value' => '300-', 'count' => 1.0], + ['label' => '$0.00 - $19.99', 'value' => '0-20', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-300', 'count' => 2], ], ], 'manual_calculation_with_price_step_200' => [ @@ -121,7 +121,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$200.00 - $399.99', 'value' => '200-400', 'count' => 2], - ['label' => '$400.00 and above', 'value' => '400-', 'count' => 1], + ['label' => '$400.00 and above', 'value' => '400-600', 'count' => 1], ], ], 'manual_calculation_with_price_step_10' => [ @@ -132,7 +132,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 300.00, 'simple1001' => 300.00, 'simple1002' => 500.00], 'expectation' => [ ['label' => '$300.00 - $309.99', 'value' => '300-310', 'count' => 2], - ['label' => '$500.00 and above', 'value' => '500-', 'count' => 1], + ['label' => '$500.00 and above', 'value' => '500-510', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_10' => [ @@ -145,7 +145,7 @@ public function getFiltersDataProvider(): array 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], ['label' => '$20.00 - $29.99', 'value' => '20-30', 'count' => 1], - ['label' => '$30.00 and above', 'value' => '30-', 'count' => 1], + ['label' => '$30.00 and above', 'value' => '30-40', 'count' => 1], ], ], 'manual_calculation_with_number_of_intervals_2' => [ @@ -157,7 +157,7 @@ public function getFiltersDataProvider(): array 'products_data' => ['simple1000' => 10.00, 'simple1001' => 20.00, 'simple1002' => 30.00], 'expectation' => [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 2], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 2], ], ], ]; diff --git a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php index 435dd29e16dfa..760f4031b8844 100644 --- a/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php +++ b/dev/tests/integration/testsuite/Magento/LayeredNavigation/Block/Navigation/Search/Bundle/PriceFilterTest.php @@ -32,7 +32,7 @@ public function testGetFilters(): void ['is_filterable_in_search' => 1], [ ['label' => '$10.00 - $19.99', 'value' => '10-20', 'count' => 1], - ['label' => '$20.00 and above', 'value' => '20-', 'count' => 1], + ['label' => '$20.00 and above', 'value' => '20-30', 'count' => 1], ] ); } diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php new file mode 100644 index 0000000000000..2194200181729 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCatalog/Model/ResourceModel/GetAssetIdsByContentFieldTest.php @@ -0,0 +1,95 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentCatalog\Model\ResourceModel; + +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetAssetIdsByContentFieldTest + */ +class GetAssetIdsByContentFieldTest extends TestCase +{ + private const STORE_FIELD = 'store_id'; + private const STATUS_FIELD = 'content_status'; + private const STATUS_ENABLED = '1'; + private const STATUS_DISABLED = '0'; + private const FIXTURE_ASSET_ID = 2020; + private const DEFAULT_STORE_ID = '1'; + + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentField; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getAssetIdsByContentField = $objectManager->get(GetAssetIdsByContentFieldInterface::class); + } + + /** + * Test for getting asset id by category fields + * + * @dataProvider dataProvider + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/category_with_asset.php + * + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testCategoryFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Test for getting asset id by product fields + * + * @dataProvider dataProvider + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCatalog/_files/product_with_asset.php + * + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testProductFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Data provider for tests + * + * @return array + */ + public static function dataProvider(): array + { + return [ + [self::STATUS_FIELD, self::STATUS_ENABLED, [self::FIXTURE_ASSET_ID]], + [self::STATUS_FIELD, self::STATUS_DISABLED, []], + [self::STORE_FIELD, self::DEFAULT_STORE_ID, [self::FIXTURE_ASSET_ID]], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php b/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php new file mode 100644 index 0000000000000..bd6a08a7ab189 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaContentCms/Model/ResourceModel/GetAssetIdsByContentFieldTest.php @@ -0,0 +1,110 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + * + */ +declare(strict_types=1); + +namespace Magento\MediaContentCms\Model\ResourceModel; + +use Magento\Framework\Exception\InvalidArgumentException; +use Magento\MediaContentApi\Api\GetAssetIdsByContentFieldInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for GetAssetIdsByContentFieldTest + */ +class GetAssetIdsByContentFieldTest extends TestCase +{ + private const STORE_FIELD = 'store_id'; + private const STATUS_FIELD = 'content_status'; + private const STATUS_ENABLED = '1'; + private const STATUS_DISABLED = '0'; + private const FIXTURE_ASSET_ID = 2020; + private const DEFAULT_STORE_ID = '1'; + private const ADMIN_STORE_ID = '0'; + + /** + * @var GetAssetIdsByContentFieldInterface + */ + private $getAssetIdsByContentField; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->getAssetIdsByContentField = $objectManager->get(GetAssetIdsByContentFieldInterface::class); + } + + /** + * Test for getting asset id by block field + * + * @dataProvider blockDataProvider + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/block_with_asset.php + * + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testBlockFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Test for getting asset id by page field + * + * @dataProvider pageDataProvider + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + * @magentoDataFixture Magento/MediaContentCms/_files/page_with_asset.php + * + * @param string $field + * @param string $value + * @param array $expectedAssetIds + * @throws InvalidArgumentException + */ + public function testPageFields(string $field, string $value, array $expectedAssetIds): void + { + $this->assertEquals( + $expectedAssetIds, + $this->getAssetIdsByContentField->execute($field, $value) + ); + } + + /** + * Data provider for block tests + * + * @return array + */ + public static function blockDataProvider(): array + { + return [ + [self::STATUS_FIELD, self::STATUS_ENABLED, [self::FIXTURE_ASSET_ID]], + [self::STATUS_FIELD, self::STATUS_DISABLED, []], + [self::STORE_FIELD, self::DEFAULT_STORE_ID, [self::FIXTURE_ASSET_ID]], + ]; + } + + /** + * Data provider for page tests + * + * @return array + */ + public static function pageDataProvider(): array + { + return [ + [self::STATUS_FIELD, self::STATUS_ENABLED, [self::FIXTURE_ASSET_ID]], + [self::STATUS_FIELD, self::STATUS_DISABLED, []], + [self::STORE_FIELD, self::ADMIN_STORE_ID, [self::FIXTURE_ASSET_ID]], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php similarity index 62% rename from dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php rename to dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php index f63674754ea3d..bd0df51162620 100644 --- a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsBlacklistedTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/IsExcludedTest.php @@ -7,18 +7,17 @@ namespace Magento\MediaGallery\Model; -use Magento\MediaGalleryApi\Api\IsPathBlacklistedInterface; +use Magento\MediaGalleryApi\Api\IsPathExcludedInterface; use Magento\TestFramework\Helper\Bootstrap; use PHPUnit\Framework\TestCase; /** - * Test for IsPathBlacklistedInterface + * Test for IsPathExcludedInterface */ -class IsBlacklistedTest extends TestCase +class IsExcludedTest extends TestCase { - /** - * @var IsPathBlacklistedInterface + * @var IsPathExcludedInterface */ private $service; @@ -27,23 +26,23 @@ class IsBlacklistedTest extends TestCase */ protected function setUp(): void { - $this->service = Bootstrap::getObjectManager()->get(IsPathBlacklistedInterface::class); + $this->service = Bootstrap::getObjectManager()->get(IsPathExcludedInterface::class); } /** - * Testing the blacklisted paths + * Testing the excluded paths * * @param string $path - * @param bool $isBlacklisted + * @param bool $isExcluded * @dataProvider pathsProvider */ - public function testExecute(string $path, bool $isBlacklisted): void + public function testExecute(string $path, bool $isExcluded): void { - $this->assertEquals($isBlacklisted, $this->service->execute($path)); + $this->assertEquals($isExcluded, $this->service->execute($path)); } /** - * Provider of paths and if the path should be in the blacklist + * Provider of paths and if the path should be in the excluded list * * @return array */ diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php index beb146b0b816f..def1eb3231be4 100644 --- a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/ResourceModel/AssetKeywordsTest.php @@ -8,9 +8,9 @@ namespace Magento\MediaGallery\Model\ResourceModel; use Behat\Gherkin\Keywords\KeywordsInterface; -use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; -use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterface; +use Magento\MediaGalleryApi\Api\Data\AssetKeywordsInterfaceFactory; +use Magento\MediaGalleryApi\Api\Data\KeywordInterfaceFactory; use Magento\MediaGalleryApi\Api\GetAssetsByPathsInterface; use Magento\MediaGalleryApi\Api\GetAssetsKeywordsInterface; use Magento\MediaGalleryApi\Api\SaveAssetsKeywordsInterface; @@ -66,29 +66,44 @@ protected function setUp(): void * * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php * @dataProvider keywordsProvider - * @param array $keywords + * @param string[] $keywords + * @param string[] $updatedKeywords * @throws \Magento\Framework\Exception\LocalizedException */ - public function testSaveAndGetKeywords(array $keywords): void + public function testSaveAndGetKeywords(array $keywords, array $updatedKeywords): void { - $keywords = ['pear', 'plum']; - $loadedAssets = $this->getAssetsByPath->execute([self::FIXTURE_ASSET_PATH]); $this->assertCount(1, $loadedAssets); $loadedAsset = current($loadedAssets); + $this->updateAssetKeywords($loadedAsset->getId(), $keywords); + $this->updateAssetKeywords($loadedAsset->getId(), $updatedKeywords); + } + + /** + * Update Asset keywords + * + * @param int $assetId + * @param string[] $keywords + */ + private function updateAssetKeywords(int $assetId, array $keywords): void + { $assetKeywords = $this->assetsKeywordsFactory->create( [ - 'assetId' => $loadedAsset->getId(), + 'assetId' => $assetId, 'keywords' => $this->getKeywords($keywords) ] ); $this->saveAssetsKeywords->execute([$assetKeywords]); - $loadedAssetKeywords = $this->getAssetsKeywords->execute([$loadedAsset->getId()]); + $loadedAssetKeywords = $this->getAssetsKeywords->execute([$assetId]); - $this->assertCount(1, $loadedAssetKeywords); + if (empty($keywords)) { + $this->assertEmpty($loadedAssetKeywords); + return; + } + $this->assertCount(1, $loadedAssetKeywords); /** @var AssetKeywordsInterface $loadedAssetKeyword */ $loadedAssetKeyword = current($loadedAssetKeywords); @@ -115,10 +130,17 @@ public function testSaveAndGetKeywords(array $keywords): void public function keywordsProvider(): array { return [ - [['one-keyword']], - [['кириллица']], - [['plum', 'pear']], - [[]] + [['one-keyword'],['plum','orange']], + [['кириллица'],[]], + [[],['plum']], + [['plum', 'pear'],['plum','pear']], + [['plum', 'pear'],['plum','orange']], + [['plum', 'pear','grape'],['plum','orange']], + [['plum', 'pear','grape'],['mango']], + [['plum', 'pear','grape'],['orange']], + [['plum', 'pear','grape'],[]], + [['plum', 'pear'],['plum', 'pear','grape','mango','orange']], + [[],[]] ]; } diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/Model/SearchAssetsTest.php b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/SearchAssetsTest.php new file mode 100644 index 0000000000000..924c7d81365a2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/Model/SearchAssetsTest.php @@ -0,0 +1,99 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\MediaGallery\Model; + +use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\Search\FilterGroupBuilder; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\MediaGalleryApi\Api\SearchAssetsInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Verify SearchAssets By searchCriteria + */ +class SearchAssetsTest extends TestCase +{ + private const FIXTURE_ASSET_PATH = 'testDirectory/path.jpg'; + + /** + * @var SearchAssetsInterfcae + */ + private $searchAssets; + + /** + * @var SearchCriteriaBuilder + */ + private $searchCriteriaBuilder; + + /** + * @var FilterGroupBuilder + */ + private $filterGroupBuilder; + + /** + * @var FilterBuilder + */ + private $filterBuilder; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->filterBuilder = Bootstrap::getObjectManager()->get(FilterBuilder::class); + $this->filterGroupBuilder = Bootstrap::getObjectManager()->get(FilterGroupBuilder::class); + $this->searchCriteriaBuilder = Bootstrap::getObjectManager()->get(SearchCriteriaBuilder::class); + $this->searchAssets = Bootstrap::getObjectManager()->get(SearchAssetsInterface::class); + } + + /** + * Verify search asstes by searching with search criteria + * + * @dataProvider searchCriteriaProvider + * @magentoDataFixture Magento/MediaGallery/_files/media_asset.php + */ + public function testExecute(array $searchCriteriaData): void + { + $titleFilter = $this->filterBuilder->setField($searchCriteriaData['field']) + ->setConditionType($searchCriteriaData['conditionType']) + ->setValue($searchCriteriaData['value']) + ->create(); + $searchCriteria = $this->searchCriteriaBuilder + ->setFilterGroups([$this->filterGroupBuilder->setFilters([$titleFilter])->create()]) + ->create(); + + $assets = $this->searchAssets->execute($searchCriteria); + + $this->assertCount(1, $assets); + $this->assertEquals($assets[0]->getPath(), self::FIXTURE_ASSET_PATH); + } + + /** + * Search criteria params provider + * + * @return array + */ + public function searchCriteriaProvider(): array + { + return [ + [ + ['field' => 'id', 'conditionType' => 'eq', 'value' => 2020], + ], + [ + ['field' => 'title', 'conditionType' => 'fulltext', 'value' => 'Img'], + ], + [ + ['field' => 'content_type', 'conditionType' => 'eq', 'value' => 'image'] + ], + [ + ['field' => 'description', 'conditionType' => 'fulltext', 'value' => 'description'] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php b/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php index 1a2dce9e032fa..33efe102362ba 100644 --- a/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php +++ b/dev/tests/integration/testsuite/Magento/MediaGallery/_files/media_asset.php @@ -18,6 +18,7 @@ [ 'id' => 2020, 'path' => 'testDirectory/path.jpg', + 'description' => 'Description of an image', 'contentType' => 'image', 'title' => 'Img', 'source' => 'Local', diff --git a/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php b/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php index 1cefa80d8f611..cff3bae8bc2e1 100644 --- a/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php +++ b/dev/tests/integration/testsuite/Magento/MemoryUsageTest.php @@ -32,7 +32,7 @@ protected function setUp(): void */ public function testAppReinitializationNoMemoryLeak() { - $this->markTestSkipped('Test fails at Travis. Skipped until MAGETWO-47111'); + $this->markTestSkipped('Skipped until MAGETWO-47111'); $this->_deallocateUnusedMemory(); $actualMemoryUsage = $this->_helper->getRealMemoryUsage(); diff --git a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php index 8fafec2ee091f..dca0ef14663f4 100644 --- a/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php +++ b/dev/tests/integration/testsuite/Magento/MessageQueue/Model/Cron/ConsumersRunnerTest.php @@ -127,6 +127,7 @@ public function testSpecificConsumerAndRerun() $specificConsumer = 'exportProcessor'; $config = $this->config; $config['cron_consumers_runner'] = ['consumers' => [$specificConsumer], 'max_messages' => 0]; + $config['queue'] = ['only_spawn_when_message_available' => 0]; $this->writeConfig($config); $this->reRunConsumersAndCheckLocks($specificConsumer); $this->reRunConsumersAndCheckLocks($specificConsumer); diff --git a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php index 8d6caad63ab77..74160f9460851 100644 --- a/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php +++ b/dev/tests/integration/testsuite/Magento/Newsletter/Controller/Adminhtml/NewsletterTemplateTest.php @@ -62,11 +62,19 @@ protected function tearDown(): void public function testSaveActionCreateNewTemplateAndVerifySuccessMessage() { $this->getRequest()->setParam('id', $this->model->getId()); + $this->getRequest()->setParam('is_legacy', 1); + $this->dispatch('backend/newsletter/template/save'); + /** * Check that errors was generated and set to session */ $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); + + $this->model->load($this->getRequest()->getPostValue('code'), 'template_code'); + + $this->assertEquals(0, $this->model->getIsLegacy()); + /** * Check that success message is set */ @@ -90,6 +98,8 @@ public function testSaveActionEditTemplateAndVerifySuccessMessage() $this->assertEquals('some_unique_code', $this->model->getTemplateCode()); $this->getRequest()->setParam('id', $this->model->getId()); + $this->getRequest()->setParam('is_legacy', 1); + $this->dispatch('backend/newsletter/template/save'); /** @@ -97,6 +107,10 @@ public function testSaveActionEditTemplateAndVerifySuccessMessage() */ $this->assertSessionMessages($this->isEmpty(), \Magento\Framework\Message\MessageInterface::TYPE_ERROR); + $this->model->load($this->getRequest()->getPostValue('code'), 'template_code'); + + $this->assertEquals(0, $this->model->getIsLegacy()); + /** * Check that success message is set */ diff --git a/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php b/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php index f84b493c43f29..c08a31c909d8a 100644 --- a/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php +++ b/dev/tests/integration/testsuite/Magento/Payment/Block/Transparent/IframeTest.php @@ -37,7 +37,7 @@ public function testToHtml($xssString) $content = $block->toHtml(); $this->assertStringNotContainsString($xssString, $content, 'Params must be escaped'); - $this->assertStringContainsString($block->escapeXssInUrl($xssString), $content, 'Content must be present'); + $this->assertStringContainsString($block->escapeJs($xssString), $content, 'Content must be present'); } /** diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php index 9821a148589fd..05e572f5b64f0 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/NvpTest.php @@ -95,7 +95,7 @@ public function testRequestTotalsAndLineItemsWithFPT() . '&SHIPPINGAMT=0.00&ITEMAMT=112.70&TAXAMT=0.00' . '&L_NAME0=Simple+Product+FPT&L_QTY0=1&L_AMT0=100.00' . '&L_NAME1=FPT&L_QTY1=1&L_AMT1=12.70' - . '&METHOD=SetExpressCheckout&VERSION=72.0&BUTTONSOURCE=Magento_Cart_'; + . '&METHOD=SetExpressCheckout&VERSION=72.0&BUTTONSOURCE=Magento_2_'; $this->httpClient->method('write') ->with( @@ -146,7 +146,7 @@ public function testCallRefundTransaction() $httpQuery = 'TRANSACTIONID=fooTransactionId&REFUNDTYPE=Partial' .'&CURRENCYCODE=USD&AMT=145.98&METHOD=RefundTransaction' - .'&VERSION=72.0&BUTTONSOURCE=Magento_Cart_'; + .'&VERSION=72.0&BUTTONSOURCE=Magento_2_'; $this->httpClient->expects($this->once())->method('write') ->with( diff --git a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php index 274475b35ba6d..c10785624fc59 100644 --- a/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php +++ b/dev/tests/integration/testsuite/Magento/Paypal/Model/Api/PayflowNvpTest.php @@ -95,7 +95,7 @@ public function testRequestLineItems() . 'L_NAME1=Simple 2&L_QTY1=2&L_COST1=9.69&' . 'L_NAME2=Simple 3&L_QTY2=3&L_COST2=11.69&' . 'L_NAME3=Discount&L_QTY3=1&L_COST3=-10.00&' - . 'TRXTYPE=A&ACTION=S&BUTTONSOURCE=Magento_Cart_'; + . 'TRXTYPE=A&ACTION=S&BUTTONSOURCE=Magento_2_'; $this->httpClient->method('write') ->with( diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php index a8a12650f9935..aebe8b4e3ef47 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowLinkTest.php @@ -119,14 +119,14 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void cart_id: "$cartId" payment_method: { code: "$paymentMethod" - payflow_link: + payflow_link: { cancel_url:"paypal/payflow/cancelPayment" return_url:"paypal/payflow/returnUrl" error_url:"paypal/payflow/errorUrl" } } - }) { + }) { cart { selected_payment_method { code @@ -142,7 +142,7 @@ public function testResolvePlaceOrderWithPayflowLinkForCustomer(): void QUERY; $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); - $button = 'Magento_Cart_' . $productMetadata->getEdition(); + $button = 'Magento_2_' . $productMetadata->getEdition(); $payflowLinkResponse = new DataObject( [ diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProCCVaultTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProCCVaultTest.php new file mode 100644 index 0000000000000..3ebfaf8890edb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/PlaceOrderWithPayflowProCCVaultTest.php @@ -0,0 +1,363 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Customer; + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterface; +use Magento\Checkout\Api\Data\ShippingInformationInterfaceFactory; +use Magento\Checkout\Api\ShippingInformationManagementInterface; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\DataObject; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Integration\Model\Oauth\Token; +use Magento\PaypalGraphQl\PaypalPayflowProAbstractTest; +use Magento\Quote\Api\BillingAddressManagementInterface; +use Magento\Quote\Api\CartManagementInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Model\QuoteFactory; +use Magento\Quote\Model\QuoteIdMask; +use Magento\Quote\Model\QuoteIdMaskFactory; +use Magento\Quote\Model\QuoteIdToMaskedQuoteId; +use Magento\Quote\Model\ResourceModel\Quote as QuoteResource; +use Magento\Quote\Model\ShippingAddressManagementInterface; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\Vault\Model\PaymentTokenRepository; + +/** + * End to end place order test using payflowpro_cc_vault via graphql endpoint for customer + * + * @magentoAppArea graphql + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class PlaceOrderWithPayflowProCCVaultTest extends PaypalPayflowProAbstractTest +{ + /** + * @var SerializerInterface + */ + private $json; + + /** + * @var QuoteIdToMaskedQuoteId + */ + private $quoteIdToMaskedId; + + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->objectManager->get(SerializerInterface::class); + $this->quoteIdToMaskedId = $this->objectManager->get(QuoteIdToMaskedQuoteId::class); + } + + /** + * Place order use payflowpro method and save cart data to future + * + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * + * @return void + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testPlaceOrderWithCCVault(): void + { + $this->placeOrderPayflowPro('is_active_payment_token_enabler: true'); + $publicHash = $this->getVaultCartData()->getPublicHash(); + /** @var CartManagementInterface $cartManagement */ + $cartManagement = $this->objectManager->get(CartManagementInterface::class); + /** @var CartRepositoryInterface $cartRepository */ + $cartRepository = $this->objectManager->get(CartRepositoryInterface::class); + /** @var QuoteIdMaskFactory $quoteIdMaskFactory */ + $quoteIdMaskFactory = $this->objectManager->get(QuoteIdMaskFactory::class); + $cartId = $cartManagement->createEmptyCartForCustomer(1); + $cart = $cartRepository->get($cartId); + $cart->setReservedOrderId('test_quote_1'); + $cartRepository->save($cart); + /** @var QuoteIdMask $quoteIdMask */ + $quoteIdMask = $quoteIdMaskFactory->create(); + $quoteIdMask->setQuoteId($cartId) + ->save(); + + $reservedQuoteId = 'test_quote_1'; + $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); + $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); + + /** @var ProductRepositoryInterface $productRepository */ + $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + /** @var QuoteFactory $quoteFactory */ + $quoteFactory = $this->objectManager->get(QuoteFactory::class); + /** @var QuoteResource $quoteResource */ + $quoteResource = $this->objectManager->get(QuoteResource::class); + $product = $productRepository->get('simple_product'); + $quote = $quoteFactory->create(); + $quoteResource->load($quote, 'test_quote_1', 'reserved_order_id'); + $quote->addProduct($product, 2); + $cartRepository->save($quote); + + /** @var AddressInterfaceFactory $quoteAddressFactory */ + $quoteAddressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + /** @var DataObjectHelper $dataObjectHelper */ + $dataObjectHelper = $this->objectManager->get(DataObjectHelper::class); + /** @var ShippingAddressManagementInterface $shippingAddressManagement */ + $shippingAddressManagement = $this->objectManager->get(ShippingAddressManagementInterface::class); + + $quoteAddressData = [ + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => '75477', + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_COMPANY => 'CompanyName', + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_REGION_ID => 1, + ]; + $quoteAddress = $quoteAddressFactory->create(); + $dataObjectHelper->populateWithArray($quoteAddress, $quoteAddressData, AddressInterfaceFactory::class); + + $quote = $quoteFactory->create(); + $quoteResource->load($quote, 'test_quote_1', 'reserved_order_id'); + $shippingAddressManagement->assign($quote->getId(), $quoteAddress); + + /** @var BillingAddressManagementInterface $billingAddressManagement */ + $billingAddressManagement = $this->objectManager->get(BillingAddressManagementInterface::class); + $billingAddressManagement->assign($quote->getId(), $quoteAddress); + + /** @var ShippingInformationInterfaceFactory $shippingInformationFactory */ + $shippingInformationFactory = $this->objectManager->get(ShippingInformationInterfaceFactory::class); + /** @var ShippingInformationManagementInterface $shippingInformationManagement */ + $shippingInformationManagement = $this->objectManager->get(ShippingInformationManagementInterface::class); + $quoteAddress = $quote->getShippingAddress(); + + /** @var ShippingInformationInterface $shippingInformation */ + $shippingInformation = $shippingInformationFactory->create([ + 'data' => [ + ShippingInformationInterface::SHIPPING_ADDRESS => $quoteAddress, + ShippingInformationInterface::SHIPPING_CARRIER_CODE => 'flatrate', + ShippingInformationInterface::SHIPPING_METHOD_CODE => 'flatrate', + ], + ]); + $shippingInformationManagement->saveAddressInformation($quote->getId(), $shippingInformation); + + $secondQuery = <<<QUERY +mutation { +setPaymentMethodOnCart(input: { +payment_method: { + code: "payflowpro_cc_vault", + payflowpro_cc_vault: { + public_hash:"{$publicHash}" + } +}, +cart_id: "{$cartId}"}) +{ +cart { + selected_payment_method {code} + } +} +placeOrder(input: {cart_id: "{$cartId}"}) { +order {order_number} + } +} +QUERY; + /** @var Token $tokenModel */ + $tokenModel = $this->objectManager->create(Token::class); + $customerToken = $tokenModel->createCustomerToken(1)->getToken(); + + $requestHeaders = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $customerToken + ]; + $vaultResponse = $this->graphQlRequest->send($secondQuery, [], '', $requestHeaders); + + $responseData = $this->json->unserialize($vaultResponse->getContent()); + $this->assertArrayHasKey('data', $responseData); + $this->assertTrue( + isset($responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code']) + ); + $this->assertEquals( + 'payflowpro_cc_vault', + $responseData['data']['setPaymentMethodOnCart']['cart']['selected_payment_method']['code'] + ); + $this->assertTrue( + isset($responseData['data']['placeOrder']['order']['order_number']) + ); + $this->assertEquals( + 'test_quote_1', + $responseData['data']['placeOrder']['order']['order_number'] + ); + } + + /** + * @param $isActivePaymentTokenEnabler + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function placeOrderPayflowPro($isActivePaymentTokenEnabler) + { + $paymentMethod = 'payflowpro'; + $this->enablePaymentMethod($paymentMethod); + $this->enablePaymentMethod('payflowpro_cc_vault'); + $reservedQuoteId = 'test_quote'; + + $payload = 'BILLTOCITY=CityM&AMT=0.00&BILLTOSTREET=Green+str,+67&VISACARDLEVEL=12&SHIPTOCITY=CityM' + . '&NAMETOSHIP=John+Smith&ZIP=75477&BILLTOLASTNAME=Smith&BILLTOFIRSTNAME=John' + . '&RESPMSG=Verified&PROCCVV2=M&STATETOSHIP=AL&NAME=John+Smith&BILLTOZIP=75477&CVV2MATCH=Y' + . '&PNREF=B70CCC236815&ZIPTOSHIP=75477&SHIPTOCOUNTRY=US&SHIPTOSTREET=Green+str,+67&CITY=CityM' + . '&HOSTCODE=A&LASTNAME=Smith&STATE=AL&SECURETOKEN=MYSECURETOKEN&CITYTOSHIP=CityM&COUNTRYTOSHIP=US' + . '&AVSDATA=YNY&ACCT=1111&AUTHCODE=111PNI&FIRSTNAME=John&RESULT=0&IAVS=N&POSTFPSMSG=No+Rules+Triggered&' + . 'BILLTOSTATE=AL&BILLTOCOUNTRY=US&EXPDATE=0222&CARDTYPE=0&PREFPSMSG=No+Rules+Triggered&SHIPTOZIP=75477&' + . 'PROCAVS=A&COUNTRY=US&AVSZIP=N&ADDRESS=Green+str,+67&BILLTONAME=John+Smith&' + . 'ADDRESSTOSHIP=Green+str,+67&' + . 'AVSADDR=Y&SECURETOKENID=MYSECURETOKENID&SHIPTOSTATE=AL&TRANSTIME=2019-06-24+07%3A53%3A10'; + + $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); + $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + payment_method: { + code: "{$paymentMethod}", + payflowpro: { + {$isActivePaymentTokenEnabler} + cc_details: { + cc_exp_month: 12, + cc_exp_year: 2030, + cc_last_4: 1111, + cc_type: "IV", + } + } + }, + cart_id: "{$cartId}"}) + { + cart { + selected_payment_method { + code + } + } + } + createPayflowProToken( + input: { + cart_id:"{$cartId}", + urls: { + cancel_url: "paypal/transparent/cancel/" + error_url: "paypal/transparent/error/" + return_url: "paypal/transparent/response/" + } + } + ) { + response_message + result + result_code + secure_token + secure_token_id + } + handlePayflowProResponse(input: { + paypal_payload: "$payload", + cart_id: "{$cartId}" + }) + { + cart { + selected_payment_method { + code + } + } + } + placeOrder(input: {cart_id: "{$cartId}"}) { + order { + order_number + } + } +} +QUERY; + + /** @var Token $tokenModel */ + $tokenModel = $this->objectManager->create(Token::class); + $customerToken = $tokenModel->createCustomerToken(1)->getToken(); + + $requestHeaders = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $customerToken + ]; + $paypalResponse = new DataObject( + [ + 'result' => '0', + 'securetoken' => 'mysecuretoken', + 'securetokenid' => 'mysecuretokenid', + 'respmsg' => 'Approved', + 'result_code' => '0', + ] + ); + + $this->gatewayMock + ->method('postRequest') + ->willReturn($paypalResponse); + + $this->gatewayMock + ->method('postRequest') + ->willReturn( + new DataObject( + [ + 'result' => '0', + 'pnref' => 'A70AAC2378BA', + 'respmsg' => 'Approved', + 'authcode' => '647PNI', + 'avsaddr' => 'Y', + 'avszip' => 'N', + 'hostcode' => 'A', + 'procavs' => 'A', + 'visacardlevel' => '12', + 'transtime' => '2019-06-24 10:12:03', + 'firstname' => 'Cristian', + 'lastname' => 'Partica', + 'amt' => '14.99', + 'acct' => '1111', + 'expdate' => '0221', + 'cardtype' => '0', + 'iavs' => 'N', + 'result_code' => '0', + ] + ) + ); + + $response = $this->graphQlRequest->send($query, [], '', $requestHeaders); + + return $this->json->unserialize($response->getContent()); + } + + /** + * Get saved cart data + * + * @return PaymentTokenInterface + */ + private function getVaultCartData() + { + /** @var PaymentTokenManagement $tokenManagement */ + $tokenManagement = $this->objectManager->get(PaymentTokenManagement::class); + $token = $tokenManagement->getByGatewayToken( + 'B70CCC236815', + 'payflowpro', + 1 + ); + /** @var PaymentTokenRepository $tokenRepository */ + $tokenRepository = $this->objectManager->get(PaymentTokenRepository::class); + return $tokenRepository->getById($token->getEntityId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/SaveCartDataWithPayflowProTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/SaveCartDataWithPayflowProTest.php new file mode 100644 index 0000000000000..11bd306211b9a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Customer/SaveCartDataWithPayflowProTest.php @@ -0,0 +1,258 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PaypalGraphQl\Model\Resolver\Customer; + +use Magento\Integration\Model\Oauth\Token; +use Magento\PaypalGraphQl\PaypalPayflowProAbstractTest; +use Magento\Framework\Serialize\SerializerInterface; +use Magento\Quote\Model\QuoteIdToMaskedQuoteId; +use Magento\Framework\DataObject; +use Magento\Vault\Api\Data\PaymentTokenInterface; +use Magento\Vault\Model\PaymentTokenManagement; +use Magento\Vault\Model\PaymentTokenRepository; + +/** + * End to end place order test using payflowpro via graphql endpoint for customer + * + * @magentoAppArea graphql + */ +class SaveCartDataWithPayflowProTest extends PaypalPayflowProAbstractTest +{ + /** + * @var SerializerInterface + */ + private $json; + + /** + * @var QuoteIdToMaskedQuoteId + */ + private $quoteIdToMaskedId; + + protected function setUp(): void + { + parent::setUp(); + + $this->json = $this->objectManager->get(SerializerInterface::class); + $this->quoteIdToMaskedId = $this->objectManager->get(QuoteIdToMaskedQuoteId::class); + } + + /** + * Place order use payflowpro method and save cart data to future + * + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * + * @return void + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testPlaceOrderAndSaveDataForFuturePayflowPro(): void + { + $responseData = $this->placeOrderPayflowPro('is_active_payment_token_enabler: true'); + $this->assertArrayHasKey('data', $responseData); + $this->assertArrayHasKey('createPayflowProToken', $responseData['data']); + $this->assertNotEmpty($this->getVaultCartData()->getPublicHash()); + $this->assertNotEmpty($this->getVaultCartData()->getTokenDetails()); + $this->assertNotEmpty($this->getVaultCartData()->getGatewayToken()); + $this->assertTrue($this->getVaultCartData()->getIsActive()); + $this->assertTrue($this->getVaultCartData()->getIsVisible()); + } + + /** + * Place order use payflowpro method and not save cart data to future + * + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Sales/_files/default_rollback.php + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/GraphQl/Catalog/_files/simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/customer/create_empty_cart.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/add_simple_product.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_shipping_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_new_billing_address.php + * @magentoDataFixture Magento/GraphQl/Quote/_files/set_flatrate_shipping_method.php + * + * @return void + */ + public function testPlaceOrderAndNotSaveDataForFuturePayflowPro(): void + { + $responseData = $this->placeOrderPayflowPro('is_active_payment_token_enabler: false'); + $this->assertArrayHasKey('data', $responseData); + $this->assertArrayHasKey('createPayflowProToken', $responseData['data']); + $this->assertNotEmpty($this->getVaultCartData()->getPublicHash()); + $this->assertNotEmpty($this->getVaultCartData()->getTokenDetails()); + $this->assertNotEmpty($this->getVaultCartData()->getGatewayToken()); + $this->assertTrue($this->getVaultCartData()->getIsActive()); + $this->assertFalse($this->getVaultCartData()->getIsVisible()); + } + + /** + * @param $isActivePaymentTokenEnabler + * + * @return array + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + private function placeOrderPayflowPro($isActivePaymentTokenEnabler) + { + $paymentMethod = 'payflowpro'; + $this->enablePaymentMethod($paymentMethod); + $this->enablePaymentMethod('payflowpro_cc_vault'); + $reservedQuoteId = 'test_quote'; + + $payload = 'BILLTOCITY=CityM&AMT=0.00&BILLTOSTREET=Green+str,+67&VISACARDLEVEL=12&SHIPTOCITY=CityM' + . '&NAMETOSHIP=John+Smith&ZIP=75477&BILLTOLASTNAME=Smith&BILLTOFIRSTNAME=John' + . '&RESPMSG=Verified&PROCCVV2=M&STATETOSHIP=AL&NAME=John+Smith&BILLTOZIP=75477&CVV2MATCH=Y' + . '&PNREF=B70CCC236815&ZIPTOSHIP=75477&SHIPTOCOUNTRY=US&SHIPTOSTREET=Green+str,+67&CITY=CityM' + . '&HOSTCODE=A&LASTNAME=Smith&STATE=AL&SECURETOKEN=MYSECURETOKEN&CITYTOSHIP=CityM&COUNTRYTOSHIP=US' + . '&AVSDATA=YNY&ACCT=1111&AUTHCODE=111PNI&FIRSTNAME=John&RESULT=0&IAVS=N&POSTFPSMSG=No+Rules+Triggered&' + . 'BILLTOSTATE=AL&BILLTOCOUNTRY=US&EXPDATE=0222&CARDTYPE=0&PREFPSMSG=No+Rules+Triggered&SHIPTOZIP=75477&' + . 'PROCAVS=A&COUNTRY=US&AVSZIP=N&ADDRESS=Green+str,+67&BILLTONAME=John+Smith&' + . 'ADDRESSTOSHIP=Green+str,+67&' + . 'AVSADDR=Y&SECURETOKENID=MYSECURETOKENID&SHIPTOSTATE=AL&TRANSTIME=2019-06-24+07%3A53%3A10'; + + $cart = $this->getQuoteByReservedOrderId($reservedQuoteId); + $cartId = $this->quoteIdToMaskedId->execute((int)$cart->getId()); + + $query = <<<QUERY +mutation { + setPaymentMethodOnCart(input: { + payment_method: { + code: "{$paymentMethod}", + payflowpro: { + {$isActivePaymentTokenEnabler} + cc_details: { + cc_exp_month: 12, + cc_exp_year: 2030, + cc_last_4: 1111, + cc_type: "IV", + } + } + }, + cart_id: "{$cartId}"}) + { + cart { + selected_payment_method { + code + } + } + } + createPayflowProToken( + input: { + cart_id:"{$cartId}", + urls: { + cancel_url: "paypal/transparent/cancel/" + error_url: "paypal/transparent/error/" + return_url: "paypal/transparent/response/" + } + } + ) { + response_message + result + result_code + secure_token + secure_token_id + } + handlePayflowProResponse(input: { + paypal_payload: "$payload", + cart_id: "{$cartId}" + }) + { + cart { + selected_payment_method { + code + } + } + } + placeOrder(input: {cart_id: "{$cartId}"}) { + order { + order_number + } + } +} +QUERY; + + /** @var Token $tokenModel */ + $tokenModel = $this->objectManager->create(Token::class); + $customerToken = $tokenModel->createCustomerToken(1)->getToken(); + + $requestHeaders = [ + 'Content-Type' => 'application/json', + 'Accept' => 'application/json', + 'Authorization' => 'Bearer ' . $customerToken + ]; + $paypalResponse = new DataObject( + [ + 'result' => '0', + 'securetoken' => 'mysecuretoken', + 'securetokenid' => 'mysecuretokenid', + 'respmsg' => 'Approved', + 'result_code' => '0', + ] + ); + + $this->gatewayMock + ->method('postRequest') + ->willReturn($paypalResponse); + + $this->gatewayMock + ->method('postRequest') + ->willReturn( + new DataObject( + [ + 'result' => '0', + 'pnref' => 'A70AAC2378BA', + 'respmsg' => 'Approved', + 'authcode' => '647PNI', + 'avsaddr' => 'Y', + 'avszip' => 'N', + 'hostcode' => 'A', + 'procavs' => 'A', + 'visacardlevel' => '12', + 'transtime' => '2019-06-24 10:12:03', + 'firstname' => 'Cristian', + 'lastname' => 'Partica', + 'amt' => '14.99', + 'acct' => '1111', + 'expdate' => '0221', + 'cardtype' => '0', + 'iavs' => 'N', + 'result_code' => '0', + ] + ) + ); + + $response = $this->graphQlRequest->send($query, [], '', $requestHeaders); + + return $this->json->unserialize($response->getContent()); + } + + /** + * Get saved cart data + * + * @return PaymentTokenInterface + */ + private function getVaultCartData() + { + /** @var PaymentTokenManagement $tokenManagement */ + $tokenManagement = $this->objectManager->get(PaymentTokenManagement::class); + $token = $tokenManagement->getByGatewayToken( + 'B70CCC236815', + 'payflowpro', + 1 + ); + /** @var PaymentTokenRepository $tokenRepository */ + $tokenRepository = $this->objectManager->get(PaymentTokenRepository::class); + return $tokenRepository->getById($token->getEntityId()); + } +} diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php index 797876cc2318f..a0776250cfc56 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPayflowLinkTest.php @@ -117,14 +117,14 @@ public function testResolvePlaceOrderWithPayflowLink(): void cart_id: "$cartId" payment_method: { code: "$paymentMethod" - payflow_link: + payflow_link: { cancel_url:"paypal/payflow/cancel" return_url:"paypal/payflow/return" error_url:"paypal/payflow/error" } } - }) { + }) { cart { selected_payment_method { code @@ -140,7 +140,7 @@ public function testResolvePlaceOrderWithPayflowLink(): void QUERY; $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); - $button = 'Magento_Cart_' . $productMetadata->getEdition(); + $button = 'Magento_2_' . $productMetadata->getEdition(); $payflowLinkResponse = new DataObject( [ @@ -219,14 +219,14 @@ public function testResolveWithPayflowLinkDeclined(): void cart_id: "$cartId" payment_method: { code: "$paymentMethod" - payflow_link: + payflow_link: { cancel_url:"paypal/payflow/cancelPayment" return_url:"paypal/payflow/returnUrl" error_url:"paypal/payflow/returnUrl" } } - }) { + }) { cart { selected_payment_method { code diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php index 5de1ded43405a..468d9036992b9 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/Model/Resolver/Guest/PlaceOrderWithPaymentsAdvancedTest.php @@ -108,7 +108,7 @@ public function testResolvePlaceOrderWithPaymentsAdvanced(): void $cartId = $this->getMaskedQuoteIdByReservedOrderId->execute('test_quote'); $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); - $button = 'Magento_Cart_' . $productMetadata->getEdition(); + $button = 'Magento_2_' . $productMetadata->getEdition(); $payflowLinkResponse = new DataObject( [ @@ -256,7 +256,7 @@ private function setPaymentMethodAndPlaceOrder(string $cartId, string $paymentMe error_url:"paypal/payflowadvanced/customerror" } } - }) { + }) { cart { selected_payment_method { code @@ -300,7 +300,7 @@ private function setPaymentMethodWithInValidUrl(string $cartId, string $paymentM error_url:"paypal/payflowadvanced/error" } } - }) { + }) { cart { selected_payment_method { code diff --git a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php index 7a622ab15814e..6c4f96121a96d 100644 --- a/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php +++ b/dev/tests/integration/testsuite/Magento/PaypalGraphQl/_files/paypal_place_order_request.php @@ -13,7 +13,7 @@ $notifyUrl = $url->getUrl('paypal/ipn/'); $productMetadata = ObjectManager::getInstance()->get(ProductMetadataInterface::class); -$button = 'Magento_Cart_' . $productMetadata->getEdition(); +$button = 'Magento_2_' . $productMetadata->getEdition(); return [ 'TOKEN' => $token, diff --git a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php index c2e499c455983..35f2283494b1c 100644 --- a/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Persistent/Observer/SynchronizePersistentOnLoginObserverTest.php @@ -3,20 +3,35 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Persistent\Observer; +use DateTime; +use DateTimeZone; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Event; +use Magento\Framework\Event\Observer; +use Magento\Framework\ObjectManagerInterface; +use Magento\Persistent\Model\Session; +use Magento\Persistent\Model\SessionFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + /** * @magentoDataFixture Magento/Customer/_files/customer.php + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class SynchronizePersistentOnLoginObserverTest extends \PHPUnit\Framework\TestCase +class SynchronizePersistentOnLoginObserverTest extends TestCase { /** - * @var \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver + * @var SynchronizePersistentOnLoginObserver */ protected $_model; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ protected $_objectManager; @@ -30,45 +45,70 @@ class SynchronizePersistentOnLoginObserverTest extends \PHPUnit\Framework\TestCa */ protected $_customerSession; + /** + * @var CustomerInterface + */ + private $customer; + + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->_objectManager = Bootstrap::getObjectManager(); $this->_persistentSession = $this->_objectManager->get(\Magento\Persistent\Helper\Session::class); $this->_customerSession = $this->_objectManager->get(\Magento\Customer\Model\Session::class); $this->_model = $this->_objectManager->create( - \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver::class, + SynchronizePersistentOnLoginObserver::class, [ 'persistentSession' => $this->_persistentSession, 'customerSession' => $this->_customerSession ] ); + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); + $this->customer = $customerRepository->getById(1); } /** - * @covers \Magento\Persistent\Observer\SynchronizePersistentOnLoginObserver::execute + * Test that persistent session is created on customer login */ - public function testSynchronizePersistentOnLogin() + public function testSynchronizePersistentOnLogin(): void { - $event = new \Magento\Framework\Event(); - $observer = new \Magento\Framework\Event\Observer(['event' => $event]); - - /** @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository */ - $customerRepository = $this->_objectManager->create( - \Magento\Customer\Api\CustomerRepositoryInterface::class - ); - - /** @var $customer \Magento\Customer\Api\Data\CustomerInterface */ - $customer = $customerRepository->getById(1); - $event->setData('customer', $customer); + $sessionModel = $this->_objectManager->create(Session::class); + $sessionModel->loadByCustomerId($this->customer->getId()); + $this->assertNull($sessionModel->getCustomerId()); + $event = new Event(); + $observer = new Observer(['event' => $event]); + $event->setData('customer', $this->customer); $this->_persistentSession->setRememberMeChecked(true); $this->_model->execute($observer); - // check that persistent session has been stored for Customer - /** @var \Magento\Persistent\Model\Session $sessionModel */ - $sessionModel = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Persistent\Model\Session::class - ); + /** @var Session $sessionModel */ + $sessionModel = $this->_objectManager->create(Session::class); + $sessionModel->loadByCustomerId($this->customer->getId()); + $this->assertEquals($this->customer->getId(), $sessionModel->getCustomerId()); + } + + /** + * Test that expired persistent session is renewed on customer login + */ + public function testExpiredPersistentSessionShouldBeRenewedOnLogin(): void + { + $lastUpdatedAt = (new DateTime('-1day'))->setTimezone(new DateTimeZone('UTC'))->format('Y-m-d H:i:s'); + /** @var Session $sessionModel */ + $sessionModel = $this->_objectManager->create(SessionFactory::class)->create(); + $sessionModel->setCustomerId($this->customer->getId()); + $sessionModel->setUpdatedAt($lastUpdatedAt); + $sessionModel->save(); + $event = new Event(); + $observer = new Observer(['event' => $event]); + $event->setData('customer', $this->customer); + $this->_persistentSession->setRememberMeChecked(true); + $this->_model->execute($observer); + /** @var Session $sessionModel */ + $sessionModel = $this->_objectManager->create(Session::class); $sessionModel->loadByCustomerId(1); - $this->assertEquals(1, $sessionModel->getCustomerId()); + $this->assertGreaterThan($lastUpdatedAt, $sessionModel->getUpdatedAt()); } } diff --git a/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php b/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php index e7a1de90fc933..53ed800dbdb31 100644 --- a/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Phpserver/PhpserverTest.php @@ -5,6 +5,9 @@ */ namespace Magento\Phpserver; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; + /** * @magentoAppIsolation enabled * @@ -19,47 +22,54 @@ class PhpserverTest extends \PHPUnit\Framework\TestCase { const BASE_URL = '127.0.0.1:8082'; - private static $serverPid; + /** + * @var Process + */ + private $serverProcess; /** * @var \Laminas\Http\Client */ private $httpClient; + private function getUrl($url) + { + return sprintf('http://%s/%s', self::BASE_URL, ltrim($url, '/')); + } + /** - * Instantiate phpserver in the pub folder + * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ - public static function setUpBeforeClass(): void + protected function setUp(): void { - if (!(defined('TRAVIS') && TRAVIS === true)) { - self::markTestSkipped('Travis environment test'); - } - $return = []; + $this->httpClient = new \Laminas\Http\Client(null, ['timeout' => 10]); - $baseDir = __DIR__ . '/../../../../../../'; + /** @var Process $process */ + $phpBinaryFinder = new PhpExecutableFinder(); + $phpBinaryPath = $phpBinaryFinder->find(); $command = sprintf( - 'cd %s && php -S %s -t ./pub/ ./phpserver/router.php >/dev/null 2>&1 & echo $!', - $baseDir, - static::BASE_URL + "%s -S %s -t ./pub ./phpserver/router.php", + $phpBinaryPath, + self::BASE_URL ); - // phpcs:ignore - exec($command, $return); - static::$serverPid = (int) $return[0]; - } - - private function getUrl($url) - { - return sprintf('http://%s/%s', self::BASE_URL, ltrim($url, '/')); + $this->serverProcess = Process::fromShellCommandline( + $command, + realpath(__DIR__ . '/../../../../../../') + ); + $this->serverProcess->start(); + $this->serverProcess->waitUntil(function ($type, $output) { + return strpos($output, "Development Server") !== false; + }); } - protected function setUp(): void + protected function tearDown(): void { - $this->httpClient = new \Laminas\Http\Client(null, ['timeout' => 10]); + $this->serverProcess->stop(); } public function testServerHasPid() { - $this->assertTrue(static::$serverPid > 0); + $this->assertTrue($this->serverProcess->getPid() > 0); } public function testServerResponds() @@ -86,9 +96,4 @@ public function testStaticImageFile() $this->assertFalse($response->isClientError()); $this->assertStringStartsWith('image/gif', $response->getHeaders()->get('Content-Type')->getMediaType()); } - - public static function tearDownAfterClass(): void - { - posix_kill(static::$serverPid, SIGKILL); - } } diff --git a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php index 5f2cee2368c98..94fe0e85a8ddf 100644 --- a/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php +++ b/dev/tests/integration/testsuite/Magento/ProductAlert/Model/EmailTest.php @@ -3,24 +3,32 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); namespace Magento\ProductAlert\Model; use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Catalog\Model\Product; use Magento\Customer\Api\AccountManagementInterface; use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Helper\View; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\MailException; use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Store\Model\StoreManagerInterface; use Magento\Store\Model\Website; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Mail\Template\TransportBuilderMock; +use Magento\TestFramework\ObjectManager; +use PHPUnit\Framework\TestCase; /** * Test for Magento\ProductAlert\Model\Email class. * * @magentoAppIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class EmailTest extends \PHPUnit\Framework\TestCase +class EmailTest extends TestCase { /** * @var Email @@ -28,7 +36,7 @@ class EmailTest extends \PHPUnit\Framework\TestCase protected $_emailModel; /** - * @var \Magento\TestFramework\ObjectManager + * @var ObjectManager */ protected $_objectManager; @@ -38,7 +46,7 @@ class EmailTest extends \PHPUnit\Framework\TestCase protected $customerAccountManagement; /** - * @var \Magento\Customer\Helper\View + * @var View */ protected $_customerViewHelper; @@ -62,11 +70,11 @@ class EmailTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - $this->_objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + $this->_objectManager = Bootstrap::getObjectManager(); $this->customerAccountManagement = $this->_objectManager->create( AccountManagementInterface::class ); - $this->_customerViewHelper = $this->_objectManager->create(\Magento\Customer\Helper\View::class); + $this->_customerViewHelper = $this->_objectManager->create(View::class); $this->transportBuilder = $this->_objectManager->get(TransportBuilderMock::class); $this->customerRepository = $this->_objectManager->create(CustomerRepositoryInterface::class); $this->productRepository = $this->_objectManager->create(ProductRepositoryInterface::class); @@ -100,7 +108,7 @@ public function testSend($isCustomerIdUsed) $this->_emailModel->setCustomerData($customer); } - /** @var \Magento\Catalog\Model\Product $product */ + /** @var Product $product */ $product = $this->productRepository->getById(1); $this->_emailModel->addPriceProduct($product); @@ -165,4 +173,36 @@ public function testEmailForDifferentCustomers(): void ); } } + + /** + * @magentoAppArea frontend + * @magentoDataFixture Magento/Customer/_files/customer.php + * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @magentoDataFixture Magento/Store/_files/second_store_with_second_identity.php + */ + public function testScopedMessageIdentity() + { + /** @var Website $website */ + $website = $this->_objectManager->create(Website::class); + $website->load(1); + $this->_emailModel->setWebsite($website); + + /** @var StoreManagerInterface $storeManager */ + $storeManager = $this->_objectManager->create(StoreManagerInterface::class); + $store = $storeManager->getStore('fixture_second_store'); + $this->_emailModel->setStoreId($store->getId()); + + $customer = $this->customerRepository->getById(1); + $this->_emailModel->setCustomerData($customer); + + /** @var Product $product */ + $product = $this->productRepository->getById(1); + + $this->_emailModel->addPriceProduct($product); + $this->_emailModel->send(); + + $from = $this->transportBuilder->getSentMessage()->getFrom()[0]; + $this->assertEquals('Fixture Store Owner', $from->getName()); + $this->assertEquals('fixture.store.owner@example.com', $from->getEmail()); + } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/CustomerManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/CustomerManagementTest.php new file mode 100644 index 0000000000000..a34bfa382d427 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/CustomerManagementTest.php @@ -0,0 +1,91 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model; + +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Api\Data\CustomerInterface; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use PHPUnit\Framework\TestCase; + +/** + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + */ +class CustomerManagementTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + + /** + * @var CustomerManagement + */ + private $customerManagemet; + + /** + * @var CustomerInterface + */ + private $customer; + + protected function setUp(): void + { + $this->objectManager = BootstrapHelper::getObjectManager(); + $this->customerManagemet = $this->objectManager->create(CustomerManagement::class); + $this->customer = $this->objectManager->create(CustomerInterface::class); + } + + protected function tearDown(): void + { + /** @var CustomerRepositoryInterface $customerRepository */ + $customerRepository = $this->objectManager->create(CustomerRepositoryInterface::class); + $customer = $customerRepository->get('john1.doe001@test.com'); + $customerRepository->delete($customer); + } + + /** + * @magentoDataFixture Magento/Sales/_files/quote.php + */ + public function testCustomerAddressIdQuote(): void + { + $reservedOrderId = 'test01'; + + $this->customer->setEmail('john1.doe001@test.com') + ->setFirstname('doe') + ->setLastname('john'); + + $quote = $this->getQuote($reservedOrderId)->setCustomer($this->customer); + $this->customerManagemet->populateCustomerInfo($quote); + self::assertNotNull($quote->getBillingAddress()->getCustomerAddressId()); + self::assertNotNull($quote->getShippingAddress()->getCustomerAddressId()); + } + + /** + * Gets quote by reserved order ID. + * + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote(string $reservedOrderId): Quote + { + /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ + $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + + /** @var CartRepositoryInterface $quoteRepository */ + $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + $items = $quoteRepository->getList($searchCriteria) + ->getItems(); + + return array_pop($items); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php new file mode 100644 index 0000000000000..3aadad7e9ebec --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Product/Plugin/UpdateQuoteItemsTest.php @@ -0,0 +1,78 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Product\Plugin; + +use Magento\Catalog\Model\ProductRepository; +use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Tests for update quote items plugin + * + * @magentoAppArea adminhtml + */ +class UpdateQuoteItemsTest extends TestCase +{ + /** + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepository + */ + private $productRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->getQuoteByReservedOrderId = $objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $objectManager->get(ProductRepository::class); + } + + /** + * Test to mark the quote as need to recollect and doesn't update the field "updated_at" after change product price + * + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @return void + */ + public function testMarkQuoteRecollectAfterChangeProductPrice(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->assertNotNull($quote); + $this->assertFalse((bool)$quote->getTriggerRecollect()); + $this->assertNotEmpty($quote->getItems()); + $quoteItem = current($quote->getItems()); + $product = $quoteItem->getProduct(); + + $product->setPrice((float)$product->getPrice() + 10); + $this->productRepository->save($product); + + /** @var AdapterInterface $connection */ + $connection = $quote->getResource()->getConnection(); + $select = $connection->select() + ->from( + $connection->getTableName('quote'), + ['updated_at', 'trigger_recollect'] + )->where( + "reserved_order_id = 'test_order_with_simple_product_without_address'" + ); + + $quoteRow = $connection->fetchRow($select); + $this->assertNotEmpty($quoteRow); + $this->assertTrue((bool)$quoteRow['trigger_recollect']); + $this->assertEquals($quote->getUpdatedAt(), $quoteRow['updated_at']); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/CartItemPersisterTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/CartItemPersisterTest.php new file mode 100644 index 0000000000000..647b8a188a55c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/Quote/Item/CartItemPersisterTest.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Quote\Model\Quote\Item; + +use Magento\Framework\Exception\InputException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\NoSuchEntityException; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartItemInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; + +/** + * Test for quote item persister model. + * + * @see \Magento\Quote\Model\Quote\Item\CartItemPersister + * @magentoDbIsolation enabled + */ +class CartItemPersisterTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var CartItemPersister */ + private $model; + + /** @var CartInterfaceFactory */ + private $quoteFactory; + + /** @var CartItemInterfaceFactory */ + private $itemFactory; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->model = $this->objectManager->get(CartItemPersister::class); + $this->quoteFactory = $this->objectManager->get(CartInterfaceFactory::class); + $this->itemFactory = $this->objectManager->get(CartItemInterfaceFactory::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_disabled.php + * + * @return void + */ + public function testSaveDisabledItem(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setSku('product_disabled')->setQty(1); + $this->expectExceptionObject( + new LocalizedException(__('Product that you are trying to add is not available.')) + ); + $this->model->save($quote, $item); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testSaveQuoteItemWithoutQty(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setSku('simple-1'); + $this->expectExceptionObject(InputException::invalidFieldValue('qty', null)); + $this->model->save($quote, $item); + } + + /** + * @return void + */ + public function testSaveQuoteItemWithNotExistingProduct(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setSku('not_existing_product_sku')->setQty(1); + $this->expectExceptionObject( + new NoSuchEntityException( + __('The product that was requested doesn\'t exist. Verify the product and try again.') + ) + ); + $this->model->save($quote, $item); + } + + /** + * @return void + */ + public function testUpdateNotExistingQuoteItem(): void + { + $quote = $this->quoteFactory->create(); + $item = $this->itemFactory->create(); + $item->setItemId(989)->setQty(1); + $this->expectExceptionObject( + new NoSuchEntityException( + __('The %1 Cart doesn\'t contain the %2 item.', null, 989) + ) + ); + $this->model->save($quote, $item); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_taxable_product_and_customer.php + * + * @return void + */ + public function testUpdateQuoteItemMoreQty(): void + { + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_taxable_product'); + $quoteItem = current($quote->getItems()); + $item = $this->itemFactory->create(); + $item->setQty(9999)->setSku($quoteItem->getSku())->setItemId($quoteItem->getItemId()); + $this->expectExceptionObject(new LocalizedException(__('The requested qty is not available'))); + $this->model->save($quote, $item); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php index dac05f17089a1..facb4879650b1 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteManagementTest.php @@ -9,22 +9,30 @@ use Magento\Catalog\Api\ProductRepositoryInterface; use Magento\Catalog\Model\Product\Type; -use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\StateException; +use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\CartManagementInterface; -use Magento\Quote\Api\CartRepositoryInterface; use Magento\Sales\Api\OrderManagementInterface; use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\ObjectManager; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\Framework\TestCase; /** * Class for testing QuoteManagement model + * + * @see \Magento\Quote\Model\QuoteManagement + * @magentoDbIsolation enabled + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteManagementTest extends \PHPUnit\Framework\TestCase +class QuoteManagementTest extends TestCase { /** - * @var ObjectManager + * @var ObjectManagerInterface */ private $objectManager; @@ -33,14 +41,46 @@ class QuoteManagementTest extends \PHPUnit\Framework\TestCase */ private $cartManagement; + /** + * @var OrderRepositoryInterface + */ + private $orderRepository; + + /** + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var ProductRepositoryInterface + */ + private $productRepository; + + /** + * @var CustomerRepositoryInterface + */ + private $customerRepository; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** * @inheritdoc */ protected function setUp(): void { - $this->objectManager = Bootstrap::getObjectManager(); + parent::setUp(); - $this->cartManagement = $this->objectManager->create(CartManagementInterface::class); + $this->objectManager = Bootstrap::getObjectManager(); + $this->cartManagement = $this->objectManager->get(CartManagementInterface::class); + $this->orderRepository = $this->objectManager->get(OrderRepositoryInterface::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); } /** @@ -48,22 +88,20 @@ protected function setUp(): void * * @magentoAppIsolation enabled * @magentoDataFixture Magento/Sales/_files/quote_with_bundle.php + * + * @return void */ - public function testSubmit() + public function testSubmit(): void { - $quote = $this->getQuote('test01'); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); $orderId = $this->cartManagement->placeOrder($quote->getId()); - - /** @var OrderRepositoryInterface $orderRepository */ - $orderRepository = $this->objectManager->create(OrderRepositoryInterface::class); - $order = $orderRepository->get($orderId); - + $order = $this->orderRepository->get($orderId); $orderItems = $order->getItems(); - self::assertCount(3, $orderItems); + $this->assertCount(3, $orderItems); foreach ($orderItems as $orderItem) { if ($orderItem->getProductType() == Type::TYPE_SIMPLE) { - self::assertNotEmpty($orderItem->getParentItem(), 'Parent is not set for child product'); - self::assertNotEmpty($orderItem->getParentItemId(), 'Parent is not set for child product'); + $this->assertNotEmpty($orderItem->getParentItem(), 'Parent is not set for child product'); + $this->assertNotEmpty($orderItem->getParentItemId(), 'Parent is not set for child product'); } } } @@ -75,17 +113,13 @@ public function testSubmit() * @magentoAppIsolation enabled * @magentoDataFixture Magento/Sales/_files/quote_with_bundle.php */ - public function testSubmitWithDeletedItem() + public function testSubmitWithDeletedItem(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('Some of the products below do not have all the required options.'); - - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get('simple-2'); - $productRepository->delete($product); - $quote = $this->getQuote('test01'); - + $this->productRepository->deleteById('simple-2'); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $this->expectExceptionObject( + new LocalizedException(__('Some of the products below do not have all the required options.')) + ); $this->cartManagement->placeOrder($quote->getId()); } @@ -95,13 +129,11 @@ public function testSubmitWithDeletedItem() * @magentoDataFixture Magento/Sales/_files/quote.php * @magentoDbIsolation enabled */ - public function testSubmitWithItemOutOfStock() + public function testSubmitWithItemOutOfStock(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('Some of the products are out of stock.'); - $this->makeProductOutOfStock('simple'); - $quote = $this->getQuote('test01'); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $this->expectExceptionObject(new LocalizedException(__('Some of the products are out of stock.'))); $this->cartManagement->placeOrder($quote->getId()); } @@ -111,21 +143,20 @@ public function testSubmitWithItemOutOfStock() * Order should not start placing if order validation is failed. * * @magentoDataFixture Magento/Quote/Fixtures/quote_without_customer_email.php + * + * @return void */ - public function testSubmitWithEmptyCustomerEmail() + public function testSubmitWithEmptyCustomerEmail(): void { - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); - $this->expectExceptionMessage('Email has a wrong format'); - - $quote = $this->getQuote('test01'); - $orderManagement = $this->getMockForAbstractClass(OrderManagementInterface::class); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $orderManagement = $this->createMock(OrderManagementInterface::class); $orderManagement->expects($this->never()) ->method('place'); $cartManagement = $this->objectManager->create( CartManagementInterface::class, ['orderManagement' => $orderManagement] ); - + $this->expectExceptionObject(new LocalizedException(__('Email has a wrong format'))); try { $cartManagement->placeOrder($quote->getId()); } catch (ExpectationFailedException $e) { @@ -134,24 +165,56 @@ public function testSubmitWithEmptyCustomerEmail() } /** - * Gets quote by reserved order ID. + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Customer/_files/customer.php * - * @param string $reservedOrderId - * @return Quote + * @return void */ - private function getQuote(string $reservedOrderId): Quote + public function testAssignCustomerToQuote(): void { - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) - ->create(); + $customer = $this->customerRepository->get('customer@example.com'); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $result = $this->cartManagement->assignCustomer($quote->getId(), $customer->getId(), $customer->getStoreId()); + $this->assertTrue($result); + $customerQuote = $this->cartManagement->getCartForCustomer($customer->getId()); + $this->assertEquals($quote->getId(), $customerQuote->getId()); + $this->assertEquals($customer->getId(), $customerQuote->getCustomerId()); + $this->assertEquals($customer->getEmail(), $customerQuote->getCustomerEmail()); + } - /** @var CartRepositoryInterface $quoteRepository */ - $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); - $items = $quoteRepository->getList($searchCriteria) - ->getItems(); + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_simple_product_saved.php + * @magentoDataFixture Magento/Customer/_files/customer_for_second_website.php + * + * @return void + */ + public function testAssignCustomerFromAnotherWebsiteToQuote(): void + { + $websiteId = $this->storeManager->getWebsite('test')->getId(); + $customer = $this->customerRepository->get('customer@example.com', $websiteId); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_simple_product_without_address'); + $this->expectExceptionObject( + new StateException( + __('The customer can\'t be assigned to the cart. The cart belongs to a different store.') + ) + ); + $this->cartManagement->assignCustomer($quote->getId(), $customer->getId(), $quote->getStoreId()); + } - return array_pop($items); + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * @magentoDataFixture Magento/Customer/_files/customer_with_uk_address.php + * + * @return void + */ + public function testAssignCustomerToQuoteAlreadyHaveCustomer(): void + { + $customer = $this->customerRepository->get('customer_uk_address@test.com'); + $quote = $this->getQuoteByReservedOrderId->execute('test_order_with_customer_without_address'); + $this->expectExceptionObject( + new StateException(__('The customer can\'t be assigned to the cart because the cart isn\'t anonymous.')) + ); + $this->cartManagement->assignCustomer($quote->getId(), $customer->getId(), $quote->getStoreId()); } /** @@ -160,14 +223,12 @@ private function getQuote(string $reservedOrderId): Quote * @param string $sku * @return void */ - private function makeProductOutOfStock(string $sku) + private function makeProductOutOfStock(string $sku): void { - /** @var ProductRepositoryInterface $productRepository */ - $productRepository = $this->objectManager->get(ProductRepositoryInterface::class); - $product = $productRepository->get($sku); + $product = $this->productRepository->get($sku); $extensionAttributes = $product->getExtensionAttributes(); $stockItem = $extensionAttributes->getStockItem(); $stockItem->setIsInStock(false); - $productRepository->save($product); + $this->productRepository->save($product); } } diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php index 3b18ee0ceaa5e..f3684e5167b58 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteRepositoryTest.php @@ -3,26 +3,37 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Quote\Model; -use Magento\Store\Model\StoreRepository; -use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; -use Magento\Framework\ObjectManagerInterface; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Framework\Api\SearchCriteria; -use Magento\Framework\Api\SearchResults; use Magento\Framework\Api\FilterBuilder; +use Magento\Framework\Api\SearchCriteria; +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartExtension; use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartInterfaceFactory; +use Magento\Quote\Api\Data\CartItemInterfaceFactory; use Magento\Quote\Api\Data\CartSearchResultsInterface; -use Magento\Quote\Api\Data\CartExtension; -use Magento\User\Api\Data\UserInterface; +use Magento\Quote\Api\Data\ShippingAssignmentInterface; +use Magento\Quote\Api\Data\ShippingInterface; use Magento\Quote\Model\Quote\Address as QuoteAddress; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap as BootstrapHelper; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; /** + * Test for quote repository + * + * @see \Magento\Quote\Model\QuoteRepository + * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @magentoDbIsolation disabled */ -class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase +class QuoteRepositoryTest extends TestCase { /** * @var ObjectManagerInterface @@ -30,7 +41,7 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $objectManager; /** - * @var QuoteRepository + * @var CartRepositoryInterface */ private $quoteRepository; @@ -45,14 +56,63 @@ class QuoteRepositoryTest extends \PHPUnit\Framework\TestCase private $filterBuilder; /** - * Set up + * @var GetQuoteByReservedOrderId + */ + private $getQuoteByReservedOrderId; + + /** + * @var StoreRepositoryInterface + */ + private $storeRepository; + + /** + * @var AddressInterfaceFactory + */ + private $addressFactory; + + /** + * @var CartInterfaceFactory + */ + private $quoteFactory; + + /** + * @var CartItemInterfaceFactory + */ + private $itemFactory; + + /** + * @var CartInterface|null + */ + private $quote; + + /** + * @inheritdoc */ protected function setUp(): void { + parent::setUp(); + $this->objectManager = BootstrapHelper::getObjectManager(); - $this->quoteRepository = $this->objectManager->create(QuoteRepository::class); - $this->searchCriteriaBuilder = $this->objectManager->create(SearchCriteriaBuilder::class); - $this->filterBuilder = $this->objectManager->create(FilterBuilder::class); + $this->quoteRepository = $this->objectManager->create(CartRepositoryInterface::class); + $this->searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $this->filterBuilder = $this->objectManager->get(FilterBuilder::class); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); + $this->addressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + $this->quoteFactory = $this->objectManager->get(CartInterfaceFactory::class); + $this->itemFactory = $this->objectManager->get(CartItemInterfaceFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + if ($this->quote instanceof CartInterface) { + $this->quoteRepository->delete($this->quote); + } + + parent::tearDown(); } /** @@ -60,22 +120,18 @@ protected function setUp(): void * * @magentoDataFixture Magento/Sales/_files/quote.php * @magentoDataFixture Magento/Store/_files/second_store.php + * + * @return void */ - public function testGetQuoteWithCustomStoreId() + public function testGetQuoteWithCustomStoreId(): void { $secondStoreCode = 'fixture_second_store'; $reservedOrderId = 'test01'; - - $storeRepository = $this->objectManager->create(StoreRepository::class); - $secondStore = $storeRepository->get($secondStoreCode); - - // Set store_id in quote to second store_id - $quote = $this->getQuote($reservedOrderId); + $secondStore = $this->storeRepository->get($secondStoreCode); + $quote = $this->getQuoteByReservedOrderId->execute($reservedOrderId); $quote->setStoreId($secondStore->getId()); $this->quoteRepository->save($quote); - $savedQuote = $this->quoteRepository->get($quote->getId()); - $this->assertEquals( $secondStore->getId(), $savedQuote->getStoreId(), @@ -85,8 +141,10 @@ public function testGetQuoteWithCustomStoreId() /** * @magentoDataFixture Magento/Sales/_files/quote.php + * + * @return void */ - public function testGetList() + public function testGetList(): void { $searchCriteria = $this->getSearchCriteria('test01'); $searchResult = $this->quoteRepository->getList($searchCriteria); @@ -95,62 +153,50 @@ public function testGetList() /** * @magentoDataFixture Magento/Sales/_files/quote.php + * + * @return void */ - public function testGetListDoubleCall() + public function testGetListDoubleCall(): void { $searchCriteria1 = $this->getSearchCriteria('test01'); $searchCriteria2 = $this->getSearchCriteria('test02'); $searchResult = $this->quoteRepository->getList($searchCriteria1); $this->performAssertions($searchResult); $searchResult = $this->quoteRepository->getList($searchCriteria2); - $this->assertEmpty($searchResult->getItems()); } /** * @magentoAppIsolation enabled + * + * @return void */ - public function testSaveWithNotExistingCustomerAddress() + public function testSaveWithNotExistingCustomerAddress(): void { $addressData = include __DIR__ . '/../../Sales/_files/address_data.php'; - - /** @var QuoteAddress $billingAddress */ - $billingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); - $billingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_BILLING) - ->setCustomerAddressId('not_existing'); - - /** @var QuoteAddress $shippingAddress */ - $shippingAddress = $this->objectManager->create(QuoteAddress::class, ['data' => $addressData]); - $shippingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_SHIPPING) - ->setCustomerAddressId('not_existing'); - - /** @var Shipping $shipping */ - $shipping = $this->objectManager->create(Shipping::class); + $billingAddress = $this->addressFactory->create(['data' => $addressData]); + $billingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_BILLING)->setCustomerAddressId('not_existing'); + $shippingAddress = $this->addressFactory->create(['data' => $addressData]); + $shippingAddress->setAddressType(QuoteAddress::ADDRESS_TYPE_SHIPPING)->setCustomerAddressId('not_existing'); + $shipping = $this->objectManager->get(ShippingInterface::class); $shipping->setAddress($shippingAddress); - - /** @var ShippingAssignment $shippingAssignment */ - $shippingAssignment = $this->objectManager->create(ShippingAssignment::class); + $shippingAssignment = $this->objectManager->get(ShippingAssignmentInterface::class); $shippingAssignment->setItems([]); $shippingAssignment->setShipping($shipping); - - /** @var CartExtension $extensionAttributes */ - $extensionAttributes = $this->objectManager->create(CartExtension::class); + $extensionAttributes = $this->objectManager->get(CartExtension::class); $extensionAttributes->setShippingAssignments([$shippingAssignment]); - - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $quote->setStoreId(1) + $this->quote = $this->quoteFactory->create(); + $this->quote->setStoreId(1) ->setIsActive(true) - ->setIsMultiShipping(false) + ->setIsMultiShipping(0) ->setBillingAddress($billingAddress) ->setShippingAddress($shippingAddress) ->setExtensionAttributes($extensionAttributes) ->save(); - $this->quoteRepository->save($quote); - - $this->assertNull($quote->getBillingAddress()->getCustomerAddressId()); + $this->quoteRepository->save($this->quote); + $this->assertNull($this->quote->getBillingAddress()->getCustomerAddressId()); $this->assertNull( - $quote->getExtensionAttributes() + $this->quote->getExtensionAttributes() ->getShippingAssignments()[0] ->getShipping() ->getAddress() @@ -159,21 +205,37 @@ public function testSaveWithNotExistingCustomerAddress() } /** - * Returns quote by reserved order id. + * @magentoDataFixture Magento/Catalog/_files/multiple_products.php * - * @param string $reservedOrderId - * @return CartInterface + * @return void */ - private function getQuote(string $reservedOrderId) + public function testSaveQuoteWithItems(): void { - $searchCriteria = $this->getSearchCriteria($reservedOrderId); - $searchResult = $this->quoteRepository->getList($searchCriteria); - $items = $searchResult->getItems(); - - /** @var CartInterface $quote */ - $quote = array_pop($items); + $items = $this->prepareQuoteItems(['simple1', 'simple2']); + $this->quote = $this->quoteFactory->create(); + $this->quote->setItems($items); + $this->quoteRepository->save($this->quote); + $this->assertCount(2, $this->quote->getItemsCollection()); + $this->assertEquals(2, $this->quote->getItemsCount()); + $this->assertEquals(2, $this->quote->getItemsQty()); + } - return $quote; + /** + * Prepare quote items by products sku. + * + * @param array $productsSku + * @return array + */ + private function prepareQuoteItems(array $productsSku): array + { + $items = []; + foreach ($productsSku as $sku) { + $item = $this->itemFactory->create(); + $item->setSku($sku)->setQty(1); + $items[] = $item; + } + + return $items; } /** @@ -182,7 +244,7 @@ private function getQuote(string $reservedOrderId) * @param string $filterValue * @return SearchCriteria */ - private function getSearchCriteria($filterValue) + private function getSearchCriteria(string $filterValue): SearchCriteria { $filters = []; $filters[] = $this->filterBuilder->setField('reserved_order_id') @@ -197,24 +259,19 @@ private function getSearchCriteria($filterValue) /** * Perform assertions * - * @param SearchResults|CartSearchResultsInterface $searchResult + * @param CartSearchResultsInterface $searchResult + * @return void */ - private function performAssertions($searchResult) + private function performAssertions(CartSearchResultsInterface $searchResult): void { $expectedExtensionAttributes = [ 'firstname' => 'firstname', 'lastname' => 'lastname', - 'email' => 'admin@example.com' + 'email' => 'admin@example.com', ]; - $items = $searchResult->getItems(); - - /** @var CartInterface $actualQuote */ $actualQuote = array_pop($items); - - /** @var UserInterface $testAttribute */ $testAttribute = $actualQuote->getExtensionAttributes()->getQuoteTestAttribute(); - $this->assertInstanceOf(CartInterface::class, $actualQuote); $this->assertEquals('test01', $actualQuote->getReservedOrderId()); $this->assertEquals($expectedExtensionAttributes['firstname'], $testAttribute->getFirstName()); diff --git a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php index a9fdbf59a371b..081cae5f98ee5 100644 --- a/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php +++ b/dev/tests/integration/testsuite/Magento/Quote/Model/QuoteTest.php @@ -8,46 +8,111 @@ namespace Magento\Quote\Model; use Magento\Catalog\Api\ProductRepositoryInterface; -use Magento\Catalog\Model\ProductRepository; use Magento\Customer\Api\CustomerRepositoryInterface; use Magento\Customer\Api\Data\CustomerInterface; use Magento\Customer\Api\Data\CustomerInterfaceFactory; -use Magento\Customer\Model\Data\Customer; +use Magento\Customer\Model\CustomerFactory; +use Magento\Customer\Model\GroupFactory; +use Magento\Customer\Model\GroupManagement; +use Magento\Customer\Model\ResourceModel\Customer as CustomerResourceModel; +use Magento\Framework\Api\DataObjectHelper; +use Magento\Framework\Api\ExtensibleDataObjectConverter; use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\ObjectManagerInterface; use Magento\Quote\Api\Data\AddressInterface; +use Magento\Quote\Api\Data\AddressInterfaceFactory; +use Magento\Quote\Api\Data\CartInterface; +use Magento\Quote\Api\Data\CartItemInterface; +use Magento\Quote\Api\Data\CartItemInterfaceFactory; use Magento\TestFramework\Helper\Bootstrap; -use Magento\TestFramework\ObjectManager; -use Magento\Framework\Api\SearchCriteriaBuilder; -use Magento\Quote\Api\CartRepositoryInterface; -use Magento\Framework\Api\ExtensibleDataInterface; +use Magento\TestFramework\Quote\Model\GetQuoteByReservedOrderId; +use PHPUnit\Framework\TestCase; /** + * Tests for quote model. + * + * @see \Magento\Quote\Model\Quote + * + * @magentoDbIsolation enabled * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class QuoteTest extends \PHPUnit\Framework\TestCase +class QuoteTest extends TestCase { - /** - * @var ObjectManager - */ + /** @var ObjectManagerInterface */ private $objectManager; + /** @var ProductRepositoryInterface */ + private $productRepository; + + /** @var GetQuoteByReservedOrderId */ + private $getQuoteByReservedOrderId; + + /** @var QuoteFactory */ + private $quoteFactory; + + /** @var DataObjectHelper */ + private $dataObjectHelper; + + /** @var CustomerRepositoryInterface */ + private $customerRepository; + + /** @var CustomerInterfaceFactory */ + private $customerDataFactory; + + /** @var CustomerFactory */ + private $customerFactory; + + /** @var AddressInterfaceFactory */ + private $addressFactory; + + /** @var CartItemInterfaceFactory */ + private $itemFactory; + + /** @var CustomerResourceModel */ + private $customerResourceModel; + + /** @var int */ + private $customerIdToDelete; + + /** @var GroupFactory */ + private $groupFactory; + + /** @var ExtensibleDataObjectConverter */ + private $extensibleDataObjectConverter; + /** * @inheritdoc */ protected function setUp(): void { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->productRepository = $this->objectManager->get(ProductRepositoryInterface::class); + $this->productRepository->cleanCache(); + $this->getQuoteByReservedOrderId = $this->objectManager->get(GetQuoteByReservedOrderId::class); + $this->quoteFactory = $this->objectManager->get(QuoteFactory::class); + $this->dataObjectHelper = $this->objectManager->get(DataObjectHelper::class); + $this->customerRepository = $this->objectManager->get(CustomerRepositoryInterface::class); + $this->customerDataFactory = $this->objectManager->get(CustomerInterfaceFactory::class); + $this->customerFactory = $this->objectManager->get(CustomerFactory::class); + $this->addressFactory = $this->objectManager->get(AddressInterfaceFactory::class); + $this->itemFactory = $this->objectManager->get(CartItemInterfaceFactory::class); + $this->customerResourceModel = $this->objectManager->get(CustomerResourceModel::class); + $this->groupFactory = $this->objectManager->get(GroupFactory::class); + $this->extensibleDataObjectConverter = $this->objectManager->get(ExtensibleDataObjectConverter::class); } /** - * @param ExtensibleDataInterface $entity - * @return array + * @inheritdoc */ - private function convertToArray(ExtensibleDataInterface $entity): array + protected function tearDown(): void { - return $this->objectManager - ->create(\Magento\Framework\Api\ExtensibleDataObjectConverter::class) - ->toFlatArray($entity); + if ($this->customerIdToDelete) { + $this->customerRepository->deleteById($this->customerIdToDelete); + } + + parent::tearDown(); } /** @@ -57,16 +122,10 @@ private function convertToArray(ExtensibleDataInterface $entity): array */ public function testCollectTotalsWithVirtual(): void { - $quote = $this->objectManager->create(Quote::class); - $quote->load('test01', 'reserved_order_id'); - - $productRepository = $this->objectManager->create( - ProductRepositoryInterface::class - ); - $product = $productRepository->get('virtual-product', false, null, true); + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $product = $this->productRepository->get('virtual-product', false, null, true); $quote->addProduct($product); $quote->collectTotals(); - $this->assertEquals(2, $quote->getItemsQty()); $this->assertEquals(1, $quote->getVirtualItemsQty()); $this->assertEquals(20, $quote->getGrandTotal()); @@ -75,15 +134,13 @@ public function testCollectTotalsWithVirtual(): void /** * @magentoDataFixture Magento/Catalog/_files/product_virtual.php - * @magentoDataFixture Magento/Quote/_files/empty_quote.php + * * @return void */ - public function testGetAddressWithVirtualProduct() + public function testGetAddressWithVirtualProduct(): void { - /** @var Quote $quote */ $quote = $this->objectManager->create(Quote::class); - $quote->load('reserved_order_id_1', 'reserved_order_id'); - $billingAddress = $this->objectManager->create(AddressInterface::class); + $billingAddress = $this->addressFactory->create(); $billingAddress->setFirstname('Joe') ->setLastname('Doe') ->setCountryId('US') @@ -93,7 +150,7 @@ public function testGetAddressWithVirtualProduct() ->setPostcode('11501') ->setTelephone('123456789'); $quote->setBillingAddress($billingAddress); - $shippingAddress = $this->objectManager->create(AddressInterface::class); + $shippingAddress = $this->addressFactory->create(); $shippingAddress->setFirstname('Joe') ->setLastname('Doe') ->setCountryId('US') @@ -103,10 +160,7 @@ public function testGetAddressWithVirtualProduct() ->setPostcode('07102') ->setTelephone('9734685221'); $quote->setShippingAddress($shippingAddress); - $productRepository = $this->objectManager->create( - ProductRepositoryInterface::class - ); - $product = $productRepository->get('virtual-product', false, null, true); + $product = $this->productRepository->get('virtual-product', false, null, true); $quote->addProduct($product); $quote->save(); $expectedAddress = $quote->getBillingAddress(); @@ -118,73 +172,43 @@ public function testGetAddressWithVirtualProduct() */ public function testSetCustomerData(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - /** @var CustomerInterfaceFactory $customerFactory */ - $customerFactory = $this->objectManager->create( - CustomerInterfaceFactory::class - ); - /** @var \Magento\Framework\Api\DataObjectHelper $dataObjectHelper */ - $dataObjectHelper = $this->objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); - $expected = $this->_getCustomerDataArray(); - $customer = $customerFactory->create(); - $dataObjectHelper->populateWithArray( - $customer, - $expected, - \Magento\Customer\Api\Data\CustomerInterface::class - ); - - $this->assertEquals($expected, $this->convertToArray($customer)); + $quote = $this->quoteFactory->create(); + $expected = $this->getCustomerDataArray(); + $customer = $this->customerDataFactory->create(); + $this->dataObjectHelper->populateWithArray($customer, $expected, CustomerInterfaceFactory::class); + $this->assertEquals($expected, $this->extensibleDataObjectConverter->toFlatArray($customer)); $quote->setCustomer($customer); $customer = $quote->getCustomer(); - $this->assertEquals($expected, $this->convertToArray($customer)); - $this->assertEquals('qa@example.com', $quote->getCustomerEmail()); - $this->assertEquals('Joe', $quote->getCustomerFirstname()); - $this->assertEquals('Dou', $quote->getCustomerLastname()); - $this->assertEquals('Ivan', $quote->getCustomerMiddlename()); + $this->assertEquals($expected, $this->extensibleDataObjectConverter->toFlatArray($customer)); + $this->assertEquals($expected[CustomerInterface::EMAIL], $quote->getCustomerEmail()); + $this->assertEquals($expected[CustomerInterface::FIRSTNAME], $quote->getCustomerFirstname()); + $this->assertEquals($expected[CustomerInterface::LASTNAME], $quote->getCustomerLastname()); + $this->assertEquals($expected[CustomerInterface::MIDDLENAME], $quote->getCustomerMiddlename()); } /** + * @magentoAppArea adminhtml + * * @return void */ public function testUpdateCustomerData(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerFactory = $this->objectManager->create( - CustomerInterfaceFactory::class - ); - /** @var \Magento\Framework\Api\DataObjectHelper $dataObjectHelper */ - $dataObjectHelper = $this->objectManager->create(\Magento\Framework\Api\DataObjectHelper::class); - $expected = $this->_getCustomerDataArray(); - //For save in repository - $expected = $this->removeIdFromCustomerData($expected); - $customerDataSet = $customerFactory->create(); - $dataObjectHelper->populateWithArray( - $customerDataSet, - $expected, - \Magento\Customer\Api\Data\CustomerInterface::class - ); - $this->assertEquals($expected, $this->convertToArray($customerDataSet)); - /** - * @var \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository - */ - $customerRepository = $this->objectManager - ->create(\Magento\Customer\Api\CustomerRepositoryInterface::class); - $customerRepository->save($customerDataSet); + $quote = $this->quoteFactory->create(); + $expected = $this->getCustomerDataArray(); + unset($expected[CustomerInterface::ID]); + $customerDataSet = $this->customerDataFactory->create(); + $this->dataObjectHelper->populateWithArray($customerDataSet, $expected, CustomerInterface::class); + $this->assertEquals($expected, $this->extensibleDataObjectConverter->toFlatArray($customerDataSet)); + $customer = $this->customerRepository->save($customerDataSet); + $this->customerIdToDelete = $customer->getId(); $quote->setCustomer($customerDataSet); - $expected = $this->_getCustomerDataArray(); - $expected = $this->changeEmailInCustomerData('test@example.com', $expected); - $customerDataUpdated = $customerFactory->create(); - $dataObjectHelper->populateWithArray( - $customerDataUpdated, - $expected, - \Magento\Customer\Api\Data\CustomerInterface::class - ); + $expected = $this->getCustomerDataArray(); + $expected[CustomerInterface::EMAIL] = 'test@example.com'; + $customerDataUpdated = $this->customerDataFactory->create(); + $this->dataObjectHelper->populateWithArray($customerDataUpdated, $expected, CustomerInterface::class); $quote->updateCustomerData($customerDataUpdated); $customer = $quote->getCustomer(); - $expected = $this->changeEmailInCustomerData('test@example.com', $expected); - $actual = $this->convertToArray($customer); + $actual = $this->extensibleDataObjectConverter->toFlatArray($customer); foreach ($expected as $item) { $this->assertContains($item, $actual); } @@ -198,19 +222,11 @@ public function testUpdateCustomerData(): void */ public function testGetCustomerGroupFromCustomer(): void { - /** Preconditions */ - /** @var CustomerInterfaceFactory $customerFactory */ - $customerFactory = $this->objectManager->create( - CustomerInterfaceFactory::class - ); $customerGroupId = 3; - $customerData = $customerFactory->create()->setId(1)->setGroupId($customerGroupId); - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); + $customerData = $this->customerDataFactory->create()->setId(1)->setGroupId($customerGroupId); + $quote = $this->quoteFactory->create(); $quote->setCustomer($customerData); $quote->unsetData('customer_group_id'); - - /** Execute SUT */ $this->assertEquals($customerGroupId, $quote->getCustomerGroupId(), "Customer group ID is invalid"); } @@ -220,19 +236,11 @@ public function testGetCustomerGroupFromCustomer(): void */ public function testGetCustomerTaxClassId(): void { - /** - * Preconditions: create quote and assign ID of customer group created in fixture to it. - */ $fixtureGroupCode = 'custom_group'; $fixtureTaxClassId = 3; - /** @var \Magento\Customer\Model\Group $group */ - $group = $this->objectManager->create(\Magento\Customer\Model\Group::class); - $fixtureGroupId = $group->load($fixtureGroupCode, 'customer_group_code')->getId(); - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); + $fixtureGroupId = $this->groupFactory->create()->load($fixtureGroupCode, 'customer_group_code')->getId(); + $quote = $this->quoteFactory->create(); $quote->setCustomerGroupId($fixtureGroupId); - - /** Execute SUT */ $this->assertEquals($fixtureTaxClassId, $quote->getCustomerTaxClassId(), 'Customer tax class ID is invalid.'); } @@ -246,60 +254,37 @@ public function testGetCustomerTaxClassId(): void */ public function testAssignCustomerWithAddressChangeAddressesNotSpecified(): void { - /** Preconditions: - * Customer with two addresses created - * First address is default billing, second is default shipping. - */ - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); - - /** Execute SUT */ + $quote = $this->quoteFactory->create(); + $customerData = $this->prepareQuoteForTestAssignCustomerWithAddressChange($quote); $quote->assignCustomerWithAddressChange($customerData); - - /** Check if SUT caused expected effects */ $fixtureCustomerId = 1; $this->assertEquals($fixtureCustomerId, $quote->getCustomerId(), 'Customer ID in quote is invalid.'); $expectedBillingAddressData = [ - 'street' => 'Green str, 67', - 'telephone' => 3468676, - 'postcode' => 75477, - 'country_id' => 'US', - 'city' => 'CityM', - 'lastname' => 'Smith', - 'firstname' => 'John', - 'customer_id' => 1, - 'customer_address_id' => 1, - 'region_id' => 1 + AddressInterface::KEY_STREET => 'Green str, 67', + AddressInterface::KEY_TELEPHONE => 3468676, + AddressInterface::KEY_POSTCODE => 75477, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityM', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_CUSTOMER_ID => 1, + AddressInterface::CUSTOMER_ADDRESS_ID => 1, + AddressInterface::KEY_REGION_ID => 1, ]; - $billingAddress = $quote->getBillingAddress(); - foreach ($expectedBillingAddressData as $field => $value) { - $this->assertEquals( - $value, - $billingAddress->getData($field), - "'{$field}' value in quote billing address is invalid." - ); - } + $this->assertQuoteAddress($expectedBillingAddressData, $quote->getBillingAddress()); $expectedShippingAddressData = [ - 'customer_address_id' => 2, - 'telephone' => 3234676, - 'postcode' => 47676, - 'country_id' => 'US', - 'city' => 'CityX', - 'street' => 'Black str, 48', - 'lastname' => 'Smith', - 'firstname' => 'John', - 'customer_id' => 1, - 'region_id' => 1 + AddressInterface::CUSTOMER_ADDRESS_ID => 2, + AddressInterface::KEY_TELEPHONE => 3234676, + AddressInterface::KEY_POSTCODE => 47676, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityX', + AddressInterface::KEY_STREET => 'Black str, 48', + AddressInterface::KEY_LASTNAME => 'Smith', + AddressInterface::KEY_FIRSTNAME => 'John', + AddressInterface::KEY_CUSTOMER_ID => 1, + AddressInterface::KEY_REGION_ID => 1, ]; - $shippingAddress = $quote->getShippingAddress(); - foreach ($expectedShippingAddressData as $field => $value) { - $this->assertEquals( - $value, - $shippingAddress->getData($field), - "'{$field}' value in quote shipping address is invalid." - ); - } + $this->assertQuoteAddress($expectedShippingAddressData, $quote->getShippingAddress()); } /** @@ -312,63 +297,37 @@ public function testAssignCustomerWithAddressChangeAddressesNotSpecified(): void */ public function testAssignCustomerWithAddressChange(): void { - /** Preconditions: - * Customer with two addresses created - * First address is default billing, second is default shipping. - */ - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); - /** @var \Magento\Quote\Model\Quote\Address $quoteBillingAddress */ + $quote = $this->quoteFactory->create(); + $customerData = $this->prepareQuoteForTestAssignCustomerWithAddressChange($quote); $expectedBillingAddressData = [ - 'street' => 'Billing str, 67', - 'telephone' => 16546757, - 'postcode' => 2425457, - 'country_id' => 'US', - 'city' => 'CityBilling', - 'lastname' => 'LastBilling', - 'firstname' => 'FirstBilling', - 'region_id' => 1 + AddressInterface::KEY_STREET => 'Billing str, 67', + AddressInterface::KEY_TELEPHONE => 16546757, + AddressInterface::KEY_POSTCODE => 2425457, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityBilling', + AddressInterface::KEY_LASTNAME => 'LastBilling', + AddressInterface::KEY_FIRSTNAME => 'FirstBilling', + AddressInterface::KEY_REGION_ID => 1, ]; - $quoteBillingAddress = $this->objectManager->create(\Magento\Quote\Model\Quote\Address::class); + $quoteBillingAddress = $this->addressFactory->create(); $quoteBillingAddress->setData($expectedBillingAddressData); - $expectedShippingAddressData = [ - 'telephone' => 787878787, - 'postcode' => 117785, - 'country_id' => 'US', - 'city' => 'CityShipping', - 'street' => 'Shipping str, 48', - 'lastname' => 'LastShipping', - 'firstname' => 'FirstShipping', - 'region_id' => 1 + AddressInterface::KEY_TELEPHONE => 787878787, + AddressInterface::KEY_POSTCODE => 117785, + AddressInterface::KEY_COUNTRY_ID => 'US', + AddressInterface::KEY_CITY => 'CityShipping', + AddressInterface::KEY_STREET => 'Shipping str, 48', + AddressInterface::KEY_LASTNAME => 'LastShipping', + AddressInterface::KEY_FIRSTNAME => 'FirstShipping', + AddressInterface::KEY_REGION_ID => 1, ]; - $quoteShippingAddress = $this->objectManager->create(\Magento\Quote\Model\Quote\Address::class); + $quoteShippingAddress = $this->addressFactory->create(); $quoteShippingAddress->setData($expectedShippingAddressData); - - /** Execute SUT */ $quote->assignCustomerWithAddressChange($customerData, $quoteBillingAddress, $quoteShippingAddress); - - /** Check if SUT caused expected effects */ $fixtureCustomerId = 1; $this->assertEquals($fixtureCustomerId, $quote->getCustomerId(), 'Customer ID in quote is invalid.'); - - $billingAddress = $quote->getBillingAddress(); - foreach ($expectedBillingAddressData as $field => $value) { - $this->assertEquals( - $value, - $billingAddress->getData($field), - "'{$field}' value in quote billing address is invalid." - ); - } - $shippingAddress = $quote->getShippingAddress(); - foreach ($expectedShippingAddressData as $field => $value) { - $this->assertEquals( - $value, - $shippingAddress->getData($field), - "'{$field}' value in quote shipping address is invalid." - ); - } + $this->assertQuoteAddress($expectedBillingAddressData, $quote->getBillingAddress()); + $this->assertQuoteAddress($expectedShippingAddressData, $quote->getShippingAddress()); } /** @@ -379,14 +338,11 @@ public function testAssignCustomerWithAddressChange(): void * @magentoDataFixture Magento/Backend/_files/allowed_countries_fr.php * @return void */ - public function testAssignCustomerWithAddressChangeWithNotAllowedCountry() + public function testAssignCustomerWithAddressChangeWithNotAllowedCountry(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $customerData = $this->_prepareQuoteForTestAssignCustomerWithAddressChange($quote); + $quote = $this->quoteFactory->create(); + $customerData = $this->prepareQuoteForTestAssignCustomerWithAddressChange($quote); $quote->assignCustomerWithAddressChange($customerData); - - /** Check that addresses are empty */ $this->assertNull($quote->getBillingAddress()->getCountryId()); $this->assertNull($quote->getShippingAddress()->getCountryId()); } @@ -397,17 +353,9 @@ public function testAssignCustomerWithAddressChangeWithNotAllowedCountry() */ public function testAddProductUpdateItem(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $quote->load('test01', 'reserved_order_id'); - + $quote = $this->quoteFactory->create(); $productStockQty = 100; - - $productRepository = $this->objectManager->create( - ProductRepositoryInterface::class - ); - $product = $productRepository->get('simple-1', false, null, true); - + $product = $this->productRepository->get('simple-1', false, null, true); $quote->addProduct($product, 50); $quote->setTotalsCollectedFlag(false)->collectTotals(); $this->assertEquals(50, $quote->getItemsQty()); @@ -418,98 +366,19 @@ public function testAddProductUpdateItem(): void 'related_product' => '', 'product' => $product->getId(), 'qty' => 1, - 'id' => 0 + 'id' => 0, ]; $updateParams = new \Magento\Framework\DataObject($params); $quote->updateItem($updateParams['id'], $updateParams); $quote->setTotalsCollectedFlag(false)->collectTotals(); $this->assertEquals(1, $quote->getItemsQty()); - - $this->expectException(\Magento\Framework\Exception\LocalizedException::class); + $this->expectException(LocalizedException::class); // TODO: fix test or implementation as described in https://github.com/magento-engcom/msi/issues/1037 // $this->expectExceptionMessage('The requested qty is not available'); $updateParams['qty'] = $productStockQty + 1; $quote->updateItem($updateParams['id'], $updateParams); } - /** - * Prepare quote for testing assignCustomerWithAddressChange method. - * - * Customer with two addresses created. First address is default billing, second is default shipping. - * - * @param Quote $quote - * @return CustomerInterface - */ - protected function _prepareQuoteForTestAssignCustomerWithAddressChange(Quote $quote): CustomerInterface - { - $customerRepository = $this->objectManager->create( - CustomerRepositoryInterface::class - ); - $fixtureCustomerId = 1; - /** @var \Magento\Customer\Model\Customer $customer */ - $customer = $this->objectManager->create(\Magento\Customer\Model\Customer::class); - $fixtureSecondAddressId = 2; - $customer->load($fixtureCustomerId)->setDefaultShipping($fixtureSecondAddressId)->save(); - $customerData = $customerRepository->getById($fixtureCustomerId); - $this->assertEmpty( - $quote->getBillingAddress()->getId(), - "Precondition failed: billing address should be empty." - ); - $this->assertEmpty( - $quote->getShippingAddress()->getId(), - "Precondition failed: shipping address should be empty." - ); - return $customerData; - } - - /** - * @param string $email - * @param array $customerData - * @return array - */ - protected function changeEmailInCustomerData(string $email, array $customerData): array - { - $customerData[\Magento\Customer\Model\Data\Customer::EMAIL] = $email; - return $customerData; - } - - /** - * @param array $customerData - * @return array - */ - protected function removeIdFromCustomerData(array $customerData): array - { - unset($customerData[\Magento\Customer\Model\Data\Customer::ID]); - return $customerData; - } - - /** - * @return array - */ - protected function _getCustomerDataArray(): array - { - return [ - Customer::CONFIRMATION => 'test', - Customer::CREATED_AT => '2/3/2014', - Customer::CREATED_IN => 'Default', - Customer::DEFAULT_BILLING => 'test', - Customer::DEFAULT_SHIPPING => 'test', - Customer::DOB => '2014-02-03 00:00:00', - Customer::EMAIL => 'qa@example.com', - Customer::FIRSTNAME => 'Joe', - Customer::GENDER => 0, - Customer::GROUP_ID => \Magento\Customer\Model\GroupManagement::NOT_LOGGED_IN_ID, - Customer::ID => 1, - Customer::LASTNAME => 'Dou', - Customer::MIDDLENAME => 'Ivan', - Customer::PREFIX => 'Dr.', - Customer::STORE_ID => 1, - Customer::SUFFIX => 'Jr.', - Customer::TAXVAT => 1, - Customer::WEBSITE_ID => 1 - ]; - } - /** * Test to verify that reserved_order_id will be changed if it already in used * @@ -519,9 +388,7 @@ protected function _getCustomerDataArray(): array */ public function testReserveOrderId(): void { - /** @var Quote $quote */ - $quote = $this->objectManager->create(Quote::class); - $quote->load('reserved_order_id', 'reserved_order_id'); + $quote = $this->getQuoteByReservedOrderId->execute('reserved_order_id'); $quote->reserveOrderId(); $this->assertEquals('reserved_order_id', $quote->getReservedOrderId()); $quote->setReservedOrderId('100000001'); @@ -536,19 +403,10 @@ public function testReserveOrderId(): void */ public function testAddedProductToQuoteIsSalable(): void { - $productId = 99; - - /** @var ProductRepository $productRepository */ - $productRepository = $this->objectManager->get(ProductRepository::class); - - /** @var Quote $quote */ - $product = $productRepository->getById($productId, false, null, true); - + $product = $this->productRepository->getById(99, false, null, true); $this->expectException(LocalizedException::class); - $this->expectExceptionMessage('Product that you are trying to add is not available.'); - - $quote = $this->objectManager->create(Quote::class); - $quote->addProduct($product); + $this->expectExceptionMessage((string)__('Product that you are trying to add is not available.')); + $this->quoteFactory->create()->addProduct($product); } /** @@ -558,20 +416,15 @@ public function testAddedProductToQuoteIsSalable(): void */ public function testGetItemById(): void { - $quote = $this->objectManager->create(Quote::class); - $quote->load('test01', 'reserved_order_id'); - - $quoteItem = $this->objectManager->create(\Magento\Quote\Model\Quote\Item::class); - - $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - $product = $productRepository->get('simple'); - + $quote = $this->getQuoteByReservedOrderId->execute('test01'); + $quoteItem = $this->itemFactory->create(); + $product = $this->productRepository->get('simple'); $quoteItem->setProduct($product); $quote->addItem($quoteItem); $quote->save(); - - $this->assertInstanceOf(\Magento\Quote\Model\Quote\Item::class, $quote->getItemById($quoteItem->getId())); - $this->assertEquals($quoteItem->getId(), $quote->getItemById($quoteItem->getId())->getId()); + $item = $quote->getItemById($quoteItem->getId()); + $this->assertInstanceOf(CartItemInterface::class, $item); + $this->assertEquals($quoteItem->getId(), $item->getId()); } /** @@ -586,42 +439,32 @@ public function testGetItemById(): void * * @magentoDataFixture Magento/Sales/_files/quote.php * @dataProvider giftMessageDataProvider - * @throws LocalizedException * @return void */ public function testMerge( - $guestItemGiftMessageId, - $customerItemGiftMessageId, - $guestOrderGiftMessageId, - $customerOrderGiftMessageId, - $expectedItemGiftMessageId, - $expectedOrderGiftMessageId + ?int $guestItemGiftMessageId, + ?int $customerItemGiftMessageId, + ?int $guestOrderGiftMessageId, + ?int $customerOrderGiftMessageId, + ?int $expectedItemGiftMessageId, + ?int $expectedOrderGiftMessageId ): void { - $productRepository = $this->objectManager->create(ProductRepositoryInterface::class); - $product = $productRepository->get('simple', false, null, true); - - /** @var Quote $quote */ - $guestQuote = $this->getQuote('test01'); + $product = $this->productRepository->get('simple', false, null, true); + $guestQuote = $this->getQuoteByReservedOrderId->execute('test01'); $guestQuote->setGiftMessageId($guestOrderGiftMessageId); - - /** @var Quote $customerQuote */ - $customerQuote = $this->objectManager->create(Quote::class); + $customerQuote = $this->quoteFactory->create(); $customerQuote->setReservedOrderId('test02') ->setStoreId($guestQuote->getStoreId()) ->addProduct($product); $customerQuote->setGiftMessageId($customerOrderGiftMessageId); - $guestItem = $guestQuote->getItemByProduct($product); $guestItem->setGiftMessageId($guestItemGiftMessageId); - $customerItem = $customerQuote->getItemByProduct($product); $customerItem->setGiftMessageId($customerItemGiftMessageId); - $customerQuote->merge($guestQuote); $mergedItemItem = $customerQuote->getItemByProduct($product); - - self::assertEquals($expectedOrderGiftMessageId, $customerQuote->getGiftMessageId()); - self::assertEquals($expectedItemGiftMessageId, $mergedItemItem->getGiftMessageId()); + $this->assertEquals($expectedOrderGiftMessageId, $customerQuote->getGiftMessageId()); + $this->assertEquals($expectedItemGiftMessageId, $mergedItemItem->getGiftMessageId()); } /** @@ -638,7 +481,7 @@ public function giftMessageDataProvider(): array 'guestOrderId' => null, 'customerOrderId' => 11, 'expectedItemId' => 1, - 'expectedOrderId' => 11 + 'expectedOrderId' => 11, ], [ 'guestItemId' => 1, @@ -646,28 +489,252 @@ public function giftMessageDataProvider(): array 'guestOrderId' => 11, 'customerOrderId' => 22, 'expectedItemId' => 1, - 'expectedOrderId' => 11 - ] + 'expectedOrderId' => 11, + ], + ]; + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_with_custom_file_option.php + * + * @return void + */ + public function testAddProductWithoutChosenOptions(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_with_custom_file_option'); + $result = $quote->addProduct($product); + $this->assertEquals( + (string)__( + 'The product\'s required option(s) weren\'t entered. Make sure the options are entered and try again.' + ), + $result + ); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testAddProductWithInvalidRequestParams(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $this->expectExceptionObject( + new LocalizedException(__('We found an invalid request for adding product to quote.')) + ); + $quote->addProduct($product, ''); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_out_of_stock.php + * + * @return void + */ + public function testAddProductOutOfStock(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-out-of-stock'); + $this->expectExceptionObject( + new LocalizedException(__('Product that you are trying to add is not available.')) + ); + $quote->addProduct($product, 1); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * + * @return void + */ + public function testAddProductWithMoreQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $this->expectExceptionObject(new LocalizedException(__('The requested qty is not available'))); + $quote->addProduct($product, 1500); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_with_qty_increments.php + * + * @return void + */ + public function testAddProductWithQtyIncrements(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_product_with_qty_increments'); + $this->expectExceptionObject( + new LocalizedException(__('You can buy this product only in quantities of %1 at a time.', 3)) + ); + $quote->addProduct($product, 1); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_min_max_sale_qty.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithMinSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_product_min_max_sale_qty'); + $messages = [ + (string)__('The fewest you may purchase is %1.', 5), + (string)__('The fewest you may purchase is %1', 5), + ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 1); + } + + /** + * @magentoDataFixture Magento/Catalog/_files/simple_product_min_max_sale_qty.php + * + * @return void + */ + public function testAddProductWithMaxSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple_product_min_max_sale_qty'); + $messages = [ + (string)__('The most you may purchase is %1.', 20), + (string)__('The requested qty exceeds the maximum qty allowed in shopping cart'), + ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 25); + } + + /** + * @magentoConfigFixture current_store cataloginventory/item_options/enable_qty_increments 1 + * @magentoConfigFixture current_store cataloginventory/item_options/qty_increments 3 + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithConfigQtyIncrements(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $this->expectExceptionObject( + new LocalizedException(__('You can buy this product only in quantities of %1 at a time.', 3)) + ); + $quote->addProduct($product, 1); + } + + /** + * @magentoConfigFixture current_store cataloginventory/item_options/min_sale_qty 5 + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithConfigMinSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $messages = [ + (string)__('The fewest you may purchase is %1.', 5), + (string)__('The fewest you may purchase is %1', 5), + ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 1); + } + + /** + * @magentoConfigFixture current_store cataloginventory/item_options/max_sale_qty 20 + * @magentoDataFixture Magento/Catalog/_files/product_simple_duplicated.php + * @magentoAppIsolation enabled + * + * @return void + */ + public function testAddProductWithConfigMaxSaleQty(): void + { + $quote = $this->quoteFactory->create(); + $product = $this->productRepository->get('simple-1'); + $messages = [ + (string)__('The most you may purchase is %1.', 20), + (string)__('The requested qty exceeds the maximum qty allowed in shopping cart'), ]; + $this->expectException(LocalizedException::class); + $this->expectExceptionMessageMatches('/' . implode('|', $messages) . '/'); + $quote->addProduct($product, 25); } /** - * Gets quote by reserved order id. + * Assert address in quote. * - * @param string $reservedOrderId - * @return Quote + * @param array $expectedAddress + * @param AddressInterface $quoteAddress + * @return void + */ + private function assertQuoteAddress(array $expectedAddress, AddressInterface $quoteAddress): void + { + foreach ($expectedAddress as $field => $value) { + $this->assertEquals( + $value, + $quoteAddress->getData($field), + sprintf('"%s" value in quote %s address is invalid.', $field, $quoteAddress->getAddressType()) + ); + } + } + + /** + * Prepare quote for testing assignCustomerWithAddressChange method. + * Customer with two addresses created. First address is default billing, second is default shipping. + * + * @param CartInterface $quote + * @return CustomerInterface */ - private function getQuote(string $reservedOrderId): Quote + private function prepareQuoteForTestAssignCustomerWithAddressChange(CartInterface $quote): CustomerInterface { - /** @var SearchCriteriaBuilder $searchCriteriaBuilder */ - $searchCriteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); - $searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) - ->create(); + $fixtureCustomerId = 1; + $fixtureSecondAddressId = 2; + $customer = $this->customerFactory->create(); + $this->customerResourceModel->load($customer, $fixtureCustomerId); + $customer->setDefaultShipping($fixtureSecondAddressId); + $this->customerResourceModel->save($customer); + $customerData = $customer->getDataModel(); + $this->assertEmpty( + $quote->getBillingAddress()->getId(), + "Precondition failed: billing address should be empty." + ); + $this->assertEmpty( + $quote->getShippingAddress()->getId(), + "Precondition failed: shipping address should be empty." + ); - /** @var CartRepositoryInterface $quoteRepository */ - $quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); - $items = $quoteRepository->getList($searchCriteria)->getItems(); + return $customerData; + } - return array_pop($items); + /** + * @return array + */ + private function getCustomerDataArray(): array + { + return [ + CustomerInterface::CONFIRMATION => 'test', + CustomerInterface::CREATED_AT => '2/3/2014', + CustomerInterface::CREATED_IN => 'Default', + CustomerInterface::DEFAULT_BILLING => 'test', + CustomerInterface::DEFAULT_SHIPPING => 'test', + CustomerInterface::DOB => '2014-02-03 00:00:00', + CustomerInterface::EMAIL => 'qa@example.com', + CustomerInterface::FIRSTNAME => 'Joe', + CustomerInterface::GENDER => 0, + CustomerInterface::GROUP_ID => GroupManagement::NOT_LOGGED_IN_ID, + CustomerInterface::ID => 1, + CustomerInterface::LASTNAME => 'Dou', + CustomerInterface::MIDDLENAME => 'Ivan', + CustomerInterface::PREFIX => 'Dr.', + CustomerInterface::STORE_ID => 1, + CustomerInterface::SUFFIX => 'Jr.', + CustomerInterface::TAXVAT => 1, + CustomerInterface::WEBSITE_ID => 1, + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php index c7a09163c5fa1..2ff5b7c6ee3d2 100644 --- a/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php +++ b/dev/tests/integration/testsuite/Magento/Reports/Block/Adminhtml/Config/Form/Field/YtdStartTest.php @@ -33,26 +33,25 @@ public function testGetElementHtml(): void $this->dispatch('backend/admin/system_config/edit/section/reports/'); $body = $this->getResponse()->getBody(); - $this->assertStringContainsString($this->getOptionsHtml('01'), $body); + $this->assertOptionSelected('01', $body); } /** - * Options html + * Assert that given option is selected. * - * @param string $selected - * @return string + * @param string $option Option value. + * @param string $content HTML content + * @return void */ - private function getOptionsHtml(string $selected): string + private function assertOptionSelected(string $option, string $content): void { - $html = ''; - foreach ($this->monthNumbers as $number) { - $html .= $number === $selected - ? '<option value="' . $selected . '" selected="selected">' . $selected . '</option>' - : '<option value="' . $number . '">' . $number . '</option>'; - - $html .= PHP_EOL; + foreach ($this->monthNumbers as $monthNumber) { + $regEx = "\<option[^\>]+value\=\\\"$monthNumber\\\"[^\>]*?"; + if ($monthNumber === $option) { + $regEx .= 'selected\=\"selected\"[^\>]*?'; + } + $regEx .= "\>$monthNumber\<\/option\>"; + $this->assertRegExp("#$regEx#", $content); } - - return $html; } } diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product.php new file mode 100644 index 0000000000000..42e0a956f6c8d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductCompareAddProductObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/out_of_stock_product_with_category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$product = $productRepository->get('out-of-stock-product'); +$session = $objectManager->get(Session::class); +/** @var CatalogProductCompareAddProductObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductCompareAddProductObserver::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originValues = [ + 'reports/options/enabled' => $config->getValue('reports/options/enabled'), + 'reports/options/product_compare_enabled' => $config->getValue('reports/options/product_compare_enabled'), +]; + +try { + $config->setValue('reports/options/enabled', 1); + $config->setValue('reports/options/product_compare_enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => $product->getId()]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + foreach ($originValues as $key => $value) { + $config->setValue($key, $value); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_rollback.php new file mode 100644 index 0000000000000..677bfb32cd8e9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_out_of_stock_product_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/out_of_stock_product_with_category.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product.php new file mode 100644 index 0000000000000..1695247e8ba13 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductCompareAddProductObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +$session = $objectManager->get(Session::class); +/** @var CatalogProductCompareAddProductObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductCompareAddProductObserver::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originValues = [ + 'reports/options/enabled' => $config->getValue('reports/options/enabled'), + 'reports/options/product_compare_enabled' => $config->getValue('reports/options/product_compare_enabled'), +]; + +try { + $config->setValue('reports/options/enabled', 1); + $config->setValue('reports/options/product_compare_enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => 6]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + foreach ($originValues as $key => $value) { + $config->setValue($key, $value); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_rollback.php new file mode 100644 index 0000000000000..1181618afecdb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_compared_product_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer.php new file mode 100644 index 0000000000000..33e09155e7c03 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductViewObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_product_disabled.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originalValue = $config->getValue('reports/options/enabled'); +/** @var CatalogProductViewObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductViewObserver::class); +$product = $productRepository->get('product_disabled'); + +try { + $config->setValue('reports/options/enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => $product->getId()]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + $config->setValue('reports/options/enabled', $originalValue); +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php new file mode 100644 index 0000000000000..f3dedf0a35d96 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_disabled_product_by_customer_rollback.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductViewObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originalValue = $config->getValue('reports/options/enabled'); +/** @var CatalogProductViewObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductViewObserver::class); + +try { + $config->setValue('reports/options/enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => 6]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + $config->setValue('reports/options/enabled', $originalValue); +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer.php new file mode 100644 index 0000000000000..f3dedf0a35d96 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer.php @@ -0,0 +1,45 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\Session; +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\DataObject; +use Magento\Framework\Event\Observer; +use Magento\Reports\Observer\CatalogProductViewObserver; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var Session $session */ +$session = $objectManager->get(Session::class); +/** @var MutableScopeConfigInterface $config */ +$config = $objectManager->get(MutableScopeConfigInterface::class); +$originalValue = $config->getValue('reports/options/enabled'); +/** @var CatalogProductViewObserver $reportObserver */ +$reportObserver = $objectManager->get(CatalogProductViewObserver::class); + +try { + $config->setValue('reports/options/enabled', 1); + $session->loginById(1); + $reportObserver->execute( + new Observer( + [ + 'event' => new DataObject( + [ + 'product' => new DataObject(['id' => 6]), + ] + ), + ] + ) + ); +} finally { + $session->logout(); + $config->setValue('reports/options/enabled', $originalValue); +} diff --git a/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_rollback.php new file mode 100644 index 0000000000000..1181618afecdb --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Reports/_files/recently_viewed_product_by_customer_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/second_product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/customer_review_with_rating_rollback.php b/dev/tests/integration/testsuite/Magento/Review/_files/customer_review_with_rating_rollback.php new file mode 100644 index 0000000000000..0931d881a6fdc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/customer_review_with_rating_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/different_reviews_rollback.php b/dev/tests/integration/testsuite/Magento/Review/_files/different_reviews_rollback.php new file mode 100644 index 0000000000000..328c1e229da5c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/different_reviews_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings.php b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings.php new file mode 100644 index 0000000000000..0c097f62101f8 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Review\Model\ResourceModel\Rating\Collection as RatingCollection; +use Magento\Review\Model\ResourceModel\Rating as RatingResourceModel; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->loadArea(FrontNameResolver::AREA_CODE); + +$objectManager = Bootstrap::getObjectManager(); + +$storeId = $objectManager->get(StoreManagerInterface::class)->getStore()->getId(); + +/** @var RatingResourceModel $ratingResourceModel */ +$ratingResourceModel = $objectManager->create(RatingResourceModel::class); + +/** @var RatingCollection $ratingCollection */ +$ratingCollection = $objectManager->create(RatingCollection::class)->setOrder('rating_code', 'ASC'); +$position = 0; + +foreach ($ratingCollection as $rating) { + $rating->setStores([$storeId])->setPosition($position++); + $ratingResourceModel->save($rating); +} diff --git a/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings_rollback.php b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings_rollback.php new file mode 100644 index 0000000000000..3a96a1be17a8b --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Review/_files/set_position_and_add_store_to_all_ratings_rollback.php @@ -0,0 +1,29 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +use Magento\Backend\App\Area\FrontNameResolver; +use Magento\Review\Model\ResourceModel\Rating\Collection as RatingCollection; +use Magento\Review\Model\ResourceModel\Rating as RatingResourceModel; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +Bootstrap::getInstance()->loadArea(FrontNameResolver::AREA_CODE); +$objectManager = Bootstrap::getObjectManager(); + +$storeId = Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId(); + +/** @var RatingResourceModel $ratingResourceModel */ +$ratingResourceModel = $objectManager->create(RatingResourceModel::class); + +/** @var RatingCollection $ratingCollection */ +$ratingCollection = Bootstrap::getObjectManager()->create(RatingCollection::class); + +foreach ($ratingCollection as $rating) { + $rating->setStores([])->setPosition(0); + $ratingResourceModel->save($rating); +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php new file mode 100644 index 0000000000000..0a8db20d86966 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Address/FormTest.php @@ -0,0 +1,75 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Address; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks order address edit form block + * + * @see \Magento\Sales\Block\Adminhtml\Order\Address\Form + * + * @magentoAppArea adminhtml + */ +class FormTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Form */ + private $block; + + /** @var Registry */ + private $registry; + + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Form::class); + $this->orderFactory = $this->objectManager->get(OrderInterfaceFactory::class); + $this->registry = $this->objectManager->get(Registry::class); + } + + /** + * @return void + */ + protected function tearDown(): void + { + $this->registry->unregister('order_address'); + + parent::tearDown(); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testGetFormValues(): void + { + $this->registry->unregister('order_address'); + $order = $this->orderFactory->create()->loadByIncrementId(100000001); + $address = $order->getShippingAddress(); + $this->registry->register('order_address', $address); + $formValues = $this->block->getFormValues(); + $this->assertEquals($address->getData(), $formValues); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/AddressTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/AddressTest.php new file mode 100644 index 0000000000000..8542d9bc48dcd --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/AddressTest.php @@ -0,0 +1,111 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order; + +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Framework\View\LayoutInterface; +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Model\Order\Address as AddressType; +use Magento\Sales\Model\OrderFactory; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Checks order address edit block + * + * @see \Magento\Sales\Block\Adminhtml\Order\Address + * + * @magentoAppArea adminhtml + */ +class AddressTest extends TestCase +{ + /** @var ObjectManagerInterface */ + private $objectManager; + + /** @var Address */ + private $block; + + /** @var Registry */ + private $registry; + + /** @var OrderFactory */ + private $orderFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->objectManager = Bootstrap::getObjectManager(); + $this->block = $this->objectManager->get(LayoutInterface::class)->createBlock(Address::class); + $this->registry = $this->objectManager->get(Registry::class); + $this->orderFactory = $this->objectManager->get(OrderFactory::class); + } + + /** + * @inheritdoc + */ + protected function tearDown(): void + { + $this->registry->unregister('order_address'); + + parent::tearDown(); + } + + /** + * @dataProvider addressTypeProvider + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @param string $type + * @return void + */ + public function testGetHeaderText(string $type): void + { + $order = $this->orderFactory->create()->loadByIncrementId(100000001); + $address = $this->getAddressByType($order, $type); + $this->registry->unregister('order_address'); + $this->registry->register('order_address', $address); + $text = $this->block->getHeaderText(); + $this->assertEquals( + (string)__('Edit Order %1 %2 Address', $order->getIncrementId(), ucfirst($type)), + (string)$text + ); + } + + /** + * @return array + */ + public function addressTypeProvider(): array + { + return [ + 'billing_address' => [ + AddressType::TYPE_BILLING, + ], + 'shipping_address' => [ + AddressType::TYPE_SHIPPING, + ] + ]; + } + + /** + * Get address by address type + * + * @param OrderInterface $order + * @param string $type + * @return OrderAddressInterface|null + */ + private function getAddressByType(OrderInterface $order, string $type): ?OrderAddressInterface + { + return $type === AddressType::TYPE_BILLING ? $order->getBillingAddress() : $order->getShippingAddress(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/AbstractAddressFormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/AbstractAddressFormTest.php new file mode 100644 index 0000000000000..5219fd72ec94e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/AbstractAddressFormTest.php @@ -0,0 +1,125 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create; + +use Magento\Config\Model\Config\Backend\Admin\Custom; +use Magento\Customer\Api\Data\AddressInterface; +use Magento\Customer\Model\Address\AddressModelInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Customer\Model\Metadata\FormFactory; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Data\Form; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Framework\View\LayoutInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Class consist of basic logic to check address form + */ +abstract class AbstractAddressFormTest extends TestCase +{ + /** @var LayoutInterface */ + protected $layout; + + /** @var CustomerRegistry */ + protected $customerRegistry; + + /** @var CartRepositoryInterface */ + private $quoteRepository; + + /** @var BlockInterface */ + private $form; + + /** @var ScopeConfigInterface */ + private $config; + + /** @var array */ + private $formAttributes; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $objectManager = Bootstrap::getObjectManager(); + $this->layout = $objectManager->get(LayoutInterface::class); + $this->form = $this->getFormBlock(); + $this->customerRegistry = $objectManager->get(CustomerRegistry::class); + $this->quoteRepository = $objectManager->get(CartRepositoryInterface::class); + $this->config = $objectManager->get(ScopeConfigInterface::class); + $this->formAttributes = array_keys($objectManager->get(FormFactory::class) + ->create('customer_address', 'adminhtml_customer_address')->getAttributes()); + } + + /** + * Check that all form values are filled according to address attributes values + * + * @param int $customerId + * @return void + */ + protected function checkFormValuesExist(int $customerId): void + { + $address = $this->getAddress($customerId); + $form = $this->prepareForm($customerId); + foreach ($this->formAttributes as $attribute) { + $this->assertEquals($address->getData($attribute), $form->getElement($attribute)->getValue()); + } + } + + /** + * Check that form values is empty + * + * @param int $customerId + * @return void + */ + protected function checkFormValuesAreEmpty(int $customerId): void + { + $defaultCountryCode = $this->config->getValue(Custom::XML_PATH_GENERAL_COUNTRY_DEFAULT); + $form = $this->prepareForm($customerId); + foreach ($this->formAttributes as $attribute) { + if ($attribute === AddressInterface::COUNTRY_ID) { + $this->assertEquals($defaultCountryCode, $form->getElement($attribute)->getValue()); + continue; + } + $this->assertNull($form->getElement($attribute)->getValue()); + } + } + + /** + * Prepare form + * + * @param int $customerId + * @return Form + */ + private function prepareForm(int $customerId): Form + { + $quote = $this->quoteRepository->getForCustomer($customerId); + $this->form->getCreateOrderModel()->setQuote($quote); + + return $this->form->getForm(); + } + + /** + * Get form block + * + * @return BlockInterface + */ + abstract protected function getFormBlock(): BlockInterface; + + /** + * Get appropriate customer address + * + * @param int $customerId + * @return AddressModelInterface + */ + abstract protected function getAddress(int $customerId): AddressModelInterface; +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Billing/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Billing/FormTest.php new file mode 100644 index 0000000000000..58a4616e3a219 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Billing/FormTest.php @@ -0,0 +1,59 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Billing; + +use Magento\Customer\Model\Address\AddressModelInterface; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Sales\Block\Adminhtml\Order\Create\AbstractAddressFormTest; +use Magento\Sales\Block\Adminhtml\Order\Create\Shipping\Address; + +/** + * Class checks billing address form behaviour + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + * + */ +class FormTest extends AbstractAddressFormTest +{ + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * + * @return void + */ + public function testFormValuesExist(): void + { + $this->checkFormValuesExist(1); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testFormValuesAreEmpty(): void + { + $this->checkFormValuesAreEmpty(1); + } + + /** + * @inheritdoc + */ + protected function getFormBlock(): BlockInterface + { + return $this->layout->createBlock(Address::class); + } + + /** + * @inheritdoc + */ + protected function getAddress(int $customerId): AddressModelInterface + { + return $this->customerRegistry->retrieve($customerId)->getDefaultBillingAddress(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php index 580f5a3a1dbc9..861559acd8c20 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Form/AccountTest.php @@ -94,21 +94,21 @@ public function testGetFormWithCustomer() $expectedFields = ['group_id', 'email']; $form = $this->accountBlock->getForm(); - self::assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); + $this->assertEquals(1, $form->getElements()->count(), "Form has invalid number of fieldsets"); $fieldset = $form->getElements()[0]; $content = $form->toHtml(); - self::assertEquals(count($expectedFields), $fieldset->getElements()->count()); + $this->assertEquals(count($expectedFields), $fieldset->getElements()->count()); foreach ($fieldset->getElements() as $element) { - self::assertTrue( + $this->assertTrue( in_array($element->getId(), $expectedFields), sprintf('Unexpected field "%s" in form.', $element->getId()) ); } - self::assertStringContainsString( - '<option value="'.$customerGroup.'" selected="selected">Wholesale</option>', + self::assertRegExp( + '/<option value="'.$customerGroup.'".*?selected="selected"\>Wholesale\<\/option\>/is', $content, 'The Customer Group specified for the chosen customer should be selected.' ); @@ -150,14 +150,14 @@ public function testGetFormWithUserDefinedAttribute() $form->setUseContainer(true); $content = $form->toHtml(); - self::assertStringContainsString( - '<option value="1" selected="selected">Yes</option>', + self::assertRegExp( + '/\<option value="1".*?selected="selected"\>Yes\<\/option\>/is', $content, 'Default value for user defined custom attribute should be selected.' ); - self::assertStringContainsString( - '<option value="3" selected="selected">Retailer</option>', + self::assertRegExp( + '/<option value="3".*?selected="selected"\>Retailer\<\/option\>/is', $content, 'The Customer Group specified for the chosen store should be selected.' ); diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/FormTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/FormTest.php new file mode 100644 index 0000000000000..5d6191d355e72 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/Create/Shipping/FormTest.php @@ -0,0 +1,57 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Block\Adminhtml\Order\Create\Shipping; + +use Magento\Customer\Model\Address\AddressModelInterface; +use Magento\Framework\View\Element\BlockInterface; +use Magento\Sales\Block\Adminhtml\Order\Create\AbstractAddressFormTest; + +/** + * Class checks shipping address form behaviour + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class FormTest extends AbstractAddressFormTest +{ + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_address_saved.php + * + * @return void + */ + public function testFormValuesExist(): void + { + $this->checkFormValuesExist(1); + } + + /** + * @magentoDataFixture Magento/Checkout/_files/quote_with_customer_without_address.php + * + * @return void + */ + public function testFormValuesAreEmpty(): void + { + $this->checkFormValuesAreEmpty(1); + } + + /** + * @inheritdoc + */ + protected function getFormBlock(): BlockInterface + { + return $this->layout->createBlock(Address::class); + } + + /** + * @inheritdoc + */ + protected function getAddress(int $customerId): AddressModelInterface + { + return $this->customerRegistry->retrieve($customerId)->getDefaultShippingAddress(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/TotalsTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/TotalsTest.php index 11c13258283fe..d7c37052247f5 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/TotalsTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Adminhtml/Order/TotalsTest.php @@ -72,6 +72,36 @@ public function testTotalsInclTax(): void $this->assertShipping($blockTotals->toHtml(), (float) $order->getShippingInclTax()); } + /** + * Test block totals including canceled amount. + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testTotalCanceled(): void + { + $order = $this->orderFactory->create()->loadByIncrementId('100000001'); + $order->cancel(); + $blockTotals = $this->getBlockTotals()->setOrder($order); + $this->assertCanceled($blockTotals->toHtml(), $order->getTotalCanceled()); + } + + /** + * Check if canceled amount present in block. + * + * @param string $blockTotalsHtml + * @param float $amount + * @return void + */ + private function assertCanceled(string $blockTotalsHtml, float $amount): void + { + $this->assertTrue( + $this->isBlockContainsTotalAmount($blockTotalsHtml, __('Total Canceled'), $amount), + 'Canceled amount is missing or incorrect.' + ); + } + /** * Check if subtotal amount present in block. * diff --git a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/ShipmentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/ShipmentTest.php index 434dacec5c6b8..384445a7cd5f8 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/ShipmentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Block/Order/PrintOrder/ShipmentTest.php @@ -151,7 +151,7 @@ private function assertOrderInformation(OrderInterface $order, string $html): vo foreach ([$order->getShippingAddress(), $order->getBillingAddress()] as $address) { $addressBoxXpath = ($address->getAddressType() == 'shipping') ? "//div[contains(@class, 'box-order-shipping-address')]//address[contains(., '%s')]" - : "//div[contains(@class, 'box-order-billing-method')]//address[contains(., '%s')]"; + : "//div[contains(@class, 'box-order-billing-address')]//address[contains(., '%s')]"; $this->assertEquals( 1, Xpath::getElementsCountForXpath(sprintf($addressBoxXpath, $address->getName()), $html), diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/AddressSaveTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/AddressSaveTest.php new file mode 100644 index 0000000000000..31b5fdd81f592 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/AddressSaveTest.php @@ -0,0 +1,168 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order; + +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Api\OrderAddressRepositoryInterface; +use Magento\Sales\Model\Order\Address as AddressType; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Class check address save action + * + * @see \Magento\Sales\Controller\Adminhtml\Order\AddressSave + * + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + */ +class AddressSaveTest extends AbstractBackendController +{ + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var OrderAddressRepositoryInterface */ + private $orderAddressRepository; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->orderAddressRepository = $this->_objectManager->get(OrderAddressRepositoryInterface::class); + } + + /** + * @dataProvider addressTypeProvider + * + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @param string $type + * @return void + */ + public function testSave(string $type): void + { + $data = [ + OrderAddressInterface::FIRSTNAME => 'New test name', + OrderAddressInterface::LASTNAME => 'New test lastname', + OrderAddressInterface::STREET => ['new test street'], + OrderAddressInterface::CITY => 'New Test City', + OrderAddressInterface::COUNTRY_ID => 'UA', + OrderAddressInterface::REGION => '1111', + OrderAddressInterface::POSTCODE => '97203', + OrderAddressInterface::TELEPHONE => '5555555555', + ]; + $order = $this->orderFactory->create()->loadByIncrementId(100000001); + $addressId = $this->getAddressIdByType($order, $type); + $this->dispatchWithParams( + ['address_id' => $addressId], + $data + ); + $this->assertSessionMessages( + $this->containsEqual((string)__('You updated the order address.')) + ); + $this->assertRedirect( + $this->stringContains(sprintf('sales/order/view/order_id/%s/', $order->getId())) + ); + $this->assertAddressData($addressId, $data); + } + + /** + * @return array + */ + public function addressTypeProvider(): array + { + return [ + 'billing_address' => [ + AddressType::TYPE_BILLING, + ], + 'shipping_address' => [ + AddressType::TYPE_SHIPPING, + ] + ]; + } + + /** + * @dataProvider wrongRequestDataProvider + * + * @param array $params + * @param array $post + * @return void + */ + public function testInvalidRequest(array $params, array $post = []): void + { + $this->dispatchWithParams($params, $post); + $this->assertRedirect($this->stringContains('backend/sales/order/index/')); + } + + /** + * @return array + */ + public function wrongRequestDataProvider(): array + { + return [ + 'empty_post' => [ + ['address_id' => 1], + ], + 'wrong_address_id' => [ + ['address_id' => 7852147], + ], + ]; + } + + /** + * Check updated address data + * + * @param int $addressId + * @param array $expectedData + * @return void + */ + private function assertAddressData(int $addressId, array $expectedData): void + { + $address = $this->orderAddressRepository->get($addressId); + foreach ($expectedData as $key => $value) { + $key === OrderAddressInterface::STREET + ? $this->assertEquals(reset($value), $address->getData($key)) + : $this->assertEquals($value, $address->getData($key)); + } + } + + /** + * Get address id by address type + * + * @param OrderInterface $order + * @param string $type + * @return int + */ + private function getAddressIdByType(OrderInterface $order, string $type): int + { + return $type === AddressType::TYPE_BILLING + ? (int)$order->getBillingAddressId() + : (int)$order->getShippingAddressId(); + } + + /** + * Dispatch with params + * + * @param array $params + * @param array $post + * @return void + */ + private function dispatchWithParams(array $params, array $post): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParams($params); + $this->getRequest()->setPostValue($post); + $this->dispatch('backend/sales/order/addressSave'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/AddressTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/AddressTest.php new file mode 100644 index 0000000000000..6b508b662e7a4 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/AddressTest.php @@ -0,0 +1,76 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order; + +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Framework\Registry; +use Magento\Sales\Api\Data\OrderAddressInterface; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Class check address edit action + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Address + * + * @magentoDbIsolation disabled + * @magentoAppArea adminhtml + */ +class AddressTest extends AbstractBackendController +{ + /** @var OrderInterfaceFactory */ + private $orderFactory; + + /** @var Registry */ + private $registry; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->orderFactory = $this->_objectManager->get(OrderInterfaceFactory::class); + $this->registry = $this->_objectManager->get(Registry::class); + } + + /** + * @magentoDataFixture Magento/Sales/_files/order.php + * + * @return void + */ + public function testSuccessfulEdit(): void + { + $order = $this->orderFactory->create()->loadByIncrementId(100000001); + $this->dispatchWithAddressId((int)$order->getBillingAddressId()); + $this->assertInstanceOf(OrderAddressInterface::class, $this->registry->registry('order_address')); + } + + /** + * @return void + */ + public function testWithNotExistingAddressId(): void + { + $this->dispatchWithAddressId(51728); + $this->assertRedirect($this->stringContains('backend/sales/order/index/')); + } + + /** + * Dispatch request with address_id param + * + * @param int $addressId + * @return void + */ + private function dispatchWithAddressId(int $addressId): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setParam('address_id', $addressId); + $this->dispatch('backend/sales/order/address'); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/IndexTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/IndexTest.php new file mode 100644 index 0000000000000..764c48b523968 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Create/IndexTest.php @@ -0,0 +1,67 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Controller\Adminhtml\Order\Create; + +use Magento\Backend\Model\Session\Quote; +use Magento\Framework\App\Request\Http; +use Magento\Framework\Registry; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\TestCase\AbstractBackendController; + +/** + * Class checks create order index controller. + * + * @see \Magento\Sales\Controller\Adminhtml\Order\Create\Index + * + * @magentoAppArea adminhtml + * @magentoDbIsolation enabled + */ +class IndexTest extends AbstractBackendController +{ + /** @var Registry */ + private $registry; + + /** @var StoreManagerInterface */ + private $storeManager; + + /** @var Quote */ + private $quoteSession; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->registry = $this->_objectManager->get(Registry::class); + $this->storeManager = $this->_objectManager->get(StoreManagerInterface::class); + $this->quoteSession = $this->_objectManager->get(Quote::class); + } + + /** + * @magentoDataFixture Magento/Customer/_files/customer.php + * + * @return void + */ + public function testExecute(): void + { + $customerId = 1; + $this->getRequest()->setMethod(Http::METHOD_GET); + $this->getRequest()->setParam('customer_id', $customerId); + $this->dispatch('backend/sales/order_create/index'); + $store = $this->storeManager->getStore(); + $this->assertEquals($customerId, $this->quoteSession->getCustomerId()); + $ruleData = $this->registry->registry('rule_data'); + $this->assertNotNull($ruleData); + $this->assertEquals( + ['store_id' => $store->getId(), 'website_id' => $store->getWebsiteId(), 'customer_group_id' => 1], + $ruleData->getData() + ); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php index e5cc9e49a0186..52dafc0590afb 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Creditmemo/AddCommentTest.php @@ -42,7 +42,7 @@ public function testSendEmailOnAddCreditmemoComment(): void $message = $this->transportBuilder->getSentMessage(); $subject =__('Update to your %1 credit memo', $order->getStore()->getFrontendName())->render(); $messageConstraint = $this->logicalAnd( - new StringContains($order->getBillingAddress()->getName()), + new StringContains($order->getCustomerName()), new RegularExpression( sprintf( "/Your order #%s has been updated with a status of.*%s/", diff --git a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php index f77812b96b306..ee59a55acd9b1 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Controller/Adminhtml/Order/Invoice/AddCommentTest.php @@ -43,7 +43,7 @@ public function testSendEmailOnAddInvoiceComment(): void $message = $this->transportBuilder->getSentMessage(); $subject = __('Update to your %1 invoice', $order->getStore()->getFrontendName())->render(); $messageConstraint = $this->logicalAnd( - new StringContains($order->getBillingAddress()->getName()), + new StringContains($order->getCustomerName()), new RegularExpression( sprintf( "/Your order #%s has been updated with a status of.*%s/", diff --git a/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php b/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php index 6605d43c84264..3919f91a3241e 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/CustomerData/LastOrderedItemsTest.php @@ -42,7 +42,7 @@ public function testDefaultFormatterIsAppliedWhenBasicIntegration() $this->assertEquals( LastOrderedItems::SIDEBAR_ORDER_LIMIT, count($data['items']), - 'Section items count should not be greater then ' . LastOrderedItems::SIDEBAR_ORDER_LIMIT + 'Section items count should not be greater than ' . LastOrderedItems::SIDEBAR_ORDER_LIMIT ); } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php index ca7e6fc41888b..e1cc942d4ae28 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/AdminOrder/CreateTest.php @@ -249,6 +249,7 @@ public function testGetCustomerWishlistNoCustomerId() * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/Catalog/_files/product_simple.php * @magentoAppIsolation enabled + * @magentoDbIsolation disabled */ public function testGetCustomerWishlist() { diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php index 72e741493d8f8..bc51f8acb2f6f 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/CreditmemoSenderTest.php @@ -5,10 +5,33 @@ */ namespace Magento\Sales\Model\Order\Email\Sender; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Container\CreditmemoIdentity; use Magento\TestFramework\Helper\Bootstrap; class CreditmemoSenderTest extends \PHPUnit\Framework\TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const OLD_CUSTOMER_EMAIL = 'customer@null.com'; + const ORDER_EMAIL = 'customer@null.com'; + + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager() + ->get(CustomerRepositoryInterface::class); + } + /** * @magentoDataFixture Magento/Sales/_files/order.php */ @@ -35,4 +58,110 @@ public function testSend() $this->assertTrue($result); $this->assertNotEmpty($creditmemo->getEmailSent()); } + + /** + * Test that when a customer email is modified, the credit memo is sent to the new email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasModified() + { + $customer = $this->customerRepository->getById(1); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + $this->customerRepository->save($customer); + + $order = $this->createOrder(); + $creditmemo = $this->createCreditmemo($order); + + $this->assertEmpty($creditmemo->getEmailSent()); + + $craditmemoIdentity = $this->createCreditMemoIdentity(); + $creditmemoSender = $this->createCreditMemoSender($craditmemoIdentity); + $result = $creditmemoSender->send($creditmemo, true); + + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $craditmemoIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($creditmemo->getEmailSent()); + } + + /** + * Test that when a customer email is not modified, the credit memo is sent to the old customer email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasNotModified() + { + $order = $this->createOrder(); + $creditmemo = $this->createCreditmemo($order); + + $this->assertEmpty($creditmemo->getEmailSent()); + + $craditmemoIdentity = $this->createCreditMemoIdentity(); + $creditmemoSender = $this->createCreditMemoSender($craditmemoIdentity); + $result = $creditmemoSender->send($creditmemo, true); + + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $craditmemoIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($creditmemo->getEmailSent()); + } + + /** + * Test that when an order has not customer the credit memo is sent to the order email + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend + */ + public function testSendWithoutCustomer() + { + $order = $this->createOrder(); + $creditmemo = $this->createCreditmemo($order); + + $this->assertEmpty($creditmemo->getEmailSent()); + + $creditmemoIdentity = $this->createCreditMemoIdentity(); + $creditmemoSender = $this->createCreditMemoSender($creditmemoIdentity); + $result = $creditmemoSender->send($creditmemo, true); + + $this->assertEquals(self::ORDER_EMAIL, $creditmemoIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($creditmemo->getEmailSent()); + } + + private function createCreditmemo(Order $order): Order\Creditmemo + { + $creditmemo = Bootstrap::getObjectManager()->create( + \Magento\Sales\Model\Order\Creditmemo::class + ); + $creditmemo->setOrder($order); + return $creditmemo; + } + + private function createOrder(): Order + { + $order = Bootstrap::getObjectManager() + ->create(Order::class); + $order->loadByIncrementId('100000001'); + + return $order; + } + + private function createCreditMemoIdentity(): CreditmemoIdentity + { + return Bootstrap::getObjectManager()->create( + CreditmemoIdentity::class + ); + } + + private function createCreditMemoSender(CreditmemoIdentity $creditmemoIdentity): CreditmemoSender + { + return Bootstrap::getObjectManager() + ->create( + CreditmemoSender::class, + [ + 'identityContainer' => $creditmemoIdentity, + ] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php index fa3421fe9cc94..55af8e9d2ee62 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/InvoiceSenderTest.php @@ -3,35 +3,178 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Sales\Model\Order\Email\Sender; -class InvoiceSenderTest extends \PHPUnit\Framework\TestCase +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Container\InvoiceIdentity; +use Magento\Sales\Model\Order\Invoice; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Framework\App\Area; +use PHPUnit\Framework\TestCase; + +class InvoiceSenderTest extends TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const OLD_CUSTOMER_EMAIL = 'customer@null.com'; + const ORDER_EMAIL = 'customer@null.com'; + + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager() + ->get(CustomerRepositoryInterface::class); + } + /** * @magentoDataFixture Magento/Sales/_files/order.php */ public function testSend() { - \Magento\TestFramework\Helper\Bootstrap::getInstance() - ->loadArea(\Magento\Framework\App\Area::AREA_FRONTEND); - $order = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Sales\Model\Order::class); + Bootstrap::getInstance() + ->loadArea(Area::AREA_FRONTEND); + $order = Bootstrap::getObjectManager() + ->create(Order::class); $order->loadByIncrementId('100000001'); $order->setCustomerEmail('customer@example.com'); - $invoice = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Sales\Model\Order\Invoice::class + $invoice = Bootstrap::getObjectManager()->create( + Invoice::class ); $invoice->setOrder($order); + $invoice->setTotalQty(1); + $invoice->setBaseSubtotal(50); + $invoice->setBaseTaxAmount(10); + $invoice->setBaseShippingAmount(5); + /** @var InvoiceSender $invoiceSender */ + $invoiceSender = Bootstrap::getObjectManager() + ->create(InvoiceSender::class); + + $this->assertEmpty($invoice->getEmailSent()); + $result = $invoiceSender->send($invoice, true); + $this->assertTrue($result); + $this->assertNotEmpty($invoice->getEmailSent()); + $this->assertEquals($invoice->getBaseSubtotal(), $order->getBaseSubtotal()); + $this->assertEquals($invoice->getBaseTaxAmount(), $order->getBaseTaxAmount()); + $this->assertEquals($invoice->getBaseShippingAmount(), $order->getBaseShippingAmount()); + } + + /** + * Test that when a customer email is modified, the invoice is sent to the new email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasModified() + { + $customer = $this->customerRepository->getById(1); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + $this->customerRepository->save($customer); + + $order = $this->createOrder(); + $invoice = $this->createInvoice($order); + $invoiceIdentity = $this->createInvoiceEntity(); + $invoiceSender = $this->createInvoiceSender($invoiceIdentity); + + $this->assertEmpty($invoice->getEmailSent()); + $result = $invoiceSender->send($invoice, true); + + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($invoice->getEmailSent()); + } + + /** + * Test that when a customer email is not modified, the invoice is sent to the old customer email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasNotModified() + { + $order = $this->createOrder(); + $invoice = $this->createInvoice($order); + $invoiceIdentity = $this->createInvoiceEntity(); + $invoiceSender = $this->createInvoiceSender($invoiceIdentity); + + $this->assertEmpty($invoice->getEmailSent()); + $result = $invoiceSender->send($invoice, true); + + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $invoiceIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($invoice->getEmailSent()); + } + + /** + * Test that when an order has not customer the invoice is sent to the order email + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend + */ + public function testSendWithoutCustomer() + { + $order = $this->createOrder(); + $invoice = $this->createInvoice($order); + + /** @var InvoiceIdentity $invoiceIdentity */ + $invoiceIdentity = $this->createInvoiceEntity(); /** @var InvoiceSender $invoiceSender */ - $invoiceSender = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Sales\Model\Order\Email\Sender\InvoiceSender::class); + $invoiceSender = $this->createInvoiceSender($invoiceIdentity); $this->assertEmpty($invoice->getEmailSent()); $result = $invoiceSender->send($invoice, true); + $this->assertEquals(self::ORDER_EMAIL, $invoiceIdentity->getCustomerEmail()); $this->assertTrue($result); $this->assertNotEmpty($invoice->getEmailSent()); } + + private function createInvoice(Order $order): Invoice + { + $invoice = Bootstrap::getObjectManager()->create( + Invoice::class + ); + $invoice->setOrder($order); + + return $invoice; + } + + private function createOrder(): Order + { + $order = Bootstrap::getObjectManager() + ->create(Order::class); + $order->loadByIncrementId('100000001'); + + return $order; + } + + private function createInvoiceEntity(): InvoiceIdentity + { + return Bootstrap::getObjectManager()->create( + InvoiceIdentity::class + ); + } + + private function createInvoiceSender(InvoiceIdentity $invoiceIdentity): InvoiceSender + { + return Bootstrap::getObjectManager() + ->create( + InvoiceSender::class, + [ + 'identityContainer' => $invoiceIdentity, + ] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php index 83bc7e10647b4..42d8e2bc0bcbb 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/Order/Email/Sender/ShipmentSenderTest.php @@ -5,6 +5,11 @@ */ namespace Magento\Sales\Model\Order\Email\Sender; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\Customer\Model\ResourceModel\CustomerRepository; +use Magento\Sales\Model\Order; +use Magento\Sales\Model\Order\Email\Container\ShipmentIdentity; +use Magento\Sales\Model\Order\Shipment; use Magento\Sales\Model\Order\ShipmentFactory; use Magento\TestFramework\Helper\Bootstrap; @@ -16,6 +21,25 @@ */ class ShipmentSenderTest extends \PHPUnit\Framework\TestCase { + const NEW_CUSTOMER_EMAIL = 'new.customer@example.com'; + const OLD_CUSTOMER_EMAIL = 'customer@null.com'; + const ORDER_EMAIL = 'customer@null.com'; + + /** + * @var CustomerRepository + */ + private $customerRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->customerRepository = Bootstrap::getObjectManager() + ->get(CustomerRepositoryInterface::class); + } + /** * @magentoDataFixture Magento/Sales/_files/order.php */ @@ -39,6 +63,76 @@ public function testSend() $this->assertNotEmpty($shipment->getEmailSent()); } + /** + * Test that when a customer email is modified, the shipment is sent to the new email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasModified() + { + $customer = $this->customerRepository->getById(1); + $customer->setEmail(self::NEW_CUSTOMER_EMAIL); + $this->customerRepository->save($customer); + + $order = $this->createOrder(); + $shipment = $this->createShipment($order); + $shipmentIdentity = $this->createShipmentEntity(); + $shipmentSender = $this->createShipmentSender($shipmentIdentity); + + $this->assertEmpty($shipment->getEmailSent()); + $result = $shipmentSender->send($shipment, true); + + $this->assertEquals(self::NEW_CUSTOMER_EMAIL, $shipmentIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($shipment->getEmailSent()); + } + + /** + * Test that when a customer email is not modified, the shipment is sent to the old customer email + * + * @magentoDataFixture Magento/Sales/_files/order_with_customer.php + * @magentoAppArea frontend + */ + public function testSendWhenCustomerEmailWasNotModified() + { + $order = $this->createOrder(); + $shipment = $this->createShipment($order); + $shipmentIdentity = $this->createShipmentEntity(); + $shipmentSender = $this->createShipmentSender($shipmentIdentity); + + $this->assertEmpty($shipment->getEmailSent()); + $result = $shipmentSender->send($shipment, true); + + $this->assertEquals(self::OLD_CUSTOMER_EMAIL, $shipmentIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($shipment->getEmailSent()); + } + + /** + * Test that when an order has not customer the shipment is sent to the order email + * + * @magentoDataFixture Magento/Sales/_files/order.php + * @magentoAppArea frontend + */ + public function testSendWithoutCustomer() + { + $order = $this->createOrder(); + $shipment = $this->createShipment($order); + + /** @var ShipmentIdentity $shipmentIdentity */ + $shipmentIdentity = $this->createShipmentEntity(); + /** @var ShipmentSender $shipmentSender */ + $shipmentSender = $this->createShipmentSender($shipmentIdentity); + + $this->assertEmpty($shipment->getEmailSent()); + $result = $shipmentSender->send($shipment, true); + + $this->assertEquals(self::ORDER_EMAIL, $shipmentIdentity->getCustomerEmail()); + $this->assertTrue($result); + $this->assertNotEmpty($shipment->getEmailSent()); + } + /** * Check the correctness and stability of set/get packages of shipment * @@ -65,4 +159,41 @@ public function testPackages() $shipment->load($shipment->getId()); $this->assertEquals($packages, $shipment->getPackages()); } + + private function createShipment(Order $order): Shipment + { + $shipment = Bootstrap::getObjectManager()->create( + Shipment::class + ); + $shipment->setOrder($order); + + return $shipment; + } + + private function createOrder(): Order + { + $order = Bootstrap::getObjectManager() + ->create(Order::class); + $order->loadByIncrementId('100000001'); + + return $order; + } + + private function createShipmentEntity(): ShipmentIdentity + { + return Bootstrap::getObjectManager()->create( + ShipmentIdentity::class + ); + } + + private function createShipmentSender(ShipmentIdentity $shipmentIdentity): ShipmentSender + { + return Bootstrap::getObjectManager() + ->create( + ShipmentSender::class, + [ + 'identityContainer' => $shipmentIdentity, + ] + ); + } } diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Address/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Address/CollectionTest.php new file mode 100644 index 0000000000000..52284b3c9ddf9 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/Order/Address/CollectionTest.php @@ -0,0 +1,101 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sales\Model\ResourceModel\Order\Address; + +use Magento\Store\Model\StoreManagerInterface; +use Magento\Sales\Model\Order\Payment; +use Magento\Sales\Model\Order; +use Magento\Sales\Api\Data\OrderAddressInterface as OrderAddress; +use Magento\Backend\Model\Locale\Resolver; +use Magento\Framework\Locale\ResolverInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * Test for address collection + * + * @magentoAppArea adminhtml + */ +class CollectionTest extends TestCase +{ + /** + * @var ResolverInterface|MockObject + */ + private $localeResolverMock; + + /** + * @var CollectionFactory + */ + private $addressCollectionFactory; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + $this->localeResolverMock = $this->createMock(ResolverInterface::class); + Bootstrap::getObjectManager()->removeSharedInstance(ResolverInterface::class); + Bootstrap::getObjectManager()->removeSharedInstance(Resolver::class); + Bootstrap::getObjectManager()->addSharedInstance($this->localeResolverMock, ResolverInterface::class); + Bootstrap::getObjectManager()->addSharedInstance($this->localeResolverMock, Resolver::class); + + $addressData = [ + OrderAddress::REGION => 'Alabama', + OrderAddress::REGION_ID => '1', + OrderAddress::POSTCODE => '11111', + OrderAddress::LASTNAME => 'lastname', + OrderAddress::FIRSTNAME => 'firstname', + OrderAddress::STREET => 'street', + OrderAddress::CITY => 'Montgomery', + OrderAddress::EMAIL => 'admin@example.com', + OrderAddress::TELEPHONE => '11111111', + OrderAddress::COUNTRY_ID => 'US' + ]; + $billingAddress = Bootstrap::getObjectManager()->create(OrderAddress::class, ['data' => $addressData]); + $billingAddress->setAddressType('billing'); + $shippingAddress = clone $billingAddress; + $shippingAddress->setId(null)->setAddressType('shipping'); + $payment = Bootstrap::getObjectManager()->create(Payment::class); + $payment->setMethod('payflowpro') + ->setCcExpMonth('5') + ->setCcLast4('0005') + ->setCcType('AE') + ->setCcExpYear('2022'); + $order = Bootstrap::getObjectManager()->create(Order::class); + $order->setIncrementId('100000001') + ->setSubtotal(100) + ->setBaseSubtotal(100) + ->setCustomerEmail('admin@example.com') + ->setCustomerIsGuest(true) + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId(Bootstrap::getObjectManager()->get(StoreManagerInterface::class)->getStore()->getId()) + ->setPayment($payment); + $order->save(); + + $this->addressCollectionFactory = Bootstrap::getObjectManager()->get(CollectionFactory::class); + } + + /** + * @magentoDataFixture Magento/Directory/_files/region_name_jp.php + */ + public function testCollectionWithJpLocale(): void + { + $locale = 'JA_jp'; + $this->localeResolverMock->method('getLocale')->willReturn($locale); + + $order = Bootstrap::getObjectManager()->create(Order::class) + ->loadByIncrementId('100000001'); + + $collection = $this->addressCollectionFactory->create()->setOrderFilter($order); + foreach ($collection as $address) { + $this->assertEquals('アラバマ', $address->getData(OrderAddress::REGION)); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php index c7aa78d96f5e6..74a9ccc35d379 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php +++ b/dev/tests/integration/testsuite/Magento/Sales/Model/ResourceModel/OrderTest.php @@ -3,34 +3,46 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Sales\Model\ResourceModel; -use Magento\Store\Api\StoreRepositoryInterface; -use Magento\Store\Model\StoreManagerInterface; use Magento\Framework\Event\ManagerInterface; +use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\Registry; +use Magento\Sales\Api\OrderRepositoryInterface; +use Magento\Sales\Model\Order as OrderModel; +use Magento\Sales\Model\Order\Address; +use Magento\Sales\Model\Order\Item; +use Magento\Sales\Model\Order\Payment; use Magento\Sales\Model\ResourceModel\Order\CollectionFactory as OrderCollectionFactory; +use Magento\Store\Api\StoreRepositoryInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; /** + * Test for \Magento\Sales\Model\ResourceModel\Order. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ -class OrderTest extends \PHPUnit\Framework\TestCase +class OrderTest extends TestCase { /** - * @var \Magento\Sales\Model\ResourceModel\Order + * @var Order */ - protected $resourceModel; + private $resourceModel; /** * @var int */ - protected $orderIncrementId; + private $orderIncrementId; /** - * @var \Magento\Framework\ObjectManagerInterface + * @var ObjectManagerInterface */ - protected $objectManager; + private $objectManager; /** * @var StoreManagerInterface @@ -47,8 +59,9 @@ class OrderTest extends \PHPUnit\Framework\TestCase */ protected function setUp(): void { - $this->objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $this->resourceModel = $this->objectManager->create(\Magento\Sales\Model\ResourceModel\Order::class); + $this->objectManager = Bootstrap::getObjectManager(); + + $this->resourceModel = $this->objectManager->create(Order::class); $this->orderIncrementId = '100000001'; $this->storeManager = $this->objectManager->get(StoreManagerInterface::class); $this->storeRepository = $this->objectManager->get(StoreRepositoryInterface::class); @@ -59,7 +72,7 @@ protected function setUp(): void */ protected function tearDown(): void { - $registry = $this->objectManager->get(\Magento\Framework\Registry::class); + $registry = $this->objectManager->get(Registry::class); $registry->unregister('isSecureArea'); $registry->register('isSecureArea', true); @@ -73,14 +86,15 @@ protected function tearDown(): void $defaultStore = $this->storeRepository->get('default'); $this->storeManager->setCurrentStore($defaultStore->getId()); - - parent::tearDown(); } /** + * Test save order + * * @magentoDataFixture Magento/Catalog/_files/product_simple.php + * @return void */ - public function testSaveOrder() + public function testSaveOrder(): void { $addressData = [ 'region' => 'CA', @@ -94,31 +108,28 @@ public function testSaveOrder() 'country_id' => 'US' ]; - $billingAddress = $this->objectManager->create( - \Magento\Sales\Model\Order\Address::class, - ['data' => $addressData] - ); + $billingAddress = $this->objectManager->create(Address::class, ['data' => $addressData]); $billingAddress->setAddressType('billing'); $shippingAddress = clone $billingAddress; $shippingAddress->setId(null)->setAddressType('shipping'); - $payment = $this->objectManager->create(\Magento\Sales\Model\Order\Payment::class); + $payment = $this->objectManager->create(Payment::class); $payment->setMethod('checkmo'); - /** @var \Magento\Sales\Model\Order\Item $orderItem */ - $orderItem = $this->objectManager->create(\Magento\Sales\Model\Order\Item::class); + /** @var Item $orderItem */ + $orderItem = $this->objectManager->create(Item::class); $orderItem->setProductId(1) ->setQtyOrdered(2) ->setBasePrice(10) ->setPrice(10) ->setRowTotal(10); - /** @var \Magento\Sales\Model\Order $order */ - $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); + /** @var OrderModel $order */ + $order = $this->objectManager->create(OrderModel::class); $order->setIncrementId($this->orderIncrementId) - ->setState(\Magento\Sales\Model\Order::STATE_PROCESSING) - ->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)) + ->setState(OrderModel::STATE_PROCESSING) + ->setStatus($order->getConfig()->getStateDefaultStatus(OrderModel::STATE_PROCESSING)) ->setSubtotal(100) ->setBaseSubtotal(100) ->setBaseGrandTotal(100) @@ -128,7 +139,7 @@ public function testSaveOrder() ->setShippingAddress($shippingAddress) ->setStoreId( $this->objectManager - ->get(\Magento\Store\Model\StoreManagerInterface::class) + ->get(StoreManagerInterface::class) ->getStore() ->getId() ) @@ -141,26 +152,36 @@ public function testSaveOrder() } /** - * Check that store name with length within 255 chars can be saved in table sales_order + * Check that store name and x_forwarded_for with length within 255 chars can be saved in table sales_order * * @magentoDataFixture Magento/Store/_files/store_with_long_name.php * @magentoDbIsolation disabled * @return void */ - public function testSaveStoreName() + public function testSaveLongNames(): void { + $xForwardedFor = str_repeat('x', 255); + $store = $this->storeRepository->get('test_2'); $this->storeManager->setCurrentStore($store->getId()); $eventManager = $this->objectManager->get(ManagerInterface::class); $eventManager->dispatch('store_add', ['store' => $store]); - $order = $this->objectManager->create(\Magento\Sales\Model\Order::class); - $payment = $this->objectManager->create(\Magento\Sales\Model\Order\Payment::class); + $order = $this->objectManager->create(OrderModel::class); + $payment = $this->objectManager->create(Payment::class); $payment->setMethod('checkmo'); - $order->setStoreId($store->getId())->setPayment($payment); + + $order->setStoreId($store->getId()); + $order->setXForwardedFor($xForwardedFor); + $order->setPayment($payment); $this->resourceModel->save($order); - $orderRepository = $this->objectManager->create(\Magento\Sales\Api\OrderRepositoryInterface::class); + + $orderRepository = $this->objectManager->create(OrderRepositoryInterface::class); $order = $orderRepository->get($order->getId()); + $this->assertEquals(255, strlen($order->getStoreName())); + $this->assertEquals(255, strlen($order->getXForwardedFor())); + + $this->assertEquals($xForwardedFor, $order->getXForwardedFor()); $this->assertStringContainsString($store->getWebsite()->getName(), $order->getStoreName()); $this->assertStringContainsString($store->getGroup()->getName(), $order->getStoreName()); } diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_creditmemo_with_two_items.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_creditmemo_with_two_items.php new file mode 100644 index 0000000000000..478a10665cd7e --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_creditmemo_with_two_items.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Sales\Model\Order\CreditmemoFactory; +use Magento\Sales\Model\Service\CreditmemoService; +use Magento\Sales\Model\Order; +use Magento\TestFramework\Helper\Bootstrap; + +Resolver::getInstance()->requireDataFixture( + 'Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php' +); + +$objectManager = Bootstrap::getObjectManager(); + +/** @var CreditmemoFactory $creditMemoFactory */ +$creditMemoFactory = $objectManager->create(CreditmemoFactory::class); +/** @var CreditmemoService $creditMemoService */ +$creditMemoService = $objectManager->create(CreditmemoService::class); + +/** @var Order $order */ +$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); + +$creditMemo = $creditMemoFactory->createByOrder($order); +$creditMemo->setAdjustment(1.23); +$creditMemo->setBaseGrandTotal(10); +$creditMemo->addComment('some_comment', false, true); +$creditMemo->addComment('some_other_comment', false, true); +$creditMemo->addComment('not_visible', false, false); + +$creditMemoService->refund($creditMemo); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_creditmemo_with_two_items_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_creditmemo_with_two_items_rollback.php new file mode 100644 index 0000000000000..b8a065f9383d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_creditmemo_with_two_items_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php new file mode 100644 index 0000000000000..c14ff93b6393a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options.php @@ -0,0 +1,115 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Sales\Api\InvoiceManagementInterface; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'year' => '2015', + 'month' => '9', + 'day' => '9', + 'hour' => '2', + 'minute' => '2', + 'day_part' => 'am', + 'date_internal' => '', + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product2 = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product2 = $repository->get('simple_with_cross'); + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setName($product->getName()); +$orderItem->setSku($product->getSku()); +$orderItem->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order\Item $orderItem2 */ +$orderItem2 = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem2->setProductId($product2->getId()); +$orderItem2->setSku($product2->getSku()); +$orderItem2->setName($product2->getName()); +$orderItem2->setQtyOrdered(1); +$orderItem2->setBasePrice($product2->getPrice()); +$orderItem2->setPrice($product2->getPrice()); +$orderItem2->setRowTotal($product2->getPrice()); +$orderItem2->setProductType($product2->getTypeId()); +$orderItem2->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000001'); +$order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@null.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->addItem($orderItem2); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(100); +$order->setBaseSubtotal(100); +$order->setBaseGrandTotal(100); +$order->setGrandTotal(100); +$order->setOrderCurrencyCode('USD'); +$order->setBaseCurrencyCode('EUR'); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); +/** @var InvoiceManagementInterface $orderService */ +$orderService = $objectManager->create( + InvoiceManagementInterface::class +); +$invoice = $orderService->prepareInvoice($order); +$invoice->register(); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options_rollback.php new file mode 100644 index 0000000000000..80d6adb0cd9fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_invoice_with_two_products_and_custom_options_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php new file mode 100644 index 0000000000000..39ac3d1912b93 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options.php @@ -0,0 +1,130 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'year' => '2015', + 'month' => '9', + 'day' => '9', + 'hour' => '2', + 'minute' => '2', + 'day_part' => 'am', + 'date_internal' => '', + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var $product \Magento\Catalog\Model\Product */ +$product2 = $objectManager->create(\Magento\Catalog\Model\Product::class); +$product2 = $repository->get('simple_with_cross'); + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setName($product->getName()); +$orderItem->setSku($product->getSku()); +$orderItem->setQtyOrdered(4); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order\Item $orderItem2 */ +$orderItem2 = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem2->setProductId($product2->getId()); +$orderItem2->setSku($product2->getSku()); +$orderItem2->setName($product2->getName()); +$orderItem2->setQtyOrdered(1); +$orderItem2->setBasePrice($product2->getPrice()); +$orderItem2->setPrice($product2->getPrice()); +$orderItem2->setRowTotal($product2->getPrice()); +$orderItem2->setProductType($product2->getTypeId()); +$orderItem2->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000002'); +$order->setState(\Magento\Sales\Model\Order::STATE_PROCESSING); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_PROCESSING)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@null.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->addItem($orderItem2); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(60); +$order->setBaseSubtotal(60); +$order->setBaseGrandTotal(60); +$order->setGrandTotal(60); +$order->setOrderCurrencyCode('USD'); +$order->setBaseCurrencyCode('EUR'); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); + +$orderService = $objectManager->create( + \Magento\Sales\Api\InvoiceManagementInterface::class +); +/** @var \Magento\Sales\Api\Data\InvoiceInterface $invoice */ +$invoice = $orderService->prepareInvoice($order, [$orderItem->getId() => 3]); +$invoice->register(); +$invoice->setGrandTotal(50); +$invoice->setBaseGrandTotal(50); +$invoice->setSubTotal(30); +$invoice->setShippingInclTax(20); +$invoice->setShippingAmount(20); +$invoice->setBaseShippingAmount(20); +$invoice->setShippingInclTax(25); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); + +$invoice = $orderService->prepareInvoice($order, [$orderItem2->getId() => 1]); +$invoice->register(); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options_rollback.php new file mode 100644 index 0000000000000..80d6adb0cd9fa --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_multiple_invoices_with_two_products_and_custom_options_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_in_category_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_two_items.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_two_items.php index 7cebee082e99f..cc71d59256c40 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_two_items.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customer_order_with_two_items.php @@ -98,12 +98,15 @@ ->setBaseSubtotal(20) ->setBaseShippingAmount(10) ->setBaseGrandTotal(30) + ->setBaseCurrencyCode('USD') + ->setOrderCurrencyCode('USD') ->setCustomerIsGuest(false) ->setCustomerEmail($customerDataModel->getEmail()) ->setCustomerId($customerDataModel->getId()) ->setBillingAddress($billingAddress) ->setShippingAddress($shippingAddress) ->setShippingDescription('Flat Rate - Fixed') + ->setShippingMethod('flatrate_flatrate') ->setStoreId($mainWebsite->getDefaultStore()->getId()) ->addItem($firstOrderItem) ->addItem($secondOrderItem) diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices.php new file mode 100644 index 0000000000000..3894082c20030 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices.php @@ -0,0 +1,140 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/three_customers.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); + +$addressData = include __DIR__ . '/../../../Magento/Sales/_files/address_data.php'; + +$billingAddress = $objectManager->create(\Magento\Sales\Model\Order\Address::class, ['data' => $addressData]); +$billingAddress->setAddressType('billing'); + +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +$payment2 = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment2->setMethod('checkmo'); + +/** @var $product \Magento\Catalog\Model\Product */ +$product = $objectManager->create(\Magento\Catalog\Model\Product::class); +$repository = $objectManager->create(\Magento\Catalog\Model\ProductRepository::class); +$product = $repository->get('simple'); + +$optionValuesByType = [ + 'field' => 'Test value', + 'date_time' => [ + 'year' => '2015', + 'month' => '9', + 'day' => '9', + 'hour' => '2', + 'minute' => '2', + 'day_part' => 'am', + 'date_internal' => '', + ], + 'drop_down' => '3-1-select', + 'radio' => '4-1-radio', +]; + +$requestInfo = ['options' => []]; +$productOptions = $product->getOptions(); +foreach ($productOptions as $option) { + $requestInfo['options'][$option->getOptionId()] = $optionValuesByType[$option->getType()]; +} + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem->setProductId($product->getId()); +$orderItem->setSku($product->getSku()); +$orderItem->setName($product->getName()); +$orderItem->setQtyOrdered(1); +$orderItem->setBasePrice($product->getPrice()); +$orderItem->setPrice($product->getPrice()); +$orderItem->setRowTotal($product->getPrice()); +$orderItem->setProductType($product->getTypeId()); +$orderItem->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order = $objectManager->create(\Magento\Sales\Model\Order::class); +$order->setIncrementId('100000001'); +$order->setState(\Magento\Sales\Model\Order::STATE_NEW); +$order->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_NEW)); +$order->setCustomerIsGuest(true); +$order->setCustomerEmail('customer@null.com'); +$order->setCustomerFirstname('firstname'); +$order->setCustomerLastname('lastname'); +$order->setBillingAddress($billingAddress); +$order->setShippingAddress($shippingAddress); +$order->setAddresses([$billingAddress, $shippingAddress]); +$order->setPayment($payment); +$order->addItem($orderItem); +$order->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order->setSubtotal(100); +$order->setBaseSubtotal(100); +$order->setBaseGrandTotal(100); +$order->setOrderCurrencyCode('USD'); +$order->setGrandTotal(100); +$order->setCustomerId(1) + ->setCustomerIsGuest(false) + ->save(); + +$orderService = $objectManager->create( + \Magento\Sales\Api\InvoiceManagementInterface::class +); +$invoice = $orderService->prepareInvoice($order); +$invoice->register(); +$order = $invoice->getOrder(); +$order->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order)->save(); + +/** @var \Magento\Sales\Model\Order\Item $orderItem */ +$orderItem2 = $objectManager->create(\Magento\Sales\Model\Order\Item::class); +$orderItem2->setProductId($product->getId()); +$orderItem2->setSku($product->getSku()); +$orderItem2->setQtyOrdered(1); +$orderItem2->setBasePrice($product->getPrice()); +$orderItem2->setPrice($product->getPrice()); +$orderItem2->setRowTotal($product->getPrice()); +$orderItem2->setProductType($product->getTypeId()); +$orderItem2->setProductOptions(['info_buyRequest' => $requestInfo]); + +/** @var \Magento\Sales\Model\Order $order */ +$order2 = $objectManager->create(\Magento\Sales\Model\Order::class); +$order2->setIncrementId('100000002'); +$order2->setState(\Magento\Sales\Model\Order::STATE_NEW); +$order2->setStatus($order->getConfig()->getStateDefaultStatus(\Magento\Sales\Model\Order::STATE_NEW)); +$order2->setCustomerIsGuest(true); +$order2->setCustomerEmail('customer@null.com'); +$order2->setCustomerFirstname('firstname'); +$order2->setCustomerLastname('lastname'); +$order2->setBillingAddress($billingAddress); +$order2->setShippingAddress($shippingAddress); +$order2->setAddresses([$billingAddress, $shippingAddress]); +$order2->setPayment($payment2); +$order2->addItem($orderItem2); +$order2->setStoreId($objectManager->get(\Magento\Store\Model\StoreManagerInterface::class)->getStore()->getId()); +$order2->setSubtotal(100); +$order2->setBaseSubtotal(100); +$order2->setBaseGrandTotal(100); +$order2->setCustomerId(2) + ->setCustomerIsGuest(false) + ->save(); + +$invoice2 = $orderService->prepareInvoice($order2); +$invoice2->register(); +$order2 = $invoice2->getOrder(); +$order2->setIsInProcess(true); +$transactionSave = $objectManager + ->create(\Magento\Framework\DB\Transaction::class); +$transactionSave->addObject($invoice)->addObject($order2)->save(); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices_rollback.php new file mode 100644 index 0000000000000..29c6c3b26a7c0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/customers_with_invoices_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/three_customers_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php index 4140ce1c81f20..924562781e16b 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order.php @@ -61,6 +61,8 @@ ->setGrandTotal(100) ->setBaseSubtotal(100) ->setBaseGrandTotal(100) + ->setOrderCurrencyCode('USD') + ->setBaseCurrencyCode('USD') ->setCustomerIsGuest(true) ->setCustomerEmail('customer@null.com') ->setBillingAddress($billingAddress) diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_rollback.php new file mode 100644 index 0000000000000..dffb191d142a1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_customer_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_different_types_of_product.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_different_types_of_product.php index cefe464cbba09..7065eba4bbb92 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_different_types_of_product.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_different_types_of_product.php @@ -87,6 +87,8 @@ /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderConfigurableItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); $orderConfigurableItem->setProductId($configurableProduct->getId())->setQtyOrdered($qtyOrdered); +$orderConfigurableItem->setSku($configurableProduct->getSku()); +$orderConfigurableItem->setName($configurableProduct->getName()); $orderConfigurableItem->setBasePrice($configurableProduct->getPrice()); $orderConfigurableItem->setPrice($configurableProduct->getPrice()); $orderConfigurableItem->setRowTotal($configurableProduct->getPrice()); @@ -184,6 +186,8 @@ /** @var \Magento\Sales\Model\Order\Item $orderItem */ $orderBundleItem = $objectManager->create(\Magento\Sales\Model\Order\Item::class); $orderBundleItem->setProductId($bundleProduct->getId()); +$orderBundleItem->setSku($bundleProduct->getSku()); +$orderBundleItem->setName($bundleProduct->getName()); $orderBundleItem->setQtyOrdered(1); $orderBundleItem->setBasePrice($bundleProduct->getPrice()); $orderBundleItem->setPrice($bundleProduct->getPrice()); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock.php index 9bd4c9b303cb9..e93e4143311b5 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock.php @@ -4,25 +4,82 @@ * See COPYING.txt for license details. */ -use Magento\Sales\Api\Data\OrderInterfaceFactory; +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Sales\Api\OrderRepositoryInterface; use Magento\Sales\Model\Order; -use Magento\TestFramework\Helper\Bootstrap; +use Magento\Sales\Model\Order\Address as OrderAddress; +use Magento\Sales\Model\Order\Item as OrderItem; +use Magento\Sales\Model\Order\Payment; +use Magento\Store\Model\StoreManagerInterface; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +use Magento\Catalog\Model\Product; +use Magento\TestFramework\Helper\Bootstrap; -Resolver::getInstance()->requireDataFixture( - 'Magento/Sales/_files/customer_order_item_with_product_and_custom_options.php' -); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/product_simple_without_custom_options.php'); $objectManager = Bootstrap::getObjectManager(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->create(ProductRepositoryInterface::class); +$product = $productRepository->get('simple-2'); +$billingAddress = $objectManager->create( + OrderAddress::class, + [ + 'data' => [ + 'region' => 'CA', + 'region_id' => '12', + 'postcode' => '11111', + 'lastname' => 'lastname', + 'firstname' => 'firstname', + 'street' => 'street', + 'city' => 'Los Angeles', + 'email' => 'admin@example.com', + 'telephone' => '11111111', + 'country_id' => 'US', + ], + ], +); +$billingAddress->setAddressType('billing'); +$shippingAddress = clone $billingAddress; +$shippingAddress->setId(null)->setAddressType('shipping'); + +/** @var Payment $payment */ +$payment = $objectManager->create(\Magento\Sales\Model\Order\Payment::class); +$payment->setMethod('checkmo'); + +/** @var OrderItem $orderItem */ +$orderItem = $objectManager->create(OrderItem::class); +$orderItem->setProductId($product->getId()) + ->setQtyOrdered(2) + ->setBasePrice($product->getPrice()) + ->setPrice($product->getPrice()) + ->setRowTotal($product->getPrice()) + ->setProductType('simple-2') + ->setName($product->getName()) + ->setSku($product->getSku()) + ->setProductOptions(['info_buyRequest' => ['qty' => 1]]); + /** @var Order $order */ -$order = $objectManager->get(OrderInterfaceFactory::class)->create()->loadByIncrementId('100000001'); -$order->setCustomerId(1)->setCustomerIsGuest(false)->save(); +$order = $objectManager->create(Order::class); +$order->setIncrementId('100000001') + ->setCustomerIsGuest(false) + ->setCustomerId(1) + ->setCustomerEmail('customer@null.com') + ->setBillingAddress($billingAddress) + ->setShippingAddress($shippingAddress) + ->setStoreId($objectManager->get(StoreManagerInterface::class)->getStore()->getId()) + ->addItem($orderItem) + ->setPayment($payment); +/** @var OrderRepositoryInterface $orderRepository */ +$orderRepository = $objectManager->create(OrderRepositoryInterface::class); +$orderRepository->save($order); // load product and set it out of stock -/** @var \Magento\Catalog\Api\ProductRepositoryInterface $repository */ -$productRepository = Bootstrap::getObjectManager()->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); -$productSku = 'simple'; -/** @var \Magento\Catalog\Model\Product $product */ +/** @var ProductRepositoryInterface $repository */ +$productRepository = Bootstrap::getObjectManager()->create(ProductRepositoryInterface::class); +$productSku = 'simple-2'; +/** @var Product $product */ $product = $productRepository->get($productSku); // set product as out of stock $product->setStockData( diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock_rollback.php index c4b3a1a18b03a..e6ff6de159a17 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock_rollback.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/order_with_product_out_of_stock_rollback.php @@ -5,6 +5,8 @@ */ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); Resolver::getInstance()->requireDataFixture( - 'Magento/Sales/_files/customer_order_item_with_product_and_custom_options_rollback.php' + 'Magento/Catalog/_files/product_simple_without_custom_options_rollback.php' ); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php b/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php index 5be2fcefbde26..ea39a8bf78b4e 100644 --- a/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/quote.php @@ -3,7 +3,11 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + \Magento\TestFramework\Helper\Bootstrap::getInstance()->loadArea('frontend'); + +$storeManager = Magento\TestFramework\Helper\Bootstrap::getObjectManager() + ->get(\Magento\Store\Model\StoreManagerInterface::class); $product = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Catalog\Model\Product::class); $product->setTypeId('simple') ->setId(1) @@ -23,7 +27,9 @@ 'is_in_stock' => 1, 'manage_stock' => 1, ] - )->save(); + ) + ->setWebsiteIds([$storeManager->getStore()->getWebsiteId()]) + ->save(); $productRepository = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); @@ -39,9 +45,7 @@ $shippingAddress = clone $billingAddress; $shippingAddress->setId(null)->setAddressType('shipping'); -$store = Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Magento\Store\Model\StoreManagerInterface::class) - ->getStore(); +$store = $storeManager->getStore(); /** @var \Magento\Quote\Model\Quote $quote */ $quote = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(\Magento\Quote\Model\Quote::class); diff --git a/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_for_two_diff_customers_rollback.php b/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_for_two_diff_customers_rollback.php new file mode 100644 index 0000000000000..570c6f777f198 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sales/_files/two_orders_for_two_diff_customers_rollback.php @@ -0,0 +1,11 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/default_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/two_customers_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php new file mode 100644 index 0000000000000..b3ae30379a897 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Model/Quote/DiscountTest.php @@ -0,0 +1,132 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\SalesRule\Model\Quote; + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\ObjectManagerInterface; +use Magento\Quote\Api\CartRepositoryInterface; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\Quote\Item; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test discount totals calculation model + */ +class DiscountTest extends TestCase +{ + /** + * @var ObjectManagerInterface + */ + private $objectManager; + /** + * @var SearchCriteriaBuilder + */ + private $criteriaBuilder; + /** + * @var CartRepositoryInterface + */ + private $quoteRepository; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $this->objectManager = Bootstrap::getObjectManager(); + $this->criteriaBuilder = $this->objectManager->get(SearchCriteriaBuilder::class); + $this->quoteRepository = $this->objectManager->get(CartRepositoryInterface::class); + } + + /** + * @magentoAppIsolation enabled + * @magentoDataFixture Magento/SalesRule/_files/cart_rule_product_sku.php + * @magentoDataFixture Magento/Checkout/_files/quote_with_bundle_product_with_dynamic_price.php + * @dataProvider bundleProductWithDynamicPriceAndCartPriceRuleDataProvider + * @param string $coupon + * @param array $discounts + * @param float $totalDiscount + * @return void + */ + public function testBundleProductWithDynamicPriceAndCartPriceRule( + string $coupon, + array $discounts, + float $totalDiscount + ): void { + $quote = $this->getQuote('quote_with_bundle_product_with_dynamic_price'); + $quote->setCouponCode($coupon); + $quote->collectTotals(); + $this->quoteRepository->save($quote); + $this->assertEquals(21.98, $quote->getBaseSubtotal()); + $this->assertEquals($totalDiscount, $quote->getShippingAddress()->getDiscountAmount()); + $items = $quote->getAllItems(); + $this->assertCount(3, $items); + /** @var Item $item*/ + $item = array_shift($items); + $this->assertEquals('bundle_product_with_dynamic_price-simple1-simple2', $item->getSku()); + $this->assertEquals($discounts[$item->getSku()], $item->getDiscountAmount()); + $item = array_shift($items); + $this->assertEquals('simple1', $item->getSku()); + $this->assertEquals(5.99, $item->getPrice()); + $this->assertEquals($discounts[$item->getSku()], $item->getDiscountAmount()); + $item = array_shift($items); + $this->assertEquals('simple2', $item->getSku()); + $this->assertEquals(15.99, $item->getPrice()); + $this->assertEquals($discounts[$item->getSku()], $item->getDiscountAmount()); + } + + /** + * @return array + */ + public function bundleProductWithDynamicPriceAndCartPriceRuleDataProvider(): array + { + return [ + [ + 'bundle_product_with_dynamic_price_coupon_code', + [ + 'bundle_product_with_dynamic_price-simple1-simple2' => 0, + 'simple1' => 3, + 'simple2' => 7.99, + ], + -10.99 + ], + [ + 'simple1_coupon_code', + [ + 'bundle_product_with_dynamic_price-simple1-simple2' => 0, + 'simple1' => 3, + 'simple2' => 0, + ], + -3 + ], + [ + 'simple2_coupon_code', + [ + 'bundle_product_with_dynamic_price-simple1-simple2' => 0, + 'simple1' => 0, + 'simple2' => 8, + ], + -8 + ] + ]; + } + + /** + * @param string $reservedOrderId + * @return Quote + */ + private function getQuote(string $reservedOrderId): Quote + { + $searchCriteria = $this->criteriaBuilder->addFilter('reserved_order_id', $reservedOrderId) + ->create(); + $carts = $this->quoteRepository->getList($searchCriteria) + ->getItems(); + return array_shift($carts); + } +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php index f9a8b96ab1f2f..4ed096fa4418a 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/Plugin/CouponUsagesTest.php @@ -3,35 +3,34 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - namespace Magento\SalesRule\Plugin; use Magento\Framework\DataObject; use Magento\Framework\ObjectManagerInterface; -use Magento\Sales\Model\Order; +use Magento\Quote\Model\Quote; +use Magento\Quote\Model\QuoteManagement; +use Magento\Sales\Api\OrderManagementInterface; use Magento\Sales\Model\Service\OrderService; use Magento\SalesRule\Model\Coupon; use Magento\SalesRule\Model\ResourceModel\Coupon\Usage; use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; /** - * Test increasing coupon usages after after order placing and decreasing after order cancellation. + * Test increasing coupon usages after order placing and decreasing after order cancellation. * + * @magentoAppArea frontend * @magentoDbIsolation enabled * @magentoAppIsolation enabled */ -class CouponUsagesTest extends \PHPUnit\Framework\TestCase +class CouponUsagesTest extends TestCase { /** * @var ObjectManagerInterface */ private $objectManager; - /** - * @var Coupon - */ - private $coupon; - /** * @var Usage */ @@ -43,9 +42,9 @@ class CouponUsagesTest extends \PHPUnit\Framework\TestCase private $couponUsage; /** - * @var Order + * @var QuoteManagement */ - private $order; + private $quoteManagement; /** * @var OrderService @@ -58,36 +57,38 @@ class CouponUsagesTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { $this->objectManager = Bootstrap::getObjectManager(); - $this->coupon = $this->objectManager->get(Coupon::class); $this->usage = $this->objectManager->get(Usage::class); $this->couponUsage = $this->objectManager->get(DataObject::class); - $this->order = $this->objectManager->get(Order::class); + $this->quoteManagement = $this->objectManager->get(QuoteManagement::class); $this->orderService = $this->objectManager->get(OrderService::class); } /** * Test increasing coupon usages after after order placing and decreasing after order cancellation. * - * @magentoDataFixture Magento/Customer/_files/customer.php * @magentoDataFixture Magento/SalesRule/_files/coupons_limited_order.php */ - public function testOrderCancellation() + public function testSubmitQuoteAndCancelOrder() { $customerId = 1; $couponCode = 'one_usage'; - $orderId = '100000001'; + $reservedOrderId = 'test01'; - $this->coupon->loadByCode($couponCode); - $this->order->loadByIncrementId($orderId); + /** @var Coupon $coupon */ + $coupon = $this->objectManager->get(Coupon::class); + $coupon->loadByCode($couponCode); + /** @var Quote $quote */ + $quote = $this->objectManager->get(Quote::class); + $quote->load($reservedOrderId, 'reserved_order_id'); // Make sure coupon usages value is incremented then order is placed. - $this->orderService->place($this->order); - $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $this->coupon->getId()); - $this->coupon->loadByCode($couponCode); + $order = $this->quoteManagement->submit($quote); + $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); + $coupon->loadByCode($couponCode); self::assertEquals( 1, - $this->coupon->getTimesUsed() + $coupon->getTimesUsed() ); self::assertEquals( 1, @@ -95,17 +96,66 @@ public function testOrderCancellation() ); // Make sure order coupon usages value is decremented then order is cancelled. - $this->orderService->cancel($this->order->getId()); - $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $this->coupon->getId()); - $this->coupon->loadByCode($couponCode); + $this->orderService->cancel($order->getId()); + $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); + $coupon->loadByCode($couponCode); self::assertEquals( 0, - $this->coupon->getTimesUsed() + $coupon->getTimesUsed() ); self::assertEquals( 0, $this->couponUsage->getTimesUsed() ); } + + /** + * Test to decrement coupon usages after exception on order placing + * + * @magentoDataFixture Magento/SalesRule/_files/coupons_limited_order.php + */ + public function testSubmitQuoteWithError() + { + $customerId = 1; + $couponCode = 'one_usage'; + $reservedOrderId = 'test01'; + $exceptionMessage = 'Some test exception'; + + /** @var Coupon $coupon */ + $coupon = $this->objectManager->get(Coupon::class); + $coupon->loadByCode($couponCode); + /** @var Quote $quote */ + $quote = $this->objectManager->get(Quote::class); + $quote->load($reservedOrderId, 'reserved_order_id'); + + /** @var OrderManagementInterface|MockObject $orderManagement */ + $orderManagement = $this->createMock(OrderManagementInterface::class); + $orderManagement->expects($this->once()) + ->method('place') + ->willThrowException(new \Exception($exceptionMessage)); + + /** @var QuoteManagement $quoteManagement */ + $quoteManagement = $this->objectManager->create( + QuoteManagement::class, + ['orderManagement' => $orderManagement] + ); + + try { + $quoteManagement->submit($quote); + } catch (\Exception $exception) { + $this->assertEquals($exceptionMessage, $exception->getMessage()); + + $this->usage->loadByCustomerCoupon($this->couponUsage, $customerId, $coupon->getId()); + $coupon->loadByCode($couponCode); + self::assertEquals( + 0, + $coupon->getTimesUsed() + ); + self::assertEquals( + 0, + $this->couponUsage->getTimesUsed() + ); + } + } } diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php new file mode 100644 index 0000000000000..6ac4f65f36e5f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping.php @@ -0,0 +1,66 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\SalesRule\Model\Rule; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +$websiteId = $objectManager->get(StoreManagerInterface::class) + ->getWebsite() + ->getId(); + +/** @var Rule $salesRule */ +$salesRule = $objectManager->create(Rule::class); +$salesRule->setData( + [ + 'name' => '10% Off on orders with shipping discount', + 'is_active' => 1, + 'customer_group_ids' => [1], + 'coupon_type' => Rule::COUPON_TYPE_NO_COUPON, + 'simple_action' => 'by_percent', + 'discount_amount' => 10, + 'discount_step' => 0, + 'apply_to_shipping' => 1, + 'stop_rules_processing' => 1, + 'website_ids' => [$websiteId], + 'store_labels' => [ + 'store_id' => 0, + 'store_label' => 'Discount Label for 10% off', + ] + ] +); + +$salesRule->getConditions()->loadArray([ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'conditions' => [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Address::class, + 'attribute' => 'base_subtotal', + 'operator' => '>=', + 'value' => '20', + 'is_value_processed' => false, + 'actions' => [ + [ + 'type' => \Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator'=>'all' + ], + ], + ], + ], +]); + +$salesRule->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping_rollback.php new file mode 100644 index 0000000000000..f5de93e529b22 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_10_percent_off_with_discount_on_shipping_rollback.php @@ -0,0 +1,10 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/SalesRule/_files/rules_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_product_sku.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_product_sku.php new file mode 100644 index 0000000000000..2d9c498fa30fc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_product_sku.php @@ -0,0 +1,87 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Customer\Model\GroupManagement; +use Magento\SalesRule\Api\CouponRepositoryInterface; +use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\Rule; +use Magento\SalesRule\Model\Rule\Condition\Combine; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +if (!isset($skus)) { + $skus = [ + 'bundle_product_with_dynamic_price', + 'simple1', + 'simple2', + ]; +} +$objectManager = Bootstrap::getObjectManager(); +/** @var $couponRepository CouponRepositoryInterface */ +$couponRepository = $objectManager->get(CouponRepositoryInterface::class); +/** @var $storeManager StoreManagerInterface */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +foreach ($skus as $sku) { + /** @var Rule $salesRule */ + $salesRule = $objectManager->create(Rule::class); + $salesRule->loadPost( + [ + 'name' => '50% Off for ' . $sku, + 'is_active' => 1, + 'customer_group_ids' => [GroupManagement::NOT_LOGGED_IN_ID], + 'coupon_type' => Rule::COUPON_TYPE_SPECIFIC, + 'simple_action' => 'by_percent', + 'discount_amount' => 50, + 'discount_step' => 0, + 'stop_rules_processing' => 0, + 'website_ids' => [ + $storeManager->getWebsite()->getId() + ], + 'conditions' => [ + 1 => [ + 'type' => Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + ] + ], + 'actions' => [ + 1 => [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product\Combine::class, + 'attribute' => null, + 'operator' => null, + 'value' => '1', + 'is_value_processed' => null, + 'aggregator' => 'all', + 'actions' => [ + 1 => [ + 'type' => Magento\SalesRule\Model\Rule\Condition\Product::class, + 'attribute' => 'sku', + 'operator' => '==', + 'value' => $sku, + 'is_value_processed' => false, + ] + ] + ] + ], + 'store_labels' => [ + + 'store_id' => 0, + 'store_label' => 'Promo code for ' . $sku, + + ] + ] + ); + $salesRule->save(); + $coupon = $objectManager->create(Coupon::class); + $coupon->setRuleId($salesRule->getId()) + ->setCode($sku . '_coupon_code') + ->setType(0); + $couponRepository->save($coupon); +} diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_product_sku_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_product_sku_rollback.php new file mode 100644 index 0000000000000..59b92873c6242 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/cart_rule_product_sku_rollback.php @@ -0,0 +1,50 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Api\SearchCriteriaBuilder; +use Magento\Framework\Registry; +use Magento\SalesRule\Api\RuleRepositoryInterface; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\Helper\Bootstrap; + +if (!isset($skus)) { + $skus = [ + 'bundle_product_with_dynamic_price', + 'simple1', + 'simple2', + ]; +} +$objectManager = Bootstrap::getObjectManager(); + +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); + +/** @var SearchCriteriaBuilder $searchCriteriaBuilder */ +$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class); +$names = array_map( + function ($sku) { + return '50% Off for ' . $sku; + }, + $skus +); +$searchCriteria = $searchCriteriaBuilder->addFilter('name', $names, 'in') + ->create(); +/** @var RuleRepositoryInterface $ruleRepository */ +$ruleRepository = $objectManager->get(RuleRepositoryInterface::class); +$items = $ruleRepository->getList($searchCriteria) + ->getItems(); +/** @var Rule $salesRule */ +foreach ($items as $salesRule) { + if ($salesRule !== null && $salesRule->getRuleId()) { + $ruleRepository->deleteById($salesRule->getRuleId()); + } +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited.php index ee477318f52b8..164f8c0d5b7c2 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited.php @@ -3,26 +3,34 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - use Magento\SalesRule\Model\Coupon; +use Magento\SalesRule\Model\ResourceModel\Rule\Collection; +use Magento\SalesRule\Model\Rule; +use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/SalesRule/_files/rules.php'); -$collection = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class +$collection = Bootstrap::getObjectManager()->create( + Collection::class ); $items = array_values($collection->getItems()); +/** @var Rule $rule */ +foreach ($items as $rule) { + $rule->setSimpleAction('by_percent') + ->setDiscountAmount(10) + ->save(); +} /** @var Coupon $coupon */ -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Coupon::class); +$coupon = Bootstrap::getObjectManager()->create(Coupon::class); $coupon->setRuleId($items[0]->getId()) ->setCode('one_usage') ->setType(0) ->setUsageLimit(1) ->save(); -$coupon = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Coupon::class); +$coupon = Bootstrap::getObjectManager()->create(Coupon::class); $coupon->setRuleId($items[1]->getId()) ->setCode('one_usage_per_customer') ->setType(0) diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order.php index 79ee6ffb91f14..83f17ef0d9d46 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order.php @@ -3,25 +3,28 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ - -use Magento\Sales\Model\Order; +use Magento\Quote\Model\Quote; use Magento\TestFramework\Helper\Bootstrap; use Magento\TestFramework\Workaround\Override\Fixture\Resolver; Resolver::getInstance()->requireDataFixture('Magento/SalesRule/_files/coupons_limited.php'); -Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order.php'); +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/quote_with_customer.php'); $collection = Bootstrap::getObjectManager()->create( \Magento\SalesRule\Model\ResourceModel\Rule\Collection::class ); $items = array_values($collection->getItems()); -/** @var Order $order */ -$order = Bootstrap::getObjectManager()->create(Order::class); +/** @var Quote $quote */ +$quote = Bootstrap::getObjectManager()->create(Quote::class); +$quote->load('test01', 'reserved_order_id'); +$quote->getShippingAddress() + ->setShippingMethod('flatrate_flatrate') + ->setShippingDescription('Flat Rate - Fixed') + ->setCollectShippingRates(true) + ->collectShippingRates() + ->save(); -$order->loadByIncrementId('100000001') - ->setCouponCode('one_usage') +$quote->setCouponCode('one_usage') ->setAppliedRuleIds("{$items[0]->getId()}") - ->setCreatedAt('2014-10-25 10:10:10') - ->setCustomerId(1) ->save(); diff --git a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order_rollback.php b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order_rollback.php index f44b6d3a75c97..15e58a3e53da5 100644 --- a/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order_rollback.php +++ b/dev/tests/integration/testsuite/Magento/SalesRule/_files/coupons_limited_order_rollback.php @@ -5,5 +5,5 @@ */ use Magento\TestFramework\Workaround\Override\Fixture\Resolver; +Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/quote_with_customer_rollback.php'); Resolver::getInstance()->requireDataFixture('Magento/SalesRule/_files/coupons_limited_rollback.php'); -Resolver::getInstance()->requireDataFixture('Magento/Sales/_files/order_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Search/Controller/Adminhtml/Synonyms/DeleteTest.php b/dev/tests/integration/testsuite/Magento/Search/Controller/Adminhtml/Synonyms/DeleteTest.php new file mode 100644 index 0000000000000..ed8c3dba8f8d0 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Search/Controller/Adminhtml/Synonyms/DeleteTest.php @@ -0,0 +1,62 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Search\Controller\Adminhtml\Synonyms; + +use Magento\Framework\App\Request\Http as HttpRequest; +use Magento\Search\Model\SynonymGroup; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\TestCase\AbstractBackendController; +use Magento\Search\Model\ResourceModel\SynonymGroup\Collection; + +/** + * Test for class \Magento\Search\Controller\Adminhtml\Synonyms\Delete + * + * @magentoAppArea adminhtml + */ +class DeleteTest extends AbstractBackendController +{ + + /** Test Delete Synonyms + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @magentoDataFixture Magento/Search/_files/synonym_group.php + * @return void + */ + public function testExecute(): void + { + $synonymGroupModel=$this->getTestFixture(); + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue(['group_id' => $synonymGroupModel->getGroupId()]); + $this->dispatch('backend/search/synonyms/delete'); + $this->assertSessionMessages($this->equalTo([(string)__('The synonym group has been deleted.')])); + } + + /** + * Test execute with no params + * + * @return void + */ + public function testExecuteNoId(): void + { + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->dispatch('backend/search/synonyms/delete'); + $this->assertSessionMessages($this->equalTo([(string)__('We can't find a synonym group to delete.')])); + } + + /** + * Gets synonym group Fixture. + * + * @return SynonymGroup + */ + private function getTestFixture(): SynonymGroup + { + /** @var Collection */ + $synonymGroupCollection = Bootstrap::getObjectManager()->get(Collection::class); + return $synonymGroupCollection->getLastItem(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Search/Model/ResourceModel/SynonymGroupTest.php b/dev/tests/integration/testsuite/Magento/Search/Model/ResourceModel/SynonymGroupTest.php index 73f3424eab53c..78b57ab6bcc37 100644 --- a/dev/tests/integration/testsuite/Magento/Search/Model/ResourceModel/SynonymGroupTest.php +++ b/dev/tests/integration/testsuite/Magento/Search/Model/ResourceModel/SynonymGroupTest.php @@ -5,8 +5,16 @@ */ namespace Magento\Search\Model\ResourceModel; +/** + * Test for class \Magento\Search\Model\ResourceModel\SynonymGroup + */ class SynonymGroupTest extends \PHPUnit\Framework\TestCase { + /** + * Test Get By Scope + * + * @return void + */ public function testGetByScope() { $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); @@ -16,32 +24,36 @@ public function testGetByScope() $synonymGroupModel1->setStoreId(0); $synonymGroupModel1->setSynonymGroup('a,b,c'); $synonymGroupModel1->save(); + $group1 = $synonymGroupModel1->getGroupId(); /** @var \Magento\Search\Model\SynonymGroup $synonymGroupModel2 */ $synonymGroupModel2 = $objectManager->create(\Magento\Search\Model\SynonymGroup::class); $synonymGroupModel2->setWebsiteId(0); $synonymGroupModel2->setStoreId(1); $synonymGroupModel2->setSynonymGroup('d,e,f'); $synonymGroupModel2->save(); + $group2 = $synonymGroupModel2->getGroupId(); /** @var \Magento\Search\Model\SynonymGroup $synonymGroupModel3 */ $synonymGroupModel3 = $objectManager->create(\Magento\Search\Model\SynonymGroup::class); $synonymGroupModel3->setWebsiteId(1); $synonymGroupModel3->setStoreId(0); $synonymGroupModel3->setSynonymGroup('g,h,i'); $synonymGroupModel3->save(); + $group3 = $synonymGroupModel3->getGroupId(); /** @var \Magento\Search\Model\SynonymGroup $synonymGroupModel4 */ $synonymGroupModel4 = $objectManager->create(\Magento\Search\Model\SynonymGroup::class); $synonymGroupModel4->setWebsiteId(0); $synonymGroupModel4->setStoreId(0); $synonymGroupModel4->setSynonymGroup('d,e,f'); $synonymGroupModel4->save(); + $group4 = $synonymGroupModel4->getGroupId(); /** @var \Magento\Search\Model\ResourceModel\SynonymGroup $resourceModel */ $resourceModel = $objectManager->create(\Magento\Search\Model\ResourceModel\SynonymGroup::class); $this->assertEquals( - [['group_id' => 1, 'synonyms' => 'a,b,c'], ['group_id' => 4, 'synonyms' => 'd,e,f']], + [['group_id' => $group1, 'synonyms' => 'a,b,c'], ['group_id' => $group4, 'synonyms' => 'd,e,f']], $resourceModel->getByScope(0, 0) ); - $this->assertEquals([['group_id' => 2, 'synonyms' => 'd,e,f']], $resourceModel->getByScope(0, 1)); - $this->assertEquals([['group_id' => 3, 'synonyms' => 'g,h,i']], $resourceModel->getByScope(1, 0)); + $this->assertEquals([['group_id' => $group2, 'synonyms' => 'd,e,f']], $resourceModel->getByScope(0, 1)); + $this->assertEquals([['group_id' => $group3, 'synonyms' => 'g,h,i']], $resourceModel->getByScope(1, 0)); } } diff --git a/dev/tests/integration/testsuite/Magento/Search/_files/synonym_group.php b/dev/tests/integration/testsuite/Magento/Search/_files/synonym_group.php new file mode 100644 index 0000000000000..d66a3a84ec13d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Search/_files/synonym_group.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Search\Model\SynonymGroupRepository; +use Magento\Search\Api\Data\SynonymGroupInterface; + +$objectManager = Bootstrap::getObjectManager(); + +$synonymsGroupModel = $objectManager->create(SynonymGroupInterface::class); +$synonymGroupRepository=$objectManager->create(SynonymGroupRepository::class); +$synonymsGroupModel->setStoreId(Magento\Store\Model\Store::DEFAULT_STORE_ID)->setStoreId(0)->setWebsiteId(0); + +$synonymGroupRepository->save($synonymsGroupModel); diff --git a/dev/tests/integration/testsuite/Magento/Search/_files/synonym_group_rollback.php b/dev/tests/integration/testsuite/Magento/Search/_files/synonym_group_rollback.php new file mode 100644 index 0000000000000..47027dcc54404 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Search/_files/synonym_group_rollback.php @@ -0,0 +1,17 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Search\Model\ResourceModel\SynonymGroup\Collection; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Search\Model\SynonymGroupRepository; + +$objectManager = Bootstrap::getObjectManager(); + +$synonymGroupModel = $objectManager->get(Collection::class)->getLastItem(); + +$synonymGroupRepository=$objectManager->create(SynonymGroupRepository::class); +$synonymGroupRepository->delete($synonymGroupModel); diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php index e630ab0f83ce2..3ec185e71a1e5 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/GenerateFixturesCommandTest.php @@ -16,7 +16,8 @@ /** * Class GenerateFixturesCommandCommandTest - * @package Magento\Setup\Console\Command + * + * @magentoDbIsolation disabled */ class GenerateFixturesCommandTest extends \Magento\TestFramework\Indexer\TestCase { @@ -79,14 +80,6 @@ protected function setUp(): void parent::setUp(); } - /** - * @return string - */ - private function getEdition() - { - return trim(file_get_contents(__DIR__ . '/_files/edition')); - } - /** * teardown */ @@ -116,7 +109,7 @@ public static function setUpBeforeClass(): void */ public function testExecute() { - $profile = BP . "/setup/performance-toolkit/profiles/{$this->getEdition()}/small.xml"; + $profile = realpath(__DIR__ . "/_files/min_profile.xml"); $this->commandTester->execute( [ GenerateFixturesCommand::PROFILE_ARGUMENT => $profile, diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php index e95837a65c77b..1d589d73b3762 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/PriceIndexerDimensionsModeSetCommandTest.php @@ -13,6 +13,8 @@ /** * Test command that sets indexer mode for catalog_product_price indexer + * + * @magentoDbIsolation disabled */ class PriceIndexerDimensionsModeSetCommandTest extends \Magento\TestFramework\Indexer\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/min_profile.xml b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/min_profile.xml new file mode 100644 index 0000000000000..6f6c548621b21 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/min_profile.xml @@ -0,0 +1,122 @@ +<?xml version="1.0"?> +<!-- +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +--> +<config xmlns:xi="http://www.w3.org/2001/XInclude"> + <profile> + <admin_users>2</admin_users><!-- Number of admin users to generate --> + <websites>1</websites> <!-- Number of websites to generate --> + <store_groups>1</store_groups> <!--Number of stores--> + <store_views>1</store_views> <!-- Number of store views --> + <assign_entities_to_all_websites>0</assign_entities_to_all_websites> <!-- Whether to assign all products per each website --> + <simple_products>1</simple_products> <!-- Simple products count --> + <configurable_products> <!-- Configurable product --> + <config> + <attributes> + <attribute> + <options>3</options> + <swatches>image</swatches> + </attribute> + <attribute> + <options>8</options> + </attribute> + </attributes> + <sku>Configurable Product %s</sku> + <products>16</products> + </config> + </configurable_products> + <product-images> + <images-count>1</images-count> + <images-per-product>1</images-per-product> + </product-images> + <categories>1</categories> <!-- Number of categories to generate --> + <categories_nesting_level>1</categories_nesting_level> <!-- Nesting level for categories --> + <customers>1</customers> <!-- Number of customers to generate --> + + <catalog_price_rules>1</catalog_price_rules> <!-- Number of catalog price rules --> + <cart_price_rules>1</cart_price_rules> <!-- Number of cart price rules --> + <cart_price_rules_floor>1</cart_price_rules_floor> + <coupon_codes>1</coupon_codes> <!-- Number of coupon codes --> + + <product_attribute_sets>1</product_attribute_sets> <!-- Number of product attribute sets --> + <product_attribute_sets_attributes>1</product_attribute_sets_attributes> <!-- Number of attributes per set --> + <product_attribute_sets_attributes_values>1</product_attribute_sets_attributes_values> <!-- Number of values per attribute --> + + <order_quotes_enable>true</order_quotes_enable> + <order_simple_product_count_from>1</order_simple_product_count_from> + <order_simple_product_count_to>1</order_simple_product_count_to> + <orders>1</orders> <!-- Orders count --> + + <tax_rates_file>tax_rates_small.csv</tax_rates_file> <!-- Tax rates file in fixtures directory--> + + <configs> <!-- Config variables and values for change --> + <config> + <path>admin/security/use_form_key</path> + <scope>default</scope> + <scopeId>0</scopeId> + <value>0</value> + </config> + <config> + <path>admin/security/session_lifetime</path> + <scope>default</scope> + <scopeId>0</scopeId> + <value>7200</value> + </config> + <config> + <path>admin/security/admin_account_sharing</path> + <scope>default</scope> + <scopeId>0</scopeId> + <value>1</value> + </config> + <config> + <path>carriers/flatrate/active</path> + <scope>default</scope> + <scopeId>0</scopeId> + <value>1</value> + </config> + </configs> + <indexers> <!-- Indexer mode value (true - Update by Schedule, false - Update on Save) --> + <indexer> + <id>catalog_category_product</id> + <set_scheduled>false</set_scheduled> + </indexer> + <indexer> + <id>catalog_product_category</id> + <set_scheduled>false</set_scheduled> + </indexer> + <indexer> + <id>catalog_product_price</id> + <set_scheduled>false</set_scheduled> + </indexer> + <indexer> + <id>catalog_product_attribute</id> + <set_scheduled>false</set_scheduled> + </indexer> + <indexer> + <id>cataloginventory_stock</id> + <set_scheduled>false</set_scheduled> + </indexer> + <indexer> + <id>catalogrule_rule</id> + <set_scheduled>false</set_scheduled> + </indexer> + <indexer> + <id>catalogrule_product</id> + <set_scheduled>false</set_scheduled> + </indexer> + <indexer> + <id>catalogsearch_fulltext</id> + <set_scheduled>false</set_scheduled> + </indexer> + </indexers> + <xi:include href="../../../../../../../../../setup/performance-toolkit/config/searchTermsLarge.xml" /> + <xi:include href="../../../../../../../../../setup/performance-toolkit/config/attributeSets.xml" /> + <xi:include href="../../../../../../../../../setup/performance-toolkit/config/searchConfig.xml" /> + <xi:include href="../../../../../../../../../setup/performance-toolkit/config/customerConfig.xml" /> + <xi:include href="../../../../../../../../../setup/performance-toolkit/config/description.xml" /> + <xi:include href="../../../../../../../../../setup/performance-toolkit/config/shortDescription.xml" /> + </profile> +</config> diff --git a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/root/lib/internal/Magento/Framework/Test/Unit/View/Element/UiComponentFactoryTest.php b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/root/lib/internal/Magento/Framework/Test/Unit/View/Element/UiComponentFactoryTest.php index 5643ef10782e8..3152b4a6e0efb 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/root/lib/internal/Magento/Framework/Test/Unit/View/Element/UiComponentFactoryTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Console/Command/_files/root/lib/internal/Magento/Framework/Test/Unit/View/Element/UiComponentFactoryTest.php @@ -115,7 +115,7 @@ public function testNonRootComponent() $name = "fieldset"; $context = $this->createMock(\Magento\Framework\View\Element\UiComponent\ContextInterface::class); $arguments = ['context' => $context]; - $defintionArguments = [ + $definitionArguments = [ 'componentType' => 'select', 'attributes' => [ 'class' => '\Some\Class', @@ -132,7 +132,7 @@ public function testNonRootComponent() $this->dataMock->expects($this->once()) ->method('get') ->with($name) - ->willReturn($defintionArguments); + ->willReturn($definitionArguments); $this->objectManagerMock->expects($this->once()) ->method('create') ->with('\Some\Class', $expectedArguments); diff --git a/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php b/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php index 3322e30780b4d..9737467422aba 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Declaration/WhitelistDeclarationTest.php @@ -19,7 +19,9 @@ use Magento\TestFramework\ObjectManager; /** - * Class WhitelistDeclarationTest + * Checks whitelisted tables behaviour + * + * @magentoDbIsolation disabled */ class WhitelistDeclarationTest extends \PHPUnit\Framework\TestCase { diff --git a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php index 3069f682f9688..2829cffd8d8a7 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Fixtures/FixtureModelTest.php @@ -13,6 +13,8 @@ /** * Class Application test * + * @magentoDbIsolation disabled + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class FixtureModelTest extends \Magento\TestFramework\Indexer\TestCase diff --git a/dev/tests/integration/testsuite/Magento/Setup/Model/ConfigOptionsListCollectorTest.php b/dev/tests/integration/testsuite/Magento/Setup/Model/ConfigOptionsListCollectorTest.php index 4e04d2dfa0854..3b68f6801c52d 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Model/ConfigOptionsListCollectorTest.php +++ b/dev/tests/integration/testsuite/Magento/Setup/Model/ConfigOptionsListCollectorTest.php @@ -5,6 +5,10 @@ */ namespace Magento\Setup\Model; +use Magento\Framework\Component\ComponentRegistrarInterface; +use Magento\Setup\Validator\DbValidator; +use Laminas\ServiceManager\ServiceLocatorInterface; + class ConfigOptionsListCollectorTest extends \PHPUnit\Framework\TestCase { /** @@ -14,7 +18,7 @@ class ConfigOptionsListCollectorTest extends \PHPUnit\Framework\TestCase protected function setUp(): void { - $this->objectManagerProvider = $this->createMock(\Magento\Setup\Model\ObjectManagerProvider::class); + $this->objectManagerProvider = $this->createMock(ObjectManagerProvider::class); $this->objectManagerProvider ->expects($this->any()) ->method('get') @@ -24,11 +28,13 @@ protected function setUp(): void public function testCollectOptionsLists() { $objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); - $fullModuleListMock = $this->createMock(\Magento\Framework\Module\FullModuleList::class); - $fullModuleListMock->expects($this->once())->method('getNames')->willReturn(['Magento_Backend']); + $componentRegistrar = $this->createMock(ComponentRegistrarInterface::class); + $componentRegistrar->expects($this->once()) + ->method('getPaths') + ->willReturn(['Magento_Backend'=>'app/code/Magento/Backend']); - $dbValidator = $this->createMock(\Magento\Setup\Validator\DbValidator::class); - $configGenerator = $this->createMock(\Magento\Setup\Model\ConfigGenerator::class); + $dbValidator = $this->createMock(DbValidator::class); + $configGenerator = $this->createMock(ConfigGenerator::class); $setupOptions = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() ->create( @@ -39,7 +45,7 @@ public function testCollectOptionsLists() ] ); - $serviceLocator = $this->getMockForAbstractClass(\Laminas\ServiceManager\ServiceLocatorInterface::class); + $serviceLocator = $this->getMockForAbstractClass(ServiceLocatorInterface::class); $serviceLocator->expects($this->once()) ->method('get') @@ -51,7 +57,7 @@ public function testCollectOptionsLists() \Magento\Setup\Model\ConfigOptionsListCollector::class, [ 'objectManagerProvider' => $this->objectManagerProvider, - 'fullModuleList' => $fullModuleListMock, + 'componentRegistrar' => $componentRegistrar, 'serviceLocator' => $serviceLocator ] ); diff --git a/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock b/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock index 9d047d2e675eb..3e6deb9165908 100644 --- a/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock +++ b/dev/tests/integration/testsuite/Magento/Setup/Model/_files/testSkeleton/composer.lock @@ -819,10 +819,6 @@ "LICENSE.txt", "LICENSE.txt" ], - [ - ".travis.yml", - ".travis.yml" - ], [ "app/bootstrap.php", "app/bootstrap.php" diff --git a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php index 55f9fe8f17dc7..3fe45fd71aead 100644 --- a/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php +++ b/dev/tests/integration/testsuite/Magento/Shipping/Controller/Adminhtml/Order/Shipment/AddCommentTest.php @@ -42,7 +42,7 @@ public function testSendEmailOnShipmentCommentAdd(): void $message = $this->transportBuilder->getSentMessage(); $subject =__('Update to your %1 shipment', $order->getStore()->getFrontendName())->render(); $messageConstraint = $this->logicalAnd( - new StringContains($order->getBillingAddress()->getName()), + new StringContains($order->getCustomerName()), new RegularExpression( sprintf( "/Your order #%s has been updated with a status of.*%s/", diff --git a/dev/tests/integration/testsuite/Magento/Sitemap/Block/RobotsTest.php b/dev/tests/integration/testsuite/Magento/Sitemap/Block/RobotsTest.php new file mode 100644 index 0000000000000..c4b9470e89b7a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Sitemap/Block/RobotsTest.php @@ -0,0 +1,96 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Sitemap\Block; + +use Magento\Framework\View\LayoutInterface; +use Magento\Sitemap\Model\SitemapFactory; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for \Magento\Sitemap\Block\Robots. + */ +class RobotsTest extends TestCase +{ + private const STUB_SITEMAP_FILENAME = 'sitemap_file.xml'; + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @var SitemapFactory + */ + private $sitemapFactory; + + /** + * @var StoreManagerInterface + */ + private $storeManager; + + /** + * @inheridoc + */ + protected function setUp(): void + { + $this->layout = Bootstrap::getObjectManager()->get(LayoutInterface::class); + $this->sitemapFactory = Bootstrap::getObjectManager()->get(SitemapFactory::class); + $this->storeManager = Bootstrap::getObjectManager()->get(StoreManagerInterface::class); + } + + /** + * Test toHtml with few websites + * + * @magentoDataFixture Magento/Store/_files/multiple_websites_with_store_groups_stores.php + * @magentoConfigFixture default_store sitemap/search_engines/submission_robots 1 + * @magentoConfigFixture second_store_view_store sitemap/search_engines/submission_robots 1 + * + * @return void + */ + public function testToHtml(): void + { + $secondSitemapFile = 'second_' . self::STUB_SITEMAP_FILENAME; + + $this->createSitemap(self::STUB_SITEMAP_FILENAME, 1); + $this->createSitemap($secondSitemapFile, 2); + + $this->assertStringContainsString(self::STUB_SITEMAP_FILENAME, $this->getToHtmlOutput(1)); + $this->assertStringContainsString($secondSitemapFile, $this->getToHtmlOutput(2)); + } + + /** + * Returns toHtml output per store + * + * @param int $storeId + * @return string + */ + private function getToHtmlOutput(int $storeId): string + { + $this->storeManager->setCurrentStore($storeId); + $block = $this->layout->createBlock(Robots::class); + + return $block->toHtml(); + } + + /** + * Create Sitemap + * + * @param string $fileName + * @param int $storeId + * @param string $siteMath + * @return void + */ + private function createSitemap(string $fileName, int $storeId, string $siteMath = '/'): void + { + $model = $this->sitemapFactory->create(); + $model->setData(['sitemap_filename' => $fileName, 'store_id' => $storeId, 'sitemap_path' => $siteMath]); + $model->save(); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Store/Model/StoreSwitcherTest.php b/dev/tests/integration/testsuite/Magento/Store/Model/StoreSwitcherTest.php index 3fabbcad3ad28..a0f93c889df55 100644 --- a/dev/tests/integration/testsuite/Magento/Store/Model/StoreSwitcherTest.php +++ b/dev/tests/integration/testsuite/Magento/Store/Model/StoreSwitcherTest.php @@ -42,7 +42,7 @@ protected function setUp(): void */ public function testSwitch(): void { - $redirectUrl = "http://domain.com/?SID=e5h3e086dce3ckkqt9ia7avl27&___store=fixture_second_store"; + $redirectUrl = "http://domain.com/?___store=fixture_second_store"; $expectedUrl = "http://domain.com/"; $fromStoreCode = 'test'; /** @var \Magento\Store\Api\StoreRepositoryInterface $storeRepository */ diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_identity.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_identity.php new file mode 100644 index 0000000000000..495992a38c1fe --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_identity.php @@ -0,0 +1,38 @@ +<?php +/** + * Create fixture store with second identity + * + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store.php'); + +$objectManager = Bootstrap::getObjectManager(); +$store = $objectManager->create(Store::class); +if ($storeId = $store->load('fixture_second_store', 'code')->getId()) { + /** @var Config $configResource */ + $configResource = $objectManager->get(Config::class); + $configResource->saveConfig( + 'trans_email/ident_general/name', + 'Fixture Store Owner', + ScopeInterface::SCOPE_STORES, + $storeId + ); + $configResource->saveConfig( + 'trans_email/ident_general/email', + 'fixture.store.owner@example.com', + ScopeInterface::SCOPE_STORES, + $storeId + ); + $scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $scopeConfig->clean(); +} diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_identity_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_identity_rollback.php new file mode 100644 index 0000000000000..6769a640a13d1 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_store_with_second_identity_rollback.php @@ -0,0 +1,32 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Config\Model\ResourceModel\Config; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\Store; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +$store = $objectManager->create(Store::class); +$storeId = $store->load('fixture_second_store', 'code')->getId(); + +if ($storeId) { + $configResource = $objectManager->get(Config::class); + $configResource->deleteConfig( + 'trans_email/ident_general/name', + ScopeInterface::SCOPE_STORES, + $storeId + ); + $configResource->deleteConfig( + 'trans_email/ident_general/email', + ScopeInterface::SCOPE_STORES, + $storeId + ); +} + +Resolver::getInstance()->requireDataFixture('Magento/Store/_files/second_store_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_store_group_and_store.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_store_group_and_store.php new file mode 100644 index 0000000000000..a4e2b05b1a28c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_store_group_and_store.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Helper\DefaultCategory; +use Magento\CatalogSearch\Model\Indexer\Fulltext; +use Magento\Framework\Indexer\IndexerRegistry; +use Magento\Store\Api\Data\GroupInterface; +use Magento\Store\Api\Data\GroupInterfaceFactory; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\StoreInterfaceFactory; +use \Magento\Store\Api\Data\WebsiteInterface; +use \Magento\Store\Api\Data\WebsiteInterfaceFactory; +use Magento\Store\Model\ResourceModel\Group as GroupResource; +use Magento\Store\Model\ResourceModel\Store as StoreResource; +use Magento\Store\Model\ResourceModel\Website as WebsiteResource; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var StoreManagerInterface $storeManager */ +$storeManager = $objectManager->get(StoreManagerInterface::class); +/** @var WebsiteResource $websiteResource */ +$websiteResource = $objectManager->get(WebsiteResource::class); +/** @var StoreResource $storeResource */ +$storeResource = $objectManager->get(StoreResource::class); +/** @var GroupResource $groupResource */ +$groupResource = $objectManager->get(GroupResource::class); +/** @var DefaultCategory $defaultCategory */ +$defaultCategory = $objectManager->get(DefaultCategory::class); +/** @var WebsiteInterface $website */ +$website = $objectManager->get(WebsiteInterfaceFactory::class)->create(); +$website->setCode('test')->setName('Test Website'); +$websiteResource->save($website); +/** @var GroupInterface $storeGroup */ +$storeGroup = $objectManager->get(GroupInterfaceFactory::class)->create(); +$storeGroup->setCode('second_group') + ->setRootCategoryId($defaultCategory->getId()) + ->setName('second store group') + ->setWebsite($website); +$groupResource->save($storeGroup); +/* Refresh stores memory cache */ +$storeManager->reinitStores(); + +/** @var StoreInterface $store */ +$store = $objectManager->get(StoreInterfaceFactory::class)->create(); +$store->setCode('fixture_second_store') + ->setWebsiteId($website->getId()) + ->setGroupId($storeGroup->getId()) + ->setName('Fixture Second Store') + ->setSortOrder(10) + ->setIsActive(1); +$storeResource->save($store); +/* Refresh CatalogSearch index */ +/** @var IndexerRegistry $indexerRegistry */ +$indexerRegistry = $objectManager->get(IndexerRegistry::class); +$indexerRegistry->get(Fulltext::INDEXER_ID)->reindexAll(); diff --git a/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_store_group_and_store_rollback.php b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_store_group_and_store_rollback.php new file mode 100644 index 0000000000000..bc95019b8dc97 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Store/_files/second_website_with_store_group_and_store_rollback.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\Store\Api\Data\StoreInterface; +use Magento\Store\Api\Data\StoreInterfaceFactory; +use Magento\Store\Api\Data\WebsiteInterface; +use Magento\Store\Api\Data\WebsiteInterfaceFactory; +use Magento\Store\Model\ResourceModel\Store as StoreResource; +use Magento\Store\Model\ResourceModel\Website as WebsiteResource; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var WebsiteResource $websiteResource */ +$websiteResource = $objectManager->get(WebsiteResource::class); +/** @var StoreResource $storeResource */ +$storeResource = $objectManager->get(StoreResource::class); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var WebsiteInterface $website */ +$website = $objectManager->get(WebsiteInterfaceFactory::class)->create(); +$websiteResource->load($website, 'test', 'code'); +if ($website->getId()) { + $websiteResource->delete($website); +} +/** @var StoreInterface $store */ +$store = $objectManager->get(StoreInterfaceFactory::class)->create(); +$storeResource->load($store, 'fixture_second_store', 'code'); +if ($store->getId()) { + $storeResource->delete($store); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); diff --git a/dev/tests/integration/testsuite/Magento/Tax/Controller/Adminhtml/RateTest.php b/dev/tests/integration/testsuite/Magento/Tax/Controller/Adminhtml/RateTest.php index 890f9e7cacb6a..64ee23900dc2a 100644 --- a/dev/tests/integration/testsuite/Magento/Tax/Controller/Adminhtml/RateTest.php +++ b/dev/tests/integration/testsuite/Magento/Tax/Controller/Adminhtml/RateTest.php @@ -6,6 +6,8 @@ namespace Magento\Tax\Controller\Adminhtml; +use Magento\Framework\App\Request\Http as HttpRequest; + /** * @magentoAppArea adminhtml */ @@ -283,4 +285,34 @@ public function testAjaxNonLoadAction() $this->assertArrayHasKey('error_message', $result); $this->assertTrue(strlen($result['error_message'])>0); } + + /** Test Delete Tax Rate + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @return void + */ + public function testDeleteRate(): void + { + $rateId = 2; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue(['rate' => $rateId]); + $this->dispatch('backend/tax/rate/delete'); + $successMessage = (string)__('You deleted the tax rate.'); + $this->assertSessionMessages($this->equalTo([$successMessage])); + } + + /** Test Delete Incorrect Tax Rate + * @magentoAppIsolation enabled + * @magentoDbIsolation enabled + * @return void + */ + public function testDeleteIncorrectRate(): void + { + $incorrectRateId = 20999; + $this->getRequest()->setMethod(HttpRequest::METHOD_POST); + $this->getRequest()->setPostValue(['rate' => $incorrectRateId]); + $this->dispatch('backend/tax/rate/delete'); + $errorMessage = (string)_("We can't delete this rate because of an incorrect rate ID."); + $this->assertSessionMessages($this->equalTo([$errorMessage])); + } } diff --git a/dev/tests/integration/testsuite/Magento/Tax/_files/tax_rule_region_1_al_rollback.php b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_rule_region_1_al_rollback.php new file mode 100644 index 0000000000000..fc9fde077fb3d --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Tax/_files/tax_rule_region_1_al_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Tax\Model\Calculation\Rate; +use Magento\Tax\Model\Calculation\RateFactory; +use Magento\Tax\Model\Calculation\RateRepository; +use Magento\Tax\Model\ResourceModel\Calculation\Rule\Collection; +use Magento\Tax\Model\ResourceModel\Calculation\Rule\CollectionFactory; +use Magento\Tax\Model\TaxRuleRepository; +use Magento\TestFramework\Helper\Bootstrap; + +$objectManager = Bootstrap::getObjectManager(); +/** @var TaxRuleRepository $taxRuleRepository */ +$taxRuleRepository = $objectManager->get(TaxRuleRepository::class); +/** @var Collection $taxRuleCollection */ +$taxRuleCollection = $objectManager->get(CollectionFactory::class)->create(); +/** @var Rate $rate */ +$rate = $objectManager->get(RateFactory::class)->create(); +/** @var RateRepository $rateRepository */ +$rateRepository = $objectManager->get(RateRepository::class); +$taxRuleCollection->addFieldToFilter('code', 'AL Test Rule'); +$taxRule = $taxRuleCollection->getFirstItem(); +if ($taxRule->getId()) { + $taxRuleRepository->delete($taxRule); +} + +$rate->loadByCode('US-AL-*-Rate-1'); +if ($rate->getId()) { + $rateRepository->delete($rate); +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php new file mode 100644 index 0000000000000..326ec789da45a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class FixturesAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php new file mode 100644 index 0000000000000..e0049895577cc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +/** + * Test interface for testing fixtures override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface FixturesInterface +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php new file mode 100644 index 0000000000000..8679c254aa73f --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Fixtures/FixturesTest.php @@ -0,0 +1,208 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Fixtures; + +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Store\Model\ScopeInterface; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** + * Class checks that fixtures override config inherited from abstract class and interface. + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class FixturesTest extends FixturesAbstractClass implements FixturesInterface +{ + /** + * @var ScopeConfigInterface + */ + private $config; + + /** + * @var FixtureCallStorage + */ + private $fixtureCallStorage; + + /** + * @inheritdoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->config = $this->objectManager->get(ScopeConfigInterface::class); + $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); + } + + /** + * @magentoAdminConfigFixture test_section/test_group/field_2 new_value + * @magentoAdminConfigFixture test_section/test_group/field_3 new_value + * @magentoConfigFixture current_store test_section/test_group/field_2 new_value + * @magentoConfigFixture current_store test_section/test_group/field_3 new_value + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * @magentoDataFixtureBeforeTransaction Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixtureBeforeTransaction Magento/TestModuleOverrideConfig/_files/fixture3_first_module.php + * @dataProvider interfaceDataProvider + * @param array $configs + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testInterfaceInheritance( + array $configs, + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($configs); + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @magentoAdminConfigFixture test_section/test_group/field_2 new_value + * @magentoConfigFixture current_store test_section/test_group/field_2 new_value + * @magentoDataFixture Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @magentoDataFixtureBeforeTransaction Magento/TestModuleOverrideConfig/_files/fixture2_first_module.php + * @dataProvider abstractDataProvider + * @param array $configs + * @param array $storeConfigs + * @param array $fixtures + * @return void + */ + public function testAbstractInheritance( + array $configs, + array $storeConfigs, + array $fixtures + ): void { + $this->assertConfigFieldValues($configs); + $this->assertConfigFieldValues($storeConfigs, ScopeInterface::SCOPE_STORES); + $this->assertUsedFixturesCount($fixtures); + } + + /** + * @return array + */ + public function interfaceDataProvider(): array + { + return [ + 'first_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => 'new_value', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => 'new_value', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 2, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 2, + 'fixture3_first_module.php' => 2, + ], + ], + 'second_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => '3rd field default value', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => 'overridden config fixture value for method', + 'test_section/test_group/field_3' => '3rd field website scope default value', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 2, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 2, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * @return array + */ + public function abstractDataProvider(): array + { + return [ + 'first_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => 'overridden config fixture value for data set from abstract', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for class', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => 'overridden config fixture value for data set from abstract', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 2, + 'fixture2_first_module.php' => 0, + 'fixture2_second_module.php' => 0, + 'fixture3_first_module.php' => 2, + ], + ], + 'second_data_set' => [ + 'admin_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for data set from abstract', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => '3rd field default value', + ], + 'store_configs' => [ + 'test_section/test_group/field_1' => 'overridden config fixture value for data set from abstract', + 'test_section/test_group/field_2' => '2nd field default value', + 'test_section/test_group/field_3' => '3rd field website scope default value', + ], + 'fixtures' => [ + 'fixture1_first_module.php' => 0, + 'fixture2_first_module.php' => 0, + 'fixture1_second_module.php' => 2, + 'fixture3_first_module.php' => 0, + ], + ], + ]; + } + + /** + * Asserts config field values. + * + * @param array $configs + * @param string $scope + * @return void + */ + private function assertConfigFieldValues( + array $configs, + string $scope = ScopeConfigInterface::SCOPE_TYPE_DEFAULT + ): void { + foreach ($configs as $path => $expectedValue) { + $this->assertEquals($expectedValue, $this->config->getValue($path, $scope)); + } + } + + /** + * Asserts count of used fixtures. + * + * @param array $fixtures + * @return void + */ + private function assertUsedFixturesCount(array $fixtures): void + { + foreach ($fixtures as $fixture => $count) { + $this->assertEquals($count, $this->fixtureCallStorage->getFixturesCount($fixture)); + } + } +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php new file mode 100644 index 0000000000000..445aa0c501c0a --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipAbstractClass.php @@ -0,0 +1,20 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +use Magento\TestModuleOverrideConfig\AbstractOverridesTest; + +/** + * Test abstract class for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +abstract class SkipAbstractClass extends AbstractOverridesTest +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php new file mode 100644 index 0000000000000..99a9332460211 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipInterface.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Test interface for testing skip override config inheritance. + * + * phpcs:disable Generic.Classes.DuplicateClassName + */ +interface SkipInterface +{ + +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php new file mode 100644 index 0000000000000..e5eb1e3a419f7 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/Inheritance/Skip/SkipTest.php @@ -0,0 +1,56 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestModuleOverrideConfig\Inheritance\Skip; + +/** + * Class checks that test method can be skipped using inherited from abstract class/interface override config + * + * phpcs:disable Generic.Classes.DuplicateClassName + * + * @magentoAppIsolation enabled + */ +class SkipTest extends SkipAbstractClass implements SkipInterface +{ + /** + * @return void + */ + public function testAbstractSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from abstract class'); + } + + /** + * @return void + */ + public function testInterfaceSkip(): void + { + $this->fail('This test should be skipped via override config in method node inherited from interface'); + } + + /** + * @dataProvider skipDataProvider + * + * @param string $message + * @return void + */ + public function testSkipDataSet(string $message): void + { + $this->fail($message); + } + + /** + * @return array + */ + public function skipDataProvider(): array + { + return [ + 'first_data_set' => ['This test should be skipped in data set node inherited from abstract class'], + 'second_data_set' => ['This test should be skipped in data set node inherited from interface'], + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php index 67aaf3116f004..d351847ba4d1c 100644 --- a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/AddFixtureTest.php @@ -31,6 +31,17 @@ protected function setUp(): void $this->config = $this->objectManager->get(ScopeConfigInterface::class); } + /** + * Checks that fixture added in global node successfully applied + * + * @return void + */ + public function testGloballyAddFixture(): void + { + $value = $this->config->getValue('test_section/test_group/field_4', ScopeInterface::SCOPE_STORES); + $this->assertEquals('4th field globally overridden value', $value); + } + /** * Checks that fixture added in test class node successfully applied * diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php index 6e60d4cd90d97..9684f1754dad9 100644 --- a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoConfigFixture/ReplaceFixtureTest.php @@ -31,6 +31,19 @@ protected function setUp(): void $this->config = $this->objectManager->get(ScopeConfigInterface::class); } + /** + * Checks that fixture can be replaced in global node + * + * @magentoConfigFixture current_store test_section/test_group/field_5 new_value + * + * @return void + */ + public function testGloballyReplaceFixture(): void + { + $value = $this->config->getValue('test_section/test_group/field_5', ScopeInterface::SCOPE_STORES); + $this->assertEquals('5th field globally replaced value', $value); + } + /** * Checks that fixture can be replaced in test class node * diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php index 063a717a53669..62e9abcd96659 100644 --- a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/MagentoDataFixture/SortFixturesTest.php @@ -27,6 +27,7 @@ protected function setUp(): void { parent::setUp(); + // phpstan:ignore "Class Magento\TestModuleOverrideConfig\Model\FixtureCallStorage not found." $this->fixtureCallStorage = $this->objectManager->get(FixtureCallStorage::class); } @@ -61,6 +62,7 @@ public function sortFixturesProvider(): array 'fixture2_first_module.php', 'fixture1_third_module.php', 'fixture3_first_module.php', + 'global_fixture_first_module.php',// globally added fixture 'fixture2_second_module.php', ], ], @@ -70,6 +72,7 @@ public function sortFixturesProvider(): array 'fixture1_second_module.php', 'fixture2_first_module.php', 'fixture3_first_module.php', + 'global_fixture_first_module.php',// globally added fixture 'fixture2_second_module.php', ], ], diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php new file mode 100644 index 0000000000000..2681d5b006e1c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** @var FixtureCallStorage $fixtureStorage */ +$fixtureStorage = Bootstrap::getObjectManager()->get(FixtureCallStorage::class); +$fixtureStorage->addFixtureToStorage(basename(__FILE__)); diff --git a/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module_rollback.php b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module_rollback.php new file mode 100644 index 0000000000000..c2b0beacee170 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/TestModuleOverrideConfig/_files/global_fixture_first_module_rollback.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Helper\Bootstrap; +use Magento\TestModuleOverrideConfig\Model\FixtureCallStorage; + +/** @var FixtureCallStorage $fixtureStorage */ +$fixtureStorage = Bootstrap::getObjectManager()->get(FixtureCallStorage::class); +$fixtureStorage->clearStorage(); diff --git a/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php index b3a47bc793a43..e1b645d0f1bbd 100644 --- a/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/DesignTest.php @@ -49,6 +49,9 @@ public function testChangeDesign() $this->assertEquals('Magento/luma', $design->getDesignTheme()->getThemePath()); } + /** + * @magentoDbIsolation disabled + */ public function testCRUD() { $this->_model->setData( @@ -110,7 +113,7 @@ public function testLoadChangeCache() \Magento\Store\Model\StoreManagerInterface::class )->getDefaultStoreView()->getId(); // fixture design_change - + // phpcs:ignore Magento2.Security.InsecureFunction $cacheId = 'design_change_' . md5($storeId . $date); /** @var \Magento\Theme\Model\Design $design */ diff --git a/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/StoreThemesResolverInterfaceTest.php b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/StoreThemesResolverInterfaceTest.php new file mode 100644 index 0000000000000..2f25d99bad6d2 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Theme/Model/Theme/StoreThemesResolverInterfaceTest.php @@ -0,0 +1,147 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Theme\Model\Theme; + +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\App\Config\ScopeConfigInterface; +use Magento\Framework\Serialize\Serializer\Json; +use Magento\Store\Model\ScopeInterface; +use Magento\Store\Model\StoreManagerInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Theme\Model\ResourceModel\Theme\CollectionFactory; +use Magento\Theme\Model\ResourceModel\Theme\Collection; +use PHPUnit\Framework\TestCase; + +class StoreThemesResolverInterfaceTest extends TestCase +{ + const XML_PATH_THEME_USER_AGENT = 'design/theme/ua_regexp'; + /** + * @var StoreThemesResolverInterface + */ + private $model; + /** + * @var Collection + */ + private $themesCollection; + /** + * @var MutableScopeConfigInterface + */ + private $mutableScopeConfig; + /** + * @var Json + */ + private $serializer; + /** + * @var StoreManagerInterface + */ + private $storeManager; + /** + * @var string + */ + private $userAgentDesignConfig; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + $objectManager = Bootstrap::getObjectManager(); + $this->model = $objectManager->get(StoreThemesResolverInterface::class); + $themesCollectionFactory = $objectManager->get(CollectionFactory::class); + $this->themesCollection = $themesCollectionFactory->create(); + $this->mutableScopeConfig = $objectManager->get(MutableScopeConfigInterface::class); + $this->serializer = $objectManager->get(Json::class); + $this->storeManager = $objectManager->get(StoreManagerInterface::class); + $scopeConfig = $objectManager->get(ScopeConfigInterface::class); + $this->userAgentDesignConfig = $scopeConfig->getValue( + self::XML_PATH_THEME_USER_AGENT, + ScopeInterface::SCOPE_STORE + ); + } + + /** + * @inheritDoc + */ + protected function tearDown(): void + { + $this->mutableScopeConfig->setValue( + self::XML_PATH_THEME_USER_AGENT, + $this->userAgentDesignConfig, + ScopeInterface::SCOPE_STORE + ); + parent::tearDown(); + } + + /** + * @param array $config + * @param array $expected + * @dataProvider getThemesDataProvider + */ + public function testGetThemes(array $config, array $expected): void + { + $store = $this->storeManager->getStore(); + $registeredThemes = []; + foreach ($this->themesCollection as $theme) { + $registeredThemes[$theme->getCode()] = $theme->getId(); + } + // convert themes code to id + foreach ($config as $key => $item) { + $config[$key]['value'] = $registeredThemes[$item['value']]; + } + $this->mutableScopeConfig->setValue( + self::XML_PATH_THEME_USER_AGENT, + $config ? $this->serializer->serialize($config) : null, + ScopeInterface::SCOPE_STORE, + $store->getCode() + ); + $expected = array_map( + function ($theme) use ($registeredThemes) { + return $registeredThemes[$theme]; + }, + $expected + ); + $this->assertEquals( + $expected, + $this->model->getThemes($store), + '', + 0.0, + 10, + true + ); + } + + /** + * @return array + */ + public function getThemesDataProvider(): array + { + return [ + [ + [ + ], + [ + 'Magento/luma' + ] + ], + [ + [ + [ + 'search' => '\/Chrome\/i', + 'regexp' => '\/Chrome\/i', + 'value' => 'Magento/blank', + ] + ], + [ + 'Magento/luma', + 'Magento/blank' + ] + ] + ]; + } +} diff --git a/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php b/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php index 8c434e7e10cf7..c87278f230beb 100644 --- a/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php +++ b/dev/tests/integration/testsuite/Magento/Translation/Controller/AjaxTest.php @@ -12,6 +12,8 @@ /** * Test for Magento\Translation\Controller\Ajax class. + * + * @magentoDbIsolation disabled */ class AjaxTest extends \Magento\TestFramework\TestCase\AbstractController { diff --git a/dev/tests/integration/testsuite/Magento/Translation/Model/InlineParserTest.php b/dev/tests/integration/testsuite/Magento/Translation/Model/InlineParserTest.php index 6b1dd4758d633..5078219fbb41f 100644 --- a/dev/tests/integration/testsuite/Magento/Translation/Model/InlineParserTest.php +++ b/dev/tests/integration/testsuite/Magento/Translation/Model/InlineParserTest.php @@ -3,93 +3,134 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + +declare(strict_types=1); + namespace Magento\Translation\Model; -class InlineParserTest extends \PHPUnit\Framework\TestCase +use Magento\Framework\App\Config\MutableScopeConfigInterface; +use Magento\Framework\App\State; +use Magento\Framework\Translate\Inline; +use Magento\Framework\App\Area; +use Magento\Store\Model\ScopeInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Translation\Model\Inline\Parser; +use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; + +/** + * Test for \Magento\Translation\Model\Inline\Parser. + */ +class InlineParserTest extends TestCase { + private const STUB_STORE = 'default'; + private const XML_PATH_TRANSLATE_INLINE_ACTIVE = 'dev/translate_inline/active'; + /** - * @var \Magento\Translation\Model\Inline\Parser + * @var Parser */ - protected $_inlineParser; - - /** @var string */ - protected $_storeId = 'default'; + private $model; + /** + * @inheritDoc + */ protected function setUp(): void { - /** @var $inline \Magento\Framework\Translate\Inline */ - $inline = \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->create(\Magento\Framework\Translate\Inline::class); - $this->_inlineParser = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Translation\Model\Inline\Parser::class, - ['translateInline' => $inline] - ); - /* Called getConfig as workaround for setConfig bug */ - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Store\Model\StoreManagerInterface::class - )->getStore( - $this->_storeId - )->getConfig( - 'dev/translate_inline/active' - ); - \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->get( - \Magento\Framework\App\Config\MutableScopeConfigInterface::class - )->setValue( - 'dev/translate_inline/active', - true, - \Magento\Store\Model\ScopeInterface::SCOPE_STORE, - $this->_storeId - ); + $inline = Bootstrap::getObjectManager()->create(Inline::class); + $this->model = Bootstrap::getObjectManager()->create(Parser::class, ['translateInline' => $inline]); + Bootstrap::getObjectManager()->get(MutableScopeConfigInterface::class) + ->setValue(self::XML_PATH_TRANSLATE_INLINE_ACTIVE, true, ScopeInterface::SCOPE_STORE, self::STUB_STORE); } /** + * Process ajax post test + * * @dataProvider processAjaxPostDataProvider + * + * @param string $originalText + * @param string $translatedText + * @param string $area + * @param bool|null $isPerStore + * @return void */ - public function testProcessAjaxPost($originalText, $translatedText, $isPerStore = null) - { + public function testProcessAjaxPost( + string $originalText, + string $translatedText, + string $area, + ?bool $isPerStore = null + ): void { + Bootstrap::getObjectManager()->get(State::class) + ->setAreaCode($area); + $inputArray = [['original' => $originalText, 'custom' => $translatedText]]; if ($isPerStore !== null) { $inputArray[0]['perstore'] = $isPerStore; } - $this->_inlineParser->processAjaxPost($inputArray); + $this->model->processAjaxPost($inputArray); - $model = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create( - \Magento\Translation\Model\StringUtils::class - ); + $model = Bootstrap::getObjectManager()->create(StringUtils::class); $model->load($originalText); + try { $this->assertEquals($translatedText, $model->getTranslate()); $model->delete(); } catch (\Exception $e) { $model->delete(); - \Magento\TestFramework\Helper\Bootstrap::getObjectManager() - ->get(\Psr\Log\LoggerInterface::class) + Bootstrap::getObjectManager()->get(LoggerInterface::class) ->critical($e); } } /** + * Data provider for testProcessAjaxPost + * * @return array */ - public function processAjaxPostDataProvider() + public function processAjaxPostDataProvider(): array { return [ - ['original text 1', 'translated text 1'], - ['original text 2', 'translated text 2', true] + ['original text 1', 'translated text 1', Area::AREA_ADMINHTML], + ['original text 1', 'translated text 1', Area::AREA_FRONTEND], + ['original text 2', 'translated text 2', Area::AREA_ADMINHTML, true], + ['original text 2', 'translated text 2', Area::AREA_FRONTEND, true], ]; } - public function testSetGetIsJson() + /** + * Set get is json test + * + * @dataProvider allowedAreasDataProvider + * + * @param string $area + * @return void + */ + public function testSetGetIsJson(string $area): void { - $isJsonProperty = new \ReflectionProperty(get_class($this->_inlineParser), '_isJson'); + Bootstrap::getObjectManager()->get(State::class) + ->setAreaCode($area); + + $isJsonProperty = new \ReflectionProperty(get_class($this->model), '_isJson'); $isJsonProperty->setAccessible(true); - $this->assertFalse($isJsonProperty->getValue($this->_inlineParser)); + $this->assertFalse($isJsonProperty->getValue($this->model)); - $setIsJsonMethod = new \ReflectionMethod($this->_inlineParser, 'setIsJson'); + $setIsJsonMethod = new \ReflectionMethod($this->model, 'setIsJson'); $setIsJsonMethod->setAccessible(true); - $setIsJsonMethod->invoke($this->_inlineParser, true); + $setIsJsonMethod->invoke($this->model, true); - $this->assertTrue($isJsonProperty->getValue($this->_inlineParser)); + $this->assertTrue($isJsonProperty->getValue($this->model)); + } + + /** + * Data provider for testSetGetIsJson + * + * @return array + */ + public function allowedAreasDataProvider(): array + { + return [ + [Area::AREA_ADMINHTML], + [Area::AREA_FRONTEND] + ]; } } diff --git a/dev/tests/integration/testsuite/Magento/Ui/Component/Control/ButtonTest.php b/dev/tests/integration/testsuite/Magento/Ui/Component/Control/ButtonTest.php new file mode 100644 index 0000000000000..d5bfda2afcd48 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Component/Control/ButtonTest.php @@ -0,0 +1,83 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Ui\Component\Control; + +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Test for the button control. + * + * @magentoAppArea frontend + */ +class ButtonTest extends TestCase +{ + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->layout = $objectManager->get(LayoutInterface::class); + } + + /** + * Create the block. + * + * @return Button + */ + private function createBlock(): Button + { + /** @var Button $block */ + $block = $this->layout->createBlock(Button::class, 'button_block'); + $block->setLayout($this->layout); + + return $block; + } + + /** + * Test resulting button HTML. + * + * @return void + */ + public function testToHtml(): void + { + $block = $this->createBlock(); + $block->addData( + [ + 'type' => 'button', + 'on_click' => $onclick = 'console.log("Button pressed!")', + 'disabled' => false, + 'title' => 'A button control', + 'label' => 'A button control', + 'class' => 'button', + 'id' => 'button', + 'element_name' => 'some-name', + 'value' => 'Press a button', + 'data-style' => 'width: 100px', + 'style' => 'height: 200px' + ] + ); + + $html = $block->toHtml(); + $this->assertStringContainsString('<button ', $html); + $this->assertStringContainsString('<span>A button control</span>', $html); + $this->assertStringNotContainsString('onclick=', $html); + $this->assertStringNotContainsString('style=', $html); + $this->assertMatchesRegularExpression('/\<script.*?\>.*?' .preg_quote($onclick) .'.*?\<\/script\>/ims', $html); + $this->assertStringContainsString('height', $html); + $this->assertStringContainsString('200px', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/Ui/Component/Control/SplitButtonTest.php b/dev/tests/integration/testsuite/Magento/Ui/Component/Control/SplitButtonTest.php new file mode 100644 index 0000000000000..48caf7ed8b859 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Ui/Component/Control/SplitButtonTest.php @@ -0,0 +1,92 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Ui\Component\Control; + +use Magento\Framework\View\LayoutInterface; +use Magento\TestFramework\Helper\Bootstrap; +use PHPUnit\Framework\TestCase; + +/** + * Testing SplitButton widget + * + * @magentoAppArea frontend + */ +class SplitButtonTest extends TestCase +{ + + /** + * @var LayoutInterface + */ + private $layout; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $objectManager = Bootstrap::getObjectManager(); + $this->layout = $objectManager->get(LayoutInterface::class); + } + + /** + * Create the block. + * + * @return SplitButton + */ + private function createBlock(): SplitButton + { + /** @var SplitButton $block */ + $block = $this->layout->createBlock(SplitButton::class, 'button_block'); + $block->setLayout($this->layout); + + return $block; + } + + /** + * Test resulting button HTML. + * + * @return void + */ + public function testToHtml(): void + { + $block = $this->createBlock(); + $block->addData( + [ + 'title' => 'Split button control', + 'label' => 'Split button control', + 'has_split' => true, + 'button_class' => 'aclass', + 'id' => 'split-button', + 'disabled' => false, + 'class' => 'aclass', + 'data_attribute' => ['bind' => ['var' => 'val']], + 'id_hard' => 'split-button', + 'options' => [ + [ + 'id' => 'an-option', + 'disabled' => false, + 'title' => 'An option', + 'label' => 'An option', + 'onclick' => $onclick = 'console.log("option")', + 'style' => 'width: 100px' + ] + ] + ] + ); + + $html = $block->toHtml(); + $this->assertStringContainsString('<button ', $html); + $this->assertStringContainsString('<span>Split button control</span>', $html); + $this->assertStringNotContainsString('onclick=', $html); + $this->assertStringNotContainsString('style=', $html); + $this->assertMatchesRegularExpression('/\<script.*?\>.*?' . preg_quote($onclick) . '.*?\<\/script\>/ims', $html); + $this->assertStringContainsString('width', $html); + $this->assertStringContainsString('100px', $html); + } +} diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Category/EditTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Category/EditTest.php index 04fe9cf1e94a5..93101acbd573f 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Category/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Category/EditTest.php @@ -123,24 +123,20 @@ private function _checkButtons($block, $expected) if (isset($expected['back_button'])) { if ($expected['back_button']) { if ($block->getCategory()->getId()) { - $this->assertEquals( - 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( - '//button[contains(@class, "back") and contains(@onclick, "/category")]', - $buttonsHtml - ), - 'Back button is not present in category URL rewrite edit block' - ); - } else { - $this->assertEquals( - 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( - '//button[contains(@class,"back")]', - $buttonsHtml - ), + $this->assertRegExp( + '/setLocation\([\\\'\"]\S+?\/category/i', + $buttonsHtml, 'Back button is not present in category URL rewrite edit block' ); } + $this->assertEquals( + 1, + \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + '//button[contains(@class,"back")]', + $buttonsHtml + ), + 'Back button is not present in category URL rewrite edit block' + ); } else { $this->assertEquals( 0, diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Product/EditTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Product/EditTest.php index 61b4e5867b822..6398be57fb09a 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Product/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Catalog/Product/EditTest.php @@ -155,24 +155,20 @@ private function _checkButtons($block, $expected) if (isset($expected['back_button'])) { if ($expected['back_button']) { if ($block->getProduct()->getId()) { - $this->assertEquals( - 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( - '//button[contains(@class, "back") and contains(@onclick, "/product")]', - $buttonsHtml - ), - 'Back button is not present in product URL rewrite edit block' - ); - } else { - $this->assertEquals( - 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( - '//button[contains(@class,"back")]', - $buttonsHtml - ), - 'Back button is not present in product URL rewrite edit block' + $this->assertRegExp( + '/setLocation\([\\\'\"]\S+?\/product/i', + $buttonsHtml, + 'Back button is not present in category URL rewrite edit block' ); } + $this->assertEquals( + 1, + \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + '//button[contains(@class,"back")]', + $buttonsHtml + ), + 'Back button is not present in product URL rewrite edit block' + ); } else { $this->assertEquals( 0, diff --git a/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Cms/Page/EditTest.php b/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Cms/Page/EditTest.php index 0c83fd8f582e6..c2cb2d172b3f0 100644 --- a/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Cms/Page/EditTest.php +++ b/dev/tests/integration/testsuite/Magento/UrlRewrite/Block/Cms/Page/EditTest.php @@ -123,24 +123,20 @@ private function _checkButtons($block, $expected) if (isset($expected['back_button'])) { if ($expected['back_button']) { if ($block->getCmsPage()->getId()) { - $this->assertEquals( - 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( - '//button[contains(@class, "back") and contains(@onclick, "/cms_page")]', - $buttonsHtml - ), - 'Back button is not present in CMS page URL rewrite edit block' - ); - } else { - $this->assertEquals( - 1, - \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( - '//button[contains(@class,"back")]', - $buttonsHtml - ), - 'Back button is not present in CMS page URL rewrite edit block' + $this->assertRegExp( + '/setLocation\([\\\'\"]\S+?\/cms_page/i', + $buttonsHtml, + 'Back button is not present in category URL rewrite edit block' ); } + $this->assertEquals( + 1, + \Magento\TestFramework\Helper\Xpath::getElementsCountForXpath( + '//button[contains(@class, "back")]', + $buttonsHtml + ), + 'Back button is not present in CMS page URL rewrite edit block' + ); } else { $this->assertEquals( 0, diff --git a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php index e0e93dbd37b13..5d3d8b2a090c9 100644 --- a/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php +++ b/dev/tests/integration/testsuite/Magento/Widget/Controller/Adminhtml/Widget/InstanceTest.php @@ -34,8 +34,8 @@ protected function setUp(): void public function testEditAction() { $this->dispatch('backend/admin/widget_instance/edit'); - $this->assertStringContainsString( - '<option value="cms_page_link" selected="selected">', + $this->assertRegExp( + '/<option value="cms_page_link".*?selected="selected"\>/is', $this->getResponse()->getBody() ); } diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/Wishlist/Item/ColumnTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/Wishlist/Item/ColumnTest.php index 908cd3aebae61..a2e8390bea3ef 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/Wishlist/Item/ColumnTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/Wishlist/Item/ColumnTest.php @@ -21,7 +21,7 @@ /** * Test wish list item column. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/WishlistTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/WishlistTest.php index bc953c3e999b1..b2fc1977a54ca 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/WishlistTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Block/Customer/WishlistTest.php @@ -18,7 +18,7 @@ * Class test block wish list on customer account page. * * @magentoAppArea frontend - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppIsolation disabled */ class WishlistTest extends TestCase diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/AddTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/AddTest.php index 082a023a961f4..492aa5935238e 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/AddTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/AddTest.php @@ -14,12 +14,12 @@ use Magento\Framework\Message\MessageInterface; use Magento\TestFramework\TestCase\AbstractController; use Magento\TestFramework\Wishlist\Model\GetWishlistByCustomerId; -use Zend\Stdlib\Parameters; +use Laminas\Stdlib\Parameters; /** * Test for add product to wish list. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend * @magentoDataFixture Magento/Customer/_files/customer.php */ diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/CartTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/CartTest.php index 29849d5e80aca..a7ab5115043f7 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/CartTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/CartTest.php @@ -69,7 +69,8 @@ public function testAddSimpleProductToCart(): void $item = $this->getWishlistByCustomerId->getItemBySku(1, 'simple-1'); $this->assertNotNull($item); $this->performAddToCartRequest(['item' => $item->getId(), 'qty' => 3]); - $message = sprintf('You added %s to your shopping cart.', $item->getName()); + $message = sprintf("\n" . 'You added %s to your ' . + '<a href="http://localhost/index.php/checkout/cart/">shopping cart</a>.', $item->getName()); $this->assertSessionMessages($this->equalTo([(string)__($message)]), MessageInterface::TYPE_SUCCESS); $this->assertCount(0, $this->getWishlistByCustomerId->execute(1)->getItemCollection()); $cart = $this->cartFactory->create(); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/IndexTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/IndexTest.php index ca480e28997fe..c13a635f73e7f 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/IndexTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/IndexTest.php @@ -14,7 +14,7 @@ /** * Test wish list on customer account page. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend */ class IndexTest extends AbstractController diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/RemoveTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/RemoveTest.php index f6cea2f9757f6..23e303995aa42 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/RemoveTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/RemoveTest.php @@ -16,7 +16,7 @@ /** * Test for remove product from wish list. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend * @magentoDataFixture Magento/Wishlist/_files/wishlist.php */ diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/SendTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/SendTest.php index d087dd567c354..deae6f51d9f37 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/SendTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/SendTest.php @@ -19,7 +19,7 @@ /** * Test sending wish list. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend * @magentoDataFixture Magento/Wishlist/_files/wishlist.php */ diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateItemOptionsTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateItemOptionsTest.php index 4301121704078..7873829207c1f 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateItemOptionsTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateItemOptionsTest.php @@ -153,6 +153,33 @@ public function testUpdateItemNotSpecifyAsWishListItem(): void $this->assertRedirect($this->stringContains('wishlist/index/index/wishlist_id/')); } + /** + * @magentoDataFixture Magento/Wishlist/_files/wishlist_with_grouped_product.php + * @magentoDbIsolation disabled + * + * @return void + */ + public function testUpdateItemOptionsForGroupedProduct(): void + { + $this->customerSession->setCustomerId(1); + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'grouped'); + $this->assertNotNull($item); + $params = [ + 'id' => $item->getId(), + 'product' => $item->getProductId(), + 'super_group' => $this->performGroupedOption(), + 'qty' => 1, + ]; + $this->performUpdateWishListItemRequest($params); + $message = sprintf("%s has been updated in your Wish List.", $item->getProduct()->getName()); + $this->assertSessionMessages($this->equalTo([(string)__($message)]), MessageInterface::TYPE_SUCCESS); + $this->assertRedirect($this->stringContains('wishlist/index/index/wishlist_id/' . $item->getWishlistId())); + $this->assertUpdatedItem( + $this->getWishlistByCustomerId->getItemBySku(1, 'grouped'), + $params + ); + } + /** * Perform request update wish list item. * @@ -195,4 +222,20 @@ private function performConfigurableOption(ProductInterface $product): array return [$attributeId => $option['value_index']]; } + + /** + * Perform group option to select. + * + * @return array + */ + private function performGroupedOption(): array + { + $simple1 = $this->productRepository->get('simple_11'); + $simple2 = $this->productRepository->get('simple_22'); + + return [ + $simple1->getId() => '3', + $simple2->getId() => '3', + ]; + } } diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateTest.php index be5762c72be86..6faa2a7a1aa69 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/Index/UpdateTest.php @@ -16,7 +16,7 @@ /** * Test for update wish list item. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppArea frontend * @magentoDataFixture Magento/Wishlist/_files/wishlist.php */ diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/SharedTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/SharedTest.php index 1e4f6c6584e9a..6cd6f3a5fe5ba 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Controller/SharedTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Controller/SharedTest.php @@ -10,6 +10,7 @@ class SharedTest extends \Magento\TestFramework\TestCase\AbstractController { /** * @magentoDataFixture Magento/Wishlist/_files/wishlist_shared.php + * @magentoDbIsolation disabled * @return void */ public function testAllcartAction() diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php index b6c2886f33021..9a95ed4fd462d 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/ResourceModel/Item/CollectionTest.php @@ -46,7 +46,7 @@ protected function setUp(): void * * @magentoDataFixture Magento/Wishlist/_files/wishlist_shared.php * @magentoAppIsolation enabled - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled */ public function testLoadedProductAttributes() { diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php b/dev/tests/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php index 84ee7d8984cc4..203a15d35ea96 100644 --- a/dev/tests/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php +++ b/dev/tests/integration/testsuite/Magento/Wishlist/Model/WishlistTest.php @@ -20,7 +20,7 @@ /** * Tests for wish list model. * - * @magentoDbIsolation enabled + * @magentoDbIsolation disabled * @magentoAppIsolation disabled */ class WishlistTest extends TestCase @@ -116,17 +116,13 @@ public function testGetItemCollection(): void } /** - * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * @magentoDataFixture Magento/Wishlist/_files/wishlist_with_disabled_simple_product.php * * @return void */ public function testGetItemCollectionWithDisabledProduct(): void { - $productSku = 'simple'; $customerId = 1; - $product = $this->productRepository->get($productSku); - $product->setStatus(ProductStatus::STATUS_DISABLED); - $this->productRepository->save($product); $this->assertEmpty($this->getWishlistByCustomerId->execute($customerId)->getItemCollection()->getItems()); } @@ -143,7 +139,12 @@ public function testAddConfigurableProductToWishList(): void $configurableOptions = $configurableProduct->getTypeInstance()->getConfigurableOptions($configurableProduct); $attributeId = key($configurableOptions); $option = reset($configurableOptions[$attributeId]); - $buyRequest = ['super_attribute' => [$attributeId => $option['value_index']]]; + $buyRequest = [ + 'super_attribute' => [ + $attributeId => $option['value_index'] + ], + 'action' => 'add', + ]; $wishlist = $this->getWishlistByCustomerId->execute(1); $wishlist->addNewItem($configurableProduct, $buyRequest); $item = $this->getWishlistByCustomerId->getItemBySku(1, 'Configurable product'); @@ -166,7 +167,12 @@ public function testAddBundleProductToWishList(): void $option = reset($bundleOptions); $productLinks = $option->getProductLinks(); $this->assertNotNull($productLinks[0]); - $buyRequest = ['bundle_option' => [$option->getOptionId() => $productLinks[0]->getId()]]; + $buyRequest = [ + 'bundle_option' => [ + $option->getOptionId() => $productLinks[0]->getId() + ], + 'action' => 'add', + ]; $skuWithChosenOption = implode('-', [$bundleProduct->getSku(), $productLinks[0]->getSku()]); $wishlist = $this->getWishlistByCustomerId->execute(1); $wishlist->addNewItem($bundleProduct, $buyRequest); @@ -217,6 +223,27 @@ public function testUpdateItemQtyInWishList(): void $this->assertEquals(55, $updatedItem->getQty()); } + /** + * Update description of wishlist item + * + * @magentoDataFixture Magento/Wishlist/_files/wishlist.php + * + * @return void + */ + public function testUpdateItemDescriptionInWishList(): void + { + $itemDescription = 'Test Description'; + $wishlist = $this->getWishlistByCustomerId->execute(1); + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'simple'); + $item->setDescription($itemDescription); + $this->assertNotNull($item); + $buyRequest = $this->dataObjectFactory->create(['data' => ['qty' => 55]]); + $wishlist->updateItem($item, $buyRequest); + $updatedItem = $this->getWishlistByCustomerId->getItemBySku(1, 'simple'); + $this->assertEquals(55, $updatedItem->getQty()); + $this->assertEquals($itemDescription, $updatedItem->getDescription()); + } + /** * @return void */ @@ -241,6 +268,27 @@ public function testUpdateNotExistingProductInWishList(): void $wishlist->updateItem($item, []); } + /** + * Test that admin user should be able to update wishlist on second website + * + * @magentoAppArea adminhtml + * @magentoDbIsolation disabled + * @magentoDataFixture Magento/Wishlist/_files/wishlist_on_second_website.php + * + * @return void + */ + public function testUpdateWishListItemOnSecondWebsite(): void + { + $wishlist = $this->getWishlistByCustomerId->execute(1); + $item = $this->getWishlistByCustomerId->getItemBySku(1, 'simple-2'); + $this->assertNotNull($item); + $this->assertEquals(1, $item->getQty()); + $buyRequest = $this->dataObjectFactory->create(['data' => ['qty' => 2]]); + $wishlist->updateItem($item->getId(), $buyRequest); + $updatedItem = $this->getWishlistByCustomerId->getItemBySku(1, 'simple-2'); + $this->assertEquals(2, $updatedItem->getQty()); + } + /** * Assert item in wish list. * diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product.php new file mode 100644 index 0000000000000..22583483ddf69 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_product_disabled.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var WishlistResource $wishListResource */ +$wishListResource = $objectManager->get(WishlistResource::class); +/** @var Wishlist $wishlist */ +$wishlist = $objectManager->get(WishlistFactory::class)->create(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $customerRepository->get('customer@example.com'); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +$product = $productRepository->get('product_disabled'); +$wishlist->loadByCustomerId($customer->getId(), true); +$item = $wishlist->addNewItem($product); +$wishlist->setSharingCode('wishlist_disabled_item'); +$wishListResource->save($wishlist); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product_rollback.php new file mode 100644 index 0000000000000..665644cd9b6db --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_product_rollback.php @@ -0,0 +1,33 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var WishlistResource $wishListResource */ +$wishListResource = $objectManager->get(WishlistResource::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var Wishlist $wishlist */ +$wishlist = $objectManager->get(WishlistFactory::class)->create(); +$wishlist->loadByCustomerId(1); +if ($wishlist->getId()) { + $wishListResource->delete($wishlist); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_product_disabled.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_simple_product.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_simple_product.php new file mode 100644 index 0000000000000..6fe032d5990dc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_disabled_simple_product.php @@ -0,0 +1,18 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +use Magento\Catalog\Model\Product\Attribute\Source\Status as ProductStatus; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Wishlist/_files/wishlist.php'); + +$objectManager = \Magento\TestFramework\Helper\Bootstrap::getObjectManager(); +$productRepository = $objectManager->create(\Magento\Catalog\Api\ProductRepositoryInterface::class); + +$productSku = 'simple'; +$product = $productRepository->get($productSku); +$product->setStatus(ProductStatus::STATUS_DISABLED); +$productRepository->save($product); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_grouped_product.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_grouped_product.php new file mode 100644 index 0000000000000..94d23436e2a00 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_grouped_product.php @@ -0,0 +1,41 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Model\CustomerRegistry; +use Magento\Framework\DataObject; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/GroupedProduct/_files/product_grouped_with_simple.php' +); + +$objectManager = Bootstrap::getObjectManager(); +/** @var CustomerRegistry $customerRegistry */ +$customerRegistry = $objectManager->get(CustomerRegistry::class); +$customer = $customerRegistry->retrieve(1); +$wishlistFactory = $objectManager->get(WishlistFactory::class); +$wishlist = $wishlistFactory->create(); +$wishlist->loadByCustomerId($customer->getId(), true); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$product = $productRepository->get('grouped'); +$simple1 = $productRepository->get('simple_11'); +$simple2 = $productRepository->get('simple_22'); +$buyRequest = new DataObject([ + 'product' => $product->getId(), + 'super_group' => + [ + $simple1->getId() => '1', + $simple2->getId() => '1', + ], + 'action' => 'add', +]); +$wishlist->addNewItem($product, $buyRequest); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_grouped_product_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_grouped_product_rollback.php new file mode 100644 index 0000000000000..dc8c56d8eb73c --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_grouped_product_rollback.php @@ -0,0 +1,13 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); +Resolver::getInstance()->requireDataFixture( + 'Magento/GroupedProduct/_files/product_grouped_with_simple_rollback.php' +); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_not_visible_product.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_not_visible_product.php new file mode 100644 index 0000000000000..3777ed5258a65 --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_not_visible_product.php @@ -0,0 +1,35 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + + +use Magento\Catalog\Api\ProductRepositoryInterface; +use Magento\Customer\Api\CustomerRepositoryInterface; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer.php'); +Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/simple_products_not_visible_individually.php'); + +$objectManager = Bootstrap::getObjectManager(); +/** @var WishlistResource $wishListResource */ +$wishListResource = $objectManager->get(WishlistResource::class); +/** @var Wishlist $wishList */ +$wishList = $objectManager->get(WishlistFactory::class)->create(); +/** @var ProductRepositoryInterface $productRepository */ +$productRepository = $objectManager->get(ProductRepositoryInterface::class); +$productRepository->cleanCache(); +/** @var CustomerRepositoryInterface $customerRepository */ +$customerRepository = $objectManager->get(CustomerRepositoryInterface::class); +$customer = $customerRepository->get('customer@example.com'); +$product = $productRepository->get('simple_not_visible_1'); +$wishList->loadByCustomerId($customer->getId(), true); +$item = $wishList->addNewItem($product); +$wishList->setSharingCode('fixture_unique_code'); +$wishListResource->save($wishList); diff --git a/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_not_visible_product_rollback.php b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_not_visible_product_rollback.php new file mode 100644 index 0000000000000..d747566d68ddc --- /dev/null +++ b/dev/tests/integration/testsuite/Magento/Wishlist/_files/wishlist_with_not_visible_product_rollback.php @@ -0,0 +1,34 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Registry; +use Magento\TestFramework\Helper\Bootstrap; +use Magento\Wishlist\Model\ResourceModel\Wishlist as WishlistResource; +use Magento\Wishlist\Model\Wishlist; +use Magento\Wishlist\Model\WishlistFactory; +use Magento\TestFramework\Workaround\Override\Fixture\Resolver; + +$objectManager = Bootstrap::getObjectManager(); +/** @var Registry $registry */ +$registry = $objectManager->get(Registry::class); +/** @var WishlistResource $wishListResource */ +$wishListResource = $objectManager->get(WishlistResource::class); +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', true); +/** @var Wishlist $wishlist */ +$wishlist = $objectManager->get(WishlistFactory::class)->create(); +$wishlist->loadByCustomerId(1); +if ($wishlist->getId()) { + $wishListResource->delete($wishlist); +} + +$registry->unregister('isSecureArea'); +$registry->register('isSecureArea', false); + +Resolver::getInstance() + ->requireDataFixture('Magento/Catalog/_files/simple_products_not_visible_individually_rollback.php'); +Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_rollback.php'); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/list/toolbar.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/list/toolbar.test.js new file mode 100644 index 0000000000000..d434b9fab0fcf --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Catalog/frontend/js/product/list/toolbar.test.js @@ -0,0 +1,47 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'Magento_Catalog/js/product/list/toolbar' +], function ($) { + 'use strict'; + + describe('Magento_Catalog/js/product/list/toolbar', function () { + var widget, + toolbar; + + beforeEach(function () { + toolbar = $('<div class="toolbar"></div>'); + }); + + afterEach(function () { + toolbar.remove(); + }); + + it('Widget extends jQuery object', function () { + expect($.mage.productListToolbarForm).toBeDefined(); + }); + + it('Toolbar is initialized', function () { + spyOn($.mage.productListToolbarForm.prototype, '_create'); + + toolbar.productListToolbarForm(); + + expect($.mage.productListToolbarForm.prototype._create).toEqual(jasmine.any(Function)); + expect($.mage.productListToolbarForm.prototype._create).toHaveBeenCalledTimes(1); + }); + + it('Toolbar receives options properly', function () { + toolbar.productListToolbarForm(); + expect(toolbar.productListToolbarForm('option', 'page')).toBe('p'); + }); + + it('Toolbar receives element properly', function () { + widget = toolbar.productListToolbarForm(); + expect(widget).toBe(toolbar); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/empty-cart.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/empty-cart.test.js new file mode 100644 index 0000000000000..7de56c869a81a --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/empty-cart.test.js @@ -0,0 +1,113 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable max-nested-callbacks */ +/*jscs:disable jsDoc*/ +define([ + 'squire', 'jquery', 'ko' +], function (Squire, $, ko) { + 'use strict'; + + describe('Magento_Checkout/js/empty-cart', function () { + var injector = new Squire(), + cartData = ko.observable({}), + mocks = { + 'Magento_Customer/js/customer-data': { + get: jasmine.createSpy('get', function () { + return cartData; + }).and.callThrough(), + reload: jasmine.createSpy(), + getInitCustomerData: function () {} + } + }, + deferred, + emptyCart; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require(['Magento_Checkout/js/empty-cart'], function (instance) { + emptyCart = instance; + done(); + }); + }); + + afterEach(function () { + try { + injector.clean(); + injector.remove(); + } catch (e) {} + + cartData({}); + }); + + describe('Check Cart data preparation process', function () { + it('Tests that Cart data is NOT checked before initialization', function () { + spyOn(mocks['Magento_Customer/js/customer-data'], 'getInitCustomerData').and.callFake(function () { + deferred = $.Deferred(); + + return deferred.promise(); + }); + expect(emptyCart()).toBe(undefined); + + expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); + expect(mocks['Magento_Customer/js/customer-data'].getInitCustomerData).toHaveBeenCalled(); + expect(mocks['Magento_Customer/js/customer-data'].reload).not.toHaveBeenCalled(); + }); + + it('Tests that Cart data does NOT reload if there are no items in it', function () { + spyOn(mocks['Magento_Customer/js/customer-data'], 'getInitCustomerData').and.callFake(function () { + deferred = $.Deferred(); + + deferred.resolve(); + + return deferred.promise(); + }); + cartData({ + items: [] + }); + emptyCart(); + + expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); + expect(mocks['Magento_Customer/js/customer-data'].reload).not.toHaveBeenCalled(); + }); + + it('Tests that Cart data is checked only after initialization', function () { + spyOn(mocks['Magento_Customer/js/customer-data'], 'getInitCustomerData').and.callFake(function () { + deferred = $.Deferred(); + + return deferred.promise(); + }); + cartData({ + items: [1] + }); + emptyCart(); + + expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); + expect(mocks['Magento_Customer/js/customer-data'].reload).not.toHaveBeenCalled(); + + deferred.resolve(); + + expect(mocks['Magento_Customer/js/customer-data'].reload).toHaveBeenCalledWith(['cart'], false); + }); + + it('Tests that Cart data reloads if it has items', function () { + spyOn(mocks['Magento_Customer/js/customer-data'], 'getInitCustomerData').and.callFake(function () { + deferred = $.Deferred(); + + deferred.resolve(); + + return deferred.promise(); + }); + cartData({ + items: [1] + }); + emptyCart(); + + expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); + expect(mocks['Magento_Customer/js/customer-data'].reload).toHaveBeenCalledWith(['cart'], false); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/region-updater.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/region-updater.test.js new file mode 100644 index 0000000000000..0fdf36f0cfecc --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/region-updater.test.js @@ -0,0 +1,243 @@ +/* + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +/*eslint-disable max-nested-callbacks*/ +/*jscs:disable jsDoc*/ +define([ + 'jquery', + 'Magento_Checkout/js/region-updater' +], function ($) { + 'use strict'; + + var regionJson = { + 'config': { + 'show_all_regions': true, + 'regions_required': [ + 'US' + ] + }, + 'US': { + '1': { + 'code': 'AL', + 'name': 'Alabama' + }, + '2': { + 'code': 'AK', + 'name': 'Alaska' + }, + '3': { + 'code': 'AS', + 'name': 'American Samoa' + } + }, + 'DE': { + '81': { + 'code': 'BAY', + 'name': 'Bayern' + }, + '82': { + 'code': 'BER', + 'name': 'Berlin' + }, + '83': { + 'code': 'BRG', + 'name': 'Brandenburg' + } + } + }, + defaultCountry = 'GB', + countries = { + '': '', + 'US': 'United States', + 'GB': 'United Kingdom', + 'DE': 'Germany', + 'IT': 'Italy' + }, + regions = { + '': 'Please select a region, state or province.' + }, + countryEl, + regionSelectEl, + regionInputEl, + postalCodeEl, + formEl, + containerEl; + + function createFormField() { + var fieldWrapperEl = document.createElement('div'), + labelEl = document.createElement('label'), + inputControlEl = document.createElement('div'), + i; + + fieldWrapperEl.appendChild(labelEl); + fieldWrapperEl.appendChild(inputControlEl); + + for (i = 0; i < arguments.length; i++) { + inputControlEl.appendChild(arguments[i]); + } + labelEl.setAttribute('class', 'label'); + fieldWrapperEl.setAttribute('class', 'field required'); + inputControlEl.setAttribute('class', 'control'); + + return fieldWrapperEl; + } + + function buildSelectOptions(select, options, defaultOption) { + var optionValue, + optionEl; + + defaultOption = typeof defaultOption === 'undefined' ? '' : defaultOption; + + // eslint-disable-next-line guard-for-in + for (optionValue in options) { + if (options.hasOwnProperty(optionValue)) { + optionEl = document.createElement('option'); + optionEl.setAttribute('value', optionValue); + optionEl.textContent = countries[optionValue]; + // eslint-disable-next-line max-depth + if (defaultOption === optionValue) { + optionEl.setAttribute('selected', 'selected'); + } + select.add(optionEl); + } + } + } + + function init(config) { + var defaultConfig = { + 'optionalRegionAllowed': true, + 'regionListId': '#' + regionSelectEl.id, + 'regionInputId': '#' + regionInputEl.id, + 'postcodeId': '#' + postalCodeEl.id, + 'form': '#' + formEl.id, + 'regionJson': regionJson, + 'defaultRegion': 0, + 'countriesWithOptionalZip': ['GB'] + }; + + $(countryEl).regionUpdater($.extend({}, defaultConfig, config || {})); + } + + beforeEach(function () { + containerEl = document.createElement('div'); + formEl = document.createElement('form'); + regionSelectEl = document.createElement('select'); + regionInputEl = document.createElement('input'); + postalCodeEl = document.createElement('input'); + countryEl = document.createElement('select'); + regionSelectEl.setAttribute('id', 'region_id'); + regionSelectEl.setAttribute('style', 'display:none;'); + regionInputEl.setAttribute('id', 'region'); + regionInputEl.setAttribute('style', 'display:none;'); + countryEl.setAttribute('id', 'country'); + postalCodeEl.setAttribute('id', 'zip'); + formEl.setAttribute('id', 'test_form'); + formEl.appendChild(createFormField(countryEl)); + formEl.appendChild(createFormField(regionSelectEl, regionInputEl)); + formEl.appendChild(createFormField(postalCodeEl)); + containerEl.appendChild(formEl); + buildSelectOptions(regionSelectEl, regions); + buildSelectOptions(countryEl, countries, defaultCountry); + document.body.appendChild(containerEl); + }); + + afterEach(function () { + $(containerEl).remove(); + formEl = undefined; + containerEl = undefined; + regionSelectEl = undefined; + regionInputEl = undefined; + postalCodeEl = undefined; + countryEl = undefined; + }); + + describe('Magento_Checkout/js/region-updater', function () { + it('Check that default country is selected', function () { + init(); + expect($(countryEl).val()).toBe(defaultCountry); + }); + it('Check that region list is not displayed when selected country has no predefined regions', function () { + init(); + $(countryEl).val('GB').change(); + expect($(regionInputEl).is(':visible')).toBe(true); + expect($(regionInputEl).is(':disabled')).toBe(false); + expect($(regionSelectEl).is(':visible')).toBe(false); + expect($(regionSelectEl).is(':disabled')).toBe(true); + }); + it('Check country that has predefined and optional regions', function () { + init(); + $(countryEl).val('DE').change(); + expect($(regionSelectEl).is(':visible')).toBe(true); + expect($(regionSelectEl).is(':disabled')).toBe(false); + expect($(regionSelectEl).hasClass('required-entry')).toBe(false); + expect($(regionInputEl).is(':visible')).toBe(false); + expect( + $(regionSelectEl).find('option') + .map(function () { + return this.textContent; + }) + .get() + ).toContain('Berlin'); + }); + it('Check country that has predefined and required regions', function () { + init(); + $(countryEl).val('US').change(); + expect($(regionSelectEl).is(':visible')).toBe(true); + expect($(regionSelectEl).is(':disabled')).toBe(false); + expect($(regionSelectEl).hasClass('required-entry')).toBe(true); + expect($(regionInputEl).is(':visible')).toBe(false); + expect( + $(regionSelectEl).find('option') + .map(function () { + return this.textContent; + }) + .get() + ).toContain('Alaska'); + }); + it('Check that region fields are not displayed for country with optional regions if configured', function () { + init({ + optionalRegionAllowed: false + }); + $(countryEl).val('DE').change(); + expect($(regionSelectEl).is(':visible')).toBe(false); + expect($(regionInputEl).is(':visible')).toBe(false); + }); + it('Check that initial values are not overwritten - region input', function () { + $(countryEl).val('GB'); + $(regionInputEl).val('Liverpool'); + $(postalCodeEl).val('L13 0AL'); + init(); + expect($(countryEl).val()).toBe('GB'); + expect($(regionInputEl).val()).toBe('Liverpool'); + expect($(postalCodeEl).val()).toBe('L13 0AL'); + }); + it('Check that initial values are not overwritten - region select', function () { + $(countryEl).val('US'); + $(postalCodeEl).val('99501'); + init({ + defaultRegion: '2' + }); + expect($(countryEl).val()).toBe('US'); + expect($(regionSelectEl).find('option:selected').text()).toBe('Alaska'); + expect($(postalCodeEl).val()).toBe('99501'); + }); + it('Check that region values are cleared out on country change - region input', function () { + $(countryEl).val('GB'); + $(regionInputEl).val('Liverpool'); + init(); + $(countryEl).val('IT').change(); + expect($(countryEl).val()).toBe('IT'); + expect($(regionInputEl).val()).toBe(''); + }); + it('Check that region values are cleared out on country change - region select', function () { + $(countryEl).val('US'); + init({ + defaultRegion: '2' + }); + $(countryEl).val('DE').change(); + expect($(countryEl).val()).toBe('DE'); + expect($(regionSelectEl).val()).toBe(''); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/form/element/email.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/form/element/email.test.js new file mode 100644 index 0000000000000..d8c27e1a00c91 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Checkout/frontend/js/view/form/element/email.test.js @@ -0,0 +1,66 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint max-nested-callbacks: 0 */ + +define(['squire', 'ko'], function (Squire, ko) { + 'use strict'; + + describe('Magento_Checkout/js/view/form/element/email', function () { + var injector = new Squire(), + mocks = { + 'Magento_Customer/js/model/customer': { + isLoggedIn: ko.observable() + }, + 'Magento_Customer/js/action/check-email-availability': jasmine.createSpy(), + 'Magento_Customer/js/action/login': jasmine.createSpy(), + 'Magento_Checkout/js/model/quote': { + isVirtual: jasmine.createSpy(), + shippingAddress: jasmine.createSpy() + }, + 'Magento_Checkout/js/checkout-data': jasmine.createSpyObj( + 'checkoutData', + [ + 'setInputFieldEmailValue', + 'setValidatedEmailValue', + 'setCheckedEmailValue', + 'getInputFieldEmailValue', + 'getValidatedEmailValue', + 'getCheckedEmailValue' + ] + ), + 'Magento_Checkout/js/model/full-screen-loader': jasmine.createSpy(), + 'mage/validation': jasmine.createSpy() + }, + Component; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require(['Magento_Checkout/js/view/form/element/email'], function (Constr) { + Component = new Constr({ + provider: 'provName', + name: '', + index: '', + isPasswordVisible: false, + isEmailCheckComplete: null + }); + done(); + }); + }); + + afterEach(function () { + try { + injector.clean(); + injector.remove(); + } catch (e) {} + }); + + describe('"resolveInitialPasswordVisibility" method', function () { + it('Check return type of method.', function () { + expect(typeof Component.resolveInitialPasswordVisibility()).toEqual('boolean'); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js index fc00772375e3b..45c509a901c47 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Customer/frontend/js/model/customer/address.test.js @@ -22,9 +22,12 @@ define([ it('Check on empty object.', function () { var addressData = { region: {} + }, + expected = { + customAttributes: [] }; - expect(JSON.stringify(customerAddress(addressData))).toEqual(JSON.stringify({})); + expect(JSON.stringify(customerAddress(addressData))).toEqual(JSON.stringify(expected)); }); it('Check on function call with empty address data.', function () { @@ -49,7 +52,8 @@ define([ } }), expected = { - regionId: '1' + regionId: '1', + customAttributes: [] }; expect(JSON.stringify(result)).toEqual(JSON.stringify(expected)); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Multishipping/frontend/js/multi-shipping.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Multishipping/frontend/js/multi-shipping.test.js new file mode 100644 index 0000000000000..65ee180476f3a --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Multishipping/frontend/js/multi-shipping.test.js @@ -0,0 +1,110 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable max-nested-callbacks */ +define([ + 'squire', + 'jquery', + 'ko', + 'multiShipping' +], function (Squire, $, ko, MultiShipping) { + 'use strict'; + + describe('Magento_Multishipping/js/multi-shipping', function () { + var injector = new Squire(), + Obj; + + describe('Check Cart data preparation process', function () { + var customerData = ko.observable({}), + mocks = { + 'Magento_Customer/js/customer-data': { + get: jasmine.createSpy('get', function () { + return customerData; + }).and.callThrough(), + reload: jasmine.createSpy() + } + }, + summaryCount = {}; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require(['multiShipping'], function (Instance) { + Obj = Instance; + done(); + }); + }); + + afterEach(function () { + try { + injector.clean(); + injector.remove(); + } catch (e) {} + + customerData({}); + }); + + it('Prepare Cart data with the same items qty', function () { + summaryCount['summary_count'] = 0; + customerData(summaryCount); + new Obj({}); + + expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); + expect(mocks['Magento_Customer/js/customer-data'].reload).not.toHaveBeenCalled(); + }); + + it('Prepare Cart data with different items qty', function () { + summaryCount['summary_count'] = 1; + customerData(summaryCount); + new Obj({}); + + expect(mocks['Magento_Customer/js/customer-data'].get).toHaveBeenCalledWith('cart'); + expect(mocks['Magento_Customer/js/customer-data'].reload).toHaveBeenCalledWith(['cart'], false); + }); + }); + + describe('Check Multishipping events', function () { + var addNewAddressBtn, + addressflag, + canContinueBtn, + canContinueFlag; + + beforeEach(function () { + addNewAddressBtn = $('<button type="button" data-role="add-new-address"/>'); + addressflag = $('<input type="hidden" value="0" id="add_new_address_flag"/>'); + canContinueBtn = $('<button type="submit" data-role="can-continue" data-flag="1"/>'); + canContinueFlag = $('<input type="hidden" value="0" id="can_continue_flag"/>'); + $(document.body).append(addNewAddressBtn) + .append(addressflag) + .append(canContinueBtn) + .append(canContinueFlag); + }); + + afterEach(function () { + addNewAddressBtn.remove(); + addressflag.remove(); + canContinueBtn.remove(); + canContinueFlag.remove(); + }); + + it('Check add new address event', function () { + Obj = new MultiShipping({}); + Obj.element = jasmine.createSpyObj('element', ['submit']); + addNewAddressBtn.click(); + + expect(Obj.element.submit).toHaveBeenCalled(); + expect(addressflag.val()).toBe('1'); + }); + + it('Check can continue event', function () { + Obj = new MultiShipping({}); + Obj.element = jasmine.createSpyObj('element', ['submit']); + canContinueBtn.click(); + + expect(Obj.element.submit).not.toHaveBeenCalled(); + expect(canContinueFlag.val()).toBe('1'); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Security/view/base/web/js/escaper.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Security/view/base/web/js/escaper.test.js new file mode 100644 index 0000000000000..f8fa6bc70435f --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Security/view/base/web/js/escaper.test.js @@ -0,0 +1,165 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/* eslint-disable max-nested-callbacks */ +define([ + 'escaper' +], function (escaper) { + 'use strict'; + + /** + * Dataprovider for escapeHtml tests + * + * @return {Object} + */ + function escapeHtmlDataProvider() { + return { + 'empty input': { + data: '', + expected: '' + }, + 'null input': { + data: null, + expected: '' + }, + 'empty with allowed tags': { + data: '', + expected: '', + allowedTags: ['div'] + }, + 'null allowed tags': { + data: null, + expected: '', + allowedTags: ['div'] + }, + 'malicious code not executed during processing of allowedTags': { + data: '<div>foo<img src="bad" onerror="document.body.innerHTML = """></div>', + expected: '<div>foo</div>', + allowedTags: ['div'] + }, + 'text with special characters': { + data: '&<>"\'&<>"' ', + expected: '&<>"\'&amp;&lt;&gt;&quot;&#039;&#9;' + }, + 'text with special characters and allowed tag': { + data: '&<br/>"\'&<>"' ', + expected: '&<br>"\'&<>"\'\t', + allowedTags: ['br'] + }, + 'text with multiple allowed tags, includes self closing tag': { + data: '<span>some text in tags<br /></span>', + expected: '<span>some text in tags<br></span>', + allowedTags: ['span', 'br'] + }, + 'text with multiple allowed tags and allowed attribute in double quotes': { + data: 'Only <span id="sku_max_allowed"><b>2</b></span> in stock', + expected: 'Only <span id="sku_max_allowed"><b>2</b></span> in stock', + allowedTags: ['span', 'b'] + }, + 'text with multiple allowed tags and allowed attribute in single quotes': { + data: 'Only <span id=\'sku_max_allowed\'><b>2</b></span> in stock', + expected: 'Only <span id="sku_max_allowed"><b>2</b></span> in stock', + allowedTags: ['span', 'b'] + }, + 'text with multiple allowed tags with allowed attribute': { + data: 'Only registered users can write reviews. Please <a href="%1">Sign in</a> or <a href="%2">' + + 'create an account</a>', + expected: 'Only registered users can write reviews. Please <a href="%1">Sign in</a> or ' + + '<a href="%2">create an account</a>', + allowedTags: ['a'] + }, + 'text with not allowed attribute in single quotes': { + data: 'Only <span type=\'1\'><b>2</b></span> in stock', + expected: 'Only <span><b>2</b></span> in stock', + allowedTags: ['span', 'b'] + }, + 'text with allowed and not allowed tags': { + data: 'Only registered users can write reviews. Please <a href="%1">Sign in<span>three</span></a> ' + + 'or <a href="%2"><span id="action">create an account</span></a>', + expected: 'Only registered users can write reviews. Please <a href="%1">Sign inthree</a> or ' + + '<a href="%2">create an account</a>', + allowedTags: ['a'] + }, + 'text with allowed and not allowed tags, with allowed and not allowed attributes': { + data: 'Some test <span style="fine">text in span tag</span> <strong>text in strong tag</strong> ' + + '<a type="some-type" href="http://domain.com/" style="bad" onclick="alert(1)">' + + 'Click here</a><script>alert(1)' + + '</script>', + expected: 'Some test <span style="fine">text in span tag</span> text in strong tag ' + + '<a href="http://domain.com/">' + + 'Click here</a>alert(1)', + allowedTags: ['a', 'span'] + }, + 'text with html comment': { + data: 'Only <span><b>2</b></span> in stock <!-- HTML COMMENT -->', + expected: 'Only <span><b>2</b></span> in stock ', + allowedTags: ['span', 'b'] + }, + 'text with multiple comments': { + data: 'Only <span><b>2</b></span> <!-- HTML COMMENT -->in stock <!-- HTML COMMENT -->', + expected: 'Only <span><b>2</b></span> in stock ', + allowedTags: ['span', 'b'] + }, + 'text with multi-line html comment': { + data: 'Only <span><b>2</b></span> in stock <!-- --!\n\n><img src=#>-->', + expected: 'Only <span><b>2</b></span> in stock ', + allowedTags: ['span', 'b'] + }, + 'text with non ascii characters': { + data: 'абвгдمثال幸福', + expected: 'абвгдمثال幸福', + allowedTags: [] + }, + 'html and body tags': { + data: '<html><body><span>String</span></body></html>', + expected: '<span>String</span>', + allowedTags: ['span'] + }, + 'invalid tag': { + data: '<some tag> some text', + expected: ' some text', + allowedTags: ['span'] + }, + 'text with allowed script tag': { + data: '<span><script>some text in tags</script></span>', + expected: '<span>some text in tags</span>', + allowedTags: ['span', 'script'] + }, + 'text with invalid html': { + data: '<spa>n id="id1">Some string</span>', + expected: 'n id="id1">Some string', + allowedTags: ['span'] + } + }; + } + + describe('Magento_Security/js/escaper', function () { + describe('escapeHtml', function () { + var data = escapeHtmlDataProvider(), + scenarioName, + testData; + + for (scenarioName in data) { // eslint-disable-line guard-for-in + testData = data[scenarioName]; + + (function (dataSet) { // eslint-disable-line no-loop-func + /* + * Change to "it" instead of "xit" to run the tests. + * These are skipped due to PhantomJS not supporting DOMParser. The limitations of this ancient + * testing framework were not considered a limitation for a cross-browser solution of the escaper + * and the compromise was these tests won't be run automatically. Jasmine will generate + * _SpecRunner.html which can be loaded in a real browser to execute the tests when they need to be + * run. Regarding risk, this escaper doesn't have any external dependencies so it's very unlikely + * it will break unless modified directly. And in the event it's modified, run the tests in real + * supported browsers before merging. + */ + xit(scenarioName, function () { + expect(escaper.escapeHtml(dataSet.data, dataSet.allowedTags)).toEqual(dataSet.expected); + }); + })(testData); + } + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/block-loader.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/block-loader.test.js new file mode 100644 index 0000000000000..03ac8322e8560 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/block-loader.test.js @@ -0,0 +1,85 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'squire', + 'jquery' +], function (Squire, $) { + 'use strict'; + + var blockLoaderTmpl = '<div data-role="loader" class="loading-mask" style="position: absolute;">\n' + + ' <div class="loader">\n' + + ' <img src="<%= loaderImageHref %>" alt="Loading..." title="Loading..." style="position: absolute;">\n' + + ' </div>\n' + + '</div>', + injector = new Squire(), + mocks = { + 'Magento_Ui/js/lib/knockout/template/loader': { + /** Method stub. */ + loadTemplate: function () { + var defer = $.Deferred(); + + defer.resolve(blockLoaderTmpl); + + return defer; + } + } + }, + obj, + ko; + + beforeEach(function (done) { + injector.mock(mocks); + injector.require(['Magento_Ui/js/block-loader', 'ko'], function (blockLoader, knockout) { + obj = blockLoader; + ko = knockout; + done(); + }); + }); + + afterEach(function () { + try { + injector.clean(); + injector.remove(); + } catch (e) { + } + }); + + describe('Magento_Ui/js/block-loader', function () { + var blockContentLoadingClass = '_block-content-loading', + loaderImageUrl = 'https://static.magento.com/loader.gif', + element = $('<span data-bind="blockLoader: isLoading"/>'), + isLoading, + context; + + beforeEach(function () { + isLoading = ko.observable(); + context = ko.bindingContext.prototype.createChildContext({ + isLoading: isLoading + }); + obj(loaderImageUrl); + $('body').append(element); + ko.applyBindings(context, element[0]); + }); + + afterEach(function () { + ko.cleanNode(element[0]); + element.remove(); + }); + + it('Check adding loader block to element', function () { + isLoading(true); + expect(element.hasClass(blockContentLoadingClass)).toBeTruthy(); + expect(element.children().attr('class')).toEqual('loading-mask'); + expect(element.find('img').attr('src')).toEqual(loaderImageUrl); + }); + + it('Check removing loader block from element', function () { + isLoading(false); + expect(element.hasClass(blockContentLoadingClass)).toBeFalsy(); + expect(element.children().length).toEqual(0); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js new file mode 100644 index 0000000000000..2c2cdab2d46da --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/masonry.test.js @@ -0,0 +1,84 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*eslint max-nested-callbacks: 0*/ +define([ + 'jquery', + 'ko', + 'Magento_Ui/js/grid/masonry' +], function ($, ko, Masonry) { + 'use strict'; + + var Component, + rows, + container = '<div data-id="masonry_grid" id="masonry_grid"><div class="masonry-image-column"></div></div>'; + + beforeEach(function () { + rows = [ + { + _rowIndex: 0, + category: {}, + 'category_id': 695, + 'category_name': 'People', + 'comp_url': 'https://stock.adobe.com/Rest/Libraries/Watermarked/Download/327515738/2', + 'content_type': 'image/jpeg', + 'country_name': 'Malaysia', + 'creation_date': '2020-03-02 10:41:51', + 'creator_id': 208217780, + 'creator_name': 'NajmiArif', + height: 3264, + id: 327515738, + 'id_field_name': 'id', + 'is_downloaded': 0, + 'is_licensed_locally': 0, + keywords: [], + 'media_type_id': 1, + overlay: '', + path: '', + 'premium_level_id': 0, + 'thumbnail_240_url': 'https://t4.ftcdn.net/jpg/03/27/51/57/240_F_327515738_n.jpg', + 'thumbnail_500_ur': 'https://as2.ftcdn.net/jpg/03/27/51/57/500_F_327515738_n.jpg', + title: 'Neon effect picture of man wearing medical mask for viral or pandemic disease', + width: 4896 + } + + ]; + + $(container).appendTo('body'); + + Component = new Masonry({ + defaults: { + rows: ko.observable() + } + }); + + }); + + afterEach(function () { + $('#masonry_grid').remove(); + }); + + describe('check initComponent', function () { + it('verify setLayoutstyles called and grid iniztilized', function () { + var setlayoutStyles = spyOn(Component, 'setLayoutStyles'); + + expect(Component).toBeDefined(); + Component.containerId = 'masonry_grid'; + Component.initComponent(rows); + Component.rows().forEach(function (image) { + expect(image.styles).toBeDefined(); + expect(image.css).toBeDefined(); + }); + expect(setlayoutStyles).toHaveBeenCalled(); + }); + it('verify events triggered', function () { + var setLayoutStyles = spyOn(Component, 'setLayoutStyles'); + + Component.initComponent(rows); + window.dispatchEvent(new Event('resize')); + expect(setLayoutStyles).toHaveBeenCalled(); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/url-filter-applier.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/url-filter-applier.test.js new file mode 100644 index 0000000000000..a3d49e382de51 --- /dev/null +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/grid/url-filter-applier.test.js @@ -0,0 +1,75 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +/*eslint max-nested-callbacks: 0*/ +define([ + 'Magento_Ui/js/grid/url-filter-applier' +], function (UrlFilterApplier) { + 'use strict'; + + describe('Magento_Ui/js/grid/url-filter-applier', function () { + var urlFilterApplierObj, + filterComponentMock = { + setData: jasmine.createSpy(), + apply: jasmine.createSpy() + }; + + beforeEach(function () { + urlFilterApplierObj = new UrlFilterApplier({}); + urlFilterApplierObj.filterComponent = jasmine.createSpy().and.returnValue(filterComponentMock); + }); + + describe('"getFilterParam" method', function () { + it('return object from url with a simple filters parameter', function () { + var urlSearch = '?filters[name]=test'; + + expect(urlFilterApplierObj.getFilterParam(urlSearch)).toEqual({ + 'name': 'test' + }); + }); + it('return object from url with multiple filters parameter', function () { + var urlSearch = '?filters[name]=test&filters[qty]=1'; + + expect(urlFilterApplierObj.getFilterParam(urlSearch)).toEqual({ + 'name': 'test', + 'qty': '1' + }); + }); + it('return object from url with multiple filters parameter and another parameter', function () { + var urlSearch = '?filters[name]=test&filters[qty]=1&anotherparam=1'; + + expect(urlFilterApplierObj.getFilterParam(urlSearch)).toEqual({ + 'name': 'test', + 'qty': '1' + }); + }); + it('return object from url with multiple filters parameter and filter value as array', function () { + var urlSearch = '?filters[name]=[27,23]&filters[qty]=1&anotherparam=1'; + + expect(urlFilterApplierObj.getFilterParam(urlSearch)).toEqual({ + 'name': ['27', '23'], + 'qty': '1' + }); + }); + it('return object from url with another parameter', function () { + var urlSearch = '?anotherparam=1'; + + expect(urlFilterApplierObj.getFilterParam(urlSearch)).toEqual({}); + }); + }); + + describe('"apply" method', function () { + it('applies url filter on filter component', function () { + urlFilterApplierObj.searchString = '?filters[name]=test&filters[qty]=1'; + urlFilterApplierObj.apply(); + expect(urlFilterApplierObj.filterComponent().setData).toHaveBeenCalledWith({ + 'name': 'test', + 'qty': '1' + }, false); + expect(urlFilterApplierObj.filterComponent().apply).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js index 38728bca39192..a8ea949b52ebc 100644 --- a/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js +++ b/dev/tests/js/jasmine/tests/app/code/Magento/Ui/base/js/lib/ko/bind/datepicker.test.js @@ -8,6 +8,7 @@ define([ 'jquery', 'moment', 'mageUtils', + 'mage/calendar', 'Magento_Ui/js/lib/knockout/bindings/datepicker' ], function (ko, $, moment, utils) { 'use strict'; @@ -18,6 +19,7 @@ define([ config; beforeEach(function () { + jasmine.clock().install(); element = $('<input />'); observable = ko.observable(); @@ -38,6 +40,7 @@ define([ }); afterEach(function () { + jasmine.clock().uninstall(); element.remove(); }); @@ -62,6 +65,8 @@ define([ expectedDate = moment(date, utils.convertToMomentFormat(inputFormat)).toDate(); observable(date); + jasmine.clock().tick(100); + expect(expectedDate.valueOf()).toEqual(element.datepicker('getDate').valueOf()); }); @@ -69,6 +74,8 @@ define([ element.datepicker('setTimezoneDate').blur().trigger('change'); observable(''); + jasmine.clock().tick(100); + expect(null).toEqual(element.datepicker('getDate')); }); }); diff --git a/dev/tests/js/jasmine/tests/lib/mage/apply.test.js b/dev/tests/js/jasmine/tests/lib/mage/apply.test.js index c2c5d65db34f4..86e0e9b78c952 100644 --- a/dev/tests/js/jasmine/tests/lib/mage/apply.test.js +++ b/dev/tests/js/jasmine/tests/lib/mage/apply.test.js @@ -64,7 +64,7 @@ define([ .toHaveBeenCalledWith(jasmine.any(Object), node); done(); - }, preset.timeout); + }, 100); }); }); }); diff --git a/dev/tests/setup-integration/etc/di/preferences/cli/ce.php b/dev/tests/setup-integration/etc/di/preferences/cli/ce.php new file mode 100644 index 0000000000000..b5605d98206f3 --- /dev/null +++ b/dev/tests/setup-integration/etc/di/preferences/cli/ce.php @@ -0,0 +1,9 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +return [ + '\Magento\Framework\Mview\TriggerCleaner' => '\Magento\TestFramework\Mview\DummyTriggerCleaner', +]; diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php index 10a839df83a5e..9507e50d71638 100644 --- a/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Deploy/CliCommand.php @@ -6,17 +6,20 @@ namespace Magento\TestFramework\Deploy; use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Shell; use Magento\Framework\Shell\CommandRenderer; use Magento\Setup\Console\Command\InstallCommand; /** * The purpose of this class is enable/disable module and upgrade commands execution. + * + * @SuppressWarnings(PHPMD.TooManyPublicMethods) */ class CliCommand { /** - * @var \Magento\Framework\Shell + * @var Shell */ private $shell; @@ -36,10 +39,7 @@ class CliCommand private $deploymentConfig; /** - * ShellCommand constructor. - * - * @param TestModuleManager $testEnv - * @param DeploymentConfig $deploymentConfig + * @param TestModuleManager $testEnv * @internal param Shell $shell */ public function __construct( @@ -53,8 +53,9 @@ public function __construct( /** * Copy Test module files and execute enable module command. * - * @param string $moduleName + * @param string $moduleName * @return string + * @throws LocalizedException */ public function introduceModule($moduleName) { @@ -65,13 +66,14 @@ public function introduceModule($moduleName) /** * Execute enable module command. * - * @param string $moduleName + * @param string $moduleName * @return string + * @throws LocalizedException */ public function enableModule($moduleName) { $initParams = $this->parametersHolder->getInitParams(); - $enableModuleCommand = 'php -f ' . BP . '/bin/magento module:enable ' . $moduleName + $enableModuleCommand = $this->getCliScriptCommand() . ' module:enable ' . $moduleName . ' -n -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; return $this->shell->execute($enableModuleCommand); } @@ -81,11 +83,12 @@ public function enableModule($moduleName) * * @param array $installParams * @return string + * @throws LocalizedException */ public function upgrade($installParams = []) { $initParams = $this->parametersHolder->getInitParams(); - $upgradeCommand = 'php -f ' . BP . '/bin/magento setup:upgrade -vvv -n --magento-init-params="' + $upgradeCommand = $this->getCliScriptCommandWithDI() . 'setup:upgrade -vvv -n --magento-init-params="' . $initParams['magento-init-params'] . '"'; $installParams = $this->toCliArguments($installParams); $upgradeCommand .= ' ' . implode(" ", array_keys($installParams)); @@ -96,13 +99,14 @@ public function upgrade($installParams = []) /** * Execute disable module command. * - * @param string $moduleName + * @param string $moduleName * @return string + * @throws LocalizedException */ public function disableModule($moduleName) { $initParams = $this->parametersHolder->getInitParams(); - $disableModuleCommand = 'php -f ' . BP . '/bin/magento module:disable '. $moduleName + $disableModuleCommand = $this->getCliScriptCommand() . ' module:disable ' . $moduleName . ' -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; return $this->shell->execute($disableModuleCommand); } @@ -111,6 +115,7 @@ public function disableModule($moduleName) * Split quote db configuration. * * @return void + * @throws LocalizedException */ public function splitQuote() { @@ -118,7 +123,7 @@ public function splitQuote() $installParams = $this->toCliArguments( $this->parametersHolder->getDbData('checkout') ); - $command = 'php -f ' . BP . '/bin/magento setup:db-schema:split-quote ' . + $command = $this->getCliScriptCommand() . ' setup:db-schema:split-quote ' . implode(" ", array_keys($installParams)) . ' -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; @@ -130,6 +135,7 @@ public function splitQuote() * Split sales db configuration. * * @return void + * @throws LocalizedException */ public function splitSales() { @@ -137,7 +143,7 @@ public function splitSales() $installParams = $this->toCliArguments( $this->parametersHolder->getDbData('sales') ); - $command = 'php -f ' . BP . '/bin/magento setup:db-schema:split-sales ' . + $command = $this->getCliScriptCommand() . ' setup:db-schema:split-sales ' . implode(" ", array_keys($installParams)) . ' -vvv --magento-init-params="' . $initParams['magento-init-params'] . '"'; @@ -151,7 +157,7 @@ public function splitSales() public function cacheClean() { $initParams = $this->parametersHolder->getInitParams(); - $command = 'php -f ' . BP . '/bin/magento cache:clean ' . + $command = $this->getCliScriptCommand() . ' cache:clean ' . ' -vvv --magento-init-params=' . $initParams['magento-init-params']; @@ -162,11 +168,12 @@ public function cacheClean() * Uninstall module * * @param string $moduleName + * @throws LocalizedException */ public function uninstallModule($moduleName) { $initParams = $this->parametersHolder->getInitParams(); - $command = 'php -f ' . BP . '/bin/magento module:uninstall ' . $moduleName . ' --remove-data ' . + $command = $this->getCliScriptCommand() . ' module:uninstall ' . $moduleName . ' --remove-data ' . ' -vvv --non-composer --magento-init-params="' . $initParams['magento-init-params'] . '"'; @@ -240,4 +247,29 @@ public function afterInstall() ->get(DeploymentConfig::class); $this->deploymentConfig->resetData(); } + + /** + * Get custom magento-cli command with additional DI configuration + * + * @return string + */ + private function getCliScriptCommandWithDI(): string + { + $params['MAGE_DIRS']['base']['path'] = BP; + $params['INTEGRATION_TESTS_CLI_AUTOLOADER'] = TESTS_BASE_DIR . '/framework/autoload.php'; + $params['TESTS_BASE_DIR'] = TESTS_BASE_DIR; + return 'INTEGRATION_TEST_PARAMS="' . urldecode(http_build_query($params)) . '"' + . ' ' . PHP_BINARY . ' -f ' . INTEGRATION_TESTS_BASE_DIR + . '/bin/magento '; + } + + /** + * Get basic magento-cli command + * + * @return string + */ + private function getCliScriptCommand() + { + return PHP_BINARY . ' -f ' . BP . '/bin/magento '; + } } diff --git a/dev/tests/setup-integration/framework/Magento/TestFramework/Mview/DummyTriggerCleaner.php b/dev/tests/setup-integration/framework/Magento/TestFramework/Mview/DummyTriggerCleaner.php new file mode 100644 index 0000000000000..1a7108150eb24 --- /dev/null +++ b/dev/tests/setup-integration/framework/Magento/TestFramework/Mview/DummyTriggerCleaner.php @@ -0,0 +1,24 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\TestFramework\Mview; + +/** + * Stub for \Magento\Framework\Mview\TriggerCleaner + */ +class DummyTriggerCleaner +{ + /** + * Remove the outdated trigger from the system + * + * @return bool + */ + public function removeTriggers(): bool + { + return true; + } +} diff --git a/dev/tests/setup-integration/framework/bootstrap.php b/dev/tests/setup-integration/framework/bootstrap.php index 01f60a3376ff8..e3eed312a21b9 100644 --- a/dev/tests/setup-integration/framework/bootstrap.php +++ b/dev/tests/setup-integration/framework/bootstrap.php @@ -3,8 +3,9 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ -use Magento\Framework\Autoload\AutoloaderRegistry; + use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Autoload\AutoloaderRegistry; require_once __DIR__ . '/../../../../app/bootstrap.php'; require_once __DIR__ . '/autoload.php'; @@ -13,7 +14,7 @@ $testsBaseDir = dirname(__DIR__); $integrationTestsDir = realpath("{$testsBaseDir}/../integration"); -$fixtureBaseDir = $integrationTestsDir. '/testsuite'; +$fixtureBaseDir = $integrationTestsDir . '/testsuite'; if (!defined('TESTS_BASE_DIR')) { define('TESTS_BASE_DIR', $testsBaseDir); @@ -30,8 +31,11 @@ if (!defined('MAGENTO_MODULES_PATH')) { define('MAGENTO_MODULES_PATH', __DIR__ . '/../../../../app/code/Magento/'); } -$settings = new \Magento\TestFramework\Bootstrap\Settings($testsBaseDir, get_defined_constants()); +if (!defined('INTEGRATION_TESTS_BASE_DIR')) { + define('INTEGRATION_TESTS_BASE_DIR', $integrationTestsDir); +} +$settings = new \Magento\TestFramework\Bootstrap\Settings($testsBaseDir, get_defined_constants()); try { setCustomErrorHandler(); @@ -96,8 +100,10 @@ ); /* Unset declared global variables to release the PHPUnit from maintaining their values between tests */ - unset($testsBaseDir, $logWriter, $settings, $shell, $application, $bootstrap); + unset($testsBaseDir, $settings, $shell, $application, $bootstrap); } catch (\Exception $e) { + // phpcs:disable Magento2.Security.LanguageConstruct echo $e . PHP_EOL; exit(1); + // phpcs:enable Magento2.Security.LanguageConstruct } diff --git a/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist b/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist index 1a93397caaa4a..d15c5f1818784 100644 --- a/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/setup-integration/framework/tests/unit/phpunit.xml.dist @@ -6,7 +6,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" @@ -15,7 +15,7 @@ <!-- Test suites definition --> <testsuites> <testsuite name="Unit Tests for Integration Tests Framework"> - <directory suffix="Test.php">testsuite</directory> + <directory>testsuite</directory> </testsuite> </testsuites> <php> diff --git a/dev/tests/setup-integration/phpunit.xml.dist b/dev/tests/setup-integration/phpunit.xml.dist index 65a3273c4fdcd..0317d0e39efb1 100644 --- a/dev/tests/setup-integration/phpunit.xml.dist +++ b/dev/tests/setup-integration/phpunit.xml.dist @@ -16,7 +16,7 @@ <!-- Test suites definition --> <testsuites> <testsuite name="Magento Setup/Upgrade Tests"> - <directory suffix="Test.php">testsuite</directory> + <directory>testsuite</directory> </testsuite> </testsuites> <!-- Code coverage filters --> diff --git a/dev/tests/static/framework/Magento/PhpStan/Reflection/Php/DataObjectClassReflectionExtension.php b/dev/tests/static/framework/Magento/PhpStan/Reflection/Php/DataObjectClassReflectionExtension.php new file mode 100644 index 0000000000000..f5a05a4f5ed2e --- /dev/null +++ b/dev/tests/static/framework/Magento/PhpStan/Reflection/Php/DataObjectClassReflectionExtension.php @@ -0,0 +1,105 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PhpStan\Reflection\Php; + +use Magento\Framework\DataObject; +use Magento\Framework\Session\SessionManager; +use PHPStan\DependencyInjection\Container; +use PHPStan\Reflection\Annotations\AnnotationsMethodsClassReflectionExtension; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\MethodsClassReflectionExtension; + +/** + * Extension to add support of magic methods (get/set/uns/has) based on @see DataObject + */ +class DataObjectClassReflectionExtension implements MethodsClassReflectionExtension +{ + private const MAGIC_METHODS_PREFIXES = [ + 'get', + 'set', + 'uns', + 'has' + ]; + + private const PREFIX_LENGTH = 3; + + /** + * @var Container + */ + private $container; + + /** + * @var AnnotationsMethodsClassReflectionExtension + */ + private $annotationsMethodsClassReflectionExtension; + + /** + * @param Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + $this->annotationsMethodsClassReflectionExtension = $this->container + ->getByType(AnnotationsMethodsClassReflectionExtension::class); + } + + /** + * Check if class has relations with DataObject and requested method can be considered as a magic method. + * + * @param ClassReflection $classReflection + * @param string $methodName + * + * @return bool + */ + public function hasMethod(ClassReflection $classReflection, string $methodName): bool + { + // Workaround due to annotation extension always loads last. + if ($this->annotationsMethodsClassReflectionExtension->hasMethod($classReflection, $methodName)) { + // In case when annotation already available for the method, we will not use 'magic methods' approach. + return false; + } + if ($classReflection->isSubclassOf(DataObject::class) || $classReflection->getName() == DataObject::class) { + return in_array($this->getPrefix($methodName), self::MAGIC_METHODS_PREFIXES); + } + /** SessionManager redirects all calls to `__call` to container which extends DataObject */ + if ($classReflection->isSubclassOf(SessionManager::class) + || $classReflection->getName() === SessionManager::class + ) { + /** @see \Magento\Framework\Session\SessionManager::__call */ + return in_array($this->getPrefix($methodName), self::MAGIC_METHODS_PREFIXES); + } + + return false; + } + + /** + * Get prefix from method name. + * + * @param string $methodName + * + * @return string + */ + private function getPrefix(string $methodName): string + { + return (string)substr($methodName, 0, self::PREFIX_LENGTH); + } + + /** + * Get method reflection instance. + * + * @param ClassReflection $classReflection + * @param string $methodName + * + * @return MethodReflection + */ + public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection + { + return new DataObjectMethodReflection($classReflection, $methodName); + } +} diff --git a/dev/tests/static/framework/Magento/PhpStan/Reflection/Php/DataObjectMethodReflection.php b/dev/tests/static/framework/Magento/PhpStan/Reflection/Php/DataObjectMethodReflection.php new file mode 100644 index 0000000000000..f4f6c2c1bed44 --- /dev/null +++ b/dev/tests/static/framework/Magento/PhpStan/Reflection/Php/DataObjectMethodReflection.php @@ -0,0 +1,269 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PhpStan\Reflection\Php; + +use PHPStan\Reflection\ClassMemberReflection; +use PHPStan\Reflection\ClassReflection; +use PHPStan\Reflection\FunctionVariant; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ParameterReflection; +use PHPStan\Reflection\ParametersAcceptor; +use PHPStan\Reflection\Php\DummyParameter; +use PHPStan\TrinaryLogic; +use PHPStan\Type\BooleanType; +use PHPStan\Type\Generic\TemplateTypeMap; +use PHPStan\Type\IntegerType; +use PHPStan\Type\MixedType; +use PHPStan\Type\ObjectType; +use PHPStan\Type\StringType; +use PHPStan\Type\Type; +use PHPStan\Type\UnionType; +use PHPStan\Type\VoidType; + +/** + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class DataObjectMethodReflection implements MethodReflection +{ + private const PREFIX_LENGTH = 3; + + /** + * @var ClassReflection + */ + private $classReflection; + + /** + * @var string + */ + private $methodName; + + /** + * @param ClassReflection $classReflection + * @param string $methodName + */ + public function __construct(ClassReflection $classReflection, string $methodName) + { + $this->classReflection = $classReflection; + $this->methodName = $methodName; + } + + /** + * Get methods class reflection. + * + * @return ClassReflection + */ + public function getDeclaringClass(): ClassReflection + { + return $this->classReflection; + } + + /** + * Get if method is static. + * + * @return bool + */ + public function isStatic(): bool + { + return false; + } + + /** + * Get if method is private. + * + * @return bool + */ + public function isPrivate(): bool + { + return false; + } + + /** + * Get if method is public. + * + * @return bool + */ + public function isPublic(): bool + { + return true; + } + + /** + * Get Method PHP Doc comment message. + * + * @return string|null + */ + public function getDocComment(): ?string + { + return null; + } + + /** + * Get Method Name. + * + * @return string + */ + public function getName(): string + { + return $this->methodName; + } + + /** + * Get Prototype. + * + * @return ClassMemberReflection + */ + public function getPrototype(): ClassMemberReflection + { + return $this; + } + + /** + * Get Magic Methods variant based on type (get/set/has/uns). + * + * @return ParametersAcceptor[] + */ + public function getVariants(): array + { + return [ + new FunctionVariant( + TemplateTypeMap::createEmpty(), + TemplateTypeMap::createEmpty(), + $this->getMethodParameters(), + false, + $this->getReturnType() + ) + ]; + } + + /** + * Get prefix from method name. + * + * @return string + */ + private function getMethodNamePrefix(): string + { + return (string)substr($this->methodName, 0, self::PREFIX_LENGTH); + } + + /** + * Get Magic Methods parameters. + * + * @return ParameterReflection[] + */ + private function getMethodParameters(): array + { + $params = []; + switch ($this->getMethodNamePrefix()) { + case 'set': + $params[] = new DummyParameter( + 'value', + new MixedType(), + false, + null, + false, + null + ); + break; + case 'get': + $params[] = new DummyParameter( + 'index', + new UnionType([new StringType(), new IntegerType()]), + true, + null, + false, + null + ); + break; + } + + return $params; + } + + /** + * Get Magic Methods return type. + * + * @return Type + */ + private function getReturnType(): Type + { + switch ($this->getMethodNamePrefix()) { + case 'set': + case 'uns': + $returnType = new ObjectType($this->classReflection->getName()); + break; + case 'has': + $returnType = new BooleanType(); + break; + default: + $returnType = new MixedType(); + break; + } + + return $returnType; + } + + /** + * Get if method is deprecated. + * + * @return TrinaryLogic + */ + public function isDeprecated(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + /** + * Get deprecated description. + * + * @return string|null + */ + public function getDeprecatedDescription(): ?string + { + return null; + } + + /** + * Get if method is final. + * + * @return TrinaryLogic + */ + public function isFinal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + /** + * Get if method is internal. + * + * @return TrinaryLogic + */ + public function isInternal(): TrinaryLogic + { + return TrinaryLogic::createNo(); + } + + /** + * Get method throw type. + * + * @return Type|null + */ + public function getThrowType(): ?Type + { + return new VoidType(); + } + + /** + * Get if method has side effect. + * + * @return TrinaryLogic + */ + public function hasSideEffects(): TrinaryLogic + { + return TrinaryLogic::createYes(); + } +} diff --git a/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/PhpStan.php b/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/PhpStan.php index 9048262722d48..43b3166e4b187 100644 --- a/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/PhpStan.php +++ b/dev/tests/static/framework/Magento/TestFramework/CodingStandard/Tool/PhpStan.php @@ -19,7 +19,7 @@ class PhpStan implements ToolInterface * * @see https://github.com/phpstan/phpstan#rule-levels */ - private const RULE_LEVEL = 0; + private const RULE_LEVEL = 1; /** * Memory limit required by PHPStan for full Magento project scan. diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/DbRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/DbRule.php index cb3a9e2cc9c35..70b5b00cbdfc7 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/DbRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/DbRule.php @@ -7,6 +7,9 @@ */ namespace Magento\TestFramework\Dependency; +/** + * Class to get DB dependencies information + */ class DbRule implements \Magento\TestFramework\Dependency\RuleInterface { /** @@ -37,7 +40,7 @@ public function __construct(array $tables) */ public function getDependencyInfo($currentModule, $fileType, $file, &$contents) { - if ('php' != $fileType || !preg_match('#.*/(Setup|Resource)/.*\.php$#', $file)) { + if ('php' !== $fileType || !preg_match('#.*/(Setup|Resource|Query)/.*\.php$#', $file)) { return []; } diff --git a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php index 2076daf811fb1..6fdeeb816f2cf 100644 --- a/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php +++ b/dev/tests/static/framework/Magento/TestFramework/Dependency/PhpRule.php @@ -167,8 +167,9 @@ private function caseClassesAndIdentifiers($currentModule, $file, &$contents) '[_\\\\]|', Files::init()->getNamespaces() ) - . '[_\\\\])[a-zA-Z0-9]+)' - . '(?<class_inside_module>[a-zA-Z0-9_\\\\]*))\b(?:::(?<module_scoped_key>[a-z0-9_]+)[\'"])?~'; + . '(?<delimiter>[_\\\\]))[a-zA-Z0-9]{2,})' + . '(?<class_inside_module>\\4[a-zA-Z0-9_\\\\]{2,})?)\b' + . '(?:::(?<module_scoped_key>[A-Za-z0-9_/.]+)[\'"])?~'; if (!preg_match_all($pattern, $contents, $matches)) { return []; diff --git a/dev/tests/static/framework/tests/unit/phpunit.xml.dist b/dev/tests/static/framework/tests/unit/phpunit.xml.dist index aca48f6f8d1d0..f1cd910f3b02b 100644 --- a/dev/tests/static/framework/tests/unit/phpunit.xml.dist +++ b/dev/tests/static/framework/tests/unit/phpunit.xml.dist @@ -6,7 +6,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" beStrictAboutTestsThatDoNotTestAnything="false" @@ -14,7 +14,7 @@ > <testsuites> <testsuite name="Magento Unit Tests for Static Code Analysis Framework"> - <directory suffix="Test.php">testsuite/Magento/TestFramework</directory> + <directory>testsuite/Magento/TestFramework</directory> </testsuite> </testsuites> <php> diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Formatters/FilteredErrorFormatterTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Formatters/FilteredErrorFormatterTest.php index 8512f311f15a2..0c918779fd7ba 100644 --- a/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Formatters/FilteredErrorFormatterTest.php +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Formatters/FilteredErrorFormatterTest.php @@ -38,7 +38,7 @@ public function testFormatErrors( string $expected ): void { $formatter = new FilteredErrorFormatter( - new FuzzyRelativePathHelper(self::DIRECTORY_PATH, '/', []), + new FuzzyRelativePathHelper(self::DIRECTORY_PATH, [], '/'), false, false, false, @@ -48,6 +48,7 @@ public function testFormatErrors( $analysisResult = new AnalysisResult( $fileErrors, [], + [], false, false, null diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithCorrectUsageOfDataObject.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithCorrectUsageOfDataObject.php new file mode 100644 index 0000000000000..72270fdf6c829 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithCorrectUsageOfDataObject.php @@ -0,0 +1,44 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PhpStan\Reflection\DataObject\Fixtures; + +use Magento\Framework\DataObject; + +class ClassWithCorrectUsageOfDataObject +{ + /** + * @var DataObject + */ + private $container; + + /** + * ClassWithCorrectUsageOfDataObject constructor. + */ + public function __construct() + { + $this->container = new DataObject(); + } + + /** + * Process with amazing stuff. + * + * @return void + */ + public function doStuff(): void + { + $a = $this->container->getSomething('1'); + + if ($this->container->hasSomething()) { + $this->container->setSomething2($a); + } else { + $this->container->unsetSomething(); + $this->container->setSometing2($this->container->getStuff()); + } + } +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithIncorrectUsageOfDataObject.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithIncorrectUsageOfDataObject.php new file mode 100644 index 0000000000000..c6539875daef7 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithIncorrectUsageOfDataObject.php @@ -0,0 +1,49 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PhpStan\Reflection\DataObject\Fixtures; + +use Magento\Framework\DataObject; + +class ClassWithIncorrectUsageOfDataObject +{ + /** + * @var DataObject + */ + private $container; + + /** + * ClassWithIncorrectUsageOfDataObject constructor. + */ + public function __construct() + { + $this->container = new DataObject(); + } + + /** + * Do Magic Stuff. + * + * 'get' - args: $index[optional] - string|int, return: mixed; + * 'set' - args: $value - mixed, return: self; + * 'uns' - args: -, return: self; + * 'has' - args: -, return: bool; + */ + public function doStuff(): void + { + $this->container->getBaz( + $this->container->unsFoo( + $this->container->setBaz() + ) + ); + $this->container->hasFoo( + $this->container->setStuff() + ); + + $this->container->getSomething($this->container->hasFoo()); + } +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithSessionManagerUsage.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithSessionManagerUsage.php new file mode 100644 index 0000000000000..508dbee8f4a40 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/ClassWithSessionManagerUsage.php @@ -0,0 +1,69 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\PhpStan\Reflection\DataObject\Fixtures; + +use Magento\Framework\Session\SessionManager; + +/** + * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + */ +class ClassWithSessionManagerUsage +{ + /** + * @var SessionManager + */ + private $container; + + /** + * @param SessionManager $sessionManager + */ + public function __construct(SessionManager $sessionManager) + { + $this->container = $sessionManager; + } + + /** + * Do Magic Stuff. + * + * 'get' - args: $index[optional] - string|int, return: mixed; + * 'set' - args: $value - mixed, return: self; + * 'uns' - args: -, return: self; + * 'has' - args: -, return: bool; + */ + public function doStuff(): void + { + $this->container->getBaz( + $this->container->unsFoo( + $this->container->setBaz() + ) + ); + $this->container->hasFoo( + $this->container->setStuff() + ); + + $this->container->getSomething($this->container->hasFoo()); + } + + /** + * Correct usage. + * + * @return void + */ + public function doCorrectStuff(): void + { + $a = $this->container->getSomething('1'); + + if ($this->container->hasSomething()) { + $this->container->setSomething2($a); + } else { + $this->container->unsetSomething(); + $this->container->setSometing2($this->container->getStuff()); + } + } +} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/config.neon b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/config.neon new file mode 100644 index 0000000000000..dcbe472527097 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/Fixtures/config.neon @@ -0,0 +1,4 @@ +services: + - + class: Magento\PhpStan\Reflection\Php\DataObjectClassReflectionExtension + tags: {phpstan.broker.methodsClassReflectionExtension: {priority: 100}} diff --git a/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/MagicMethodsTest.php b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/MagicMethodsTest.php new file mode 100644 index 0000000000000..fd82995d0e9d4 --- /dev/null +++ b/dev/tests/static/framework/tests/unit/testsuite/Magento/PhpStan/Reflection/DataObject/MagicMethodsTest.php @@ -0,0 +1,109 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\PhpStan\Reflection\DataObject; + +use PHPStan\Rules\Methods\CallMethodsRule; +use PHPStan\Rules\Rule; +use PHPStan\Testing\RuleTestCase; + +class MagicMethodsTest extends RuleTestCase +{ + /** + * @inheritdoc + */ + protected function getRule(): Rule + { + return $this->getContainer()->getByType(CallMethodsRule::class); + } + + /** + * Return directory path of fixture directory. + * + * @return string + */ + private static function getFixturesDir(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . 'Fixtures' . DIRECTORY_SEPARATOR; + } + + /** + * Add config files. + * + * @return string[] + */ + public static function getAdditionalConfigFiles(): array + { + return [self::getFixturesDir() . 'config.neon']; + } + + /** + * Test Data Object Reflection extension. + */ + public function testExtension() + { + $this->analyse( + [ + self::getFixturesDir() . 'ClassWithCorrectUsageOfDataObject.php', + self::getFixturesDir() . 'ClassWithIncorrectUsageOfDataObject.php', + self::getFixturesDir() . 'ClassWithSessionManagerUsage.php' + ], + [ + // phpcs:disable Generic.Files.LineLength.TooLong + [ + 'Parameter #1 $index of method Magento\Framework\DataObject::getBaz() expects int|string, Magento\Framework\DataObject given.', + 38 + ], + [ + 'Method Magento\Framework\DataObject::unsFoo() invoked with 1 parameter, 0 required.', + 39 + ], + [ + 'Method Magento\Framework\DataObject::setBaz() invoked with 0 parameters, 1 required.', + 40 + ], + [ + 'Method Magento\Framework\DataObject::hasFoo() invoked with 1 parameter, 0 required.', + 43 + ], + [ + 'Method Magento\Framework\DataObject::setStuff() invoked with 0 parameters, 1 required.', + 44 + ], + [ + 'Parameter #1 $index of method Magento\Framework\DataObject::getSomething() expects int|string, bool given.', + 47 + ], + [ + 'Parameter #1 $index of method Magento\Framework\Session\SessionManager::getBaz() expects int|string, Magento\Framework\Session\SessionManager given.', + 41 + ], + [ + 'Method Magento\Framework\Session\SessionManager::unsFoo() invoked with 1 parameter, 0 required.', + 42 + ], + [ + 'Method Magento\Framework\Session\SessionManager::setBaz() invoked with 0 parameters, 1 required.', + 43 + ], + [ + 'Method Magento\Framework\Session\SessionManager::hasFoo() invoked with 1 parameter, 0 required.', + 46 + ], + [ + 'Method Magento\Framework\Session\SessionManager::setStuff() invoked with 0 parameters, 1 required.', + 47 + ], + [ + 'Parameter #1 $index of method Magento\Framework\Session\SessionManager::getSomething() expects int|string, bool given.', + 50 + ] + // phpcs:enable Generic.Files.LineLength.TooLong + ] + ); + } +} diff --git a/dev/tests/static/phpunit-all.xml.dist b/dev/tests/static/phpunit-all.xml.dist index 7f067d9290f3a..1d1464161da58 100644 --- a/dev/tests/static/phpunit-all.xml.dist +++ b/dev/tests/static/phpunit-all.xml.dist @@ -8,7 +8,7 @@ */ --> <phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd" + xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/9.1/phpunit.xsd" colors="true" columns="max" bootstrap="./framework/bootstrap.php" diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php index 7f7d9be162dec..d82c5e068f880 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/ClassesTest.php @@ -31,12 +31,12 @@ class ClassesTest extends \PHPUnit\Framework\TestCase /** * @var array */ - private static $keywordsBlacklist = ["String", "Array", "Boolean", "Element"]; + private static $excludeKeywords = ["String", "Array", "Boolean", "Element"]; /** * @var array|null */ - private $referenceBlackList = null; + private $excludeReference = null; /** * Set Up @@ -307,7 +307,7 @@ private function assertClassNamespace(string $file, string $relativePath, string public function testClassReferences() { $this->markTestSkipped("To be fixed in MC-33329. The test is not working properly " - . "after blacklisting logic was fixed. Previously it was ignoring all files."); + . "after excluded logic was fixed. Previously it was ignoring all files."); $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); $invoker( /** @@ -373,7 +373,7 @@ function ($file) { ); $vendorClasses = array_filter($vendorClasses, 'strlen'); - $vendorClasses = $this->referenceBlacklistFilter($vendorClasses); + $vendorClasses = $this->excludedReferenceFilter($vendorClasses); if (!empty($vendorClasses)) { $this->assertClassesExist($vendorClasses, $file); } @@ -392,7 +392,7 @@ function ($file) { $badClasses = $this->handleAliasClasses($aliasClasses, $badClasses); } - $badClasses = $this->referenceBlacklistFilter($badClasses); + $badClasses = $this->excludedReferenceFilter($badClasses); $badClasses = $this->removeSpecialCases($badClasses, $file, $contents, $namespacePath); $this->assertClassReferences($badClasses, $file); }, @@ -426,12 +426,12 @@ private function handleAliasClasses(array $aliasClasses, array $badClasses): arr * @param array $classes * @return array */ - private function referenceBlacklistFilter(array $classes): array + private function excludedReferenceFilter(array $classes): array { - // exceptions made for the files from the blacklist - $blacklistClasses = $this->getReferenceBlacklist(); + // exceptions made for the files from the exclusion + $excludeClasses = $this->getExcludedReferences(); foreach ($classes as $class) { - if (in_array($class, $blacklistClasses)) { + if (in_array($class, $excludeClasses)) { unset($classes[array_search($class, $classes)]); } } @@ -444,16 +444,16 @@ private function referenceBlacklistFilter(array $classes): array * * @return array */ - private function getReferenceBlacklist(): array + private function getExcludedReferences(): array { - if (!isset($this->referenceBlackList)) { - $this->referenceBlackList = file( + if (!isset($this->excludeReference)) { + $this->excludeReference = file( __DIR__ . '/_files/blacklist/reference.txt', FILE_IGNORE_NEW_LINES ); } - return $this->referenceBlackList; + return $this->excludeReference; } /** @@ -479,7 +479,7 @@ private function removeSpecialCases(array $badClasses, string $file, string $con } // Remove usage of key words such as "Array", "String", and "Boolean" - if (in_array($badClass, self::$keywordsBlacklist)) { + if (in_array($badClass, self::$excludeKeywords)) { unset($badClasses[array_search($badClass, $badClasses)]); continue; } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php index a5d9f9be41d23..2f9d7f6fdac82 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DeclarativeDependencyTest.php @@ -8,14 +8,20 @@ namespace Magento\Test\Integrity; -use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Utility\AggregateInvoker; use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Exception\LocalizedException; +use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; +use Magento\TestFramework\Inspection\Exception as InspectionException; +use PHPUnit\Framework\TestCase; /** * Class DeclarativeDependencyTest + * Test for undeclared dependencies in declarative schema */ -class DeclarativeDependencyTest extends \PHPUnit\Framework\TestCase +class DeclarativeDependencyTest extends TestCase { /** * @var DeclarativeSchemaDependencyProvider @@ -25,7 +31,7 @@ class DeclarativeDependencyTest extends \PHPUnit\Framework\TestCase /** * Sets up data * - * @throws \Exception + * @throws InspectionException */ protected function setUp(): void { @@ -37,15 +43,16 @@ protected function setUp(): void 'MAGETWO-43654: The build is running from vendor/magento. DependencyTest is skipped.' ); } - $this->dependencyProvider = new DeclarativeSchemaDependencyProvider(); + $objectManager = ObjectManager::getInstance(); + $this->dependencyProvider = $objectManager->create(DeclarativeSchemaDependencyProvider::class); } /** - * @throws \Exception + * @throws LocalizedException */ public function testUndeclaredDependencies() { - $invoker = new \Magento\Framework\App\Utility\AggregateInvoker($this); + $invoker = new AggregateInvoker($this); $invoker( /** * Check undeclared modules dependencies for specified file @@ -107,7 +114,7 @@ private function prepareFiles(array $files): array */ private function getErrorMessage(string $id): string { - $decodedId = $this->dependencyProvider->decodeDependencyId($id); + $decodedId = DeclarativeSchemaDependencyProvider::decodeDependencyId($id); $entityType = $decodedId['entityType']; if ($entityType === DeclarativeSchemaDependencyProvider::SCHEMA_ENTITY_TABLE) { $message = sprintf( @@ -131,14 +138,13 @@ private function getErrorMessage(string $id): string * * @param string $file * @return mixed - * @throws \Exception + * @throws InspectionException */ private function readJsonFile(string $file, bool $asArray = false) { $decodedJson = json_decode(file_get_contents($file), $asArray); if (null == $decodedJson) { - //phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception("Invalid Json: $file"); + throw new InspectionException("Invalid Json: $file"); } return $decodedJson; diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php index 965bc6184144b..e1d35431c5e1d 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DeclarativeSchemaDependencyProvider.php @@ -11,6 +11,8 @@ use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Setup\Declaration\Schema\Config\Converter; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Inspection\Exception as InspectionException; /** * Provide information on the dependency between the modules according to the declarative schema. @@ -19,60 +21,48 @@ */ class DeclarativeSchemaDependencyProvider { - /** - * Types of dependency between modules. - */ - const TYPE_HARD = 'hard'; - - /** - * The identifier of dependency for mapping. - */ - const MAP_TYPE_DECLARED = 'declared'; - - /** - * The identifier of dependency for mapping. - */ - const MAP_TYPE_FOUND = 'found'; - /** * Declarative name for table entity of the declarative schema. */ - const SCHEMA_ENTITY_TABLE = 'table'; + public const SCHEMA_ENTITY_TABLE = 'table'; /** * Declarative name for column entity of the declarative schema. */ - const SCHEMA_ENTITY_COLUMN = 'column'; + public const SCHEMA_ENTITY_COLUMN = 'column'; /** * Declarative name for constraint entity of the declarative schema. */ - const SCHEMA_ENTITY_CONSTRAINT = 'constraint'; + public const SCHEMA_ENTITY_CONSTRAINT = 'constraint'; /** * Declarative name for index entity of the declarative schema. */ - const SCHEMA_ENTITY_INDEX = 'index'; + public const SCHEMA_ENTITY_INDEX = 'index'; /** * @var array */ - private $mapDependencies = []; + private $dbSchemaDeclaration = []; /** * @var array */ - private $dbSchemaDeclaration = []; + private $moduleSchemaFileMapping = []; /** - * @var array + * @var DependencyProvider */ - private $packageModuleMapping = []; + private $dependencyProvider; /** - * @var array + * @param DependencyProvider $dependencyProvider */ - private $moduleSchemaFileMapping = []; + public function __construct(DependencyProvider $dependencyProvider) + { + $this->dependencyProvider = $dependencyProvider; + } /** * Provide declared dependencies between modules based on the declarative schema configuration. @@ -83,15 +73,19 @@ class DeclarativeSchemaDependencyProvider */ public function getDeclaredExistingModuleDependencies(string $moduleName): array { - $this->initDeclaredDependencies(); $dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName)); $dependencies = $this->filterSelfDependency($moduleName, $dependencies); - $declared = $this->getDeclaredDependencies($moduleName, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + $declared = $this->dependencyProvider->getDeclaredDependencies( + $moduleName, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_DECLARED + ); $existingDeclared = []; foreach ($dependencies as $dependency) { $checkResult = array_intersect($declared, $dependency); if ($checkResult) { + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $existingDeclared = array_merge($existingDeclared, array_values($checkResult)); } } @@ -113,7 +107,6 @@ public function getDeclaredExistingModuleDependencies(string $moduleName): array */ public function getUndeclaredModuleDependencies(string $moduleName): array { - $this->initDeclaredDependencies(); $dependencies = $this->getDependenciesFromFiles($this->getSchemaFileNameByModuleName($moduleName)); $dependencies = $this->filterSelfDependency($moduleName, $dependencies); return $this->collectDependencies($moduleName, $dependencies); @@ -124,7 +117,7 @@ public function getUndeclaredModuleDependencies(string $moduleName): array * * @param string $module * @return string - * @throws \Exception + * @throws LocalizedException */ private function getSchemaFileNameByModuleName(string $module): string { @@ -145,42 +138,6 @@ private function getSchemaFileNameByModuleName(string $module): string return $this->moduleSchemaFileMapping[$module] ?? ''; } - /** - * Initialise map of dependencies. - * - * @throws \Exception - */ - private function initDeclaredDependencies() - { - if (empty($this->mapDependencies)) { - $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); - foreach ($jsonFiles as $file) { - $json = new \Magento\Framework\Config\Composer\Package($this->readJsonFile($file)); - $moduleName = $this->convertModuleName($json->get('name')); - $require = array_keys((array)$json->get('require')); - $this->presetDependencies($moduleName, $require, self::TYPE_HARD); - } - } - } - - /** - * Read data from json file. - * - * @param string $file - * @return mixed - * @throws \Exception - */ - private function readJsonFile(string $file, bool $asArray = false) - { - $decodedJson = json_decode(file_get_contents($file), $asArray); - if (null == $decodedJson) { - //phpcs:ignore Magento2.Exceptions.DirectThrow - throw new \Exception("Invalid Json: $file"); - } - - return $decodedJson; - } - /** * Remove self dependencies. * @@ -188,10 +145,10 @@ private function readJsonFile(string $file, bool $asArray = false) * @param array $dependencies * @return array */ - private function filterSelfDependency(string $moduleName, array $dependencies):array + private function filterSelfDependency(string $moduleName, array $dependencies): array { foreach ($dependencies as $id => $modules) { - $decodedId = $this->decodeDependencyId($id); + $decodedId = self::decodeDependencyId($id); $entityType = $decodedId['entityType']; if ($entityType === self::SCHEMA_ENTITY_TABLE || $entityType === "column") { if (array_search($moduleName, $modules) !== false) { @@ -222,6 +179,7 @@ private function filterComplexDependency(string $moduleName, array $modules): ar } else { foreach ($modules as $dependencySet) { if (array_search($moduleName, $dependencySet) === false) { + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $resultDependencies = array_merge( $resultDependencies, $dependencySet @@ -237,7 +195,7 @@ private function filterComplexDependency(string $moduleName, array $modules): ar * Retrieve declarative schema declaration. * * @return array - * @throws \Exception + * @throws LocalizedException */ private function getDeclarativeSchema(): array { @@ -260,10 +218,9 @@ private function getDeclarativeSchema(): array array_push($tableDeclaration['modules'], $moduleName); $moduleDeclaration = array_replace_recursive( $moduleDeclaration, - [self::SCHEMA_ENTITY_TABLE => - [ - $tableName => $tableDeclaration, - ] + [self::SCHEMA_ENTITY_TABLE => [ + $tableName => $tableDeclaration, + ] ] ); foreach ($entityTypes as $entityType) { @@ -272,11 +229,9 @@ private function getDeclarativeSchema(): array } $moduleDeclaration = array_replace_recursive( $moduleDeclaration, - [self::SCHEMA_ENTITY_TABLE => - [ - $tableName => - $this->addModuleAssigment($tableDeclaration, $entityType, $moduleName) - ] + [self::SCHEMA_ENTITY_TABLE => [ + $tableName => $this->addModuleAssigment($tableDeclaration, $entityType, $moduleName) + ] ] ); } @@ -295,7 +250,7 @@ private function getDeclarativeSchema(): array * @param string $entityType * @param null|string $entityName * @return array - * @throws \Exception + * @throws LocalizedException */ private function resolveEntityDependencies(string $tableName, string $entityType, ?string $entityName = null): array { @@ -378,7 +333,7 @@ private function getDependenciesFromFiles($file) * * @param array $moduleDeclaration * @return array - * @throws \Exception + * @throws LocalizedException */ private function getDisabledDependencies(array $moduleDeclaration): array { @@ -467,6 +422,7 @@ private function getConstraintDependencies(array $moduleDeclaration): array $this->getDependencyId($tableName, self::SCHEMA_ENTITY_CONSTRAINT, $constraintName); switch ($constraintDeclaration['type']) { case 'foreign': + //phpcs:ignore Magento2.Performance.ForeachArrayMerge $constraintDependencies = array_merge( $constraintDependencies, $this->getFKDependencies($constraintDeclaration) @@ -490,7 +446,7 @@ private function getConstraintDependencies(array $moduleDeclaration): array * @param string $tableName * @param array $entityDeclaration * @return array - * @throws \Exception + * @throws LocalizedException */ private function getComplexDependency(string $tableName, array $entityDeclaration): array { @@ -516,7 +472,7 @@ private function getComplexDependency(string $tableName, array $entityDeclaratio * * @param array $moduleDeclaration * @return array - * @throws \Exception + * @throws LocalizedException */ private function getIndexDependencies(array $moduleDeclaration): array { @@ -586,11 +542,13 @@ public static function decodeDependencyId(string $id): array /** * Collect module dependencies. * - * @param string $currentModuleName + * @param $currentModuleName * @param array $dependencies * @return array + * @throws InspectionException + * @throws LocalizedException */ - private function collectDependencies($currentModuleName, $dependencies = []) + private function collectDependencies($currentModuleName, $dependencies = []): array { if (empty($dependencies)) { return []; @@ -599,160 +557,43 @@ private function collectDependencies($currentModuleName, $dependencies = []) $this->collectDependency($dependencyName, $dependency, $currentModuleName); } - return $this->getDeclaredDependencies($currentModuleName, self::TYPE_HARD, self::MAP_TYPE_FOUND); + return $this->dependencyProvider->getDeclaredDependencies( + $currentModuleName, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_FOUND + ); } /** - * Collect a module dependency. + * Collect a module dependency. * * @param string $dependencyName * @param array $dependency * @param string $currentModule + * @throws LocalizedException + * @throws InspectionException */ private function collectDependency( string $dependencyName, array $dependency, string $currentModule ) { - $declared = $this->getDeclaredDependencies($currentModule, self::TYPE_HARD, self::MAP_TYPE_DECLARED); + $declared = $this->dependencyProvider->getDeclaredDependencies( + $currentModule, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_DECLARED + ); $checkResult = array_intersect($declared, $dependency); if (empty($checkResult)) { - $this->addDependencies( + $this->dependencyProvider->addDependencies( $currentModule, - self::TYPE_HARD, - self::MAP_TYPE_FOUND, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_FOUND, [ $dependencyName => $dependency, ] ); } } - - /** - * Add dependencies to dependency list. - * - * @param string $moduleName - * @param array $packageNames - * @param string $type - * - * @return void - * @throws \Exception - */ - private function presetDependencies( - string $moduleName, - array $packageNames, - string $type - ): void { - $packageNames = array_filter($packageNames, function ($packageName) { - return $this->getModuleName($packageName) || - 0 === strpos($packageName, 'magento/') && 'magento/magento-composer-installer' != $packageName; - }); - - foreach ($packageNames as $packageName) { - $this->addDependencies( - $moduleName, - $type, - self::MAP_TYPE_DECLARED, - [$this->convertModuleName($packageName)] - ); - } - } - - /** - * Returns package name on module name mapping. - * - * @return array - * @throws \Exception - */ - private function getPackageModuleMapping(): array - { - if (!$this->packageModuleMapping) { - $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); - - $packageModuleMapping = []; - foreach ($jsonFiles as $file) { - $moduleXml = simplexml_load_file(dirname($file) . '/etc/module.xml'); - $moduleName = str_replace('_', '\\', (string)$moduleXml->module->attributes()->name); - $composerJson = $this->readJsonFile($file); - $packageName = $composerJson->name; - $packageModuleMapping[$packageName] = $moduleName; - } - - $this->packageModuleMapping = $packageModuleMapping; - } - - return $this->packageModuleMapping; - } - - /** - * Retrieve Magento style module name. - * - * @param string $packageName - * @return null|string - * @throws \Exception - */ - private function getModuleName(string $packageName): ?string - { - return $this->getPackageModuleMapping()[$packageName] ?? null; - } - - /** - * Retrieve array of dependency items. - * - * @param $module - * @param $type - * @param $mapType - * @return array - */ - private function getDeclaredDependencies(string $module, string $type, string $mapType) - { - return $this->mapDependencies[$module][$type][$mapType] ?? []; - } - - /** - * Add dependency map items. - * - * @param $module - * @param $type - * @param $mapType - * @param $dependencies - */ - protected function addDependencies(string $module, string $type, string $mapType, array $dependencies) - { - $this->mapDependencies[$module][$type][$mapType] = array_merge_recursive( - $this->getDeclaredDependencies($module, $type, $mapType), - $dependencies - ); - } - - /** - * Converts a composer json component name into the Magento Module form. - * - * @param string $jsonName The name of a composer json component or dependency e.g. 'magento/module-theme' - * @return string The corresponding Magento Module e.g. 'Magento\Theme' - * @throws \Exception - */ - private function convertModuleName(string $jsonName): string - { - $moduleName = $this->getModuleName($jsonName); - if ($moduleName) { - return $moduleName; - } - - if (strpos($jsonName, 'magento/magento') !== false - || strpos($jsonName, 'magento/framework') !== false - ) { - $moduleName = str_replace('/', "\t", $jsonName); - $moduleName = str_replace('framework-', "Framework\t", $moduleName); - $moduleName = str_replace('-', ' ', $moduleName); - $moduleName = ucwords($moduleName); - $moduleName = str_replace("\t", '\\', $moduleName); - $moduleName = str_replace(' ', '', $moduleName); - } else { - $moduleName = $jsonName; - } - - return $moduleName; - } } diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DependencyProvider.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DependencyProvider.php new file mode 100644 index 0000000000000..fc1eb128db4ff --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/DependencyProvider.php @@ -0,0 +1,221 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Test\Integrity\Dependency; + +use Magento\Framework\App\Utility\Files; +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Config\Composer\Package; +use Magento\TestFramework\Inspection\Exception as InspectionException; + +class DependencyProvider +{ + /** + * Types of dependency between modules. + */ + public const TYPE_HARD = 'hard'; + + /** + * The identifier of dependency for mapping. + */ + public const MAP_TYPE_DECLARED = 'declared'; + + /** + * The identifier of dependency for mapping. + */ + public const MAP_TYPE_FOUND = 'found'; + + /** + * @var array + */ + private $mapDependencies = []; + + /** + * @var array + */ + private $packageModuleMapping = []; + + /** + * @var bool + */ + private $isInited = false; + + /** + * Add dependency map items. + * + * @param $module + * @param $type + * @param $mapType + * @param $dependencies + * @throws LocalizedException + * @throws InspectionException + */ + public function addDependencies(string $module, string $type, string $mapType, array $dependencies) + { + if (!$this->isInited) { + $this->initDeclaredDependencies(); + } + $this->mapDependencies[$module][$type][$mapType] = array_merge_recursive( + $this->getDeclaredDependencies($module, $type, $mapType), + $dependencies + ); + } + + /** + * Retrieve array of dependency items. + * + * @param $module + * @param $type + * @param $mapType + * @return array + * @throws LocalizedException + * @throws InspectionException + */ + public function getDeclaredDependencies(string $module, string $type, string $mapType): array + { + if (!$this->isInited) { + $this->initDeclaredDependencies(); + } + return $this->mapDependencies[$module][$type][$mapType] ?? []; + } + + /** + * Initialise map of dependencies. + * + * @throws InspectionException + * @throws LocalizedException + */ + private function initDeclaredDependencies() + { + $this->isInited = true; + if (empty($this->mapDependencies)) { + $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); + foreach ($jsonFiles as $file) { + $json = new Package($this->readJsonFile($file)); + $moduleName = $this->convertModuleName($json->get('name')); + $require = array_keys((array)$json->get('require')); + $this->presetDependencies($moduleName, $require, self::TYPE_HARD); + } + } + } + + /** + * Add dependencies to dependency list. + * + * @param string $moduleName + * @param array $packageNames + * @param string $type + * + * @return void + * @throws InspectionException + * @throws LocalizedException + */ + private function presetDependencies(string $moduleName, array $packageNames, string $type): void + { + $packageNames = array_filter($packageNames, function ($packageName) { + return $this->getModuleName($packageName) || + 0 === strpos($packageName, 'magento/') && 'magento/magento-composer-installer' != $packageName; + }); + + foreach ($packageNames as $packageName) { + $this->addDependencies( + $moduleName, + $type, + self::MAP_TYPE_DECLARED, + [$this->convertModuleName($packageName)] + ); + } + } + + /** + * @param string $jsonName + * @return string + * @throws InspectionException + * @throws LocalizedException + */ + private function convertModuleName(string $jsonName): string + { + $moduleName = $this->getModuleName($jsonName); + if ($moduleName) { + return $moduleName; + } + + if (strpos($jsonName, 'magento/magento') !== false + || strpos($jsonName, 'magento/framework') !== false + ) { + $moduleName = str_replace('/', "\t", $jsonName); + $moduleName = str_replace('framework-', "Framework\t", $moduleName); + $moduleName = str_replace('-', ' ', $moduleName); + $moduleName = ucwords($moduleName); + $moduleName = str_replace("\t", '\\', $moduleName); + $moduleName = str_replace(' ', '', $moduleName); + } else { + $moduleName = $jsonName; + } + + return $moduleName; + } + + /** + * Read data from json file. + * + * @param string $file + * @return mixed + * @throws InspectionException + */ + private function readJsonFile(string $file, bool $asArray = false) + { + $decodedJson = json_decode(file_get_contents($file), $asArray); + if (null == $decodedJson) { + throw new InspectionException("Invalid Json: $file"); + } + + return $decodedJson; + } + + /** + * Retrieve Magento style module name. + * + * @param string $packageName + * @return null|string + * @throws InspectionException + * @throws LocalizedException + */ + private function getModuleName(string $packageName): ?string + { + return $this->getPackageModuleMapping()[$packageName] ?? null; + } + + /** + * Returns package name on module name mapping. + * + * @return array + * @throws InspectionException + * @throws LocalizedException + */ + private function getPackageModuleMapping(): array + { + if (!$this->packageModuleMapping) { + $jsonFiles = Files::init()->getComposerFiles(ComponentRegistrar::MODULE, false); + + $packageModuleMapping = []; + foreach ($jsonFiles as $file) { + $moduleXml = simplexml_load_file(dirname($file) . '/etc/module.xml'); + $moduleName = str_replace('_', '\\', (string)$moduleXml->module->attributes()->name); + $composerJson = $this->readJsonFile($file); + $packageName = $composerJson->name; + $packageModuleMapping[$packageName] = $moduleName; + } + + $this->packageModuleMapping = $packageModuleMapping; + } + + return $this->packageModuleMapping; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/GraphQlSchemaDependencyProvider.php b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/GraphQlSchemaDependencyProvider.php new file mode 100644 index 0000000000000..b5a0b177b35fe --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/Dependency/GraphQlSchemaDependencyProvider.php @@ -0,0 +1,160 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Test\Integrity\Dependency; + +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeReaderComposite; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Exception\LocalizedException; +use Magento\TestFramework\Inspection\Exception as InspectionException; + +/** + * Provide information on the dependency between the modules according to the GraphQL schema. + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + */ +class GraphQlSchemaDependencyProvider +{ + /** + * @var array + */ + private $parsedSchema = []; + + /** + * @var DependencyProvider + */ + private $dependencyProvider; + + /** + * @param DependencyProvider $dependencyProvider + */ + public function __construct(DependencyProvider $dependencyProvider) + { + $this->dependencyProvider = $dependencyProvider; + } + + /** + * Provide declared dependencies between modules based on the declarative schema configuration. + * + * @param string $moduleName + * @return array + * @throws LocalizedException + * @throws InspectionException + */ + public function getDeclaredExistingModuleDependencies(string $moduleName): array + { + $dependencies = $this->getDependenciesFromSchema($moduleName); + $declared = $this->dependencyProvider->getDeclaredDependencies( + $moduleName, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_DECLARED + ); + return array_unique(array_values(array_intersect($declared, $dependencies))); + } + + /** + * Provide undeclared dependencies between modules based on the declarative schema configuration. + * + * [ + * $dependencyId => [$module1, $module2, $module3 ...], + * ... + * ] + * + * @param string $moduleName + * @return array + * @throws InspectionException + * @throws LocalizedException + */ + public function getUndeclaredModuleDependencies(string $moduleName): array + { + $dependencies = $this->getDependenciesFromSchema($moduleName); + return $this->collectDependencies($moduleName, $dependencies); + } + + /** + * Get parsed GraphQl schema + * + * @return array + */ + private function getGraphQlSchemaDeclaration(): array + { + if (!$this->parsedSchema) { + $objectManager = ObjectManager::getInstance(); + $typeReader = $objectManager->create(TypeReaderComposite::class); + $reader = $objectManager->create(GraphQlReader::class, ['typeReader' => $typeReader]); + $this->parsedSchema = $reader->read(); + } + + return $this->parsedSchema; + } + + /** + * Get dependencies from GraphQl schema + * + * @param string $moduleName + * @return array + */ + private function getDependenciesFromSchema(string $moduleName): array + { + $schema = $this->getGraphQlSchemaDeclaration(); + + $dependencies = []; + + foreach ($schema as $type) { + if (isset($type['module']) && $type['module'] === $moduleName && isset($type['implements'])) { + $interfaces = array_keys($type['implements']); + foreach ($interfaces as $interface) { + $dependOnModule = $schema[$interface]['module']; + if ($dependOnModule !== $moduleName) { + $dependencies[] = $dependOnModule; + } + } + + } + } + return array_unique($dependencies); + } + + /** + * Collect module dependencies. + * + * @param string $currentModuleName + * @param array $dependencies + * @return array + * @throws InspectionException + * @throws LocalizedException + */ + private function collectDependencies(string $currentModuleName, array $dependencies = []): array + { + if (empty($dependencies)) { + return []; + } + $declared = $this->dependencyProvider->getDeclaredDependencies( + $currentModuleName, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_DECLARED + ); + $checkResult = array_intersect($declared, $dependencies); + + if (empty($checkResult)) { + $this->dependencyProvider->addDependencies( + $currentModuleName, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_FOUND, + [$currentModuleName => $dependencies] + ); + } + + return $this->dependencyProvider->getDeclaredDependencies( + $currentModuleName, + DependencyProvider::TYPE_HARD, + DependencyProvider::MAP_TYPE_FOUND + ); + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php index 6711de91200dd..d79e94af2bc01 100644 --- a/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/DependencyTest.php @@ -8,10 +8,12 @@ */ namespace Magento\Test\Integrity; +use Magento\Framework\App\Bootstrap; use Magento\Framework\App\Utility\Files; use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Exception\LocalizedException; use Magento\Test\Integrity\Dependency\DeclarativeSchemaDependencyProvider; +use Magento\Test\Integrity\Dependency\GraphQlSchemaDependencyProvider; use Magento\TestFramework\Dependency\DbRule; use Magento\TestFramework\Dependency\DiRule; use Magento\TestFramework\Dependency\LayoutRule; @@ -764,7 +766,7 @@ function (&$moduleName) { $this->_setDependencies($currentModule, $type, self::MAP_TYPE_REDUNDANT, $moduleName); } - $this->addDependency($currentModule, $type, self::MAP_TYPE_FOUND, $moduleName); + self::addDependency($currentModule, $type, self::MAP_TYPE_FOUND, $moduleName); } if (empty($declaredDependencies)) { @@ -782,7 +784,9 @@ function (&$moduleName) { */ public function collectRedundant() { - $schemaDependencyProvider = new DeclarativeSchemaDependencyProvider(); + $objectManager = Bootstrap::create(BP, $_SERVER)->getObjectManager(); + $schemaDependencyProvider = $objectManager->create(DeclarativeSchemaDependencyProvider::class); + $graphQlSchemaDependencyProvider = $objectManager->create(GraphQlSchemaDependencyProvider::class); foreach (array_keys(self::$mapDependencies) as $module) { $declared = $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_DECLARED); @@ -790,7 +794,8 @@ public function collectRedundant() $found = array_merge( $this->_getDependencies($module, self::TYPE_HARD, self::MAP_TYPE_FOUND), $this->_getDependencies($module, self::TYPE_SOFT, self::MAP_TYPE_FOUND), - $schemaDependencyProvider->getDeclaredExistingModuleDependencies($module) + $schemaDependencyProvider->getDeclaredExistingModuleDependencies($module), + $graphQlSchemaDependencyProvider->getDeclaredExistingModuleDependencies($module) ); $found['Magento\Framework'] = 'Magento\Framework'; $this->_setDependencies($module, self::TYPE_HARD, self::MAP_TYPE_REDUNDANT, array_diff($declared, $found)); diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/GraphQlDependencyTest.php b/dev/tests/static/testsuite/Magento/Test/Integrity/GraphQlDependencyTest.php new file mode 100644 index 0000000000000..9ef14d90a6f3b --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/GraphQlDependencyTest.php @@ -0,0 +1,136 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Test\Integrity; + +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\Utility\AggregateInvoker; +use Magento\Framework\App\Utility\Files; +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Exception\LocalizedException; +use Magento\Test\Integrity\Dependency\GraphQlSchemaDependencyProvider; +use Magento\TestFramework\Inspection\Exception as InspectionException; +use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\TestCase; + +class GraphQlDependencyTest extends TestCase +{ + /** + * @var GraphQlSchemaDependencyProvider + */ + private $dependencyProvider; + + /** + * Sets up data + * + * @throws InspectionException + */ + protected function setUp(): void + { + $root = BP; + $rootJson = $this->readJsonFile($root . '/composer.json', true); + if (preg_match('/magento\/project-*/', $rootJson['name']) == 1) { + // The Dependency test is skipped for vendor/magento build + self::markTestSkipped( + 'MAGETWO-43654: The build is running from vendor/magento. DependencyTest is skipped.' + ); + } + $objectManager = ObjectManager::getInstance(); + $this->dependencyProvider = $objectManager->create(GraphQlSchemaDependencyProvider::class); + } + + /** + * @throws LocalizedException + */ + public function testUndeclaredDependencies() + { + $invoker = new AggregateInvoker($this); + $invoker( + /** + * Check undeclared modules dependencies for specified file + * + * @param string $fileType + * @param string $file + * @throws LocalizedException + * @throws InspectionException + * @throws AssertionFailedError + */ + function ($file) { + $componentRegistrar = new ComponentRegistrar(); + $foundModuleName = ''; + foreach ($componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + if (strpos($file, $moduleDir . '/') !== false) { + $foundModuleName = str_replace('_', '\\', $moduleName); + break; + } + } + if (empty($foundModuleName)) { + return; + } + + $undeclaredDependency = $this->dependencyProvider->getUndeclaredModuleDependencies($foundModuleName); + + $result = []; + foreach ($undeclaredDependency as $name => $modules) { + $modules = array_unique($modules); + $result[] = $this->getErrorMessage($name) . "\n" . implode("\t\n", $modules) . "\n"; + } + if (!empty($result)) { + $this->fail( + 'Module ' . $moduleName . ' has undeclared dependencies: ' . "\n" . implode("\t\n", $result) + ); + } + }, + $this->prepareFiles(Files::init()->getDbSchemaFiles('schema.graphqls')) + ); + } + + /** + * Convert file list to data provider structure. + * + * @param string[] $files + * @return array + */ + private function prepareFiles(array $files): array + { + $result = []; + foreach ($files as $relativePath => $file) { + $absolutePath = reset($file); + $result[$relativePath] = [$absolutePath]; + } + return $result; + } + + /** + * Retrieve error message for dependency. + * + * @param string $id + * @return string + */ + private function getErrorMessage(string $id): string + { + return sprintf('%s has undeclared dependency on one of the following modules:', $id); + } + + /** + * Read data from json file. + * + * @param string $file + * @return mixed + * @throws InspectionException + */ + private function readJsonFile(string $file, bool $asArray = false) + { + $decodedJson = json_decode(file_get_contents($file), $asArray); + if (null == $decodedJson) { + throw new InspectionException("Invalid Json: $file"); + } + + return $decodedJson; + } +} diff --git a/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/redundant_dependencies_webapi.php b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/redundant_dependencies_webapi.php new file mode 100644 index 0000000000000..41478ade9f901 --- /dev/null +++ b/dev/tests/static/testsuite/Magento/Test/Integrity/_files/dependency_test/whitelist/redundant_dependencies_webapi.php @@ -0,0 +1,12 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +return [ + 'Magento\GraphQl' => [ + 'Magento\Webapi' => 'Magento\Webapi' + ] +]; diff --git a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php index ffc991572addf..324753b4bd4ec 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php +++ b/dev/tests/static/testsuite/Magento/Test/Php/LiveCodeTest.php @@ -326,9 +326,19 @@ public function testCodeStyle() touch($reportFile); } $codeSniffer = new CodeSniffer('Magento', $reportFile, new Wrapper()); - $result = $codeSniffer->run( - $this->isFullScan() ? $this->getFullWhitelist() : self::getWhitelist(['php', 'phtml']) - ); + $fileList = $this->isFullScan() ? $this->getFullWhitelist() : self::getWhitelist(['php', 'phtml']); + $ignoreList = Files::init()->readLists(__DIR__ . '/_files/phpcs/ignorelist/*.txt'); + if ($ignoreList) { + $ignoreListPattern = sprintf('#(%s)#i', implode('|', $ignoreList)); + $fileList = array_filter( + $fileList, + function ($path) use ($ignoreListPattern) { + return !preg_match($ignoreListPattern, $path); + } + ); + } + + $result = $codeSniffer->run($fileList); $report = file_get_contents($reportFile); $this->assertEquals( 0, @@ -348,8 +358,19 @@ public function testCodeMess() if (!$codeMessDetector->canRun()) { $this->markTestSkipped('PHP Mess Detector is not available.'); } + $fileList = self::getWhitelist(['php']); + $ignoreList = Files::init()->readLists(__DIR__ . '/_files/phpmd/ignorelist/*.txt'); + if ($ignoreList) { + $ignoreListPattern = sprintf('#(%s)#i', implode('|', $ignoreList)); + $fileList = array_filter( + $fileList, + function ($path) use ($ignoreListPattern) { + return !preg_match($ignoreListPattern, $path); + } + ); + } - $result = $codeMessDetector->run(self::getWhitelist(['php'])); + $result = $codeMessDetector->run($fileList); $output = ""; if (file_exists($reportFile)) { diff --git a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon index fb6eb4dd5c958..d487fbe602acf 100644 --- a/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon +++ b/dev/tests/static/testsuite/Magento/Test/Php/_files/phpstan/phpstan.neon @@ -26,8 +26,23 @@ parameters: - '#Function setCustomErrorHandler not found#' # Ignore 'return statement is missing' error when 'void' is present in return type list - '#Method (?:.*?) should return (?:.*?)void(?:.*?) but return statement is missing.#' + # Ignore constants, defined dynamically. + - '#Constant TESTS_WEB_API_ADAPTER not found.#' + - '#Constant TESTS_BASE_URL not found.#' + - '#Constant TESTS_XDEBUG_ENABLED not found.#' + - '#Constant TESTS_XDEBUG_SESSION not found.#' + - '#Constant INTEGRATION_TESTS_DIR not found.#' + - '#Constant MAGENTO_MODULES_PATH not found.#' + - '#Constant TESTS_MODULES_PATH not found.#' + - '#Constant TESTS_INSTALLATION_DB_CONFIG_FILE not found.#' + - '#Constant T_[A-Z_]+ not found.#' + services: + - + class: Magento\PhpStan\Reflection\Php\DataObjectClassReflectionExtension + tags: {phpstan.broker.methodsClassReflectionExtension: {priority: 100}} + errorFormatter.filtered: class: Magento\PhpStan\Formatters\FilteredErrorFormatter arguments: diff --git a/dev/tools/UpgradeScripts/pre_composer_update_2.3.php b/dev/tools/UpgradeScripts/pre_composer_update_2.3.php deleted file mode 100644 index e6f1ddb31c4a3..0000000000000 --- a/dev/tools/UpgradeScripts/pre_composer_update_2.3.php +++ /dev/null @@ -1,419 +0,0 @@ -#!/usr/bin/php -<?php -/** - * Copyright © Magento, Inc. All rights reserved. - * See COPYING.txt for license details. - */ -declare(strict_types=1); - -$_scriptName = basename(__FILE__); - -define( - 'SYNOPSIS', -<<<SYNOPSIS -Updates Magento with 2.3 requirements that can't be done by `composer update` or `bin/magento setup:upgrade`. -Run this script after upgrading to PHP 7.1/7.2 and before running `composer update` or `bin/magento setup:upgrade`. - -Steps included: - - Require new version of the metapackage - - Update "require-dev" section - - Add "Laminas\\Mvc\\Controller\\": "setup/src/Zend/Mvc/Controller/" to composer.json "autoload":"psr-4" section - - Update Magento/Updater if it's installed - - Update name, version, and description fields in the root composer.json - -Usage: php -f $_scriptName -- --root='</path/to/magento/root/>' [--composer='</path/to/composer/executable>'] - [--edition='<community|enterprise>'] [--repo='<composer_repo_url>'] [--version='<version_constraint>'] - [--help] - -Required: - --root='</path/to/magento/root/>' - Path to the Magento installation root directory - -Optional: - --composer='</path/to/composer/executable>' - Path to the composer executable - - Default: The composer found in the system PATH - - --edition='<community|enterprise>' - Target Magento edition for the update. Open Source = 'community', Commerce = 'enterprise' - - Default: The edition currently required in composer.json - - --repo='<composer_repo_url>' - The Magento repository url to use to pull the new packages - - Default: The Magento repository configured in composer.json - - --version='<version_constraint>' - A composer version constraint for allowable 2.3 packages. Versions other than 2.3 are not handled by this script - See https://getcomposer.org/doc/articles/versions.md#writing-version-constraints for more information. - - Default: The latest 2.3 version available in the Magento repository - - --help - Display this message -SYNOPSIS -); - -$opts = getopt('', [ - 'root:', - 'composer:', - 'edition:', - 'repo:', - 'version:', - 'help' -]); - -// Log levels available for use with output() function -define('INFO', 0); -define('WARN', 1); -define('ERROR', 2); - -if (isset($opts['help'])) { - output(SYNOPSIS); - exit(0); -} - -try { - if (version_compare(PHP_VERSION, '7.1', '<') || version_compare(PHP_VERSION, '7.3', '>=')) { - preg_match('/^\d+\.\d+\.\d+/',PHP_VERSION, $matches); - $phpVersion = $matches[0]; - throw new Exception("Invalid PHP version '$phpVersion'. Magento 2.3 requires PHP 7.1 or 7.2"); - } - - /**** Populate and Validate Settings ****/ - - if (empty($opts['root']) || !is_dir($opts['root'])) { - throw new BadMethodCallException('Existing Magento root directory must be supplied with --root'); - } - $rootDir = $opts['root']; - - $composerFile = "$rootDir/composer.json"; - if (!file_exists($composerFile)) { - throw new InvalidArgumentException("Supplied Magento root directory '$rootDir' does not contain composer.json"); - } - - $composerData = json_decode(file_get_contents($composerFile), true); - - $metapackageMatcher = '/^magento\/product\-(?<edition>community|enterprise)\-edition$/'; - foreach (array_keys($composerData['require']) as $requiredPackage) { - if (preg_match($metapackageMatcher, $requiredPackage, $matches)) { - $edition = $matches['edition']; - break; - } - } - if (empty($edition)) { - throw new InvalidArgumentException("No Magento metapackage found in $composerFile"); - } - - // Override composer.json edition if one is passed to the script - if (!empty($opts['edition'])) { - $edition = $opts['edition']; - } - $edition = strtolower($edition); - - if ($edition !== 'community' && $edition !== 'enterprise') { - throw new InvalidArgumentException("Only 'community' and 'enterprise' editions allowed; '$edition' given"); - } - - $composerExec = (!empty($opts['composer']) ? $opts['composer'] : 'composer'); - if (basename($composerExec, '.phar') != 'composer') { - throw new InvalidArgumentException("'$composerExec' is not a composer executable"); - } - - // Use 'command -v' to check if composer is executable - exec("command -v $composerExec", $out, $composerFailed); - if ($composerFailed) { - if ($composerExec == 'composer') { - $message = 'Composer executable is not available in the system PATH'; - } - else { - $message = "Invalid composer executable '$composerExec'"; - } - throw new InvalidArgumentException($message); - } - - // The composer command uses the Magento root as the working directory so this script can be run from anywhere - $composerExec = "$composerExec --working-dir='$rootDir'"; - - // Set the version constraint to any 2.3 package if not specified - $constraint = !empty($opts['version']) ? $opts['version'] : '2.3.*'; - - // Composer package names - $project = "magento/project-$edition-edition"; - $metapackage = "magento/product-$edition-edition"; - - // Get the list of potential Magento repositories to search for the update package - $mageUrls = []; - $authFailed = []; - if (!empty($opts['repo'])) { - $mageUrls[] = $opts['repo']; - } - else { - foreach ($composerData['repositories'] as $label => $repo) { - if (is_string($label) && strpos(strtolower($label), 'mage') !== false || strpos($repo['url'], '.mage') !== false) { - $mageUrls[] = $repo['url']; - } - } - - if (count($mageUrls) == 0) { - throw new InvalidArgumentException('No Magento repository urls found in composer.json'); - } - } - - $tempDir = findUnusedFilename($rootDir, 'temp_project'); - $projectConstraint = "$project='$constraint'"; - $version = null; - $description = null; - - output("**** Searching for a matching version of $project ****"); - - // Try to retrieve a 2.3 package from each Magento repository until one is found - foreach ($mageUrls as $repoUrl) { - try { - output("\\nChecking $repoUrl"); - deleteFilepath($tempDir); - runComposer("create-project --repository=$repoUrl $projectConstraint $tempDir --no-install"); - - // Make sure the downloaded package is 2.3 - $newComposer = json_decode(file_get_contents("$tempDir/composer.json"), true); - $version = $newComposer['version']; - $description = $newComposer['description']; - - if (strpos($version, '2.3.') !== 0) { - throw new InvalidArgumentException("Bad 2.3 version constraint '$constraint'; version $version found"); - } - - // If no errors occurred, set this as the correct repo, forget errors from previous repos, and move forward - output("\\n**** Found compatible $project version: $version ****"); - $repo = $repoUrl; - unset($exception); - break; - } - catch (Exception $e) { - // If this repository doesn't have a valid package, save the error but continue checking any others - output("Failed to find a valid 2.3 $project package on $repoUrl", WARN); - $exception = $e; - } - } - - // If a valid project package hasn't been found, throw the last error - if (isset($exception)) { - throw $exception; - } - - output("\\n**** Executing Updates ****"); - - $composerBackup = findUnusedFilename($rootDir, 'composer.json.bak'); - output("\\nBacking up $composerFile to $composerBackup"); - copy($composerFile, $composerBackup); - - // Add the repository to composer.json if needed without overwriting any existing ones - $repoUrls = array_map(function ($r) { return $r['url']; }, $composerData['repositories']); - if (!in_array($repo, $repoUrls)) { - $repoLabels = array_map('strtolower',array_keys($composerData['repositories'])); - $newLabel = 'magento'; - if (in_array($newLabel, $repoLabels)) { - $count = count($repoLabels); - for ($i = 1; $i <= $count; $i++) { - if (!in_array("$newLabel-$i", $repoLabels)) { - $newLabel = "$newLabel-$i"; - break; - } - } - } - output("\\nAdding $repo to composer repositories under label '$newLabel'"); - runComposer("config repositories.$newLabel composer $repo"); - } - - output("\\nUpdating Magento metapackage requirement to $metapackage=$version"); - if ($edition == 'enterprise') { - // Community -> Enterprise upgrades need to remove the community edition metapackage - runComposer('remove magento/product-community-edition --no-update'); - output(''); - } - runComposer("require $metapackage=$version --no-update"); - - output('\nUpdating "require-dev" section of composer.json'); - runComposer('require --dev ' . - 'allure-framework/allure-phpunit:~1.2.0 ' . - 'friendsofphp/php-cs-fixer:~2.14.0 ' . - 'lusitanian/oauth:~0.8.10 ' . - 'magento/magento-coding-standard:~3.0.0 ' . - 'magento/magento2-functional-testing-framework:~2.4.3 ' . - 'pdepend/pdepend:2.5.2 ' . - 'phpmd/phpmd:@stable ' . - 'phpunit/phpunit:~6.5.0 ' . - 'sebastian/phpcpd:~3.0.0 ' . - 'squizlabs/php_codesniffer:3.4.0 ' . - '--sort-packages --no-update'); - output(''); - runComposer('remove --dev sjparkinson/static-review fabpot/php-cs-fixer --no-update'); - - output('\nAdding "Zend\\\\Mvc\\\\Controller\\\\": "setup/src/Zend/Mvc/Controller/" to "autoload": "psr-4"'); - $composerData['autoload']['psr-4']['Laminas\\Mvc\\Controller\\'] = 'setup/src/Zend/Mvc/Controller/'; - - if (preg_match('/^magento\/project\-(community|enterprise)\-edition$/', $composerData['name'])) { - output('\nUpdating project name, version, and description'); - $composerData['name'] = $project; - $composerData['version'] = $version; - $composerData['description'] = $description; - } - - file_put_contents($composerFile, json_encode($composerData, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT)); - - // Update Magento/Updater if it's installed - $updateDir = "$rootDir/update"; - if (file_exists($updateDir)) { - $updateBackup = findUnusedFilename($rootDir, 'update.bak'); - output("\\nBacking up Magento/Updater directory $updateDir to $updateBackup"); - rename($updateDir, $updateBackup); - output('\nUpdating Magento/Updater'); - rename("$tempDir/update", $updateDir); - } - - // Remove temp project directory that was used for repo/version validation and new source for Magento/Updater - deleteFilepath($tempDir); - - output("\\n**** Script Complete! $composerFile updated to Magento version $version ****"); - if (count($authFailed) > 0) { - output('Repository authentication failures occurred!', WARN); - output(' * Failed authentication could result in incorrect package versions', WARN); - output(' * To resolve, add credentials for the repositories to auth.json', WARN); - output(' * URL(s) failing authentication: ' . join(', ', array_keys($authFailed)), WARN); - } -} catch (Exception $e) { - if ($e->getPrevious()) { - $e = $e->getPrevious(); - } - - try { - output($e->getMessage(), ERROR, get_class($e)); - output('Script failed! See usage information with --help', ERROR); - - if (isset($composerBackup) && file_exists($composerBackup)) { - output("Resetting $composerFile backup"); - deleteFilepath($composerFile); - rename($composerBackup, $composerFile); - } - if (isset($updateBackup) && file_exists($updateBackup)) { - output("Resetting $updateDir backup"); - deleteFilepath($updateDir); - rename($updateBackup, $updateDir); - } - if (isset($tempDir) && file_exists($tempDir)) { - output('Removing temporary project directory'); - deleteFilepath($tempDir); - } - } - catch (Exception $e2) { - output($e2->getMessage(), ERROR, get_class($e2)); - output('Backup restoration or directory cleanup failed', ERROR); - } - - exit($e->getCode() == 0 ? 1 : $e->getCode()); -} - -/** - * Gets a variant of a filename that doesn't already exist so we don't overwrite anything - * - * @param string $dir - * @param string $filename - * @return string - */ -function findUnusedFilename($dir, $filename) { - $unique = "$dir/$filename"; - if (file_exists($unique)) { - $unique = tempnam($dir, "$filename."); - unlink($unique); - } - return $unique; -} - -/** - * Execute a composer command, reload $composerData afterwards, and check for repo authentication warnings - * - * @param string $command - * @return array Command output split by lines - * @throws RuntimeException - */ -function runComposer($command) -{ - global $composerExec, $composerData, $composerFile, $authFailed; - $command = "$composerExec $command --no-interaction"; - output(" Running command:\\n $command"); - exec("$command 2>&1", $lines, $exitCode); - $output = ' ' . join('\n ', $lines); - - // Reload composer object from the updated composer.json - $composerData = json_decode(file_get_contents($composerFile), true); - - if (0 !== $exitCode) { - $output = "Error encountered running command:\\n $command\\n$output"; - throw new RuntimeException($output, $exitCode); - } - output($output); - - if (strpos($output, 'URL required authentication.') !== false) { - preg_match("/'(https?:\/\/)?(?<url>[^\/']+)(\/[^']*)?' URL required authentication/", $output, $matches); - $authUrl = $matches['url']; - $authFailed[$authUrl] = 1; - output("Repository authentication failed; make sure '$authUrl' exists in auth.json", WARN); - } - - return $lines; -} - -/** - * Deletes a file or a directory and all its contents - * - * @param string $path - * @throws Exception - */ -function deleteFilepath($path) { - if (!file_exists($path)) { - return; - } - if (is_dir($path)) { - $files = array_diff(scandir($path), array('..', '.')); - foreach ($files as $file) { - deleteFilepath("$path/$file"); - } - rmdir($path); - } - else { - unlink($path); - } - if (file_exists($path)) { - throw new Exception("Failed to delete $path"); - } -} - -/** - * Logs the given text with \n newline replacement and log level formatting - * - * @param string $string Text to log - * @param int $level One of INFO, WARN, or ERROR - * @param string $label Optional message label; defaults to WARNING for $level = WARN and ERROR for $level = ERROR - */ -function output($string, $level = INFO, $label = '') { - $string = str_replace('\n', PHP_EOL, $string); - - if (!empty($label)) { - $label = "$label: "; - } - else if ($level == WARN) { - $label = 'WARNING: '; - } - else if ($level == ERROR) { - $label = 'ERROR: '; - } - $string = "$label$string"; - - if ($level == WARN) { - error_log($string); - } - elseif ($level == ERROR) { - error_log(PHP_EOL . $string); - } - else { - echo $string . PHP_EOL; - } -} diff --git a/lib/internal/Magento/Framework/Acl.php b/lib/internal/Magento/Framework/Acl.php index 86b28f7e2ceb4..8c80bf94e3f2a 100644 --- a/lib/internal/Magento/Framework/Acl.php +++ b/lib/internal/Magento/Framework/Acl.php @@ -9,6 +9,7 @@ * ACL. Can be queried for relations between roles and resources. * * @api + * @since 100.0.2 */ class Acl extends \Zend_Acl { diff --git a/lib/internal/Magento/Framework/Acl/AclResource.php b/lib/internal/Magento/Framework/Acl/AclResource.php index 585b4b5d8514b..47a85a069329e 100644 --- a/lib/internal/Magento/Framework/Acl/AclResource.php +++ b/lib/internal/Magento/Framework/Acl/AclResource.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class AclResource extends \Zend_Acl_Resource { diff --git a/lib/internal/Magento/Framework/Acl/AclResource/ProviderInterface.php b/lib/internal/Magento/Framework/Acl/AclResource/ProviderInterface.php index 34f85f3641ccd..c11fb7fcd56ed 100644 --- a/lib/internal/Magento/Framework/Acl/AclResource/ProviderInterface.php +++ b/lib/internal/Magento/Framework/Acl/AclResource/ProviderInterface.php @@ -9,6 +9,7 @@ * Acl resources provider interface * * @api + * @since 100.0.2 */ interface ProviderInterface { diff --git a/lib/internal/Magento/Framework/Acl/Builder.php b/lib/internal/Magento/Framework/Acl/Builder.php index 50e4f0b7b5ca9..03adaca0589ce 100644 --- a/lib/internal/Magento/Framework/Acl/Builder.php +++ b/lib/internal/Magento/Framework/Acl/Builder.php @@ -11,6 +11,7 @@ * On consequent requests, ACL object is deserialized from cache. * * @api + * @since 100.0.2 */ class Builder { @@ -77,7 +78,7 @@ public function getAcl() * Remove cached ACL instance. * * @return $this - * @since 100.2.0 + * @since 101.0.0 */ public function resetRuntimeAcl() { diff --git a/lib/internal/Magento/Framework/Acl/Data/CacheInterface.php b/lib/internal/Magento/Framework/Acl/Data/CacheInterface.php index bd6ce6d2c2095..e0a1617fd226d 100644 --- a/lib/internal/Magento/Framework/Acl/Data/CacheInterface.php +++ b/lib/internal/Magento/Framework/Acl/Data/CacheInterface.php @@ -10,7 +10,7 @@ * Interface for caching ACL data * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface CacheInterface extends \Magento\Framework\Cache\FrontendInterface { diff --git a/lib/internal/Magento/Framework/Acl/LoaderInterface.php b/lib/internal/Magento/Framework/Acl/LoaderInterface.php index 920186fc2121a..a61fcbdc24255 100644 --- a/lib/internal/Magento/Framework/Acl/LoaderInterface.php +++ b/lib/internal/Magento/Framework/Acl/LoaderInterface.php @@ -12,6 +12,7 @@ * with data (roles/rules/resources) persisted in external storage. * * @api + * @since 100.0.2 */ interface LoaderInterface { diff --git a/lib/internal/Magento/Framework/Acl/RootResource.php b/lib/internal/Magento/Framework/Acl/RootResource.php index 326416c346563..9247784e94414 100644 --- a/lib/internal/Magento/Framework/Acl/RootResource.php +++ b/lib/internal/Magento/Framework/Acl/RootResource.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class RootResource { diff --git a/lib/internal/Magento/Framework/Amqp/Config.php b/lib/internal/Magento/Framework/Amqp/Config.php index 684c5cd38b1e4..44e033f4a7b80 100644 --- a/lib/internal/Magento/Framework/Amqp/Config.php +++ b/lib/internal/Magento/Framework/Amqp/Config.php @@ -16,7 +16,7 @@ * Reads the Amqp config in the deployed environment configuration * * @api - * @since 100.0.0 + * @since 103.0.0 */ class Config { @@ -96,7 +96,6 @@ class Config * @param DeploymentConfig $config * @param string $connectionName * @param ConnectionFactory|null $connectionFactory - * @since 100.0.0 */ public function __construct( DeploymentConfig $config, @@ -113,7 +112,7 @@ public function __construct( * Destructor * * @return void - * @since 100.0.0 + * @since 103.0.0 */ public function __destruct() { @@ -126,7 +125,7 @@ public function __destruct() * @param string $key * @return string * @throws \LogicException - * @since 100.0.0 + * @since 103.0.0 */ public function getValue($key) { @@ -162,7 +161,7 @@ private function createConnection(): AbstractConnection * * @return AMQPChannel * @throws \LogicException - * @since 100.0.0 + * @since 103.0.0 */ public function getChannel() { diff --git a/lib/internal/Magento/Framework/Amqp/ConnectionTypeResolver.php b/lib/internal/Magento/Framework/Amqp/ConnectionTypeResolver.php index 9b600278144f0..54fa80f59c292 100644 --- a/lib/internal/Magento/Framework/Amqp/ConnectionTypeResolver.php +++ b/lib/internal/Magento/Framework/Amqp/ConnectionTypeResolver.php @@ -12,7 +12,7 @@ * Amqp connection type resolver. * * @api - * @since 100.0.0 + * @since 103.0.0 */ class ConnectionTypeResolver implements ConnectionTypeResolverInterface { @@ -27,7 +27,6 @@ class ConnectionTypeResolver implements ConnectionTypeResolverInterface * Initialize dependencies. * * @param DeploymentConfig $deploymentConfig - * @since 100.0.0 */ public function __construct(DeploymentConfig $deploymentConfig) { @@ -42,7 +41,7 @@ public function __construct(DeploymentConfig $deploymentConfig) /** * {@inheritdoc} - * @since 100.0.0 + * @since 103.0.0 */ public function getConnectionType($connectionName) { diff --git a/lib/internal/Magento/Framework/Amqp/Exchange.php b/lib/internal/Magento/Framework/Amqp/Exchange.php index e57fa09b83d1b..56fb25debc9bc 100644 --- a/lib/internal/Magento/Framework/Amqp/Exchange.php +++ b/lib/internal/Magento/Framework/Amqp/Exchange.php @@ -18,7 +18,7 @@ * Class message exchange. * * @api - * @since 100.0.0 + * @since 103.0.0 */ class Exchange implements ExchangeInterface { @@ -57,7 +57,6 @@ class Exchange implements ExchangeInterface * @param ResponseQueueNameBuilder $responseQueueNameBuilder * @param CommunicationConfigInterface $communicationConfig * @param int $rpcConnectionTimeout - * @since 100.0.0 */ public function __construct( Config $amqpConfig, @@ -75,7 +74,7 @@ public function __construct( /** * {@inheritdoc} - * @since 100.0.0 + * @since 103.0.0 */ public function enqueue($topic, EnvelopeInterface $envelope) { diff --git a/lib/internal/Magento/Framework/Amqp/ExchangeFactory.php b/lib/internal/Magento/Framework/Amqp/ExchangeFactory.php index 5291b3ab59794..1474d45dd5363 100644 --- a/lib/internal/Magento/Framework/Amqp/ExchangeFactory.php +++ b/lib/internal/Magento/Framework/Amqp/ExchangeFactory.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\Amqp\Exchange * * @api - * @since 100.0.0 + * @since 103.0.0 */ class ExchangeFactory implements \Magento\Framework\MessageQueue\ExchangeFactoryInterface { @@ -38,7 +38,6 @@ class ExchangeFactory implements \Magento\Framework\MessageQueue\ExchangeFactory * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param ConfigPool $configPool * @param string $instanceName - * @since 100.0.0 */ public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, @@ -52,7 +51,7 @@ public function __construct( /** * {@inheritdoc} - * @since 100.0.0 + * @since 103.0.0 */ public function create($connectionName, array $data = []) { diff --git a/lib/internal/Magento/Framework/Amqp/Queue.php b/lib/internal/Magento/Framework/Amqp/Queue.php index ff5bae5017fb9..84689c294af0c 100644 --- a/lib/internal/Magento/Framework/Amqp/Queue.php +++ b/lib/internal/Magento/Framework/Amqp/Queue.php @@ -17,7 +17,7 @@ * Class Queue * * @api - * @since 100.0.0 + * @since 103.0.0 */ class Queue implements QueueInterface { @@ -48,7 +48,6 @@ class Queue implements QueueInterface * @param EnvelopeFactory $envelopeFactory * @param string $queueName * @param LoggerInterface $logger - * @since 100.0.0 */ public function __construct( Config $amqpConfig, @@ -64,7 +63,7 @@ public function __construct( /** * @inheritdoc - * @since 100.0.0 + * @since 103.0.0 */ public function dequeue() { @@ -99,7 +98,7 @@ public function dequeue() /** * @inheritdoc - * @since 100.0.0 + * @since 103.0.0 */ public function acknowledge(EnvelopeInterface $envelope) { @@ -120,7 +119,7 @@ public function acknowledge(EnvelopeInterface $envelope) /** * @inheritdoc - * @since 100.0.0 + * @since 103.0.0 */ public function subscribe($callback) { @@ -154,7 +153,7 @@ public function subscribe($callback) /** * @inheritdoc - * @since 100.0.0 + * @since 103.0.0 */ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionMessage = null) { @@ -173,7 +172,7 @@ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionM /** * @inheritdoc - * @since 100.0.0 + * @since 103.0.0 */ public function push(EnvelopeInterface $envelope) { diff --git a/lib/internal/Magento/Framework/Amqp/QueueFactory.php b/lib/internal/Magento/Framework/Amqp/QueueFactory.php index 9f1635a87977b..9fe8a3551823a 100644 --- a/lib/internal/Magento/Framework/Amqp/QueueFactory.php +++ b/lib/internal/Magento/Framework/Amqp/QueueFactory.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\Amqp\Queue * * @api - * @since 100.0.0 + * @since 103.0.0 */ class QueueFactory implements \Magento\Framework\MessageQueue\QueueFactoryInterface { @@ -38,7 +38,6 @@ class QueueFactory implements \Magento\Framework\MessageQueue\QueueFactoryInterf * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param ConfigPool $configPool * @param string $instanceName - * @since 100.0.0 */ public function __construct( \Magento\Framework\ObjectManagerInterface $objectManager, @@ -52,7 +51,7 @@ public function __construct( /** * {@inheritdoc} - * @since 100.0.0 + * @since 103.0.0 */ public function create($queueName, $connectionName) { diff --git a/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php b/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php index caa5db4e7ef5c..d5aaf1769eae4 100644 --- a/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php +++ b/lib/internal/Magento/Framework/Amqp/Topology/ArgumentProcessor.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Amqp\Topology; /** - * @deprecated 100.0.0 + * @deprecated 103.0.0 * see: https://github.com/php-amqplib/php-amqplib/issues/405 */ trait ArgumentProcessor diff --git a/lib/internal/Magento/Framework/Api/AbstractExtensibleObject.php b/lib/internal/Magento/Framework/Api/AbstractExtensibleObject.php index 902709bbedcd3..ae92ee9d934f0 100644 --- a/lib/internal/Magento/Framework/Api/AbstractExtensibleObject.php +++ b/lib/internal/Magento/Framework/Api/AbstractExtensibleObject.php @@ -13,8 +13,9 @@ * @SuppressWarnings(PHPMD.NumberOfChildren) * phpcs:disable Magento2.Classes.AbstractApi * @api - * @deprecated + * @deprecated 103.0.0 * @see \Magento\Framework\Model\AbstractExtensibleModel + * @since 100.0.2 */ abstract class AbstractExtensibleObject extends AbstractSimpleObject implements CustomAttributesDataInterface { diff --git a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php index 029a78bf0ddf7..35f115634ccda 100644 --- a/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php +++ b/lib/internal/Magento/Framework/Api/AbstractSimpleObjectBuilder.php @@ -9,7 +9,7 @@ /** * Base Builder Class for simple data Objects - * @deprecated Every builder should have their own implementation of \Magento\Framework\Api\SimpleBuilderInterface + * @deprecated 103.0.0 Every builder should have their own implementation of \Magento\Framework\Api\SimpleBuilderInterface * @SuppressWarnings(PHPMD.NumberOfChildren) */ abstract class AbstractSimpleObjectBuilder implements SimpleBuilderInterface diff --git a/lib/internal/Magento/Framework/Api/AttributeInterface.php b/lib/internal/Magento/Framework/Api/AttributeInterface.php index d208d861d4d2e..5324ddb499fd5 100644 --- a/lib/internal/Magento/Framework/Api/AttributeInterface.php +++ b/lib/internal/Magento/Framework/Api/AttributeInterface.php @@ -10,6 +10,7 @@ * Interface for custom attribute value. * * @api + * @since 100.0.2 */ interface AttributeInterface { diff --git a/lib/internal/Magento/Framework/Api/CustomAttributesDataInterface.php b/lib/internal/Magento/Framework/Api/CustomAttributesDataInterface.php index 3019f84abb15a..d6567db27fc10 100644 --- a/lib/internal/Magento/Framework/Api/CustomAttributesDataInterface.php +++ b/lib/internal/Magento/Framework/Api/CustomAttributesDataInterface.php @@ -10,6 +10,7 @@ * Interface for entities which can be extended with custom attributes. * * @api + * @since 100.0.2 */ interface CustomAttributesDataInterface extends ExtensibleDataInterface { diff --git a/lib/internal/Magento/Framework/Api/Data/ImageContentInterface.php b/lib/internal/Magento/Framework/Api/Data/ImageContentInterface.php index abd770dd12f14..06467be9522cc 100644 --- a/lib/internal/Magento/Framework/Api/Data/ImageContentInterface.php +++ b/lib/internal/Magento/Framework/Api/Data/ImageContentInterface.php @@ -10,6 +10,7 @@ * Image Content data interface * * @api + * @since 100.0.2 */ interface ImageContentInterface { diff --git a/lib/internal/Magento/Framework/Api/Data/VideoContentInterface.php b/lib/internal/Magento/Framework/Api/Data/VideoContentInterface.php index 7b1ea05cf939c..6c8f796ee8655 100644 --- a/lib/internal/Magento/Framework/Api/Data/VideoContentInterface.php +++ b/lib/internal/Magento/Framework/Api/Data/VideoContentInterface.php @@ -12,6 +12,7 @@ * Video Content data interface * * @api + * @since 100.0.2 */ interface VideoContentInterface extends ExtensibleDataInterface { diff --git a/lib/internal/Magento/Framework/Api/ExtensibleDataInterface.php b/lib/internal/Magento/Framework/Api/ExtensibleDataInterface.php index affe86992f273..38cc075559804 100644 --- a/lib/internal/Magento/Framework/Api/ExtensibleDataInterface.php +++ b/lib/internal/Magento/Framework/Api/ExtensibleDataInterface.php @@ -10,6 +10,7 @@ * Interface for entities which can be extended with extension attributes. * * @api + * @since 100.0.2 */ interface ExtensibleDataInterface { diff --git a/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinDataInterface.php b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinDataInterface.php index a0a2f649900fb..3bcd0c2fec29d 100644 --- a/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinDataInterface.php +++ b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinDataInterface.php @@ -10,6 +10,7 @@ * Interface of data holder for extension attribute joins. * * @api + * @since 100.0.2 */ interface JoinDataInterface { diff --git a/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessorInterface.php b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessorInterface.php index 1eaeaef24f419..7fb44aa337cc7 100644 --- a/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessorInterface.php +++ b/lib/internal/Magento/Framework/Api/ExtensionAttribute/JoinProcessorInterface.php @@ -12,6 +12,7 @@ * Join processor allows to join extension attributes during collections loading. * * @api + * @since 100.0.2 */ interface JoinProcessorInterface { diff --git a/lib/internal/Magento/Framework/Api/ExtensionAttributesInterface.php b/lib/internal/Magento/Framework/Api/ExtensionAttributesInterface.php index 5578ed0137a62..b808fb5a22a38 100644 --- a/lib/internal/Magento/Framework/Api/ExtensionAttributesInterface.php +++ b/lib/internal/Magento/Framework/Api/ExtensionAttributesInterface.php @@ -10,6 +10,7 @@ * Marker interface for all extension attributes interfaces. * * @api + * @since 100.0.2 */ interface ExtensionAttributesInterface { diff --git a/lib/internal/Magento/Framework/Api/Filter.php b/lib/internal/Magento/Framework/Api/Filter.php index 03af29a00ad67..7e99a0038bd78 100644 --- a/lib/internal/Magento/Framework/Api/Filter.php +++ b/lib/internal/Magento/Framework/Api/Filter.php @@ -13,6 +13,7 @@ * * @api * @codeCoverageIgnore + * @since 100.0.2 */ class Filter extends AbstractSimpleObject { diff --git a/lib/internal/Magento/Framework/Api/FilterBuilder.php b/lib/internal/Magento/Framework/Api/FilterBuilder.php index 056cc07657deb..c1e7fee39390a 100644 --- a/lib/internal/Magento/Framework/Api/FilterBuilder.php +++ b/lib/internal/Magento/Framework/Api/FilterBuilder.php @@ -11,6 +11,7 @@ * * @api * @method Filter create() + * @since 100.0.2 */ class FilterBuilder extends AbstractSimpleObjectBuilder { diff --git a/lib/internal/Magento/Framework/Api/ImageContentValidatorInterface.php b/lib/internal/Magento/Framework/Api/ImageContentValidatorInterface.php index 686e47f7a933f..eecd7dbe8f19b 100644 --- a/lib/internal/Magento/Framework/Api/ImageContentValidatorInterface.php +++ b/lib/internal/Magento/Framework/Api/ImageContentValidatorInterface.php @@ -13,6 +13,7 @@ * Image content validation interface * * @api + * @since 100.0.2 */ interface ImageContentValidatorInterface { diff --git a/lib/internal/Magento/Framework/Api/ImageProcessorInterface.php b/lib/internal/Magento/Framework/Api/ImageProcessorInterface.php index 676e89974fe09..38e66fc9a1feb 100644 --- a/lib/internal/Magento/Framework/Api/ImageProcessorInterface.php +++ b/lib/internal/Magento/Framework/Api/ImageProcessorInterface.php @@ -13,6 +13,7 @@ * Interface ImageProcessorInterface * * @api + * @since 100.0.2 */ interface ImageProcessorInterface { diff --git a/lib/internal/Magento/Framework/Api/MetadataObjectInterface.php b/lib/internal/Magento/Framework/Api/MetadataObjectInterface.php index 64593d87f2a2d..a4a48fcbf7bb0 100644 --- a/lib/internal/Magento/Framework/Api/MetadataObjectInterface.php +++ b/lib/internal/Magento/Framework/Api/MetadataObjectInterface.php @@ -10,6 +10,7 @@ * Provides metadata about an attribute. * * @api + * @since 100.0.2 */ interface MetadataObjectInterface { diff --git a/lib/internal/Magento/Framework/Api/MetadataServiceInterface.php b/lib/internal/Magento/Framework/Api/MetadataServiceInterface.php index c6c3492420922..8993d7bc0003d 100644 --- a/lib/internal/Magento/Framework/Api/MetadataServiceInterface.php +++ b/lib/internal/Magento/Framework/Api/MetadataServiceInterface.php @@ -10,6 +10,7 @@ * MetadataService returns custom attribute metadata for a given class or interface it implements * * @api + * @since 100.0.2 */ interface MetadataServiceInterface { diff --git a/lib/internal/Magento/Framework/Api/Search/Document.php b/lib/internal/Magento/Framework/Api/Search/Document.php index 7454fa7974ece..c1edec9860f67 100644 --- a/lib/internal/Magento/Framework/Api/Search/Document.php +++ b/lib/internal/Magento/Framework/Api/Search/Document.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class Document extends AbstractSimpleObject implements DocumentInterface, \IteratorAggregate { diff --git a/lib/internal/Magento/Framework/Api/Search/FilterGroup.php b/lib/internal/Magento/Framework/Api/Search/FilterGroup.php index aef3df998a550..a8057d6d0d8b0 100644 --- a/lib/internal/Magento/Framework/Api/Search/FilterGroup.php +++ b/lib/internal/Magento/Framework/Api/Search/FilterGroup.php @@ -12,6 +12,7 @@ * Groups two or more filters together using a logical OR * * @api + * @since 100.0.2 */ class FilterGroup extends AbstractSimpleObject { diff --git a/lib/internal/Magento/Framework/Api/Search/FilterGroupBuilder.php b/lib/internal/Magento/Framework/Api/Search/FilterGroupBuilder.php index cfde284524482..64bb7431819b0 100644 --- a/lib/internal/Magento/Framework/Api/Search/FilterGroupBuilder.php +++ b/lib/internal/Magento/Framework/Api/Search/FilterGroupBuilder.php @@ -14,6 +14,7 @@ * Builder for FilterGroup Data. * * @api + * @since 100.0.2 */ class FilterGroupBuilder extends AbstractSimpleObjectBuilder { diff --git a/lib/internal/Magento/Framework/Api/Search/SearchCriteria.php b/lib/internal/Magento/Framework/Api/Search/SearchCriteria.php index 964e506120167..728b32ebfd06e 100644 --- a/lib/internal/Magento/Framework/Api/Search/SearchCriteria.php +++ b/lib/internal/Magento/Framework/Api/Search/SearchCriteria.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class SearchCriteria extends BaseSearchCriteria implements SearchCriteriaInterface { diff --git a/lib/internal/Magento/Framework/Api/Search/SearchCriteriaBuilder.php b/lib/internal/Magento/Framework/Api/Search/SearchCriteriaBuilder.php index 918a16a255729..ba443f116f652 100644 --- a/lib/internal/Magento/Framework/Api/Search/SearchCriteriaBuilder.php +++ b/lib/internal/Magento/Framework/Api/Search/SearchCriteriaBuilder.php @@ -14,6 +14,7 @@ * Builder for SearchCriteria Service Data Object * * @api + * @since 100.0.2 */ class SearchCriteriaBuilder extends AbstractSimpleObjectBuilder { diff --git a/lib/internal/Magento/Framework/Api/Search/SearchCriteriaInterface.php b/lib/internal/Magento/Framework/Api/Search/SearchCriteriaInterface.php index 6dda50569ca03..a61a33260fc5c 100644 --- a/lib/internal/Magento/Framework/Api/Search/SearchCriteriaInterface.php +++ b/lib/internal/Magento/Framework/Api/Search/SearchCriteriaInterface.php @@ -12,6 +12,7 @@ * * @api * @package Magento\Framework\Api\Search + * @since 100.0.2 */ interface SearchCriteriaInterface extends BaseSearchCriteriaInterface { diff --git a/lib/internal/Magento/Framework/Api/Search/SearchInterface.php b/lib/internal/Magento/Framework/Api/Search/SearchInterface.php index 9793e005b70b5..0b161b04561e8 100644 --- a/lib/internal/Magento/Framework/Api/Search/SearchInterface.php +++ b/lib/internal/Magento/Framework/Api/Search/SearchInterface.php @@ -9,6 +9,7 @@ * Search API for all requests * * @api + * @since 100.0.2 */ interface SearchInterface { diff --git a/lib/internal/Magento/Framework/Api/Search/SearchResultInterface.php b/lib/internal/Magento/Framework/Api/Search/SearchResultInterface.php index 792401124b96e..92941d9a9c025 100644 --- a/lib/internal/Magento/Framework/Api/Search/SearchResultInterface.php +++ b/lib/internal/Magento/Framework/Api/Search/SearchResultInterface.php @@ -11,6 +11,7 @@ * Interface SearchResultInterface * * @api + * @since 100.0.2 */ interface SearchResultInterface extends SearchResultsInterface { diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CustomFilterInterface.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CustomFilterInterface.php index c068970c93b12..a076348262a61 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CustomFilterInterface.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/FilterProcessor/CustomFilterInterface.php @@ -10,7 +10,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ interface CustomFilterInterface { @@ -20,7 +20,7 @@ interface CustomFilterInterface * @param Filter $filter * @param AbstractDb $collection * @return bool Whether the filter was applied - * @since 100.2.0 + * @since 101.0.0 */ public function apply(Filter $filter, AbstractDb $collection); } diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor/CustomJoinInterface.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor/CustomJoinInterface.php index 4ca55b6a1a72d..0fd4f077af984 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor/CustomJoinInterface.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessor/JoinProcessor/CustomJoinInterface.php @@ -9,7 +9,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ interface CustomJoinInterface { @@ -18,7 +18,7 @@ interface CustomJoinInterface * * @param AbstractDb $collection * @return bool - * @since 100.2.0 + * @since 101.0.0 */ public function apply(AbstractDb $collection); } diff --git a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessorInterface.php b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessorInterface.php index 722e1b96254d0..a87a92d8d9dfa 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessorInterface.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteria/CollectionProcessorInterface.php @@ -10,7 +10,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ interface CollectionProcessorInterface { @@ -21,7 +21,7 @@ interface CollectionProcessorInterface * @param AbstractDb $collection * @throws \InvalidArgumentException * @return void - * @since 100.2.0 + * @since 101.0.0 */ public function process(SearchCriteriaInterface $searchCriteria, AbstractDb $collection); } diff --git a/lib/internal/Magento/Framework/Api/SearchCriteriaInterface.php b/lib/internal/Magento/Framework/Api/SearchCriteriaInterface.php index ae4057ae9c21e..45397b6a195c2 100644 --- a/lib/internal/Magento/Framework/Api/SearchCriteriaInterface.php +++ b/lib/internal/Magento/Framework/Api/SearchCriteriaInterface.php @@ -10,6 +10,7 @@ * Search criteria interface. * * @api + * @since 100.0.2 */ interface SearchCriteriaInterface { diff --git a/lib/internal/Magento/Framework/Api/SearchResultsInterface.php b/lib/internal/Magento/Framework/Api/SearchResultsInterface.php index ba72685a80f49..c29f1134e0706 100644 --- a/lib/internal/Magento/Framework/Api/SearchResultsInterface.php +++ b/lib/internal/Magento/Framework/Api/SearchResultsInterface.php @@ -11,6 +11,7 @@ * Search results interface. * * @api + * @since 100.0.2 */ interface SearchResultsInterface { diff --git a/lib/internal/Magento/Framework/Api/SortOrder.php b/lib/internal/Magento/Framework/Api/SortOrder.php index 67897ea22570d..b2da7d180395d 100644 --- a/lib/internal/Magento/Framework/Api/SortOrder.php +++ b/lib/internal/Magento/Framework/Api/SortOrder.php @@ -13,6 +13,7 @@ * Data object for sort order. * * @api + * @since 100.0.2 */ class SortOrder extends AbstractSimpleObject { diff --git a/lib/internal/Magento/Framework/Api/SortOrderBuilder.php b/lib/internal/Magento/Framework/Api/SortOrderBuilder.php index 6960440d4d522..6b3365ae5bf9c 100644 --- a/lib/internal/Magento/Framework/Api/SortOrderBuilder.php +++ b/lib/internal/Magento/Framework/Api/SortOrderBuilder.php @@ -11,6 +11,7 @@ * @method SortOrder create() * * @api + * @since 100.0.2 */ class SortOrderBuilder extends AbstractSimpleObjectBuilder { diff --git a/lib/internal/Magento/Framework/App/Action/AbstractAction.php b/lib/internal/Magento/Framework/App/Action/AbstractAction.php index 27f8838f9b165..f4c11203f2760 100644 --- a/lib/internal/Magento/Framework/App/Action/AbstractAction.php +++ b/lib/internal/Magento/Framework/App/Action/AbstractAction.php @@ -11,7 +11,7 @@ /** * Abstract redirect/forward action class * - * @deprecated Inheritance in controllers should be avoided in favor of composition + * @deprecated 103.0.0 Inheritance in controllers should be avoided in favor of composition * @see \Magento\Framework\App\ActionInterface */ abstract class AbstractAction implements \Magento\Framework\App\ActionInterface diff --git a/lib/internal/Magento/Framework/App/Action/Action.php b/lib/internal/Magento/Framework/App/Action/Action.php index 9d9b26313d7c6..b06c9799ddc04 100644 --- a/lib/internal/Magento/Framework/App/Action/Action.php +++ b/lib/internal/Magento/Framework/App/Action/Action.php @@ -23,13 +23,14 @@ * It contains standard action behavior (event dispatching, flag checks) * Action classes that do not extend from this class will lose this behavior and might not function correctly * - * @deprecated Inheritance in controllers should be avoided in favor of composition + * @deprecated 103.0.0 Inheritance in controllers should be avoided in favor of composition * @see \Magento\Framework\App\ActionInterface * * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.NumberOfChildren) + * @since 100.0.2 */ abstract class Action extends AbstractAction { diff --git a/lib/internal/Magento/Framework/App/Action/Context.php b/lib/internal/Magento/Framework/App/Action/Context.php index 5f5f013f454f2..1d2f04a23035b 100644 --- a/lib/internal/Magento/Framework/App/Action/Context.php +++ b/lib/internal/Magento/Framework/App/Action/Context.php @@ -19,6 +19,7 @@ * the classes they were introduced for. * * @api + * @since 100.0.2 */ class Context implements \Magento\Framework\ObjectManager\ContextInterface { diff --git a/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php b/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php index 389bd8089967b..6d85e0b80ee55 100644 --- a/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php +++ b/lib/internal/Magento/Framework/App/Action/HttpHeadActionInterface.php @@ -13,7 +13,7 @@ /** * Marker for actions processing HEAD requests. * - * @deprecated Both GET and HEAD requests map to HttpGetActionInterface + * @deprecated 102.0.2 Both GET and HEAD requests map to HttpGetActionInterface */ interface HttpHeadActionInterface extends ActionInterface { diff --git a/lib/internal/Magento/Framework/App/ActionFactory.php b/lib/internal/Magento/Framework/App/ActionFactory.php index 94f5ef36eb9c9..4edf022e43509 100644 --- a/lib/internal/Magento/Framework/App/ActionFactory.php +++ b/lib/internal/Magento/Framework/App/ActionFactory.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class ActionFactory { diff --git a/lib/internal/Magento/Framework/App/ActionFlag.php b/lib/internal/Magento/Framework/App/ActionFlag.php index 55201504c968f..3d6c2756595ad 100644 --- a/lib/internal/Magento/Framework/App/ActionFlag.php +++ b/lib/internal/Magento/Framework/App/ActionFlag.php @@ -13,6 +13,7 @@ * Please use plugins to prevent action dispatching instead. * * @api + * @since 100.0.2 */ class ActionFlag { diff --git a/lib/internal/Magento/Framework/App/ActionInterface.php b/lib/internal/Magento/Framework/App/ActionInterface.php index 1501abe9eb956..12e87ad9f4f96 100644 --- a/lib/internal/Magento/Framework/App/ActionInterface.php +++ b/lib/internal/Magento/Framework/App/ActionInterface.php @@ -9,6 +9,7 @@ * Magento application action controller type. Every action controller in Application should implement this interface. * * @api + * @since 100.0.2 */ interface ActionInterface { diff --git a/lib/internal/Magento/Framework/App/Area/FrontNameResolverFactory.php b/lib/internal/Magento/Framework/App/Area/FrontNameResolverFactory.php index 6a4f79dfefe82..fb46ec621b4f8 100644 --- a/lib/internal/Magento/Framework/App/Area/FrontNameResolverFactory.php +++ b/lib/internal/Magento/Framework/App/Area/FrontNameResolverFactory.php @@ -12,6 +12,7 @@ * Keeping it for backward compatibility * * @api + * @since 100.0.2 */ class FrontNameResolverFactory { diff --git a/lib/internal/Magento/Framework/App/Area/FrontNameResolverInterface.php b/lib/internal/Magento/Framework/App/Area/FrontNameResolverInterface.php index fcdee6276c63c..960c933042e62 100644 --- a/lib/internal/Magento/Framework/App/Area/FrontNameResolverInterface.php +++ b/lib/internal/Magento/Framework/App/Area/FrontNameResolverInterface.php @@ -17,6 +17,7 @@ * for areas with dynamic front names. * * @api + * @since 100.0.2 */ interface FrontNameResolverInterface { diff --git a/lib/internal/Magento/Framework/App/AreaList.php b/lib/internal/Magento/Framework/App/AreaList.php index ce7ef5391cea4..f98fbec12a134 100644 --- a/lib/internal/Magento/Framework/App/AreaList.php +++ b/lib/internal/Magento/Framework/App/AreaList.php @@ -7,6 +7,9 @@ */ namespace Magento\Framework\App; +/** + * Lists router area codes & processes resolves FrontEndNames to area codes + */ class AreaList { /** @@ -72,7 +75,7 @@ public function getCodeByFrontName($frontName) $resolver = $this->_resolverFactory->create($areaInfo['frontNameResolver']); $areaInfo['frontName'] = $resolver->getFrontName(true); } - if (isset($areaInfo['frontName']) && $areaInfo['frontName'] == $frontName) { + if (isset($areaInfo['frontName']) && $areaInfo['frontName'] === $frontName) { return $areaCode; } } diff --git a/lib/internal/Magento/Framework/App/Bootstrap.php b/lib/internal/Magento/Framework/App/Bootstrap.php index 500eebcf9a7a7..93d5535d0e10e 100644 --- a/lib/internal/Magento/Framework/App/Bootstrap.php +++ b/lib/internal/Magento/Framework/App/Bootstrap.php @@ -23,6 +23,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Bootstrap { diff --git a/lib/internal/Magento/Framework/App/Cache/Manager.php b/lib/internal/Magento/Framework/App/Cache/Manager.php index 7ac53d5d6e070..b3531f6e0d163 100644 --- a/lib/internal/Magento/Framework/App/Cache/Manager.php +++ b/lib/internal/Magento/Framework/App/Cache/Manager.php @@ -12,6 +12,7 @@ * Cache status manager * * @api + * @since 100.0.2 */ class Manager { diff --git a/lib/internal/Magento/Framework/App/Cache/StateInterface.php b/lib/internal/Magento/Framework/App/Cache/StateInterface.php index c9f6db3dd8899..1f0b2fa6e00d5 100644 --- a/lib/internal/Magento/Framework/App/Cache/StateInterface.php +++ b/lib/internal/Magento/Framework/App/Cache/StateInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface StateInterface { diff --git a/lib/internal/Magento/Framework/App/Cache/Type/FrontendPool.php b/lib/internal/Magento/Framework/App/Cache/Type/FrontendPool.php index 596fd833ab1ed..b89ba7067d725 100644 --- a/lib/internal/Magento/Framework/App/Cache/Type/FrontendPool.php +++ b/lib/internal/Magento/Framework/App/Cache/Type/FrontendPool.php @@ -12,6 +12,7 @@ * In-memory readonly pool of cache front-ends with enforced access control, specific to cache types * * @api + * @since 100.0.2 */ class FrontendPool { diff --git a/lib/internal/Magento/Framework/App/Cache/TypeListInterface.php b/lib/internal/Magento/Framework/App/Cache/TypeListInterface.php index 8f18f5b57d6cc..c379133e94a3b 100644 --- a/lib/internal/Magento/Framework/App/Cache/TypeListInterface.php +++ b/lib/internal/Magento/Framework/App/Cache/TypeListInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface TypeListInterface { diff --git a/lib/internal/Magento/Framework/App/CacheInterface.php b/lib/internal/Magento/Framework/App/CacheInterface.php index 374fb3cf68936..9052d93191929 100644 --- a/lib/internal/Magento/Framework/App/CacheInterface.php +++ b/lib/internal/Magento/Framework/App/CacheInterface.php @@ -10,6 +10,7 @@ * System cache model interface * * @api + * @since 100.0.2 */ interface CacheInterface { diff --git a/lib/internal/Magento/Framework/App/Config/Data/ProcessorInterface.php b/lib/internal/Magento/Framework/App/Config/Data/ProcessorInterface.php index 83ff73ab387a9..7d0b53cf1bc12 100644 --- a/lib/internal/Magento/Framework/App/Config/Data/ProcessorInterface.php +++ b/lib/internal/Magento/Framework/App/Config/Data/ProcessorInterface.php @@ -9,6 +9,7 @@ * Processes data from admin store configuration fields * * @api + * @since 100.0.2 */ interface ProcessorInterface { diff --git a/lib/internal/Magento/Framework/App/Config/DataInterface.php b/lib/internal/Magento/Framework/App/Config/DataInterface.php index 6495bbf0bc4da..e267e7311be1a 100644 --- a/lib/internal/Magento/Framework/App/Config/DataInterface.php +++ b/lib/internal/Magento/Framework/App/Config/DataInterface.php @@ -9,6 +9,7 @@ * Configuration data storage * * @api + * @since 100.0.2 */ interface DataInterface { diff --git a/lib/internal/Magento/Framework/App/Config/Element.php b/lib/internal/Magento/Framework/App/Config/Element.php index e5b4a5c384513..d714783847dc5 100644 --- a/lib/internal/Magento/Framework/App/Config/Element.php +++ b/lib/internal/Magento/Framework/App/Config/Element.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Element extends \Magento\Framework\Simplexml\Element { diff --git a/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php b/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php index 50a250f9c6591..79305e651eeaa 100644 --- a/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php +++ b/lib/internal/Magento/Framework/App/Config/InitialConfigSource.php @@ -25,7 +25,7 @@ class InitialConfigSource implements ConfigSourceInterface /** * @var string - * @deprecated 100.2.0 Initial configs can not be separated since 2.2.0 version + * @deprecated 101.0.0 Initial configs can not be separated since 2.2.0 version */ private $fileKey; diff --git a/lib/internal/Magento/Framework/App/Config/MutableScopeConfigInterface.php b/lib/internal/Magento/Framework/App/Config/MutableScopeConfigInterface.php index 32d081b853f40..15c7530e80373 100644 --- a/lib/internal/Magento/Framework/App/Config/MutableScopeConfigInterface.php +++ b/lib/internal/Magento/Framework/App/Config/MutableScopeConfigInterface.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ interface MutableScopeConfigInterface extends \Magento\Framework\App\Config\ScopeConfigInterface { diff --git a/lib/internal/Magento/Framework/App/Config/ReinitableConfigInterface.php b/lib/internal/Magento/Framework/App/Config/ReinitableConfigInterface.php index 7974f97ef1ffc..f180a4ce401b1 100644 --- a/lib/internal/Magento/Framework/App/Config/ReinitableConfigInterface.php +++ b/lib/internal/Magento/Framework/App/Config/ReinitableConfigInterface.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ interface ReinitableConfigInterface extends \Magento\Framework\App\Config\MutableScopeConfigInterface { diff --git a/lib/internal/Magento/Framework/App/Config/Scope/Validator.php b/lib/internal/Magento/Framework/App/Config/Scope/Validator.php index 9c0f60286e093..df9807ac55c79 100644 --- a/lib/internal/Magento/Framework/App/Config/Scope/Validator.php +++ b/lib/internal/Magento/Framework/App/Config/Scope/Validator.php @@ -15,7 +15,7 @@ use Magento\Framework\Phrase; /** - * @deprecated 100.2.0 Added in order to avoid backward incompatibility because class was moved to another directory. + * @deprecated 101.0.0 Added in order to avoid backward incompatibility because class was moved to another directory. * @see \Magento\Framework\App\Scope\Validator */ class Validator implements ValidatorInterface diff --git a/lib/internal/Magento/Framework/App/Config/ScopeConfigInterface.php b/lib/internal/Magento/Framework/App/Config/ScopeConfigInterface.php index 122e6801ed187..cdac6f3552eea 100644 --- a/lib/internal/Magento/Framework/App/Config/ScopeConfigInterface.php +++ b/lib/internal/Magento/Framework/App/Config/ScopeConfigInterface.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ interface ScopeConfigInterface { diff --git a/lib/internal/Magento/Framework/App/Config/Storage/WriterInterface.php b/lib/internal/Magento/Framework/App/Config/Storage/WriterInterface.php index 35b24692bd228..c1e20cf332023 100644 --- a/lib/internal/Magento/Framework/App/Config/Storage/WriterInterface.php +++ b/lib/internal/Magento/Framework/App/Config/Storage/WriterInterface.php @@ -12,6 +12,7 @@ /** * Interface \Magento\Framework\App\Config\Storage\WriterInterface * @api + * @since 100.0.2 */ interface WriterInterface { diff --git a/lib/internal/Magento/Framework/App/Config/Value.php b/lib/internal/Magento/Framework/App/Config/Value.php index 6fde4dded4695..c18da34f86a26 100644 --- a/lib/internal/Magento/Framework/App/Config/Value.php +++ b/lib/internal/Magento/Framework/App/Config/Value.php @@ -24,6 +24,7 @@ * @api * * @SuppressWarnings(PHPMD.NumberOfChildren) + * @since 100.0.2 */ class Value extends \Magento\Framework\Model\AbstractModel implements \Magento\Framework\App\Config\ValueInterface { @@ -134,6 +135,7 @@ public function afterSave() * {@inheritdoc}. In addition, it sets status 'invalidate' for config caches * * @return $this + * @since 100.1.0 */ public function afterDelete() { diff --git a/lib/internal/Magento/Framework/App/DocRootLocator.php b/lib/internal/Magento/Framework/App/DocRootLocator.php index d73baf8e4e742..698001044bdf3 100644 --- a/lib/internal/Magento/Framework/App/DocRootLocator.php +++ b/lib/internal/Magento/Framework/App/DocRootLocator.php @@ -22,7 +22,7 @@ class DocRootLocator private $request; /** - * @deprecated + * @deprecated 102.0.2 * @var ReadFactory */ private $readFactory; diff --git a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php index 4b10a503f423c..6caf2c0f88dfa 100644 --- a/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php +++ b/lib/internal/Magento/Framework/App/Filesystem/DirectoryList.php @@ -57,6 +57,11 @@ class DirectoryList extends \Magento\Framework\Filesystem\DirectoryList */ const VAR_DIR = 'var'; + /** + * Storage of files which was exported. + */ + const VAR_EXPORT = 'var_export'; + /** * Temporary files */ @@ -136,7 +141,7 @@ class DirectoryList extends \Magento\Framework\Filesystem\DirectoryList const GENERATED_METADATA = 'metadata'; /** - * {@inheritdoc} + * @inheritdoc */ public static function getDefaultConfig() { @@ -146,6 +151,7 @@ public static function getDefaultConfig() self::CONFIG => [parent::PATH => 'app/etc'], self::LIB_INTERNAL => [parent::PATH => 'lib/internal'], self::VAR_DIR => [parent::PATH => 'var'], + self::VAR_EXPORT => [parent::PATH => 'var/export'], self::CACHE => [parent::PATH => 'var/cache'], self::LOG => [parent::PATH => 'var/log'], self::DI => [parent::PATH => 'generated/metadata'], @@ -169,7 +175,7 @@ public static function getDefaultConfig() } /** - * {@inheritdoc} + * @inheritdoc */ public function __construct($root, array $config = []) { diff --git a/lib/internal/Magento/Framework/App/FrontControllerInterface.php b/lib/internal/Magento/Framework/App/FrontControllerInterface.php index afd3091097d19..712f3876355c1 100644 --- a/lib/internal/Magento/Framework/App/FrontControllerInterface.php +++ b/lib/internal/Magento/Framework/App/FrontControllerInterface.php @@ -11,6 +11,7 @@ * Every application area has own front controller. * * @api + * @since 100.0.2 */ interface FrontControllerInterface { diff --git a/lib/internal/Magento/Framework/App/Language/Dictionary.php b/lib/internal/Magento/Framework/App/Language/Dictionary.php index b4a6fdc1b5ce3..42e3f7f55018e 100644 --- a/lib/internal/Magento/Framework/App/Language/Dictionary.php +++ b/lib/internal/Magento/Framework/App/Language/Dictionary.php @@ -13,6 +13,7 @@ * A service for reading language package dictionaries * * @api + * @since 100.0.2 */ class Dictionary { diff --git a/lib/internal/Magento/Framework/App/ObjectManager.php b/lib/internal/Magento/Framework/App/ObjectManager.php index 1cc1745c3a57f..f18102a1dbc78 100644 --- a/lib/internal/Magento/Framework/App/ObjectManager.php +++ b/lib/internal/Magento/Framework/App/ObjectManager.php @@ -15,6 +15,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class ObjectManager extends \Magento\Framework\ObjectManager\ObjectManager { diff --git a/lib/internal/Magento/Framework/App/ObjectManager/ConfigCache.php b/lib/internal/Magento/Framework/App/ObjectManager/ConfigCache.php index 0df11cb3cb6e1..846dd6011c732 100644 --- a/lib/internal/Magento/Framework/App/ObjectManager/ConfigCache.php +++ b/lib/internal/Magento/Framework/App/ObjectManager/ConfigCache.php @@ -68,7 +68,7 @@ public function save(array $config, $key) * Get serializer * * @return SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php b/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php index 6abf2aca8d641..1d73bdf4a9956 100644 --- a/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php +++ b/lib/internal/Magento/Framework/App/ObjectManager/ConfigLoader.php @@ -86,7 +86,7 @@ public function load($area) * Get serializer * * @return SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/App/ObjectManagerFactory.php b/lib/internal/Magento/Framework/App/ObjectManagerFactory.php index b781a92b4714a..0c468a767ded2 100644 --- a/lib/internal/Magento/Framework/App/ObjectManagerFactory.php +++ b/lib/internal/Magento/Framework/App/ObjectManagerFactory.php @@ -19,6 +19,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class ObjectManagerFactory { @@ -294,7 +295,7 @@ protected function _loadPrimaryConfig(DirectoryList $directoryList, $driverPool, * @param \Magento\Framework\ObjectManager\Config\Config $diConfig * @param \Magento\Framework\ObjectManager\DefinitionInterface $definitions * @return \Magento\Framework\Interception\PluginList\PluginList - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ protected function _createPluginList( diff --git a/lib/internal/Magento/Framework/App/PlainTextRequestInterface.php b/lib/internal/Magento/Framework/App/PlainTextRequestInterface.php index c986a2309888a..fbcabd15adba8 100644 --- a/lib/internal/Magento/Framework/App/PlainTextRequestInterface.php +++ b/lib/internal/Magento/Framework/App/PlainTextRequestInterface.php @@ -13,7 +13,7 @@ * To read already parsed request data use \Magento\Framework\App\RequestInterface. * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface PlainTextRequestInterface { @@ -21,7 +21,7 @@ interface PlainTextRequestInterface * Returns textual representation of request to Magento. * * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getContent(); } diff --git a/lib/internal/Magento/Framework/App/ProductMetadataInterface.php b/lib/internal/Magento/Framework/App/ProductMetadataInterface.php index 4d55092d45e03..a62b06077a842 100644 --- a/lib/internal/Magento/Framework/App/ProductMetadataInterface.php +++ b/lib/internal/Magento/Framework/App/ProductMetadataInterface.php @@ -9,6 +9,7 @@ * Magento application product metadata * * @api + * @since 100.0.2 */ interface ProductMetadataInterface { diff --git a/lib/internal/Magento/Framework/App/ReinitableConfig.php b/lib/internal/Magento/Framework/App/ReinitableConfig.php index 70bc82229995d..09ba71a279636 100644 --- a/lib/internal/Magento/Framework/App/ReinitableConfig.php +++ b/lib/internal/Magento/Framework/App/ReinitableConfig.php @@ -9,7 +9,7 @@ /** * @inheritdoc - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class ReinitableConfig extends MutableScopeConfig implements ReinitableConfigInterface { diff --git a/lib/internal/Magento/Framework/App/Request/Http.php b/lib/internal/Magento/Framework/App/Request/Http.php index 5fc4716f4bbf8..961e871fbfa91 100644 --- a/lib/internal/Magento/Framework/App/Request/Http.php +++ b/lib/internal/Magento/Framework/App/Request/Http.php @@ -200,12 +200,7 @@ public function isDirectAccessFrontendName($code) public function getBasePath() { $path = parent::getBasePath(); - if (empty($path)) { - $path = '/'; - } else { - $path = str_replace('\\', '/', $path); - } - return $path; + return empty($path) ? '/' : str_replace('\\', '/', $path); } /** @@ -298,10 +293,9 @@ public function getBeforeForwardInfo($name = null) { if ($name === null) { return $this->beforeForwardInfo; - } elseif (isset($this->beforeForwardInfo[$name])) { - return $this->beforeForwardInfo[$name]; } - return null; + + return $this->beforeForwardInfo[$name] ?? null; } /** @@ -311,13 +305,9 @@ public function getBeforeForwardInfo($name = null) */ public function isAjax() { - if ($this->isXmlHttpRequest()) { - return true; - } - if ($this->getParam('ajax') || $this->getParam('isAjax')) { - return true; - } - return false; + return $this->isXmlHttpRequest() + || $this->getParam('ajax') + || $this->getParam('isAjax'); } /** @@ -365,7 +355,7 @@ public static function getDistroBaseUrlPath($server) $result = ''; if (isset($server['SCRIPT_NAME'])) { $envPath = str_replace('\\', '/', dirname(str_replace('\\', '/', $server['SCRIPT_NAME']))); - if ($envPath != '.' && $envPath != '/') { + if ($envPath !== '.' && $envPath !== '/') { $result = $envPath; } } diff --git a/lib/internal/Magento/Framework/App/Request/PathInfoProcessorInterface.php b/lib/internal/Magento/Framework/App/Request/PathInfoProcessorInterface.php index 60d867cb4388f..3c2896569c199 100644 --- a/lib/internal/Magento/Framework/App/Request/PathInfoProcessorInterface.php +++ b/lib/internal/Magento/Framework/App/Request/PathInfoProcessorInterface.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ interface PathInfoProcessorInterface { diff --git a/lib/internal/Magento/Framework/App/RequestContentInterface.php b/lib/internal/Magento/Framework/App/RequestContentInterface.php index 90848f34ccd66..29e26e0702163 100644 --- a/lib/internal/Magento/Framework/App/RequestContentInterface.php +++ b/lib/internal/Magento/Framework/App/RequestContentInterface.php @@ -11,7 +11,7 @@ * Direct usage of RequestInterface and PlainTextRequestInterface is preferable. * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface RequestContentInterface extends RequestInterface, PlainTextRequestInterface { diff --git a/lib/internal/Magento/Framework/App/RequestInterface.php b/lib/internal/Magento/Framework/App/RequestInterface.php index 7abcc9208af5c..a830d46978b84 100644 --- a/lib/internal/Magento/Framework/App/RequestInterface.php +++ b/lib/internal/Magento/Framework/App/RequestInterface.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ interface RequestInterface { diff --git a/lib/internal/Magento/Framework/App/RequestSafetyInterface.php b/lib/internal/Magento/Framework/App/RequestSafetyInterface.php index f56ff9aaf8ab5..56d6312a60c7c 100644 --- a/lib/internal/Magento/Framework/App/RequestSafetyInterface.php +++ b/lib/internal/Magento/Framework/App/RequestSafetyInterface.php @@ -10,6 +10,7 @@ * Request safety check. Can be used to identify if current application request is safe (does not modify state) or not. * * @api + * @since 100.0.2 */ interface RequestSafetyInterface { diff --git a/lib/internal/Magento/Framework/App/ResourceConnection.php b/lib/internal/Magento/Framework/App/ResourceConnection.php index b543cc970f640..00dc88dcd7b17 100644 --- a/lib/internal/Magento/Framework/App/ResourceConnection.php +++ b/lib/internal/Magento/Framework/App/ResourceConnection.php @@ -1,8 +1,11 @@ <?php + /** * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\App; use Magento\Framework\App\ResourceConnection\ConfigInterface as ResourceConfigInterface; @@ -15,16 +18,14 @@ * This class provides access to all these connections. * * @api + * @since 100.0.2 */ class ResourceConnection { - const AUTO_UPDATE_ONCE = 0; - - const AUTO_UPDATE_NEVER = -1; - - const AUTO_UPDATE_ALWAYS = 1; - - const DEFAULT_CONNECTION = 'default'; + public const AUTO_UPDATE_ONCE = 0; + public const AUTO_UPDATE_NEVER = -1; + public const AUTO_UPDATE_ALWAYS = 1; + public const DEFAULT_CONNECTION = 'default'; /** * Instances of actual connections. @@ -93,8 +94,7 @@ public function __construct( public function getConnection($resourceName = self::DEFAULT_CONNECTION) { $connectionName = $this->config->getConnectionName($resourceName); - $connection = $this->getConnectionByName($connectionName); - return $connection; + return $this->getConnectionByName($connectionName); } /** @@ -160,15 +160,15 @@ public function getConnectionByName($connectionName) */ private function getProcessConnectionName($connectionName) { - return $connectionName . '_process_' . getmypid(); + return $connectionName . '_process_' . getmypid(); } /** * Get resource table name, validated by db adapter. * - * @param string|string[] $modelEntity + * @param string|string[] $modelEntity * @param string $connectionName - * @return string + * @return string * @api */ public function getTableName($modelEntity, $connectionName = self::DEFAULT_CONNECTION) @@ -212,9 +212,9 @@ public function getTablePlaceholder($tableName) /** * Build a trigger name. * - * @param string $tableName The table that is the subject of the trigger - * @param string $time Either "before" or "after" - * @param string $event The DB level event which activates the trigger, i.e. "update" or "insert" + * @param string $tableName The table that is the subject of the trigger + * @param string $time Either "before" or "after" + * @param string $event The DB level event which activates the trigger, i.e. "update" or "insert" * @return string */ public function getTriggerName($tableName, $time, $event) @@ -275,9 +275,9 @@ public function getIdxName( /** * Retrieve 32bit UNIQUE HASH for a Table foreign key. * - * @param string $priTableName the target table name + * @param string $priTableName the target table name * @param string $priColumnName the target table column name - * @param string $refTableName the reference table name + * @param string $refTableName the reference table name * @param string $refColumnName the reference table column name * @return string */ @@ -298,14 +298,12 @@ public function getFkName($priTableName, $priColumnName, $refTableName, $refColu * * @param string $resourceName * @return string + * @since 102.0.0 */ public function getSchemaName($resourceName) { return $this->deploymentConfig->get( - ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . - '/' . - $resourceName . - '/dbname' + ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTIONS . '/' . $resourceName . '/dbname' ); } @@ -320,8 +318,6 @@ public function getTablePrefix() return $this->tablePrefix; } - return (string) $this->deploymentConfig->get( - ConfigOptionsListConstants::CONFIG_PATH_DB_PREFIX - ); + return (string)$this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_PREFIX); } } diff --git a/lib/internal/Magento/Framework/App/ResourceConnection/SourceProviderInterface.php b/lib/internal/Magento/Framework/App/ResourceConnection/SourceProviderInterface.php index f8ff407d4507e..0a67b4cac9785 100644 --- a/lib/internal/Magento/Framework/App/ResourceConnection/SourceProviderInterface.php +++ b/lib/internal/Magento/Framework/App/ResourceConnection/SourceProviderInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface SourceProviderInterface extends \Traversable { diff --git a/lib/internal/Magento/Framework/App/Response/HttpInterface.php b/lib/internal/Magento/Framework/App/Response/HttpInterface.php index 17825aeb88d65..f18978b4fcaf0 100644 --- a/lib/internal/Magento/Framework/App/Response/HttpInterface.php +++ b/lib/internal/Magento/Framework/App/Response/HttpInterface.php @@ -9,6 +9,7 @@ * HTTP response interface * * @api + * @since 100.0.2 */ interface HttpInterface extends \Magento\Framework\App\ResponseInterface { @@ -24,7 +25,7 @@ public function setHttpResponseCode($code); * Get HTTP response code * * @return int - * @since 100.2.0 + * @since 101.0.0 */ public function getHttpResponseCode(); @@ -37,7 +38,7 @@ public function getHttpResponseCode(); * @param string $value * @param boolean $replace * @return self - * @since 100.2.0 + * @since 101.0.0 */ public function setHeader($name, $value, $replace = false); @@ -49,7 +50,7 @@ public function setHeader($name, $value, $replace = false); * * @param string $name * @return \Laminas\Http\Header\HeaderInterface|bool - * @since 100.2.0 + * @since 101.0.0 */ public function getHeader($name); @@ -58,7 +59,7 @@ public function getHeader($name); * * @param string $name * @return self - * @since 100.2.0 + * @since 101.0.0 */ public function clearHeader($name); @@ -76,7 +77,7 @@ public function clearHeader($name); * @param null|int|string $version * @param null|string $phrase * @return self - * @since 100.2.0 + * @since 101.0.0 */ public function setStatusHeader($httpCode, $version = null, $phrase = null); @@ -85,7 +86,7 @@ public function setStatusHeader($httpCode, $version = null, $phrase = null); * * @param string $value * @return self - * @since 100.2.0 + * @since 101.0.0 */ public function appendBody($value); @@ -96,7 +97,7 @@ public function appendBody($value); * * @param string $value * @return self - * @since 100.2.0 + * @since 101.0.0 */ public function setBody($value); @@ -108,7 +109,7 @@ public function setBody($value); * @param string $url * @param int $code * @return self - * @since 100.2.0 + * @since 101.0.0 */ public function setRedirect($url, $code = 302); } diff --git a/lib/internal/Magento/Framework/App/ResponseInterface.php b/lib/internal/Magento/Framework/App/ResponseInterface.php index f55e2cbaa2c1c..98633720d3ba4 100644 --- a/lib/internal/Magento/Framework/App/ResponseInterface.php +++ b/lib/internal/Magento/Framework/App/ResponseInterface.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ interface ResponseInterface { diff --git a/lib/internal/Magento/Framework/App/Route/Config.php b/lib/internal/Magento/Framework/App/Route/Config.php index b8bcd75482d28..787fe6363aa07 100644 --- a/lib/internal/Magento/Framework/App/Route/Config.php +++ b/lib/internal/Magento/Framework/App/Route/Config.php @@ -153,7 +153,7 @@ public function getModulesByFrontName($frontName, $scope = null) * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/App/Route/ConfigInterface.php b/lib/internal/Magento/Framework/App/Route/ConfigInterface.php index 88b1c42261fc7..26d1a6bf30ac0 100644 --- a/lib/internal/Magento/Framework/App/Route/ConfigInterface.php +++ b/lib/internal/Magento/Framework/App/Route/ConfigInterface.php @@ -9,6 +9,7 @@ * Routes configuration interface * * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/App/Rss/DataProviderInterface.php b/lib/internal/Magento/Framework/App/Rss/DataProviderInterface.php index 2c61c855945f2..4c20469ea9f59 100644 --- a/lib/internal/Magento/Framework/App/Rss/DataProviderInterface.php +++ b/lib/internal/Magento/Framework/App/Rss/DataProviderInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface DataProviderInterface { diff --git a/lib/internal/Magento/Framework/App/ScopeInterface.php b/lib/internal/Magento/Framework/App/ScopeInterface.php index 5821bf2aafa2a..81de45c5d9240 100644 --- a/lib/internal/Magento/Framework/App/ScopeInterface.php +++ b/lib/internal/Magento/Framework/App/ScopeInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface ScopeInterface { diff --git a/lib/internal/Magento/Framework/App/State.php b/lib/internal/Magento/Framework/App/State.php index 5d6ebaa2cc070..bc2b85b37442b 100644 --- a/lib/internal/Magento/Framework/App/State.php +++ b/lib/internal/Magento/Framework/App/State.php @@ -12,6 +12,7 @@ * Note: Area code communication and emulation will be removed from this class. * * @api + * @since 100.0.2 */ class State { @@ -219,7 +220,7 @@ private function checkAreaCode($areaCode) * Get Instance of AreaList * * @return AreaList - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getAreaListInstance() { diff --git a/lib/internal/Magento/Framework/App/StaticResource.php b/lib/internal/Magento/Framework/App/StaticResource.php index 86b2b15d3c446..9befb1e0fa9e5 100644 --- a/lib/internal/Magento/Framework/App/StaticResource.php +++ b/lib/internal/Magento/Framework/App/StaticResource.php @@ -124,17 +124,28 @@ public function launch() ) ) { $this->response->setHttpResponseCode(404); - } else { - $path = $this->request->get('resource'); + return $this->response; + } + + $path = $this->request->get('resource'); + try { $params = $this->parsePath($path); - $this->state->setAreaCode($params['area']); - $this->objectManager->configure($this->configLoader->load($params['area'])); - $file = $params['file']; - unset($params['file']); - $asset = $this->assetRepo->createAsset($file, $params); - $this->response->setFilePath($asset->getSourceFile()); - $this->publisher->publish($asset); + } catch (\InvalidArgumentException $e) { + if ($appMode == \Magento\Framework\App\State::MODE_PRODUCTION) { + $this->response->setHttpResponseCode(404); + return $this->response; + } + throw $e; } + + $this->state->setAreaCode($params['area']); + $this->objectManager->configure($this->configLoader->load($params['area'])); + $file = $params['file']; + unset($params['file']); + $asset = $this->assetRepo->createAsset($file, $params); + $this->response->setFilePath($asset->getSourceFile()); + $this->publisher->publish($asset); + return $this->response; } @@ -215,7 +226,7 @@ private function getFilesystem() * Retrieves LoggerInterface instance * * @return LoggerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getLogger() { diff --git a/lib/internal/Magento/Framework/App/TemplateTypesInterface.php b/lib/internal/Magento/Framework/App/TemplateTypesInterface.php index bc12d4d7e1ba5..37ae5e5431b92 100644 --- a/lib/internal/Magento/Framework/App/TemplateTypesInterface.php +++ b/lib/internal/Magento/Framework/App/TemplateTypesInterface.php @@ -8,7 +8,7 @@ /** * Template Types interface * - * @deprecated 100.2.0 because of incorrect location + * @deprecated 101.0.0 because of incorrect location */ interface TemplateTypesInterface { diff --git a/lib/internal/Magento/Framework/App/Test/Unit/AreaListTest.php b/lib/internal/Magento/Framework/App/Test/Unit/AreaListTest.php index b7b70c20c10dd..d03dc1eae33b4 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/AreaListTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/AreaListTest.php @@ -107,6 +107,19 @@ public function testGetFrontNameWhenAreaCodeAndFrontNameArentSet() $this->assertSame('test', $model->getArea($code)); } + public function testGetFrontNameWhenFrontNameIsInvalid() : void + { + $this->_model = new AreaList( + $this->objectManagerMock, + $this->_resolverFactory, + [ + 'testAreaCode' => [] + ] + ); + + $this->assertNull($this->_model->getFrontName('0')); + } + public function testGetCodes() { $areas = ['area1' => 'value1', 'area2' => 'value2']; diff --git a/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php b/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php index 21cb911393ee1..ee422ed8b8e17 100644 --- a/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php +++ b/lib/internal/Magento/Framework/App/Test/Unit/StaticResourceTest.php @@ -84,6 +84,9 @@ class StaticResourceTest extends TestCase */ private $object; + /** + * @inheridoc + */ protected function setUp(): void { $this->stateMock = $this->createMock(State::class); @@ -109,6 +112,9 @@ protected function setUp(): void ); } + /** + * Test to lunch on production mode + */ public function testLaunchProductionMode() { $this->stateMock->expects($this->once()) @@ -249,6 +255,9 @@ public function launchDataProvider() ]; } + /** + * Test to lunch with wrong path on developer mode + */ public function testLaunchWrongPath() { $this->expectException('InvalidArgumentException'); @@ -263,6 +272,28 @@ public function testLaunchWrongPath() $this->object->launch(); } + /** + * Test to lunch with wrong path on production mode + */ + public function testLaunchWrongPathProductionMode() + { + $mode = State::MODE_PRODUCTION; + $path = 'wrong/path.js'; + + $this->stateMock->method('getMode')->willReturn($mode); + $this->deploymentConfigMock->method('getConfigData') + ->with(ConfigOptionsListConstants::CONFIG_PATH_SCD_ON_DEMAND_IN_PRODUCTION) + ->willReturn(true); + $this->requestMock->method('get')->with('resource')->willReturn($path); + $this->responseMock->expects($this->once()) + ->method('setHttpResponseCode') + ->with(404); + $this->object->launch(); + } + + /** + * Test to Ability to handle exceptions on developer mode + */ public function testCatchExceptionDeveloperMode() { $this->objectManagerMock->expects($this->once()) @@ -286,6 +317,9 @@ public function testCatchExceptionDeveloperMode() $this->assertTrue($this->object->catchException($bootstrap, $exception)); } + /** + * Test to lunch with wrong path + */ public function testLaunchPathAbove() { $this->expectException('InvalidArgumentException'); diff --git a/lib/internal/Magento/Framework/App/Utility/Files.php b/lib/internal/Magento/Framework/App/Utility/Files.php index 901bbbde3dc9f..4298577f3147b 100644 --- a/lib/internal/Magento/Framework/App/Utility/Files.php +++ b/lib/internal/Magento/Framework/App/Utility/Files.php @@ -1020,7 +1020,7 @@ protected function _accumulateFilesByPatterns(array $patterns, $filePattern, arr /** * Parse meta-info of a static file in module * - * @deprecated Replaced with method accumulateStaticFiles() + * @deprecated 102.0.4 Replaced with method accumulateStaticFiles() * * @param string $file * @return array @@ -1116,7 +1116,7 @@ public function getJsFilesForArea($area) } else { $frontendPaths = [BP . "/lib/web/mage"]; /* current structure of /lib/web/mage directory contains frontend javascript in the root, - backend javascript in subdirectories. That's why script shouldn't go recursive throught subdirectories + backend javascript in subdirectories. That's why script shouldn't go recursive through subdirectories to get js files for frontend */ $files = array_merge($files, self::getFiles($frontendPaths, '*.js', false)); } diff --git a/lib/internal/Magento/Framework/App/View/Asset/Publisher.php b/lib/internal/Magento/Framework/App/View/Asset/Publisher.php index 0af5a8199ab88..ea50f96615139 100644 --- a/lib/internal/Magento/Framework/App/View/Asset/Publisher.php +++ b/lib/internal/Magento/Framework/App/View/Asset/Publisher.php @@ -14,6 +14,7 @@ * A publishing service for view assets * * @api + * @since 100.0.2 */ class Publisher { diff --git a/lib/internal/Magento/Framework/App/View/Deployment/Version.php b/lib/internal/Magento/Framework/App/View/Deployment/Version.php index 67f6d3c1ed779..e86e51eaf83d3 100644 --- a/lib/internal/Magento/Framework/App/View/Deployment/Version.php +++ b/lib/internal/Magento/Framework/App/View/Deployment/Version.php @@ -109,7 +109,7 @@ private function generateVersion() * Get logger * * @return LoggerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getLogger() { diff --git a/lib/internal/Magento/Framework/App/ViewInterface.php b/lib/internal/Magento/Framework/App/ViewInterface.php index a659cd371a9a4..6a61154ce8b40 100644 --- a/lib/internal/Magento/Framework/App/ViewInterface.php +++ b/lib/internal/Magento/Framework/App/ViewInterface.php @@ -10,8 +10,9 @@ * Later replaced with Magento\Framework\View\Result component * * @api - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Framework\View\Result\Layout + * @since 100.0.2 */ interface ViewInterface { diff --git a/lib/internal/Magento/Framework/AppInterface.php b/lib/internal/Magento/Framework/AppInterface.php index 25a0a9e785daa..e0397f8544536 100644 --- a/lib/internal/Magento/Framework/AppInterface.php +++ b/lib/internal/Magento/Framework/AppInterface.php @@ -14,6 +14,7 @@ * Implementations of this interface should implement application type specific initialization. * * @api + * @since 100.0.2 */ interface AppInterface { diff --git a/lib/internal/Magento/Framework/Archive/ArchiveInterface.php b/lib/internal/Magento/Framework/Archive/ArchiveInterface.php index 69a524107f1e0..c6e85b558b24e 100644 --- a/lib/internal/Magento/Framework/Archive/ArchiveInterface.php +++ b/lib/internal/Magento/Framework/Archive/ArchiveInterface.php @@ -13,6 +13,7 @@ /** * @api + * @since 100.0.2 */ interface ArchiveInterface { diff --git a/lib/internal/Magento/Framework/Authorization/PolicyInterface.php b/lib/internal/Magento/Framework/Authorization/PolicyInterface.php index 0d9a5d3af74c9..0afcc5c80e596 100644 --- a/lib/internal/Magento/Framework/Authorization/PolicyInterface.php +++ b/lib/internal/Magento/Framework/Authorization/PolicyInterface.php @@ -9,6 +9,7 @@ * Responsible for internal authorization decision making based on provided role, resource and privilege * * @api + * @since 100.0.2 */ interface PolicyInterface { diff --git a/lib/internal/Magento/Framework/Authorization/RoleLocatorInterface.php b/lib/internal/Magento/Framework/Authorization/RoleLocatorInterface.php index b273f2274df34..29b8cbdc377d4 100644 --- a/lib/internal/Magento/Framework/Authorization/RoleLocatorInterface.php +++ b/lib/internal/Magento/Framework/Authorization/RoleLocatorInterface.php @@ -11,6 +11,7 @@ * Should be implemented by application developer that uses \Magento\Framework\Authorization component. * * @api + * @since 100.0.2 */ interface RoleLocatorInterface { diff --git a/lib/internal/Magento/Framework/AuthorizationInterface.php b/lib/internal/Magento/Framework/AuthorizationInterface.php index 6077e2740b75c..65f0755a48a5c 100644 --- a/lib/internal/Magento/Framework/AuthorizationInterface.php +++ b/lib/internal/Magento/Framework/AuthorizationInterface.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ interface AuthorizationInterface { diff --git a/lib/internal/Magento/Framework/Backup/AbstractBackup.php b/lib/internal/Magento/Framework/Backup/AbstractBackup.php index 66bcdbf16a54b..a46f7d629f6f1 100644 --- a/lib/internal/Magento/Framework/Backup/AbstractBackup.php +++ b/lib/internal/Magento/Framework/Backup/AbstractBackup.php @@ -12,6 +12,7 @@ * Class to work with archives * * @api + * @since 100.0.2 */ abstract class AbstractBackup implements BackupInterface, SourceFileInterface { @@ -311,6 +312,7 @@ protected function _filterName($name) * Check if keep files of backup * * @return bool + * @since 102.0.0 */ public function keepSourceFile() { @@ -322,6 +324,7 @@ public function keepSourceFile() * * @param bool $keepSourceFile * @return $this + * @since 102.0.0 */ public function setKeepSourceFile(bool $keepSourceFile) { diff --git a/lib/internal/Magento/Framework/Backup/BackupException.php b/lib/internal/Magento/Framework/Backup/BackupException.php index 7694c084c10a7..6fef1d92ddbfe 100644 --- a/lib/internal/Magento/Framework/Backup/BackupException.php +++ b/lib/internal/Magento/Framework/Backup/BackupException.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class BackupException extends \Magento\Framework\Exception\LocalizedException { diff --git a/lib/internal/Magento/Framework/Backup/BackupInterface.php b/lib/internal/Magento/Framework/Backup/BackupInterface.php index 16aada9689c11..e16eef51d25a9 100644 --- a/lib/internal/Magento/Framework/Backup/BackupInterface.php +++ b/lib/internal/Magento/Framework/Backup/BackupInterface.php @@ -14,7 +14,8 @@ /** * @api * - * @deprecated Backups should be done using other means. + * @deprecated 101.0.7 Backups should be done using other means. + * @since 100.0.2 */ interface BackupInterface { diff --git a/lib/internal/Magento/Framework/Backup/Db.php b/lib/internal/Magento/Framework/Backup/Db.php index d3b72f30d8cb3..b7e0edf7c4f47 100644 --- a/lib/internal/Magento/Framework/Backup/Db.php +++ b/lib/internal/Magento/Framework/Backup/Db.php @@ -14,6 +14,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @api + * @since 100.0.2 */ class Db extends AbstractBackup { diff --git a/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php b/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php index a019ccac06b66..78ace64e4fe68 100644 --- a/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php +++ b/lib/internal/Magento/Framework/Backup/Db/BackupDbInterface.php @@ -8,7 +8,8 @@ /** * @api * - * @deprecated Backups should be done using other means. + * @deprecated 101.0.7 Backups should be done using other means. + * @since 100.0.2 */ interface BackupDbInterface { diff --git a/lib/internal/Magento/Framework/Backup/Db/BackupFactory.php b/lib/internal/Magento/Framework/Backup/Db/BackupFactory.php index d1c9c3df1e9aa..a9e1cdd133f75 100644 --- a/lib/internal/Magento/Framework/Backup/Db/BackupFactory.php +++ b/lib/internal/Magento/Framework/Backup/Db/BackupFactory.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class BackupFactory { diff --git a/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php b/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php index ae5879290eb20..78940f93e33c9 100644 --- a/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php +++ b/lib/internal/Magento/Framework/Backup/Db/BackupInterface.php @@ -8,7 +8,8 @@ /** * @api * - * @deprecated Backups should be done using other means. + * @deprecated 101.0.7 Backups should be done using other means. + * @since 100.0.2 */ interface BackupInterface { diff --git a/lib/internal/Magento/Framework/Backup/Exception/CantLoadSnapshot.php b/lib/internal/Magento/Framework/Backup/Exception/CantLoadSnapshot.php index 45d1cc21b06da..b351bc85a613b 100644 --- a/lib/internal/Magento/Framework/Backup/Exception/CantLoadSnapshot.php +++ b/lib/internal/Magento/Framework/Backup/Exception/CantLoadSnapshot.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class CantLoadSnapshot extends \Magento\Framework\Backup\BackupException { diff --git a/lib/internal/Magento/Framework/Backup/Exception/FtpConnectionFailed.php b/lib/internal/Magento/Framework/Backup/Exception/FtpConnectionFailed.php index 9b722da10a1bd..311de25343eb7 100644 --- a/lib/internal/Magento/Framework/Backup/Exception/FtpConnectionFailed.php +++ b/lib/internal/Magento/Framework/Backup/Exception/FtpConnectionFailed.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class FtpConnectionFailed extends \Magento\Framework\Backup\BackupException { diff --git a/lib/internal/Magento/Framework/Backup/Exception/FtpValidationFailed.php b/lib/internal/Magento/Framework/Backup/Exception/FtpValidationFailed.php index 53af4bfd3061b..1b197576b32c2 100644 --- a/lib/internal/Magento/Framework/Backup/Exception/FtpValidationFailed.php +++ b/lib/internal/Magento/Framework/Backup/Exception/FtpValidationFailed.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class FtpValidationFailed extends \Magento\Framework\Backup\BackupException { diff --git a/lib/internal/Magento/Framework/Backup/Exception/NotEnoughFreeSpace.php b/lib/internal/Magento/Framework/Backup/Exception/NotEnoughFreeSpace.php index b373134d2e9a6..48cedca5aecbb 100644 --- a/lib/internal/Magento/Framework/Backup/Exception/NotEnoughFreeSpace.php +++ b/lib/internal/Magento/Framework/Backup/Exception/NotEnoughFreeSpace.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class NotEnoughFreeSpace extends \Magento\Framework\Backup\BackupException { diff --git a/lib/internal/Magento/Framework/Backup/Exception/NotEnoughPermissions.php b/lib/internal/Magento/Framework/Backup/Exception/NotEnoughPermissions.php index ba7e80a503f4f..df89932680029 100644 --- a/lib/internal/Magento/Framework/Backup/Exception/NotEnoughPermissions.php +++ b/lib/internal/Magento/Framework/Backup/Exception/NotEnoughPermissions.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class NotEnoughPermissions extends \Magento\Framework\Backup\BackupException { diff --git a/lib/internal/Magento/Framework/Backup/Factory.php b/lib/internal/Magento/Framework/Backup/Factory.php index effa796a2a074..1692cc168d5f2 100644 --- a/lib/internal/Magento/Framework/Backup/Factory.php +++ b/lib/internal/Magento/Framework/Backup/Factory.php @@ -15,6 +15,7 @@ /** * @api + * @since 100.0.2 */ class Factory { diff --git a/lib/internal/Magento/Framework/Backup/Filesystem.php b/lib/internal/Magento/Framework/Backup/Filesystem.php index f3946444cec20..72996cdd28fda 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem.php @@ -317,7 +317,7 @@ protected function _getTarTmpPath() /** * @return Ftp - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function getRollBackFtp() { @@ -333,7 +333,7 @@ protected function getRollBackFtp() /** * @return Fs - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function getRollBackFs() { diff --git a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/AbstractRollback.php b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/AbstractRollback.php index 8c8c21a8405b6..976c07ef986e0 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/AbstractRollback.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/AbstractRollback.php @@ -11,6 +11,7 @@ * Filesystem rollback workers abstract class * * @api + * @since 100.0.2 */ abstract class AbstractRollback { diff --git a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php index 4820b83ceb7a8..b8eca279fdf22 100644 --- a/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php +++ b/lib/internal/Magento/Framework/Backup/Filesystem/Rollback/Fs.php @@ -97,7 +97,7 @@ public function run() * Get file system helper instance * * @return Helper - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getFsHelper() { diff --git a/lib/internal/Magento/Framework/Bulk/BulkManagementInterface.php b/lib/internal/Magento/Framework/Bulk/BulkManagementInterface.php index 9cfb2b1c97a4d..d2d247bdb271c 100644 --- a/lib/internal/Magento/Framework/Bulk/BulkManagementInterface.php +++ b/lib/internal/Magento/Framework/Bulk/BulkManagementInterface.php @@ -8,7 +8,7 @@ /** * Interface BulkManagementInterface * @api - * @since 100.2.0 + * @since 103.0.0 */ interface BulkManagementInterface { @@ -20,7 +20,7 @@ interface BulkManagementInterface * @param string $description * @param int $userId * @return boolean - * @since 100.2.0 + * @since 103.0.0 */ public function scheduleBulk($bulkUuid, array $operations, $description, $userId = null); @@ -29,7 +29,7 @@ public function scheduleBulk($bulkUuid, array $operations, $description, $userId * * @param string $bulkId * @return boolean - * @since 100.2.0 + * @since 103.0.0 */ public function deleteBulk($bulkId); } diff --git a/lib/internal/Magento/Framework/Bulk/BulkStatusInterface.php b/lib/internal/Magento/Framework/Bulk/BulkStatusInterface.php index 0f1deb0ceeeaf..45352af9c5c8c 100644 --- a/lib/internal/Magento/Framework/Bulk/BulkStatusInterface.php +++ b/lib/internal/Magento/Framework/Bulk/BulkStatusInterface.php @@ -8,7 +8,7 @@ /** * Interface BulkStatusInterface * @api - * @since 100.2.0 + * @since 103.0.0 */ interface BulkStatusInterface { @@ -18,7 +18,7 @@ interface BulkStatusInterface * @param string $bulkUuid * @param int|null $failureType * @return \Magento\Framework\Bulk\OperationInterface[] - * @since 100.2.0 + * @since 103.0.0 */ public function getFailedOperationsByBulkId($bulkUuid, $failureType = null); @@ -28,7 +28,7 @@ public function getFailedOperationsByBulkId($bulkUuid, $failureType = null); * @param string $bulkUuid * @param int $status * @return int - * @since 100.2.0 + * @since 103.0.0 */ public function getOperationsCountByBulkIdAndStatus($bulkUuid, $status); @@ -37,7 +37,7 @@ public function getOperationsCountByBulkIdAndStatus($bulkUuid, $status); * * @param int $userId * @return BulkSummaryInterface[] - * @since 100.2.0 + * @since 103.0.0 */ public function getBulksByUser($userId); @@ -49,7 +49,7 @@ public function getBulksByUser($userId); * * @param string $bulkUuid * @return int NOT_STARTED | IN_PROGRESS | FINISHED_SUCCESFULLY | FINISHED_WITH_FAILURE - * @since 100.2.0 + * @since 103.0.0 */ public function getBulkStatus($bulkUuid); } diff --git a/lib/internal/Magento/Framework/Bulk/BulkSummaryInterface.php b/lib/internal/Magento/Framework/Bulk/BulkSummaryInterface.php index 69e9b71ca00c3..f06ea9289cf6f 100644 --- a/lib/internal/Magento/Framework/Bulk/BulkSummaryInterface.php +++ b/lib/internal/Magento/Framework/Bulk/BulkSummaryInterface.php @@ -8,7 +8,7 @@ /** * Interface BulkSummaryInterface * @api - * @since 100.2.0 + * @since 103.0.0 */ interface BulkSummaryInterface { @@ -35,7 +35,7 @@ interface BulkSummaryInterface * Get bulk uuid * * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function getBulkId(); @@ -44,7 +44,7 @@ public function getBulkId(); * * @param string $bulkUuid * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setBulkId($bulkUuid); @@ -52,7 +52,7 @@ public function setBulkId($bulkUuid); * Get bulk description * * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function getDescription(); @@ -61,7 +61,7 @@ public function getDescription(); * * @param string $description * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setDescription($description); @@ -69,7 +69,7 @@ public function setDescription($description); * Get bulk scheduled time * * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function getStartTime(); @@ -78,7 +78,7 @@ public function getStartTime(); * * @param string $timestamp * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setStartTime($timestamp); @@ -86,7 +86,7 @@ public function setStartTime($timestamp); * Get user id * * @return int - * @since 100.2.0 + * @since 103.0.0 */ public function getUserId(); @@ -95,7 +95,7 @@ public function getUserId(); * * @param int $userId * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setUserId($userId); @@ -103,7 +103,7 @@ public function setUserId($userId); * Get total number of operations scheduled in scope of this bulk * * @return int - * @since 100.2.0 + * @since 103.0.0 */ public function getOperationCount(); @@ -112,7 +112,7 @@ public function getOperationCount(); * * @param int $operationCount * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setOperationCount($operationCount); } diff --git a/lib/internal/Magento/Framework/Bulk/OperationInterface.php b/lib/internal/Magento/Framework/Bulk/OperationInterface.php index c1cac9f171430..a621106be3806 100644 --- a/lib/internal/Magento/Framework/Bulk/OperationInterface.php +++ b/lib/internal/Magento/Framework/Bulk/OperationInterface.php @@ -8,14 +8,14 @@ /** * Interface OperationInterface * @api - * @since 100.2.0 + * @since 103.0.0 */ interface OperationInterface extends \Magento\Framework\Api\ExtensibleDataInterface { /**#@+ * Constants for keys of data array. Identical to the name of the getter in snake case */ - const ID = 'id'; + const ID = 'operation_key'; const BULK_ID = 'bulk_uuid'; const TOPIC_NAME = 'topic_name'; const SERIALIZED_DATA = 'serialized_data'; @@ -39,7 +39,7 @@ interface OperationInterface extends \Magento\Framework\Api\ExtensibleDataInterf * Operation id * * @return int - * @since 100.2.0 + * @since 103.0.0 */ public function getId(); @@ -48,7 +48,7 @@ public function getId(); * * @param int $id * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setId($id); @@ -56,7 +56,7 @@ public function setId($id); * Get bulk uuid * * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function getBulkUuid(); @@ -65,7 +65,7 @@ public function getBulkUuid(); * * @param string $bulkId * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setBulkUuid($bulkId); @@ -73,7 +73,7 @@ public function setBulkUuid($bulkId); * Message Queue Topic * * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function getTopicName(); @@ -82,7 +82,7 @@ public function getTopicName(); * * @param string $topic * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setTopicName($topic); @@ -90,7 +90,7 @@ public function setTopicName($topic); * Serialized Data * * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function getSerializedData(); @@ -99,7 +99,7 @@ public function getSerializedData(); * * @param string $serializedData * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setSerializedData($serializedData); @@ -107,7 +107,7 @@ public function setSerializedData($serializedData); * Result serialized Data * * @return string - * @since 100.3.0 + * @since 103.0.0 */ public function getResultSerializedData(); @@ -116,7 +116,7 @@ public function getResultSerializedData(); * * @param string $resultSerializedData * @return $this - * @since 100.3.0 + * @since 103.0.0 */ public function setResultSerializedData($resultSerializedData); @@ -126,7 +126,7 @@ public function setResultSerializedData($resultSerializedData); * OPEN | COMPLETE | RETRIABLY_FAILED | NOT_RETRIABLY_FAILED * * @return int - * @since 100.2.0 + * @since 103.0.0 */ public function getStatus(); @@ -135,7 +135,7 @@ public function getStatus(); * * @param int $status * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setStatus($status); @@ -143,7 +143,7 @@ public function setStatus($status); * Get result message * * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function getResultMessage(); @@ -152,7 +152,7 @@ public function getResultMessage(); * * @param string $resultMessage * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setResultMessage($resultMessage); @@ -160,7 +160,7 @@ public function setResultMessage($resultMessage); * Get error code * * @return int - * @since 100.2.0 + * @since 103.0.0 */ public function getErrorCode(); @@ -169,7 +169,7 @@ public function getErrorCode(); * * @param int $errorCode * @return $this - * @since 100.2.0 + * @since 103.0.0 */ public function setErrorCode($errorCode); } diff --git a/lib/internal/Magento/Framework/Bulk/OperationManagementInterface.php b/lib/internal/Magento/Framework/Bulk/OperationManagementInterface.php index e86d3ca8c1624..4c603f3c5f76e 100644 --- a/lib/internal/Magento/Framework/Bulk/OperationManagementInterface.php +++ b/lib/internal/Magento/Framework/Bulk/OperationManagementInterface.php @@ -9,20 +9,21 @@ /** * Interface OperationManagementInterface * @api - * @since 100.2.0 + * @since 103.0.0 */ interface OperationManagementInterface { /** * Used by consumer to change status after processing operation * - * @param int $operationId + * @param string $bulkUuid + * @param int $operationKey * @param int $status * @param int|null $errorCode * @param string|null $message property to update Result Message * @param string|null $data serialized data object of failed message * @return boolean - * @since 100.2.0 + * @since 103.0.0 */ - public function changeOperationStatus($operationId, $status, $errorCode = null, $message = null, $data = null); + public function changeOperationStatus($bulkUuid, $operationKey, $status, $errorCode = null, $message = null, $data = null); // @codingStandardsIgnoreLine } diff --git a/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php b/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php index cd53516290252..d0c05613fbddd 100644 --- a/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php +++ b/lib/internal/Magento/Framework/Cache/Backend/RemoteSynchronizedCache.php @@ -237,7 +237,7 @@ public function save($data, $id, $tags = [], $specificLifetime = false) $dataToSave = $data; $remHash = $this->loadRemoteDataVersion($id); - if ($remHash !== false) { + if ($remHash !== false && $this->getDataVersion($data) === $remHash) { $dataToSave = $this->remote->load($id); } else { $this->remote->save($data, $id, $tags, $specificLifetime); diff --git a/lib/internal/Magento/Framework/Cache/Frontend/Decorator/TagScope.php b/lib/internal/Magento/Framework/Cache/Frontend/Decorator/TagScope.php index c837544a75f0f..1c5f04364b087 100644 --- a/lib/internal/Magento/Framework/Cache/Frontend/Decorator/TagScope.php +++ b/lib/internal/Magento/Framework/Cache/Frontend/Decorator/TagScope.php @@ -10,6 +10,7 @@ * Cache frontend decorator that limits the cleaning scope within a tag * * @api + * @since 100.0.2 */ class TagScope extends \Magento\Framework\Cache\Frontend\Decorator\Bare { diff --git a/lib/internal/Magento/Framework/Cache/FrontendInterface.php b/lib/internal/Magento/Framework/Cache/FrontendInterface.php index d81d7fbe1c7cc..c57c97238bddd 100644 --- a/lib/internal/Magento/Framework/Cache/FrontendInterface.php +++ b/lib/internal/Magento/Framework/Cache/FrontendInterface.php @@ -9,6 +9,7 @@ * Interface of a cache frontend - an ultimate publicly available interface to an actual cache storage * * @api + * @since 100.0.2 */ interface FrontendInterface { diff --git a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php index bca23e0dcf31a..d3b4c8852267a 100644 --- a/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php +++ b/lib/internal/Magento/Framework/Cache/LockGuardedCacheLoader.php @@ -116,7 +116,7 @@ public function lockedLoadData( callable $dataSaver ) { $cachedData = $dataLoader(); //optimistic read - $deadline = microtime(true) + $this->loadTimeout / 100; + $deadline = microtime(true) + $this->loadTimeout / 1000; if (empty($this->allowParallelGenerationConfigValue)) { $cacheConfig = $this @@ -131,7 +131,7 @@ public function lockedLoadData( return $dataCollector(); } - if ($this->locker->lock($lockName, $this->lockTimeout / 1000)) { + if ($this->locker->lock($lockName, 0)) { try { $data = $dataCollector(); $dataSaver($data); diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php index bf936c9eb7994..ac6185262fb15 100644 --- a/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/Backend/RemoteSynchronizedCacheTest.php @@ -61,11 +61,13 @@ protected function setUp(): void } /** + * Test that exception is thrown if cache is not configured. + * * @param array $options * * @dataProvider initializeWithExceptionDataProvider */ - public function testInitializeWithException($options) + public function testInitializeWithException($options): void { $this->expectException('Zend_Cache_Exception'); $this->objectManager->getObject( @@ -79,7 +81,7 @@ public function testInitializeWithException($options) /** * @return array */ - public function initializeWithExceptionDataProvider() + public function initializeWithExceptionDataProvider(): array { return [ 'empty_backend_option' => [ @@ -104,11 +106,13 @@ public function initializeWithExceptionDataProvider() } /** + * Test that exception is not thrown if cache is configured. + * * @param array $options * * @dataProvider initializeWithOutExceptionDataProvider */ - public function testInitializeWithOutException($options) + public function testInitializeWithOutException($options): void { $result = $this->objectManager->getObject( RemoteSynchronizedCache::class, @@ -122,7 +126,7 @@ public function testInitializeWithOutException($options) /** * @return array */ - public function initializeWithOutExceptionDataProvider() + public function initializeWithOutExceptionDataProvider(): array { $connectionMock = $this->getMockBuilder(Mysql::class) ->disableOriginalConstructor() @@ -151,9 +155,11 @@ public function initializeWithOutExceptionDataProvider() } /** - * Test that load will always return newest data. + * Test that load will return the newest data. + * + * @return void */ - public function testLoadWithLocalData() + public function testLoad(): void { $localData = 1; $remoteData = 2; @@ -182,7 +188,12 @@ public function testLoadWithLocalData() $this->assertEquals($remoteData, $this->remoteSyncCacheInstance->load(1)); } - public function testLoadWithNoLocalAndNoRemoteData() + /** + * Test that load will not return data when no local data and no remote data exist. + * + * @return void + */ + public function testLoadWithNoLocalAndNoRemoteData(): void { $localData = false; $remoteData = false; @@ -197,10 +208,15 @@ public function testLoadWithNoLocalAndNoRemoteData() ->method('load') ->willReturn($remoteData); - $this->assertEquals($remoteData, $this->remoteSyncCacheInstance->load(1)); + $this->assertEquals(false, $this->remoteSyncCacheInstance->load(1)); } - public function testLoadWithNoLocalAndRemoteData() + /** + * Test that load will return the newest data when only remote data exists. + * + * @return void + */ + public function testLoadWithNoLocalAndWithRemoteData(): void { $localData = false; $remoteData = 1; @@ -223,7 +239,109 @@ public function testLoadWithNoLocalAndRemoteData() $this->assertEquals($remoteData, $this->remoteSyncCacheInstance->load(1)); } - public function testRemove() + /** + * Test that load will return the newest data when local data and remote data are the same. + * + * @return void + */ + public function testLoadWithEqualLocalAndRemoteData(): void + { + $localData = 1; + $remoteData = 1; + + $this->localCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn($localData); + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(\hash('sha256', (string)$remoteData)); + + $this->assertEquals($localData, $this->remoteSyncCacheInstance->load(1)); + } + + /** + * Test that load will return stale cache. + * + * @return void + */ + public function testLoadWithStaleCache(): void + { + $localData = 1; + + $this->localCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn($localData); + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(false); + + $closure = \Closure::bind(function ($cacheInstance) { + $cacheInstance->_options['use_stale_cache'] = true; + }, null, $this->remoteSyncCacheInstance); + $closure($this->remoteSyncCacheInstance); + + $this->remoteCacheMockExample + ->expects($this->at(2)) + ->method('load') + ->willReturn(true); + + $this->assertEquals($localData, $this->remoteSyncCacheInstance->load(1)); + } + + /** + * Test that load will generate data on the first attempt. + * + * @return void + */ + public function testLoadWithoutStaleCache(): void + { + $localData = 1; + + $this->localCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn($localData); + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(false); + + $closure = \Closure::bind(function ($cacheInstance) { + $cacheInstance->_options['use_stale_cache'] = true; + }, null, $this->remoteSyncCacheInstance); + $closure($this->remoteSyncCacheInstance); + + $this->remoteCacheMockExample + ->expects($this->at(2)) + ->method('load') + ->willReturn(false); + + $closure = \Closure::bind(function ($cacheInstance) { + return $cacheInstance->lockSign; + }, null, $this->remoteSyncCacheInstance); + $lockSign = $closure($this->remoteSyncCacheInstance); + + $this->remoteCacheMockExample + ->expects($this->at(4)) + ->method('load') + ->willReturn($lockSign); + + $this->assertEquals(false, $this->remoteSyncCacheInstance->load(1)); + } + + /** + * Test data remove. + * + * @return void + */ + public function testRemove(): void { $this->remoteCacheMockExample ->expects($this->exactly(2)) @@ -238,7 +356,12 @@ public function testRemove() $this->remoteSyncCacheInstance->remove(1); } - public function testClean() + /** + * Test data clean. + * + * @return void + */ + public function testClean(): void { $this->remoteCacheMockExample ->expects($this->exactly(1)) @@ -248,7 +371,12 @@ public function testClean() $this->remoteSyncCacheInstance->clean(); } - public function testSaveWithRemoteData() + /** + * Test data save when remote data exist. + * + * @return void + */ + public function testSaveWithEqualRemoteData(): void { $remoteData = 1; @@ -270,7 +398,27 @@ public function testSaveWithRemoteData() $this->remoteSyncCacheInstance->save($remoteData, 1); } - public function testSaveWithoutRemoteData() + public function testSaveWithMismatchedRemoteData() + { + $remoteData = '1'; + + $this->remoteCacheMockExample + ->expects($this->at(0)) + ->method('load') + ->willReturn(\hash('sha256', $remoteData)); + + $this->remoteCacheMockExample->expects($this->exactly(2))->method('save'); + $this->localCacheMockExample->expects($this->once())->method('save'); + + $this->remoteSyncCacheInstance->save(2, 1); + } + + /** + * Test data save when remote data is not exist. + * + * @return void + */ + public function testSaveWithoutRemoteData(): void { $this->remoteCacheMockExample ->expects($this->at(0)) diff --git a/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php b/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php new file mode 100644 index 0000000000000..9dcfb89373c2b --- /dev/null +++ b/lib/internal/Magento/Framework/Cache/Test/Unit/LockGuardedCacheLoaderTest.php @@ -0,0 +1,181 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Cache\Test\Unit; + +use Magento\Framework\Cache\LockGuardedCacheLoader; +use Magento\Framework\Lock\LockManagerInterface; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +class LockGuardedCacheLoaderTest extends TestCase +{ + /** + * @var LockManagerInterface|MockObject + */ + private $lockManagerInterfaceMock; + + /** + * @var LockGuardedCacheLoader + */ + private $LockGuardedCacheLoader; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + $this->lockManagerInterfaceMock = $this->getMockForAbstractClass(LockManagerInterface::class); + + $objectManager = new ObjectManagerHelper($this); + + $this->LockGuardedCacheLoader = $objectManager->getObject( + LockGuardedCacheLoader::class, + [ + 'locker' => $this->lockManagerInterfaceMock + ] + ); + } + + /** + * Verify optimistic data read from cache. + * + * @return void + */ + public function testOptimisticDataRead(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return 'loaded_data'; + }; + + $dataCollector = function () { + return true; + }; + + $dataSaver = function () { + return true; + }; + + $this->lockManagerInterfaceMock->expects($this->never())->method('lock'); + $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); + + $this->assertEquals( + 'loaded_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } + + /** + * Verify data is collected when deadline to read from cache is reached. + * + * @return void + */ + public function testDataCollectedAfterDeadlineReached(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return false; + }; + + $dataCollector = function () { + return 'collected_data'; + }; + + $dataSaver = function () { + return true; + }; + + $this->lockManagerInterfaceMock + ->expects($this->atLeastOnce())->method('lock') + ->with($lockName, 0) + ->willReturn(false); + + $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); + + $this->assertEquals( + 'collected_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } + + /** + * Verify data write to cache. + * + * @return void + */ + public function testDataWrite(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return false; + }; + + $dataCollector = function () { + return 'collected_data'; + }; + + $dataSaver = function () { + return true; + }; + + $this->lockManagerInterfaceMock + ->expects($this->once())->method('lock') + ->with($lockName, 0) + ->willReturn(true); + + $this->lockManagerInterfaceMock->expects($this->once())->method('unlock'); + + $this->assertEquals( + 'collected_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } + + /** + * Verify data collected when Parallel Generation is allowed. + * + * @return void + */ + public function testDataCollectedWithParallelGeneration(): void + { + $lockName = \uniqid('lock_name_1_', true); + + $dataLoader = function () { + return false; + }; + + $dataCollector = function () { + return 'collected_data'; + }; + + $dataSaver = function () { + return true; + }; + + $closure = \Closure::bind(function ($cacheLoader) { + return $cacheLoader->allowParallelGenerationConfigValue = true; + }, null, $this->LockGuardedCacheLoader); + $closure($this->LockGuardedCacheLoader); + + $this->lockManagerInterfaceMock + ->expects($this->once())->method('lock') + ->with($lockName, 0) + ->willReturn(false); + + $this->lockManagerInterfaceMock->expects($this->never())->method('unlock'); + + $this->assertEquals( + 'collected_data', + $this->LockGuardedCacheLoader->lockedLoadData($lockName, $dataLoader, $dataCollector, $dataSaver) + ); + } +} diff --git a/lib/internal/Magento/Framework/Code/Generator/DefinedClasses.php b/lib/internal/Magento/Framework/Code/Generator/DefinedClasses.php index 363c6f59b17ea..e0ae472ba20cb 100644 --- a/lib/internal/Magento/Framework/Code/Generator/DefinedClasses.php +++ b/lib/internal/Magento/Framework/Code/Generator/DefinedClasses.php @@ -40,7 +40,7 @@ public function isClassLoadableFromMemory($className) * * @param string $className * @return bool - * @deprecated + * @deprecated 102.0.0 */ public function isClassLoadableFromDisc($className) { diff --git a/lib/internal/Magento/Framework/Code/NameBuilder.php b/lib/internal/Magento/Framework/Code/NameBuilder.php index 993235054e490..8ceac1c569766 100644 --- a/lib/internal/Magento/Framework/Code/NameBuilder.php +++ b/lib/internal/Magento/Framework/Code/NameBuilder.php @@ -9,6 +9,7 @@ * Builds namespace with classname out of the parts. * * @api + * @since 100.0.2 */ class NameBuilder { diff --git a/lib/internal/Magento/Framework/Code/Reader/SourceArgumentsReader.php b/lib/internal/Magento/Framework/Code/Reader/SourceArgumentsReader.php index 31243f6ad98f9..840cc2a3e943c 100644 --- a/lib/internal/Magento/Framework/Code/Reader/SourceArgumentsReader.php +++ b/lib/internal/Magento/Framework/Code/Reader/SourceArgumentsReader.php @@ -87,7 +87,7 @@ public function getConstructorArgumentTypes( * @param string $argument * @param array $availableNamespaces * @return string - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see getConstructorArgumentTypes */ protected function resolveNamespaces($argument, $availableNamespaces) @@ -102,7 +102,7 @@ protected function resolveNamespaces($argument, $availableNamespaces) * @param string $token * @return string * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ protected function removeToken($argument, $token) { @@ -118,7 +118,7 @@ protected function removeToken($argument, $token) * * @param array $file * @return array - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see getConstructorArgumentTypes */ protected function getImportedNamespaces(array $file) diff --git a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php b/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php index b79ba49a24ddd..3f69c3258d0b9 100644 --- a/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php +++ b/lib/internal/Magento/Framework/Communication/Config/Reader/XmlReader/Converter.php @@ -62,7 +62,7 @@ public function __construct( * The getter function to get the new ConfigParser dependency. * * @return \Magento\Framework\Communication\Config\ConfigParser - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getConfigParser() { diff --git a/lib/internal/Magento/Framework/Component/ComponentRegistrar.php b/lib/internal/Magento/Framework/Component/ComponentRegistrar.php index bd20582875ce0..7fea9a4e98b67 100644 --- a/lib/internal/Magento/Framework/Component/ComponentRegistrar.php +++ b/lib/internal/Magento/Framework/Component/ComponentRegistrar.php @@ -9,6 +9,7 @@ * Provides ability to statically register components. * * @api + * @since 100.0.2 */ class ComponentRegistrar implements ComponentRegistrarInterface { diff --git a/lib/internal/Magento/Framework/Config/AbstractXml.php b/lib/internal/Magento/Framework/Config/AbstractXml.php index caead98147bf5..35ce8348c4fc0 100644 --- a/lib/internal/Magento/Framework/Config/AbstractXml.php +++ b/lib/internal/Magento/Framework/Config/AbstractXml.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ abstract class AbstractXml { diff --git a/lib/internal/Magento/Framework/Config/CacheInterface.php b/lib/internal/Magento/Framework/Config/CacheInterface.php index 7064dcfca0547..5d28cf7563a80 100644 --- a/lib/internal/Magento/Framework/Config/CacheInterface.php +++ b/lib/internal/Magento/Framework/Config/CacheInterface.php @@ -11,6 +11,7 @@ * Config cache interface. * * @api + * @since 100.0.2 */ interface CacheInterface extends \Magento\Framework\Cache\FrontendInterface { diff --git a/lib/internal/Magento/Framework/Config/Composer/Package.php b/lib/internal/Magento/Framework/Config/Composer/Package.php index a8ff6ac724c3f..bc0b195297980 100644 --- a/lib/internal/Magento/Framework/Config/Composer/Package.php +++ b/lib/internal/Magento/Framework/Config/Composer/Package.php @@ -9,6 +9,7 @@ /** * A model that represents composer package * @api + * @since 100.0.2 */ class Package { diff --git a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php index 775611c63a9f7..96999187bd6cb 100644 --- a/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php +++ b/lib/internal/Magento/Framework/Config/ConfigOptionsListConstants.php @@ -11,6 +11,7 @@ /** * Deployment configuration options constant storage * @api + * @since 100.0.2 */ class ConfigOptionsListConstants { diff --git a/lib/internal/Magento/Framework/Config/Converter/Dom/Flat.php b/lib/internal/Magento/Framework/Config/Converter/Dom/Flat.php index 7f64705a6bde0..d989f03b44f4d 100644 --- a/lib/internal/Magento/Framework/Config/Converter/Dom/Flat.php +++ b/lib/internal/Magento/Framework/Config/Converter/Dom/Flat.php @@ -11,6 +11,7 @@ * Universal converter of any XML data to an array representation with no data loss * * @api + * @since 100.0.2 */ class Flat { diff --git a/lib/internal/Magento/Framework/Config/ConverterInterface.php b/lib/internal/Magento/Framework/Config/ConverterInterface.php index 4e299e31fb206..ed4b811253c40 100644 --- a/lib/internal/Magento/Framework/Config/ConverterInterface.php +++ b/lib/internal/Magento/Framework/Config/ConverterInterface.php @@ -9,6 +9,7 @@ * Config DOM-to-array converter interface. * * @api + * @since 100.0.2 */ interface ConverterInterface { diff --git a/lib/internal/Magento/Framework/Config/Data.php b/lib/internal/Magento/Framework/Config/Data.php index 2ae8eb378064a..cc11b32c410ba 100644 --- a/lib/internal/Magento/Framework/Config/Data.php +++ b/lib/internal/Magento/Framework/Config/Data.php @@ -13,6 +13,7 @@ * * @SuppressWarnings(PHPMD.NumberOfChildren) * @api + * @since 100.0.2 */ class Data implements \Magento\Framework\Config\DataInterface { diff --git a/lib/internal/Magento/Framework/Config/Data/ConfigData.php b/lib/internal/Magento/Framework/Config/Data/ConfigData.php index b6ea96e36ec56..1c3d2524efa57 100644 --- a/lib/internal/Magento/Framework/Config/Data/ConfigData.php +++ b/lib/internal/Magento/Framework/Config/Data/ConfigData.php @@ -9,6 +9,7 @@ /** * Data transfer object to store config data for config options * @api + * @since 100.0.2 */ class ConfigData { diff --git a/lib/internal/Magento/Framework/Config/Data/Scoped.php b/lib/internal/Magento/Framework/Config/Data/Scoped.php index 9806d89fd5737..e453e8397a9a5 100644 --- a/lib/internal/Magento/Framework/Config/Data/Scoped.php +++ b/lib/internal/Magento/Framework/Config/Data/Scoped.php @@ -11,6 +11,7 @@ /** * Provides scoped configuration * @api + * @since 100.0.2 */ class Scoped extends \Magento\Framework\Config\Data { diff --git a/lib/internal/Magento/Framework/Config/DataInterface.php b/lib/internal/Magento/Framework/Config/DataInterface.php index f0fbedae5a500..0418915a2dadb 100644 --- a/lib/internal/Magento/Framework/Config/DataInterface.php +++ b/lib/internal/Magento/Framework/Config/DataInterface.php @@ -9,6 +9,7 @@ * Config data interface. * * @api + * @since 100.0.2 */ interface DataInterface { diff --git a/lib/internal/Magento/Framework/Config/Dom.php b/lib/internal/Magento/Framework/Config/Dom.php index e36f9615db26b..227eec631a8fe 100644 --- a/lib/internal/Magento/Framework/Config/Dom.php +++ b/lib/internal/Magento/Framework/Config/Dom.php @@ -18,6 +18,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * @since 100.0.2 */ class Dom { @@ -83,7 +84,6 @@ class Dom /** * @var array - * @since 2.2.0 */ private static $resolvedSchemaPaths = []; @@ -122,7 +122,6 @@ public function __construct( * * @param string $errorFormat * @return string[] - * @since 2.1.0 */ private static function getXmlErrors($errorFormat) { diff --git a/lib/internal/Magento/Framework/Config/Dom/UrnResolver.php b/lib/internal/Magento/Framework/Config/Dom/UrnResolver.php index 41c83c940ca04..44ebe82422aba 100644 --- a/lib/internal/Magento/Framework/Config/Dom/UrnResolver.php +++ b/lib/internal/Magento/Framework/Config/Dom/UrnResolver.php @@ -16,6 +16,7 @@ /** * @api + * @since 100.0.2 */ class UrnResolver { diff --git a/lib/internal/Magento/Framework/Config/Dom/ValidationException.php b/lib/internal/Magento/Framework/Config/Dom/ValidationException.php index 3c74a80bdfab1..1c77e510841eb 100644 --- a/lib/internal/Magento/Framework/Config/Dom/ValidationException.php +++ b/lib/internal/Magento/Framework/Config/Dom/ValidationException.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class ValidationException extends \InvalidArgumentException { diff --git a/lib/internal/Magento/Framework/Config/Dom/ValidationSchemaException.php b/lib/internal/Magento/Framework/Config/Dom/ValidationSchemaException.php index d65cfa9a2ec9b..81548c31f6ba3 100644 --- a/lib/internal/Magento/Framework/Config/Dom/ValidationSchemaException.php +++ b/lib/internal/Magento/Framework/Config/Dom/ValidationSchemaException.php @@ -13,7 +13,7 @@ /** * @api - * @since 100.2.0 + * @since 101.0.0 */ class ValidationSchemaException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Config/DomFactory.php b/lib/internal/Magento/Framework/Config/DomFactory.php index 542ebd642c71d..f079674a50b9e 100644 --- a/lib/internal/Magento/Framework/Config/DomFactory.php +++ b/lib/internal/Magento/Framework/Config/DomFactory.php @@ -8,6 +8,7 @@ /** * Magento configuration DOM factory * @api + * @since 100.0.2 */ class DomFactory { diff --git a/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php b/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php index ffc32a300ccfe..c9fa5e3ddc606 100644 --- a/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php +++ b/lib/internal/Magento/Framework/Config/File/ConfigFilePool.php @@ -9,6 +9,7 @@ /** * Stores file key to file name config * @api + * @since 100.0.2 */ class ConfigFilePool { @@ -39,7 +40,7 @@ class ConfigFilePool * Initial files for configuration * * @var array - * @deprecated 100.2.0 Magento does not support custom config file pools since 2.2.0 version + * @deprecated 101.0.0 Magento does not support custom config file pools since 2.2.0 version */ private $initialConfigFiles = [ self::DIST => [ @@ -91,7 +92,7 @@ public function getPath($fileKey) * Returns application initial config files. * * @return array - * @deprecated 100.2.0 Magento does not support custom config file pools since 2.2.0 version + * @deprecated 101.0.0 Magento does not support custom config file pools since 2.2.0 version * @since 100.1.3 */ public function getInitialFilePools() @@ -104,7 +105,7 @@ public function getInitialFilePools() * * @param string $pool * @return array - * @deprecated 100.2.0 Magento does not support custom config file pools since 2.2.0 version + * @deprecated 101.0.0 Magento does not support custom config file pools since 2.2.0 version * @since 100.1.3 */ public function getPathsByPool($pool) diff --git a/lib/internal/Magento/Framework/Config/FileIterator.php b/lib/internal/Magento/Framework/Config/FileIterator.php index 0de1d8ae528f9..7cf1f34b6deb6 100644 --- a/lib/internal/Magento/Framework/Config/FileIterator.php +++ b/lib/internal/Magento/Framework/Config/FileIterator.php @@ -12,6 +12,7 @@ /** * Class FileIterator * @api + * @since 100.0.2 */ class FileIterator implements \Iterator, \Countable { diff --git a/lib/internal/Magento/Framework/Config/FileIteratorFactory.php b/lib/internal/Magento/Framework/Config/FileIteratorFactory.php index 15d0edd57ded8..337a6110096b9 100644 --- a/lib/internal/Magento/Framework/Config/FileIteratorFactory.php +++ b/lib/internal/Magento/Framework/Config/FileIteratorFactory.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class FileIteratorFactory { diff --git a/lib/internal/Magento/Framework/Config/FileResolver.php b/lib/internal/Magento/Framework/Config/FileResolver.php index 30967a8fa0014..959537d76ab91 100644 --- a/lib/internal/Magento/Framework/Config/FileResolver.php +++ b/lib/internal/Magento/Framework/Config/FileResolver.php @@ -55,7 +55,7 @@ class FileResolver implements \Magento\Framework\Config\FileResolverInterface, D /** * @var DirectoryList - * @deprecated Unused class property + * @deprecated 102.0.0 Unused class property */ private $directoryList; diff --git a/lib/internal/Magento/Framework/Config/FileResolverInterface.php b/lib/internal/Magento/Framework/Config/FileResolverInterface.php index a83f1da03e123..95bf7f5249561 100644 --- a/lib/internal/Magento/Framework/Config/FileResolverInterface.php +++ b/lib/internal/Magento/Framework/Config/FileResolverInterface.php @@ -9,6 +9,7 @@ * File resolver interface. * * @api + * @since 100.0.2 */ interface FileResolverInterface { diff --git a/lib/internal/Magento/Framework/Config/Reader/Filesystem.php b/lib/internal/Magento/Framework/Config/Reader/Filesystem.php index e2008b95c3b61..b05269b33689d 100644 --- a/lib/internal/Magento/Framework/Config/Reader/Filesystem.php +++ b/lib/internal/Magento/Framework/Config/Reader/Filesystem.php @@ -12,6 +12,7 @@ /** * @SuppressWarnings(PHPMD.NumberOfChildren) * @api + * @since 100.0.2 */ class Filesystem implements \Magento\Framework\Config\ReaderInterface { diff --git a/lib/internal/Magento/Framework/Config/ReaderInterface.php b/lib/internal/Magento/Framework/Config/ReaderInterface.php index d85ed2030a262..97883d21489b5 100644 --- a/lib/internal/Magento/Framework/Config/ReaderInterface.php +++ b/lib/internal/Magento/Framework/Config/ReaderInterface.php @@ -11,6 +11,7 @@ * Config reader interface. * * @api + * @since 100.0.2 */ interface ReaderInterface { diff --git a/lib/internal/Magento/Framework/Config/SchemaLocatorInterface.php b/lib/internal/Magento/Framework/Config/SchemaLocatorInterface.php index 242f3d62a5f2a..985614051b665 100644 --- a/lib/internal/Magento/Framework/Config/SchemaLocatorInterface.php +++ b/lib/internal/Magento/Framework/Config/SchemaLocatorInterface.php @@ -11,6 +11,7 @@ * Config schema locator interface. * * @api + * @since 100.0.2 */ interface SchemaLocatorInterface { diff --git a/lib/internal/Magento/Framework/Config/ScopeInterface.php b/lib/internal/Magento/Framework/Config/ScopeInterface.php index 052d97eebc166..bd6bc92cdea48 100644 --- a/lib/internal/Magento/Framework/Config/ScopeInterface.php +++ b/lib/internal/Magento/Framework/Config/ScopeInterface.php @@ -9,6 +9,7 @@ * Config scope interface. * * @api + * @since 100.0.2 */ interface ScopeInterface { diff --git a/lib/internal/Magento/Framework/Config/ScopeListInterface.php b/lib/internal/Magento/Framework/Config/ScopeListInterface.php index c45938db93d41..47526c8ca0a86 100644 --- a/lib/internal/Magento/Framework/Config/ScopeListInterface.php +++ b/lib/internal/Magento/Framework/Config/ScopeListInterface.php @@ -9,6 +9,7 @@ * Config scope list interface. * * @api + * @since 100.0.2 */ interface ScopeListInterface { diff --git a/lib/internal/Magento/Framework/Config/Theme.php b/lib/internal/Magento/Framework/Config/Theme.php index 812e61483b77b..a5e216d335ae4 100644 --- a/lib/internal/Magento/Framework/Config/Theme.php +++ b/lib/internal/Magento/Framework/Config/Theme.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Theme { diff --git a/lib/internal/Magento/Framework/Config/ValidationStateInterface.php b/lib/internal/Magento/Framework/Config/ValidationStateInterface.php index 20869d20b9dc1..6d1199adda5e2 100644 --- a/lib/internal/Magento/Framework/Config/ValidationStateInterface.php +++ b/lib/internal/Magento/Framework/Config/ValidationStateInterface.php @@ -9,6 +9,7 @@ * Config validation state interface. * * @api + * @since 100.0.2 */ interface ValidationStateInterface { diff --git a/lib/internal/Magento/Framework/Config/View.php b/lib/internal/Magento/Framework/Config/View.php index 05863caeec2b6..1f15c71e5ddce 100644 --- a/lib/internal/Magento/Framework/Config/View.php +++ b/lib/internal/Magento/Framework/Config/View.php @@ -9,6 +9,7 @@ * View configuration files handler * * @api + * @since 100.0.2 */ class View extends \Magento\Framework\Config\Reader\Filesystem { diff --git a/lib/internal/Magento/Framework/Console/CommandListInterface.php b/lib/internal/Magento/Framework/Console/CommandListInterface.php index 1547b5b671bfd..104d22cd735bf 100644 --- a/lib/internal/Magento/Framework/Console/CommandListInterface.php +++ b/lib/internal/Magento/Framework/Console/CommandListInterface.php @@ -8,6 +8,7 @@ /** * Contains a list of Console commands * @api + * @since 100.0.2 */ interface CommandListInterface { diff --git a/lib/internal/Magento/Framework/Controller/Result/Json.php b/lib/internal/Magento/Framework/Controller/Result/Json.php index f46d0875c4250..a40cbf8f78200 100644 --- a/lib/internal/Magento/Framework/Controller/Result/Json.php +++ b/lib/internal/Magento/Framework/Controller/Result/Json.php @@ -15,6 +15,7 @@ * Actual for controller actions that serve ajax requests * * @api + * @since 100.0.2 */ class Json extends AbstractResult { diff --git a/lib/internal/Magento/Framework/Controller/Result/Redirect.php b/lib/internal/Magento/Framework/Controller/Result/Redirect.php index 120b18d873cff..02daae818b120 100644 --- a/lib/internal/Magento/Framework/Controller/Result/Redirect.php +++ b/lib/internal/Magento/Framework/Controller/Result/Redirect.php @@ -17,6 +17,7 @@ * so this is a result object that implements all necessary properties of a HTTP redirect * * @api + * @since 100.0.2 */ class Redirect extends AbstractResult { diff --git a/lib/internal/Magento/Framework/Controller/Result/RedirectFactory.php b/lib/internal/Magento/Framework/Controller/Result/RedirectFactory.php index 0e622b1cc5e1a..797fcea4186dc 100644 --- a/lib/internal/Magento/Framework/Controller/Result/RedirectFactory.php +++ b/lib/internal/Magento/Framework/Controller/Result/RedirectFactory.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class RedirectFactory { diff --git a/lib/internal/Magento/Framework/Controller/ResultFactory.php b/lib/internal/Magento/Framework/Controller/ResultFactory.php index bb7ab1c8b6c85..88efcdf8d3386 100644 --- a/lib/internal/Magento/Framework/Controller/ResultFactory.php +++ b/lib/internal/Magento/Framework/Controller/ResultFactory.php @@ -12,6 +12,7 @@ * Result Factory * * @api + * @since 100.0.2 */ class ResultFactory { diff --git a/lib/internal/Magento/Framework/Controller/ResultInterface.php b/lib/internal/Magento/Framework/Controller/ResultInterface.php index f20f32078a9b5..3fe42ae07f566 100644 --- a/lib/internal/Magento/Framework/Controller/ResultInterface.php +++ b/lib/internal/Magento/Framework/Controller/ResultInterface.php @@ -14,6 +14,7 @@ * and be able to set it to the HTTP response * * @api + * @since 100.0.2 */ interface ResultInterface { diff --git a/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php b/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php index db30eb4844edb..4187650938bf9 100644 --- a/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php +++ b/lib/internal/Magento/Framework/Css/PreProcessor/Instruction/MagentoImport.php @@ -48,7 +48,7 @@ class MagentoImport implements PreProcessorInterface /** * @var \Magento\Framework\View\Design\Theme\ListInterface - * @deprecated 100.1.1 + * @deprecated 100.0.2 */ protected $themeList; diff --git a/lib/internal/Magento/Framework/CurrencyInterface.php b/lib/internal/Magento/Framework/CurrencyInterface.php index ca042bda849aa..8ddaae5459c78 100644 --- a/lib/internal/Magento/Framework/CurrencyInterface.php +++ b/lib/internal/Magento/Framework/CurrencyInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface CurrencyInterface { diff --git a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php index f654fd263f605..d4c6108295809 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php +++ b/lib/internal/Magento/Framework/DB/Adapter/AdapterInterface.php @@ -12,6 +12,7 @@ * Magento Database Adapter Interface * * @api + * @since 100.0.2 */ interface AdapterInterface { @@ -36,7 +37,7 @@ interface AdapterInterface const INSERT_ON_DUPLICATE = 1; const INSERT_IGNORE = 2; - + /** Strategy for updating data in table. See https://dev.mysql.com/doc/refman/5.7/en/replace.html */ const REPLACE = 4; @@ -258,7 +259,7 @@ public function renameTable($oldTableName, $newTableName, $schemaName = null); * * @param string $tableName * @param string $columnName - * @param array|string $definition string specific or universal array DB Server definition + * @param array|string $definition string specific or universal array DB Server definition * @param string $schemaName * @return \Magento\Framework\DB\Adapter\AdapterInterface */ @@ -273,7 +274,7 @@ public function addColumn($tableName, $columnName, $definition, $schemaName = nu * @param string $oldColumnName * @param string $newColumnName * @param array|string $definition - * @param boolean $flushData flush table statistic + * @param boolean $flushData flush table statistic * @param string $schemaName * @return \Magento\Framework\DB\Adapter\AdapterInterface */ @@ -323,8 +324,8 @@ public function tableColumnExists($tableName, $columnName, $schemaName = null); * * @param string $tableName * @param string $indexName - * @param string|array $fields the table column name or array of ones - * @param string $indexType the index type + * @param string|array $fields the table column name or array of ones + * @param string $indexType the index type * @param string $schemaName * @return \Zend_Db_Statement_Interface */ @@ -468,7 +469,7 @@ public function insertMultiple($table, array $data); * array('value1', 'value2') * * @param string $table - * @param string[] $columns the data array column map + * @param string[] $columns the data array column map * @param array $data * @return int */ @@ -550,7 +551,7 @@ public function fetchAll($sql, $bind = [], $fetchMode = null); * @param string|\Magento\Framework\DB\Select $sql An SQL SELECT statement. * @param mixed $bind Data to bind into SELECT placeholders. * @param mixed $fetchMode Override current fetch mode. - * @return array + * @return mixed Array, object, or scalar depending on fetch mode. */ public function fetchRow($sql, $bind = [], $fetchMode = null); @@ -750,7 +751,7 @@ public function saveDdlCache($tableCacheKey, $ddlType, $data); * Return false if cache does not exists * * @param string $tableCacheKey the table cache key - * @param int $ddlType the DDL constant + * @param int $ddlType the DDL constant * @return string|array|int|false */ public function loadDdlCache($tableCacheKey, $ddlType); @@ -864,7 +865,7 @@ public function getGreatestSql(array $data); * * @see INTERVAL_* constants for $unit * - * @param \Zend_Db_Expr|string $date quoted field name or SQL statement + * @param \Zend_Db_Expr|string $date quoted field name or SQL statement * @param int $interval * @param string $unit * @return \Zend_Db_Expr @@ -876,7 +877,7 @@ public function getDateAddSql($date, $interval, $unit); * * @see INTERVAL_* constants for $unit * - * @param \Zend_Db_Expr|string $date quoted field name or SQL statement + * @param \Zend_Db_Expr|string $date quoted field name or SQL statement * @param int|string $interval * @param string $unit * @return \Zend_Db_Expr @@ -895,7 +896,7 @@ public function getDateSubSql($date, $interval, $unit); * %m Month, numeric (00..12) * %Y Year, numeric, four digits * - * @param \Zend_Db_Expr|string $date quoted field name or SQL statement + * @param \Zend_Db_Expr|string $date quoted field name or SQL statement * @param string $format * @return \Zend_Db_Expr */ @@ -932,7 +933,7 @@ public function getStandardDeviationSql($expressionField); * * @see INTERVAL_* constants for $unit * - * @param \Zend_Db_Expr|string $date quoted field name or SQL statement + * @param \Zend_Db_Expr|string $date quoted field name or SQL statement * @param string $unit * @return \Zend_Db_Expr */ @@ -951,9 +952,9 @@ public function getTableName($tableName); /** * Build a trigger name based on table name and trigger details * - * @param string $tableName The table that is the subject of the trigger - * @param string $time Either "before" or "after" - * @param string $event The DB level event which activates the trigger, i.e. "update" or "insert" + * @param string $tableName The table that is the subject of the trigger + * @param string $time Either "before" or "after" + * @param string $event The DB level event which activates the trigger, i.e. "update" or "insert" * @return string */ public function getTriggerName($tableName, $time, $event); @@ -964,7 +965,7 @@ public function getTriggerName($tableName, $time, $event); * Check index name length and allowed symbols * * @param string $tableName - * @param string|array $fields the columns list + * @param string|array $fields the columns list * @param string $indexType * @return string */ diff --git a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php index 7db91c06d9649..00c216d1f2100 100644 --- a/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php +++ b/lib/internal/Magento/Framework/DB/Adapter/Pdo/Mysql.php @@ -639,7 +639,7 @@ public function query($sql, $bind = []) * @throws Zend_Db_Adapter_Exception To re-throw \PDOException. * @throws LocalizedException In case multiple queries are attempted at once, to protect from SQL injection * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function multiQuery($sql, $bind = []) { @@ -2776,7 +2776,7 @@ public function addIndex( if (in_array(strtolower($indexType), ['primary', 'unique'])) { $match = []; // phpstan:ignore - if (preg_match('#SQLSTATE\[23000\]: [^:]+: 1062[^\']+\'([\d-\.]+)\'#', $e->getMessage(), $match)) { + if (preg_match('#SQLSTATE\[23000\]: [^:]+: 1062[^\']+\'([\d.-]+)\'#', $e->getMessage(), $match)) { $ids = explode('-', $match[1]); $this->_removeDuplicateEntry($tableName, $fields, $ids); continue; @@ -2788,6 +2788,7 @@ public function addIndex( $this->resetDdlCache($tableName, $schemaName); + // @phpstan-ignore-next-line return $result; } @@ -3816,7 +3817,7 @@ protected function _getInsertSqlQuery($tableName, array $columns, array $values, * @param array $columns * @param array $values * @return string - * @since 100.2.0 + * @since 101.0.0 */ protected function _getReplaceSqlQuery($tableName, array $columns, array $values) { diff --git a/lib/internal/Magento/Framework/DB/Ddl/Table.php b/lib/internal/Magento/Framework/DB/Ddl/Table.php index 9d343806d66b3..e812b49f49d23 100644 --- a/lib/internal/Magento/Framework/DB/Ddl/Table.php +++ b/lib/internal/Magento/Framework/DB/Ddl/Table.php @@ -11,6 +11,7 @@ * Data Definition for table * * @api + * @since 100.0.2 */ class Table { diff --git a/lib/internal/Magento/Framework/DB/Ddl/Trigger.php b/lib/internal/Magento/Framework/DB/Ddl/Trigger.php index 94427c3d9c39d..1fd58be46d6c7 100644 --- a/lib/internal/Magento/Framework/DB/Ddl/Trigger.php +++ b/lib/internal/Magento/Framework/DB/Ddl/Trigger.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Trigger { diff --git a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php index 2fee8d55fb673..21192dc1a2dd4 100644 --- a/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php +++ b/lib/internal/Magento/Framework/DB/Query/BatchRangeIterator.php @@ -35,7 +35,7 @@ class BatchRangeIterator implements BatchIteratorInterface /** * @var string - * @deprecated unused class property + * @deprecated 102.0.0 unused class property */ private $rangeFieldAlias; diff --git a/lib/internal/Magento/Framework/DB/Query/Generator.php b/lib/internal/Magento/Framework/DB/Query/Generator.php index 0538aec760bd7..af119b16780ee 100644 --- a/lib/internal/Magento/Framework/DB/Query/Generator.php +++ b/lib/internal/Magento/Framework/DB/Query/Generator.php @@ -134,7 +134,7 @@ public function generate( * @return BatchIteratorInterface * @throws LocalizedException Throws if incorrect "FROM" part in \Select exists * @see \Magento\Framework\DB\Query\Generator - * @deprecated 100.2.0 This is a temporary solution which is made due to the fact that we + * @deprecated 100.1.8 This is a temporary solution which is made due to the fact that we * can't change method generate() in version 2.1 due to a backwards incompatibility. * In 2.2 version need to use original method generate() with additional parameter. */ diff --git a/lib/internal/Magento/Framework/DB/Select.php b/lib/internal/Magento/Framework/DB/Select.php index 075aa6b24faa7..7d2799cf50679 100644 --- a/lib/internal/Magento/Framework/DB/Select.php +++ b/lib/internal/Magento/Framework/DB/Select.php @@ -28,6 +28,7 @@ * @method \Magento\Framework\DB\Select distinct($flag = true) * @method \Magento\Framework\DB\Select reset($part = null) * @method \Magento\Framework\DB\Select columns($cols = '*', $correlationName = null) + * @since 100.0.2 */ class Select extends \Zend_Db_Select { diff --git a/lib/internal/Magento/Framework/DB/SelectFactory.php b/lib/internal/Magento/Framework/DB/SelectFactory.php index 3c64e78839c4d..306e649addff8 100644 --- a/lib/internal/Magento/Framework/DB/SelectFactory.php +++ b/lib/internal/Magento/Framework/DB/SelectFactory.php @@ -32,7 +32,6 @@ class SelectFactory /** * @param SelectRenderer $selectRenderer * @param array $parts - * @since 100.1.0 */ public function __construct( SelectRenderer $selectRenderer, diff --git a/lib/internal/Magento/Framework/DB/Sql/ColumnValueExpression.php b/lib/internal/Magento/Framework/DB/Sql/ColumnValueExpression.php index cf033dbe297f2..b51da7c47936b 100644 --- a/lib/internal/Magento/Framework/DB/Sql/ColumnValueExpression.php +++ b/lib/internal/Magento/Framework/DB/Sql/ColumnValueExpression.php @@ -10,7 +10,7 @@ * * Just a wrapper over Expression for implementing the specific type of expression. * @api - * @since 100.2.0 + * @since 100.1.8 */ class ColumnValueExpression extends Expression { diff --git a/lib/internal/Magento/Framework/DB/TemporaryTableService.php b/lib/internal/Magento/Framework/DB/TemporaryTableService.php index 881ebbe5c85ad..750b65217e50f 100644 --- a/lib/internal/Magento/Framework/DB/TemporaryTableService.php +++ b/lib/internal/Magento/Framework/DB/TemporaryTableService.php @@ -12,7 +12,7 @@ * Use this class to create an index with that you want to query later for quick data access * * @api - * @since 100.2.0 + * @since 100.1.8 */ class TemporaryTableService { @@ -43,7 +43,6 @@ class TemporaryTableService * @param \Magento\Framework\Math\Random $random * @param string[] $allowedIndexMethods * @param string[] $allowedEngines - * @since 100.2.0 */ public function __construct( \Magento\Framework\Math\Random $random, @@ -79,7 +78,7 @@ public function __construct( * @param string $dbEngine * @return string * @throws \InvalidArgumentException - * @since 100.2.0 + * @since 100.1.8 */ public function createFromSelect( Select $select, @@ -150,7 +149,7 @@ public function createFromSelect( * * @param string $name * @return bool - * @since 100.2.0 + * @since 100.1.8 */ public function dropTable($name) { diff --git a/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php b/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php index e448095f0f066..cf29393820f8b 100644 --- a/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php +++ b/lib/internal/Magento/Framework/DB/Test/Unit/Adapter/Pdo/MysqlTest.php @@ -8,6 +8,7 @@ namespace Magento\Framework\DB\Test\Unit\Adapter\Pdo; use Magento\Framework\DB\Adapter\AdapterInterface; +use Magento\Framework\DB\Adapter\Pdo\Mysql as PdoMysqlAdapter; use Magento\Framework\DB\LoggerInterface; use Magento\Framework\DB\Select; use Magento\Framework\DB\Select\SelectRenderer; @@ -29,20 +30,6 @@ class MysqlTest extends TestCase { const CUSTOM_ERROR_HANDLER_MESSAGE = 'Custom error handler message'; - /** - * Adapter for test - * - * @var \Magento\Framework\DB\Adapter\Pdo\Mysql|MockObject - */ - protected $_adapter; - - /** - * Mock DB adapter for DDL query tests - * - * @var \Magento\Framework\DB\Adapter\Pdo\Mysql|MockObject - */ - protected $_mockAdapter; - /** * @var SelectFactory|MockObject */ @@ -58,89 +45,31 @@ class MysqlTest extends TestCase */ private $serializerMock; + /** + * @var MockObject|\Zend_Db_Profiler + */ + private $profiler; + + /** + * @var \PDO|MockObject + */ + private $connection; + /** * Setup */ protected function setUp(): void { - $string = $this->createMock(StringUtils::class); - $dateTime = $this->createMock(DateTime::class); - $logger = $this->getMockForAbstractClass(LoggerInterface::class); - $selectFactory = $this->getMockBuilder(SelectFactory::class) - ->disableOriginalConstructor() - ->getMock(); $this->serializerMock = $this->getMockBuilder(SerializerInterface::class) ->disableOriginalConstructor() ->getMockForAbstractClass(); $this->schemaListenerMock = $this->getMockBuilder(SchemaListener::class) ->disableOriginalConstructor() ->getMock(); - $this->_mockAdapter = $this->getMockBuilder(\Magento\Framework\DB\Adapter\Pdo\Mysql::class) - ->setMethods(['beginTransaction', 'getTransactionLevel', 'getSchemaListener']) - ->setConstructorArgs( - [ - 'string' => $string, - 'dateTime' => $dateTime, - 'logger' => $logger, - 'selectFactory' => $selectFactory, - 'config' => [ - 'dbname' => 'dbname', - 'username' => 'user', - 'password' => 'password', - ], - 'serializer' => $this->serializerMock - ] - ) - ->getMock(); - - $this->_mockAdapter->expects($this->any()) - ->method('getTransactionLevel') - ->willReturn(1); - - $this->_adapter = $this->getMockBuilder(\Magento\Framework\DB\Adapter\Pdo\Mysql::class) - ->setMethods( - [ - 'getCreateTable', - '_connect', - '_beginTransaction', - '_commit', - '_rollBack', - 'query', - 'fetchRow', - 'getSchemaListener' - ] - )->setConstructorArgs( - [ - 'string' => $string, - 'dateTime' => $dateTime, - 'logger' => $logger, - 'selectFactory' => $selectFactory, - 'config' => [ - 'dbname' => 'not_exists', - 'username' => 'not_valid', - 'password' => 'not_valid', - ], - 'serializer' => $this->serializerMock, - ] - ) - ->getMock(); - $this->_mockAdapter->expects($this->any()) - ->method('getSchemaListener') - ->willReturn($this->schemaListenerMock); - $this->_adapter->expects($this->any()) - ->method('getSchemaListener') - ->willReturn($this->schemaListenerMock); - - $profiler = $this->createMock( + $this->profiler = $this->createMock( \Zend_Db_Profiler::class ); - - $resourceProperty = new \ReflectionProperty( - get_class($this->_adapter), - '_profiler' - ); - $resourceProperty->setAccessible(true); - $resourceProperty->setValue($this->_adapter, $profiler); + $this->connection = $this->createMock(\PDO::class); } /** @@ -148,7 +77,8 @@ protected function setUp(): void */ public function testPrepareColumnValueForBigint($value, $expectedResult) { - $result = $this->_adapter->prepareColumnValue( + $adapter = $this->getMysqlPdoAdapterMock([]); + $result = $adapter->prepareColumnValue( ['DATA_TYPE' => 'bigint'], $value ); @@ -189,8 +119,9 @@ public function bigintResultProvider() */ public function testCheckNotDdlTransaction($query) { + $mockAdapter = $this->getMysqlPdoAdapterMockForDdlQueryTest(); try { - $this->_mockAdapter->query($query); + $mockAdapter->query($query); } catch (\Exception $e) { $this->assertStringNotContainsString( $e->getMessage(), @@ -198,10 +129,10 @@ public function testCheckNotDdlTransaction($query) ); } - $select = new Select($this->_mockAdapter, new SelectRenderer([])); + $select = new Select($mockAdapter, new SelectRenderer([])); $select->from('user'); try { - $this->_mockAdapter->query($select); + $mockAdapter->query($select); } catch (\Exception $e) { $this->assertStringNotContainsString( $e->getMessage(), @@ -219,7 +150,7 @@ public function testCheckDdlTransaction($ddlQuery) { $this->expectException('Exception'); $this->expectExceptionMessage('DDL statements are not allowed in transactions'); - $this->_mockAdapter->query($ddlQuery); + $this->getMysqlPdoAdapterMockForDdlQueryTest()->query($ddlQuery); } public function testMultipleQueryException() @@ -229,7 +160,7 @@ public function testMultipleQueryException() $sql = "SELECT COUNT(*) AS _num FROM test; "; $sql .= "INSERT INTO test(id) VALUES (1); "; $sql .= "SELECT COUNT(*) AS _num FROM test; "; - $this->_mockAdapter->query($sql); + $this->getMysqlPdoAdapterMockForDdlQueryTest()->query($sql); } /** @@ -265,8 +196,9 @@ public static function sqlQueryProvider() */ public function testAsymmetricRollBackFailure() { + $adapter = $this->getMysqlPdoAdapterMock([]); $this->expectExceptionMessage(AdapterInterface::ERROR_ASYMMETRIC_ROLLBACK_MESSAGE); - $this->_adapter->rollBack(); + $adapter->rollBack(); } /** @@ -274,8 +206,9 @@ public function testAsymmetricRollBackFailure() */ public function testAsymmetricCommitFailure() { + $adapter = $this->getMysqlPdoAdapterMock([]); $this->expectExceptionMessage(AdapterInterface::ERROR_ASYMMETRIC_COMMIT_MESSAGE); - $this->_adapter->commit(); + $adapter->commit(); } /** @@ -283,11 +216,13 @@ public function testAsymmetricCommitFailure() */ public function testAsymmetricCommitSuccess() { - $this->assertEquals(0, $this->_adapter->getTransactionLevel()); - $this->_adapter->beginTransaction(); - $this->assertEquals(1, $this->_adapter->getTransactionLevel()); - $this->_adapter->commit(); - $this->assertEquals(0, $this->_adapter->getTransactionLevel()); + $adapter = $this->getMysqlPdoAdapterMock(['_connect']); + $this->addConnectionMock($adapter); + $this->assertEquals(0, $adapter->getTransactionLevel()); + $adapter->beginTransaction(); + $this->assertEquals(1, $adapter->getTransactionLevel()); + $adapter->commit(); + $this->assertEquals(0, $adapter->getTransactionLevel()); } /** @@ -295,11 +230,13 @@ public function testAsymmetricCommitSuccess() */ public function testAsymmetricRollBackSuccess() { - $this->assertEquals(0, $this->_adapter->getTransactionLevel()); - $this->_adapter->beginTransaction(); - $this->assertEquals(1, $this->_adapter->getTransactionLevel()); - $this->_adapter->rollBack(); - $this->assertEquals(0, $this->_adapter->getTransactionLevel()); + $adapter = $this->getMysqlPdoAdapterMock(['_connect']); + $this->addConnectionMock($adapter); + $this->assertEquals(0, $adapter->getTransactionLevel()); + $adapter->beginTransaction(); + $this->assertEquals(1, $adapter->getTransactionLevel()); + $adapter->rollBack(); + $this->assertEquals(0, $adapter->getTransactionLevel()); } /** @@ -307,21 +244,22 @@ public function testAsymmetricRollBackSuccess() */ public function testNestedTransactionCommitSuccess() { - $this->_adapter->expects($this->exactly(2)) + $adapter = $this->getMysqlPdoAdapterMock(['_connect', '_beginTransaction', '_commit']); + $adapter->expects($this->exactly(2)) ->method('_connect'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_beginTransaction'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_commit'); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->assertEquals(3, $this->_adapter->getTransactionLevel()); - $this->_adapter->commit(); - $this->_adapter->commit(); - $this->_adapter->commit(); - $this->assertEquals(0, $this->_adapter->getTransactionLevel()); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $this->assertEquals(3, $adapter->getTransactionLevel()); + $adapter->commit(); + $adapter->commit(); + $adapter->commit(); + $this->assertEquals(0, $adapter->getTransactionLevel()); } /** @@ -329,21 +267,22 @@ public function testNestedTransactionCommitSuccess() */ public function testNestedTransactionRollBackSuccess() { - $this->_adapter->expects($this->exactly(2)) + $adapter = $this->getMysqlPdoAdapterMock(['_connect', '_beginTransaction', '_rollBack']); + $adapter->expects($this->exactly(2)) ->method('_connect'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_beginTransaction'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_rollBack'); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->assertEquals(3, $this->_adapter->getTransactionLevel()); - $this->_adapter->rollBack(); - $this->_adapter->rollBack(); - $this->_adapter->rollBack(); - $this->assertEquals(0, $this->_adapter->getTransactionLevel()); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $this->assertEquals(3, $adapter->getTransactionLevel()); + $adapter->rollBack(); + $adapter->rollBack(); + $adapter->rollBack(); + $this->assertEquals(0, $adapter->getTransactionLevel()); } /** @@ -351,21 +290,22 @@ public function testNestedTransactionRollBackSuccess() */ public function testNestedTransactionLastRollBack() { - $this->_adapter->expects($this->exactly(2)) + $adapter = $this->getMysqlPdoAdapterMock(['_connect', '_beginTransaction', '_rollBack']); + $adapter->expects($this->exactly(2)) ->method('_connect'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_beginTransaction'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_rollBack'); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->assertEquals(3, $this->_adapter->getTransactionLevel()); - $this->_adapter->commit(); - $this->_adapter->commit(); - $this->_adapter->rollBack(); - $this->assertEquals(0, $this->_adapter->getTransactionLevel()); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $this->assertEquals(3, $adapter->getTransactionLevel()); + $adapter->commit(); + $adapter->commit(); + $adapter->rollBack(); + $this->assertEquals(0, $adapter->getTransactionLevel()); } /** @@ -374,20 +314,21 @@ public function testNestedTransactionLastRollBack() */ public function testIncompleteRollBackFailureOnCommit() { - $this->_adapter->expects($this->exactly(2))->method('_connect'); + $adapter = $this->getMysqlPdoAdapterMock(['_connect']); + $this->addConnectionMock($adapter); try { - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->_adapter->rollBack(); - $this->_adapter->commit(); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->rollBack(); + $adapter->commit(); throw new \Exception('Test Failed!'); } catch (\Exception $e) { $this->assertEquals( AdapterInterface::ERROR_ROLLBACK_INCOMPLETE_MESSAGE, $e->getMessage() ); - $this->_adapter->rollBack(); + $adapter->rollBack(); } } @@ -397,20 +338,21 @@ public function testIncompleteRollBackFailureOnCommit() */ public function testIncompleteRollBackFailureOnBeginTransaction() { - $this->_adapter->expects($this->exactly(2))->method('_connect'); + $adapter = $this->getMysqlPdoAdapterMock(['_connect']); + $this->addConnectionMock($adapter); try { - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->_adapter->rollBack(); - $this->_adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->rollBack(); + $adapter->beginTransaction(); throw new \Exception('Test Failed!'); } catch (\Exception $e) { $this->assertEquals( AdapterInterface::ERROR_ROLLBACK_INCOMPLETE_MESSAGE, $e->getMessage() ); - $this->_adapter->rollBack(); + $adapter->rollBack(); } } @@ -419,24 +361,27 @@ public function testIncompleteRollBackFailureOnBeginTransaction() */ public function testSequentialTransactionsSuccess() { - $this->_adapter->expects($this->exactly(4)) + $adapter = $this->getMysqlPdoAdapterMock(['_connect', '_beginTransaction', '_rollBack', '_commit']); + $this->addConnectionMock($adapter); + + $adapter->expects($this->exactly(4)) ->method('_connect'); - $this->_adapter->expects($this->exactly(2)) + $adapter->expects($this->exactly(2)) ->method('_beginTransaction'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_rollBack'); - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('_commit'); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->_adapter->beginTransaction(); - $this->_adapter->rollBack(); - $this->_adapter->rollBack(); - $this->_adapter->rollBack(); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->beginTransaction(); + $adapter->rollBack(); + $adapter->rollBack(); + $adapter->rollBack(); - $this->_adapter->beginTransaction(); - $this->_adapter->commit(); + $adapter->beginTransaction(); + $adapter->commit(); } /** @@ -444,6 +389,7 @@ public function testSequentialTransactionsSuccess() */ public function testInsertOnDuplicateWithQuotedColumnName() { + $adapter = $this->getMysqlPdoAdapterMock([]); $table = 'some_table'; $data = [ 'index' => 'indexValue', @@ -457,12 +403,12 @@ public function testInsertOnDuplicateWithQuotedColumnName() $stmtMock = $this->createMock(\Zend_Db_Statement_Pdo::class); $bind = ['indexValue', 'rowValue', 'selectValue', 'insertValue']; - $this->_adapter->expects($this->once()) + $adapter->expects($this->once()) ->method('query') ->with($sqlQuery, $bind) ->willReturn($stmtMock); - $this->_adapter->insertOnDuplicate($table, $data, $fields); + $adapter->insertOnDuplicate($table, $data, $fields); } /** @@ -475,15 +421,14 @@ public function testInsertOnDuplicateWithQuotedColumnName() */ public function testAddColumn($options, $expectedQuery) { - $connectionMock = $this->createPartialMock( - \Magento\Framework\DB\Adapter\Pdo\Mysql::class, + $adapter = $this->getMysqlPdoAdapterMock( ['tableColumnExists', '_getTableName', 'rawQuery', 'resetDdlCache', 'quote', 'getSchemaListener'] ); - $connectionMock->expects($this->any())->method('getSchemaListener')->willReturn($this->schemaListenerMock); - $connectionMock->expects($this->any())->method('_getTableName')->willReturnArgument(0); - $connectionMock->expects($this->any())->method('quote')->willReturnArgument(0); - $connectionMock->expects($this->once())->method('rawQuery')->with($expectedQuery); - $connectionMock->addColumn('tableName', 'columnName', $options); + $adapter->expects($this->any())->method('getSchemaListener')->willReturn($this->schemaListenerMock); + $adapter->expects($this->any())->method('_getTableName')->willReturnArgument(0); + $adapter->expects($this->any())->method('quote')->willReturnArgument(0); + $adapter->expects($this->once())->method('rawQuery')->with($expectedQuery); + $adapter->addColumn('tableName', 'columnName', $options); } /** @@ -514,7 +459,7 @@ public function addColumnDataProvider() */ public function testGetIndexName($name, $fields, $indexType, $expectedName) { - $resultIndexName = $this->_mockAdapter->getIndexName($name, $fields, $indexType); + $resultIndexName = $this->getMysqlPdoAdapterMockForDdlQueryTest()->getIndexName($name, $fields, $indexType); $this->assertStringStartsWith($expectedName, $resultIndexName); } @@ -556,4 +501,296 @@ public function testConfigValidationByPortWithException() ['config' => ['host' => 'localhost', 'port' => '33390']] ); } + + /** + * @param string $indexName + * @param string $indexType + * @param array $keyLists + * @param \Exception $exception + * @param string $query + * @throws \ReflectionException + * @throws \Zend_Db_Exception + * @dataProvider addIndexWithDuplicationsInDBDataProvider + */ + public function testAddIndexWithDuplicationsInDB( + string $indexName, + string $indexType, + array $keyLists, + string $query, + string $exceptionMessage, + array $ids + ) { + $tableName = 'core_table'; + $fields = ['sku', 'field2']; + $quotedFields = [$this->quoteIdentifier('sku'), $this->quoteIdentifier('field2')]; + + $exception = new \Exception( + sprintf( + $exceptionMessage, + $tableName, + implode(',', $quotedFields) + ) + ); + + $this->expectException(get_class($exception)); + $this->expectExceptionMessage($exception->getMessage()); + + $adapter = $this->getMysqlPdoAdapterMock([ + 'describeTable', + 'getIndexList', + 'quoteIdentifier', + '_getTableName', + 'rawQuery', + '_removeDuplicateEntry', + 'resetDdlCache', + ]); + $this->addConnectionMock($adapter); + $columns = ['sku' => [], 'field2' => [], 'comment' => [], 'timestamp' => []]; + $schemaName = null; + + $this->schemaListenerMock + ->expects($this->once()) + ->method('addIndex') + ->with($tableName, $indexName, $fields, $indexType); + + $adapter + ->expects($this->once()) + ->method('describeTable') + ->with($tableName, $schemaName) + ->willReturn($columns); + $adapter + ->expects($this->once()) + ->method('getIndexList') + ->with($tableName, $schemaName) + ->willReturn($keyLists); + $adapter + ->expects($this->once()) + ->method('_getTableName') + ->with($tableName, $schemaName) + ->willReturn($tableName); + $adapter + ->method('quoteIdentifier') + ->willReturnMap([ + [$tableName, false, $this->quoteIdentifier($tableName)], + [$indexName, false, $this->quoteIdentifier($indexName)], + [$fields[0], false, $quotedFields[0]], + [$fields[1], false, $quotedFields[1]], + ]); + $adapter + ->expects($this->once()) + ->method('rawQuery') + ->with( + sprintf( + $query, + $tableName, + implode(',', $quotedFields) + ) + ) + ->willThrowException($exception); + $adapter + ->expects($this->exactly((int)in_array(strtolower($indexType), ['primary', 'unique']))) + ->method('_removeDuplicateEntry') + ->with($tableName, $fields, $ids) + ->willThrowException($exception); + $adapter + ->expects($this->never()) + ->method('resetDdlCache'); + + $adapter->addIndex($tableName, $indexName, $fields, $indexType); + } + + /** + * @return array + */ + public function addIndexWithDuplicationsInDBDataProvider(): array + { + return [ + 'New unique index' => [ + 'indexName' => 'SOME_UNIQUE_INDEX', + 'indexType' => AdapterInterface::INDEX_TYPE_UNIQUE, + 'keyLists' => [ + 'PRIMARY' => [ + 'INDEX_TYPE' => [ + AdapterInterface::INDEX_TYPE_PRIMARY + ] + ], + ], + 'query' => 'ALTER TABLE `%s` ADD UNIQUE `SOME_UNIQUE_INDEX` (%s)', + 'exceptionMessage' => 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry \'1-1-1\' ' + . 'for key \'SOME_UNIQUE_INDEX\', query was: ' + . 'ALTER TABLE `%s` ADD UNIQUE `SOME_UNIQUE_INDEX` (%s)', + 'ids' => [1, 1, 1], + ], + 'Existing unique index' => [ + 'indexName' => 'SOME_UNIQUE_INDEX', + 'indexType' => AdapterInterface::INDEX_TYPE_UNIQUE, + 'keyLists' => [ + 'PRIMARY' => [ + 'INDEX_TYPE' => [ + AdapterInterface::INDEX_TYPE_PRIMARY + ] + ], + 'SOME_UNIQUE_INDEX' => [ + 'INDEX_TYPE' => [ + AdapterInterface::INDEX_TYPE_UNIQUE + ] + ], + ], + 'query' => 'ALTER TABLE `%s` DROP INDEX `SOME_UNIQUE_INDEX`, ADD UNIQUE `SOME_UNIQUE_INDEX` (%s)', + 'exceptionMessage' => 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry \'1-2-5\' ' + . 'for key \'SOME_UNIQUE_INDEX\', query was: ' + . 'ALTER TABLE `%s` DROP INDEX `SOME_UNIQUE_INDEX`, ADD UNIQUE `SOME_UNIQUE_INDEX` (%s)', + 'ids' => [1, 2, 5], + ], + 'New primary index' => [ + 'indexName' => 'PRIMARY', + 'indexType' => AdapterInterface::INDEX_TYPE_PRIMARY, + 'keyLists' => [ + 'SOME_UNIQUE_INDEX' => [ + 'INDEX_TYPE' => [ + AdapterInterface::INDEX_TYPE_UNIQUE + ] + ], + ], + 'query' => 'ALTER TABLE `%s` ADD PRIMARY KEY (%s)', + 'exceptionMessage' => 'SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry \'1-3-4\' ' + . 'for key \'PRIMARY\', query was: ' + . 'ALTER TABLE `%s` ADD PRIMARY KEY (%s)', + 'ids' => [1, 3, 4], + ], + ]; + } + + /** + * @param string $field + * @return string + */ + private function quoteIdentifier(string $field): string + { + if (strpos($field, '`') !== 0) { + $field = '`' . $field . '`'; + } + + return $field; + } + + public function testAddIndexForNonExitingField() + { + $tableName = 'core_table'; + $this->expectException(\Zend_Db_Exception::class); + $this->expectExceptionMessage(sprintf( + 'There is no field "%s" that you are trying to create an index on "%s"', + 'sku', + $tableName + )); + + $adapter = $this->getMysqlPdoAdapterMock(['describeTable', 'getIndexList', 'quoteIdentifier', '_getTableName']); + + $fields = ['sku', 'field2']; + $schemaName = null; + + $adapter + ->expects($this->once()) + ->method('describeTable') + ->with($tableName, $schemaName) + ->willReturn([]); + $adapter + ->expects($this->once()) + ->method('getIndexList') + ->with($tableName, $schemaName) + ->willReturn([]); + $adapter + ->expects($this->once()) + ->method('_getTableName') + ->with($tableName, $schemaName) + ->willReturn($tableName); + $adapter + ->method('quoteIdentifier') + ->willReturnMap([ + [$tableName, $tableName], + ]); + + $adapter->addIndex($tableName, 'SOME_INDEX', $fields); + } + + /** + * @return MockObject|PdoMysqlAdapter + * @throws \ReflectionException + */ + private function getMysqlPdoAdapterMockForDdlQueryTest(): MockObject + { + $mockAdapter = $this->getMysqlPdoAdapterMock(['beginTransaction', 'getTransactionLevel', 'getSchemaListener']); + $mockAdapter + ->method('getTransactionLevel') + ->willReturn(1); + + return $mockAdapter; + } + + /** + * @param array $methods + * @return MockObject|PdoMysqlAdapter + * @throws \ReflectionException + */ + private function getMysqlPdoAdapterMock(array $methods): MockObject + { + if (empty($methods)) { + $methods = array_merge($methods, ['query']); + } + $methods = array_unique(array_merge($methods, ['getSchemaListener'])); + + $string = $this->createMock(StringUtils::class); + $dateTime = $this->createMock(DateTime::class); + $logger = $this->getMockForAbstractClass(LoggerInterface::class); + $selectFactory = $this->getMockBuilder(SelectFactory::class) + ->disableOriginalConstructor() + ->getMock(); + + $adapterMock = $this->getMockBuilder(PdoMysqlAdapter::class) + ->setMethods( + $methods + )->setConstructorArgs( + [ + 'string' => $string, + 'dateTime' => $dateTime, + 'logger' => $logger, + 'selectFactory' => $selectFactory, + 'config' => [ + 'dbname' => 'not_exists', + 'username' => 'not_valid', + 'password' => 'not_valid', + ], + 'serializer' => $this->serializerMock, + ] + ) + ->getMock(); + + $adapterMock + ->method('getSchemaListener') + ->willReturn($this->schemaListenerMock); + + /** add profiler Mock */ + $resourceProperty = new \ReflectionProperty( + get_class($adapterMock), + '_profiler' + ); + $resourceProperty->setAccessible(true); + $resourceProperty->setValue($adapterMock, $this->profiler); + + return $adapterMock; + } + + /** + * @param MockObject $pdoAdapterMock + * @throws \ReflectionException + */ + private function addConnectionMock(MockObject $pdoAdapterMock): void + { + $resourceProperty = new \ReflectionProperty( + get_class($pdoAdapterMock), + '_connection' + ); + $resourceProperty->setAccessible(true); + $resourceProperty->setValue($pdoAdapterMock, $this->connection); + } } diff --git a/lib/internal/Magento/Framework/DB/Tree.php b/lib/internal/Magento/Framework/DB/Tree.php index 1aeaf122131f6..6fbd014213bc8 100644 --- a/lib/internal/Magento/Framework/DB/Tree.php +++ b/lib/internal/Magento/Framework/DB/Tree.php @@ -18,7 +18,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * phpcs:ignoreFile * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ class Tree { @@ -82,7 +82,7 @@ class Tree * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function __construct($config = []) { @@ -156,7 +156,7 @@ public function __construct($config = []) * @param string $name * @return $this * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function setIdField($name) { @@ -170,7 +170,7 @@ public function setIdField($name) * @param string $name * @return $this * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function setLeftField($name) { @@ -184,7 +184,7 @@ public function setLeftField($name) * @param string $name * @return $this * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function setRightField($name) { @@ -198,7 +198,7 @@ public function setRightField($name) * @param string $name * @return $this * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function setLevelField($name) { @@ -212,7 +212,7 @@ public function setLevelField($name) * @param string $name * @return $this * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function setPidField($name) { @@ -226,7 +226,7 @@ public function setPidField($name) * @param string $name * @return $this * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function setTable($name) { @@ -237,7 +237,7 @@ public function setTable($name) /** * @return array * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function getKeys() { @@ -256,7 +256,7 @@ public function getKeys() * @param array $data * @return string * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function clear($data = []) { @@ -283,7 +283,7 @@ public function clear($data = []) * @param string|int $nodeId * @return array * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function getNodeInfo($nodeId) { @@ -303,7 +303,7 @@ public function getNodeInfo($nodeId) * @param array $data * @return false|string * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function appendChild($nodeId, $data) { @@ -371,7 +371,7 @@ public function appendChild($nodeId, $data) /** * @return array * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function checkNodes() { @@ -403,7 +403,7 @@ public function checkNodes() * @param string|int $nodeId * @return bool|Node|void * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function removeNode($nodeId) { @@ -479,7 +479,7 @@ public function removeNode($nodeId) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedFormalParameter) * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function moveNode($eId, $pId, $aId = 0) { @@ -815,7 +815,7 @@ public function moveNode($eId, $pId, $aId = 0) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function moveNodes($eId, $pId, $aId = 0) { @@ -1015,7 +1015,7 @@ public function moveNodes($eId, $pId, $aId = 0) * @param string $fields * @return void * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function addTable($tableName, $joinCondition, $fields = '*') { @@ -1026,7 +1026,7 @@ public function addTable($tableName, $joinCondition, $fields = '*') * @param Select $select * @return void * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ protected function _addExtTablesToSelect(Select &$select) { @@ -1041,7 +1041,7 @@ protected function _addExtTablesToSelect(Select &$select) * @param int $endLevel * @return NodeSet * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function getChildren($nodeId, $startLevel = 0, $endLevel = 0) { @@ -1088,7 +1088,7 @@ public function getChildren($nodeId, $startLevel = 0, $endLevel = 0) * @param string|int $nodeId * @return Node * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ public function getNode($nodeId) { diff --git a/lib/internal/Magento/Framework/DB/Tree/Node.php b/lib/internal/Magento/Framework/DB/Tree/Node.php index eb954a696e21e..507a09476abba 100644 --- a/lib/internal/Magento/Framework/DB/Tree/Node.php +++ b/lib/internal/Magento/Framework/DB/Tree/Node.php @@ -11,7 +11,7 @@ /** * @SuppressWarnings(PHPMD.UnusedPrivateField) * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ class Node { @@ -53,14 +53,14 @@ class Node /** * @var bool * - * @deprecated + * @deprecated 102.0.0 */ public $hasChild = false; /** * @var float|int * - * @deprecated + * @deprecated 102.0.0 */ public $numChild = 0; @@ -69,7 +69,7 @@ class Node * @param array $keys * @throws LocalizedException * - * @deprecated + * @deprecated 102.0.0 */ public function __construct($nodeData, $keys) { @@ -103,7 +103,7 @@ public function __construct($nodeData, $keys) * @param string $name * @return null|array * - * @deprecated + * @deprecated 102.0.0 */ public function getData($name) { @@ -117,7 +117,7 @@ public function getData($name) /** * @return int * - * @deprecated + * @deprecated 102.0.0 */ public function getLevel() { @@ -127,7 +127,7 @@ public function getLevel() /** * @return int * - * @deprecated + * @deprecated 102.0.0 */ public function getLeft() { @@ -137,7 +137,7 @@ public function getLeft() /** * @return int * - * @deprecated + * @deprecated 102.0.0 */ public function getRight() { @@ -147,7 +147,7 @@ public function getRight() /** * @return string|int * - * @deprecated + * @deprecated 102.0.0 */ public function getPid() { @@ -157,7 +157,7 @@ public function getPid() /** * @return string|int * - * @deprecated + * @deprecated 102.0.0 */ public function getId() { @@ -169,7 +169,7 @@ public function getId() * * @return bool * - * @deprecated + * @deprecated 102.0.0 */ public function isParent() { diff --git a/lib/internal/Magento/Framework/DB/Tree/NodeSet.php b/lib/internal/Magento/Framework/DB/Tree/NodeSet.php index 75e677b77ae45..e861a90f7cd0d 100644 --- a/lib/internal/Magento/Framework/DB/Tree/NodeSet.php +++ b/lib/internal/Magento/Framework/DB/Tree/NodeSet.php @@ -8,7 +8,7 @@ /** * TODO implements iterators * - * @deprecated Not used anymore. + * @deprecated 102.0.0 Not used anymore. */ class NodeSet implements \Iterator, \Countable { @@ -35,7 +35,7 @@ class NodeSet implements \Iterator, \Countable /** * Constructor * - * @deprecated + * @deprecated 102.0.0 */ public function __construct() { @@ -49,7 +49,7 @@ public function __construct() * @param Node $node * @return int * - * @deprecated + * @deprecated 102.0.0 */ public function addNode(Node $node) { @@ -61,7 +61,7 @@ public function addNode(Node $node) /** * @return int * - * @deprecated + * @deprecated 102.0.0 */ public function count() { @@ -71,7 +71,7 @@ public function count() /** * @return bool * - * @deprecated + * @deprecated 102.0.0 */ public function valid() { @@ -81,7 +81,7 @@ public function valid() /** * @return false|int * - * @deprecated + * @deprecated 102.0.0 */ public function next() { @@ -95,7 +95,7 @@ public function next() /** * @return int * - * @deprecated + * @deprecated 102.0.0 */ public function key() { @@ -105,7 +105,7 @@ public function key() /** * @return Node * - * @deprecated + * @deprecated 102.0.0 */ public function current() { @@ -115,7 +115,7 @@ public function current() /** * @return void * - * @deprecated + * @deprecated 102.0.0 */ public function rewind() { diff --git a/lib/internal/Magento/Framework/Data/AbstractSearchResult.php b/lib/internal/Magento/Framework/Data/AbstractSearchResult.php index f9272683005ce..05c8e39f52465 100644 --- a/lib/internal/Magento/Framework/Data/AbstractSearchResult.php +++ b/lib/internal/Magento/Framework/Data/AbstractSearchResult.php @@ -64,7 +64,7 @@ abstract class AbstractSearchResult extends AbstractDataObject implements Search /** * @var \Magento\Framework\DB\Select - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $select; diff --git a/lib/internal/Magento/Framework/Data/Argument/InterpreterInterface.php b/lib/internal/Magento/Framework/Data/Argument/InterpreterInterface.php index 2deabdf9829ae..178efc3b23463 100644 --- a/lib/internal/Magento/Framework/Data/Argument/InterpreterInterface.php +++ b/lib/internal/Magento/Framework/Data/Argument/InterpreterInterface.php @@ -9,6 +9,7 @@ * Interface that encapsulates complexity of expression computation * * @api + * @since 100.0.2 */ interface InterpreterInterface { diff --git a/lib/internal/Magento/Framework/Data/Collection.php b/lib/internal/Magento/Framework/Data/Collection.php index 7c9cf02ac6a47..51a066f2660dd 100644 --- a/lib/internal/Magento/Framework/Data/Collection.php +++ b/lib/internal/Magento/Framework/Data/Collection.php @@ -18,6 +18,7 @@ * TODO: Refactor use of \Magento\Framework\Option\ArrayInterface in library. * * @api + * @since 100.0.2 */ class Collection implements \IteratorAggregate, \Countable, ArrayInterface, CollectionDataSourceInterface { diff --git a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php index 8a22c9a1ce4fc..b829f063ac2de 100644 --- a/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php +++ b/lib/internal/Magento/Framework/Data/Collection/AbstractDb.php @@ -19,6 +19,7 @@ * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ abstract class AbstractDb extends \Magento\Framework\Data\Collection { diff --git a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php index 629b992b32cfd..6103a7df5bf0d 100644 --- a/lib/internal/Magento/Framework/Data/Collection/Filesystem.php +++ b/lib/internal/Magento/Framework/Data/Collection/Filesystem.php @@ -25,6 +25,7 @@ * At least one target directory must be set * * @api + * @since 100.0.2 */ class Filesystem extends \Magento\Framework\Data\Collection { diff --git a/lib/internal/Magento/Framework/Data/Form.php b/lib/internal/Magento/Framework/Data/Form.php index abeda0c17542e..a9cdfbf5f9ae7 100644 --- a/lib/internal/Magento/Framework/Data/Form.php +++ b/lib/internal/Magento/Framework/Data/Form.php @@ -16,6 +16,7 @@ /** * @api + * @since 100.0.2 */ class Form extends \Magento\Framework\Data\Form\AbstractForm { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php index ff1e3ba63ba8a..33f2c215b63bb 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/AbstractElement.php @@ -5,10 +5,13 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Data\Form; use Magento\Framework\Data\Form\AbstractForm; use Magento\Framework\Data\Form\Element\Renderer\RendererInterface; use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Data form abstract class @@ -17,6 +20,7 @@ * @api * @author Magento Core Team <core@magentocommerce.com> * @SuppressWarnings(PHPMD.NumberOfChildren) + * @since 100.0.2 */ abstract class AbstractElement extends AbstractForm { @@ -64,21 +68,51 @@ abstract class AbstractElement extends AbstractForm */ private $lockHtmlAttribute = 'data-locked'; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { $this->_escaper = $escaper; parent::__construct($factoryElement, $factoryCollection, $data); $this->_renderer = \Magento\Framework\Data\Form::getElementRenderer(); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + + /** + * Generate this element's ID. + * + * @return string + */ + private function generateElementId(): string + { + if (!$this->hasData('formelementhookid')) { + $this->setData('formelementhookid', 'elemId' .$this->random->getRandomString(10)); + } + + return $this->getData('formelementhookid'); } /** @@ -394,6 +428,46 @@ public function getBeforeElementHtml() return $this->getData('before_element_html'); } + /** + * Generate HTML to replace unsecure attributes. + * + * @return string + */ + private function generateAttributesSubstitute(): string + { + $html = ''; + + //Rendering element's style as separate tag. + if ($this->getStyle()) { + $selector = "*[formelementhookid='{$this->generateElementId()}']"; + if ($id = $this->getHtmlId()) { + $selector = "#{$id}"; + } + $html .= $this->secureRenderer->renderStyleAsTag($this->getStyle(), $selector); + } + + //Rendering each event listener as a separate script tag. + $events = array_filter( + $this->getHtmlAttributes(), + function (string $attribute): bool { + return mb_strpos($attribute, 'on') === 0; + } + ); + foreach ($events as $event) { + $eventShort = mb_substr($event, 2); + $methodName = 'getOn' .$eventShort; + if ($eventListener = $this->$methodName()) { + $html .= $this->secureRenderer->renderEventListenerAsTag( + $event, + $eventListener, + "*[formelementhookid='{$this->generateElementId()}']" + ); + } + } + + return $html; + } + /** * Get the after element html. * @@ -401,7 +475,7 @@ public function getBeforeElementHtml() */ public function getAfterElementHtml() { - return $this->getData('after_element_html'); + return $this->getData('after_element_html') .$this->generateAttributesSubstitute(); } /** @@ -507,6 +581,16 @@ public function serialize($attributes = [], $valueSeparator = '=', $fieldSeparat } else { unset($this->_data['checked']); } + $attributes[] = 'formelementhookid'; + $this->generateElementId(); + //Unset attributes that are to be rendered as separate tags + $attributes = array_filter( + $attributes, + function (string $attribute): bool { + return $attribute !== 'style' && mb_strpos($attribute, 'on') !== 0; + } + ); + return parent::serialize($attributes, $valueSeparator, $fieldSeparator, $quote); } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php b/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php index 2a68432207b23..ce6639a98db2c 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Checkboxes.php @@ -110,15 +110,16 @@ public function getElementHtml() } /** - * @param mixed $value - * @return string|void + * Was given value selected? + * + * @param string $value + * @return string|null */ public function getChecked($value) { - if ($checked = $this->getValue()) { - } elseif ($checked = $this->getData('checked')) { - } else { - return; + $checked = $this->getValue() ?? $this->getData('checked'); + if (!$checked) { + return null; } if (!is_array($checked)) { $checked = [(string)$checked]; @@ -130,12 +131,14 @@ public function getChecked($value) if (in_array((string)$value, $checked)) { return 'checked'; } - return; + return null; } /** - * @param mixed $value - * @return string + * Was value disabled for selection? + * + * @param string $value + * @return string|null */ public function getDisabled($value) { @@ -151,34 +154,40 @@ public function getDisabled($value) return 'disabled'; } } - return; + return null; } /** - * @param mixed $value - * @return mixed + * Get onclick event handler. + * + * @param string $value + * @return string|null */ - public function getOnclick($value) + public function getOnclick($value = '$value') { if ($onclick = $this->getData('onclick')) { return str_replace('$value', $value, $onclick); } - return; + return null; } /** - * @param mixed $value - * @return mixed + * Get onchange event handler. + * + * @param string $value + * @return string|null */ - public function getOnchange($value) + public function getOnchange($value = '$value') { if ($onchange = $this->getData('onchange')) { return str_replace('$value', $value, $onchange); } - return; + return null; } /** + * Render a checkbox. + * * @param array $option * @return string */ diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php b/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php index 70dca9ec16618..5847ab6eedd0b 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Editablemultiselect.php @@ -13,7 +13,10 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Form editable multiselect element. @@ -26,24 +29,40 @@ class Editablemultiselect extends \Magento\Framework\Data\Form\Element\Multisele private $serializer; /** - * Editablemultiselect constructor. + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer - * @throws \RuntimeException + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, array $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $random = $random ?? ObjectManager::getInstance()->get(Random::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); + $this->serializer = $serializer ?: ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->secureRenderer = $secureRenderer; + $this->random = $random; } /** @@ -77,8 +96,10 @@ public function getElementHtml() $jsObjectName = $this->getJsObjectName(); // TODO: TaxRateEditableMultiselect should be moved to a static .js module. - $html .= " - <script type='text/javascript'> + $html .= $this->secureRenderer->renderTag( + 'script', + ['type' => 'text/javascript'], + <<<script require([ 'jquery' ], function( $ ){ @@ -108,7 +129,11 @@ function check( tries, delay ){ check(8, 500); }); - </script>"; +script + , + false + ); + return $html; } @@ -121,9 +146,9 @@ function check( tries, delay ){ */ protected function _optionToHtml($option, $selected) { - $html = '<option value="' . $this->_escape($option['value']) . '"'; + $optionId = 'optId' .$this->random->getRandomString(8); + $html = '<option value="' . $this->_escape($option['value']) . '" id="' . $optionId . '" '; $html .= isset($option['title']) ? 'title="' . $this->_escape($option['title']) . '"' : ''; - $html .= isset($option['style']) ? 'style="' . $option['style'] . '"' : ''; if (in_array((string)$option['value'], $selected)) { $html .= ' selected="selected"'; } @@ -134,6 +159,10 @@ protected function _optionToHtml($option, $selected) } $html .= '>' . $this->_escape($option['label']) . '</option>' . "\n"; + if (!empty($option['style'])) { + $html .= $this->secureRenderer->renderStyleAsTag($option['style'], "#$optionId"); + } + return $html; } } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Editor.php b/lib/internal/Magento/Framework/Data/Form/Element/Editor.php index 41e27b1d431b2..7eaf7f0f4af37 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Editor.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Editor.php @@ -6,7 +6,10 @@ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Form editor element @@ -20,6 +23,16 @@ class Editor extends Textarea */ private $serializer; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * Editor constructor. * @param Factory $factoryElement @@ -27,6 +40,8 @@ class Editor extends Textarea * @param Escaper $escaper * @param array $data * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer + * @param Random|null $random + * @param SecureHtmlRenderer|null $secureRenderer * @throws \RuntimeException */ public function __construct( @@ -34,7 +49,9 @@ public function __construct( CollectionFactory $factoryCollection, Escaper $escaper, $data = [], - \Magento\Framework\Serialize\Serializer\Json $serializer = null + \Magento\Framework\Serialize\Serializer\Json $serializer = null, + ?Random $random = null, + ?SecureHtmlRenderer $secureRenderer = null ) { parent::__construct($factoryElement, $factoryCollection, $escaper, $data); @@ -45,8 +62,10 @@ public function __construct( $this->setType('textarea'); $this->setExtType('textarea'); } - $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() - ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->serializer = $serializer ?? ObjectManager::getInstance() + ->get(\Magento\Framework\Serialize\Serializer\Json::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); } /** @@ -120,9 +139,11 @@ public function getPluginConfigOptions($pluginName, $key = null) */ public function getElementHtml() { - $js = ' - <script type="text/javascript"> - //<![CDATA[ + $js = $this->secureRenderer->renderTag( + 'script', + ['type' => 'text/javascript'], + <<<script + //<![CDATA[ openEditorPopup = function(url, name, specs, parent) { if ((typeof popups == "undefined") || popups[name] == undefined || popups[name].closed) { if (typeof popups == "undefined") { @@ -142,7 +163,10 @@ public function getElementHtml() } } //]]> - </script>'; +script + , + false + ); if ($this->isEnabled()) { $jsSetupObject = 'wysiwyg' . $this->getHtmlId(); @@ -189,15 +213,21 @@ public function getElementHtml() if ($this->getPluginConfigOptions('magentowidget', 'window_url')) { $html = $this->_getButtonsHtml() . $js . parent::getElementHtml(); if ($this->getConfig('add_widgets')) { - $html .= '<script type="text/javascript"> - //<![CDATA[ - require(["jquery", "mage/translate", "mage/adminhtml/wysiwyg/widget"], function(jQuery){ - (function($) { - $.mage.translate.add(' . $this->serializer->serialize($this->getButtonTranslations()) . ') - })(jQuery); - }); - //]]> - </script>'; + $html .= $this->secureRenderer->renderTag( + 'script', + ['type' => 'text/javascript'], + <<<script + //<![CDATA[ + require(["jquery", "mage/translate", "mage/adminhtml/wysiwyg/widget"], function(jQuery){ + (function($) { + $.mage.translate.add({$this->serializer->serialize($this->getButtonTranslations())}) + })(jQuery); + }); + //]]>' +script + , + false + ); } $html = $this->_wrapIntoContainer($html); return $html; @@ -390,14 +420,20 @@ protected function _prepareOptions($options) */ protected function _getButtonHtml($data) { + $id = empty($data['id']) ? 'buttonId' .$this->random->getRandomString(10) : $data['id']; + $html = '<button type="button"'; $html .= ' class="scalable ' . (isset($data['class']) ? $data['class'] : '') . '"'; - $html .= isset($data['onclick']) ? ' onclick="' . $data['onclick'] . '"' : ''; - $html .= isset($data['style']) ? ' style="' . $data['style'] . '"' : ''; - $html .= isset($data['id']) ? ' id="' . $data['id'] . '"' : ''; + $html .= ' id="' . $id . '"'; $html .= '>'; $html .= isset($data['title']) ? '<span><span><span>' . $data['title'] . '</span></span></span>' : ''; $html .= '</button>'; + if (!empty($data['onclick'])) { + $html .= $this->secureRenderer->renderEventListenerAsTag('onclick', $data['onclick'], "#$id"); + } + if (!empty($data['style'])) { + $html .= $this->secureRenderer->renderStyleAsTag($data['style'], "#$id"); + } return $html; } @@ -416,11 +452,14 @@ protected function _wrapIntoContainer($html) return '<div class="admin__control-wysiwig">' . $html . '</div>'; } - $html = '<div id="editor' . $this->getHtmlId() . '"' - . ($this->getConfig('no_display') ? ' style="display:none;"' : '') + $id = 'editor' .$this->getHtmlId(); + $html = '<div id="' .$id .'" ' . ($this->getConfig('container_class') ? ' class="admin__control-wysiwig ' . $this->getConfig('container_class') . '"' : '') . '>' . $html . '</div>'; + if ($this->getConfig('no_display')) { + $html .= $this->secureRenderer->renderStyleAsTag('display: none;', "#$id"); + } return $html; } @@ -498,7 +537,6 @@ protected function isToggleButtonVisible() protected function getInlineJs($jsSetupObject, $forceLoad) { $jsString = ' - <script type="text/javascript"> //<![CDATA[ window.tinyMCE_GZ = window.tinyMCE_GZ || {}; window.tinyMCE_GZ.loaded = true; @@ -538,9 +576,8 @@ protected function getInlineJs($jsSetupObject, $forceLoad) ')); varienGlobalEvents.attachEventHandler("formSubmit", editorFormValidationHandler); //]]> - }); - </script>'; - return $jsString; + });'; + return $this->secureRenderer->renderTag('script', ['type' => 'text/javascript'], $jsString, false); } /** diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php b/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php index 90482ab55fc71..d2c44c0f112a6 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Fieldset.php @@ -13,6 +13,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Fieldset extends AbstractElement { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Gallery.php b/lib/internal/Magento/Framework/Data/Form/Element/Gallery.php index 9f9a3306ae65a..4d62a3f667dfe 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Gallery.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Gallery.php @@ -11,28 +11,53 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +/** + * Gallery form element widget. + */ class Gallery extends AbstractElement { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $random = $random ?? ObjectManager::getInstance()->get(Random::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); $this->setType('file'); + $this->secureRenderer = $secureRenderer; + $this->random = $random; } /** - * @return string + * @inheritDoc + * * @SuppressWarnings(PHPMD.ExcessiveMethodLength) * @SuppressWarnings(PHPMD.UnusedLocalVariable) */ @@ -71,17 +96,12 @@ public function getElementHtml() $i++; $html .= '<tr class="gallery">'; foreach ($this->getValue()->getAttributeBackend()->getImageTypes() as $type) { + $linkId = 'linkId' .$this->random->getRandomString(8); $url = $image->setType($type)->getSourceUrl(); - $html .= '<td class="gallery" align="center" style="vertical-align:bottom;">'; - $html .= '<a href="' . + $html .= '<td class="gallery vertical-gallery-cell" align="center">'; + $html .= '<a previewlinkid="' .$linkId .'" href="' . $url . - '" target="_blank" onclick="imagePreview(\'' . - $this->getHtmlId() . - '_image_' . - $type . - '_' . - $image->getValueId() . - '\');return false;" ' . + '" target="_blank" ' . $this->_getUiId( 'image-' . $image->getValueId() ) . @@ -107,8 +127,13 @@ public function getElementHtml() $this->_getUiId( 'file' ) . ' ></td>'; + $html .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "imagePreview('{$this->getHtmlId()}_image_{$type}_{$image->getValueId()}');\nreturn false;", + "*[previewlinkid='{$linkId}']" + ); } - $html .= '<td class="gallery" align="center" style="vertical-align:bottom;">' . + $html .= '<td class="gallery vertical-gallery-cell" align="center">' . '<input type="input" name="' . parent::getName() . '[position][' . @@ -123,7 +148,7 @@ public function getElementHtml() $this->_getUiId( 'position-' . $image->getValueId() ) . '/></td>'; - $html .= '<td class="gallery" align="center" style="vertical-align:bottom;">' . + $html .= '<td class="gallery vertical-gallery-cell" align="center">' . '<input type="checkbox" name="' . parent::getName() . '[delete][' . @@ -140,11 +165,26 @@ public function getElementHtml() ) . '/></td>'; $html .= '</tr>'; } + + $html .= $this->secureRenderer->renderTag( + 'style', + [], + <<<style + .vertical-gallery-cell { + vertical-align:bottom; + } +style + , + false + ); } if ($i == 0) { - $html .= '<script type="text/javascript">' . - 'document.getElementById("gallery_thead").style.visibility="hidden";' . - '</script>'; + $html .= $this->secureRenderer->renderTag( + 'script', + ['type' => 'text/javascript'], + 'document.getElementById("gallery_thead").style.visibility="hidden";', + false + ); } $html .= '</tbody></table>'; @@ -152,9 +192,10 @@ public function getElementHtml() $name = $this->getName(); $parentName = parent::getName(); - $html .= <<<EndSCRIPT - - <script language="javascript"> + $html .= $this->secureRenderer->renderTag( + 'script', + ['type' => 'text/javascript'], + <<<EndSCRIPT id = 0; function addNewImg(){ @@ -218,15 +259,17 @@ function addNewImg(){ }; } - </script> - -EndSCRIPT; +EndSCRIPT + , + false + ); $html .= $this->getAfterElementHtml(); + return $html; } /** - * @return mixed + * @inheritDoc */ public function getName() { @@ -234,7 +277,9 @@ public function getName() } /** - * @return mixed + * Get name in the usual way. + * + * @return string|null */ public function getParentName() { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Image.php b/lib/internal/Magento/Framework/Data/Form/Element/Image.php index 0b37a9ab18c8c..5a0cab45d44d6 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Image.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Image.php @@ -11,7 +11,10 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; use Magento\Framework\UrlInterface; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class Image extends \Magento\Framework\Data\Form\Element\AbstractElement { @@ -21,22 +24,40 @@ class Image extends \Magento\Framework\Data\Form\Element\AbstractElement protected $_urlBuilder; /** - * @param \Magento\Framework\Data\Form\Element\Factory $factoryElement - * @param \Magento\Framework\Data\Form\Element\CollectionFactory $factoryCollection + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @param Factory $factoryElement + * @param CollectionFactory $factoryCollection * @param \Magento\Framework\Escaper $escaper * @param UrlInterface $urlBuilder * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( - \Magento\Framework\Data\Form\Element\Factory $factoryElement, - \Magento\Framework\Data\Form\Element\CollectionFactory $factoryCollection, + Factory $factoryElement, + CollectionFactory $factoryCollection, \Magento\Framework\Escaper $escaper, UrlInterface $urlBuilder, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $random = $random ?? ObjectManager::getInstance()->get(Random::class); $this->_urlBuilder = $urlBuilder; - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); $this->setType('file'); + $this->secureRenderer = $secureRenderer; + $this->random = $random; } /** @@ -55,12 +76,10 @@ public function getElementHtml() $url = $this->_urlBuilder->getBaseUrl(['_type' => UrlInterface::URL_TYPE_MEDIA]) . $url; } - $html = '<a href="' . + $linkId = 'linkId' .$this->random->getRandomString(8); + $html = '<a previewlinkid="' .$linkId .'" href="' . $url . - '"' . - ' onclick="imagePreview(\'' . - $this->getHtmlId() . - '_image\'); return false;" ' . + '" ' . $this->_getUiId( 'link' ) . @@ -78,6 +97,11 @@ public function getElementHtml() $this->_getUiId() . ' />' . '</a> '; + $html .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "imagePreview('{$this->getHtmlId()}_image');\nreturn false;", + "*[previewlinkid='{$linkId}']" + ); } $this->setClass('input-file'); $html .= parent::getElementHtml(); diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Link.php b/lib/internal/Magento/Framework/Data/Form/Element/Link.php index 83f9922304a5c..24e7a8253cdcd 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Link.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Link.php @@ -11,8 +11,14 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +/** + * Link form element widget. + */ class Link extends AbstractElement { /** @@ -20,14 +26,20 @@ class Link extends AbstractElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data + * @param SecureHtmlRenderer|null $secureHtmlRenderer + * @param Random|null $random */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureHtmlRenderer = null, + ?Random $random = null ) { - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $secureHtmlRenderer = $secureHtmlRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $random = $random ?? ObjectManager::getInstance()->get(Random::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureHtmlRenderer, $random); $this->setType('link'); } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Multiselect.php b/lib/internal/Magento/Framework/Data/Form/Element/Multiselect.php index 03daa6c08fff4..794be28f4bbec 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Multiselect.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Multiselect.php @@ -11,26 +11,50 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +/** + * Multi-select form element widget. + */ class Multiselect extends AbstractElement { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $random = $random ?? ObjectManager::getInstance()->get(Random::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); $this->setType('select'); $this->setExtType('multiple'); $this->setSize(10); + $this->secureRenderer = $secureRenderer; + $this->random = $random; } /** @@ -129,38 +153,49 @@ public function getDefaultHtml() $result .= $this->getElementHtml(); if ($this->getSelectAll() && $this->getDeselectAll()) { - $result .= '<a href="#" onclick="return ' . - $this->getJsObjectName() . - '.selectAll()">' . - $this->getSelectAll() . - '</a> <span class="separator"> | </span>'; - $result .= '<a href="#" onclick="return ' . - $this->getJsObjectName() . - '.deselectAll()">' . - $this->getDeselectAll() . - '</a>'; + $random = $this->random->getRandomString(4); + $selectAllId = 'selId' .$random; + $deselectAllId = 'deselId' .$random; + $result .= '<a href="#" id="' .$selectAllId .'">' .$this->getSelectAll() + .'</a> <span class="separator"> | </span>'; + $result .= '<a href="#" id="' .$deselectAllId .'">' .$this->getDeselectAll() .'</a>'; + + $result .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "return {$this->getJsObjectName()}.selectAll();\nreturn false;", + "#{$selectAllId}" + ); + $result .= $this->secureRenderer->renderEventListenerAsTag( + 'onclick', + "return {$this->getJsObjectName()}.deselectAll();", + "#{$deselectAllId}" + ); } $result .= $this->getNoSpan() === true ? '' : '</span>' . "\n"; - $result .= '<script type="text/javascript">' . "\n"; - $result .= ' var ' . $this->getJsObjectName() . ' = {' . "\n"; - $result .= ' selectAll: function() { ' . "\n"; - $result .= ' var sel = $("' . $this->getHtmlId() . '");' . "\n"; - $result .= ' for(var i = 0; i < sel.options.length; i ++) { ' . "\n"; - $result .= ' sel.options[i].selected = true; ' . "\n"; - $result .= ' } ' . "\n"; - $result .= ' return false; ' . "\n"; - $result .= ' },' . "\n"; - $result .= ' deselectAll: function() {' . "\n"; - $result .= ' var sel = $("' . $this->getHtmlId() . '");' . "\n"; - $result .= ' for(var i = 0; i < sel.options.length; i ++) { ' . "\n"; - $result .= ' sel.options[i].selected = false; ' . "\n"; - $result .= ' } ' . "\n"; - $result .= ' return false; ' . "\n"; - $result .= ' }' . "\n"; - $result .= ' }' . "\n"; - $result .= "\n" . '</script>'; + $script = ' var ' . $this->getJsObjectName() . ' = {' . "\n"; + $script .= ' selectAll: function() { ' . "\n"; + $script .= ' var sel = $("' . $this->getHtmlId() . '");' . "\n"; + $script .= ' for(var i = 0; i < sel.options.length; i ++) { ' . "\n"; + $script .= ' sel.options[i].selected = true; ' . "\n"; + $script .= ' } ' . "\n"; + $script .= ' return false; ' . "\n"; + $script .= ' },' . "\n"; + $script .= ' deselectAll: function() {' . "\n"; + $script .= ' var sel = $("' . $this->getHtmlId() . '");' . "\n"; + $script .= ' for(var i = 0; i < sel.options.length; i ++) { ' . "\n"; + $script .= ' sel.options[i].selected = false; ' . "\n"; + $script .= ' } ' . "\n"; + $script .= ' return false; ' . "\n"; + $script .= ' }' . "\n"; + $script .= ' }' . "\n"; + $result .= $this->secureRenderer->renderTag( + 'script', + ['type' => 'text/javascript'], + $script, + false + ); return $result; } @@ -176,19 +211,25 @@ public function getJsObjectName() } /** + * Render an option for the select. + * * @param array $option - * @param array $selected + * @param string[] $selected * @return string */ protected function _optionToHtml($option, $selected) { - $html = '<option value="' . $this->_escape($option['value']) . '"'; + $optionId = 'optId' .$this->random->getRandomString(8); + $html = '<option value="' . $this->_escape($option['value']) . '" id="' . $optionId . '" '; $html .= isset($option['title']) ? 'title="' . $this->_escape($option['title']) . '"' : ''; - $html .= isset($option['style']) ? 'style="' . $option['style'] . '"' : ''; if (in_array((string)$option['value'], $selected)) { $html .= ' selected="selected"'; } $html .= '>' . $this->_escape($option['label']) . '</option>' . "\n"; + if (!empty($option['style'])) { + $html .= $this->secureRenderer->renderStyleAsTag($option['style'], "#$optionId"); + } + return $html; } } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Radios.php b/lib/internal/Magento/Framework/Data/Form/Element/Radios.php index 1eb7c2e21c54a..d7ac0d115a3f2 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Radios.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Radios.php @@ -11,28 +11,43 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +/** + * Radio buttons form element widget. + */ class Radios extends AbstractElement { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $this->secureRenderer + = $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer); $this->setType('radios'); } /** - * @return string + * @inheritDoc */ public function getElementHtml() { @@ -48,8 +63,10 @@ public function getElementHtml() } /** + * Render choices. + * * @param array $option - * @param array $selected + * @param string[] $selected * @return string */ protected function _optionToHtml($option, $selected) @@ -57,9 +74,11 @@ protected function _optionToHtml($option, $selected) $html = '<div class="admin__field admin__field-option">' . '<input type="radio"' . $this->getRadioButtonAttributes($option); if (is_array($option)) { + $option = new DataObject($option); + $optionId = $this->getHtmlId() . $option['value']; $html .= 'value="' . $this->_escape( $option['value'] - ) . '" class="admin__control-radio" id="' . $this->getHtmlId() . $option['value'] . '"'; + ) . '" class="admin__control-radio" id="' .$optionId .'"'; if ($option['value'] == $selected) { $html .= ' checked="checked"'; } @@ -70,9 +89,10 @@ protected function _optionToHtml($option, $selected) '"><span>' . $option['label'] . '</span></label>'; - } elseif ($option instanceof \Magento\Framework\DataObject) { - $html .= 'id="' . $this->getHtmlId() . $option->getValue() . '"' . $option->serialize( - ['label', 'title', 'value', 'class', 'style'] + } elseif ($option instanceof DataObject) { + $optionId = $this->getHtmlId() . $option->getValue(); + $html .= 'id="' .$optionId .'"' .$option->serialize( + ['label', 'title', 'value', 'class'] ); if (in_array($option->getValue(), $selected)) { $html .= ' checked="checked"'; @@ -85,12 +105,23 @@ protected function _optionToHtml($option, $selected) $option->getLabel() . '</label>'; } + + if ($option->getStyle()) { + $html .= $this->secureRenderer->renderStyleAsTag($option->getStyle(), "#$optionId"); + } + if ($option->getOnclick()) { + $this->secureRenderer->renderEventListenerAsTag('onclick', $option->getOnclick(), "#$optionId"); + } + if ($option->getOnchange()) { + $this->secureRenderer->renderEventListenerAsTag('onchange', $option->getOnchange(), "#$optionId"); + } $html .= '</div>'; + return $html; } /** - * @return array + * @inheritDoc */ public function getHtmlAttributes() { @@ -98,6 +129,8 @@ public function getHtmlAttributes() } /** + * Get a choice's HTML attributes. + * * @param array $option * @return string */ diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Renderer/RendererInterface.php b/lib/internal/Magento/Framework/Data/Form/Element/Renderer/RendererInterface.php index 6cd22e0f50d5e..18a0d30ba3a6d 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Renderer/RendererInterface.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Renderer/RendererInterface.php @@ -13,6 +13,7 @@ /** * @api + * @since 100.0.2 */ interface RendererInterface { diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Select.php b/lib/internal/Magento/Framework/Data/Form/Element/Select.php index d6d95ef9da63b..e0563c6fa75ad 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Select.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Select.php @@ -5,32 +5,54 @@ */ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Form select element * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Select extends AbstractElement { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null ) { - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $random = $random ?? ObjectManager::getInstance()->get(Random::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer, $random); $this->setType('select'); $this->setExtType('combobox'); $this->_prepareOptions(); + $this->secureRenderer = $secureRenderer; + $this->random = $random; } /** @@ -104,13 +126,16 @@ protected function _optionToHtml($option, $selected) } $html .= '</optgroup>' . "\n"; } else { - $html = '<option value="' . $this->_escape($option['value']) . '"'; + $optionId = 'optId' .$this->random->getRandomString(8); + $html = '<option value="' . $this->_escape($option['value']) . '" id="' .$optionId .'" '; $html .= isset($option['title']) ? 'title="' . $this->_escape($option['title']) . '"' : ''; - $html .= isset($option['style']) ? 'style="' . $option['style'] . '"' : ''; if (in_array($option['value'], $selected)) { $html .= ' selected="selected"'; } $html .= '>' . $this->_escape($option['label']) . '</option>' . "\n"; + if (!empty($option['style'])) { + $html .= $this->secureRenderer->renderStyleAsTag($option['style'], "#$optionId"); + } } return $html; } diff --git a/lib/internal/Magento/Framework/Data/Form/Element/Time.php b/lib/internal/Magento/Framework/Data/Form/Element/Time.php index f56dbedb744a5..53d72d704483c 100644 --- a/lib/internal/Magento/Framework/Data/Form/Element/Time.php +++ b/lib/internal/Magento/Framework/Data/Form/Element/Time.php @@ -6,7 +6,9 @@ namespace Magento\Framework\Data\Form\Element; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Escaper; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Form time element @@ -15,20 +17,29 @@ */ class Time extends AbstractElement { + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + /** * @param Factory $factoryElement * @param CollectionFactory $factoryCollection * @param Escaper $escaper * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer */ public function __construct( Factory $factoryElement, CollectionFactory $factoryCollection, Escaper $escaper, - $data = [] + $data = [], + ?SecureHtmlRenderer $secureRenderer = null ) { - parent::__construct($factoryElement, $factoryCollection, $escaper, $data); + $secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + parent::__construct($factoryElement, $factoryCollection, $escaper, $data, $secureRenderer); $this->setType('time'); + $this->secureRenderer = $secureRenderer; } /** @@ -51,6 +62,7 @@ public function getName() public function getElementHtml() { $this->addClass('select admin__control-select'); + $this->addClass('select80wide'); $valueHrs = 0; $valueMin = 0; @@ -66,7 +78,7 @@ public function getElementHtml() } $html = '<input type="hidden" id="' . $this->getHtmlId() . '" ' . $this->_getUiId() . '/>'; - $html .= '<select name="' . $this->getName() . '" style="width:80px" ' + $html .= '<select name="' . $this->getName() . '" ' . $this->serialize($this->getHtmlAttributes()) . $this->_getUiId('hour') . '>' . "\n"; for ($i = 0; $i < 24; $i++) { @@ -77,7 +89,7 @@ public function getElementHtml() $html .= '</select>' . "\n"; $html .= '<span class="time-separator">: </span><select name="' - . $this->getName() . '" style="width:80px" ' + . $this->getName() . '" ' . $this->serialize($this->getHtmlAttributes()) . $this->_getUiId('minute') . '>' . "\n"; for ($i = 0; $i < 60; $i++) { @@ -88,7 +100,7 @@ public function getElementHtml() $html .= '</select>' . "\n"; $html .= '<span class="time-separator">: </span><select name="' - . $this->getName() . '" style="width:80px" ' + . $this->getName() . '" ' . $this->serialize($this->getHtmlAttributes()) . $this->_getUiId('second') . '>' . "\n"; for ($i = 0; $i < 60; $i++) { @@ -98,6 +110,18 @@ public function getElementHtml() } $html .= '</select>' . "\n"; $html .= $this->getAfterElementHtml(); + $html .= $this->secureRenderer->renderTag( + 'style', + [], + <<<style + .select80wide { + width: 80px; + } +style + , + false + ); + return $html; } } diff --git a/lib/internal/Magento/Framework/Data/Form/Filter/FilterInterface.php b/lib/internal/Magento/Framework/Data/Form/Filter/FilterInterface.php index f7205fd6946a2..a9bf479cfce05 100644 --- a/lib/internal/Magento/Framework/Data/Form/Filter/FilterInterface.php +++ b/lib/internal/Magento/Framework/Data/Form/Filter/FilterInterface.php @@ -13,6 +13,7 @@ /** * @api + * @since 100.0.2 */ interface FilterInterface { diff --git a/lib/internal/Magento/Framework/Data/Form/FormKey.php b/lib/internal/Magento/Framework/Data/Form/FormKey.php index 355460902eee6..2f8c6724f7a2e 100644 --- a/lib/internal/Magento/Framework/Data/Form/FormKey.php +++ b/lib/internal/Magento/Framework/Data/Form/FormKey.php @@ -13,6 +13,7 @@ * @api * * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) + * @since 100.0.2 */ class FormKey { diff --git a/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php b/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php index 225ff1fd140a9..a99692ed31609 100644 --- a/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php +++ b/lib/internal/Magento/Framework/Data/Form/FormKey/Validator.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class Validator { diff --git a/lib/internal/Magento/Framework/Data/OptionSourceInterface.php b/lib/internal/Magento/Framework/Data/OptionSourceInterface.php index aac9a266c7957..6d0955f294072 100644 --- a/lib/internal/Magento/Framework/Data/OptionSourceInterface.php +++ b/lib/internal/Magento/Framework/Data/OptionSourceInterface.php @@ -9,6 +9,7 @@ * Source of option values in a form of value-label pairs * * @api + * @since 100.0.2 */ interface OptionSourceInterface { diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php index 402508d016da6..8431a55244a93 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/AbstractElementTest.php @@ -18,12 +18,16 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; /** * Tests for \Magento\Framework\Data\Form\Element\AbstractElement */ class AbstractElementTest extends TestCase { + private const RANDOM_STRING = '123456abcdefg'; + /** * @var AbstractElement|MockObject */ @@ -52,13 +56,18 @@ protected function setUp(): void $this->_collectionFactoryMock = $this->createMock(CollectionFactory::class); $this->_escaperMock = $objectManager->getObject(Escaper::class); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn(self::RANDOM_STRING); $this->_model = $this->getMockForAbstractClass( AbstractElement::class, [ $this->_factoryMock, $this->_collectionFactoryMock, - $this->_escaperMock + $this->_escaperMock, + [], + $this->createMock(SecureHtmlRenderer::class), + $randomMock ] ); } @@ -342,7 +351,8 @@ public function testGetHtmlWithoutRenderer() ); $expectedHtml = '<div class="admin__field">' . "\n" - . '<input id="" name="" data-ui-id="form-element-" value="" class=" required-entry _required"/></div>' + . '<input id="" name="" data-ui-id="form-element-" value="" class=" required-entry _required"' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/></div>' . "\n"; $this->assertEquals($expectedHtml, $this->_model->getHtml()); @@ -385,7 +395,8 @@ public function testSerialize(array $initialData, $expectedValue) unset($initialData['attributes']); } $this->_model->setData($initialData); - $this->assertEquals($expectedValue, $this->_model->serialize($attributes)); + $expectedValue .= ' formelementhookid="elemId' .self::RANDOM_STRING .'"'; + $this->assertEquals(trim($expectedValue), $this->_model->serialize($attributes)); } /** @@ -542,7 +553,8 @@ public function testGetDefaultHtmlDataProvider() [ [], '<div class="admin__field">' . "\n" - . '<input id="" name="" data-ui-id="form-element-" value="" /></div>' . "\n", + . '<input id="" name="" data-ui-id="form-element-" value=""' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/></div>' . "\n", ], [ ['default_html' => 'some default html'], @@ -558,7 +570,8 @@ public function testGetDefaultHtmlDataProvider() '<div class="admin__field">' . "\n" . '<label class="label admin__field-label" for="html-id" data-ui-id="form-element-some-namelabel">' . '<span>some label</span></label>' . "\n" - . '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value" />' + . '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value"' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/>' . '</div>' . "\n" ], [ @@ -571,7 +584,8 @@ public function testGetDefaultHtmlDataProvider() ], '<label class="label admin__field-label" for="html-id" data-ui-id="form-element-some-namelabel">' . '<span>some label</span></label>' . "\n" - . '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value" />' + . '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value"' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/>' ], ]; } @@ -620,7 +634,8 @@ public function getElementHtmlDataProvider() return [ [ [], - '<input id="" name="" data-ui-id="form-element-" value="" />', + '<input id="" name="" data-ui-id="form-element-" value="" formelementhookid="elemId' + .self::RANDOM_STRING .'"/>', ], [ [ @@ -628,7 +643,8 @@ public function getElementHtmlDataProvider() 'name' => 'some-name', 'value' => 'some-value', ], - '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value" />' + '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value"' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/>' ], [ [ @@ -638,7 +654,8 @@ public function getElementHtmlDataProvider() 'before_element_html' => 'some-html', ], '<label class="addbefore" for="html-id">some-html</label>' - . '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value" />' + . '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value"' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/>' ], [ [ @@ -647,7 +664,8 @@ public function getElementHtmlDataProvider() 'value' => 'some-value', 'after_element_js' => 'some-js', ], - '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value" />some-js' + '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value"' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/>some-js' ], [ [ @@ -656,8 +674,9 @@ public function getElementHtmlDataProvider() 'value' => 'some-value', 'after_element_html' => 'some-html', ], - '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value" />' - . '<label class="addafter" for="html-id">some-html</label>' + '<input id="html-id" name="some-name" data-ui-id="form-element-some-name" value="some-value"' + .' formelementhookid="elemId' .self::RANDOM_STRING .'"/>' + . '<label class="addafter" for="html-id">some-html</label>' ] ]; } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditablemultiselectTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditablemultiselectTest.php index 9e4ae2a034a0e..bc264ab58de74 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditablemultiselectTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditablemultiselectTest.php @@ -11,6 +11,8 @@ use Magento\Framework\DataObject; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class EditablemultiselectTest extends TestCase { @@ -22,7 +24,30 @@ class EditablemultiselectTest extends TestCase protected function setUp(): void { $testHelper = new ObjectManager($this); - $this->_model = $testHelper->getObject(Editablemultiselect::class); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attrs, ?string $content): string { + $attrs = new DataObject($attrs); + + return "<$tag {$attrs->serialize()}>$content</$tag>"; + } + ); + $this->_model = $testHelper->getObject( + Editablemultiselect::class, + [ + 'random' => $randomMock, + 'secureRenderer' => $secureRendererMock + ] + ); $values = [ ['value' => 1, 'label' => 'Value1'], ['value' => 2, 'label' => 'Value2'], diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditorTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditorTest.php index efd5cf4d07e2a..9e70bc974c3ab 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditorTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/EditorTest.php @@ -20,6 +20,8 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class EditorTest extends TestCase { @@ -70,6 +72,23 @@ protected function setUp(): void $this->collectionFactoryMock = $this->createMock(CollectionFactory::class); $this->escaperMock = $this->createMock(Escaper::class); $this->configMock = $this->createPartialMock(DataObject::class, ['getData']); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attrs, ?string $content): string { + $attrs = new DataObject($attrs); + + return "<$tag {$attrs->serialize()}>$content</$tag>"; + } + ); $this->serializer = $this->createMock(Json::class); @@ -80,7 +99,9 @@ protected function setUp(): void 'factoryCollection' => $this->collectionFactoryMock, 'escaper' => $this->escaperMock, 'data' => ['config' => $this->configMock], - 'serializer' => $this->serializer + 'serializer' => $this->serializer, + 'random' => $randomMock, + 'secureRenderer' => $secureRendererMock ] ); diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/ImageTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/ImageTest.php index f86144bff1ffc..54f9aca257620 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/ImageTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/ImageTest.php @@ -19,7 +19,14 @@ use Magento\Framework\UrlInterface; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +/** + * Test for the widget. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ class ImageTest extends TestCase { /** @@ -43,11 +50,31 @@ protected function setUp(): void $collectionFactoryMock = $this->createMock(CollectionFactory::class); $escaperMock = $this->createMock(Escaper::class); $this->urlBuilder = $this->createMock(Url::class); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attrs, ?string $content): string { + $attrs = new DataObject($attrs); + + return "<$tag {$attrs->serialize()}>$content</$tag>"; + } + ); $this->_image = new Image( $factoryMock, $collectionFactoryMock, $escaperMock, - $this->urlBuilder + $this->urlBuilder, + [], + $secureRendererMock, + $randomMock ); $formMock = new DataObject(); $formMock->getHtmlIdPrefix('id_prefix'); @@ -101,9 +128,10 @@ public function testGetElementHtmlWithValue() $this->assertStringContainsString('type="file"', $html); $this->assertStringContainsString('value="test_value"', $html); $this->assertStringContainsString( - '<a href="http://localhost/media/test_value" onclick="imagePreview(\'_image\'); return false;"', + '<a previewlinkid="linkIdsome-rando-string" href="http://localhost/media/test_value"', $html ); + $this->assertStringContainsString("imagePreview('_image');\nreturn false;", $html); $this->assertStringContainsString('<input type="checkbox"', $html); } } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php index 0279a7b7b90e5..42f2f4adf2c8b 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/LinkTest.php @@ -18,9 +18,13 @@ use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class LinkTest extends TestCase { + private const RANDOM_STRING = '123456abcdef'; + /** * @var MockObject */ @@ -37,10 +41,15 @@ protected function setUp(): void $factoryMock = $this->createMock(Factory::class); $collectionFactoryMock = $this->createMock(CollectionFactory::class); $escaperMock = $objectManager->getObject(Escaper::class); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn(self::RANDOM_STRING); $this->_link = new Link( $factoryMock, $collectionFactoryMock, - $escaperMock + $escaperMock, + [], + $this->createMock(SecureHtmlRenderer::class), + $randomMock ); $formMock = new DataObject(); $formMock->getHtmlIdPrefix('id_prefix'); @@ -68,7 +77,8 @@ public function testGetElementHtml() $this->_link->setValue('Link Text'); $html = $this->_link->getElementHtml(); $this->assertEquals( - "link_before<a id=\"link_id\" data-ui-id=\"form-element-\">Link Text</a>\nlink_after", + "link_before<a id=\"link_id\" formelementhookid=\"elemId" .self::RANDOM_STRING + ."\" data-ui-id=\"form-element-\">Link Text</a>\nlink_after", $html ); } diff --git a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php index 210e47de021e7..b6caa5580158d 100644 --- a/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php +++ b/lib/internal/Magento/Framework/Data/Test/Unit/Form/Element/MultiselectTest.php @@ -13,7 +13,14 @@ use Magento\Framework\Escaper; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +/** + * Test for the widget. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ class MultiselectTest extends TestCase { /** @@ -24,11 +31,31 @@ class MultiselectTest extends TestCase protected function setUp(): void { $testHelper = new ObjectManager($this); + + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('some-rando-string'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $listener, string $selector): string { + return "<script>document.querySelector('{$selector}').{$event} = () => { {$listener} };</script>"; + } + ); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attrs, ?string $content): string { + $attrs = new DataObject($attrs); + + return "<$tag {$attrs->serialize()}>$content</$tag>"; + } + ); $escaper = new Escaper(); $this->_model = $testHelper->getObject( Editablemultiselect::class, [ - '_escaper' => $escaper + '_escaper' => $escaper, + 'random' => $randomMock, + 'secureRenderer' => $secureRendererMock ] ); $this->_model->setForm(new DataObject()); diff --git a/lib/internal/Magento/Framework/Data/Tree.php b/lib/internal/Magento/Framework/Data/Tree.php index b458338184885..14197eb20c00c 100644 --- a/lib/internal/Magento/Framework/Data/Tree.php +++ b/lib/internal/Magento/Framework/Data/Tree.php @@ -13,6 +13,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Tree { diff --git a/lib/internal/Magento/Framework/Data/Tree/Node.php b/lib/internal/Magento/Framework/Data/Tree/Node.php index f1901b71f21e6..ac0ac57e72969 100644 --- a/lib/internal/Magento/Framework/Data/Tree/Node.php +++ b/lib/internal/Magento/Framework/Data/Tree/Node.php @@ -13,6 +13,7 @@ * * @api * @author Magento Core Team <core@magentocommerce.com> + * @since 100.0.2 */ class Node extends \Magento\Framework\DataObject { diff --git a/lib/internal/Magento/Framework/Data/Tree/Node/Collection.php b/lib/internal/Magento/Framework/Data/Tree/Node/Collection.php index 94990c0340a83..cf6529988eb44 100644 --- a/lib/internal/Magento/Framework/Data/Tree/Node/Collection.php +++ b/lib/internal/Magento/Framework/Data/Tree/Node/Collection.php @@ -16,6 +16,7 @@ /** * @api + * @since 100.0.2 */ class Collection implements \ArrayAccess, \IteratorAggregate, \Countable { diff --git a/lib/internal/Magento/Framework/Data/Wysiwyg/ConfigProviderInterface.php b/lib/internal/Magento/Framework/Data/Wysiwyg/ConfigProviderInterface.php index dc1dabd42d9e8..07fbd302ac573 100644 --- a/lib/internal/Magento/Framework/Data/Wysiwyg/ConfigProviderInterface.php +++ b/lib/internal/Magento/Framework/Data/Wysiwyg/ConfigProviderInterface.php @@ -10,12 +10,14 @@ /** * Interface ConfigProviderInterface * @api + * @since 102.0.0 */ interface ConfigProviderInterface { /** * @param \Magento\Framework\DataObject $config * @return \Magento\Framework\DataObject + * @since 102.0.0 */ public function getConfig(\Magento\Framework\DataObject $config) : \Magento\Framework\DataObject; } diff --git a/lib/internal/Magento/Framework/DataObject.php b/lib/internal/Magento/Framework/DataObject.php index 6ecbca133e22a..11452c49fa162 100644 --- a/lib/internal/Magento/Framework/DataObject.php +++ b/lib/internal/Magento/Framework/DataObject.php @@ -10,6 +10,7 @@ * * @api * @SuppressWarnings(PHPMD.NumberOfChildren) + * @since 100.0.2 */ class DataObject implements \ArrayAccess { diff --git a/lib/internal/Magento/Framework/DataObject/Copy.php b/lib/internal/Magento/Framework/DataObject/Copy.php index e8bc194a1d983..7a400ac449577 100644 --- a/lib/internal/Magento/Framework/DataObject/Copy.php +++ b/lib/internal/Magento/Framework/DataObject/Copy.php @@ -268,7 +268,7 @@ protected function _setFieldsetFieldValue($target, $targetCode, $value) * @return mixed * @throws \InvalidArgumentException * - * @deprecated + * @deprecated 102.0.3 * @see \Magento\Framework\DataObject\Copy::getAttributeValueFromExtensibleObject */ protected function getAttributeValueFromExtensibleDataObject($source, $code) @@ -325,7 +325,7 @@ private function getAttributeValueFromExtensibleObject(ExtensibleDataInterface $ * @return void * @throws \InvalidArgumentException * - * @deprecated + * @deprecated 102.0.3 * @see \Magento\Framework\DataObject\Copy::setAttributeValueFromExtensibleObject */ protected function setAttributeValueFromExtensibleDataObject(ExtensibleDataInterface $target, $code, $value) diff --git a/lib/internal/Magento/Framework/Encryption/Crypt.php b/lib/internal/Magento/Framework/Encryption/Crypt.php index 930cfa7a44f68..55f4d1a31f53d 100644 --- a/lib/internal/Magento/Framework/Encryption/Crypt.php +++ b/lib/internal/Magento/Framework/Encryption/Crypt.php @@ -12,7 +12,8 @@ * Class encapsulates cryptographic algorithm * * @api - * @deprecated + * @deprecated 102.0.0 + * @since 100.0.2 */ class Crypt { diff --git a/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php b/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php index 778cfcb897e0b..61d546daf3796 100644 --- a/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php +++ b/lib/internal/Magento/Framework/Encryption/EncryptorInterface.php @@ -9,6 +9,7 @@ * Encryptor interface * * @api + * @since 100.0.2 */ interface EncryptorInterface { diff --git a/lib/internal/Magento/Framework/Encryption/Helper/Security.php b/lib/internal/Magento/Framework/Encryption/Helper/Security.php index 0320468b35f02..52597c6a028b4 100644 --- a/lib/internal/Magento/Framework/Encryption/Helper/Security.php +++ b/lib/internal/Magento/Framework/Encryption/Helper/Security.php @@ -12,6 +12,7 @@ * Class implements compareString from Laminas\Crypt * * @api + * @since 100.0.2 */ class Security { diff --git a/lib/internal/Magento/Framework/Encryption/UrlCoder.php b/lib/internal/Magento/Framework/Encryption/UrlCoder.php index 12e3bb27b6724..f63e64bb66e6c 100644 --- a/lib/internal/Magento/Framework/Encryption/UrlCoder.php +++ b/lib/internal/Magento/Framework/Encryption/UrlCoder.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class UrlCoder { diff --git a/lib/internal/Magento/Framework/EntityManager/MetadataPool.php b/lib/internal/Magento/Framework/EntityManager/MetadataPool.php index dc295b97c1d28..d787daf3d1181 100644 --- a/lib/internal/Magento/Framework/EntityManager/MetadataPool.php +++ b/lib/internal/Magento/Framework/EntityManager/MetadataPool.php @@ -46,7 +46,6 @@ class MetadataPool * @param ObjectManagerInterface $objectManager * @param SequenceFactory $sequenceFactory * @param array $metadata - * @since 100.1.0 */ public function __construct( ObjectManagerInterface $objectManager, diff --git a/lib/internal/Magento/Framework/EntityManager/Operation/Create.php b/lib/internal/Magento/Framework/EntityManager/Operation/Create.php index ae9001be9e34f..a24ec39a2156d 100644 --- a/lib/internal/Magento/Framework/EntityManager/Operation/Create.php +++ b/lib/internal/Magento/Framework/EntityManager/Operation/Create.php @@ -142,7 +142,7 @@ public function execute($entity, $arguments = []) /** * @return SequenceApplier * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSequenceApplier() { diff --git a/lib/internal/Magento/Framework/Escaper.php b/lib/internal/Magento/Framework/Escaper.php index dd7a780af09fe..9c67d2e891345 100644 --- a/lib/internal/Magento/Framework/Escaper.php +++ b/lib/internal/Magento/Framework/Escaper.php @@ -10,6 +10,7 @@ * Magento escape methods * * @api + * @since 100.0.2 */ class Escaper { @@ -41,7 +42,12 @@ class Escaper /** * @var string[] */ - private $allowedAttributes = ['id', 'class', 'href', 'target', 'title', 'style']; + private $allowedAttributes = ['id', 'class', 'href', 'title', 'style']; + + /** + * @var array + */ + private $notAllowedAttributes = ['a' => ['style']]; /** * @var string @@ -168,6 +174,16 @@ private function removeNotAllowedAttributes(\DOMDocument $domDocument) foreach ($nodes as $node) { $node->parentNode->removeAttribute($node->nodeName); } + + foreach ($this->notAllowedAttributes as $tag => $attributes) { + $nodes = $xpath->query( + '//@*[name() =\'' . implode('\' or name() = \'', $attributes) . '\']' + . '[parent::node()[name() = \'' . $tag . '\']]' + ); + foreach ($nodes as $node) { + $node->parentNode->removeAttribute($node->nodeName); + } + } } /** @@ -237,7 +253,7 @@ private function escapeAttributeValue($name, $value) * @param string $string * @param boolean $escapeSingleQuote * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function escapeHtmlAttr($string, $escapeSingleQuote = true) { @@ -263,7 +279,7 @@ public function escapeUrl($string) * * @param string $string * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function encodeUrlParam($string) { @@ -275,7 +291,7 @@ public function encodeUrlParam($string) * * @param string $string * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function escapeJs($string) { @@ -302,7 +318,7 @@ function ($matches) { * * @param string $string * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function escapeCss($string) { @@ -315,7 +331,7 @@ public function escapeCss($string) * @param string|string[]|array $data * @param string $quote * @return string|array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function escapeJsQuote($data, $quote = '\'') { @@ -335,7 +351,7 @@ public function escapeJsQuote($data, $quote = '\'') * * @param string $data * @return string - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function escapeXssInUrl($data) { @@ -383,7 +399,7 @@ private function escapeScriptIdentifiers(string $data): string * @param string $data * @param bool $addSlashes * @return string - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function escapeQuote($data, $addSlashes = false) { @@ -397,7 +413,7 @@ public function escapeQuote($data, $addSlashes = false) * Get escaper * * @return \Magento\Framework\ZendEscaper - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getEscaper() { @@ -412,7 +428,7 @@ private function getEscaper() * Get logger * * @return \Psr\Log\LoggerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getLogger() { diff --git a/lib/internal/Magento/Framework/Event.php b/lib/internal/Magento/Framework/Event.php index c7b15a8eb0722..9df9c315ccad8 100644 --- a/lib/internal/Magento/Framework/Event.php +++ b/lib/internal/Magento/Framework/Event.php @@ -13,6 +13,7 @@ /** * @api + * @since 100.0.2 */ class Event extends \Magento\Framework\DataObject { diff --git a/lib/internal/Magento/Framework/Event/Observer.php b/lib/internal/Magento/Framework/Event/Observer.php index 4b5dc47795e6c..f3d954d641dfd 100644 --- a/lib/internal/Magento/Framework/Event/Observer.php +++ b/lib/internal/Magento/Framework/Event/Observer.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class Observer extends \Magento\Framework\DataObject { diff --git a/lib/internal/Magento/Framework/Event/Observer/Collection.php b/lib/internal/Magento/Framework/Event/Observer/Collection.php index c108d422cc9ca..595ee8f70da3a 100644 --- a/lib/internal/Magento/Framework/Event/Observer/Collection.php +++ b/lib/internal/Magento/Framework/Event/Observer/Collection.php @@ -13,6 +13,7 @@ /** * @api + * @since 100.0.2 */ class Collection { diff --git a/lib/internal/Magento/Framework/Event/ObserverInterface.php b/lib/internal/Magento/Framework/Event/ObserverInterface.php index e7c9b770ea1f5..e07de37c0e15b 100644 --- a/lib/internal/Magento/Framework/Event/ObserverInterface.php +++ b/lib/internal/Magento/Framework/Event/ObserverInterface.php @@ -11,6 +11,7 @@ * Interface \Magento\Framework\Event\ObserverInterface * * @api + * @since 100.0.2 */ interface ObserverInterface { diff --git a/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php b/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php index ff142c5319006..7cc968789e4e2 100644 --- a/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php +++ b/lib/internal/Magento/Framework/Exception/AbstractAggregateException.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ abstract class AbstractAggregateException extends LocalizedException implements AggregateExceptionInterface { @@ -81,6 +82,7 @@ public function addError(Phrase $phrase) /** * @param LocalizedException $exception * @return $this + * @since 101.0.6 */ public function addException(LocalizedException $exception) { diff --git a/lib/internal/Magento/Framework/Exception/AggregateExceptionInterface.php b/lib/internal/Magento/Framework/Exception/AggregateExceptionInterface.php index d7b6f6fce3f8f..d1a9846b9cc3c 100644 --- a/lib/internal/Magento/Framework/Exception/AggregateExceptionInterface.php +++ b/lib/internal/Magento/Framework/Exception/AggregateExceptionInterface.php @@ -10,6 +10,7 @@ * not mandating to inherit from AbstractAggregateException class * * @api + * @since 101.0.7 */ interface AggregateExceptionInterface { @@ -20,6 +21,7 @@ interface AggregateExceptionInterface * @see the \Magento\Framework\Webapi\Exception which receives $errors as a set of Localized Exceptions * * @return LocalizedException[] + * @since 101.0.7 */ public function getErrors(); } diff --git a/lib/internal/Magento/Framework/Exception/AlreadyExistsException.php b/lib/internal/Magento/Framework/Exception/AlreadyExistsException.php index eaef391521979..e64f147a13ea7 100644 --- a/lib/internal/Magento/Framework/Exception/AlreadyExistsException.php +++ b/lib/internal/Magento/Framework/Exception/AlreadyExistsException.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class AlreadyExistsException extends LocalizedException { @@ -16,7 +17,6 @@ class AlreadyExistsException extends LocalizedException * @param Phrase $phrase * @param \Exception $cause * @param int $code - * @since 100.2.0 */ public function __construct(Phrase $phrase = null, \Exception $cause = null, $code = 0) { diff --git a/lib/internal/Magento/Framework/Exception/AuthenticationException.php b/lib/internal/Magento/Framework/Exception/AuthenticationException.php index 9b20dea99ccf9..b16c1f5841c3d 100644 --- a/lib/internal/Magento/Framework/Exception/AuthenticationException.php +++ b/lib/internal/Magento/Framework/Exception/AuthenticationException.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class AuthenticationException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/AuthorizationException.php b/lib/internal/Magento/Framework/Exception/AuthorizationException.php index ed310117d6717..9cc6b4d8094bc 100644 --- a/lib/internal/Magento/Framework/Exception/AuthorizationException.php +++ b/lib/internal/Magento/Framework/Exception/AuthorizationException.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class AuthorizationException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/BulkException.php b/lib/internal/Magento/Framework/Exception/BulkException.php index 168e910e0d375..a7ec480341542 100644 --- a/lib/internal/Magento/Framework/Exception/BulkException.php +++ b/lib/internal/Magento/Framework/Exception/BulkException.php @@ -12,6 +12,7 @@ * Exception thrown while processing bulk of entities * * @api + * @since 101.0.7 */ class BulkException extends AbstractAggregateException { @@ -42,6 +43,7 @@ public function __construct(Phrase $phrase = null, \Exception $cause = null, $co * Add data * * @param array $data + * @since 101.0.7 */ public function addData($data) { @@ -52,6 +54,7 @@ public function addData($data) * Retrieve data * * @return array + * @since 101.0.7 */ public function getData() { diff --git a/lib/internal/Magento/Framework/Exception/CouldNotDeleteException.php b/lib/internal/Magento/Framework/Exception/CouldNotDeleteException.php index af8e4c3c0ec57..d22a9168e1d01 100644 --- a/lib/internal/Magento/Framework/Exception/CouldNotDeleteException.php +++ b/lib/internal/Magento/Framework/Exception/CouldNotDeleteException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class CouldNotDeleteException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/CouldNotSaveException.php b/lib/internal/Magento/Framework/Exception/CouldNotSaveException.php index ea265864afd24..b0480c49d2940 100644 --- a/lib/internal/Magento/Framework/Exception/CouldNotSaveException.php +++ b/lib/internal/Magento/Framework/Exception/CouldNotSaveException.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class CouldNotSaveException extends AbstractAggregateException { diff --git a/lib/internal/Magento/Framework/Exception/CronException.php b/lib/internal/Magento/Framework/Exception/CronException.php index 22d23526a6558..feff3f3cb95a4 100644 --- a/lib/internal/Magento/Framework/Exception/CronException.php +++ b/lib/internal/Magento/Framework/Exception/CronException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class CronException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/EmailNotConfirmedException.php b/lib/internal/Magento/Framework/Exception/EmailNotConfirmedException.php index 330f34fb565ce..1a114f19d731e 100644 --- a/lib/internal/Magento/Framework/Exception/EmailNotConfirmedException.php +++ b/lib/internal/Magento/Framework/Exception/EmailNotConfirmedException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class EmailNotConfirmedException extends AuthenticationException { diff --git a/lib/internal/Magento/Framework/Exception/FileSystemException.php b/lib/internal/Magento/Framework/Exception/FileSystemException.php index 6c85314b6f2a6..81078ae7cd31c 100644 --- a/lib/internal/Magento/Framework/Exception/FileSystemException.php +++ b/lib/internal/Magento/Framework/Exception/FileSystemException.php @@ -9,6 +9,7 @@ * Magento filesystem exception * * @api + * @since 100.0.2 */ class FileSystemException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/InputException.php b/lib/internal/Magento/Framework/Exception/InputException.php index 7b1815dc0d1bb..f85baf4c9b0b9 100644 --- a/lib/internal/Magento/Framework/Exception/InputException.php +++ b/lib/internal/Magento/Framework/Exception/InputException.php @@ -12,6 +12,7 @@ * Exception to be thrown when there is an issue with the Input to a function call. * * @api + * @since 100.0.2 */ class InputException extends AbstractAggregateException { diff --git a/lib/internal/Magento/Framework/Exception/IntegrationException.php b/lib/internal/Magento/Framework/Exception/IntegrationException.php index 9adf9c740f0e2..56db6caecacf2 100644 --- a/lib/internal/Magento/Framework/Exception/IntegrationException.php +++ b/lib/internal/Magento/Framework/Exception/IntegrationException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class IntegrationException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/InvalidEmailOrPasswordException.php b/lib/internal/Magento/Framework/Exception/InvalidEmailOrPasswordException.php index ba5b7d94f5328..38e69f0476b77 100644 --- a/lib/internal/Magento/Framework/Exception/InvalidEmailOrPasswordException.php +++ b/lib/internal/Magento/Framework/Exception/InvalidEmailOrPasswordException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class InvalidEmailOrPasswordException extends AuthenticationException { diff --git a/lib/internal/Magento/Framework/Exception/LocalizedException.php b/lib/internal/Magento/Framework/Exception/LocalizedException.php index 977c69db77bbc..b279fe65906a3 100644 --- a/lib/internal/Magento/Framework/Exception/LocalizedException.php +++ b/lib/internal/Magento/Framework/Exception/LocalizedException.php @@ -14,6 +14,7 @@ * Localized exception * * @api + * @since 100.0.2 */ class LocalizedException extends \Exception { diff --git a/lib/internal/Magento/Framework/Exception/MailException.php b/lib/internal/Magento/Framework/Exception/MailException.php index 1475ba04257c9..708f03adbe7b0 100644 --- a/lib/internal/Magento/Framework/Exception/MailException.php +++ b/lib/internal/Magento/Framework/Exception/MailException.php @@ -9,6 +9,7 @@ * Magento mail exception * * @api + * @since 100.0.2 */ class MailException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/NoSuchEntityException.php b/lib/internal/Magento/Framework/Exception/NoSuchEntityException.php index 42b30d45e4c72..f39fef57ed568 100644 --- a/lib/internal/Magento/Framework/Exception/NoSuchEntityException.php +++ b/lib/internal/Magento/Framework/Exception/NoSuchEntityException.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class NoSuchEntityException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/NotFoundException.php b/lib/internal/Magento/Framework/Exception/NotFoundException.php index 40e02e70fa37c..cfc02f4f516d6 100644 --- a/lib/internal/Magento/Framework/Exception/NotFoundException.php +++ b/lib/internal/Magento/Framework/Exception/NotFoundException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class NotFoundException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/PaymentException.php b/lib/internal/Magento/Framework/Exception/PaymentException.php index 83b189d020ee3..fb52401229ba4 100644 --- a/lib/internal/Magento/Framework/Exception/PaymentException.php +++ b/lib/internal/Magento/Framework/Exception/PaymentException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class PaymentException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/Plugin/AuthenticationException.php b/lib/internal/Magento/Framework/Exception/Plugin/AuthenticationException.php index 2a5564181d43f..4d559a548eff6 100644 --- a/lib/internal/Magento/Framework/Exception/Plugin/AuthenticationException.php +++ b/lib/internal/Magento/Framework/Exception/Plugin/AuthenticationException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class AuthenticationException extends \Magento\Framework\Exception\AuthenticationException { diff --git a/lib/internal/Magento/Framework/Exception/RemoteServiceUnavailableException.php b/lib/internal/Magento/Framework/Exception/RemoteServiceUnavailableException.php index a9af31b003333..50f01473b9dc9 100644 --- a/lib/internal/Magento/Framework/Exception/RemoteServiceUnavailableException.php +++ b/lib/internal/Magento/Framework/Exception/RemoteServiceUnavailableException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class RemoteServiceUnavailableException extends AuthenticationException { diff --git a/lib/internal/Magento/Framework/Exception/SerializationException.php b/lib/internal/Magento/Framework/Exception/SerializationException.php index bae56b487975c..cbb40de6578f0 100644 --- a/lib/internal/Magento/Framework/Exception/SerializationException.php +++ b/lib/internal/Magento/Framework/Exception/SerializationException.php @@ -12,6 +12,7 @@ * Serialization Exception * * @api + * @since 100.0.2 */ class SerializationException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/SessionException.php b/lib/internal/Magento/Framework/Exception/SessionException.php index b3af3ea5b5bb0..0127e3fa551e6 100644 --- a/lib/internal/Magento/Framework/Exception/SessionException.php +++ b/lib/internal/Magento/Framework/Exception/SessionException.php @@ -9,6 +9,7 @@ * Session exception * * @api + * @since 100.0.2 */ class SessionException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/State/ExpiredException.php b/lib/internal/Magento/Framework/Exception/State/ExpiredException.php index 29748b52c8351..9f127b2d1fa29 100644 --- a/lib/internal/Magento/Framework/Exception/State/ExpiredException.php +++ b/lib/internal/Magento/Framework/Exception/State/ExpiredException.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class ExpiredException extends StateException { diff --git a/lib/internal/Magento/Framework/Exception/State/InitException.php b/lib/internal/Magento/Framework/Exception/State/InitException.php index 9ca7de26c58d7..ad2e6ef283deb 100644 --- a/lib/internal/Magento/Framework/Exception/State/InitException.php +++ b/lib/internal/Magento/Framework/Exception/State/InitException.php @@ -11,6 +11,7 @@ * An exception that indicates application initialization error * * @api + * @since 100.0.2 */ class InitException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/State/InputMismatchException.php b/lib/internal/Magento/Framework/Exception/State/InputMismatchException.php index 752ebcf4804b1..efc8e1b4afe3e 100644 --- a/lib/internal/Magento/Framework/Exception/State/InputMismatchException.php +++ b/lib/internal/Magento/Framework/Exception/State/InputMismatchException.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class InputMismatchException extends StateException { diff --git a/lib/internal/Magento/Framework/Exception/State/InvalidTransitionException.php b/lib/internal/Magento/Framework/Exception/State/InvalidTransitionException.php index 2667d1745767e..c66163e87c9c1 100644 --- a/lib/internal/Magento/Framework/Exception/State/InvalidTransitionException.php +++ b/lib/internal/Magento/Framework/Exception/State/InvalidTransitionException.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class InvalidTransitionException extends StateException { diff --git a/lib/internal/Magento/Framework/Exception/State/UserLockedException.php b/lib/internal/Magento/Framework/Exception/State/UserLockedException.php index fa39556f6eecc..63e81f987f776 100644 --- a/lib/internal/Magento/Framework/Exception/State/UserLockedException.php +++ b/lib/internal/Magento/Framework/Exception/State/UserLockedException.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class UserLockedException extends AuthenticationException { diff --git a/lib/internal/Magento/Framework/Exception/StateException.php b/lib/internal/Magento/Framework/Exception/StateException.php index 580ef6fabe2fa..ed80b9c2df35b 100644 --- a/lib/internal/Magento/Framework/Exception/StateException.php +++ b/lib/internal/Magento/Framework/Exception/StateException.php @@ -9,6 +9,7 @@ * State Exception * * @api + * @since 100.0.2 */ class StateException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Exception/TemporaryState/CouldNotSaveException.php b/lib/internal/Magento/Framework/Exception/TemporaryState/CouldNotSaveException.php index 894b5be3f0bd1..4c59205b24519 100644 --- a/lib/internal/Magento/Framework/Exception/TemporaryState/CouldNotSaveException.php +++ b/lib/internal/Magento/Framework/Exception/TemporaryState/CouldNotSaveException.php @@ -13,7 +13,7 @@ * CouldNotSaveException caused by recoverable error * * @api - * @since 100.2.0 + * @since 101.0.0 */ class CouldNotSaveException extends LocalizedCouldNotSaveException implements TemporaryStateExceptionInterface { @@ -23,7 +23,6 @@ class CouldNotSaveException extends LocalizedCouldNotSaveException implements Te * @param Phrase $phrase The Exception message to throw. * @param \Exception $previous [optional] The previous exception used for the exception chaining. * @param int $code [optional] The Exception code. - * @since 100.2.0 */ public function __construct(Phrase $phrase, \Exception $previous = null, $code = 0) { diff --git a/lib/internal/Magento/Framework/Exception/ValidatorException.php b/lib/internal/Magento/Framework/Exception/ValidatorException.php index 1066fe268df44..9dbb994c111f9 100644 --- a/lib/internal/Magento/Framework/Exception/ValidatorException.php +++ b/lib/internal/Magento/Framework/Exception/ValidatorException.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class ValidatorException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/File/Csv.php b/lib/internal/Magento/Framework/File/Csv.php index 571ad6b21efa7..1b1decdb5327c 100644 --- a/lib/internal/Magento/Framework/File/Csv.php +++ b/lib/internal/Magento/Framework/File/Csv.php @@ -130,7 +130,7 @@ public function getDataPairs($file, $keyIndex = 0, $valueIndex = 1) * @param array $data * @return $this * @throws \Magento\Framework\Exception\FileSystemException - * @deprecated + * @deprecated 102.0.0 * @see appendData */ public function saveData($file, $data) diff --git a/lib/internal/Magento/Framework/File/Size.php b/lib/internal/Magento/Framework/File/Size.php index c5a51ec1760e7..52cf572186f9f 100644 --- a/lib/internal/Magento/Framework/File/Size.php +++ b/lib/internal/Magento/Framework/File/Size.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Size { diff --git a/lib/internal/Magento/Framework/File/Uploader.php b/lib/internal/Magento/Framework/File/Uploader.php index dde98bd7d41b0..7ec5843ddcf18 100644 --- a/lib/internal/Magento/Framework/File/Uploader.php +++ b/lib/internal/Magento/Framework/File/Uploader.php @@ -21,6 +21,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * * @api + * @since 100.0.2 */ class Uploader { @@ -714,7 +715,7 @@ public static function getNewFileName($destinationFile) * * @param string $fileName * @return string - * @deprecated + * @deprecated 101.0.4 */ public static function getDispretionPath($fileName) { @@ -726,6 +727,7 @@ public static function getDispretionPath($fileName) * * @param string $fileName * @return string + * @since 101.0.4 */ public static function getDispersionPath($fileName) { diff --git a/lib/internal/Magento/Framework/Filesystem.php b/lib/internal/Magento/Framework/Filesystem.php index 01877fe17a716..1f65bd6885fcd 100644 --- a/lib/internal/Magento/Framework/Filesystem.php +++ b/lib/internal/Magento/Framework/Filesystem.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Filesystem { @@ -78,6 +79,7 @@ public function getDirectoryRead($directoryCode, $driverCode = DriverPool::FILE) * * @return \Magento\Framework\Filesystem\Directory\ReadInterface * + * @since 102.0.0 */ public function getDirectoryReadByPath($path, $driverCode = DriverPool::FILE) { diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/Read.php b/lib/internal/Magento/Framework/Filesystem/Directory/Read.php index e23eadd57d866..d16fab37818b0 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/Read.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/Read.php @@ -11,6 +11,7 @@ /** * Filesystem directory instance for read operations * @api + * @since 100.0.2 */ class Read implements ReadInterface { @@ -67,6 +68,7 @@ public function __construct( * @throws ValidatorException * * @return void + * @since 101.0.7 */ protected function validatePath( ?string $path, diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php b/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php index 85d41b6932629..513f925a8c8fc 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/ReadInterface.php @@ -8,6 +8,7 @@ /** * Interface \Magento\Framework\Filesystem\Directory\ReadInterface * @api + * @since 100.0.2 */ interface ReadInterface { diff --git a/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php b/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php index 186cbcb81bff2..5b0fd74a422c9 100644 --- a/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/Directory/WriteInterface.php @@ -8,6 +8,7 @@ /** * Interface \Magento\Framework\Filesystem\Directory\WriteInterface * @api + * @since 100.0.2 */ interface WriteInterface extends ReadInterface { diff --git a/lib/internal/Magento/Framework/Filesystem/Driver/File.php b/lib/internal/Magento/Framework/Filesystem/Driver/File.php index aec9a64a132fe..1affad5521372 100644 --- a/lib/internal/Magento/Framework/Filesystem/Driver/File.php +++ b/lib/internal/Magento/Framework/Filesystem/Driver/File.php @@ -988,7 +988,7 @@ public function getRealPath($path) */ public function getRealPathSafety($path) { - if (strpos($path, DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR) === false) { + if (strpos($path, DIRECTORY_SEPARATOR . '.') === false) { return $path; } @@ -999,6 +999,9 @@ public function getRealPathSafety($path) $path ); $pathParts = explode(DIRECTORY_SEPARATOR, $path); + if (end($pathParts) == '.') { + $pathParts[count($pathParts) - 1] = ''; + } $realPath = []; foreach ($pathParts as $pathPart) { if ($pathPart == '.') { diff --git a/lib/internal/Magento/Framework/Filesystem/DriverInterface.php b/lib/internal/Magento/Framework/Filesystem/DriverInterface.php index 39be808875141..afea4d3bc7b07 100644 --- a/lib/internal/Magento/Framework/Filesystem/DriverInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/DriverInterface.php @@ -13,6 +13,7 @@ * Class Driver * * @api + * @since 100.0.2 */ interface DriverInterface { diff --git a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php index 5d9badf42073f..e46b00bc5c74f 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/ReadFactory.php @@ -11,6 +11,7 @@ /** * Opens a file for reading * @api + * @since 100.0.2 */ class ReadFactory { diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php index af2a43ceaedc3..7a9596586f56a 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteFactory.php @@ -11,6 +11,7 @@ /** * Opens a file for reading and/or writing * @api + * @since 100.0.2 */ class WriteFactory extends ReadFactory { diff --git a/lib/internal/Magento/Framework/Filesystem/File/WriteInterface.php b/lib/internal/Magento/Framework/Filesystem/File/WriteInterface.php index ecf554de808cc..87e5f5afc95b4 100644 --- a/lib/internal/Magento/Framework/Filesystem/File/WriteInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/File/WriteInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface WriteInterface extends ReadInterface { diff --git a/lib/internal/Magento/Framework/Filesystem/Io/IoInterface.php b/lib/internal/Magento/Framework/Filesystem/Io/IoInterface.php index 93c85ebafe727..477da422def57 100644 --- a/lib/internal/Magento/Framework/Filesystem/Io/IoInterface.php +++ b/lib/internal/Magento/Framework/Filesystem/Io/IoInterface.php @@ -8,6 +8,7 @@ /** * Input/output client interface * @api + * @since 100.0.2 */ interface IoInterface { diff --git a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/FileTest.php b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/FileTest.php index dada02a3027dd..58cabe561419b 100644 --- a/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/FileTest.php +++ b/lib/internal/Magento/Framework/Filesystem/Test/Unit/Driver/FileTest.php @@ -6,6 +6,8 @@ declare(strict_types=1); +declare(strict_types=1); + namespace Magento\Framework\Filesystem\Test\Unit\Driver; use Magento\Framework\Filesystem\Driver\File; @@ -19,6 +21,9 @@ class FileTest extends TestCase /** @var bool Result of file_put_contents() function */ public static $filePutContents; + /** + * @inheritdoc + */ protected function setUp(): void { self::$fileGetContents = ''; @@ -26,18 +31,25 @@ protected function setUp(): void } /** + * Test for getAbsolutePath method. + * * @dataProvider dataProviderForTestGetAbsolutePath + * @param string $basePath + * @param string $path + * @param string $expected */ - public function testGetAbsolutePath($basePath, $path, $expected) + public function testGetAbsolutePath(string $basePath, string $path, string $expected) { $file = new File(); $this->assertEquals($expected, $file->getAbsolutePath($basePath, $path)); } /** + * Data provider for testGetAbsolutePath. + * * @return array */ - public function dataProviderForTestGetAbsolutePath() + public function dataProviderForTestGetAbsolutePath(): array { return [ ['/root/path/', 'sub', '/root/path/sub'], @@ -48,18 +60,25 @@ public function dataProviderForTestGetAbsolutePath() } /** + * Test for getRelativePath method. + * * @dataProvider dataProviderForTestGetRelativePath + * @param string $basePath + * @param string $path + * @param string $expected */ - public function testGetRelativePath($basePath, $path, $expected) + public function testGetRelativePath(string $basePath, string $path, string $expected) { $file = new File(); $this->assertEquals($expected, $file->getRelativePath($basePath, $path)); } /** + * Data provider for testGetRelativePath. + * * @return array */ - public function dataProviderForTestGetRelativePath() + public function dataProviderForTestGetRelativePath(): array { return [ ['/root/path/', 'sub', 'sub'], @@ -68,4 +87,37 @@ public function dataProviderForTestGetRelativePath() ['/root/path/sub', '/root/path/other', '/root/path/other'], ]; } + + /** + * Test for getRealPathSafety method. + * + * @dataProvider dataProviderForTestGetRealPathSafety + * @param string $path + * @param string $expected + */ + public function testGetRealPathSafety(string $path, string $expected) + { + $file = new File(); + $this->assertEquals($expected, $file->getRealPathSafety($path)); + } + + /** + * Data provider for testGetRealPathSafety; + * + * @return array + */ + public function dataProviderForTestGetRealPathSafety(): array + { + return [ + ['/1/2/3', '/1/2/3'], + ['/1/.test', '/1/.test'], + ['/1/..test', '/1/..test'], + ['/1/.test/.test', '/1/.test/.test'], + ['/1/2/./.', '/1/2/'], + ['/1/2/3/../..', '/1'], + ['/1/2/3/.', '/1/2/3/'], + ['/1/2/3/./4/5', '/1/2/3/4/5'], + ['/1/2/3/../4/5', '/1/2/4/5'] + ]; + } } diff --git a/lib/internal/Magento/Framework/Filter/FilterManager.php b/lib/internal/Magento/Framework/Filter/FilterManager.php index ca5d998af833f..1729dee2487ab 100644 --- a/lib/internal/Magento/Framework/Filter/FilterManager.php +++ b/lib/internal/Magento/Framework/Filter/FilterManager.php @@ -26,6 +26,7 @@ * @method string decrypt(string $value, $params = array()) * @method string translit(string $value) * @method string translitUrl(string $value) + * @since 100.0.2 */ class FilterManager { diff --git a/lib/internal/Magento/Framework/Filter/Template.php b/lib/internal/Magento/Framework/Filter/Template.php index ca597a7056566..be4c7eb750083 100644 --- a/lib/internal/Magento/Framework/Filter/Template.php +++ b/lib/internal/Magento/Framework/Filter/Template.php @@ -23,6 +23,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Template implements \Zend_Filter_Interface { @@ -244,7 +245,7 @@ protected function resetAfterFilterCallbacks() * * @param string[] $construction * @return string - * @deprecated Use the directive interfaces instead + * @deprecated 102.0.4 Use the directive interfaces instead */ public function varDirective($construction) { @@ -259,7 +260,8 @@ public function varDirective($construction) * * @param string[] $construction * @return string - * @deprecated Use the directive interfaces instead + * @deprecated 102.0.4 Use the directive interfaces instead + * @since 102.0.4 */ public function forDirective($construction) { @@ -283,7 +285,7 @@ public function forDirective($construction) * * @param string[] $construction * @return mixed - * @deprecated Use the directive interfaces instead + * @deprecated 102.0.4 Use the directive interfaces instead */ public function templateDirective($construction) { @@ -298,7 +300,7 @@ public function templateDirective($construction) * * @param string[] $construction * @return string - * @deprecated Use the directive interfaces instead + * @deprecated 102.0.4 Use the directive interfaces instead */ public function dependDirective($construction) { @@ -315,7 +317,7 @@ public function dependDirective($construction) * * @param string[] $construction * @return string - * @deprecated Use the directive interfaces instead + * @deprecated 102.0.4 Use the directive interfaces instead */ public function ifDirective($construction) { @@ -332,7 +334,7 @@ public function ifDirective($construction) * * @param string $value raw parameters * @return array - * @deprecated Use the directive interfaces instead + * @deprecated 102.0.4 Use the directive interfaces instead */ protected function getParameters($value) { @@ -353,7 +355,7 @@ protected function getParameters($value) * @param string $value raw parameters * @param string $default default value * @return string - * @deprecated Use \Magento\Framework\Filter\VariableResolverInterface instead + * @deprecated 102.0.4 Use \Magento\Framework\Filter\VariableResolverInterface instead */ protected function getVariable($value, $default = '{no_value_defined}') { @@ -369,7 +371,7 @@ protected function getVariable($value, $default = '{no_value_defined}') * * @param array $stack * @return array - * @deprecated Use new directive processor interfaces + * @deprecated 102.0.4 Use new directive processor interfaces */ protected function getStackArgs($stack) { @@ -396,6 +398,7 @@ protected function getStackArgs($stack) * * @param bool $strictMode Enable strict parsing of directives * @return bool The previous mode from before the change + * @since 102.0.4 */ public function setStrictMode(bool $strictMode): bool { @@ -409,6 +412,7 @@ public function setStrictMode(bool $strictMode): bool * Return if the template is rendered with strict directive processing * * @return bool + * @since 102.0.4 */ public function isStrictMode(): bool { diff --git a/lib/internal/Magento/Framework/Filter/Truncate.php b/lib/internal/Magento/Framework/Filter/Truncate.php index a4dd35b302705..d56f574bac019 100644 --- a/lib/internal/Magento/Framework/Filter/Truncate.php +++ b/lib/internal/Magento/Framework/Filter/Truncate.php @@ -11,7 +11,7 @@ * Truncate a string to a certain length if necessary, appending the $etc string. * $remainder will contain the string that has been replaced with $etc. * - * @deprecated + * @deprecated 101.0.7 * @see \Magento\Framework\Filter\TruncateFilter */ class Truncate implements \Zend_Filter_Interface diff --git a/lib/internal/Magento/Framework/Filter/VariableResolver/LegacyResolver.php b/lib/internal/Magento/Framework/Filter/VariableResolver/LegacyResolver.php index 179f36e1683d1..2cc0657e1bdb5 100644 --- a/lib/internal/Magento/Framework/Filter/VariableResolver/LegacyResolver.php +++ b/lib/internal/Magento/Framework/Filter/VariableResolver/LegacyResolver.php @@ -160,12 +160,31 @@ private function handleObjectMethod(Template $filter, array $templateVariables, { $object = $stackArgs[$i - 1]['variable']; $method = $stackArgs[$i]['name']; - if (method_exists($object, $method)) { + if ($this->isMethodCallable($object, $method)) { $args = $this->getStackArgs($stackArgs[$i]['args'], $filter, $templateVariables); $stackArgs[$i]['variable'] = call_user_func_array([$object, $method], $args); } } + /** + * Check if object method can be called. + * + * @param mixed $object + * @param string $method + * @return bool + */ + private function isMethodCallable($object, string $method): bool + { + if (method_exists($object, $method) + && substr($method, 0, 3) !== 'set' + && $method !== '___callParent' + ) { + return true; + } + + return false; + } + /** * Return if the given index should be processed for data access * diff --git a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php index 91387d7b98469..1e8b33f79854b 100644 --- a/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php +++ b/lib/internal/Magento/Framework/GraphQlSchemaStitching/GraphQlReader.php @@ -7,18 +7,21 @@ namespace Magento\Framework\GraphQlSchemaStitching; +use Magento\Framework\Component\ComponentRegistrar; use Magento\Framework\Config\FileResolverInterface; -use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface as TypeReaderComposite; use Magento\Framework\Config\ReaderInterface; +use Magento\Framework\GraphQlSchemaStitching\GraphQlReader\TypeMetaReaderInterface as TypeReaderComposite; /** * Reads *.graphqls files from modules and combines the results as array to be used with a library to configure objects */ class GraphQlReader implements ReaderInterface { - const GRAPHQL_PLACEHOLDER_FIELD_NAME = 'placeholder_graphql_field'; + public const GRAPHQL_PLACEHOLDER_FIELD_NAME = 'placeholder_graphql_field'; - const GRAPHQL_SCHEMA_FILE = 'schema.graphqls'; + public const GRAPHQL_SCHEMA_FILE = 'schema.graphqls'; + + public const GRAPHQL_INTERFACE = 'graphql_interface'; /** * File locator @@ -42,6 +45,11 @@ class GraphQlReader implements ReaderInterface */ private $defaultScope; + /** + * @var ComponentRegistrar + */ + private static $componentRegistrar; + /** * @param FileResolverInterface $fileResolver * @param TypeReaderComposite $typeReader @@ -61,7 +69,10 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc + * + * @param string|null $scope + * @return array */ public function read($scope = null) : array { @@ -76,7 +87,7 @@ public function read($scope = null) : array * Compatible with @see GraphQlReader::parseTypes */ $knownTypes = []; - foreach ($schemaFiles as $partialSchemaContent) { + foreach ($schemaFiles as $filePath => $partialSchemaContent) { $partialSchemaTypes = $this->parseTypes($partialSchemaContent); // Keep declarations from current partial schema, add missing declarations from all previously read schemas @@ -84,8 +95,8 @@ public function read($scope = null) : array $schemaContent = implode("\n", $knownTypes); $partialResults = $this->readPartialTypes($schemaContent); - $results = array_replace_recursive($results, $partialResults); + $results = $this->addModuleNameToTypes($results, $filePath); } $results = $this->copyInterfaceFieldsToConcreteTypes($results); @@ -285,4 +296,48 @@ private function removePlaceholderFromResults(array $partialResults) : array } return $partialResults; } + + /** + * Get a module name by file path + * + * @param string $file + * @return string + */ + private static function getModuleNameForRelevantFile(string $file): string + { + if (!isset(self::$componentRegistrar)) { + self::$componentRegistrar = new ComponentRegistrar(); + } + $foundModuleName = ''; + foreach (self::$componentRegistrar->getPaths(ComponentRegistrar::MODULE) as $moduleName => $moduleDir) { + if (strpos($file, $moduleDir . '/') !== false) { + $foundModuleName = str_replace('_', '\\', $moduleName); + break; + } + } + + return $foundModuleName; + } + + /** + * Add a module name to types + * + * @param array $source + * @param string $filePath + * @return array + */ + private function addModuleNameToTypes(array $source, string $filePath): array + { + foreach ($source as $typeName => $type) { + if (!isset($type['module']) && ( + ($type['type'] === self::GRAPHQL_INTERFACE && isset($type['typeResolver'])) + || isset($type['implements']) + ) + ) { + $source[$typeName]['module'] = self::getModuleNameForRelevantFile($filePath); + } + } + + return $source; + } } diff --git a/lib/internal/Magento/Framework/HTTP/ClientInterface.php b/lib/internal/Magento/Framework/HTTP/ClientInterface.php index e2e4a89af8fba..3a3aac1546698 100644 --- a/lib/internal/Magento/Framework/HTTP/ClientInterface.php +++ b/lib/internal/Magento/Framework/HTTP/ClientInterface.php @@ -10,6 +10,7 @@ * Interface for HTTP clients * * @api + * @since 100.0.2 */ interface ClientInterface { diff --git a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php index 5179eefd97a4c..2360804a595c0 100644 --- a/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php +++ b/lib/internal/Magento/Framework/HTTP/PhpEnvironment/Response.php @@ -28,6 +28,9 @@ public function getHeader($name) $headers = $this->getHeaders(); if ($headers->has($name)) { $header = $headers->get($name); + if (is_iterable($header)) { + $header = $header[0]; + } } return $header; } @@ -94,8 +97,13 @@ public function clearHeader($name) { $headers = $this->getHeaders(); if ($headers->has($name)) { - $header = $headers->get($name); - $headers->removeHeader($header); + $headerValues = $headers->get($name); + if (!is_iterable($headerValues)) { + $headerValues = [$headerValues]; + } + foreach ($headerValues as $headerValue) { + $headers->removeHeader($headerValue); + } } return $this; diff --git a/lib/internal/Magento/Framework/Image/Adapter/Config.php b/lib/internal/Magento/Framework/Image/Adapter/Config.php index d4a28b4efc600..8926f5a0a9fa7 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Config.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Config.php @@ -65,7 +65,7 @@ public function getAdapters() * Get Maximum Image Width resolution in pixels. For image resizing on client side. * * @return int - * @deprecated + * @deprecated 102.0.1 * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface::getMaxHeight() */ public function getMaxWidth(): int @@ -77,7 +77,7 @@ public function getMaxWidth(): int * Get Maximum Image Height resolution in pixels. For image resizing on client side. * * @return int - * @deprecated + * @deprecated 102.0.1 * @see \Magento\Backend\Model\Image\UploadResizeConfigInterface::getMaxHeight() */ public function getMaxHeight(): int diff --git a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php index 5cb968b1e5441..c37cb89c30587 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/Gd2.php +++ b/lib/internal/Magento/Framework/Image/Adapter/Gd2.php @@ -60,7 +60,7 @@ protected function _reset() */ public function open($filename) { - if (!$filename || filesize($filename) === 0) { + if (!$filename || filesize($filename) === 0 || !$this->validateURLScheme($filename)) { throw new \InvalidArgumentException('Wrong file'); } $this->_fileName = $filename; @@ -77,6 +77,23 @@ public function open($filename) ); } + /** + * Checks for invalid URL schema if it exists + * + * @param string $filename + * @return bool + */ + private function validateURLScheme(string $filename) : bool + { + $allowed_schemes = ['ftp', 'ftps', 'http', 'https']; + $url = parse_url($filename); + if ($url && isset($url['scheme']) && !in_array($url['scheme'], $allowed_schemes)) { + return false; + } + + return true; + } + /** * Checks whether memory limit is reached. * diff --git a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php index 24e036c02e718..46db17034ac3e 100644 --- a/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php +++ b/lib/internal/Magento/Framework/Image/Adapter/ImageMagick.php @@ -75,6 +75,10 @@ public function backgroundColor($color = null) */ public function open($filename) { + if (!empty($filename) && !$this->validateURLScheme($filename)) { + throw new \InvalidArgumentException('Wrong file'); + } + $this->_fileName = $filename; $this->_checkCanProcess(); $this->_getFileAttributes(); @@ -82,6 +86,7 @@ public function open($filename) try { $this->_imageHandler = new \Imagick($this->_fileName); } catch (\ImagickException $e) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new LocalizedException( __('Unsupported image format. File: %1', $this->_fileName), $e, @@ -93,6 +98,23 @@ public function open($filename) $this->getMimeType(); } + /** + * Checks for invalid URL schema if it exists + * + * @param string $filename + * @return bool + */ + private function validateURLScheme(string $filename) : bool + { + $allowed_schemes = ['ftp', 'ftps', 'http', 'https']; + $url = parse_url($filename); + if ($url && isset($url['scheme']) && !in_array($url['scheme'], $allowed_schemes)) { + return false; + } + + return true; + } + /** * Save image to specific path. * @@ -146,11 +168,12 @@ public function getImage() } /** - * Change the image size + * Change the image size. * * @param null|int $frameWidth * @param null|int $frameHeight * @return void + * @throws \ImagickException */ public function resize($frameWidth = null, $frameHeight = null) { @@ -308,6 +331,7 @@ public function watermark($imagePath, $positionX = 0, $positionY = 0, $opacity = $this->addSingleWatermark($positionX, $positionY, $watermark, $compositeChannels); } } catch (\ImagickException $e) { + //phpcs:ignore Magento2.Exceptions.DirectThrow throw new LocalizedException( __('Unable to create watermark.'), $e, diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php index 351e8117e6e17..70b55ab6e4a33 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/Gd2Test.php @@ -122,7 +122,6 @@ public function filesProvider() /** * Test if open() method resets cached fileType - * */ public function testOpenDifferentTypes() { @@ -154,4 +153,14 @@ public function testOpenDifferentTypes() $this->assertNotEquals($type1, $type2); } + + /** + * Test open() with invalid URL. + */ + public function testOpenInvalidURL() + { + $this->expectException(\InvalidArgumentException::class); + + $this->adapter->open('bar://foo.bar'); + } } diff --git a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php index ec353c6802e98..2a27d25dac82e 100644 --- a/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php +++ b/lib/internal/Magento/Framework/Image/Test/Unit/Adapter/ImageMagickTest.php @@ -99,4 +99,14 @@ public function testSaveWithException() $this->loggerMock->expects($this->once())->method('critical')->with($exception); $this->imageMagic->save('product/cache', 'sample.jpg'); } + + /** + * Test open() with invalid URL. + */ + public function testOpenInvalidUrl() + { + $this->expectException(\InvalidArgumentException::class); + + $this->imageMagic->open('bar://foo.bar'); + } } diff --git a/lib/internal/Magento/Framework/Indexer/Action/Base.php b/lib/internal/Magento/Framework/Indexer/Action/Base.php index 636335192cbc5..b454d49578772 100644 --- a/lib/internal/Magento/Framework/Indexer/Action/Base.php +++ b/lib/internal/Magento/Framework/Indexer/Action/Base.php @@ -37,13 +37,13 @@ class Base implements ActionInterface /** * @var AdapterInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $connection; /** * @var SourceProviderInterface[] - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $sources; @@ -54,7 +54,7 @@ class Base implements ActionInterface /** * @var HandlerInterface[] - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $handlers; @@ -65,7 +65,7 @@ class Base implements ActionInterface /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $columnTypesMap = [ 'varchar' => ['type' => Table::TYPE_TEXT, 'size' => 255], @@ -75,13 +75,13 @@ class Base implements ActionInterface /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $filterColumns; /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $searchColumns; @@ -102,7 +102,7 @@ class Base implements ActionInterface /** * @var String - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $string; @@ -113,13 +113,13 @@ class Base implements ActionInterface /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $filterable = []; /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $searchable = []; @@ -353,7 +353,7 @@ protected function prepareFields() * @param array $field * @return void * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function saveFieldByType($field) { diff --git a/lib/internal/Magento/Framework/Indexer/ActionInterface.php b/lib/internal/Magento/Framework/Indexer/ActionInterface.php index 68a10e23e1d60..f4e59182c3c51 100644 --- a/lib/internal/Magento/Framework/Indexer/ActionInterface.php +++ b/lib/internal/Magento/Framework/Indexer/ActionInterface.php @@ -7,6 +7,7 @@ /** * @api Implement custom Action Interface + * @since 100.0.2 */ interface ActionInterface { diff --git a/lib/internal/Magento/Framework/Indexer/BatchProviderInterface.php b/lib/internal/Magento/Framework/Indexer/BatchProviderInterface.php index c318638e7e6ab..e8babffbbea78 100644 --- a/lib/internal/Magento/Framework/Indexer/BatchProviderInterface.php +++ b/lib/internal/Magento/Framework/Indexer/BatchProviderInterface.php @@ -15,7 +15,7 @@ * and process them one by one in order to reduce memory consumption and improve overall performance. * * @api retrieve Batches when implementing custom Indexer\Action - * @since 100.2.0 + * @since 101.0.0 */ interface BatchProviderInterface { @@ -27,7 +27,7 @@ interface BatchProviderInterface * @param string $linkField field that is used as a record identifier. * @param int $batchSize size of the single range. * @return \Generator generator that produces entity ID ranges in the format of ['from' => ..., 'to' => ...] - * @since 100.2.0 + * @since 101.0.0 */ public function getBatches(AdapterInterface $adapter, $tableName, $linkField, $batchSize); @@ -38,7 +38,7 @@ public function getBatches(AdapterInterface $adapter, $tableName, $linkField, $b * @param Select $select * @param array $batch * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function getBatchIds(AdapterInterface $connection, Select $select, array $batch); } diff --git a/lib/internal/Magento/Framework/Indexer/BatchSizeManagementInterface.php b/lib/internal/Magento/Framework/Indexer/BatchSizeManagementInterface.php index b953afcd9e7e8..880d6feb3476f 100644 --- a/lib/internal/Magento/Framework/Indexer/BatchSizeManagementInterface.php +++ b/lib/internal/Magento/Framework/Indexer/BatchSizeManagementInterface.php @@ -10,7 +10,7 @@ /** * Batch size manager can be used to ensure that MEMORY table has enough memory for data in batch. * @api - * @since 100.2.0 + * @since 101.0.0 */ interface BatchSizeManagementInterface { @@ -20,7 +20,7 @@ interface BatchSizeManagementInterface * @param AdapterInterface $adapter database adapter. * @param int $batchSize * @return void - * @since 100.2.0 + * @since 101.0.0 */ public function ensureBatchSize(\Magento\Framework\DB\Adapter\AdapterInterface $adapter, $batchSize); } diff --git a/lib/internal/Magento/Framework/Indexer/Config/Converter.php b/lib/internal/Magento/Framework/Indexer/Config/Converter.php index 7f76b6e4295c5..f012f2b6d6101 100644 --- a/lib/internal/Magento/Framework/Indexer/Config/Converter.php +++ b/lib/internal/Magento/Framework/Indexer/Config/Converter.php @@ -238,7 +238,7 @@ protected function convertField(\DOMElement $node, $data) * * @param \DOMNode $node * @return string - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function getTranslatedNodeValue(\DOMNode $node) { diff --git a/lib/internal/Magento/Framework/Indexer/ConfigInterface.php b/lib/internal/Magento/Framework/Indexer/ConfigInterface.php index 1769676e8ab42..14c771b5b69d3 100644 --- a/lib/internal/Magento/Framework/Indexer/ConfigInterface.php +++ b/lib/internal/Magento/Framework/Indexer/ConfigInterface.php @@ -9,6 +9,7 @@ * Indexer(s) configuration * * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/Indexer/Dimension.php b/lib/internal/Magento/Framework/Indexer/Dimension.php index dacc8d7f524f5..3a484ed17dfdc 100644 --- a/lib/internal/Magento/Framework/Indexer/Dimension.php +++ b/lib/internal/Magento/Framework/Indexer/Dimension.php @@ -11,6 +11,7 @@ * Index Dimension object * * @api + * @since 101.0.6 */ class Dimension { @@ -38,6 +39,7 @@ public function __construct(string $name, string $value) * Get dimension name * * @return string + * @since 101.0.6 */ public function getName(): string { @@ -48,6 +50,7 @@ public function getName(): string * Get dimension value * * @return string + * @since 101.0.6 */ public function getValue(): string { diff --git a/lib/internal/Magento/Framework/Indexer/DimensionFactory.php b/lib/internal/Magento/Framework/Indexer/DimensionFactory.php index 8eaeff5628d60..c5bcab73a1058 100644 --- a/lib/internal/Magento/Framework/Indexer/DimensionFactory.php +++ b/lib/internal/Magento/Framework/Indexer/DimensionFactory.php @@ -13,6 +13,7 @@ * Dimension Factory * * @api + * @since 101.0.6 */ class DimensionFactory { @@ -33,6 +34,7 @@ public function __construct(ObjectManagerInterface $objectManager) * @param string $name * @param string $value * @return Dimension + * @since 101.0.6 */ public function create(string $name, string $value): Dimension { diff --git a/lib/internal/Magento/Framework/Indexer/DimensionProviderInterface.php b/lib/internal/Magento/Framework/Indexer/DimensionProviderInterface.php index ea4f56eb48d41..bd53fc36d3b44 100644 --- a/lib/internal/Magento/Framework/Indexer/DimensionProviderInterface.php +++ b/lib/internal/Magento/Framework/Indexer/DimensionProviderInterface.php @@ -10,12 +10,14 @@ /** * @api * Provide a list of dimensions + * @since 101.0.6 */ interface DimensionProviderInterface extends \IteratorAggregate { /** * Get Dimension Iterator. Returns yielded value of \Magento\Framework\Indexer\Dimension * @return \Traversable|\Magento\Framework\Indexer\Dimension[] + * @since 101.0.6 */ public function getIterator(): \Traversable; } diff --git a/lib/internal/Magento/Framework/Indexer/DimensionalIndexerInterface.php b/lib/internal/Magento/Framework/Indexer/DimensionalIndexerInterface.php index 43c4e7a7fd70b..acf1d598e89c5 100644 --- a/lib/internal/Magento/Framework/Indexer/DimensionalIndexerInterface.php +++ b/lib/internal/Magento/Framework/Indexer/DimensionalIndexerInterface.php @@ -10,6 +10,7 @@ /** * @api * Run indexer by dimensions + * @since 101.0.6 */ interface DimensionalIndexerInterface { @@ -20,6 +21,7 @@ interface DimensionalIndexerInterface * @param \Magento\Framework\Indexer\Dimension[] $dimensions * @param \Traversable $entityIds * @return void + * @since 101.0.6 */ public function executeByDimensions(array $dimensions, \Traversable $entityIds); } diff --git a/lib/internal/Magento/Framework/Indexer/FieldsetInterface.php b/lib/internal/Magento/Framework/Indexer/FieldsetInterface.php index 0ce1e8763ac96..85c5f698e1bb8 100644 --- a/lib/internal/Magento/Framework/Indexer/FieldsetInterface.php +++ b/lib/internal/Magento/Framework/Indexer/FieldsetInterface.php @@ -7,6 +7,7 @@ /** * @api Implement custom Fieldset + * @since 100.0.2 */ interface FieldsetInterface { diff --git a/lib/internal/Magento/Framework/Indexer/FieldsetPool.php b/lib/internal/Magento/Framework/Indexer/FieldsetPool.php index 747db5d2cc00f..742e15f1f43bf 100644 --- a/lib/internal/Magento/Framework/Indexer/FieldsetPool.php +++ b/lib/internal/Magento/Framework/Indexer/FieldsetPool.php @@ -9,6 +9,7 @@ /** * @api Retrieve Fieldset when implementing custom Indexer\Action + * @since 100.0.2 */ class FieldsetPool { diff --git a/lib/internal/Magento/Framework/Indexer/HandlerInterface.php b/lib/internal/Magento/Framework/Indexer/HandlerInterface.php index 5c1e9ea5a2569..8bb725a293f0c 100644 --- a/lib/internal/Magento/Framework/Indexer/HandlerInterface.php +++ b/lib/internal/Magento/Framework/Indexer/HandlerInterface.php @@ -9,6 +9,7 @@ /** * @api Implement custom Handler + * @since 100.0.2 */ interface HandlerInterface { diff --git a/lib/internal/Magento/Framework/Indexer/HandlerPool.php b/lib/internal/Magento/Framework/Indexer/HandlerPool.php index ed7abf19bad47..c4e3fdb0e7c2f 100644 --- a/lib/internal/Magento/Framework/Indexer/HandlerPool.php +++ b/lib/internal/Magento/Framework/Indexer/HandlerPool.php @@ -10,6 +10,7 @@ /** * @api Instantiate save handler when implementing custom Indexer\Action + * @since 100.0.2 */ class HandlerPool { diff --git a/lib/internal/Magento/Framework/Indexer/IndexStructure.php b/lib/internal/Magento/Framework/Indexer/IndexStructure.php index a39de2d5b8a62..2fd5bfa0bd955 100644 --- a/lib/internal/Magento/Framework/Indexer/IndexStructure.php +++ b/lib/internal/Magento/Framework/Indexer/IndexStructure.php @@ -16,7 +16,7 @@ /** * Full text search index structure. * - * @deprecated + * @deprecated 102.0.0 * @see \Magento\ElasticSearch */ class IndexStructure implements IndexStructureInterface diff --git a/lib/internal/Magento/Framework/Indexer/IndexStructureInterface.php b/lib/internal/Magento/Framework/Indexer/IndexStructureInterface.php index f4c518bdfea7a..f8cc53d676af8 100644 --- a/lib/internal/Magento/Framework/Indexer/IndexStructureInterface.php +++ b/lib/internal/Magento/Framework/Indexer/IndexStructureInterface.php @@ -11,6 +11,7 @@ * Indexer structure (schema) handler * * @api + * @since 100.0.2 */ interface IndexStructureInterface { diff --git a/lib/internal/Magento/Framework/Indexer/IndexTableRowSizeEstimatorInterface.php b/lib/internal/Magento/Framework/Indexer/IndexTableRowSizeEstimatorInterface.php index a0a8fcf18146f..537b144df9dd2 100644 --- a/lib/internal/Magento/Framework/Indexer/IndexTableRowSizeEstimatorInterface.php +++ b/lib/internal/Magento/Framework/Indexer/IndexTableRowSizeEstimatorInterface.php @@ -9,7 +9,7 @@ /** * Calculate memory size for entity according different dimensions. * @api - * @since 100.2.0 + * @since 101.0.0 */ interface IndexTableRowSizeEstimatorInterface { @@ -17,7 +17,7 @@ interface IndexTableRowSizeEstimatorInterface * Calculate memory size for entity row. * * @return float - * @since 100.2.0 + * @since 101.0.0 */ public function estimateRowSize(); } diff --git a/lib/internal/Magento/Framework/Indexer/IndexerInterface.php b/lib/internal/Magento/Framework/Indexer/IndexerInterface.php index bf572b94ccc94..597266a05fd3f 100644 --- a/lib/internal/Magento/Framework/Indexer/IndexerInterface.php +++ b/lib/internal/Magento/Framework/Indexer/IndexerInterface.php @@ -9,8 +9,9 @@ * Indexer * * @api - * @deprecated Facade will be split + * @deprecated 102.0.0 Facade will be split * @see \Magento\Framework\Indexer\ActionInterface + * @since 100.0.2 */ interface IndexerInterface { @@ -163,7 +164,7 @@ public function getLatestUpdated(); * * @return void * @throws \Exception - * @deprecated + * @deprecated 102.0.0 * @see \Magento\Framework\Indexer\ActionInterface::executeFull */ public function reindexAll(); @@ -173,7 +174,7 @@ public function reindexAll(); * * @param int $id * @return void - * @deprecated + * @deprecated 102.0.0 * @see \Magento\Framework\Indexer\ActionInterface::executeList */ public function reindexRow($id); @@ -183,7 +184,7 @@ public function reindexRow($id); * * @param int[] $ids * @return void - * @deprecated + * @deprecated 102.0.0 * @see \Magento\Framework\Indexer\ActionInterface::executeList */ public function reindexList($ids); diff --git a/lib/internal/Magento/Framework/Indexer/IndexerRegistry.php b/lib/internal/Magento/Framework/Indexer/IndexerRegistry.php index 5867565e0ccd5..bdc8479671f2e 100644 --- a/lib/internal/Magento/Framework/Indexer/IndexerRegistry.php +++ b/lib/internal/Magento/Framework/Indexer/IndexerRegistry.php @@ -7,6 +7,7 @@ /** * @api Retrieve indexer by id, for example when indexer need to be invalidated + * @since 100.0.2 */ class IndexerRegistry { diff --git a/lib/internal/Magento/Framework/Indexer/SaveHandler/IndexerInterface.php b/lib/internal/Magento/Framework/Indexer/SaveHandler/IndexerInterface.php index e03404d3eb8f6..a3111f9966ee2 100644 --- a/lib/internal/Magento/Framework/Indexer/SaveHandler/IndexerInterface.php +++ b/lib/internal/Magento/Framework/Indexer/SaveHandler/IndexerInterface.php @@ -15,6 +15,7 @@ * Indexer persistence handler * * @api + * @since 100.0.2 */ interface IndexerInterface { diff --git a/lib/internal/Magento/Framework/Indexer/SaveHandlerFactory.php b/lib/internal/Magento/Framework/Indexer/SaveHandlerFactory.php index 80231dd7aa290..9e1ee9a375e36 100644 --- a/lib/internal/Magento/Framework/Indexer/SaveHandlerFactory.php +++ b/lib/internal/Magento/Framework/Indexer/SaveHandlerFactory.php @@ -10,6 +10,7 @@ /** * @api Instantiate save handler when implementing custom Indexer\Action + * @since 100.0.2 */ class SaveHandlerFactory { diff --git a/lib/internal/Magento/Framework/Indexer/StateInterface.php b/lib/internal/Magento/Framework/Indexer/StateInterface.php index 124b8ab50b4b8..332b3227ffc88 100644 --- a/lib/internal/Magento/Framework/Indexer/StateInterface.php +++ b/lib/internal/Magento/Framework/Indexer/StateInterface.php @@ -7,6 +7,7 @@ /** * @api Retrieve status of the Indexer + * @since 100.0.2 */ interface StateInterface { diff --git a/lib/internal/Magento/Framework/Interception/Config/Config.php b/lib/internal/Magento/Framework/Interception/Config/Config.php index abf2a6d9d57b7..cb14e0f148e3e 100644 --- a/lib/internal/Magento/Framework/Interception/Config/Config.php +++ b/lib/internal/Magento/Framework/Interception/Config/Config.php @@ -37,7 +37,7 @@ class Config implements \Magento\Framework\Interception\ConfigInterface /** * Cache - * @deprecated + * @deprecated 102.0.1 * @var \Magento\Framework\Cache\FrontendInterface */ protected $_cache; diff --git a/lib/internal/Magento/Framework/Interception/ConfigLoaderInterface.php b/lib/internal/Magento/Framework/Interception/ConfigLoaderInterface.php new file mode 100644 index 0000000000000..2a739f4cf9486 --- /dev/null +++ b/lib/internal/Magento/Framework/Interception/ConfigLoaderInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Interception; + +/** + * Interception configuration loader interface. + */ +interface ConfigLoaderInterface +{ + /** + * Load interception configuration data per scope. + * + * @param string $cacheId + * @return array + */ + public function load(string $cacheId): array; +} diff --git a/lib/internal/Magento/Framework/Interception/ConfigWriterInterface.php b/lib/internal/Magento/Framework/Interception/ConfigWriterInterface.php new file mode 100644 index 0000000000000..9193937b65816 --- /dev/null +++ b/lib/internal/Magento/Framework/Interception/ConfigWriterInterface.php @@ -0,0 +1,22 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Interception; + +/** + * Interception config writer interface. + */ +interface ConfigWriterInterface +{ + /** + * Write interception configuration for scopes. + * + * @param array $scopes + * @return void + */ + public function write(array $scopes): void; +} diff --git a/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php b/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php index fac02b5d2614b..9d8d516b68fd4 100644 --- a/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php +++ b/lib/internal/Magento/Framework/Interception/ObjectManager/Config/Developer.php @@ -58,8 +58,8 @@ public function setInterceptionConfig(\Magento\Framework\Interception\ConfigInte public function getInstanceType($instanceName) { $type = parent::getInstanceType($instanceName); - if ($this->interceptionConfig && $this->interceptionConfig->hasPlugins($instanceName) - && $this->interceptableValidator->validate($instanceName) + if ($this->interceptionConfig && $this->interceptionConfig->hasPlugins($type) + && $this->interceptableValidator->validate($type) ) { return $type . '\\Interceptor'; } diff --git a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php index 610ae9473a725..26697e70a8f87 100644 --- a/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php +++ b/lib/internal/Magento/Framework/Interception/PluginList/PluginList.php @@ -9,7 +9,9 @@ use Magento\Framework\Config\Data\Scoped; use Magento\Framework\Config\ReaderInterface; use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Interception\ConfigLoaderInterface; use Magento\Framework\Interception\DefinitionInterface; +use Magento\Framework\Interception\PluginListGenerator; use Magento\Framework\Interception\PluginListInterface as InterceptionPluginList; use Magento\Framework\Interception\ObjectManager\ConfigInterface; use Magento\Framework\ObjectManager\RelationsInterface; @@ -20,8 +22,6 @@ /** * Plugin config, provides list of plugins for a type - * - * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class PluginList extends Scoped implements InterceptionPluginList { @@ -78,14 +78,19 @@ class PluginList extends Scoped implements InterceptionPluginList protected $_pluginInstances = []; /** - * @var \Psr\Log\LoggerInterface + * @var SerializerInterface */ - private $logger; + private $serializer; /** - * @var SerializerInterface + * @var ConfigLoaderInterface */ - private $serializer; + private $configLoader; + + /** + * @var PluginListGenerator + */ + private $pluginListGenerator; /** * Constructor @@ -101,6 +106,8 @@ class PluginList extends Scoped implements InterceptionPluginList * @param array $scopePriorityScheme * @param string|null $cacheId * @param SerializerInterface|null $serializer + * @param ConfigLoaderInterface|null $configLoader + * @param PluginListGenerator|null $pluginListGenerator * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -114,7 +121,9 @@ public function __construct( ClassDefinitions $classDefinitions, array $scopePriorityScheme = ['global'], $cacheId = 'plugins', - SerializerInterface $serializer = null + SerializerInterface $serializer = null, + ConfigLoaderInterface $configLoader = null, + PluginListGenerator $pluginListGenerator = null ) { $this->serializer = $serializer ?: $objectManager->get(Serialize::class); parent::__construct($reader, $configScope, $cache, $cacheId, $this->serializer); @@ -124,6 +133,8 @@ public function __construct( $this->_classDefinitions = $classDefinitions; $this->_scopePriorityScheme = $scopePriorityScheme; $this->_objectManager = $objectManager; + $this->configLoader = $configLoader ?: $this->_objectManager->get(ConfigLoaderInterface::class); + $this->pluginListGenerator = $pluginListGenerator ?: $this->_objectManager->get(PluginListGenerator::class); } /** @@ -131,88 +142,10 @@ public function __construct( * * @param string $type * @return array - * @throws \InvalidArgumentException - * @SuppressWarnings(PHPMD.CyclomaticComplexity) - * @SuppressWarnings(PHPMD.NPathComplexity) */ protected function _inheritPlugins($type) { - $type = ltrim($type, '\\'); - if (!isset($this->_inherited[$type])) { - $realType = $this->_omConfig->getOriginalInstanceType($type); - - if ($realType !== $type) { - $plugins = $this->_inheritPlugins($realType); - } elseif ($this->_relations->has($type)) { - $relations = $this->_relations->getParents($type); - $plugins = []; - foreach ($relations as $relation) { - if ($relation) { - $relationPlugins = $this->_inheritPlugins($relation); - if ($relationPlugins) { - $plugins = array_replace_recursive($plugins, $relationPlugins); - } - } - } - } else { - $plugins = []; - } - if (isset($this->_data[$type])) { - if (!$plugins) { - $plugins = $this->_data[$type]; - } else { - $plugins = array_replace_recursive($plugins, $this->_data[$type]); - } - } - $this->_inherited[$type] = null; - if (is_array($plugins) && count($plugins)) { - $this->filterPlugins($plugins); - uasort($plugins, [$this, '_sort']); - $this->trimInstanceStartingBackslash($plugins); - $this->_inherited[$type] = $plugins; - $lastPerMethod = []; - foreach ($plugins as $key => $plugin) { - // skip disabled plugins - if (isset($plugin['disabled']) && $plugin['disabled']) { - unset($plugins[$key]); - continue; - } - $pluginType = $this->_omConfig->getOriginalInstanceType($plugin['instance']); - if (!class_exists($pluginType)) { - throw new \InvalidArgumentException('Plugin class ' . $pluginType . ' doesn\'t exist'); - } - foreach ($this->_definitions->getMethodList($pluginType) as $pluginMethod => $methodTypes) { - $current = isset($lastPerMethod[$pluginMethod]) ? $lastPerMethod[$pluginMethod] : '__self'; - $currentKey = $type . '_' . $pluginMethod . '_' . $current; - if ($methodTypes & DefinitionInterface::LISTENER_AROUND) { - $this->_processed[$currentKey][DefinitionInterface::LISTENER_AROUND] = $key; - $lastPerMethod[$pluginMethod] = $key; - } - if ($methodTypes & DefinitionInterface::LISTENER_BEFORE) { - $this->_processed[$currentKey][DefinitionInterface::LISTENER_BEFORE][] = $key; - } - if ($methodTypes & DefinitionInterface::LISTENER_AFTER) { - $this->_processed[$currentKey][DefinitionInterface::LISTENER_AFTER][] = $key; - } - } - } - } - return $plugins; - } - return $this->_inherited[$type]; - } - - /** - * Trims starting backslash from plugin instance name - * - * @param array $plugins - * @return void - */ - private function trimInstanceStartingBackslash(&$plugins) - { - foreach ($plugins as &$plugin) { - $plugin['instance'] = ltrim($plugin['instance'], '\\'); - } + return $this->pluginListGenerator->inheritPlugins($type, $this->_data, $this->_inherited, $this->_processed); } /** @@ -224,16 +157,7 @@ private function trimInstanceStartingBackslash(&$plugins) */ protected function _sort($itemA, $itemB) { - if (isset($itemA['sortOrder'])) { - if (isset($itemB['sortOrder'])) { - return $itemA['sortOrder'] - $itemB['sortOrder']; - } - return $itemA['sortOrder']; - } elseif (isset($itemB['sortOrder'])) { - return (0 - (int)$itemB['sortOrder']); - } else { - return 0; - } + return ($itemA['sortOrder'] ?? PHP_INT_MIN) - ($itemB['sortOrder'] ?? PHP_INT_MIN); } /** @@ -280,8 +204,8 @@ public function getNext($type, $method, $code = '__self') protected function _loadScopedData() { $scope = $this->_configScope->getCurrentScope(); - if (false == isset($this->_loadedScopes[$scope])) { - $index = array_search($scope, $this->_scopePriorityScheme); + if (false === isset($this->_loadedScopes[$scope])) { + $index = array_search($scope, $this->_scopePriorityScheme, true); /** * Force current scope to be at the end of the scheme to ensure that default priority scopes are loaded. * Mostly happens when the current scope is primary. @@ -294,57 +218,47 @@ protected function _loadScopedData() $this->_scopePriorityScheme[] = $scope; $cacheId = implode('|', $this->_scopePriorityScheme) . "|" . $this->_cacheId; - $data = $this->_cache->load($cacheId); - if ($data) { - list($this->_data, $this->_inherited, $this->_processed) = $this->serializer->unserialize($data); - foreach ($this->_scopePriorityScheme as $scopeCode) { - $this->_loadedScopes[$scopeCode] = true; - } - } else { - foreach ($this->_loadScopedVirtualTypes() as $class) { - $this->_inheritPlugins($class); - } - foreach ($this->getClassDefinitions() as $class) { - $this->_inheritPlugins($class); - } - $this->_cache->save( - $this->serializer->serialize([$this->_data, $this->_inherited, $this->_processed]), - $cacheId - ); - } - $this->_pluginInstances = []; - } - } + $configData = $this->configLoader->load($cacheId); - /** - * Load virtual types for current scope - * - * @return array - */ - private function _loadScopedVirtualTypes() - { - $virtualTypes = []; - foreach ($this->_scopePriorityScheme as $scopeCode) { - if (!isset($this->_loadedScopes[$scopeCode])) { - $data = $this->_reader->read($scopeCode) ?: []; - unset($data['preferences']); - if (count($data) > 0) { - $this->_inherited = []; - $this->_processed = []; - $this->merge($data); - foreach ($data as $class => $config) { - if (isset($config['type'])) { - $virtualTypes[] = $class; - } + if ($configData) { + [$this->_data, $this->_inherited, $this->_processed] = $configData; + $this->_loadedScopes[$scope] = true; + } else { + $data = $this->_cache->load($cacheId); + if ($data) { + [$this->_data, $this->_inherited, $this->_processed] = $this->serializer->unserialize($data); + foreach ($this->_scopePriorityScheme as $scopeCode) { + $this->_loadedScopes[$scopeCode] = true; + } + } else { + [ + $virtualTypes, + $this->_scopePriorityScheme, + $this->_loadedScopes, + $this->_data, + $this->_inherited, + $this->_processed + ] = $this->pluginListGenerator->loadScopedVirtualTypes( + $this->_scopePriorityScheme, + $this->_loadedScopes, + $this->_data, + $this->_inherited, + $this->_processed + ); + foreach ($virtualTypes as $class) { + $this->_inheritPlugins($class); } + foreach ($this->getClassDefinitions() as $class) { + $this->_inheritPlugins($class); + } + $this->_cache->save( + $this->serializer->serialize([$this->_data, $this->_inherited, $this->_processed]), + $cacheId + ); } - $this->_loadedScopes[$scopeCode] = true; - } - if ($this->isCurrentScope($scopeCode)) { - break; } + $this->_pluginInstances = []; } - return $virtualTypes; } /** @@ -355,7 +269,7 @@ private function _loadScopedVirtualTypes() */ protected function isCurrentScope($scopeCode) { - return $this->_configScope->getCurrentScope() == $scopeCode; + return $this->_configScope->getCurrentScope() === $scopeCode; } /** @@ -376,45 +290,6 @@ protected function getClassDefinitions() */ public function merge(array $config) { - foreach ($config as $type => $typeConfig) { - if (isset($typeConfig['plugins'])) { - $type = ltrim($type, '\\'); - if (isset($this->_data[$type])) { - $this->_data[$type] = array_replace_recursive($this->_data[$type], $typeConfig['plugins']); - } else { - $this->_data[$type] = $typeConfig['plugins']; - } - } - } - } - - /** - * Remove from list not existing plugins - * - * @param array $plugins - * @return void - */ - private function filterPlugins(array &$plugins) - { - foreach ($plugins as $name => $plugin) { - if (empty($plugin['instance'])) { - unset($plugins[$name]); - $this->getLogger()->info("Reference to undeclared plugin with name '{$name}'."); - } - } - } - - /** - * Get logger - * - * @return \Psr\Log\LoggerInterface - * @deprecated 100.2.0 - */ - private function getLogger() - { - if ($this->logger === null) { - $this->logger = $this->_objectManager->get(\Psr\Log\LoggerInterface::class); - } - return $this->logger; + $this->_data = $this->pluginListGenerator->merge($config, $this->_data); } } diff --git a/lib/internal/Magento/Framework/Interception/PluginListGenerator.php b/lib/internal/Magento/Framework/Interception/PluginListGenerator.php new file mode 100644 index 0000000000000..effc291bb883b --- /dev/null +++ b/lib/internal/Magento/Framework/Interception/PluginListGenerator.php @@ -0,0 +1,431 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Interception; + +use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Config\ReaderInterface; +use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Interception\ObjectManager\ConfigInterface; +use Magento\Framework\ObjectManager\DefinitionInterface as ClassDefinitions; +use Magento\Framework\ObjectManager\RelationsInterface; +use Psr\Log\LoggerInterface; + +/** + * Plugin list configuration writer and loader for scopes. + */ +class PluginListGenerator implements ConfigWriterInterface, ConfigLoaderInterface +{ + /** + * @var ScopeInterface + */ + private $scopeConfig; + + /** + * Configuration reader + * + * @var ReaderInterface + */ + private $reader; + + /** + * Cache tag + * + * @var string + */ + private $cacheId = 'plugin-list'; + + /** + * Loaded scopes + * + * @var array + */ + private $loadedScopes = []; + + /** + * Type config + * + * @var ConfigInterface + */ + private $omConfig; + + /** + * Class relations information provider + * + * @var RelationsInterface + */ + private $relations; + + /** + * List of interception methods per plugin + * + * @var DefinitionInterface + */ + private $definitions; + + /** + * List of interceptable application classes + * + * @var ClassDefinitions + */ + private $classDefinitions; + + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var DirectoryList + */ + private $directoryList; + + /** + * @var array + */ + private $pluginData; + + /** + * @var array + */ + private $inherited = []; + + /** + * @var array + */ + private $processed; + + /** + * Scope priority loading scheme + * + * @var string[] + */ + private $scopePriorityScheme; + + /** + * @var array + */ + private $globalScopePluginData = []; + + /** + * @param ReaderInterface $reader + * @param ScopeInterface $scopeConfig + * @param ConfigInterface $omConfig + * @param RelationsInterface $relations + * @param DefinitionInterface $definitions + * @param ClassDefinitions $classDefinitions + * @param LoggerInterface $logger + * @param DirectoryList $directoryList + * @param array $scopePriorityScheme + */ + public function __construct( + ReaderInterface $reader, + ScopeInterface $scopeConfig, + ConfigInterface $omConfig, + RelationsInterface $relations, + DefinitionInterface $definitions, + ClassDefinitions $classDefinitions, + LoggerInterface $logger, + DirectoryList $directoryList, + array $scopePriorityScheme = ['global'] + ) { + $this->reader = $reader; + $this->scopeConfig = $scopeConfig; + $this->omConfig = $omConfig; + $this->relations = $relations; + $this->definitions = $definitions; + $this->classDefinitions = $classDefinitions; + $this->logger = $logger; + $this->directoryList = $directoryList; + $this->scopePriorityScheme = $scopePriorityScheme; + } + + /** + * @inheritdoc + */ + public function write(array $scopes): void + { + foreach ($scopes as $scope) { + $this->scopeConfig->setCurrentScope($scope); + if (false === isset($this->loadedScopes[$scope])) { + if (false === in_array($scope, $this->scopePriorityScheme, true)) { + $this->scopePriorityScheme[] = $scope; + } + $cacheId = implode('|', $this->scopePriorityScheme) . "|" . $this->cacheId; + [ + $virtualTypes, + $this->scopePriorityScheme, + $this->loadedScopes, + $this->pluginData, + $this->inherited, + $this->processed + ] = $this->loadScopedVirtualTypes( + $this->scopePriorityScheme, + $this->loadedScopes, + $this->pluginData, + $this->inherited, + $this->processed + ); + foreach ($virtualTypes as $class) { + $this->inheritPlugins($class, $this->pluginData, $this->inherited, $this->processed); + } + foreach (array_keys($this->pluginData) as $className) { + $this->inheritPlugins($className, $this->pluginData, $this->inherited, $this->processed); + } + foreach ($this->getClassDefinitions() as $class) { + $this->inheritPlugins($class, $this->pluginData, $this->inherited, $this->processed); + } + $this->writeConfig( + $cacheId, + [$this->pluginData, $this->inherited, $this->processed] + ); + // need global & primary scopes plugin data for other scopes + if ($scope === 'global') { + $this->globalScopePluginData = $this->pluginData; + } + if (count($this->scopePriorityScheme) > 2) { + array_pop($this->scopePriorityScheme); + // merge global & primary scopes plugin data to other scopes by default + $this->pluginData = $this->globalScopePluginData; + } + } + } + } + + /** + * @inheritdoc + */ + public function load(string $cacheId): array + { + $file = $this->directoryList->getPath(DirectoryList::GENERATED_METADATA) . '/' . $cacheId . '.' . 'php'; + if (file_exists($file)) { + return include $file; + } + + return []; + } + + /** + * Load virtual types for current scope + * + * @param array $scopePriorityScheme + * @param array $loadedScopes + * @param array|null $pluginData + * @param array $inherited + * @param array $processed + * @return array + */ + public function loadScopedVirtualTypes($scopePriorityScheme, $loadedScopes, $pluginData, $inherited, $processed) + { + $virtualTypes = []; + foreach ($scopePriorityScheme as $scopeCode) { + if (!isset($loadedScopes[$scopeCode])) { + $data = $this->reader->read($scopeCode) ?: []; + unset($data['preferences']); + if (count($data) > 0) { + $inherited = []; + $processed = []; + $pluginData = $this->merge($data, $pluginData); + foreach ($data as $class => $config) { + if (isset($config['type'])) { + $virtualTypes[] = $class; + } + } + } + $loadedScopes[$scopeCode] = true; + } + if ($this->isCurrentScope($scopeCode)) { + break; + } + } + return [$virtualTypes, $scopePriorityScheme, $loadedScopes, $pluginData, $inherited, $processed]; + } + + /** + * Returns class definitions + * + * @return array + */ + private function getClassDefinitions() + { + return $this->classDefinitions->getClasses(); + } + + /** + * Whether scope code is current scope code + * + * @param string $scopeCode + * @return bool + */ + private function isCurrentScope($scopeCode) + { + return $this->scopeConfig->getCurrentScope() === $scopeCode; + } + + /** + * Collect parent types configuration for requested type + * + * @param string $type + * @param array $pluginData + * @param array $inherited + * @param array $processed + * @return array + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) + */ + public function inheritPlugins($type, &$pluginData, &$inherited, &$processed) + { + $type = ltrim($type, '\\'); + if (!isset($inherited[$type])) { + $realType = $this->omConfig->getOriginalInstanceType($type); + + if ($realType !== $type) { + $plugins = $this->inheritPlugins($realType, $pluginData, $inherited, $processed); + } elseif ($this->relations->has($type)) { + $relations = $this->relations->getParents($type); + $plugins = []; + foreach ($relations as $relation) { + if ($relation) { + $relationPlugins = $this->inheritPlugins($relation, $pluginData, $inherited, $processed); + if ($relationPlugins) { + $plugins = array_replace_recursive($plugins, $relationPlugins); + } + } + } + } else { + $plugins = []; + } + if (isset($pluginData[$type])) { + if (!$plugins) { + $plugins = $pluginData[$type]; + } else { + $plugins = array_replace_recursive($plugins, $pluginData[$type]); + } + } + $inherited[$type] = null; + if (is_array($plugins) && count($plugins)) { + $this->filterPlugins($plugins); + uasort($plugins, function ($itemA, $itemB) { + return ($itemA['sortOrder'] ?? PHP_INT_MIN) - ($itemB['sortOrder'] ?? PHP_INT_MIN); + }); + $this->trimInstanceStartingBackslash($plugins); + $inherited[$type] = $plugins; + $lastPerMethod = []; + foreach ($plugins as $key => $plugin) { + // skip disabled plugins + if (isset($plugin['disabled']) && $plugin['disabled']) { + unset($plugins[$key]); + continue; + } + $pluginType = $this->omConfig->getOriginalInstanceType($plugin['instance']); + if (!class_exists($pluginType)) { + throw new \InvalidArgumentException('Plugin class ' . $pluginType . ' doesn\'t exist'); + } + foreach ($this->definitions->getMethodList($pluginType) as $pluginMethod => $methodTypes) { + $current = $lastPerMethod[$pluginMethod] ?? '__self'; + $currentKey = $type . '_' . $pluginMethod . '_' . $current; + if ($methodTypes & DefinitionInterface::LISTENER_AROUND) { + $processed[$currentKey][DefinitionInterface::LISTENER_AROUND] = $key; + $lastPerMethod[$pluginMethod] = $key; + } + if ($methodTypes & DefinitionInterface::LISTENER_BEFORE) { + $processed[$currentKey][DefinitionInterface::LISTENER_BEFORE][] = $key; + } + if ($methodTypes & DefinitionInterface::LISTENER_AFTER) { + $processed[$currentKey][DefinitionInterface::LISTENER_AFTER][] = $key; + } + } + } + } + return $plugins; + } + return $inherited[$type]; + } + + /** + * Trims starting backslash from plugin instance name + * + * @param array $plugins + * @return void + */ + public function trimInstanceStartingBackslash(&$plugins) + { + foreach ($plugins as &$plugin) { + $plugin['instance'] = ltrim($plugin['instance'], '\\'); + } + } + + /** + * Remove from list not existing plugins + * + * @param array $plugins + * @return void + */ + public function filterPlugins(array &$plugins) + { + foreach ($plugins as $name => $plugin) { + if (empty($plugin['instance'])) { + unset($plugins[$name]); + $this->logger->info("Reference to undeclared plugin with name '{$name}'."); + } + } + } + + /** + * Merge configuration + * + * @param array $config + * @param array|null $pluginData + * @return array + */ + public function merge(array $config, $pluginData) + { + foreach ($config as $type => $typeConfig) { + if (isset($typeConfig['plugins'])) { + $type = ltrim($type, '\\'); + if (isset($pluginData[$type])) { + $pluginData[$type] = array_replace_recursive($pluginData[$type], $typeConfig['plugins']); + } else { + $pluginData[$type] = $typeConfig['plugins']; + } + } + } + + return $pluginData; + } + + /** + * Writes config in storage + * + * @param string $key + * @param array $config + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + private function writeConfig(string $key, array $config) + { + $this->initialize(); + $configuration = sprintf('<?php return %s;', var_export($config, true)); + file_put_contents( + $this->directoryList->getPath(DirectoryList::GENERATED_METADATA) . '/' . $key . '.php', + $configuration + ); + } + + /** + * Initializes writer + * + * @return void + * @throws \Magento\Framework\Exception\FileSystemException + */ + private function initialize() + { + if (!file_exists($this->directoryList->getPath(DirectoryList::GENERATED_METADATA))) { + mkdir($this->directoryList->getPath(DirectoryList::GENERATED_METADATA)); + } + } +} diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php b/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php index 75b520e94c70e..dd8dbe2f811dc 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/ObjectManager/Config/DeveloperTest.php @@ -58,4 +58,23 @@ public function testGetOriginalInstanceTypeReturnsInterceptedClass() $this->assertEquals('SomeClass\Interceptor', $this->model->getInstanceType('SomeClass')); $this->assertEquals('SomeClass', $this->model->getOriginalInstanceType('SomeClass')); } + + /** + * Test correct instance type is returned when plugins are created for virtual type parents + * + * @return void + */ + public function testGetInstanceTypeWithPluginOnVirtualTypeParent() : void + { + $reflectionClass = new \ReflectionClass(get_class($this->model)); + $reflectionProperty = $reflectionClass->getProperty('_virtualTypes'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($this->model, ['SomeVirtualClass' => 'SomeClass']); + + $this->interceptionConfig->expects($this->once())->method('hasPlugins')->with('SomeClass')->willReturn(true); + $this->model->setInterceptionConfig($this->interceptionConfig); + + $instanceType = $this->model->getInstanceType('SomeVirtualClass'); + $this->assertEquals('SomeClass\Interceptor', $instanceType); + } } diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/PluginList/PluginListTest.php b/lib/internal/Magento/Framework/Interception/Test/Unit/PluginList/PluginListTest.php index 56740268026c2..b0cb500eeed66 100644 --- a/lib/internal/Magento/Framework/Interception/Test/Unit/PluginList/PluginListTest.php +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/PluginList/PluginListTest.php @@ -9,23 +9,23 @@ use Magento\Framework\Config\CacheInterface; use Magento\Framework\Config\ScopeInterface; +use Magento\Framework\Interception\ConfigLoaderInterface; use Magento\Framework\Interception\ObjectManager\ConfigInterface; use Magento\Framework\Interception\PluginList\PluginList; +use Magento\Framework\Interception\PluginListGenerator; use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item; -use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item\Enhanced; use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainer; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainerPlugin\Simple as ItemContainerPlugin; use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Advanced; use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Simple; use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash; -use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash\Plugin; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash\Plugin as StartingBackslashPlugin; use Magento\Framework\ObjectManager\Config\Reader\Dom; use Magento\Framework\ObjectManager\Definition\Runtime; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\Serialize\SerializerInterface; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Psr\Log\LoggerInterface; require_once __DIR__ . '/../Custom/Module/Model/Item.php'; require_once __DIR__ . '/../Custom/Module/Model/Item/Enhanced.php'; @@ -57,90 +57,146 @@ class PluginListTest extends TestCase */ private $cacheMock; - /** - * @var LoggerInterface|MockObject - */ - private $loggerMock; - /** * @var SerializerInterface|MockObject */ private $serializerMock; /** - * @var ObjectManagerInterface|MockObject + * @var ConfigLoaderInterface|MockObject */ - private $objectManagerMock; + private $configLoaderMock; protected function setUp(): void { - $readerMap = include __DIR__ . '/../_files/reader_mock_map.php'; + $loadScoped = include __DIR__ . '/../_files/load_scoped_mock_map.php'; $readerMock = $this->createMock(Dom::class); - $readerMock->expects($this->any())->method('read')->willReturnMap($readerMap); $this->configScopeMock = $this->getMockForAbstractClass(ScopeInterface::class); $this->cacheMock = $this->getMockBuilder(CacheInterface::class) ->setMethods(['get']) ->getMockForAbstractClass(); // turn cache off - $this->cacheMock->expects($this->any()) - ->method('get') - ->willReturn(false); + $this->cacheMock->method('get')->willReturn(false); $omConfigMock = $this->getMockForAbstractClass( ConfigInterface::class ); - $omConfigMock->expects($this->any())->method('getOriginalInstanceType')->willReturnArgument(0); + $omConfigMock->method('getOriginalInstanceType')->willReturnArgument(0); - $this->objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) + $objectManagerMock = $this->getMockBuilder(ObjectManagerInterface::class) ->setMethods(['get']) ->getMockForAbstractClass(); - $this->objectManagerMock->expects($this->any()) - ->method('get') - ->willReturnArgument(0); + $objectManagerMock->method('get')->willReturnArgument(0); $this->serializerMock = $this->getMockForAbstractClass(SerializerInterface::class); - $definitions = new Runtime(); - - $objectManagerHelper = new ObjectManager($this); - $this->object = $objectManagerHelper->getObject( - PluginList::class, - [ - 'reader' => $readerMock, - 'configScope' => $this->configScopeMock, - 'cache' => $this->cacheMock, - 'relations' => new \Magento\Framework\ObjectManager\Relations\Runtime(), - 'omConfig' => $omConfigMock, - 'definitions' => new \Magento\Framework\Interception\Definition\Runtime(), - 'objectManager' => $this->objectManagerMock, - 'classDefinitions' => $definitions, - 'scopePriorityScheme' => ['global'], - 'cacheId' => 'interception', - 'serializer' => $this->serializerMock - ] - ); - - $this->loggerMock = $this->getMockForAbstractClass(LoggerInterface::class); - $objectManagerHelper->setBackwardCompatibleProperty( - $this->object, - 'logger', - $this->loggerMock - ); + $this->configLoaderMock = $this->getMockBuilder(ConfigLoaderInterface::class) + ->onlyMethods(['load']) + ->getMockForAbstractClass(); + $pluginListGeneratorMock = $this->getMockBuilder(PluginListGenerator::class) + ->disableOriginalConstructor() + ->onlyMethods(['loadScopedVirtualTypes', 'inheritPlugins']) + ->getMock(); + $pluginListGeneratorMock->method('loadScopedVirtualTypes') + ->willReturnMap($loadScoped); + + $definitions = $this->getMockBuilder(Runtime::class) + ->disableOriginalConstructor() + ->getMock(); + $definitions->method('getClasses')->willReturn([]); + + // tested class is a mock to be able to set its protected properties values in closure + $this->object = $this->getMockBuilder(PluginList::class) + ->disableProxyingToOriginalMethods() + ->onlyMethods(['_inheritPlugins']) + ->setConstructorArgs( + [ + 'reader' => $readerMock, + 'configScope' => $this->configScopeMock, + 'cache' => $this->cacheMock, + 'relations' => new \Magento\Framework\ObjectManager\Relations\Runtime(), + 'omConfig' => $omConfigMock, + 'definitions' => new \Magento\Framework\Interception\Definition\Runtime(), + 'objectManager' => $objectManagerMock, + 'classDefinitions' => $definitions, + 'scopePriorityScheme' => ['global'], + 'cacheId' => 'interception', + 'serializer' => $this->serializerMock, + 'configLoader' => $this->configLoaderMock, + 'pluginListGenerator' => $pluginListGeneratorMock + ] + ) + ->getMock(); } public function testGetPlugin() { - $this->configScopeMock->expects($this->any())->method('getCurrentScope')->willReturn('backend'); + $inheritPlugins = function ($type) { + $inheritedItem = [ + Item::class => [ + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + 'simple_plugin' => [ + 'sortOrder' => 10, + 'instance' => Simple::class + ] + ] + ]; + $processedItem = [ + 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item_getName___self' => [ + 2 => 'advanced_plugin', + 4 => ['advanced_plugin'] + ], + 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item_getName_advanced_plugin' => [ + 4 => ['simple_plugin'] + ] + ]; + $inheritedItemContainer = [ + ItemContainer::class => [ + 'simple_plugin' => [ + 'sortOrder' => 15, + 'instance' => ItemContainerPlugin::class + ] + ] + ]; + $processedItemContainer = [ + 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainer_getName___self' => [ + 4 => ['simple_plugin'] + ] + ]; + $inheritedStartingBackslash = [ + StartingBackslash::class => [ + 'simple_plugin' => [ + 'sortOrder' => 20, + 'instance' => StartingBackslashPlugin::class + ] + ] + ]; + + if ($type === 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item') { + $this->_inherited = $inheritedItem; /** @phpstan-ignore-line */ + $this->_processed = $processedItem; /** @phpstan-ignore-line */ + } + if ($type === 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainer') { + $this->_inherited = array_merge($inheritedItem, $inheritedItemContainer); /** @phpstan-ignore-line */ + $this->_processed = array_merge($processedItem, $processedItemContainer); /** @phpstan-ignore-line */ + } + if ($type === 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash') { + /** @phpstan-ignore-next-line */ + $this->_inherited = array_merge($inheritedItem, $inheritedItemContainer, $inheritedStartingBackslash); + $this->_processed = array_merge($processedItem, $processedItemContainer); /** @phpstan-ignore-line */ + } + }; + $inheritPlugins = $inheritPlugins->bindTo($this->object, PluginList::class); + $this->object->method('_inheritPlugins')->willReturnCallback($inheritPlugins); + + $this->configScopeMock->method('getCurrentScope')->willReturn('backend'); $this->object->getNext(Item::class, 'getName'); - $this->object->getNext( - ItemContainer::class, - 'getName' - ); - $this->object->getNext( - StartingBackslash::class, - 'getName' - ); + $this->object->getNext(ItemContainer::class, 'getName'); + $this->object->getNext(StartingBackslash::class, 'getName'); $this->assertEquals( Simple::class, $this->object->getPlugin( @@ -156,14 +212,14 @@ public function testGetPlugin() ) ); $this->assertEquals( - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainerPlugin\Simple::class, + ItemContainerPlugin::class, $this->object->getPlugin( ItemContainer::class, 'simple_plugin' ) ); $this->assertEquals( - Plugin::class, + StartingBackslashPlugin::class, $this->object->getPlugin( StartingBackslash::class, 'simple_plugin' @@ -189,13 +245,33 @@ public function testGetPlugins( array $scopePriorityScheme = ['global'] ): void { $this->setScopePriorityScheme($scopePriorityScheme); - $this->configScopeMock->expects( - $this->any() - )->method( - 'getCurrentScope' - )->willReturn( - $scopeCode - ); + $this->configScopeMock->method('getCurrentScope')->willReturn($scopeCode); + + $inheritPlugins = function ($type) { + $inheritedItem = [ + Item::class => [ + 'simple_plugin' => [ + 'sortOrder' => 10, + 'instance' => Simple::class + ] + ] + ]; + $processedItem = [ + 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item_getName___self' => [ + 4 => [ + 'simple_plugin' + ] + ], + ]; + + if ($type === 'Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item') { + $this->_inherited = $inheritedItem; /** @phpstan-ignore-line */ + $this->_processed = $processedItem; /** @phpstan-ignore-line */ + } + }; + $inheritPlugins = $inheritPlugins->bindTo($this->object, PluginList::class); + $this->object->method('_inheritPlugins')->willReturnCallback($inheritPlugins); + $this->assertEquals($expectedResult, $this->object->getNext($type, $method, $code)); } @@ -209,139 +285,10 @@ public function getPluginsDataProvider() [4 => ['simple_plugin']], Item::class, 'getName', 'global', - ], - [ - // advanced plugin has lower sort order - [2 => 'advanced_plugin', 4 => ['advanced_plugin']], - Item::class, - 'getName', - 'backend' - ], - [ - // advanced plugin has lower sort order - [4 => ['simple_plugin']], - Item::class, - 'getName', - 'backend', - 'advanced_plugin' - ], - // simple plugin is disabled in configuration for - // \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item in frontend - [null, Item::class, 'getName', 'frontend'], - // test plugin inheritance - [ - [4 => ['simple_plugin']], - Enhanced::class, - 'getName', - 'global' - ], - [ - // simple plugin is disabled in configuration for parent - [2 => 'advanced_plugin', 4 => ['advanced_plugin']], - Enhanced::class, - 'getName', - 'frontend' - ], - [ - null, - ItemContainer::class, - 'getName', - 'global' - ], - [ - [4 => ['simple_plugin']], - ItemContainer::class, - 'getName', - 'backend' - ], - [ - // even though the scope is primary, both primary and global scopes are loaded - // because global is in default priority scheme - [ - 4 => [ - 'primary_plugin', - 'simple_plugin', - ] - ], - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class, - 'getName', - 'primary', - '__self', - ['primary', 'global'] - ], - [ - [ - 4 => [ - 'primary_plugin', - 'simple_plugin', - ] - ], - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class, - 'getName', - 'global', - '__self', - ['primary', 'global'] - ], - [ - [ - 4 => [ - 'primary_plugin', - ] - ], - \Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item::class, - 'getName', - 'frontend', - '__self', - ['primary', 'global'] - ], + ] ]; } - /** - * @covers \Magento\Framework\Interception\PluginList\PluginList::getNext - * @covers \Magento\Framework\Interception\PluginList\PluginList::_inheritPlugins - */ - public function testInheritPluginsWithNonExistingClass() - { - $this->expectException('InvalidArgumentException'); - $this->configScopeMock->expects($this->any()) - ->method('getCurrentScope') - ->willReturn('frontend'); - - $this->object->getNext('SomeType', 'someMethod'); - } - - public function testLoadScopedDataNotCached() - { - $this->configScopeMock->expects($this->exactly(3)) - ->method('getCurrentScope') - ->willReturn('scope'); - $this->serializerMock->expects($this->once()) - ->method('serialize'); - $this->serializerMock->expects($this->never()) - ->method('unserialize'); - $this->cacheMock->expects($this->once()) - ->method('save'); - - $this->assertNull($this->object->getNext('Type', 'method')); - } - - /** - * @covers \Magento\Framework\Interception\PluginList\PluginList::getNext - * @covers \Magento\Framework\Interception\PluginList\PluginList::_inheritPlugins - */ - public function testInheritPluginsWithNotExistingPlugin() - { - $this->loggerMock->expects($this->once()) - ->method('info') - ->with("Reference to undeclared plugin with name 'simple_plugin'."); - $this->configScopeMock->expects($this->any()) - ->method('getCurrentScope') - ->willReturn('frontend'); - - $this->assertNull($this->object->getNext('typeWithoutInstance', 'someMethod')); - } - /** * @covers \Magento\Framework\Interception\PluginList\PluginList::getNext * @covers \Magento\Framework\Interception\PluginList\PluginList::_loadScopedData @@ -365,6 +312,23 @@ public function testLoadScopedDataCached() ->with('global|scope|interception') ->willReturn($serializedData); + $inheritPlugins = function ($type) { + $inherited = [ + 0 => 'key', + 'Type' => null + ]; + $processed = [ + 0 => 'key' + ]; + + if ($type === 'Type') { + $this->_inherited = $inherited; /** @phpstan-ignore-line */ + $this->_processed = $processed; /** @phpstan-ignore-line */ + } + }; + $inheritPlugins = $inheritPlugins->bindTo($this->object, PluginList::class); + $this->object->method('_inheritPlugins')->willReturnCallback($inheritPlugins); + $this->assertNull($this->object->getNext('Type', 'method')); } @@ -372,29 +336,37 @@ public function testLoadScopedDataCached() * @covers \Magento\Framework\Interception\PluginList\PluginList::getNext * @covers \Magento\Framework\Interception\PluginList\PluginList::_loadScopedData */ - public function testLoadScopeDataWithEmptyData() + public function testLoadScopedDataGenerated() { - $this->objectManagerMock->expects($this->any()) - ->method('get') - ->willReturnArgument(0); - $this->configScopeMock->expects($this->any()) + $this->configScopeMock->expects($this->once()) ->method('getCurrentScope') - ->willReturn('emptyscope'); + ->willReturn('scope'); - $this->assertEquals( - [4 => ['simple_plugin']], - $this->object->getNext( - Item::class, - 'getName' - ) - ); - $this->assertEquals( - Simple::class, - $this->object->getPlugin( - Item::class, - 'simple_plugin' - ) - ); + $data = [['key'], ['key'], ['key']]; + + $this->configLoaderMock->expects($this->once()) + ->method('load') + ->with('global|scope|interception') + ->willReturn($data); + + $inheritPlugins = function ($type) { + $inherited = [ + 0 => 'key', + 'Type' => null + ]; + $processed = [ + 0 => 'key' + ]; + + if ($type === 'Type') { + $this->_inherited = $inherited; /** @phpstan-ignore-line */ + $this->_processed = $processed; /** @phpstan-ignore-line */ + } + }; + $inheritPlugins = $inheritPlugins->bindTo($this->object, PluginList::class); + $this->object->method('_inheritPlugins')->willReturnCallback($inheritPlugins); + + $this->assertNull($this->object->getNext('Type', 'method')); } /** diff --git a/lib/internal/Magento/Framework/Interception/Test/Unit/_files/load_scoped_mock_map.php b/lib/internal/Magento/Framework/Interception/Test/Unit/_files/load_scoped_mock_map.php new file mode 100644 index 0000000000000..b5002512dc093 --- /dev/null +++ b/lib/internal/Magento/Framework/Interception/Test/Unit/_files/load_scoped_mock_map.php @@ -0,0 +1,86 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\Item; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainer; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemContainerPlugin\Simple; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Advanced; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\ItemPlugin\Simple as ItemPluginSimple; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash; +use Magento\Framework\Interception\Test\Unit\Custom\Module\Model\StartingBackslash\Plugin; + +return [ + [ + [1 => 'global'], + [], + [], + [], + null, + [ + [], + [1 => 'global'], + ['global' => true], + [ + Item::class => [ + 'simple_plugin' => [ + 'sortOrder' => 10, + 'instance' => ItemPluginSimple::class, + ], + ] + ], + [], + [] + ], + ], + [ + [ + 'global', + 'backend' + ], + [], + [], + [], + null, + [ + [], + [ + 'global', + 'backend' + ], + [ + 'global' => true, + 'backend' => true + ], + [ + Item::class => [ + 'simple_plugin' => [ + 'sortOrder' => 10, + 'instance' => Simple::class, + ], + 'advanced_plugin' => [ + 'sortOrder' => 5, + 'instance' => Advanced::class, + ], + ], + ItemContainer::class => [ + 'simple_plugin' => [ + 'sortOrder' => 15, + 'instance' => Simple::class, + ], + ], + StartingBackslash::class => [ + 'simple_plugin' => [ + 'sortOrder' => 20, + 'instance' => Plugin::class, + ], + ] + ], + [], + [] + ] + ] +]; diff --git a/lib/internal/Magento/Framework/Json/Decoder.php b/lib/internal/Magento/Framework/Json/Decoder.php index 57e160cac7bbf..fbc99a036daa9 100644 --- a/lib/internal/Magento/Framework/Json/Decoder.php +++ b/lib/internal/Magento/Framework/Json/Decoder.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Json; /** - * @deprecated 100.2.0 @see \Magento\Framework\Serialize\Serializer\Json::unserialize + * @deprecated 101.0.0 @see \Magento\Framework\Serialize\Serializer\Json::unserialize */ class Decoder implements DecoderInterface { diff --git a/lib/internal/Magento/Framework/Json/DecoderInterface.php b/lib/internal/Magento/Framework/Json/DecoderInterface.php index 9aa630fd9ca0c..baef2154ea618 100644 --- a/lib/internal/Magento/Framework/Json/DecoderInterface.php +++ b/lib/internal/Magento/Framework/Json/DecoderInterface.php @@ -10,7 +10,8 @@ * * @api * - * @deprecated 100.2.0 @see \Magento\Framework\Serialize\Serializer\Json::unserialize + * @deprecated 101.0.0 @see \Magento\Framework\Serialize\Serializer\Json::unserialize + * @since 100.0.2 */ interface DecoderInterface { diff --git a/lib/internal/Magento/Framework/Json/Encoder.php b/lib/internal/Magento/Framework/Json/Encoder.php index 35d259781d67a..0c53edeb6ae00 100644 --- a/lib/internal/Magento/Framework/Json/Encoder.php +++ b/lib/internal/Magento/Framework/Json/Encoder.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Json; /** - * @deprecated 100.2.0 @see \Magento\Framework\Serialize\Serializer\Json::serialize + * @deprecated 101.0.0 @see \Magento\Framework\Serialize\Serializer\Json::serialize */ class Encoder implements EncoderInterface { diff --git a/lib/internal/Magento/Framework/Json/EncoderInterface.php b/lib/internal/Magento/Framework/Json/EncoderInterface.php index 9fb720d11fea3..59ea0f47d556c 100644 --- a/lib/internal/Magento/Framework/Json/EncoderInterface.php +++ b/lib/internal/Magento/Framework/Json/EncoderInterface.php @@ -10,7 +10,8 @@ * * @api * - * @deprecated 100.2.0 @see \Magento\Framework\Serialize\Serializer\Json::serialize + * @deprecated 101.0.0 @see \Magento\Framework\Serialize\Serializer\Json::serialize + * @since 100.0.2 */ interface EncoderInterface { diff --git a/lib/internal/Magento/Framework/Json/Helper/Data.php b/lib/internal/Magento/Framework/Json/Helper/Data.php index 67589d00eb99d..a1f2e01a2e92e 100644 --- a/lib/internal/Magento/Framework/Json/Helper/Data.php +++ b/lib/internal/Magento/Framework/Json/Helper/Data.php @@ -8,7 +8,7 @@ /** * Json data helper * - * @deprecated 100.2.0 @see \Magento\Framework\Serialize\Serializer\Json + * @deprecated 101.0.0 @see \Magento\Framework\Serialize\Serializer\Json */ class Data extends \Magento\Framework\App\Helper\AbstractHelper { diff --git a/lib/internal/Magento/Framework/Locale/ConfigInterface.php b/lib/internal/Magento/Framework/Locale/ConfigInterface.php index e5f8bc00e5466..a4ee02c323b19 100644 --- a/lib/internal/Magento/Framework/Locale/ConfigInterface.php +++ b/lib/internal/Magento/Framework/Locale/ConfigInterface.php @@ -9,6 +9,7 @@ * Provides access to locale-related config information * * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/Locale/CurrencyInterface.php b/lib/internal/Magento/Framework/Locale/CurrencyInterface.php index e52ac551f54ec..e36a905a6c473 100644 --- a/lib/internal/Magento/Framework/Locale/CurrencyInterface.php +++ b/lib/internal/Magento/Framework/Locale/CurrencyInterface.php @@ -9,6 +9,7 @@ * Provides access to currency config information * * @api + * @since 100.0.2 */ interface CurrencyInterface { diff --git a/lib/internal/Magento/Framework/Locale/Format.php b/lib/internal/Magento/Framework/Locale/Format.php index 934c9638c7392..e332840327bf7 100644 --- a/lib/internal/Magento/Framework/Locale/Format.php +++ b/lib/internal/Magento/Framework/Locale/Format.php @@ -15,6 +15,17 @@ class Format implements \Magento\Framework\Locale\FormatInterface */ private const JAPAN_LOCALE_CODE = 'ja_JP'; + /** + * Arab locale code + */ + private const ARABIC_LOCALE_CODES = [ + 'ar_DZ', + 'ar_EG', + 'ar_KW', + 'ar_MA', + 'ar_SA', + ]; + /** * @var \Magento\Framework\App\ScopeResolverInterface */ @@ -73,6 +84,11 @@ public function getNumber($value) return (float)$value; } + /** Normalize for Arabic locale */ + if (in_array($this->_localeResolver->getLocale(), self::ARABIC_LOCALE_CODES)) { + $value = $this->normalizeArabicLocale($value); + } + //trim spaces and apostrophes $value = preg_replace('/[^0-9^\^.,-]/m', '', $value); @@ -163,4 +179,22 @@ public function getPriceFormat($localeCode = null, $currencyCode = null) return $result; } + + /** + * Normalizes the number of Arabic locale. + * + * Substitutes Arabic thousands grouping and Arabic decimal separator symbols (U+066C, U+066B) + * with common grouping and decimal separator + * + * @param string $value + * @return string + */ + private function normalizeArabicLocale($value): string + { + $arabicGroupSymbol = '٬'; + $arabicDecimalSymbol = '٫'; + $value = str_replace([$arabicGroupSymbol, $arabicDecimalSymbol], [',', '.'], $value); + + return $value; + } } diff --git a/lib/internal/Magento/Framework/Locale/FormatInterface.php b/lib/internal/Magento/Framework/Locale/FormatInterface.php index 1312c8841c23b..1a15b2610be54 100644 --- a/lib/internal/Magento/Framework/Locale/FormatInterface.php +++ b/lib/internal/Magento/Framework/Locale/FormatInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface FormatInterface { diff --git a/lib/internal/Magento/Framework/Locale/ListsInterface.php b/lib/internal/Magento/Framework/Locale/ListsInterface.php index 22e9bf00ebce7..c7d509e20546a 100644 --- a/lib/internal/Magento/Framework/Locale/ListsInterface.php +++ b/lib/internal/Magento/Framework/Locale/ListsInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface ListsInterface extends OptionInterface { diff --git a/lib/internal/Magento/Framework/Locale/ResolverInterface.php b/lib/internal/Magento/Framework/Locale/ResolverInterface.php index 22ec8fb70304c..70d397ddf3cc1 100644 --- a/lib/internal/Magento/Framework/Locale/ResolverInterface.php +++ b/lib/internal/Magento/Framework/Locale/ResolverInterface.php @@ -9,6 +9,7 @@ * Manages locale config information * * @api + * @since 100.0.2 */ interface ResolverInterface { diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/CurrencyTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/CurrencyTest.php index f04300eeeb5ab..664540d33f7ad 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/CurrencyTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/CurrencyTest.php @@ -41,8 +41,8 @@ class CurrencyTest extends TestCase const TEST_NONCACHED_CURRENCY_LOCALE = 'en_US'; const TEST_CACHED_CURRENCY = 'CAD'; const TEST_CACHED_CURRENCY_LOCALE = 'en_CA'; - const TEST_NONEXISTANT_CURRENCY = 'QQQ'; - const TEST_NONEXISTANT_CURRENCY_LOCALE = 'fr_FR'; + const TEST_NONEXISTENT_CURRENCY = 'QQQ'; + const TEST_NONEXISTENT_CURRENCY_LOCALE = 'fr_FR'; const TEST_EXCEPTION_CURRENCY = 'ZZZ'; const TEST_EXCEPTION_CURRENCY_LOCALE = 'es_ES'; @@ -146,9 +146,9 @@ public function testGetCurrencyCached() $this->assertEquals([self::TEST_CACHED_CURRENCY], $retrievedCurrencyObject->getCurrencyList()); } - public function testGetNonExistantCurrency() + public function testGetNonExistentCurrency() { - $options = new \Zend_Currency(null, self::TEST_NONEXISTANT_CURRENCY_LOCALE); + $options = new \Zend_Currency(null, self::TEST_NONEXISTENT_CURRENCY_LOCALE); $this->mockCurrencyFactory ->expects($this->once()) @@ -164,10 +164,10 @@ public function testGetNonExistantCurrency() ->method('dispatch'); $retrievedCurrencyObject = $this->testCurrencyObject - ->getCurrency(self::TEST_NONEXISTANT_CURRENCY); + ->getCurrency(self::TEST_NONEXISTENT_CURRENCY); $this->assertInstanceOf('Zend_Currency', $retrievedCurrencyObject); - $this->assertEquals(self::TEST_NONEXISTANT_CURRENCY_LOCALE, $retrievedCurrencyObject->getLocale()); + $this->assertEquals(self::TEST_NONEXISTENT_CURRENCY_LOCALE, $retrievedCurrencyObject->getLocale()); $this->assertEquals('euro', $retrievedCurrencyObject->getName()); $this->assertEquals(['EUR'], $retrievedCurrencyObject->getCurrencyList()); } diff --git a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php index a204a733dc848..9c992ecff245c 100644 --- a/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php +++ b/lib/internal/Magento/Framework/Locale/Test/Unit/FormatTest.php @@ -146,6 +146,8 @@ public function provideNumbers(): array ['2,054.00', 2054], ['4,000', 4000.0, 'ja_JP'], ['4,000', 4.0, 'en_US'], + ['2٬599٫50', 2599.50, 'ar_EG'], + ['2٬000٬000٫99', 2000000.99, 'ar_SA'], ]; } } diff --git a/lib/internal/Magento/Framework/Lock/Backend/Cache.php b/lib/internal/Magento/Framework/Lock/Backend/Cache.php index 612d8541281b0..ae777a6701cde 100644 --- a/lib/internal/Magento/Framework/Lock/Backend/Cache.php +++ b/lib/internal/Magento/Framework/Lock/Backend/Cache.php @@ -31,6 +31,27 @@ class Cache implements \Magento\Framework\Lock\LockManagerInterface */ private $lockSign; + /** + * How many microseconds to wait before re-try to acquire a lock + * + * @var int + */ + private $sleepCycle = 100000; + + /** + * Lifetime of lock data in seconds. + * + * @var int + */ + private $defaultLifetime = 7200; + + /** + * Array for keeping all lock attempt to release them on destruct. + * + * @var string[] + */ + private $lockArrayState = []; + /** * @param FrontendInterface $cache */ @@ -49,18 +70,21 @@ public function lock(string $name, int $timeout = -1): bool $this->lockSign = $this->generateLockSign(); } - $data = $this->cache->load($this->getIdentifier($name)); - - if (false !== $data) { - return false; + $skipDeadline = $timeout < 0; + $deadline = microtime(true) + $timeout; + while ($this->cache->load($this->getIdentifier($name))) { + if (!$skipDeadline && $deadline <= microtime(true)) { + return false; + } + usleep($this->sleepCycle); } - $timeout = $timeout <= 0 ? null : $timeout; - $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $timeout); + $this->cache->save($this->lockSign, $this->getIdentifier($name), [], $this->defaultLifetime); $data = $this->cache->load($this->getIdentifier($name)); if ($data === $this->lockSign) { + $this->lockArrayState[$name] = 1; return true; } @@ -85,6 +109,7 @@ public function unlock(string $name): bool $removeResult = false; if ($data === $this->lockSign) { $removeResult = (bool)$this->cache->remove($this->getIdentifier($name)); + unset($this->lockArrayState[$name]); } return $removeResult; @@ -131,4 +156,26 @@ private function generateLockSign() return $sign; } + + /** + * Destruct method should release all locks that left. + * + * @return void + */ + public function __destruct() + { + $this->releaseLocks(); + } + + /** + * Release all locks that were not removed with unlock method. + * + * @return void + */ + private function releaseLocks() + { + foreach ($this->lockArrayState as $name => $value) { + $this->unlock($name); + } + } } diff --git a/lib/internal/Magento/Framework/Lock/LockManagerInterface.php b/lib/internal/Magento/Framework/Lock/LockManagerInterface.php index 76cc8506eb182..81f721fac9036 100644 --- a/lib/internal/Magento/Framework/Lock/LockManagerInterface.php +++ b/lib/internal/Magento/Framework/Lock/LockManagerInterface.php @@ -11,6 +11,7 @@ * Interface of a lock manager * * @api + * @since 101.0.5 */ interface LockManagerInterface { @@ -21,6 +22,7 @@ interface LockManagerInterface * @param int $timeout How long to wait lock acquisition in seconds, negative value means infinite timeout * @return bool * @api + * @since 101.0.5 */ public function lock(string $name, int $timeout = -1): bool; @@ -30,6 +32,7 @@ public function lock(string $name, int $timeout = -1): bool; * @param string $name lock name * @return bool * @api + * @since 101.0.5 */ public function unlock(string $name): bool; @@ -39,6 +42,7 @@ public function unlock(string $name): bool; * @param string $name lock name * @return bool * @api + * @since 101.0.5 */ public function isLocked(string $name): bool; } diff --git a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php index 5b5c87ce454b3..3e46d4fe6fc76 100644 --- a/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php +++ b/lib/internal/Magento/Framework/Lock/Test/Unit/Backend/CacheTest.php @@ -162,6 +162,6 @@ public function testLockWithNotEmptyData(): void ->with(self::LOCK_PREFIX . $identifier) ->willReturn(\uniqid('some_rand-', true)); - $this->assertEquals(false, $this->cache->lock($identifier)); + $this->assertEquals(false, $this->cache->lock($identifier, 0)); } } diff --git a/lib/internal/Magento/Framework/Mail/MailMessageInterface.php b/lib/internal/Magento/Framework/Mail/MailMessageInterface.php index 5179e6057c4c9..e5f7d355afa6c 100644 --- a/lib/internal/Magento/Framework/Mail/MailMessageInterface.php +++ b/lib/internal/Magento/Framework/Mail/MailMessageInterface.php @@ -9,8 +9,9 @@ * Mail Message interface * * @api - * @deprecated + * @deprecated 102.0.4 * @see \Magento\Framework\Mail\EmailMessageInterface + * @since 101.0.8 */ interface MailMessageInterface extends MessageInterface { @@ -19,6 +20,7 @@ interface MailMessageInterface extends MessageInterface * * @param string $html * @return $this + * @since 101.0.8 */ public function setBodyHtml($html); @@ -27,6 +29,7 @@ public function setBodyHtml($html); * * @param string $text * @return $this + * @since 101.0.8 */ public function setBodyText($text); @@ -34,6 +37,7 @@ public function setBodyText($text); * Get message source code. * * @return string + * @since 101.0.8 */ public function getRawMessage(); } diff --git a/lib/internal/Magento/Framework/Mail/Message.php b/lib/internal/Magento/Framework/Mail/Message.php index b140676466e5f..66ed5927b6833 100644 --- a/lib/internal/Magento/Framework/Mail/Message.php +++ b/lib/internal/Magento/Framework/Mail/Message.php @@ -11,7 +11,7 @@ /** * Class Message for email transportation * - * @deprecated a new message implementation was added + * @deprecated 102.0.4 a new message implementation was added * @see \Magento\Framework\Mail\EmailMessage */ class Message implements MailMessageInterface @@ -42,7 +42,7 @@ public function __construct($charset = 'utf-8') /** * @inheritdoc * - * @deprecated + * @deprecated 101.0.8 * @see \Magento\Framework\Mail\Message::setBodyText * @see \Magento\Framework\Mail\Message::setBodyHtml */ @@ -55,7 +55,7 @@ public function setMessageType($type) /** * @inheritdoc * - * @deprecated + * @deprecated 101.0.8 * @see \Magento\Framework\Mail\Message::setBodyText * @see \Magento\Framework\Mail\Message::setBodyHtml */ @@ -96,7 +96,7 @@ public function getBody() /** * @inheritdoc * - * @deprecated This function is missing the from name. The + * @deprecated 102.0.1 This function is missing the from name. The * setFromAddress() function sets both from address and from name. * @see setFromAddress() */ diff --git a/lib/internal/Magento/Framework/Mail/MessageInterface.php b/lib/internal/Magento/Framework/Mail/MessageInterface.php index c0d3afed81e39..904c723e8bf08 100644 --- a/lib/internal/Magento/Framework/Mail/MessageInterface.php +++ b/lib/internal/Magento/Framework/Mail/MessageInterface.php @@ -9,8 +9,9 @@ * Mail Message interface * * @api - * @deprecated in favor of MailMessageInterface to avoid temporal coupling (setMessageType + setBody) + * @deprecated 102.0.0 in favor of MailMessageInterface to avoid temporal coupling (setMessageType + setBody) * @see \Magento\Framework\Mail\MailMessageInterface + * @since 100.0.2 */ interface MessageInterface { @@ -46,7 +47,7 @@ public function getSubject(); * @param mixed $body * @return $this * - * @deprecated + * @deprecated 102.0.0 * @see \Magento\Framework\Mail\MailMessageInterface::setBodyHtml * @see \Magento\Framework\Mail\MailMessageInterface::setBodyText() */ @@ -105,7 +106,7 @@ public function setReplyTo($replyToAddress); * @param string $type * @return $this * - * @deprecated + * @deprecated 102.0.0 * @see \Magento\Framework\Mail\MailMessageInterface::setBodyHtml * @see \Magento\Framework\Mail\MailMessageInterface::getBodyHtml * @see \Magento\Framework\Mail\MailMessageInterface::setBodyText() diff --git a/lib/internal/Magento/Framework/Mail/Template/ConfigInterface.php b/lib/internal/Magento/Framework/Mail/Template/ConfigInterface.php index b4e6cb46045fe..bac90dcbae5e0 100644 --- a/lib/internal/Magento/Framework/Mail/Template/ConfigInterface.php +++ b/lib/internal/Magento/Framework/Mail/Template/ConfigInterface.php @@ -9,6 +9,7 @@ * High-level interface for mail templates data that hides format from the client code * * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/Mail/Template/FactoryInterface.php b/lib/internal/Magento/Framework/Mail/Template/FactoryInterface.php index 4e88a9897e4a2..2c0f99e4ac1f1 100644 --- a/lib/internal/Magento/Framework/Mail/Template/FactoryInterface.php +++ b/lib/internal/Magento/Framework/Mail/Template/FactoryInterface.php @@ -9,6 +9,7 @@ * Mail Template Factory interface * * @api + * @since 100.0.2 */ interface FactoryInterface { diff --git a/lib/internal/Magento/Framework/Mail/Template/SenderResolverInterface.php b/lib/internal/Magento/Framework/Mail/Template/SenderResolverInterface.php index 89fcb7478bdf5..f283fe54819ac 100644 --- a/lib/internal/Magento/Framework/Mail/Template/SenderResolverInterface.php +++ b/lib/internal/Magento/Framework/Mail/Template/SenderResolverInterface.php @@ -9,6 +9,7 @@ * Mail Sender Resolver interface * * @api + * @since 100.0.2 */ interface SenderResolverInterface { diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php index 234dccf8c9046..08560e1464fa3 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilder.php @@ -31,6 +31,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class TransportBuilder { @@ -242,7 +243,7 @@ public function setReplyTo($email, $name = null) * @throws InvalidArgumentException * @see setFromByScope() * - * @deprecated This function sets the from address but does not provide + * @deprecated 102.0.1 This function sets the from address but does not provide * a way of setting the correct from addresses based on the scope. */ public function setFrom($from) @@ -259,6 +260,7 @@ public function setFrom($from) * @return $this * @throws InvalidArgumentException * @throws MailException + * @since 102.0.1 */ public function setFromByScope($from, $scopeId = null) { diff --git a/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php index 85b1b181d4f9e..416fbb1ecb1dd 100644 --- a/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php +++ b/lib/internal/Magento/Framework/Mail/Template/TransportBuilderByStore.php @@ -11,7 +11,7 @@ /** * Class TransportBuilderByStore * - * @deprecated The ability to set From address based on store is now available + * @deprecated 102.0.1 The ability to set From address based on store is now available * in the \Magento\Framework\Mail\Template\TransportBuilder class * @see \Magento\Framework\Mail\Template\TransportBuilder::setFromByStore */ diff --git a/lib/internal/Magento/Framework/Mail/TemplateInterface.php b/lib/internal/Magento/Framework/Mail/TemplateInterface.php index ad13772876ea1..b1062aec92bee 100644 --- a/lib/internal/Magento/Framework/Mail/TemplateInterface.php +++ b/lib/internal/Magento/Framework/Mail/TemplateInterface.php @@ -9,6 +9,7 @@ * Mail Template interface * * @api + * @since 100.0.2 */ interface TemplateInterface extends \Magento\Framework\App\TemplateTypesInterface { diff --git a/lib/internal/Magento/Framework/Mail/TransportInterface.php b/lib/internal/Magento/Framework/Mail/TransportInterface.php index cccfad885838e..dd65439243ba3 100644 --- a/lib/internal/Magento/Framework/Mail/TransportInterface.php +++ b/lib/internal/Magento/Framework/Mail/TransportInterface.php @@ -9,6 +9,7 @@ * Mail Transport interface * * @api + * @since 100.0.2 */ interface TransportInterface { @@ -24,7 +25,7 @@ public function sendMessage(); * Get message * * @return \Magento\Framework\Mail\MessageInterface - * @since 100.2.0 + * @since 101.0.0 */ public function getMessage(); } diff --git a/lib/internal/Magento/Framework/Math/Calculator.php b/lib/internal/Magento/Framework/Math/Calculator.php index c09f90d4be4da..a8971f964a668 100644 --- a/lib/internal/Magento/Framework/Math/Calculator.php +++ b/lib/internal/Magento/Framework/Math/Calculator.php @@ -9,6 +9,7 @@ * Calculations Library * * @api + * @since 100.0.2 */ class Calculator { diff --git a/lib/internal/Magento/Framework/Math/Division.php b/lib/internal/Magento/Framework/Math/Division.php index 820a11b80dea9..c69bc4379e77f 100644 --- a/lib/internal/Magento/Framework/Math/Division.php +++ b/lib/internal/Magento/Framework/Math/Division.php @@ -9,6 +9,7 @@ * Division library * * @api + * @since 100.0.2 */ class Division { diff --git a/lib/internal/Magento/Framework/Math/FloatComparator.php b/lib/internal/Magento/Framework/Math/FloatComparator.php index 4053404369956..affec441e89fd 100644 --- a/lib/internal/Magento/Framework/Math/FloatComparator.php +++ b/lib/internal/Magento/Framework/Math/FloatComparator.php @@ -11,6 +11,7 @@ * Contains methods to compare float digits. * * @api + * @since 101.0.6 */ class FloatComparator { @@ -27,6 +28,7 @@ class FloatComparator * @param float $a * @param float $b * @return bool + * @since 101.0.6 */ public function equal(float $a, float $b): bool { @@ -39,6 +41,7 @@ public function equal(float $a, float $b): bool * @param float $a * @param float $b * @return bool + * @since 101.0.6 */ public function greaterThan(float $a, float $b): bool { @@ -51,6 +54,7 @@ public function greaterThan(float $a, float $b): bool * @param float $a * @param float $b * @return bool + * @since 101.0.6 */ public function greaterThanOrEqual(float $a, float $b): bool { diff --git a/lib/internal/Magento/Framework/Math/Random.php b/lib/internal/Magento/Framework/Math/Random.php index c2059e1935a80..8adb602e00479 100644 --- a/lib/internal/Magento/Framework/Math/Random.php +++ b/lib/internal/Magento/Framework/Math/Random.php @@ -12,6 +12,7 @@ * Random data generator * * @api + * @since 100.0.2 */ class Random { diff --git a/lib/internal/Magento/Framework/Message/AbstractMessage.php b/lib/internal/Magento/Framework/Message/AbstractMessage.php index 85789bca9047c..c7f0b283d1dd9 100644 --- a/lib/internal/Magento/Framework/Message/AbstractMessage.php +++ b/lib/internal/Magento/Framework/Message/AbstractMessage.php @@ -9,6 +9,7 @@ * Abstract message model * * @api + * @since 100.0.2 */ abstract class AbstractMessage implements MessageInterface { diff --git a/lib/internal/Magento/Framework/Message/Collection.php b/lib/internal/Magento/Framework/Message/Collection.php index 32e84fc28f5a0..dfcb06dd6f291 100644 --- a/lib/internal/Magento/Framework/Message/Collection.php +++ b/lib/internal/Magento/Framework/Message/Collection.php @@ -9,6 +9,7 @@ * Messages collection * * @api + * @since 100.0.2 */ class Collection { diff --git a/lib/internal/Magento/Framework/Message/ManagerInterface.php b/lib/internal/Magento/Framework/Message/ManagerInterface.php index 360444fa897ac..29063a4deecbd 100644 --- a/lib/internal/Magento/Framework/Message/ManagerInterface.php +++ b/lib/internal/Magento/Framework/Message/ManagerInterface.php @@ -9,6 +9,7 @@ * Adds different types of messages to the session, and allows access to existing messages. * * @api + * @since 100.0.2 */ interface ManagerInterface { diff --git a/lib/internal/Magento/Framework/Message/MessageInterface.php b/lib/internal/Magento/Framework/Message/MessageInterface.php index 2dad284152990..5a52b9b2fe649 100644 --- a/lib/internal/Magento/Framework/Message/MessageInterface.php +++ b/lib/internal/Magento/Framework/Message/MessageInterface.php @@ -9,6 +9,7 @@ * Represent a message with a type, content text, and an isSticky attribute to prevent message from being cleared. * * @api + * @since 100.0.2 */ interface MessageInterface { diff --git a/lib/internal/Magento/Framework/Message/PhraseFactory.php b/lib/internal/Magento/Framework/Message/PhraseFactory.php index 2cfa324f40dbe..7efc83a811549 100644 --- a/lib/internal/Magento/Framework/Message/PhraseFactory.php +++ b/lib/internal/Magento/Framework/Message/PhraseFactory.php @@ -9,7 +9,7 @@ /** * Factory to combine several messages into one - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class PhraseFactory { diff --git a/lib/internal/Magento/Framework/MessageQueue/BatchConsumer.php b/lib/internal/Magento/Framework/MessageQueue/BatchConsumer.php index 3dac6070e669f..e838744100a2d 100644 --- a/lib/internal/Magento/Framework/MessageQueue/BatchConsumer.php +++ b/lib/internal/Magento/Framework/MessageQueue/BatchConsumer.php @@ -275,7 +275,7 @@ private function lockMessages(array $messages) * * @return ConsumerConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getConsumerConfig() { @@ -292,7 +292,7 @@ private function getConsumerConfig() * * @return MessageController * - * @deprecated 100.1.0 + * @deprecated 103.0.0 */ private function getMessageController() { diff --git a/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactory.php b/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactory.php index 47775ac857b07..d55b893f31ca7 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactory.php +++ b/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactory.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\MessageQueue\ExchangeInterface * * @api - * @since 100.2.0 + * @since 103.0.0 */ class ExchangeFactory implements ExchangeFactoryInterface { @@ -27,7 +27,7 @@ class ExchangeFactory implements ExchangeFactoryInterface * Object Manager instance * * @var \Magento\Framework\ObjectManagerInterface - * @since 100.2.0 + * @since 103.0.0 */ protected $objectManager = null; @@ -37,7 +37,6 @@ class ExchangeFactory implements ExchangeFactoryInterface * @param \Magento\Framework\MessageQueue\ConnectionTypeResolver $connectionTypeResolver * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param ExchangeFactoryInterface[] $exchangeFactories - * @since 100.2.0 */ public function __construct( \Magento\Framework\MessageQueue\ConnectionTypeResolver $connectionTypeResolver, @@ -51,7 +50,7 @@ public function __construct( /** * @inheritdoc - * @since 100.2.0 + * @since 103.0.0 */ public function create($connectionName, array $data = []) { diff --git a/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactoryInterface.php b/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactoryInterface.php index dc691e632c9ed..d407be435332d 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactoryInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeFactoryInterface.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\MessageQueue\Bulk\ExchangeInterface * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface ExchangeFactoryInterface { @@ -21,7 +21,7 @@ interface ExchangeFactoryInterface * @return ExchangeInterface * @throws \LogicException If exchange is not defined for the specified connection type * or it doesn't implement ExchangeInterface - * @since 100.2.0 + * @since 103.0.0 */ public function create($connectionName, array $data = []); } diff --git a/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeInterface.php b/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeInterface.php index 08b64689c2940..a361d893d8d68 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/Bulk/ExchangeInterface.php @@ -9,7 +9,7 @@ * Interface for bulk exchange. * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface ExchangeInterface { @@ -19,7 +19,7 @@ interface ExchangeInterface * @param string $topic * @param \Magento\Framework\MessageQueue\EnvelopeInterface[] $envelopes * @return mixed - * @since 100.2.0 + * @since 103.0.0 */ public function enqueue($topic, array $envelopes); } diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php index 559959b55fc61..ce9bb69859bf9 100644 --- a/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvoker.php @@ -56,16 +56,31 @@ public function __construct( * @param QueueInterface $queue * @param int $maxNumberOfMessages * @param \Closure $callback + * @param int|null $maxIdleTime + * @param int|null $sleep * @return void + * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ - public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback) - { + public function invoke( + QueueInterface $queue, + $maxNumberOfMessages, + $callback, + $maxIdleTime = null, + $sleep = null + ) { $this->poisonPillVersion = $this->poisonPillRead->getLatestVersion(); + $sleep = (int) $sleep ?: 1; + $maxIdleTime = $maxIdleTime ? (int) $maxIdleTime : PHP_INT_MAX; for ($i = $maxNumberOfMessages; $i > 0; $i--) { + $idleStartTime = microtime(true); do { $message = $queue->dequeue(); + if (!$message && microtime(true) - $idleStartTime > $maxIdleTime) { + break 2; + } // phpcs:ignore Magento2.Functions.DiscouragedFunction - } while ($message === null && $this->isWaitingNextMessage() && (sleep(1) === 0)); + } while ($message === null && $this->isWaitingNextMessage() && (sleep($sleep) === 0)); if ($message === null) { break; diff --git a/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php index 36658f2e4eebe..63d4b4003c74f 100644 --- a/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/CallbackInvokerInterface.php @@ -8,7 +8,7 @@ namespace Magento\Framework\MessageQueue; /** - * Callback invoker interface + * Callback invoker interface. Invoke callbacks for consumer classes. */ interface CallbackInvokerInterface { @@ -18,7 +18,15 @@ interface CallbackInvokerInterface * @param QueueInterface $queue * @param int $maxNumberOfMessages * @param \Closure $callback + * @param int|null $maxIdleTime + * @param int|null $sleep * @return void */ - public function invoke(QueueInterface $queue, $maxNumberOfMessages, $callback); + public function invoke( + QueueInterface $queue, + $maxNumberOfMessages, + $callback, + $maxIdleTime = null, + $sleep = null + ); } diff --git a/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/Communication.php b/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/Communication.php index 6219dbf29b14c..ec8344ac69820 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/Communication.php +++ b/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/Communication.php @@ -96,7 +96,7 @@ public function read($scope = null) * @param string $methodName * @return string * - * @deprecated 100.2.0 + * @deprecated 103.0.0 * @see \Magento\Framework\Communication\Config\ReflectionGenerator::generateTopicName */ public function generateTopicName($typeName, $methodName) diff --git a/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/MessageQueue.php b/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/MessageQueue.php index 9412e1c2384f0..da361f08ee598 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/MessageQueue.php +++ b/lib/internal/Magento/Framework/MessageQueue/Code/Generator/Config/RemoteServiceReader/MessageQueue.php @@ -11,7 +11,7 @@ /** * Remote service configuration reader. * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class MessageQueue implements \Magento\Framework\Config\ReaderInterface { diff --git a/lib/internal/Magento/Framework/MessageQueue/Code/Generator/RemoteServiceGenerator.php b/lib/internal/Magento/Framework/MessageQueue/Code/Generator/RemoteServiceGenerator.php index 9dc0a9d0205f8..b68a242b50722 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Code/Generator/RemoteServiceGenerator.php +++ b/lib/internal/Magento/Framework/MessageQueue/Code/Generator/RemoteServiceGenerator.php @@ -229,7 +229,7 @@ protected function validateResultClassName() * * @return ReflectionGenerator * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getReflectionGenerator() { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config.php b/lib/internal/Magento/Framework/MessageQueue/Config.php index 9a925e1417c12..9260a4afe951e 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config.php @@ -12,7 +12,7 @@ /** * Queue configuration. * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class Config implements ConfigInterface { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Consumer/ConfigReaderPlugin.php b/lib/internal/Magento/Framework/MessageQueue/Config/Consumer/ConfigReaderPlugin.php index c791baf4deb66..21f3435e23526 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Consumer/ConfigReaderPlugin.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Consumer/ConfigReaderPlugin.php @@ -11,7 +11,7 @@ /** * Plugin which provides access to consumers declared in queue config using consumer config interface. * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class ConfigReaderPlugin { @@ -68,7 +68,10 @@ private function getConsumerConfigDataFromQueueConfig() 'consumerInstance' => $consumerData['instance_type'], 'handlers' => $handlers, 'connection' => $consumerData['connection'], - 'maxMessages' => $consumerData['max_messages'] + 'maxMessages' => $consumerData['max_messages'], + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => null ]; } diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Publisher/ConfigReaderPlugin.php b/lib/internal/Magento/Framework/MessageQueue/Config/Publisher/ConfigReaderPlugin.php index 78b82a67069bf..c1a4c9c6836e8 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Publisher/ConfigReaderPlugin.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Publisher/ConfigReaderPlugin.php @@ -11,7 +11,7 @@ /** * Plugin which provides access to publishers declared in queue config using publisher config interface. * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class ConfigReaderPlugin { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env.php b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env.php index 52e97e80e9dff..88df5546b28ff 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env.php @@ -54,8 +54,11 @@ public function read($scope = null) { $configData = $this->deploymentConfig->getConfigData(self::ENV_QUEUE) ?: []; if (isset($configData['config'])) { - $configData = $this->publisherConverter->convert($configData = $configData['config']); + $convertedConfigData = $this->publisherConverter->convert($configData['config']); + unset($configData['config']); + $configData = array_replace_recursive($configData, $convertedConfigData); } + return $configData; } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env/Validator.php b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env/Validator.php index 43f788343fa88..5ea8d4e080a8f 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env/Validator.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Env/Validator.php @@ -40,17 +40,14 @@ public function __construct( * @param array $configData * @param array|null $xmlConfigData * @return void + * @throws \LogicException */ public function validate($configData, array $xmlConfigData = []) { if (isset($configData[QueueConfig::TOPICS])) { foreach ($configData[QueueConfig::TOPICS] as $topicName => $configDataItem) { - $schemaType = $configDataItem[QueueConfig::TOPIC_SCHEMA][QueueConfig::TOPIC_SCHEMA_VALUE]; - $responseSchemaType = - $configDataItem[QueueConfig::TOPIC_RESPONSE_SCHEMA][QueueConfig::TOPIC_SCHEMA_VALUE]; + $this->validateTopic($topicName, $configDataItem); $publisherName = $configDataItem[QueueConfig::TOPIC_PUBLISHER]; - $this->validateSchemaType($schemaType, $topicName); - $this->validateResponseSchemaType($responseSchemaType, $topicName); $this->validateTopicPublisher( $this->getAvailablePublishers($configData, $xmlConfigData), $publisherName, @@ -81,6 +78,28 @@ public function validate($configData, array $xmlConfigData = []) } } + /** + * Validate topic config data + * + * @param string $topicName + * @param array $configDataItem + * @return void + * @throws \LogicException + */ + private function validateTopic(string $topicName, array $configDataItem): void + { + if (isset($configDataItem[QueueConfig::TOPIC_SCHEMA])) { + $schemaType = $configDataItem[QueueConfig::TOPIC_SCHEMA][QueueConfig::TOPIC_SCHEMA_VALUE]; + $this->validateSchemaType($schemaType, $topicName); + } + if (isset($configDataItem[QueueConfig::TOPIC_RESPONSE_SCHEMA])) { + $responseSchemaType = $configDataItem[QueueConfig::TOPIC_RESPONSE_SCHEMA][QueueConfig::TOPIC_SCHEMA_VALUE]; + if (null !== $responseSchemaType) { + $this->validateResponseSchemaType($responseSchemaType, $topicName); + } + } + } + /** * Return all available publishers from xml and env configs * @@ -88,7 +107,7 @@ public function validate($configData, array $xmlConfigData = []) * @param array $xmlConfigData * @return array */ - private function getAvailablePublishers($configData, $xmlConfigData) + private function getAvailablePublishers(array $configData, array $xmlConfigData): array { $envConfigPublishers = isset($configData[QueueConfig::PUBLISHERS]) ? $configData[QueueConfig::PUBLISHERS] : []; $xmlConfigPublishers = isset($xmlConfigData[QueueConfig::PUBLISHERS]) @@ -108,7 +127,7 @@ private function getAvailablePublishers($configData, $xmlConfigData) * @param array $xmlConfigData * @return array */ - private function getAvailableTopics($configData, $xmlConfigData) + private function getAvailableTopics(array $configData, array $xmlConfigData): array { $envConfigTopics = isset($configData[QueueConfig::TOPICS]) ? $configData[QueueConfig::TOPICS] : []; $xmlConfigTopics = isset($xmlConfigData[QueueConfig::TOPICS]) ? $xmlConfigData[QueueConfig::TOPICS] : []; diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml.php b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml.php index 16efacf628d57..e09c3e411453b 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml.php @@ -9,7 +9,7 @@ /** * MessageQueue configuration filesystem loader. Loads all publisher configuration from XML file * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class Xml extends \Magento\Framework\Config\Reader\Filesystem { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php index 0184f720b3b4e..cffc4816a12d9 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/CompositeConverter.php @@ -11,7 +11,7 @@ /** * Converts MessageQueue config from \DOMDocument to array * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class CompositeConverter implements ConverterInterface { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/Converter/TopicConfig.php b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/Converter/TopicConfig.php index e33d6ab5b1fb8..8b0d31f5abcc3 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/Converter/TopicConfig.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/Converter/TopicConfig.php @@ -15,7 +15,7 @@ /** * Converts MessageQueue config from \DOMDocument to array * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class TopicConfig implements \Magento\Framework\Config\ConverterInterface { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/SchemaLocator.php b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/SchemaLocator.php index 613d564942e2f..37b4b34aec350 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/SchemaLocator.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Reader/Xml/SchemaLocator.php @@ -9,7 +9,7 @@ /** * Schema locator for Publishers * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class SchemaLocator implements \Magento\Framework\Config\SchemaLocatorInterface { diff --git a/lib/internal/Magento/Framework/MessageQueue/Config/Topology/ConfigReaderPlugin.php b/lib/internal/Magento/Framework/MessageQueue/Config/Topology/ConfigReaderPlugin.php index b5f8d179961c6..4c0d54c518bf4 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Config/Topology/ConfigReaderPlugin.php +++ b/lib/internal/Magento/Framework/MessageQueue/Config/Topology/ConfigReaderPlugin.php @@ -10,7 +10,7 @@ /** * Plugin which provides access to topology declared in queue config using topology config interface. * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class ConfigReaderPlugin { @@ -47,6 +47,7 @@ public function afterRead( $topologyConfigDataFromQueueConfig = $this->getTopologyConfigDataFromQueueConfig(); foreach ($topologyConfigDataFromQueueConfig as $exchangeKey => $exchangeConfig) { if (isset($result[$exchangeKey])) { + // phpcs:ignore Magento2.Performance.ForeachArrayMerge $result[$exchangeKey]['bindings'] = array_merge( $exchangeConfig['bindings'], $result[$exchangeKey]['bindings'] @@ -80,7 +81,7 @@ private function getTopologyConfigDataFromQueueConfig() 'arguments' => [] ]; - $exchangeName = $queueConfigBinding['exchange']; + $exchangeName = $this->queueConfig->getExchangeByTopic($topic); $connection = $this->queueConfig->getConnectionByTopic($topic); if (isset($result[$exchangeName . '--' . $connection])) { $result[$exchangeName . '--' . $connection]['bindings'][$bindingId] = $bindingData; diff --git a/lib/internal/Magento/Framework/MessageQueue/ConfigInterface.php b/lib/internal/Magento/Framework/MessageQueue/ConfigInterface.php index a88b5dedbd269..99259dcac6b4c 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ConfigInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/ConfigInterface.php @@ -9,7 +9,7 @@ use Magento\Framework\Exception\LocalizedException; /** - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer.php b/lib/internal/Magento/Framework/MessageQueue/Consumer.php index 8f65a2d8c5ed2..34ee797fca93b 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer.php @@ -108,11 +108,18 @@ public function __construct( public function process($maxNumberOfMessages = null) { $queue = $this->configuration->getQueue(); - + $maxIdleTime = $this->configuration->getMaxIdleTime(); + $sleep = $this->configuration->getSleep(); if (!isset($maxNumberOfMessages)) { $queue->subscribe($this->getTransactionCallback($queue)); } else { - $this->invoker->invoke($queue, $maxNumberOfMessages, $this->getTransactionCallback($queue)); + $this->invoker->invoke( + $queue, + $maxNumberOfMessages, + $this->getTransactionCallback($queue), + $maxIdleTime, + $sleep + ); } } @@ -240,7 +247,7 @@ private function getTransactionCallback(QueueInterface $queue) * * @return ConsumerConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getConsumerConfig() { @@ -255,7 +262,7 @@ private function getConsumerConfig() * * @return CommunicationConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getCommunicationConfig() { @@ -271,7 +278,7 @@ private function getCommunicationConfig() * * @return QueueRepository * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getQueueRepository() { @@ -286,7 +293,7 @@ private function getQueueRepository() * * @return MessageController * - * @deprecated 100.1.0 + * @deprecated 103.0.0 */ private function getMessageController() { @@ -302,7 +309,7 @@ private function getMessageController() * * @return MessageValidator * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getMessageValidator() { @@ -318,7 +325,7 @@ private function getMessageValidator() * * @return EnvelopeFactory * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getEnvelopeFactory() { diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItem.php b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItem.php index 92242698a5ba5..4103e9cc42777 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItem.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItem.php @@ -43,6 +43,21 @@ class ConsumerConfigItem implements ConsumerConfigItemInterface */ private $maxMessages; + /** + * @var int|null + */ + private $maxIdleTime; + + /** + * @var int|null + */ + private $sleep; + + /** + * @var boolean|null + */ + private $onlySpawnWhenMessageAvailable; + /** * Initialize dependencies. * @@ -54,7 +69,7 @@ public function __construct(HandlerIteratorFactory $handlerIteratorFactory) } /** - * {@inheritdoc} + * @inheritdoc */ public function getName() { @@ -62,7 +77,7 @@ public function getName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getConnection() { @@ -70,7 +85,7 @@ public function getConnection() } /** - * {@inheritdoc} + * @inheritdoc */ public function getQueue() { @@ -78,7 +93,7 @@ public function getQueue() } /** - * {@inheritdoc} + * @inheritdoc */ public function getConsumerInstance() { @@ -86,7 +101,7 @@ public function getConsumerInstance() } /** - * {@inheritdoc} + * @inheritdoc */ public function getHandlers() { @@ -94,7 +109,7 @@ public function getHandlers() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMaxMessages() { @@ -102,7 +117,33 @@ public function getMaxMessages() } /** - * {@inheritdoc} + * @inheritdoc + */ + public function getMaxIdleTime() + { + return $this->maxIdleTime; + } + + /** + * @inheritdoc + */ + public function getSleep() + { + return $this->sleep; + } + + /** + * @inheritdoc + */ + public function getOnlySpawnWhenMessageAvailable() + { + return $this->onlySpawnWhenMessageAvailable; + } + + /** + * Populate current instance properties with data + * + * @param array $data consumer configuration data */ public function setData(array $data) { @@ -112,5 +153,8 @@ public function setData(array $data) $this->consumerInstance = $data['consumerInstance']; $this->maxMessages = $data['maxMessages']; $this->handlers->setData($data['handlers']); + $this->maxIdleTime = $data['maxIdleTime']; + $this->sleep = $data['sleep']; + $this->onlySpawnWhenMessageAvailable = $data['onlySpawnWhenMessageAvailable']; } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItemInterface.php b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItemInterface.php index 0896b7789e1f1..4eeceef5b3cc1 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItemInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/ConsumerConfigItemInterface.php @@ -53,4 +53,25 @@ public function getHandlers(); * @return int */ public function getMaxMessages(); + + /** + * Get maximal time (in seconds) for waiting new messages from queue before terminating consumer. + * + * @return int|null + */ + public function getMaxIdleTime(); + + /** + * Get time to sleep (in seconds) before checking if a new message is available in the queue. + * + * @return int|null + */ + public function getSleep(); + + /** + * Get is consumer have to be spawned only if there are messages in the queue. + * + * @return boolean|null + */ + public function getOnlySpawnWhenMessageAvailable(); } diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/FieldsTypes.php b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/FieldsTypes.php index d1409dec026de..d033fa5ecebf8 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/FieldsTypes.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/FieldsTypes.php @@ -13,7 +13,7 @@ class FieldsTypes implements ValidatorInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function validate($configData) { @@ -54,14 +54,31 @@ private function validateConsumerFieldsTypes($consumerName, $consumerConfig) ); } } - if (null !== $consumerConfig['maxMessages'] && !is_numeric($consumerConfig['maxMessages'])) { + $additionalNumericFields = ['maxMessages', 'maxIdleTime', 'sleep']; + foreach ($additionalNumericFields as $fieldName) { + if (null !== $consumerConfig[$fieldName] && !is_numeric($consumerConfig[$fieldName])) { + throw new \LogicException( + sprintf( + "Type of '%s' field specified in configuration of '%s' consumer is invalid. " + . "Given '%s', '%s' was expected.", + $fieldName, + $consumerName, + gettype($consumerConfig[$fieldName]), + 'int|null' + ) + ); + } + } + if (null !== $consumerConfig['onlySpawnWhenMessageAvailable'] + && !is_bool($consumerConfig['onlySpawnWhenMessageAvailable']) + ) { throw new \LogicException( sprintf( - "Type of 'maxMessages' field specified in configuration of '%s' consumer is invalid. " - . "Given '%s', '%s' was expected.", + "Type of 'onlySpawnWhenMessageAvailable' field specified in configuration of '%s' " + . "consumer is invalid. Given '%s', '%s' was expected.", $consumerName, - gettype($consumerConfig['maxMessages']), - 'int|null' + gettype($consumerConfig['onlySpawnWhenMessageAvailable']), + 'boolean|null' ) ); } diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/RequiredFields.php b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/RequiredFields.php index 87de2233381e2..17118a43a1912 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/RequiredFields.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Validator/RequiredFields.php @@ -13,12 +13,22 @@ class RequiredFields implements ValidatorInterface { /** - * {@inheritdoc} + * @inheritdoc */ public function validate($configData) { foreach ($configData as $consumerName => $consumerConfig) { - $requiredFields = ['name', 'queue', 'handlers', 'consumerInstance', 'connection', 'maxMessages']; + $requiredFields = [ + 'name', + 'queue', + 'handlers', + 'consumerInstance', + 'connection', + 'maxMessages', + 'maxIdleTime', + 'sleep', + 'onlySpawnWhenMessageAvailable' + ]; foreach ($requiredFields as $fieldName) { if (!array_key_exists($fieldName, $consumerConfig)) { throw new \LogicException( diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Xml/Converter.php b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Xml/Converter.php index 352bc53e94e90..e346fe07d894b 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Xml/Converter.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer/Config/Xml/Converter.php @@ -45,7 +45,7 @@ public function __construct(ConfigParser $configParser, DefaultValueProvider $de } /** - * {@inheritDoc} + * @inheritdoc */ public function convert($source) { @@ -54,6 +54,11 @@ public function convert($source) foreach ($source->getElementsByTagName('consumer') as $consumerNode) { $consumerName = $this->getAttributeValue($consumerNode, 'name'); $handler = $this->getAttributeValue($consumerNode, 'handler'); + $onlySpawnWhenMessageAvailable = $this->getAttributeValue( + $consumerNode, + 'onlySpawnWhenMessageAvailable' + ); + $result[$consumerName] = [ 'name' => $consumerName, 'queue' => $this->getAttributeValue($consumerNode, 'queue'), @@ -68,7 +73,11 @@ public function convert($source) 'connection', $this->defaultValueProvider->getConnection() ), - 'maxMessages' => $this->getAttributeValue($consumerNode, 'maxMessages') + 'maxMessages' => $this->getAttributeValue($consumerNode, 'maxMessages'), + 'maxIdleTime' => $this->getAttributeValue($consumerNode, 'maxIdleTime'), + 'sleep' => $this->getAttributeValue($consumerNode, 'sleep'), + 'onlySpawnWhenMessageAvailable' => + $onlySpawnWhenMessageAvailable === null ? null : boolval($onlySpawnWhenMessageAvailable) ]; } return $result; diff --git a/lib/internal/Magento/Framework/MessageQueue/Consumer/ConfigInterface.php b/lib/internal/Magento/Framework/MessageQueue/Consumer/ConfigInterface.php index 344b5ef1d580c..f0e2be9e37aa3 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Consumer/ConfigInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/Consumer/ConfigInterface.php @@ -12,7 +12,7 @@ * Consumer config interface provides access data declared in etc/queue_consumer.xml * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface ConfigInterface { @@ -23,7 +23,7 @@ interface ConfigInterface * @return ConsumerConfigItemInterface * @throws LocalizedException * @throws \LogicException - * @since 100.2.0 + * @since 103.0.0 */ public function getConsumer($name); @@ -32,7 +32,7 @@ public function getConsumer($name); * * @return ConsumerConfigItemInterface[] * @throws \LogicException - * @since 100.2.0 + * @since 103.0.0 */ public function getConsumers(); } diff --git a/lib/internal/Magento/Framework/MessageQueue/ConsumerConfiguration.php b/lib/internal/Magento/Framework/MessageQueue/ConsumerConfiguration.php index 09cd5dcb8d909..cdf36fd037120 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ConsumerConfiguration.php +++ b/lib/internal/Magento/Framework/MessageQueue/ConsumerConfiguration.php @@ -15,13 +15,13 @@ class ConsumerConfiguration implements ConsumerConfigurationInterface { /** - * @deprecated + * @deprecated Should be used constant from ConsumerConfigurationInterface * @see ConsumerConfigurationInterface::TOPIC_TYPE */ const CONSUMER_TYPE = "consumer_type"; /** - * @deprecated + * @deprecated Should be used constant from ConsumerConfigurationInterface * @see ConsumerConfigurationInterface::TOPIC_HANDLERS */ const HANDLERS = 'handlers'; @@ -62,7 +62,7 @@ public function __construct(QueueRepository $queueRepository, MessageQueueConfig } /** - * {@inheritdoc} + * @inheritdoc */ public function getConsumerName() { @@ -70,7 +70,7 @@ public function getConsumerName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMaxMessages() { @@ -78,7 +78,7 @@ public function getMaxMessages() } /** - * {@inheritdoc} + * @inheritdoc */ public function getQueueName() { @@ -86,7 +86,7 @@ public function getQueueName() } /** - * {@inheritdoc} + * @inheritdoc */ public function getType() { @@ -108,7 +108,7 @@ public function getType() } /** - * {@inheritdoc} + * @inheritdoc */ public function getHandlers($topicName) { @@ -116,7 +116,7 @@ public function getHandlers($topicName) } /** - * {@inheritdoc} + * @inheritdoc */ public function getTopicNames() { @@ -125,7 +125,31 @@ public function getTopicNames() } /** - * {@inheritdoc} + * @inheritdoc + */ + public function getMaxIdleTime() + { + return $this->getData(self::MAX_IDLE_TIME); + } + + /** + * @inheritdoc + */ + public function getSleep() + { + return $this->getData(self::SLEEP); + } + + /** + * @inheritdoc + */ + public function getOnlySpawnWhenMessageAvailable() + { + return $this->getData(self::ONLY_SPAWN_WHEN_MESSAGE_AVAILABLE); + } + + /** + * @inheritdoc */ public function getQueue() { @@ -134,7 +158,7 @@ public function getQueue() } /** - * {@inheritdoc} + * @inheritdoc */ public function getMessageSchemaType($topicName) { @@ -143,6 +167,7 @@ public function getMessageSchemaType($topicName) /** * Get topic configuration for current consumer. + * * @param string $topicName * @return array * @throws \LogicException @@ -174,7 +199,7 @@ private function getData($key) * * @return ConsumerConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getConsumerConfig() { @@ -189,7 +214,7 @@ private function getConsumerConfig() * * @return CommunicationConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getCommunicationConfig() { diff --git a/lib/internal/Magento/Framework/MessageQueue/ConsumerConfigurationInterface.php b/lib/internal/Magento/Framework/MessageQueue/ConsumerConfigurationInterface.php index b825949ddb019..3e54f03e71603 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ConsumerConfigurationInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/ConsumerConfigurationInterface.php @@ -18,6 +18,9 @@ interface ConsumerConfigurationInterface const TOPICS = 'topics'; const TOPIC_TYPE = 'consumer_type'; const TOPIC_HANDLERS = 'handlers'; + const MAX_IDLE_TIME = 'max_idle_time'; + const SLEEP = 'sleep'; + const ONLY_SPAWN_WHEN_MESSAGE_AVAILABLE = 'only_spawn_when_message_available'; const TYPE_SYNC = 'sync'; const TYPE_ASYNC = 'async'; @@ -42,7 +45,7 @@ public function getQueueName(); * Get consumer type sync|async. * * @return string - * @deprecated 100.2.0 + * @deprecated 103.0.0 * @see \Magento\Framework\Communication\ConfigInterface::getTopic * @throws \LogicException */ @@ -72,13 +75,38 @@ public function getHandlers($topicName); public function getTopicNames(); /** + * Get message schema type. + * * @param string $topicName * @return string */ public function getMessageSchemaType($topicName); /** + * Get message queue instance. + * * @return QueueInterface */ public function getQueue(); + + /** + * Get maximal time (in seconds) for waiting new messages from queue before terminating consumer. + * + * @return int|null + */ + public function getMaxIdleTime(); + + /** + * Get time to sleep (in seconds) before checking if a new message is available in the queue. + * + * @return int|null + */ + public function getSleep(); + + /** + * Get is consumer have to be spawned only if there are messages in the queue. + * + * @return boolean|null + */ + public function getOnlySpawnWhenMessageAvailable(); } diff --git a/lib/internal/Magento/Framework/MessageQueue/ConsumerFactory.php b/lib/internal/Magento/Framework/MessageQueue/ConsumerFactory.php index 7d9f210b4a698..d1a692ee5794e 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ConsumerFactory.php +++ b/lib/internal/Magento/Framework/MessageQueue/ConsumerFactory.php @@ -109,6 +109,10 @@ private function createConsumerConfiguration($consumerConfigItem) ConsumerConfigurationInterface::QUEUE_NAME => $consumerConfigItem->getQueue(), ConsumerConfigurationInterface::TOPICS => $topics, ConsumerConfigurationInterface::MAX_MESSAGES => $consumerConfigItem->getMaxMessages(), + ConsumerConfigurationInterface::MAX_IDLE_TIME => $consumerConfigItem->getMaxIdleTime(), + ConsumerConfigurationInterface::SLEEP => $consumerConfigItem->getSleep(), + ConsumerConfigurationInterface::ONLY_SPAWN_WHEN_MESSAGE_AVAILABLE => + $consumerConfigItem->getOnlySpawnWhenMessageAvailable() ]; return $this->objectManager->create( @@ -122,7 +126,7 @@ private function createConsumerConfiguration($consumerConfigItem) * * @return ConsumerConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getConsumerConfig() { @@ -137,7 +141,7 @@ private function getConsumerConfig() * * @return CommunicationConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getCommunicationConfig() { diff --git a/lib/internal/Magento/Framework/MessageQueue/ConsumerInterface.php b/lib/internal/Magento/Framework/MessageQueue/ConsumerInterface.php index ca45da7eca8ff..bb28a1a5d5ea7 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ConsumerInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/ConsumerInterface.php @@ -9,6 +9,8 @@ * Consumers will connect to a queue, read messages, and invoke a method to process the message contents. * * @api + * @since 103.0.0 + * @since 100.0.2 */ interface ConsumerInterface { @@ -18,6 +20,7 @@ interface ConsumerInterface * @param int|null $maxNumberOfMessages if not specified - process all queued incoming messages and terminate, * otherwise terminate execution after processing the specified number of messages * @return void + * @since 103.0.0 */ public function process($maxNumberOfMessages = null); } diff --git a/lib/internal/Magento/Framework/MessageQueue/EnvelopeInterface.php b/lib/internal/Magento/Framework/MessageQueue/EnvelopeInterface.php index 07f12d1856317..d564636f54a95 100644 --- a/lib/internal/Magento/Framework/MessageQueue/EnvelopeInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/EnvelopeInterface.php @@ -7,6 +7,8 @@ /** * @api + * @since 103.0.0 + * @since 100.0.2 */ interface EnvelopeInterface { @@ -14,6 +16,7 @@ interface EnvelopeInterface * Binary representation of message * * @return string + * @since 103.0.0 */ public function getBody(); @@ -21,6 +24,7 @@ public function getBody(); * Message metadata * * @return array + * @since 103.0.0 */ public function getProperties(); } diff --git a/lib/internal/Magento/Framework/MessageQueue/ExchangeFactory.php b/lib/internal/Magento/Framework/MessageQueue/ExchangeFactory.php index ca1ef769af90f..28e411c6dbbee 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ExchangeFactory.php +++ b/lib/internal/Magento/Framework/MessageQueue/ExchangeFactory.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\MessageQueue\ExchangeInterface * * @api - * @since 100.2.0 + * @since 103.0.0 */ class ExchangeFactory implements ExchangeFactoryInterface { @@ -27,7 +27,7 @@ class ExchangeFactory implements ExchangeFactoryInterface * Object Manager instance * * @var \Magento\Framework\ObjectManagerInterface - * @since 100.2.0 + * @since 103.0.0 */ protected $objectManager = null; @@ -37,7 +37,6 @@ class ExchangeFactory implements ExchangeFactoryInterface * @param ConnectionTypeResolver $connectionTypeResolver * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param ExchangeFactoryInterface[] $exchangeFactories - * @since 100.2.0 */ public function __construct( ConnectionTypeResolver $connectionTypeResolver, @@ -51,7 +50,7 @@ public function __construct( /** * {@inheritdoc} - * @since 100.2.0 + * @since 103.0.0 */ public function create($connectionName, array $data = []) { diff --git a/lib/internal/Magento/Framework/MessageQueue/ExchangeFactoryInterface.php b/lib/internal/Magento/Framework/MessageQueue/ExchangeFactoryInterface.php index 1e70b0266fc4f..4b29c5e25b361 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ExchangeFactoryInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/ExchangeFactoryInterface.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\MessageQueue\ExchangeInterface * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface ExchangeFactoryInterface { @@ -19,7 +19,7 @@ interface ExchangeFactoryInterface * @param string $connectionName * @param array $data * @return ExchangeInterface - * @since 100.2.0 + * @since 103.0.0 */ public function create($connectionName, array $data = []); } diff --git a/lib/internal/Magento/Framework/MessageQueue/ExchangeInterface.php b/lib/internal/Magento/Framework/MessageQueue/ExchangeInterface.php index bb36332ae25b1..4495b210f0613 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ExchangeInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/ExchangeInterface.php @@ -9,6 +9,8 @@ * Interface message Exchange * * @api + * @since 103.0.0 + * @since 100.0.2 */ interface ExchangeInterface { @@ -18,6 +20,7 @@ interface ExchangeInterface * @param string $topic * @param EnvelopeInterface $envelope * @return mixed + * @since 103.0.0 */ public function enqueue($topic, EnvelopeInterface $envelope); } diff --git a/lib/internal/Magento/Framework/MessageQueue/ExchangeRepository.php b/lib/internal/Magento/Framework/MessageQueue/ExchangeRepository.php index 1f26e5d785c1a..cf17c765b49cf 100644 --- a/lib/internal/Magento/Framework/MessageQueue/ExchangeRepository.php +++ b/lib/internal/Magento/Framework/MessageQueue/ExchangeRepository.php @@ -53,7 +53,7 @@ public function getByConnectionName($connectionName) * Get exchange factory. * * @return ExchangeFactoryInterface - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getExchangeFactory() { diff --git a/lib/internal/Magento/Framework/MessageQueue/MessageEncoder.php b/lib/internal/Magento/Framework/MessageQueue/MessageEncoder.php index 91ad24eaeb978..e8f8e6e3f7118 100644 --- a/lib/internal/Magento/Framework/MessageQueue/MessageEncoder.php +++ b/lib/internal/Magento/Framework/MessageQueue/MessageEncoder.php @@ -219,7 +219,7 @@ protected function getConverter($direction) * * @return CommunicationConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getCommunicationConfig() { diff --git a/lib/internal/Magento/Framework/MessageQueue/MessageIdGeneratorInterface.php b/lib/internal/Magento/Framework/MessageQueue/MessageIdGeneratorInterface.php index 727db7d34761a..3d88c111d1b90 100644 --- a/lib/internal/Magento/Framework/MessageQueue/MessageIdGeneratorInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/MessageIdGeneratorInterface.php @@ -9,7 +9,7 @@ * Used to generate unique id for queue message. * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface MessageIdGeneratorInterface { @@ -18,7 +18,7 @@ interface MessageIdGeneratorInterface * * @param string $topicName * @return string - * @since 100.2.0 + * @since 103.0.0 */ public function generate($topicName); } diff --git a/lib/internal/Magento/Framework/MessageQueue/MessageLockException.php b/lib/internal/Magento/Framework/MessageQueue/MessageLockException.php index 4ea462f7e1a8e..ac622aa6cffef 100644 --- a/lib/internal/Magento/Framework/MessageQueue/MessageLockException.php +++ b/lib/internal/Magento/Framework/MessageQueue/MessageLockException.php @@ -11,7 +11,7 @@ * Class MessageLockException to be thrown when a message being processed is already in the lock table. * * @api - * @since 100.1.0 + * @since 103.0.0 */ class MessageLockException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php b/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php index 7c1a947623e9f..45ce351ed97bb 100644 --- a/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php +++ b/lib/internal/Magento/Framework/MessageQueue/MessageValidator.php @@ -191,7 +191,7 @@ private function getRealType($message) * * @return CommunicationConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getCommunicationConfig() { diff --git a/lib/internal/Magento/Framework/MessageQueue/Publisher.php b/lib/internal/Magento/Framework/MessageQueue/Publisher.php index 8fe77abe69136..78d8c0f1bcafa 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Publisher.php +++ b/lib/internal/Magento/Framework/MessageQueue/Publisher.php @@ -109,7 +109,7 @@ private function isAmqpConfigured() * * @return PublisherConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getPublisherConfig() { @@ -124,7 +124,7 @@ private function getPublisherConfig() * * @return AmqpConfig * - * @deprecated + * @deprecated 100.2.0 103.0.0 */ private function getAmqpConfig() { diff --git a/lib/internal/Magento/Framework/MessageQueue/Publisher/ConfigInterface.php b/lib/internal/Magento/Framework/MessageQueue/Publisher/ConfigInterface.php index 7b5586b28a273..a10b7930c6eae 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Publisher/ConfigInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/Publisher/ConfigInterface.php @@ -12,7 +12,7 @@ * Publisher config interface provides access data declared in etc/queue_publisher.xml * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface ConfigInterface { @@ -23,7 +23,7 @@ interface ConfigInterface * @return PublisherConfigItemInterface * @throws LocalizedException * @throws \LogicException - * @since 100.2.0 + * @since 103.0.0 */ public function getPublisher($topic); @@ -32,7 +32,7 @@ public function getPublisher($topic); * * @return PublisherConfigItemInterface[] * @throws \LogicException - * @since 100.2.0 + * @since 103.0.0 */ public function getPublishers(); } diff --git a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php index 1aee963b1bd47..9f33b5b39d2ad 100644 --- a/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/PublisherInterface.php @@ -9,6 +9,8 @@ * Producer to publish messages via a specific transport to a specific queue or exchange. * * @api + * @since 103.0.0 + * @since 100.0.2 */ interface PublisherInterface { @@ -19,6 +21,7 @@ interface PublisherInterface * @param array|object $data * @return null|mixed * @throws \InvalidArgumentException If message is not formed properly + * @since 103.0.0 */ public function publish($topicName, $data); } diff --git a/lib/internal/Magento/Framework/MessageQueue/PublisherPool.php b/lib/internal/Magento/Framework/MessageQueue/PublisherPool.php index 2c1e4e64c7d71..4a289d8f0a320 100644 --- a/lib/internal/Magento/Framework/MessageQueue/PublisherPool.php +++ b/lib/internal/Magento/Framework/MessageQueue/PublisherPool.php @@ -14,7 +14,7 @@ * Publishers pool. * * @api - * @since 100.1.0 + * @since 103.0.0 */ class PublisherPool implements PublisherInterface, BulkPublisherInterface { @@ -35,7 +35,7 @@ class PublisherPool implements PublisherInterface, BulkPublisherInterface * Publisher objects pool. * * @var \Magento\Framework\MessageQueue\PublisherInterface[] - * @since 100.1.0 + * @since 103.0.0 */ protected $publishers = []; @@ -43,7 +43,7 @@ class PublisherPool implements PublisherInterface, BulkPublisherInterface * Communication config. * * @var CommunicationConfig - * @since 100.1.0 + * @since 103.0.0 */ protected $communicationConfig; @@ -65,7 +65,6 @@ class PublisherPool implements PublisherInterface, BulkPublisherInterface * @param string[] $publishers * * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @since 100.1.0 */ public function __construct( CommunicationConfig $communicationConfig, @@ -78,7 +77,7 @@ public function __construct( /** * {@inheritdoc} - * @since 100.1.0 + * @since 103.0.0 */ public function publish($topicName, $data) { @@ -163,7 +162,7 @@ private function getPublisherForConnectionNameAndType($type, $connectionName) * * @return PublisherConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getPublisherConfig() { @@ -178,7 +177,7 @@ private function getPublisherConfig() * * @return ConnectionTypeResolver * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getConnectionTypeResolver() { diff --git a/lib/internal/Magento/Framework/MessageQueue/QueueFactory.php b/lib/internal/Magento/Framework/MessageQueue/QueueFactory.php index eb734df06a59f..3e1fe8f04ba6c 100644 --- a/lib/internal/Magento/Framework/MessageQueue/QueueFactory.php +++ b/lib/internal/Magento/Framework/MessageQueue/QueueFactory.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\MessageQueue\Queuenterface * * @api - * @since 100.2.0 + * @since 103.0.0 */ class QueueFactory implements QueueFactoryInterface { @@ -27,7 +27,7 @@ class QueueFactory implements QueueFactoryInterface * Object Manager instance * * @var \Magento\Framework\ObjectManagerInterface - * @since 100.2.0 + * @since 103.0.0 */ protected $objectManager = null; @@ -37,7 +37,6 @@ class QueueFactory implements QueueFactoryInterface * @param ConnectionTypeResolver $connectionTypeResolver * @param \Magento\Framework\ObjectManagerInterface $objectManager * @param QueueFactoryInterface[] $queueFactories - * @since 100.2.0 */ public function __construct( ConnectionTypeResolver $connectionTypeResolver, @@ -51,7 +50,7 @@ public function __construct( /** * {@inheritdoc} - * @since 100.2.0 + * @since 103.0.0 */ public function create($queueName, $connectionName) { diff --git a/lib/internal/Magento/Framework/MessageQueue/QueueFactoryInterface.php b/lib/internal/Magento/Framework/MessageQueue/QueueFactoryInterface.php index 697db9296b5b0..4ca7bc6ecae87 100644 --- a/lib/internal/Magento/Framework/MessageQueue/QueueFactoryInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/QueueFactoryInterface.php @@ -9,7 +9,7 @@ * Factory class for @see \Magento\Framework\MessageQueue\QueueInterface * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface QueueFactoryInterface { @@ -19,7 +19,7 @@ interface QueueFactoryInterface * @param string $queueName * @param string $connectionName * @return QueueInterface - * @since 100.2.0 + * @since 103.0.0 */ public function create($queueName, $connectionName); } diff --git a/lib/internal/Magento/Framework/MessageQueue/QueueInterface.php b/lib/internal/Magento/Framework/MessageQueue/QueueInterface.php index 1b3814b8e857a..db64e83047c03 100644 --- a/lib/internal/Magento/Framework/MessageQueue/QueueInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/QueueInterface.php @@ -9,6 +9,8 @@ * Interface for interaction with message queue. * * @api + * @since 103.0.0 + * @since 100.0.2 */ interface QueueInterface { @@ -16,6 +18,7 @@ interface QueueInterface * Get message from queue * * @return EnvelopeInterface + * @since 103.0.0 */ public function dequeue(); @@ -24,6 +27,7 @@ public function dequeue(); * * @param EnvelopeInterface $envelope * @return void + * @since 103.0.0 */ public function acknowledge(EnvelopeInterface $envelope); @@ -32,6 +36,7 @@ public function acknowledge(EnvelopeInterface $envelope); * * @param callable|array $callback * @return void + * @since 103.0.0 */ public function subscribe($callback); @@ -42,6 +47,7 @@ public function subscribe($callback); * @param bool $requeue * @param string $rejectionMessage * @return void + * @since 103.0.0 */ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionMessage = null); @@ -50,7 +56,7 @@ public function reject(EnvelopeInterface $envelope, $requeue = true, $rejectionM * * @param EnvelopeInterface $envelope * @return void - * @since 100.1.0 + * @since 103.0.0 */ public function push(EnvelopeInterface $envelope); } diff --git a/lib/internal/Magento/Framework/MessageQueue/QueueRepository.php b/lib/internal/Magento/Framework/MessageQueue/QueueRepository.php index 5d04003c9ade0..e28d44a9b5d2b 100644 --- a/lib/internal/Magento/Framework/MessageQueue/QueueRepository.php +++ b/lib/internal/Magento/Framework/MessageQueue/QueueRepository.php @@ -57,7 +57,7 @@ public function get($connectionName, $queueName) * Get queue factory. * * @return QueueFactoryInterface - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getQueueFactory() { diff --git a/lib/internal/Magento/Framework/MessageQueue/Rpc/Consumer.php b/lib/internal/Magento/Framework/MessageQueue/Rpc/Consumer.php index 10ef24c462808..46534eb55f613 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Rpc/Consumer.php +++ b/lib/internal/Magento/Framework/MessageQueue/Rpc/Consumer.php @@ -9,7 +9,7 @@ /** * A MessageQueue Consumer to handle receiving, processing and replying to an RPC message. * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ class Consumer extends \Magento\Framework\MessageQueue\Consumer { diff --git a/lib/internal/Magento/Framework/MessageQueue/Rpc/Publisher.php b/lib/internal/Magento/Framework/MessageQueue/Rpc/Publisher.php index 89b987cbb4405..90b2763471ee0 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Rpc/Publisher.php +++ b/lib/internal/Magento/Framework/MessageQueue/Rpc/Publisher.php @@ -107,7 +107,7 @@ public function publish($topicName, $data) * * @return ResponseQueueNameBuilder * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getResponseQueueNameBuilder() { @@ -123,7 +123,7 @@ private function getResponseQueueNameBuilder() * * @return PublisherConfig * - * @deprecated 100.2.0 + * @deprecated 103.0.0 */ private function getPublisherConfig() { diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Consumer/ConfigReaderPluginTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Consumer/ConfigReaderPluginTest.php index be24c2931c62a..75a73aff33b8a 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Consumer/ConfigReaderPluginTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Consumer/ConfigReaderPluginTest.php @@ -82,7 +82,10 @@ public function testAfterRead() 'consumerInstance' => 'type1', 'handlers' => ['handlerConfig1_1_1', 'handlerConfig1_1_2', 'handlerConfig1_2_1'], 'connection' => 'connection1', - 'maxMessages' => 100 + 'maxMessages' => 100, + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false ], 'consumer2' => [ 'name' => 'consumer2', @@ -90,7 +93,10 @@ public function testAfterRead() 'consumerInstance' => 'type2', 'handlers' => [], 'connection' => 'connection2', - 'maxMessages' => 2 + 'maxMessages' => 2, + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false ], 'consumer0' => [] ]; diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Topology/ConfigReaderPluginTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Topology/ConfigReaderPluginTest.php index e04cbc5fcc3e2..9a52d55d771f3 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Topology/ConfigReaderPluginTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Config/Topology/ConfigReaderPluginTest.php @@ -8,9 +8,9 @@ namespace Magento\Framework\MessageQueue\Test\Unit\Config\Topology; use Magento\Framework\MessageQueue\Config\Topology\ConfigReaderPlugin as TopologyConfigReaderPlugin; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use Magento\Framework\MessageQueue\ConfigInterface; use Magento\Framework\MessageQueue\Topology\Config\CompositeReader as TopologyConfigCompositeReader; -use Magento\Framework\TestFramework\Unit\Helper\ObjectManager as ObjectManagerHelper; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; @@ -36,14 +36,13 @@ class ConfigReaderPluginTest extends TestCase */ private $subjectMock; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->configMock = $this->getMockBuilder(ConfigInterface::class) - ->getMockForAbstractClass(); - $this->subjectMock = $this->getMockBuilder(TopologyConfigCompositeReader::class) - ->disableOriginalConstructor() - ->setMethods(['getBinds', 'getConnectionByTopic']) - ->getMock(); + $this->configMock = $this->createMock(ConfigInterface::class); + $this->subjectMock = $this->createMock(TopologyConfigCompositeReader::class); $this->objectManagerHelper = new ObjectManagerHelper($this); $this->plugin = $this->objectManagerHelper->getObject( @@ -52,7 +51,12 @@ protected function setUp(): void ); } - public function testAfterRead() + /** + * Test get config after read + * + * @return void + */ + public function testAfterRead(): void { $binding = [ [ @@ -90,17 +94,13 @@ public function testAfterRead() 'name' => 'magento-db', 'type' => 'topic', 'connection' => 'db', - 'bindings' => [ - 'defaultBinding' => $dbDefaultBinding - ] + 'bindings' => ['defaultBinding' => $dbDefaultBinding] ], 'magento--amqp' => [ 'name' => 'magento', 'type' => 'topic', 'connection' => 'amqp', - 'bindings' => [ - 'defaultBinding' => $amqpDefaultBinding - ] + 'bindings' => ['defaultBinding' => $amqpDefaultBinding] ] ]; $expectedResult = [ @@ -138,17 +138,21 @@ public function testAfterRead() ] ] ]; - - $this->configMock->expects(static::atLeastOnce()) + $this->configMock->expects($this->atLeastOnce()) ->method('getBinds') ->willReturn($binding); - $this->configMock->expects(static::exactly(2)) + $this->configMock->expects($this->exactly(2)) + ->method('getExchangeByTopic') + ->willReturnMap([ + ['catalog.product.removed', 'magento-db'], + ['inventory.counter.updated', 'magento'] + ]); + $this->configMock->expects($this->exactly(2)) ->method('getConnectionByTopic') ->willReturnMap([ ['catalog.product.removed', 'db'], ['inventory.counter.updated', 'amqp'] ]); - $this->assertEquals($expectedResult, $this->plugin->afterRead($this->subjectMock, $result)); } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/ConsumerInstanceTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/ConsumerInstanceTest.php index 3e56d50712a65..6881a0c09ded6 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/ConsumerInstanceTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/ConsumerInstanceTest.php @@ -55,6 +55,9 @@ public function validConfigDataProvider() ], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => false ] ] ] @@ -88,6 +91,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], // @codingStandardsIgnoreStart @@ -107,6 +113,9 @@ public function invalidConfigDataProvider() ], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'consumerClass1' does not exist and thus cannot be used as 'consumerInstance'" diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/FieldsTypesTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/FieldsTypesTest.php index 655d28bf4ee87..ff7122d5dad67 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/FieldsTypesTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/FieldsTypesTest.php @@ -51,6 +51,9 @@ public function validConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ] ], @@ -63,6 +66,54 @@ public function validConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => null, + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true + ] + ] + ], + 'valid, maxIdleTime == null' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => null, + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true + ] + ] + ], + 'valid, sleep == null' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => true + ] + ] + ], + 'valid, onlySpawnWhenMessageAvailable == null' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => null ] ] ], @@ -83,6 +134,8 @@ public function testValidateInvalid($configData, $expectedExceptionMessage) /** * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function invalidConfigDataProvider() { @@ -96,6 +149,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "Type of 'name' field specified in configuration of 'consumer1' consumer is invalid." @@ -110,6 +166,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "Type of 'queue' field specified in configuration of 'consumer1' consumer is invalid." @@ -124,6 +183,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "Type of 'consumerInstance' field specified in configuration of 'consumer1' consumer is invalid." @@ -138,6 +200,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => [], 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "Type of 'connection' field specified in configuration of 'consumer1' consumer is invalid." @@ -152,6 +217,9 @@ public function invalidConfigDataProvider() 'handlers' => '', 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "Type of 'handlers' field specified in configuration of 'consumer1' consumer is invalid." @@ -166,11 +234,65 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => 'abc', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "Type of 'maxMessages' field specified in configuration of 'consumer1' consumer is invalid." . " Given 'string', 'int|null' was expected." ], + 'invalid maxIdleTime' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => 'abc', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true + ] + ], + "Type of 'maxIdleTime' field specified in configuration of 'consumer1' consumer is invalid." + . " Given 'string', 'int|null' was expected." + ], + 'invalid sleep' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => 'abc', + 'onlySpawnWhenMessageAvailable' => true + ] + ], + "Type of 'sleep' field specified in configuration of 'consumer1' consumer is invalid." + . " Given 'string', 'int|null' was expected." + ], + 'onlySpawnWhenMessageAvailable' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => 'yes' + ] + ], + "Type of 'onlySpawnWhenMessageAvailable' field specified in configuration of 'consumer1' consumer " + . "is invalid. Given 'string', 'boolean|null' was expected." + ] ]; } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/HandlersTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/HandlersTest.php index e2b2cb45c7d43..fc67fd92520aa 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/HandlersTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/HandlersTest.php @@ -64,6 +64,9 @@ public function validConfigDataProvider() ], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ] ], @@ -76,6 +79,9 @@ public function validConfigDataProvider() 'handlers' => [], 'connection' => 'connection1', 'maxMessages' => null, + 'maxIdleTime' => '500', + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => true ] ] ], @@ -109,6 +115,9 @@ public function invalidConfigDataProvider() 'handlers' => ['handlerClassOne::handlerMethodOne'], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => false ] ], "'consumer1' consumer declaration is invalid. Every handler element must be an array." @@ -125,6 +134,9 @@ public function invalidConfigDataProvider() ], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'consumer1' consumer declaration is invalid. Every handler element must be an array." @@ -141,6 +153,9 @@ public function invalidConfigDataProvider() ], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'consumer1' consumer declaration is invalid. Every handler element must be an array." @@ -157,6 +172,9 @@ public function invalidConfigDataProvider() ], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'consumer1' consumer declaration is invalid. Every handler element must be an array." @@ -177,6 +195,9 @@ public function testValidateUndeclaredService() ], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ]; $expectedExceptionMessage = 'Service method specified as handler for of consumer "consumer1" is not available.' diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/RequiredFieldsTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/RequiredFieldsTest.php index 7e213164b18b5..9a330e7e87871 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/RequiredFieldsTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/Validator/RequiredFieldsTest.php @@ -51,6 +51,9 @@ public function validConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ] ] @@ -71,6 +74,8 @@ public function testValidateInvalid($configData, $expectedExceptionMessage) /** * @return array + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function invalidConfigDataProvider() { @@ -83,6 +88,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'name' field must be specified for consumer 'consumer1'" @@ -95,6 +103,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'queue' field must be specified for consumer 'consumer1'" @@ -107,6 +118,9 @@ public function invalidConfigDataProvider() 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'consumerInstance' field must be specified for consumer 'consumer1'" @@ -119,6 +133,9 @@ public function invalidConfigDataProvider() 'consumerInstance' => 'consumerClass1', 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'connection' field must be specified for consumer 'consumer1'" @@ -131,6 +148,9 @@ public function invalidConfigDataProvider() 'consumerInstance' => 'consumerClass1', 'connection' => 'connection1', 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'handlers' field must be specified for consumer 'consumer1'" @@ -143,10 +163,58 @@ public function invalidConfigDataProvider() 'consumerInstance' => 'consumerClass1', 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], 'connection' => 'connection1', + 'maxIdleTime' => '500', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true ] ], "'maxMessages' field must be specified for consumer 'consumer1'" ], + 'missing maxIdleTime' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'sleep' => '10', + 'onlySpawnWhenMessageAvailable' => true + ] + ], + "'maxIdleTime' field must be specified for consumer 'consumer1'" + ], + 'missing sleep' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'onlySpawnWhenMessageAvailable' => true + ] + ], + "'sleep' field must be specified for consumer 'consumer1'" + ], + 'missing onlySpawnWhenMessageAvailable' => [ + [ + 'consumer1' => [ + 'name' => 'consumer1', + 'queue' => 'queue1', + 'consumerInstance' => 'consumerClass1', + 'handlers' => [['type' => 'handlerClassOne', 'method' => 'handlerMethodOne']], + 'connection' => 'connection1', + 'maxMessages' => '100', + 'maxIdleTime' => '500', + 'sleep' => '10', + ] + ], + "'onlySpawnWhenMessageAvailable' field must be specified for consumer 'consumer1'" + ], ]; } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/XsdTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/XsdTest.php index 68fe72c3ffe94..c95625d0f6026 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/XsdTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/Consumer/Config/XsdTest.php @@ -69,7 +69,7 @@ public function exemplarXmlDataProvider() /** Valid configurations */ 'valid' => [ '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> - <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100"/> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100" maxIdleTime="500" sleep="5" onlySpawnWhenMessageAvailable="true"/> <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="db"/> <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> @@ -79,7 +79,7 @@ public function exemplarXmlDataProvider() ], 'non unique consumer name' => [ '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> - <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100"/> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100" maxIdleTime="500" sleep="2" onlySpawnWhenMessageAvailable="false"/> <consumer name="consumer1" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="db"/> <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> @@ -91,7 +91,7 @@ public function exemplarXmlDataProvider() ], 'invalid handler format' => [ '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> - <consumer name="consumer1" queue="queue1" handler="handlerClass_One1::handlerMethod1" consumerInstance="consumerClass1" connection="amqp" maxMessages="100"/> + <consumer name="consumer1" queue="queue1" handler="handlerClass_One1::handlerMethod1" consumerInstance="consumerClass1" connection="amqp" maxMessages="100" maxIdleTime="500" sleep="2" onlySpawnWhenMessageAvailable="true"/> <consumer name="consumer2" queue="queue2" handler="handlerClassOne2::handler_Method2" consumerInstance="consumerClass2" connection="db"/> <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> @@ -106,7 +106,7 @@ public function exemplarXmlDataProvider() ], 'invalid maxMessages format' => [ '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> - <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="ABC"/> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="ABC" maxIdleTime="500" sleep="2" onlySpawnWhenMessageAvailable="true"/> <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="db"/> <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> @@ -116,6 +116,42 @@ public function exemplarXmlDataProvider() "Element 'consumer', attribute 'maxMessages': 'ABC' is not a valid value of the atomic type 'xs:integer'.", ], ], + 'invalid maxIdleTime format' => [ + '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100" maxIdleTime="ABC" sleep="5" onlySpawnWhenMessageAvailable="false"/> + <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="db"/> + <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> + <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> + <consumer name="consumer5" queue="queue4"/> + </config>', + [ + "Element 'consumer', attribute 'maxIdleTime': 'ABC' is not a valid value of the atomic type 'xs:integer'.", + ], + ], + 'invalid sleep format' => [ + '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100" maxIdleTime="500" sleep="ABC" onlySpawnWhenMessageAvailable="false"/> + <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="db"/> + <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> + <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> + <consumer name="consumer5" queue="queue4"/> + </config>', + [ + "Element 'consumer', attribute 'sleep': 'ABC' is not a valid value of the atomic type 'xs:integer'.", + ], + ], + 'invalid onlySpawnWhenMessageAvailable format' => [ + '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100" maxIdleTime="500" sleep="5" onlySpawnWhenMessageAvailable="text"/> + <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="db"/> + <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> + <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> + <consumer name="consumer5" queue="queue4"/> + </config>', + [ + "Element 'consumer', attribute 'onlySpawnWhenMessageAvailable': 'text' is not a valid value of the atomic type 'xs:boolean'.", + ], + ], 'unexpected element' => [ '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100"/> @@ -130,7 +166,7 @@ public function exemplarXmlDataProvider() ], 'unexpected attribute' => [ '<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> - <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100"/> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="amqp" maxMessages="100" maxIdleTime="500" sleep="2" onlySpawnWhenMessageAvailable="true"/> <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="db"/> <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php index 4465fc2907f00..aa98a0a968e89 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/ConsumerTest.php @@ -211,4 +211,22 @@ public function testProcessWithNotFoundException() $this->consumer->process($numberOfMessages); } + + /** + * Test for process method with 'getMaxIdleTime' and 'getSleep' consumer configurations + * + * @return void + */ + public function testProcessWithGetMaxIdleTimeAndGetSleepConsumerConfigurations() + { + $numberOfMessages = 1; + $this->poisonPillRead->expects($this->atLeastOnce())->method('getLatestVersion'); + $queue = $this->getMockBuilder(\Magento\Framework\MessageQueue\QueueInterface::class) + ->disableOriginalConstructor()->getMock(); + $this->configuration->expects($this->once())->method('getQueue')->willReturn($queue); + $queue->expects($this->atMost(2))->method('dequeue')->willReturn(null); + $this->configuration->expects($this->once())->method('getMaxIdleTime')->willReturn('2'); + $this->configuration->expects($this->once())->method('getSleep')->willReturn('2'); + $this->consumer->process($numberOfMessages); + } } diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.php b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.php index f2140c1edefb1..ececb0aefb194 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.php +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.php @@ -18,7 +18,10 @@ ], ], 'connection' => 'connection1', - 'maxMessages' => '100', + 'maxMessages' => '200', + 'maxIdleTime' => '500', + 'sleep' => '5', + 'onlySpawnWhenMessageAvailable' => true ], 'consumer2' => [ 'name' => 'consumer2', @@ -31,7 +34,10 @@ ], ], 'connection' => 'connection2', - 'maxMessages' => null, + 'maxMessages' => '100', + 'maxIdleTime' => '1000', + 'sleep' => '2', + 'onlySpawnWhenMessageAvailable' => false ], 'consumer3' => [ 'name' => 'consumer3', @@ -43,28 +49,86 @@ 'method' => 'handlerMethodThree' ], ], - 'connection' => 'amqp', - 'maxMessages' => null, + 'connection' => 'connection3', + 'maxMessages' => '50', + 'maxIdleTime' => '100', + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false ], 'consumer4' => [ 'name' => 'consumer4', 'queue' => 'queue4', - 'consumerInstance' => ConsumerInterface::class, + 'consumerInstance' => 'consumerClass4', + 'handlers' => [ 0 => [ 'type' => 'handlerClassFour', 'method' => 'handlerMethodFour' ], ], - 'connection' => 'amqp', - 'maxMessages' => null, + 'connection' => 'connection4', + 'maxMessages' => '10', + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false ], 'consumer5' => [ 'name' => 'consumer5', 'queue' => 'queue5', + 'consumerInstance' => 'consumerClass5', + 'handlers' => [ + 0 => [ + 'type' => 'handlerClassFive', + 'method' => 'handlerMethodFive' + ], + ], + 'connection' => 'connection5', + 'maxMessages' => null, + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false + ], + 'consumer6' => [ + 'name' => 'consumer6', + 'queue' => 'queue6', + 'consumerInstance' => 'consumerClass6', + 'handlers' => [ + 0 => [ + 'type' => 'handlerClassSix', + 'method' => 'handlerMethodSix' + ], + ], + 'connection' => 'amqp', + 'maxMessages' => null, + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false + ], + 'consumer7' => [ + 'name' => 'consumer7', + 'queue' => 'queue7', + 'consumerInstance' => ConsumerInterface::class, + 'handlers' => [ + 0 => [ + 'type' => 'handlerClassSeven', + 'method' => 'handlerMethodSeven' + ], + ], + 'connection' => 'amqp', + 'maxMessages' => null, + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false + ], + 'consumer8' => [ + 'name' => 'consumer8', + 'queue' => 'queue8', 'consumerInstance' => ConsumerInterface::class, 'handlers' => [], 'connection' => 'amqp', 'maxMessages' => null, + 'maxIdleTime' => null, + 'sleep' => null, + 'onlySpawnWhenMessageAvailable' => false ], ]; diff --git a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.xml b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.xml index f020c64a06965..14bbb75d27939 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.xml +++ b/lib/internal/Magento/Framework/MessageQueue/Test/Unit/_files/queue_consumer/valid.xml @@ -6,9 +6,12 @@ */ --> <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework-message-queue:etc/consumer.xsd"> - <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="connection1" maxMessages="100"/> - <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="connection2"/> - <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3"/> - <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour"/> - <consumer name="consumer5" queue="queue5"/> + <consumer name="consumer1" queue="queue1" handler="handlerClassOne::handlerMethodOne" consumerInstance="consumerClass1" connection="connection1" maxMessages="200" maxIdleTime="500" sleep="5" onlySpawnWhenMessageAvailable="true"/> + <consumer name="consumer2" queue="queue2" handler="handlerClassTwo::handlerMethodTwo" consumerInstance="consumerClass2" connection="connection2" maxMessages="100" maxIdleTime="1000" sleep="2"/> + <consumer name="consumer3" queue="queue3" handler="handlerClassThree::handlerMethodThree" consumerInstance="consumerClass3" connection="connection3" maxMessages="50" maxIdleTime="100"/> + <consumer name="consumer4" queue="queue4" handler="handlerClassFour::handlerMethodFour" consumerInstance="consumerClass4" connection="connection4" maxMessages="10"/> + <consumer name="consumer5" queue="queue5" handler="handlerClassFive::handlerMethodFive" consumerInstance="consumerClass5" connection="connection5"/> + <consumer name="consumer6" queue="queue6" handler="handlerClassSix::handlerMethodSix" consumerInstance="consumerClass6"/> + <consumer name="consumer7" queue="queue7" handler="handlerClassSeven::handlerMethodSeven"/> + <consumer name="consumer8" queue="queue8"/> </config> diff --git a/lib/internal/Magento/Framework/MessageQueue/Topology/ConfigInterface.php b/lib/internal/Magento/Framework/MessageQueue/Topology/ConfigInterface.php index 81d056999b573..b7b15ecd07112 100644 --- a/lib/internal/Magento/Framework/MessageQueue/Topology/ConfigInterface.php +++ b/lib/internal/Magento/Framework/MessageQueue/Topology/ConfigInterface.php @@ -13,7 +13,7 @@ * Topology config interface provides access data declared in etc/queue_topology.xml * * @api - * @since 100.2.0 + * @since 103.0.0 */ interface ConfigInterface { @@ -25,7 +25,7 @@ interface ConfigInterface * @return ExchangeConfigItemInterface * @throws LocalizedException * @throws \LogicException - * @since 100.2.0 + * @since 103.0.0 */ public function getExchange($name, $connection); @@ -34,7 +34,7 @@ public function getExchange($name, $connection); * * @return ExchangeConfigItemInterface[] * @throws \LogicException - * @since 100.2.0 + * @since 103.0.0 */ public function getExchanges(); @@ -43,7 +43,7 @@ public function getExchanges(); * * @return QueueConfigItemInterface[] * @throws \LogicException - * @since 100.2.0 + * @since 103.0.0 */ public function getQueues(); } diff --git a/lib/internal/Magento/Framework/MessageQueue/etc/consumer.xsd b/lib/internal/Magento/Framework/MessageQueue/etc/consumer.xsd index 7e3d501aaa46e..e7595bb10d3b9 100644 --- a/lib/internal/Magento/Framework/MessageQueue/etc/consumer.xsd +++ b/lib/internal/Magento/Framework/MessageQueue/etc/consumer.xsd @@ -24,6 +24,9 @@ <xs:attribute type="xs:string" name="consumerInstance" use="optional"/> <xs:attribute name="connection" use="optional" type="xs:string" /> <xs:attribute type="xs:integer" name="maxMessages" use="optional"/> + <xs:attribute type="xs:integer" name="maxIdleTime" use="optional"/> + <xs:attribute type="xs:integer" name="sleep" use="optional"/> + <xs:attribute type="xs:boolean" name="onlySpawnWhenMessageAvailable" use="optional"/> </xs:complexType> <xs:simpleType name="handlerType"> <xs:annotation> diff --git a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php index 5484103cc27ef..306159f8d22d5 100644 --- a/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractExtensibleModel.php @@ -17,6 +17,7 @@ * @SuppressWarnings(PHPMD.NumberOfChildren) * phpcs:disable Magento2.Classes.AbstractApi * @api + * @since 100.0.2 */ abstract class AbstractExtensibleModel extends AbstractModel implements \Magento\Framework\Api\CustomAttributesDataInterface @@ -387,6 +388,7 @@ private function populateExtensionAttributes(array $extensionAttributesData = [] /** * @inheritdoc + * @since 100.0.11 */ public function __sleep() { @@ -395,6 +397,7 @@ public function __sleep() /** * @inheritdoc + * @since 100.0.11 */ public function __wakeup() { diff --git a/lib/internal/Magento/Framework/Model/AbstractModel.php b/lib/internal/Magento/Framework/Model/AbstractModel.php index 8018c6176390f..b6473f8b0ab3c 100644 --- a/lib/internal/Magento/Framework/Model/AbstractModel.php +++ b/lib/internal/Magento/Framework/Model/AbstractModel.php @@ -15,6 +15,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.TooManyFields) + * @since 100.0.2 */ abstract class AbstractModel extends \Magento\Framework\DataObject { @@ -467,7 +468,7 @@ protected function _setResourceModel($resourceName, $collectionName = null) * * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - * @deprecated because resource models should be used directly + * @deprecated 101.0.0 because resource models should be used directly */ protected function _getResource() { @@ -496,7 +497,7 @@ public function getResourceName() * @TODO MAGETWO-23541: Incorrect dependencies between Model\AbstractModel and Data\Collection\Db from Framework * @throws \Magento\Framework\Exception\LocalizedException * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection - * @deprecated because collections should be used directly via factory + * @deprecated 101.0.0 because collections should be used directly via factory */ public function getResourceCollection() { @@ -517,7 +518,7 @@ public function getResourceCollection() * * @TODO MAGETWO-23541: Incorrect dependencies between Model\AbstractModel and Data\Collection\Db from Framework * @return \Magento\Framework\Model\ResourceModel\Db\Collection\AbstractCollection - * @deprecated because collections should be used directly via factory + * @deprecated 101.0.0 because collections should be used directly via factory */ public function getCollection() { @@ -587,7 +588,7 @@ protected function _afterLoad() * @param string $identifier * @param string|null $field * @return void - * @since 100.2.0 + * @since 101.0.0 */ public function beforeLoad($identifier, $field = null) { @@ -895,7 +896,7 @@ public function afterDeleteCommit() * Retrieve model resource * * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb - * @deprecated because resource models should be used directly + * @deprecated 101.0.0 because resource models should be used directly */ public function getResource() { diff --git a/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php b/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php index 430bb6f65f295..e6b492b0a04fc 100644 --- a/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php +++ b/lib/internal/Magento/Framework/Model/ActionValidator/RemoveAction.php @@ -12,6 +12,7 @@ /** * @api + * @since 100.0.2 */ class RemoveAction { diff --git a/lib/internal/Magento/Framework/Model/Context.php b/lib/internal/Magento/Framework/Model/Context.php index 910a0d48a5e9c..301398a67f6de 100644 --- a/lib/internal/Magento/Framework/Model/Context.php +++ b/lib/internal/Magento/Framework/Model/Context.php @@ -19,6 +19,7 @@ * the classes they were introduced for. * * @api + * @since 100.0.2 */ class Context implements \Magento\Framework\ObjectManager\ContextInterface { diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php b/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php index b55b33ddfa54b..221110217a200 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/AbstractResource.php @@ -15,17 +15,19 @@ * * phpcs:disable Magento2.Classes.AbstractApi * @api + * @since 100.0.2 */ abstract class AbstractResource { /** * @var Json - * @since 100.2.0 + * @since 101.0.0 */ protected $serializer; /** * @var \Psr\Log\LoggerInterface + * @since 102.0.0 */ protected $_logger; @@ -250,8 +252,8 @@ protected function _getColumnsForEntityLoad(\Magento\Framework\Model\AbstractMod * Get serializer * * @return Json - * @deprecated 100.2.0 - * @since 100.2.0 + * @deprecated 101.0.0 + * @since 101.0.0 */ protected function getSerializer() { @@ -265,7 +267,7 @@ protected function getSerializer() * Get logger * * @return \Psr\Log\LoggerInterface - * @deprecated + * @deprecated 101.0.1 */ private function getLogger() { diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php index 626fc42a80778..33427a219f9c1 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/AbstractDb.php @@ -20,6 +20,7 @@ * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * phpcs:disable Magento2.Classes.AbstractApi * @api + * @since 100.0.2 */ abstract class AbstractDb extends AbstractResource { 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 cba5f133f53c8..b63291394e1d1 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/Collection/AbstractCollection.php @@ -15,6 +15,7 @@ * phpcs:disable Magento2.Classes.AbstractApi * @api * @SuppressWarnings(PHPMD.NumberOfChildren) + * @since 100.0.2 */ abstract class AbstractCollection extends AbstractDb implements SourceProviderInterface { diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/ObjectRelationProcessor.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/ObjectRelationProcessor.php index e899cc56f4548..0851c48d50036 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/ObjectRelationProcessor.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/ObjectRelationProcessor.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class ObjectRelationProcessor { diff --git a/lib/internal/Magento/Framework/Model/ResourceModel/Db/TransactionManagerInterface.php b/lib/internal/Magento/Framework/Model/ResourceModel/Db/TransactionManagerInterface.php index 166789196cfa6..81ef2b1b5fa18 100644 --- a/lib/internal/Magento/Framework/Model/ResourceModel/Db/TransactionManagerInterface.php +++ b/lib/internal/Magento/Framework/Model/ResourceModel/Db/TransactionManagerInterface.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ interface TransactionManagerInterface { diff --git a/lib/internal/Magento/Framework/Module/DependencyChecker.php b/lib/internal/Magento/Framework/Module/DependencyChecker.php index 5855c5cf4143e..a36af333f8b1f 100644 --- a/lib/internal/Magento/Framework/Module/DependencyChecker.php +++ b/lib/internal/Magento/Framework/Module/DependencyChecker.php @@ -13,43 +13,32 @@ class DependencyChecker { /** - * Enabled module list from configuration - * - * @var array - */ - private $enabledModuleList; - - /** - * The full module list information from filesystem - * - * @var array + * @var PackageInfo */ - private $fullModuleList; + private $packageInfo; /** - * Graph - * - * @var Graph + * @var ModuleList */ - private $graph; + private $list; /** - * @var PackageInfo + * @var ModuleList\Loader */ - protected $packageInfo; + private $loader; /** * Constructor * * @param ModuleList $list * @param ModuleList\Loader $loader - * @param PackageInfoFactory $packageInfoFactory + * @param PackageInfo $packageInfo */ - public function __construct(ModuleList $list, ModuleList\Loader $loader, PackageInfoFactory $packageInfoFactory) + public function __construct(ModuleList $list, ModuleList\Loader $loader, PackageInfo $packageInfo) { - $this->enabledModuleList = $list->getNames(); - $this->fullModuleList = $loader->load(); - $this->packageInfo = $packageInfoFactory->create(); + $this->list = $list; + $this->loader = $loader; + $this->packageInfo = $packageInfo; } /** @@ -58,10 +47,11 @@ public function __construct(ModuleList $list, ModuleList\Loader $loader, Package * @param string[] $toBeDisabledModules * @param string[] $currentlyEnabledModules * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ public function checkDependenciesWhenDisableModules($toBeDisabledModules, $currentlyEnabledModules = null) { - $masterList = isset($currentlyEnabledModules) ? $currentlyEnabledModules : $this->enabledModuleList; + $masterList = $currentlyEnabledModules ?? $this->list->getNames(); // assume disable succeeds: currently enabled modules - to-be-disabled modules $enabledModules = array_diff($masterList, $toBeDisabledModules); return $this->checkDependencyGraph(false, $toBeDisabledModules, $enabledModules); @@ -73,10 +63,11 @@ public function checkDependenciesWhenDisableModules($toBeDisabledModules, $curre * @param string[] $toBeEnabledModules * @param string[] $currentlyEnabledModules * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ - public function checkDependenciesWhenEnableModules($toBeEnabledModules, $currentlyEnabledModules = null) + public function checkDependenciesWhenEnableModules(array $toBeEnabledModules, array $currentlyEnabledModules = null) { - $masterList = isset($currentlyEnabledModules) ? $currentlyEnabledModules : $this->enabledModuleList; + $masterList = $currentlyEnabledModules ?? $this->list->getNames(); // assume enable succeeds: union of currently enabled modules and to-be-enabled modules $enabledModules = array_unique(array_merge($masterList, $toBeEnabledModules)); return $this->checkDependencyGraph(true, $toBeEnabledModules, $enabledModules); @@ -89,19 +80,21 @@ public function checkDependenciesWhenEnableModules($toBeEnabledModules, $current * @param string[] $moduleNames list of modules to be enabled/disabled * @param string[] $enabledModules list of enabled modules assuming enable/disable succeeds * @return array + * @throws \Magento\Framework\Exception\LocalizedException */ - private function checkDependencyGraph($isEnable, $moduleNames, $enabledModules) + private function checkDependencyGraph(bool $isEnable, array $moduleNames, array $enabledModules) { - $this->graph = $this->createGraph(); + $fullModuleList = $this->loader->load(); + $graph = $this->createGraph($fullModuleList); $dependenciesMissingAll = []; $graphMode = $isEnable ? Graph::DIRECTIONAL : Graph::INVERSE; + $modules = array_merge( + array_keys($fullModuleList), + $this->packageInfo->getNonExistingDependencies() + ); foreach ($moduleNames as $moduleName) { $dependenciesMissing = []; - $paths = $this->graph->findPathsToReachableNodes($moduleName, $graphMode); - $modules = array_merge( - array_keys($this->fullModuleList), - $this->packageInfo->getNonExistingDependencies() - ); + $paths = $graph->findPathsToReachableNodes($moduleName, $graphMode); foreach ($modules as $module) { if (isset($paths[$module])) { if ($isEnable && !in_array($module, $enabledModules)) { @@ -119,15 +112,16 @@ private function checkDependencyGraph($isEnable, $moduleNames, $enabledModules) /** * Create the dependency graph * + * @param array $fullModuleList * @return Graph */ - private function createGraph() + private function createGraph(array $fullModuleList): Graph { $nodes = []; $dependencies = []; // build the graph data - foreach (array_keys($this->fullModuleList) as $moduleName) { + foreach (array_keys($fullModuleList) as $moduleName) { $nodes[] = $moduleName; foreach ($this->packageInfo->getRequire($moduleName) as $dependModuleName) { if ($dependModuleName) { diff --git a/lib/internal/Magento/Framework/Module/Dir/Reader.php b/lib/internal/Magento/Framework/Module/Dir/Reader.php index 141a291ea6818..67da0eeb875e6 100644 --- a/lib/internal/Magento/Framework/Module/Dir/Reader.php +++ b/lib/internal/Magento/Framework/Module/Dir/Reader.php @@ -15,6 +15,7 @@ /** * @api + * @since 100.0.2 */ class Reader { @@ -124,7 +125,11 @@ private function getFiles($filename, $subDir = '') { $result = []; foreach ($this->modulesList->getNames() as $moduleName) { - $moduleSubDir = $this->getModuleDir($subDir, $moduleName); + try { + $moduleSubDir = $this->getModuleDir($subDir, $moduleName); + } catch (\InvalidArgumentException $e) { + continue; + } $file = $moduleSubDir . '/' . $filename; $directoryRead = $this->readFactory->create($moduleSubDir); $path = $directoryRead->getRelativePath($file); diff --git a/lib/internal/Magento/Framework/Module/Manager.php b/lib/internal/Magento/Framework/Module/Manager.php index b47349631a033..73ea68ac367c3 100644 --- a/lib/internal/Magento/Framework/Module/Manager.php +++ b/lib/internal/Magento/Framework/Module/Manager.php @@ -23,7 +23,7 @@ class Manager { /** * @var Output\ConfigInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private $outputConfig; @@ -34,7 +34,7 @@ class Manager /** * @var array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private $outputConfigPaths; @@ -69,7 +69,7 @@ public function isEnabled($moduleName) * * @param string $moduleName Fully-qualified module name * @return boolean - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -87,7 +87,7 @@ public function isOutputEnabled($moduleName) * * @param string $moduleName Fully-qualified module name * @return boolean - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected function _isCustomOutputConfigEnabled($moduleName) { diff --git a/lib/internal/Magento/Framework/Module/ModuleResource.php b/lib/internal/Magento/Framework/Module/ModuleResource.php index b453ea4cba095..f7f212b829997 100644 --- a/lib/internal/Magento/Framework/Module/ModuleResource.php +++ b/lib/internal/Magento/Framework/Module/ModuleResource.php @@ -11,7 +11,7 @@ /** * Resource Model * - * @deprecated Declarative schema and data patches replace old functionality and setup_module table + * @deprecated 102.0.0 Declarative schema and data patches replace old functionality and setup_module table * So all resources related to this table, will be deprecated since 2.3.0 */ class ModuleResource extends AbstractDb implements ResourceInterface @@ -141,7 +141,7 @@ public function setDataVersion($moduleName, $version) /** * Flush all class cache * - * @deprecated This method was added as temporary solution, to increase modularity: + * @deprecated 102.0.0 This method was added as temporary solution, to increase modularity: * Because before new modules appears in resource only on next bootstrap * @return void */ diff --git a/lib/internal/Magento/Framework/Module/Output/Config.php b/lib/internal/Magento/Framework/Module/Output/Config.php index 48afc97dc5336..765998fcdb568 100644 --- a/lib/internal/Magento/Framework/Module/Output/Config.php +++ b/lib/internal/Magento/Framework/Module/Output/Config.php @@ -8,7 +8,7 @@ namespace Magento\Framework\Module\Output; /** - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -24,13 +24,13 @@ class Config implements \Magento\Framework\Module\Output\ConfigInterface /** * @var \Magento\Framework\App\Config\ScopeConfigInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_scopeConfig; /** * @var string - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ protected $_storeType; @@ -50,7 +50,7 @@ public function __construct( * Whether a module is enabled in the configuration or not * * @param string $moduleName Fully-qualified module name - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -66,7 +66,7 @@ public function isEnabled($moduleName) * Retrieve module enabled specific path * * @param string $path Fully-qualified config path - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity diff --git a/lib/internal/Magento/Framework/Module/Output/ConfigInterface.php b/lib/internal/Magento/Framework/Module/Output/ConfigInterface.php index 6813691b9c906..b657e742ee2f0 100644 --- a/lib/internal/Magento/Framework/Module/Output/ConfigInterface.php +++ b/lib/internal/Magento/Framework/Module/Output/ConfigInterface.php @@ -6,7 +6,7 @@ namespace Magento\Framework\Module\Output; /** - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. */ interface ConfigInterface @@ -15,7 +15,7 @@ interface ConfigInterface * Whether a module is enabled in the configuration or not * * @param string $moduleName Fully-qualified module name - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity @@ -28,7 +28,7 @@ public function isEnabled($moduleName); * Retrieve module enabled specific path * * @param string $path Fully-qualified config path - * @deprecated 100.2.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 + * @deprecated 101.0.0 Magento does not support disabling/enabling modules output from the Admin Panel since 2.2.0 * version. Module output can still be enabled/disabled in configuration files. However, this functionality should * not be used in future development. Module design should explicitly state dependencies to avoid requiring output * disabling. This functionality will temporarily be kept in Magento core, as there are unresolved modularity diff --git a/lib/internal/Magento/Framework/Module/Setup/Migration.php b/lib/internal/Magento/Framework/Module/Setup/Migration.php index 1de26b5c9234a..551ea3305337a 100644 --- a/lib/internal/Magento/Framework/Module/Setup/Migration.php +++ b/lib/internal/Magento/Framework/Module/Setup/Migration.php @@ -15,6 +15,7 @@ * @api * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Migration { @@ -703,7 +704,7 @@ public function getCompositeModules() * @return string|int|float|bool|array|null * @throws \InvalidArgumentException * @SuppressWarnings(PHPMD.UnusedFormalParameter) - * @deprecated + * @deprecated 101.0.1 * @see \Magento\Framework\Module\Setup\Migration::jsonDecode */ protected function _jsonDecode($encodedValue, $objectDecodeType = 1) diff --git a/lib/internal/Magento/Framework/Module/Test/Unit/DependencyCheckerTest.php b/lib/internal/Magento/Framework/Module/Test/Unit/DependencyCheckerTest.php index faba400ed5730..dfe255f90f01a 100644 --- a/lib/internal/Magento/Framework/Module/Test/Unit/DependencyCheckerTest.php +++ b/lib/internal/Magento/Framework/Module/Test/Unit/DependencyCheckerTest.php @@ -27,11 +27,6 @@ class DependencyCheckerTest extends TestCase */ private $packageInfoMock; - /** - * @var PackageInfoFactory|MockObject - */ - private $packageInfoFactoryMock; - /** * @var ModuleList|MockObject */ @@ -57,13 +52,8 @@ protected function setUp(): void ->method('getRequire') ->willReturnMap($requireMap); - $this->packageInfoFactoryMock = $this->createMock(PackageInfoFactory::class); - $this->packageInfoFactoryMock->expects($this->once()) - ->method('create') - ->willReturn($this->packageInfoMock); - - $this->listMock = $this->createMock(ModuleList::class); - $this->loaderMock = $this->createMock(Loader::class); + $this->listMock = $this->createMock(\Magento\Framework\Module\ModuleList::class); + $this->loaderMock = $this->createMock(\Magento\Framework\Module\ModuleList\Loader::class); $this->loaderMock ->expects($this->any()) ->method('load') @@ -78,7 +68,7 @@ public function testCheckDependenciesWhenDisableModules() $this->packageInfoMock->expects($this->atLeastOnce()) ->method('getNonExistingDependencies') ->willReturn([]); - $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoFactoryMock); + $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoMock); $actual = $this->checker->checkDependenciesWhenDisableModules(['B', 'D']); $expected = ['B' => ['A' => ['A', 'B']], 'D' => ['A' => ['A', 'B', 'D']]]; @@ -90,7 +80,7 @@ public function testCheckDependenciesWhenDisableModulesWithCurEnabledModules() $this->packageInfoMock->expects($this->atLeastOnce()) ->method('getNonExistingDependencies') ->willReturn([]); - $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoFactoryMock); + $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoMock); $actual = $this->checker->checkDependenciesWhenDisableModules(['B', 'D'], ['C', 'D', 'E']); $expected = ['B' => [], 'D' => []]; @@ -105,7 +95,7 @@ public function testCheckDependenciesWhenEnableModules() $this->packageInfoMock->expects($this->atLeastOnce()) ->method('getNonExistingDependencies') ->willReturn([]); - $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoFactoryMock); + $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoMock); $actual = $this->checker->checkDependenciesWhenEnableModules(['B', 'D']); $expected = [ 'B' => ['A' => ['B', 'D', 'A'], 'E' => ['B', 'E']], @@ -119,7 +109,7 @@ public function testCheckDependenciesWhenEnableModulesWithCurEnabledModules() $this->packageInfoMock->expects($this->atLeastOnce()) ->method('getNonExistingDependencies') ->willReturn([]); - $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoFactoryMock); + $this->checker = new DependencyChecker($this->listMock, $this->loaderMock, $this->packageInfoMock); $actual = $this->checker->checkDependenciesWhenEnableModules(['B', 'D'], ['C']); $expected = [ 'B' => ['A' => ['B', 'D', 'A'], 'E' => ['B', 'E']], diff --git a/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php b/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php index 522aff5d43b7b..0247503093278 100644 --- a/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php +++ b/lib/internal/Magento/Framework/Mview/Test/Unit/View/CollectionTest.php @@ -143,6 +143,15 @@ function ($elem) { $indexers )); + $this->mviewConfigMock + ->method('getView') + ->willReturnMap(array_map( + function ($elem) { + return [$elem, ['view_id' => $elem]]; + }, + $views + )); + $this->entityFactoryMock ->method('create') ->willReturnMap([ diff --git a/lib/internal/Magento/Framework/Mview/TriggerCleaner.php b/lib/internal/Magento/Framework/Mview/TriggerCleaner.php new file mode 100644 index 0000000000000..ac2db0a6f4816 --- /dev/null +++ b/lib/internal/Magento/Framework/Mview/TriggerCleaner.php @@ -0,0 +1,127 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Framework\Mview; + +use Magento\Framework\App\ResourceConnection; +use Magento\Framework\Mview\View\CollectionFactory; +use Magento\Framework\Mview\View\StateInterface; + +/** + * Class for removing old triggers that were created by mview + */ +class TriggerCleaner +{ + /** + * @var CollectionFactory + */ + private $viewCollectionFactory; + + /** + * @var ResourceConnection + */ + private $resource; + + /** + * @var ViewFactory + */ + private $viewFactory; + + /** + * @param CollectionFactory $viewCollectionFactory + * @param ResourceConnection $resource + * @param ViewFactory $viewFactory + */ + public function __construct( + CollectionFactory $viewCollectionFactory, + ResourceConnection $resource, + ViewFactory $viewFactory + ) { + $this->viewCollectionFactory = $viewCollectionFactory; + $this->resource = $resource; + $this->viewFactory = $viewFactory; + } + + /** + * Remove the outdated trigger from the system + * + * @return bool + * @throws \Exception + */ + public function removeTriggers(): bool + { + // Get list of views that are enabled + $viewCollection = $this->viewCollectionFactory->create(); + $viewList = $viewCollection->getViewsByStateMode(StateInterface::MODE_ENABLED); + + // Unsubscribe existing view to remove triggers from db + foreach ($viewList as $view) { + $view->unsubscribe(); + } + + // Remove any remaining triggers from db that are not linked to a view + $triggerTableNames = $this->getTableNamesWithTriggers(); + foreach ($triggerTableNames as $tableName) { + $view = $this->createViewByTableName($tableName); + $view->unsubscribe(); + $view->getState()->delete(); + } + + // Restore the previous state of the views to add triggers back to db + foreach ($viewList as $view) { + $view->subscribe(); + } + + return true; + } + + /** + * Retrieve list of table names that have triggers + * + * @return array + */ + private function getTableNamesWithTriggers(): array + { + $connection = $this->resource->getConnection(); + $dbName = $this->resource->getSchemaName(ResourceConnection::DEFAULT_CONNECTION); + $sql = $connection->select() + ->from( + ['information_schema.TRIGGERS'], + ['EVENT_OBJECT_TABLE'] + ) + ->distinct(true) + ->where('TRIGGER_SCHEMA = ?', $dbName); + return $connection->fetchCol($sql); + } + + /** + * Create view by db table name + * + * Create a view that has the table name so that unsubscribe can be used to + * remove triggers with the correct naming structure from the db + * + * @param string $tableName + * @return ViewInterface + */ + private function createViewByTableName(string $tableName): ViewInterface + { + $subscription[$tableName] = [ + 'name' => $tableName, + 'column' => '', + 'subscription_model' => null + ]; + $data['data'] = [ + 'subscriptions' => $subscription, + ]; + + $view = $this->viewFactory->create($data); + $view->setId('old_view'); + $view->getState()->setMode(StateInterface::MODE_ENABLED); + + return $view; + } +} diff --git a/lib/internal/Magento/Framework/Mview/View/Collection.php b/lib/internal/Magento/Framework/Mview/View/Collection.php index 91276bb376049..573bbf798d49e 100644 --- a/lib/internal/Magento/Framework/Mview/View/Collection.php +++ b/lib/internal/Magento/Framework/Mview/View/Collection.php @@ -93,7 +93,11 @@ private function getOrderedViewIds() /** @var IndexerInterface $indexer */ foreach (array_keys($this->indexerConfig->getIndexers()) as $indexerId) { $indexer = $this->_entityFactory->create(IndexerInterface::class); - $orderedViewIds[] = $indexer->load($indexerId)->getViewId(); + $viewId = $indexer->load($indexerId)->getViewId(); + $view = $this->config->getView($viewId); + if (!empty($view) && !empty($view['view_id']) && $view['view_id'] === $viewId) { + $orderedViewIds[] = $viewId; + } } $orderedViewIds = array_filter($orderedViewIds); $orderedViewIds += array_diff(array_keys($this->config->getViews()), $orderedViewIds); diff --git a/lib/internal/Magento/Framework/Notification/MessageInterface.php b/lib/internal/Magento/Framework/Notification/MessageInterface.php index a1694b042d9db..b768c0daed11b 100644 --- a/lib/internal/Magento/Framework/Notification/MessageInterface.php +++ b/lib/internal/Magento/Framework/Notification/MessageInterface.php @@ -14,6 +14,7 @@ * Interface MessageInterface * * @api + * @since 100.0.2 */ interface MessageInterface { diff --git a/lib/internal/Magento/Framework/Notification/MessageList.php b/lib/internal/Magento/Framework/Notification/MessageList.php index ac753b48c8944..62ac8e083bfd1 100644 --- a/lib/internal/Magento/Framework/Notification/MessageList.php +++ b/lib/internal/Magento/Framework/Notification/MessageList.php @@ -11,6 +11,7 @@ * * Class MessageList * @api + * @since 100.0.2 */ class MessageList { diff --git a/lib/internal/Magento/Framework/Notification/NotifierInterface.php b/lib/internal/Magento/Framework/Notification/NotifierInterface.php index 0e34e69a94541..862298663a9c6 100644 --- a/lib/internal/Magento/Framework/Notification/NotifierInterface.php +++ b/lib/internal/Magento/Framework/Notification/NotifierInterface.php @@ -12,6 +12,7 @@ * Interface NotifierInterface * * @api + * @since 100.0.2 */ interface NotifierInterface { diff --git a/lib/internal/Magento/Framework/Notification/NotifierList.php b/lib/internal/Magento/Framework/Notification/NotifierList.php index e436e21cae5fc..2b2f9f5faf8a8 100644 --- a/lib/internal/Magento/Framework/Notification/NotifierList.php +++ b/lib/internal/Magento/Framework/Notification/NotifierList.php @@ -10,6 +10,7 @@ * List of registered system notifiers * @api * + * @since 100.0.2 */ class NotifierList { diff --git a/lib/internal/Magento/Framework/Oauth/ConsumerInterface.php b/lib/internal/Magento/Framework/Oauth/ConsumerInterface.php index ae416b01eae94..3945233ca2b5d 100644 --- a/lib/internal/Magento/Framework/Oauth/ConsumerInterface.php +++ b/lib/internal/Magento/Framework/Oauth/ConsumerInterface.php @@ -9,6 +9,7 @@ * Oauth consumer interface. * * @api + * @since 100.0.2 */ interface ConsumerInterface { diff --git a/lib/internal/Magento/Framework/Oauth/Exception.php b/lib/internal/Magento/Framework/Oauth/Exception.php index 6e6753654fdd2..eaf1fd065ab01 100644 --- a/lib/internal/Magento/Framework/Oauth/Exception.php +++ b/lib/internal/Magento/Framework/Oauth/Exception.php @@ -11,6 +11,7 @@ * OAuth Exception * * @api + * @since 100.0.2 */ class Exception extends AuthenticationException { diff --git a/lib/internal/Magento/Framework/Oauth/NonceGeneratorInterface.php b/lib/internal/Magento/Framework/Oauth/NonceGeneratorInterface.php index ca364aa95abeb..79dd527afc570 100644 --- a/lib/internal/Magento/Framework/Oauth/NonceGeneratorInterface.php +++ b/lib/internal/Magento/Framework/Oauth/NonceGeneratorInterface.php @@ -11,6 +11,7 @@ * A method for generating a current timestamp is also provided by this interface. * * @api + * @since 100.0.2 */ interface NonceGeneratorInterface { diff --git a/lib/internal/Magento/Framework/Oauth/OauthInputException.php b/lib/internal/Magento/Framework/Oauth/OauthInputException.php index 1c526dd220f25..52ff15bbb75d5 100644 --- a/lib/internal/Magento/Framework/Oauth/OauthInputException.php +++ b/lib/internal/Magento/Framework/Oauth/OauthInputException.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class OauthInputException extends InputException { diff --git a/lib/internal/Magento/Framework/Oauth/OauthInterface.php b/lib/internal/Magento/Framework/Oauth/OauthInterface.php index 4f68eabaf69dc..8528e682ab4c8 100644 --- a/lib/internal/Magento/Framework/Oauth/OauthInterface.php +++ b/lib/internal/Magento/Framework/Oauth/OauthInterface.php @@ -11,6 +11,7 @@ * token requests. A method is also included for generating an OAuth header that can be used in an HTTP request. * * @api + * @since 100.0.2 */ interface OauthInterface { diff --git a/lib/internal/Magento/Framework/Oauth/TokenProviderInterface.php b/lib/internal/Magento/Framework/Oauth/TokenProviderInterface.php index 7f4bc6a320173..7b07b1550745a 100644 --- a/lib/internal/Magento/Framework/Oauth/TokenProviderInterface.php +++ b/lib/internal/Magento/Framework/Oauth/TokenProviderInterface.php @@ -11,6 +11,7 @@ * provided to help clients manipulating tokens validate and acquire the associated token consumer. * * @api + * @since 100.0.2 */ interface TokenProviderInterface { diff --git a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Repository.php b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Repository.php index 24ac714507d8e..645e3fd7eff78 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Repository.php +++ b/lib/internal/Magento/Framework/ObjectManager/Code/Generator/Repository.php @@ -16,8 +16,7 @@ /** * Class Repository - * @since 2.0.0 - * @deprecated 2.2.0 As current implementation breaks Repository contract. Not removed from codebase to prevent + * @deprecated 101.0.0 As current implementation breaks Repository contract. Not removed from codebase to prevent * possible backward incompatibilities if this functionality being used by 3rd party developers. */ class Repository extends \Magento\Framework\Code\Generator\EntityAbstract diff --git a/lib/internal/Magento/Framework/ObjectManager/Config/Config.php b/lib/internal/Magento/Framework/ObjectManager/Config/Config.php index b45a2f8bcf0ba..72b5902337b0d 100644 --- a/lib/internal/Magento/Framework/ObjectManager/Config/Config.php +++ b/lib/internal/Magento/Framework/ObjectManager/Config/Config.php @@ -336,7 +336,7 @@ public function getPreferences() * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/ObjectManager/ContextInterface.php b/lib/internal/Magento/Framework/ObjectManager/ContextInterface.php index 323f092e7cf52..742917ead47fc 100644 --- a/lib/internal/Magento/Framework/ObjectManager/ContextInterface.php +++ b/lib/internal/Magento/Framework/ObjectManager/ContextInterface.php @@ -17,6 +17,7 @@ * the classes they were introduced for. * * @api + * @since 100.0.2 */ interface ContextInterface { diff --git a/lib/internal/Magento/Framework/ObjectManagerInterface.php b/lib/internal/Magento/Framework/ObjectManagerInterface.php index 68e7056bc413b..6b165595f0f2c 100644 --- a/lib/internal/Magento/Framework/ObjectManagerInterface.php +++ b/lib/internal/Magento/Framework/ObjectManagerInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface ObjectManagerInterface { diff --git a/lib/internal/Magento/Framework/Option/ArrayInterface.php b/lib/internal/Magento/Framework/Option/ArrayInterface.php index 054d9059b02f8..3c813545bc2c5 100644 --- a/lib/internal/Magento/Framework/Option/ArrayInterface.php +++ b/lib/internal/Magento/Framework/Option/ArrayInterface.php @@ -8,7 +8,7 @@ /** * Array marker interface * - * @deprecated please use \Magento\Framework\Data\OptionSourceInterface instead. + * @deprecated 102.0.1 please use \Magento\Framework\Data\OptionSourceInterface instead. * @see \Magento\Framework\Data\OptionSourceInterface */ interface ArrayInterface extends \Magento\Framework\Data\OptionSourceInterface diff --git a/lib/internal/Magento/Framework/Phrase.php b/lib/internal/Magento/Framework/Phrase.php index da1eb2a2b3d98..3034dfc30a9e2 100644 --- a/lib/internal/Magento/Framework/Phrase.php +++ b/lib/internal/Magento/Framework/Phrase.php @@ -12,6 +12,7 @@ /** * @api + * @since 100.0.2 */ class Phrase implements \JsonSerializable { diff --git a/lib/internal/Magento/Framework/Phrase/RendererInterface.php b/lib/internal/Magento/Framework/Phrase/RendererInterface.php index e3e314696f3d1..fa8a8fcae7ae8 100644 --- a/lib/internal/Magento/Framework/Phrase/RendererInterface.php +++ b/lib/internal/Magento/Framework/Phrase/RendererInterface.php @@ -11,6 +11,7 @@ * Translated phrase renderer * * @api + * @since 100.0.2 */ interface RendererInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Adjustment/AdjustmentInterface.php b/lib/internal/Magento/Framework/Pricing/Adjustment/AdjustmentInterface.php index 9dff304ea3407..2ca6bd07d16e0 100644 --- a/lib/internal/Magento/Framework/Pricing/Adjustment/AdjustmentInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Adjustment/AdjustmentInterface.php @@ -12,6 +12,7 @@ * Interface AdjustmentInterface * * @api + * @since 100.0.2 */ interface AdjustmentInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Adjustment/CalculatorInterface.php b/lib/internal/Magento/Framework/Pricing/Adjustment/CalculatorInterface.php index ea622c2a1cba6..2c182632407ab 100644 --- a/lib/internal/Magento/Framework/Pricing/Adjustment/CalculatorInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Adjustment/CalculatorInterface.php @@ -12,6 +12,7 @@ * Calculator interface * * @api + * @since 100.0.2 */ interface CalculatorInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Adjustment/Collection.php b/lib/internal/Magento/Framework/Pricing/Adjustment/Collection.php index c7ab4b6e796c2..96e4d52e6b41d 100644 --- a/lib/internal/Magento/Framework/Pricing/Adjustment/Collection.php +++ b/lib/internal/Magento/Framework/Pricing/Adjustment/Collection.php @@ -10,6 +10,7 @@ * Adjustment collection model * * @api + * @since 100.0.2 */ class Collection { diff --git a/lib/internal/Magento/Framework/Pricing/Adjustment/Pool.php b/lib/internal/Magento/Framework/Pricing/Adjustment/Pool.php index d064e05b0b671..4d1e213a9ff6a 100644 --- a/lib/internal/Magento/Framework/Pricing/Adjustment/Pool.php +++ b/lib/internal/Magento/Framework/Pricing/Adjustment/Pool.php @@ -12,6 +12,7 @@ * Global adjustment pool model * * @api + * @since 100.0.2 */ class Pool { diff --git a/lib/internal/Magento/Framework/Pricing/Amount/AmountFactory.php b/lib/internal/Magento/Framework/Pricing/Amount/AmountFactory.php index a8340ee097f58..93da79d8588cb 100644 --- a/lib/internal/Magento/Framework/Pricing/Amount/AmountFactory.php +++ b/lib/internal/Magento/Framework/Pricing/Amount/AmountFactory.php @@ -10,6 +10,7 @@ * Class AmountFactory * * @api + * @since 100.0.2 */ class AmountFactory { diff --git a/lib/internal/Magento/Framework/Pricing/Amount/AmountInterface.php b/lib/internal/Magento/Framework/Pricing/Amount/AmountInterface.php index a625e340395cc..45240315aa126 100644 --- a/lib/internal/Magento/Framework/Pricing/Amount/AmountInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Amount/AmountInterface.php @@ -10,6 +10,7 @@ * Amount interface, the amount values are in display currency * * @api + * @since 100.0.2 */ interface AmountInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Helper/Data.php b/lib/internal/Magento/Framework/Pricing/Helper/Data.php index bc56bddea8ece..39852074300be 100644 --- a/lib/internal/Magento/Framework/Pricing/Helper/Data.php +++ b/lib/internal/Magento/Framework/Pricing/Helper/Data.php @@ -11,6 +11,7 @@ * Pricing data helper * * @api + * @since 100.0.2 */ class Data extends \Magento\Framework\App\Helper\AbstractHelper { diff --git a/lib/internal/Magento/Framework/Pricing/Price/AbstractPrice.php b/lib/internal/Magento/Framework/Pricing/Price/AbstractPrice.php index 8c49385f7e60f..89470cf24cbb6 100644 --- a/lib/internal/Magento/Framework/Pricing/Price/AbstractPrice.php +++ b/lib/internal/Magento/Framework/Pricing/Price/AbstractPrice.php @@ -16,6 +16,7 @@ * Should be the base for creating any Price type class * * @api + * @since 100.0.2 */ abstract class AbstractPrice implements PriceInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Price/BasePriceProviderInterface.php b/lib/internal/Magento/Framework/Pricing/Price/BasePriceProviderInterface.php index eb5c6d9064e21..b6367fa46c161 100644 --- a/lib/internal/Magento/Framework/Pricing/Price/BasePriceProviderInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Price/BasePriceProviderInterface.php @@ -10,6 +10,7 @@ * Interface BasePriceProviderInterface * * @api + * @since 100.0.2 */ interface BasePriceProviderInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Price/Collection.php b/lib/internal/Magento/Framework/Pricing/Price/Collection.php index 200c3a50aff89..eedb910c56b92 100644 --- a/lib/internal/Magento/Framework/Pricing/Price/Collection.php +++ b/lib/internal/Magento/Framework/Pricing/Price/Collection.php @@ -12,6 +12,7 @@ * Class Collection * * @api + * @since 100.0.2 */ class Collection implements \Iterator { diff --git a/lib/internal/Magento/Framework/Pricing/Price/Pool.php b/lib/internal/Magento/Framework/Pricing/Price/Pool.php index dfdd0c52681e1..a1c7d416b2259 100644 --- a/lib/internal/Magento/Framework/Pricing/Price/Pool.php +++ b/lib/internal/Magento/Framework/Pricing/Price/Pool.php @@ -10,6 +10,7 @@ * Class Pool * * @api + * @since 100.0.2 */ class Pool implements \Iterator, \ArrayAccess { diff --git a/lib/internal/Magento/Framework/Pricing/Price/PriceInterface.php b/lib/internal/Magento/Framework/Pricing/Price/PriceInterface.php index 7953940287eb2..2b3bd86d04b2b 100644 --- a/lib/internal/Magento/Framework/Pricing/Price/PriceInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Price/PriceInterface.php @@ -12,6 +12,7 @@ * Catalog price interface * * @api + * @since 100.0.2 */ interface PriceInterface { diff --git a/lib/internal/Magento/Framework/Pricing/PriceCurrencyInterface.php b/lib/internal/Magento/Framework/Pricing/PriceCurrencyInterface.php index ab3d26f3e0c6c..aea2aacef5edf 100644 --- a/lib/internal/Magento/Framework/Pricing/PriceCurrencyInterface.php +++ b/lib/internal/Magento/Framework/Pricing/PriceCurrencyInterface.php @@ -10,6 +10,7 @@ * Interface PriceCurrencyInterface * * @api + * @since 100.0.2 */ interface PriceCurrencyInterface { @@ -75,7 +76,7 @@ public function convertAndFormat( /** * Round price * - * @deprecated + * @deprecated 102.0.1 * @param float $price * @return float */ diff --git a/lib/internal/Magento/Framework/Pricing/PriceInfo/Base.php b/lib/internal/Magento/Framework/Pricing/PriceInfo/Base.php index ac8c301e24a5c..31b343c74e272 100644 --- a/lib/internal/Magento/Framework/Pricing/PriceInfo/Base.php +++ b/lib/internal/Magento/Framework/Pricing/PriceInfo/Base.php @@ -17,6 +17,7 @@ * Price info base model * * @api + * @since 100.0.2 */ class Base implements PriceInfoInterface { diff --git a/lib/internal/Magento/Framework/Pricing/PriceInfo/Factory.php b/lib/internal/Magento/Framework/Pricing/PriceInfo/Factory.php index e3e4af56f6095..8739520016764 100644 --- a/lib/internal/Magento/Framework/Pricing/PriceInfo/Factory.php +++ b/lib/internal/Magento/Framework/Pricing/PriceInfo/Factory.php @@ -16,6 +16,7 @@ * Price info model factory * * @api + * @since 100.0.2 */ class Factory { diff --git a/lib/internal/Magento/Framework/Pricing/PriceInfoInterface.php b/lib/internal/Magento/Framework/Pricing/PriceInfoInterface.php index 259a8a4d8c2d1..80620008c77ae 100644 --- a/lib/internal/Magento/Framework/Pricing/PriceInfoInterface.php +++ b/lib/internal/Magento/Framework/Pricing/PriceInfoInterface.php @@ -13,6 +13,7 @@ * Price info model interface * * @api + * @since 100.0.2 */ interface PriceInfoInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Render.php b/lib/internal/Magento/Framework/Pricing/Render.php index 2159fcaeaa121..0784a09191380 100644 --- a/lib/internal/Magento/Framework/Pricing/Render.php +++ b/lib/internal/Magento/Framework/Pricing/Render.php @@ -19,6 +19,7 @@ * @method string getPriceRenderHandle() * * @api + * @since 100.0.2 */ class Render extends AbstractBlock { diff --git a/lib/internal/Magento/Framework/Pricing/Render/AdjustmentRenderInterface.php b/lib/internal/Magento/Framework/Pricing/Render/AdjustmentRenderInterface.php index 3be51eb83b911..a9bfb84bcaded 100644 --- a/lib/internal/Magento/Framework/Pricing/Render/AdjustmentRenderInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Render/AdjustmentRenderInterface.php @@ -13,6 +13,7 @@ * Adjustment render interface * * @api + * @since 100.0.2 */ interface AdjustmentRenderInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Render/AmountRenderInterface.php b/lib/internal/Magento/Framework/Pricing/Render/AmountRenderInterface.php index 724593e269944..38acbe37e71db 100644 --- a/lib/internal/Magento/Framework/Pricing/Render/AmountRenderInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Render/AmountRenderInterface.php @@ -14,6 +14,7 @@ * Price amount renderer interface * * @api + * @since 100.0.2 */ interface AmountRenderInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Render/PriceBoxRenderInterface.php b/lib/internal/Magento/Framework/Pricing/Render/PriceBoxRenderInterface.php index 9c23c594a89fb..68a246a36c8dc 100644 --- a/lib/internal/Magento/Framework/Pricing/Render/PriceBoxRenderInterface.php +++ b/lib/internal/Magento/Framework/Pricing/Render/PriceBoxRenderInterface.php @@ -14,6 +14,7 @@ * Price box render interface * * @api + * @since 100.0.2 */ interface PriceBoxRenderInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Render/RendererPool.php b/lib/internal/Magento/Framework/Pricing/Render/RendererPool.php index 73efef2e477be..a32273f51c8f4 100644 --- a/lib/internal/Magento/Framework/Pricing/Render/RendererPool.php +++ b/lib/internal/Magento/Framework/Pricing/Render/RendererPool.php @@ -13,6 +13,7 @@ /** * @api + * @since 100.0.2 */ class RendererPool extends AbstractBlock { diff --git a/lib/internal/Magento/Framework/Pricing/SaleableInterface.php b/lib/internal/Magento/Framework/Pricing/SaleableInterface.php index 240012c0b18e1..9d7959ff80fea 100644 --- a/lib/internal/Magento/Framework/Pricing/SaleableInterface.php +++ b/lib/internal/Magento/Framework/Pricing/SaleableInterface.php @@ -10,6 +10,7 @@ * Interface SaleableInterface * * @api + * @since 100.0.2 */ interface SaleableInterface { diff --git a/lib/internal/Magento/Framework/Pricing/Test/Unit/Adjustment/CollectionTest.php b/lib/internal/Magento/Framework/Pricing/Test/Unit/Adjustment/CollectionTest.php index fc65eabb33d62..ae0e006de59a2 100644 --- a/lib/internal/Magento/Framework/Pricing/Test/Unit/Adjustment/CollectionTest.php +++ b/lib/internal/Magento/Framework/Pricing/Test/Unit/Adjustment/CollectionTest.php @@ -50,6 +50,7 @@ protected function setUp(): void 'adj3' => $adj3, 'adj4' => $adj4, ]; + $this->adjustmentsData = $adjustmentsData; /** @var Pool|MockObject $adjustmentPool */ $adjustmentPool = $this->getMockBuilder(Pool::class) @@ -64,9 +65,7 @@ function ($code) use ($adjustmentsData) { return $adjustmentsData[$code]; } ); - $this->adjustmentPool = $adjustmentPool; - $this->adjustmentsData = $adjustmentsData; } /** @@ -108,7 +107,7 @@ public function testGetItemByCode($adjustments, $code, $expectedResult) $item = $collection->getItemByCode($code); - $this->assertEquals($expectedResult, $item->getAdjustmentCode()); + $this->assertEquals($expectedResult, $item->getSortOrder()); } /** @@ -117,11 +116,11 @@ public function testGetItemByCode($adjustments, $code, $expectedResult) public function getItemByCodeDataProvider() { return [ - [['adj1'], 'adj1', $this->adjustmentsData['adj1']], - [['adj1', 'adj2', 'adj3', 'adj4'], 'adj1', $this->adjustmentsData['adj1']], - [['adj1', 'adj2', 'adj3', 'adj4'], 'adj2', $this->adjustmentsData['adj2']], - [['adj1', 'adj2', 'adj3', 'adj4'], 'adj3', $this->adjustmentsData['adj3']], - [['adj1', 'adj2', 'adj3', 'adj4'], 'adj4', $this->adjustmentsData['adj4']], + [['adj1'], 'adj1', 10], + [['adj1', 'adj2', 'adj3', 'adj4'], 'adj1', 10], + [['adj1', 'adj2', 'adj3', 'adj4'], 'adj2', 20], + [['adj1', 'adj2', 'adj3', 'adj4'], 'adj3', 5], + [['adj1', 'adj2', 'adj3', 'adj4'], 'adj4', Pool::DEFAULT_SORT_ORDER], ]; } diff --git a/lib/internal/Magento/Framework/Profiler.php b/lib/internal/Magento/Framework/Profiler.php index eaf0a732f98c0..4fafc68a568db 100644 --- a/lib/internal/Magento/Framework/Profiler.php +++ b/lib/internal/Magento/Framework/Profiler.php @@ -12,6 +12,7 @@ /** * @api + * @since 100.0.2 */ class Profiler { diff --git a/lib/internal/Magento/Framework/Profiler/DriverInterface.php b/lib/internal/Magento/Framework/Profiler/DriverInterface.php index fe6393b3a43e3..4230b69dab917 100644 --- a/lib/internal/Magento/Framework/Profiler/DriverInterface.php +++ b/lib/internal/Magento/Framework/Profiler/DriverInterface.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ interface DriverInterface { diff --git a/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php b/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php index 2f3caf08c534e..43b8cc22ebf2c 100644 --- a/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php +++ b/lib/internal/Magento/Framework/Reflection/DataObjectProcessor.php @@ -12,6 +12,7 @@ * Data object processor for array serialization using class reflection * * @api + * @since 100.0.2 */ class DataObjectProcessor { diff --git a/lib/internal/Magento/Framework/Reflection/MethodsMap.php b/lib/internal/Magento/Framework/Reflection/MethodsMap.php index c4a738ac28caa..0b5ab78a504da 100644 --- a/lib/internal/Magento/Framework/Reflection/MethodsMap.php +++ b/lib/internal/Magento/Framework/Reflection/MethodsMap.php @@ -239,7 +239,7 @@ public function isMethodReturnValueRequired($type, $methodName) * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/Registry.php b/lib/internal/Magento/Framework/Registry.php index d1bc227437d5f..e798b28e1e1b2 100644 --- a/lib/internal/Magento/Framework/Registry.php +++ b/lib/internal/Magento/Framework/Registry.php @@ -12,7 +12,8 @@ * It's usage should be avoid. Use service classes or data providers instead. * * @api - * @deprecated + * @deprecated 102.0.0 + * @since 100.0.2 */ class Registry { @@ -29,7 +30,7 @@ class Registry * @param string $key * @return mixed * - * @deprecated + * @deprecated 102.0.0 */ public function registry($key) { @@ -48,7 +49,7 @@ public function registry($key) * @return void * @throws \RuntimeException * - * @deprecated + * @deprecated 102.0.0 */ public function register($key, $value, $graceful = false) { @@ -67,7 +68,7 @@ public function register($key, $value, $graceful = false) * @param string $key * @return void * - * @deprecated + * @deprecated 102.0.0 */ public function unregister($key) { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php index e56559563c35a..8dffc679b5c7f 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/ConditionManager.php @@ -12,8 +12,9 @@ * MySQL search condition manager * * @api - * @deprecated + * @deprecated 102.0.0 * @see \Magento\ElasticSearch + * @since 100.0.2 */ class ConditionManager { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php index 7f8ef8c422b92..1d29f055a464b 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorage.php @@ -16,8 +16,9 @@ * MySQL search temporary storage. * * @api - * @deprecated + * @deprecated 102.0.0 * @see \Magento\ElasticSearch + * @since 100.0.2 */ class TemporaryStorage { diff --git a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php index 208f6b39b9eb4..f1fc487a91ca0 100644 --- a/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php +++ b/lib/internal/Magento/Framework/Search/Adapter/Mysql/TemporaryStorageFactory.php @@ -12,8 +12,9 @@ * * @codeCoverageIgnore * @api - * @deprecated + * @deprecated 102.0.0 * @see \Magento\ElasticSearch + * @since 100.0.2 */ class TemporaryStorageFactory { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm.php b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm.php index ffe24fa324cee..baee7221a7f17 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm.php @@ -10,6 +10,7 @@ * * @author Magento Core Team <core@magentocommerce.com> * @api + * @since 100.0.2 */ class Algorithm { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php index a3e9ed61824ed..c4f6c67200b2b 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Improved.php @@ -44,7 +44,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc */ public function getItems( BucketInterface $bucket, @@ -64,13 +64,12 @@ public function getItems( $aggregations['count'] ); - $this->algorithm->setLimits($aggregations['min'], $aggregations['max'] + 0.01); + $this->algorithm->setLimits($aggregations['min'], $aggregations['max']); $interval = $this->dataProvider->getInterval($bucket, $dimensions, $entityStorage); $data = $this->algorithm->calculateSeparators($interval); - $data[0]['from'] = ''; // We should not calculate min and max value - $data[count($data) - 1]['to'] = ''; + $data[0]['from'] = 0; $dataSize = count($data); for ($key = 0; $key < $dataSize; $key++) { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Repository.php b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Repository.php index c2d7e4025b3b3..bfba3130c6ae8 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Repository.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/Algorithm/Repository.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Repository { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/DataProviderFactory.php b/lib/internal/Magento/Framework/Search/Dynamic/DataProviderFactory.php index 6c3a59152a9a1..ccfd5ec01dee3 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/DataProviderFactory.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/DataProviderFactory.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class DataProviderFactory { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/DataProviderInterface.php b/lib/internal/Magento/Framework/Search/Dynamic/DataProviderInterface.php index fa360134663d6..374facf486ad9 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/DataProviderInterface.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/DataProviderInterface.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ interface DataProviderInterface { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/EntityStorage.php b/lib/internal/Magento/Framework/Search/Dynamic/EntityStorage.php index e32096704025f..7500ab622b65b 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/EntityStorage.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/EntityStorage.php @@ -8,6 +8,7 @@ /** * @api + * @since 100.0.2 */ class EntityStorage { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/EntityStorageFactory.php b/lib/internal/Magento/Framework/Search/Dynamic/EntityStorageFactory.php index 96e6b0113d1a7..6b7a4cd9a6df6 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/EntityStorageFactory.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/EntityStorageFactory.php @@ -10,6 +10,7 @@ /** * EntityStorage Factory * @api + * @since 100.0.2 */ class EntityStorageFactory { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/IntervalFactory.php b/lib/internal/Magento/Framework/Search/Dynamic/IntervalFactory.php index 90e66d1e7330c..261a52c27e850 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/IntervalFactory.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/IntervalFactory.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class IntervalFactory { diff --git a/lib/internal/Magento/Framework/Search/Dynamic/IntervalInterface.php b/lib/internal/Magento/Framework/Search/Dynamic/IntervalInterface.php index 547166eb175ba..f80cac17d8dc6 100644 --- a/lib/internal/Magento/Framework/Search/Dynamic/IntervalInterface.php +++ b/lib/internal/Magento/Framework/Search/Dynamic/IntervalInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface IntervalInterface { diff --git a/lib/internal/Magento/Framework/Search/EngineResolverInterface.php b/lib/internal/Magento/Framework/Search/EngineResolverInterface.php index 57ba210121bc4..47dfa6659cc25 100644 --- a/lib/internal/Magento/Framework/Search/EngineResolverInterface.php +++ b/lib/internal/Magento/Framework/Search/EngineResolverInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 102.0.0 */ interface EngineResolverInterface { @@ -16,6 +17,7 @@ interface EngineResolverInterface * It returns string identifier of Search Engine that is currently chosen in configuration * * @return string + * @since 102.0.0 */ public function getCurrentSearchEngine(); } diff --git a/lib/internal/Magento/Framework/Search/EntityMetadata.php b/lib/internal/Magento/Framework/Search/EntityMetadata.php index 8d25921813df3..9449396942c5a 100644 --- a/lib/internal/Magento/Framework/Search/EntityMetadata.php +++ b/lib/internal/Magento/Framework/Search/EntityMetadata.php @@ -9,6 +9,7 @@ /** * Entity metadata * @api + * @since 100.0.2 */ class EntityMetadata { diff --git a/lib/internal/Magento/Framework/Search/Request.php b/lib/internal/Magento/Framework/Search/Request.php index 264d4929dde56..54d9f12fcb82f 100644 --- a/lib/internal/Magento/Framework/Search/Request.php +++ b/lib/internal/Magento/Framework/Search/Request.php @@ -14,6 +14,7 @@ * * @codeCoverageIgnore * @api + * @since 100.0.2 */ class Request implements RequestInterface { @@ -151,8 +152,9 @@ public function getSize() * 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 + * @deprecated 102.0.2 * @return array + * @since 102.0.2 */ public function getSort() { diff --git a/lib/internal/Magento/Framework/Search/Request/Aggregation/DynamicBucket.php b/lib/internal/Magento/Framework/Search/Request/Aggregation/DynamicBucket.php index dfa28911ca4ce..8f691b695a017 100644 --- a/lib/internal/Magento/Framework/Search/Request/Aggregation/DynamicBucket.php +++ b/lib/internal/Magento/Framework/Search/Request/Aggregation/DynamicBucket.php @@ -10,6 +10,7 @@ /** * Dynamic Buckets * @api + * @since 100.0.2 */ class DynamicBucket implements BucketInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Binder.php b/lib/internal/Magento/Framework/Search/Request/Binder.php index 9df1ee87eb869..5016c1c11926c 100644 --- a/lib/internal/Magento/Framework/Search/Request/Binder.php +++ b/lib/internal/Magento/Framework/Search/Request/Binder.php @@ -9,6 +9,7 @@ * Data binder for search request. * * @api + * @since 100.0.2 */ class Binder { diff --git a/lib/internal/Magento/Framework/Search/Request/BucketInterface.php b/lib/internal/Magento/Framework/Search/Request/BucketInterface.php index 8da797e5a9767..1512a8a68cf78 100644 --- a/lib/internal/Magento/Framework/Search/Request/BucketInterface.php +++ b/lib/internal/Magento/Framework/Search/Request/BucketInterface.php @@ -9,6 +9,7 @@ * Aggregation Bucket Interface * * @api + * @since 100.0.2 */ interface BucketInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Builder.php b/lib/internal/Magento/Framework/Search/Request/Builder.php index 0cf959b657c76..b3e1a7f2e309b 100644 --- a/lib/internal/Magento/Framework/Search/Request/Builder.php +++ b/lib/internal/Magento/Framework/Search/Request/Builder.php @@ -16,6 +16,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Builder { @@ -104,6 +105,7 @@ public function setFrom($from) * * @param \Magento\Framework\Api\SortOrder[] $sort * @return $this + * @since 102.0.2 */ public function setSort($sort) { diff --git a/lib/internal/Magento/Framework/Search/Request/Cleaner.php b/lib/internal/Magento/Framework/Search/Request/Cleaner.php index c015f90751a23..59eeea47fe278 100644 --- a/lib/internal/Magento/Framework/Search/Request/Cleaner.php +++ b/lib/internal/Magento/Framework/Search/Request/Cleaner.php @@ -12,6 +12,7 @@ /** * @api + * @since 100.0.2 */ class Cleaner { diff --git a/lib/internal/Magento/Framework/Search/Request/Dimension.php b/lib/internal/Magento/Framework/Search/Request/Dimension.php index bc60af8c16959..6df1974cc61cc 100644 --- a/lib/internal/Magento/Framework/Search/Request/Dimension.php +++ b/lib/internal/Magento/Framework/Search/Request/Dimension.php @@ -10,6 +10,7 @@ /** * Search Request Dimension * @api + * @since 100.0.2 */ class Dimension extends AbstractKeyValuePair { diff --git a/lib/internal/Magento/Framework/Search/Request/Filter/BoolExpression.php b/lib/internal/Magento/Framework/Search/Request/Filter/BoolExpression.php index 5a1d2ad528237..6b0304ae37758 100644 --- a/lib/internal/Magento/Framework/Search/Request/Filter/BoolExpression.php +++ b/lib/internal/Magento/Framework/Search/Request/Filter/BoolExpression.php @@ -10,6 +10,7 @@ /** * Bool Filter * @api + * @since 100.0.2 */ class BoolExpression implements FilterInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Filter/Range.php b/lib/internal/Magento/Framework/Search/Request/Filter/Range.php index 1be0c690483ab..981c92203b864 100644 --- a/lib/internal/Magento/Framework/Search/Request/Filter/Range.php +++ b/lib/internal/Magento/Framework/Search/Request/Filter/Range.php @@ -11,6 +11,7 @@ * Range Filter * @SuppressWarnings(PHPMD.ShortVariable) * @api + * @since 100.0.2 */ class Range implements FilterInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Filter/Term.php b/lib/internal/Magento/Framework/Search/Request/Filter/Term.php index 5560c92164db4..50fd658c6760b 100644 --- a/lib/internal/Magento/Framework/Search/Request/Filter/Term.php +++ b/lib/internal/Magento/Framework/Search/Request/Filter/Term.php @@ -11,6 +11,7 @@ /** * Term Filter * @api + * @since 100.0.2 */ class Term extends AbstractKeyValuePair implements FilterInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Filter/Wildcard.php b/lib/internal/Magento/Framework/Search/Request/Filter/Wildcard.php index 60b5463132003..d1b487d498935 100644 --- a/lib/internal/Magento/Framework/Search/Request/Filter/Wildcard.php +++ b/lib/internal/Magento/Framework/Search/Request/Filter/Wildcard.php @@ -11,6 +11,7 @@ /** * Wildcard Filter * @api + * @since 100.0.2 */ class Wildcard extends AbstractKeyValuePair implements FilterInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/FilterInterface.php b/lib/internal/Magento/Framework/Search/Request/FilterInterface.php index 14fdd2929b15d..928f4121a84cd 100644 --- a/lib/internal/Magento/Framework/Search/Request/FilterInterface.php +++ b/lib/internal/Magento/Framework/Search/Request/FilterInterface.php @@ -9,6 +9,7 @@ * Filter Interface * * @api + * @since 100.0.2 */ interface FilterInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Mapper.php b/lib/internal/Magento/Framework/Search/Request/Mapper.php index 7586444d4b197..3cbcc618c4438 100644 --- a/lib/internal/Magento/Framework/Search/Request/Mapper.php +++ b/lib/internal/Magento/Framework/Search/Request/Mapper.php @@ -13,6 +13,7 @@ /** * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * @since 100.0.2 */ class Mapper { diff --git a/lib/internal/Magento/Framework/Search/Request/Query/BoolExpression.php b/lib/internal/Magento/Framework/Search/Request/Query/BoolExpression.php index 1ac3ba6c09b3b..c47a020b80580 100644 --- a/lib/internal/Magento/Framework/Search/Request/Query/BoolExpression.php +++ b/lib/internal/Magento/Framework/Search/Request/Query/BoolExpression.php @@ -10,6 +10,7 @@ /** * Bool Query * @api + * @since 100.0.2 */ class BoolExpression implements QueryInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Query/Filter.php b/lib/internal/Magento/Framework/Search/Request/Query/Filter.php index 7de25175793e4..850dbfd40a1fe 100644 --- a/lib/internal/Magento/Framework/Search/Request/Query/Filter.php +++ b/lib/internal/Magento/Framework/Search/Request/Query/Filter.php @@ -10,6 +10,7 @@ /** * Term Query * @api + * @since 100.0.2 */ class Filter implements QueryInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/Query/Match.php b/lib/internal/Magento/Framework/Search/Request/Query/Match.php index a1f884dbc106e..8fbd5244780bc 100644 --- a/lib/internal/Magento/Framework/Search/Request/Query/Match.php +++ b/lib/internal/Magento/Framework/Search/Request/Query/Match.php @@ -10,6 +10,7 @@ /** * Match Query * @api + * @since 100.0.2 */ class Match implements QueryInterface { diff --git a/lib/internal/Magento/Framework/Search/Request/QueryInterface.php b/lib/internal/Magento/Framework/Search/Request/QueryInterface.php index e7cf56172cbaf..bf8a9582f5808 100644 --- a/lib/internal/Magento/Framework/Search/Request/QueryInterface.php +++ b/lib/internal/Magento/Framework/Search/Request/QueryInterface.php @@ -9,6 +9,7 @@ * Query Interface * * @api + * @since 100.0.2 */ interface QueryInterface { diff --git a/lib/internal/Magento/Framework/Search/RequestInterface.php b/lib/internal/Magento/Framework/Search/RequestInterface.php index 16df80f755c07..d5879ce0dc22e 100644 --- a/lib/internal/Magento/Framework/Search/RequestInterface.php +++ b/lib/internal/Magento/Framework/Search/RequestInterface.php @@ -13,6 +13,7 @@ * Search Request * * @api + * @since 100.0.2 */ interface RequestInterface { diff --git a/lib/internal/Magento/Framework/Search/Response/Aggregation.php b/lib/internal/Magento/Framework/Search/Response/Aggregation.php index ea72597c53034..7ebe052b65ba8 100644 --- a/lib/internal/Magento/Framework/Search/Response/Aggregation.php +++ b/lib/internal/Magento/Framework/Search/Response/Aggregation.php @@ -11,6 +11,7 @@ /** * Faceted data * @api + * @since 100.0.2 */ class Aggregation implements AggregationInterface, \IteratorAggregate { diff --git a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php index 00b1ed2149bec..d7d0a8d03b28c 100644 --- a/lib/internal/Magento/Framework/Search/Response/QueryResponse.php +++ b/lib/internal/Magento/Framework/Search/Response/QueryResponse.php @@ -12,6 +12,7 @@ /** * Search Response * @api + * @since 100.0.2 */ class QueryResponse implements ResponseInterface { @@ -80,9 +81,10 @@ public function getAggregations() * 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 + * @deprecated 102.0.2 * * @return int + * @since 102.0.2 */ public function getTotal(): int { diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php index 7ce9756ff243d..d5a5aec9259dd 100644 --- a/lib/internal/Magento/Framework/Serialize/Serializer/Json.php +++ b/lib/internal/Magento/Framework/Serialize/Serializer/Json.php @@ -11,13 +11,13 @@ * Serialize data to JSON, unserialize JSON encoded data * * @api - * @since 100.2.0 + * @since 101.0.0 */ class Json implements SerializerInterface { /** * @inheritDoc - * @since 100.2.0 + * @since 101.0.0 */ public function serialize($data) { @@ -30,7 +30,7 @@ public function serialize($data) /** * @inheritDoc - * @since 100.2.0 + * @since 101.0.0 */ public function unserialize($string) { diff --git a/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php index 4a5406ff3fd99..dc63a726481ae 100644 --- a/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php +++ b/lib/internal/Magento/Framework/Serialize/Serializer/JsonHexTag.php @@ -16,13 +16,13 @@ * unserialize JSON encoded data * * @api - * @since 100.2.0 + * @since 102.0.1 */ class JsonHexTag extends Json implements SerializerInterface { /** * @inheritDoc - * @since 100.2.0 + * @since 102.0.1 */ public function serialize($data): string { diff --git a/lib/internal/Magento/Framework/Serialize/SerializerInterface.php b/lib/internal/Magento/Framework/Serialize/SerializerInterface.php index 8b448f3ba246b..24ace0c38b84e 100644 --- a/lib/internal/Magento/Framework/Serialize/SerializerInterface.php +++ b/lib/internal/Magento/Framework/Serialize/SerializerInterface.php @@ -9,7 +9,7 @@ * Interface for serializing * * @api - * @since 100.2.0 + * @since 101.0.0 */ interface SerializerInterface { @@ -19,7 +19,7 @@ interface SerializerInterface * @param string|int|float|bool|array|null $data * @return string|bool * @throws \InvalidArgumentException - * @since 100.2.0 + * @since 101.0.0 */ public function serialize($data); @@ -29,7 +29,7 @@ public function serialize($data); * @param string $string * @return string|int|float|bool|array|null * @throws \InvalidArgumentException - * @since 100.2.0 + * @since 101.0.0 */ public function unserialize($string); } diff --git a/lib/internal/Magento/Framework/Session/Generic.php b/lib/internal/Magento/Framework/Session/Generic.php index 8cd358dd56416..794ce7a64e88b 100644 --- a/lib/internal/Magento/Framework/Session/Generic.php +++ b/lib/internal/Magento/Framework/Session/Generic.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class Generic extends SessionManager { diff --git a/lib/internal/Magento/Framework/Session/SessionManager.php b/lib/internal/Magento/Framework/Session/SessionManager.php index b96925facf528..7e43a9f2d99c0 100644 --- a/lib/internal/Magento/Framework/Session/SessionManager.php +++ b/lib/internal/Magento/Framework/Session/SessionManager.php @@ -10,7 +10,8 @@ use Magento\Framework\Session\Config\ConfigInterface; /** - * Session Manager + * Standard session management. + * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.CookieAndSessionMisuse) */ @@ -200,9 +201,6 @@ public function start() session_commit(); session_id($_SESSION['new_session_id']); } - $sid = $this->sidResolver->getSid($this); - // potential custom logic for session id (ex. switching between hosts) - $this->setSessionId($sid); session_start(); if (isset($_SESSION['destroyed']) && $_SESSION['destroyed'] < time() - $this->sessionConfig->getCookieLifetime() @@ -211,7 +209,7 @@ public function start() } $this->validator->validate($this); - $this->renewCookie($sid); + $this->renewCookie(null); register_shutdown_function([$this, 'writeClose']); diff --git a/lib/internal/Magento/Framework/Session/SessionManagerInterface.php b/lib/internal/Magento/Framework/Session/SessionManagerInterface.php index 93fac2947eb61..70976a6269d18 100644 --- a/lib/internal/Magento/Framework/Session/SessionManagerInterface.php +++ b/lib/internal/Magento/Framework/Session/SessionManagerInterface.php @@ -11,6 +11,7 @@ * Session Manager Interface * * @api + * @since 100.0.2 */ interface SessionManagerInterface { diff --git a/lib/internal/Magento/Framework/Session/SidResolver.php b/lib/internal/Magento/Framework/Session/SidResolver.php index fd7af781981a9..4052f039a2f93 100644 --- a/lib/internal/Magento/Framework/Session/SidResolver.php +++ b/lib/internal/Magento/Framework/Session/SidResolver.php @@ -12,7 +12,7 @@ /** * Resolves SID by processing request parameters. * - * @deprecated 2.3.3 SIDs in URLs are no longer used + * @deprecated 102.0.2 SIDs in URLs are no longer used */ class SidResolver implements SidResolverInterface { @@ -28,13 +28,13 @@ class SidResolver implements SidResolverInterface /** * @var \Magento\Framework\UrlInterface - * @deprecated Not used anymore. + * @deprecated 102.0.5 Not used anymore. */ protected $urlBuilder; /** * @var \Magento\Framework\App\RequestInterface - * @deprecated Not used anymore. + * @deprecated 102.0.5 Not used anymore. */ protected $request; @@ -56,7 +56,7 @@ class SidResolver implements SidResolverInterface * @var bool|null * @see \Magento\Framework\UrlInterface */ - protected $_useSessionInUrl; + protected $_useSessionInUrl = false; /** * @var string @@ -88,26 +88,21 @@ public function __construct( } /** - * Get Sid - * - * @param SessionManagerInterface $session - * @return string|null - * @throws \Magento\Framework\Exception\LocalizedException - * @SuppressWarnings(PHPMD.UnusedFormalParameter) + * @inheritDoc */ public function getSid(SessionManagerInterface $session) { + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return null; } /** - * Get session id query param - * - * @param SessionManagerInterface $session - * @return string + * @inheritDoc */ public function getSessionIdQueryParam(SessionManagerInterface $session) { + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); $sessionName = $session->getName(); if ($sessionName && isset($this->sidNameMap[$sessionName])) { return $this->sidNameMap[$sessionName]; @@ -116,57 +111,42 @@ public function getSessionIdQueryParam(SessionManagerInterface $session) } /** - * Set use session var instead of SID for URL - * - * @param bool $var - * @return $this + * @inheritDoc */ public function setUseSessionVar($var) { - $this->_useSessionVar = (bool)$var; + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return $this; } /** - * Retrieve use flag session var instead of SID for URL - * - * @return bool - * @SuppressWarnings(PHPMD.BooleanGetMethodName) + * @inheritDoc */ public function getUseSessionVar() { - return $this->_useSessionVar; + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + + return false; } /** - * Set Use session in URL flag - * - * @param bool $flag - * @return $this + * @inheritDoc */ public function setUseSessionInUrl($flag = true) { - $this->_useSessionInUrl = (bool)$flag; + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return $this; } /** - * Retrieve use session in URL flag. - * - * @return bool - * @SuppressWarnings(PHPMD.BooleanGetMethodName) + * @inheritDoc */ public function getUseSessionInUrl() { - if ($this->_useSessionInUrl === null) { - //Using config value by default, can be overridden by using the - //setter. - $this->_useSessionInUrl = $this->scopeConfig->isSetFlag( - self::XML_PATH_USE_FRONTEND_SID, - $this->_scopeType - ); - } + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); - return $this->_useSessionInUrl; + return false; } } diff --git a/lib/internal/Magento/Framework/Session/SidResolverInterface.php b/lib/internal/Magento/Framework/Session/SidResolverInterface.php index 6f242c1c9e308..3c30921e7bce3 100644 --- a/lib/internal/Magento/Framework/Session/SidResolverInterface.php +++ b/lib/internal/Magento/Framework/Session/SidResolverInterface.php @@ -10,6 +10,7 @@ /** * Interface \Magento\Framework\Session\SidResolverInterface * + * @deprecated 2.3.3 SIDs in URLs are no longer used */ interface SidResolverInterface { @@ -23,6 +24,7 @@ interface SidResolverInterface * * @param \Magento\Framework\Session\SessionManagerInterface $session * @return string|null + * @deprecated SID query parameter is not used in URLs anymore. */ public function getSid(\Magento\Framework\Session\SessionManagerInterface $session); @@ -31,6 +33,7 @@ public function getSid(\Magento\Framework\Session\SessionManagerInterface $sessi * * @param \Magento\Framework\Session\SessionManagerInterface $session * @return string + * @deprecated SID query parameter is not used in URLs anymore. */ public function getSessionIdQueryParam(\Magento\Framework\Session\SessionManagerInterface $session); @@ -39,6 +42,7 @@ public function getSessionIdQueryParam(\Magento\Framework\Session\SessionManager * * @param bool $var * @return $this + * @deprecated SID query parameter is not used in URLs anymore. */ public function setUseSessionVar($var); @@ -55,6 +59,7 @@ public function getUseSessionVar(); * * @param bool $flag * @return $this + * @deprecated SID query parameter is not used in URLs anymore. */ public function setUseSessionInUrl($flag = true); @@ -62,6 +67,7 @@ public function setUseSessionInUrl($flag = true); * Retrieve use session in URL flag * * @return bool + * @deprecated SID query parameter is not used in URLs anymore. * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ public function getUseSessionInUrl(); diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/Diff.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/Diff.php index 0e857567689c4..578b3c62be573 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/Diff.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Diff/Diff.php @@ -23,6 +23,7 @@ * - new (Should be changed to) * - old () * @api + * @since 102.0.0 */ class Diff implements DiffInterface { @@ -40,6 +41,7 @@ class Diff implements DiffInterface * This changes created only for debug reasons. * * @var array + * @since 102.0.0 */ public $debugChanges; @@ -100,6 +102,7 @@ public function __construct( * All changes are sorted because there are dependencies between tables, like foreign keys. * * @inheritdoc + * @since 102.0.0 */ public function getAll() { @@ -115,6 +118,7 @@ public function getAll() * @param string $table * @param string $operation * @return ElementHistory[] + * @since 102.0.0 */ public function getChange($table, $operation) { @@ -159,6 +163,7 @@ private function getWhiteListTables() * @param ElementInterface | Table $object * @param string $operation * @return bool + * @since 102.0.0 */ public function canBeRegistered(ElementInterface $object, $operation): bool { @@ -202,6 +207,7 @@ private function isElementHaveAutoGeneratedName(ElementInterface $element): bool * * @param TableElementInterface $dtoObject * @inheritdoc + * @since 102.0.0 */ public function register( ElementInterface $dtoObject, diff --git a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/ElementInterface.php b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/ElementInterface.php index 43c4c5d9d516a..168b8c0861ad7 100644 --- a/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/ElementInterface.php +++ b/lib/internal/Magento/Framework/Setup/Declaration/Schema/Dto/ElementInterface.php @@ -11,6 +11,7 @@ * Is parent interface for all various schema structural elements: * table, column, constraint, index. * @api + * @since 102.0.0 */ interface ElementInterface { @@ -18,6 +19,7 @@ interface ElementInterface * Return name of structural element. * * @return string + * @since 102.0.0 */ public function getName(); @@ -25,6 +27,7 @@ public function getName(); * Retrieve element low level type: varchar, char, foreign key, etc.. * * @return string + * @since 102.0.0 */ public function getType(); @@ -36,6 +39,7 @@ public function getType(); * And in order to distinguish this types of elements we use this method. * * @return string + * @since 102.0.0 */ public function getElementType(); } diff --git a/lib/internal/Magento/Framework/Setup/InstallDataInterface.php b/lib/internal/Magento/Framework/Setup/InstallDataInterface.php index 68e08f658faaf..085f989a8bb82 100644 --- a/lib/internal/Magento/Framework/Setup/InstallDataInterface.php +++ b/lib/internal/Magento/Framework/Setup/InstallDataInterface.php @@ -9,6 +9,7 @@ * Interface for data installs of a module * * @api + * @since 100.0.2 */ interface InstallDataInterface { diff --git a/lib/internal/Magento/Framework/Setup/InstallSchemaInterface.php b/lib/internal/Magento/Framework/Setup/InstallSchemaInterface.php index abb0017b85e93..878f3e3e1a152 100644 --- a/lib/internal/Magento/Framework/Setup/InstallSchemaInterface.php +++ b/lib/internal/Magento/Framework/Setup/InstallSchemaInterface.php @@ -9,6 +9,7 @@ * Interface for DB schema installs of a module * * @api + * @since 100.0.2 */ interface InstallSchemaInterface { diff --git a/lib/internal/Magento/Framework/Setup/LoggerInterface.php b/lib/internal/Magento/Framework/Setup/LoggerInterface.php index 8f82cf8001127..169c2acfe9bf5 100644 --- a/lib/internal/Magento/Framework/Setup/LoggerInterface.php +++ b/lib/internal/Magento/Framework/Setup/LoggerInterface.php @@ -10,6 +10,7 @@ * Interface to Log Message in Setup * * @api + * @since 100.0.2 */ interface LoggerInterface { diff --git a/lib/internal/Magento/Framework/Setup/ModuleContextInterface.php b/lib/internal/Magento/Framework/Setup/ModuleContextInterface.php index 45c59f08bc691..d5d0b4e1efe59 100644 --- a/lib/internal/Magento/Framework/Setup/ModuleContextInterface.php +++ b/lib/internal/Magento/Framework/Setup/ModuleContextInterface.php @@ -8,6 +8,7 @@ /** * Context of a module being installed/updated: version, user data, etc. * @api + * @since 100.0.2 */ interface ModuleContextInterface { diff --git a/lib/internal/Magento/Framework/Setup/ModuleDataSetupInterface.php b/lib/internal/Magento/Framework/Setup/ModuleDataSetupInterface.php index bcf50e6ff9851..2bc55d13d2277 100644 --- a/lib/internal/Magento/Framework/Setup/ModuleDataSetupInterface.php +++ b/lib/internal/Magento/Framework/Setup/ModuleDataSetupInterface.php @@ -9,6 +9,7 @@ * DB data resource interface for a module * * @api + * @since 100.0.2 */ interface ModuleDataSetupInterface extends SetupInterface { diff --git a/lib/internal/Magento/Framework/Setup/SchemaSetupInterface.php b/lib/internal/Magento/Framework/Setup/SchemaSetupInterface.php index 48900c5cdc029..dd73c79069b1a 100644 --- a/lib/internal/Magento/Framework/Setup/SchemaSetupInterface.php +++ b/lib/internal/Magento/Framework/Setup/SchemaSetupInterface.php @@ -8,6 +8,7 @@ /** * DB schema resource interface * @api + * @since 100.0.2 */ interface SchemaSetupInterface extends SetupInterface { diff --git a/lib/internal/Magento/Framework/Setup/SetupInterface.php b/lib/internal/Magento/Framework/Setup/SetupInterface.php index 58d0387007045..434a3234ee318 100644 --- a/lib/internal/Magento/Framework/Setup/SetupInterface.php +++ b/lib/internal/Magento/Framework/Setup/SetupInterface.php @@ -9,6 +9,7 @@ * DB resource interface * * @api + * @since 100.0.2 */ interface SetupInterface { diff --git a/lib/internal/Magento/Framework/Setup/UninstallInterface.php b/lib/internal/Magento/Framework/Setup/UninstallInterface.php index 28e8104610559..72a2a9a37348a 100644 --- a/lib/internal/Magento/Framework/Setup/UninstallInterface.php +++ b/lib/internal/Magento/Framework/Setup/UninstallInterface.php @@ -9,6 +9,7 @@ * Interface for handling data removal during module uninstall * * @api + * @since 100.0.2 */ interface UninstallInterface { diff --git a/lib/internal/Magento/Framework/Setup/UpgradeDataInterface.php b/lib/internal/Magento/Framework/Setup/UpgradeDataInterface.php index f700157cab98e..93d94cce0dd99 100644 --- a/lib/internal/Magento/Framework/Setup/UpgradeDataInterface.php +++ b/lib/internal/Magento/Framework/Setup/UpgradeDataInterface.php @@ -9,6 +9,7 @@ * Interface for data upgrades of a module * * @api + * @since 100.0.2 */ interface UpgradeDataInterface { diff --git a/lib/internal/Magento/Framework/Setup/UpgradeSchemaInterface.php b/lib/internal/Magento/Framework/Setup/UpgradeSchemaInterface.php index 22c3b2110ec69..d7a3d56e55164 100644 --- a/lib/internal/Magento/Framework/Setup/UpgradeSchemaInterface.php +++ b/lib/internal/Magento/Framework/Setup/UpgradeSchemaInterface.php @@ -9,6 +9,7 @@ * Interface for DB schema upgrades of a module * * @api + * @since 100.0.2 */ interface UpgradeSchemaInterface { diff --git a/lib/internal/Magento/Framework/Shell/CommandRendererInterface.php b/lib/internal/Magento/Framework/Shell/CommandRendererInterface.php index 7460aaea74819..5632e32b790fe 100644 --- a/lib/internal/Magento/Framework/Shell/CommandRendererInterface.php +++ b/lib/internal/Magento/Framework/Shell/CommandRendererInterface.php @@ -9,6 +9,7 @@ * Shell command renderer * * @api + * @since 100.0.2 */ interface CommandRendererInterface { diff --git a/lib/internal/Magento/Framework/ShellInterface.php b/lib/internal/Magento/Framework/ShellInterface.php index 96607b8a072af..3c51916f06a23 100644 --- a/lib/internal/Magento/Framework/ShellInterface.php +++ b/lib/internal/Magento/Framework/ShellInterface.php @@ -9,6 +9,7 @@ * Shell command line wrapper encapsulates command execution and arguments escaping * * @api + * @since 100.0.2 */ interface ShellInterface { diff --git a/lib/internal/Magento/Framework/Simplexml/Config.php b/lib/internal/Magento/Framework/Simplexml/Config.php index 0d8711c5011c0..9f403e3451dcc 100644 --- a/lib/internal/Magento/Framework/Simplexml/Config.php +++ b/lib/internal/Magento/Framework/Simplexml/Config.php @@ -9,6 +9,7 @@ * Base class for simplexml based configurations * * @api + * @since 100.0.2 */ class Config { diff --git a/lib/internal/Magento/Framework/Simplexml/Element.php b/lib/internal/Magento/Framework/Simplexml/Element.php index 15602965de0a8..268eb8cd636e3 100644 --- a/lib/internal/Magento/Framework/Simplexml/Element.php +++ b/lib/internal/Magento/Framework/Simplexml/Element.php @@ -10,6 +10,7 @@ * Extends SimpleXML to add valuable functionality to \SimpleXMLElement class * * @api + * @since 100.0.2 */ class Element extends \SimpleXMLElement { diff --git a/lib/internal/Magento/Framework/Stdlib/ArrayUtils.php b/lib/internal/Magento/Framework/Stdlib/ArrayUtils.php index bd822a4942ba9..56243ae6b24cf 100644 --- a/lib/internal/Magento/Framework/Stdlib/ArrayUtils.php +++ b/lib/internal/Magento/Framework/Stdlib/ArrayUtils.php @@ -9,6 +9,7 @@ * Class ArrayUtils * * @api + * @since 100.0.2 */ class ArrayUtils { @@ -150,7 +151,7 @@ private function _decorateArrayObject($element, $key, $value, $isSkipped) * @param string $path The leading path * @param string $separator The path parts separator * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function flatten(array $data, $path = '', $separator = '/') { @@ -181,7 +182,7 @@ public function flatten(array $data, $path = '', $separator = '/') * @param array $originalArray The array to compare from * @param array $newArray The array to compare with * @return array Diff array - * @since 100.2.0 + * @since 101.0.0 */ public function recursiveDiff(array $originalArray, array $newArray) { diff --git a/lib/internal/Magento/Framework/Stdlib/BooleanUtils.php b/lib/internal/Magento/Framework/Stdlib/BooleanUtils.php index 83cf1a839f0ef..f55bc79ad6505 100644 --- a/lib/internal/Magento/Framework/Stdlib/BooleanUtils.php +++ b/lib/internal/Magento/Framework/Stdlib/BooleanUtils.php @@ -9,6 +9,7 @@ * Utility methods for the boolean data type * * @api + * @since 100.0.2 */ class BooleanUtils { @@ -71,7 +72,7 @@ public function toBoolean($value) * * @param mixed $value * @return mixed - * @since 100.2.0 + * @since 101.0.0 */ public function convert($value) { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php index 2b4cddf242113..112408bfddfdd 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadata.php @@ -8,6 +8,7 @@ /** * Class CookieMetadata * @api + * @since 100.0.2 */ class CookieMetadata { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadataFactory.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadataFactory.php index b60df1264863d..706c19e1e731a 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadataFactory.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieMetadataFactory.php @@ -11,6 +11,7 @@ /** * CookieMetadataFactory is used to construct SensitiveCookieMetadata and PublicCookieMetadata objects. * @api + * @since 100.0.2 */ class CookieMetadataFactory { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieReaderInterface.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieReaderInterface.php index 121c168483a6e..5b7c54159aed6 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieReaderInterface.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieReaderInterface.php @@ -9,6 +9,7 @@ /** * CookieReaderInterface provides the ability to read cookies sent in a request. * @api + * @since 100.0.2 */ interface CookieReaderInterface { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieScopeInterface.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieScopeInterface.php index b2870309ae5a5..d355579b4b07a 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieScopeInterface.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieScopeInterface.php @@ -8,6 +8,7 @@ /** * CookieScope is used to store default scope metadata. * @api + * @since 100.0.2 */ interface CookieScopeInterface { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieSizeLimitReachedException.php b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieSizeLimitReachedException.php index fabd3bbe2b0be..a74a4983cb5b8 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/CookieSizeLimitReachedException.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/CookieSizeLimitReachedException.php @@ -15,6 +15,7 @@ * set for the domain. * * @api + * @since 100.0.2 */ class CookieSizeLimitReachedException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/FailureToSendException.php b/lib/internal/Magento/Framework/Stdlib/Cookie/FailureToSendException.php index 681d6b43c3d04..ee9e798d258d7 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/FailureToSendException.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/FailureToSendException.php @@ -12,6 +12,7 @@ * impossible to send any cookie information back to the client. * * @api + * @since 100.0.2 */ class FailureToSendException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php index ef40ea94a6d08..14af3651834ad 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/PublicCookieMetadata.php @@ -10,6 +10,7 @@ * Class PublicCookieMetadata * * @api + * @since 100.0.2 */ class PublicCookieMetadata extends CookieMetadata { diff --git a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php index aab8e93160c8d..ed09481ab464e 100644 --- a/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php +++ b/lib/internal/Magento/Framework/Stdlib/Cookie/SensitiveCookieMetadata.php @@ -15,6 +15,7 @@ * as path and domain are only data to be exposed by SensitiveCookieMetadata * * @api + * @since 100.0.2 */ class SensitiveCookieMetadata extends CookieMetadata { diff --git a/lib/internal/Magento/Framework/Stdlib/CookieManagerInterface.php b/lib/internal/Magento/Framework/Stdlib/CookieManagerInterface.php index d4d37d656ebcf..fc357d26d8c36 100644 --- a/lib/internal/Magento/Framework/Stdlib/CookieManagerInterface.php +++ b/lib/internal/Magento/Framework/Stdlib/CookieManagerInterface.php @@ -22,6 +22,7 @@ * about how the cookie should be stored and whether JavaScript can access the cookie. * * @api + * @since 100.0.2 */ interface CookieManagerInterface extends CookieReaderInterface { diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime.php b/lib/internal/Magento/Framework/Stdlib/DateTime.php index 36db84860b373..2b828f4ba1d4e 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime.php @@ -10,6 +10,7 @@ * Internal dates * * @api + * @since 100.0.2 */ class DateTime { @@ -79,7 +80,7 @@ public function isEmptyDate($date) * @param int $time * @return string The given time in given format * - * @deprecated + * @deprecated 101.0.1 * @see Use Intl library for datetime handling: http://php.net/manual/en/book.intl.php * * @codeCoverageIgnore @@ -95,7 +96,7 @@ public function gmDate($format, $time) * @param string $timeStr * @return int * - * @deprecated + * @deprecated 101.0.1 * @see Use Intl library for datetime handling: http://php.net/manual/en/book.intl.php * * @codeCoverageIgnore diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/DateTime.php b/lib/internal/Magento/Framework/Stdlib/DateTime/DateTime.php index 646908f99693c..99f3a76b46d7d 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/DateTime.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/DateTime.php @@ -10,6 +10,7 @@ * Date conversion model * * @api + * @since 100.0.2 */ class DateTime { diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/DateTimeFormatterInterface.php b/lib/internal/Magento/Framework/Stdlib/DateTime/DateTimeFormatterInterface.php index a2d18215385fc..c1ec2695be114 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/DateTimeFormatterInterface.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/DateTimeFormatterInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface DateTimeFormatterInterface { diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/Date.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/Date.php index 1138dc2e60887..59920f1861072 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/Date.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/Date.php @@ -11,6 +11,7 @@ * Date filter. Converts date from localized to internal format. * * @api + * @since 100.0.2 */ class Date implements \Zend_Filter_Interface { diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/DateTime.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/DateTime.php index 0f2e81e6a9faf..05e9042a06a54 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/DateTime.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Filter/DateTime.php @@ -9,6 +9,7 @@ * Date/Time filter. Converts datetime from localized to internal format. * * @api + * @since 100.0.2 */ class DateTime extends Date { diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/Validator.php b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/Validator.php index 43cc83a702f6c..363fa54794bb4 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/Validator.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/Timezone/Validator.php @@ -11,6 +11,7 @@ /** * @api + * @since 100.0.2 */ class Validator { diff --git a/lib/internal/Magento/Framework/Stdlib/DateTime/TimezoneInterface.php b/lib/internal/Magento/Framework/Stdlib/DateTime/TimezoneInterface.php index d1ac24c84be9a..0ff6de9266c8a 100644 --- a/lib/internal/Magento/Framework/Stdlib/DateTime/TimezoneInterface.php +++ b/lib/internal/Magento/Framework/Stdlib/DateTime/TimezoneInterface.php @@ -11,6 +11,7 @@ /** * Timezone Interface * @api + * @since 100.0.2 */ interface TimezoneInterface { diff --git a/lib/internal/Magento/Framework/Stdlib/StringUtils.php b/lib/internal/Magento/Framework/Stdlib/StringUtils.php index 35decd7dab0a0..3e3072ee58677 100644 --- a/lib/internal/Magento/Framework/Stdlib/StringUtils.php +++ b/lib/internal/Magento/Framework/Stdlib/StringUtils.php @@ -9,6 +9,7 @@ * Magento methods to work with string * * @api + * @since 100.0.2 */ class StringUtils { diff --git a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php index edcd8dd5aaad6..97c7945a2aee3 100644 --- a/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/EscaperTest.php @@ -235,10 +235,12 @@ public function escapeHtmlDataProvider() 'allowedTags' => ['a'], ], 'text with allowed and not allowed tags, with allowed and not allowed attributes' => [ - 'data' => 'Some test <span>text in span tag</span> <strong>text in strong tag</strong> ' - . '<a type="some-type" href="http://domain.com/" onclick="alert(1)">Click here</a><script>alert(1)' + 'data' => 'Some test <span style="fine">text in span tag</span> <strong>text in strong tag</strong> ' + . '<a type="some-type" href="http://domain.com/" style="bad" onclick="alert(1)">' + . 'Click here</a><script>alert(1)' . '</script>', - 'expected' => 'Some test <span>text in span tag</span> text in strong tag <a href="http://domain.com/">' + 'expected' => 'Some test <span style="fine">text in span tag</span> text in strong tag ' + . '<a href="http://domain.com/">' . 'Click here</a>alert(1)', 'allowedTags' => ['a', 'span'], ], diff --git a/lib/internal/Magento/Framework/Test/Unit/UrlTest.php b/lib/internal/Magento/Framework/Test/Unit/UrlTest.php index eec3a3933f645..3df25149dcb74 100644 --- a/lib/internal/Magento/Framework/Test/Unit/UrlTest.php +++ b/lib/internal/Magento/Framework/Test/Unit/UrlTest.php @@ -199,17 +199,6 @@ public function getCurrentUrlProvider() ]; } - public function testGetUseSession() - { - $model = $this->getUrlModel(); - - $model->setUseSession(false); - $this->assertFalse((bool)$model->getUseSession()); - - $model->setUseSession(true); - $this->assertFalse($model->getUseSession()); - } - public function testGetBaseUrlNotLinkType() { $model = $this->getUrlModel( @@ -556,22 +545,6 @@ public function testGetRouteUrlWithValidUrl() $this->assertEquals('http://example.com', $model->getRouteUrl('http://example.com')); } - public function testAddSessionParam() - { - $model = $this->getUrlModel([ - 'session' => $this->sessionMock, - 'sidResolver' => $this->sidResolverMock, - 'queryParamsResolver' => $this->queryParamsResolverMock, - ]); - - $this->sidResolverMock->expects($this->never())->method('getSessionIdQueryParam')->with($this->sessionMock) - ->willReturn('sid'); - $this->sessionMock->expects($this->never())->method('getSessionId')->willReturn('session-id'); - $this->queryParamsResolverMock->expects($this->never())->method('setQueryParam')->with('sid', 'session-id'); - - $model->addSessionParam(); - } - /** * @param bool $result * @param string $referrer diff --git a/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php b/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php index 0ad3916156231..f8bb912ea6c85 100644 --- a/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php +++ b/lib/internal/Magento/Framework/TestFramework/Unit/Block/Adminhtml.php @@ -109,7 +109,7 @@ class Adminhtml extends \PHPUnit\Framework\TestCase /** */ - protected function setUp() + protected function setUp(): void { // These mocks are accessed via context $this->_designMock = $this->_makeMock(\Magento\Framework\View\DesignInterface::class); diff --git a/lib/internal/Magento/Framework/Translate.php b/lib/internal/Magento/Framework/Translate.php index e992482609a87..18675e19a4a96 100644 --- a/lib/internal/Magento/Framework/Translate.php +++ b/lib/internal/Magento/Framework/Translate.php @@ -461,7 +461,7 @@ private function getThemeTranslationFilesList($locale): array * @param string $locale * @return string * - * @deprecated + * @deprecated 102.0.1 * * @see \Magento\Framework\Translate::getThemeTranslationFilesList */ @@ -589,7 +589,7 @@ protected function _saveCache() * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/Translate/AdapterInterface.php b/lib/internal/Magento/Framework/Translate/AdapterInterface.php index bcd08a0b27c29..96591d3d32d53 100644 --- a/lib/internal/Magento/Framework/Translate/AdapterInterface.php +++ b/lib/internal/Magento/Framework/Translate/AdapterInterface.php @@ -10,6 +10,7 @@ * Magento translate adapter interface * * @api + * @since 100.0.2 */ interface AdapterInterface { diff --git a/lib/internal/Magento/Framework/Translate/Inline.php b/lib/internal/Magento/Framework/Translate/Inline.php index c04c240e73927..2ba27d9d5c5df 100644 --- a/lib/internal/Magento/Framework/Translate/Inline.php +++ b/lib/internal/Magento/Framework/Translate/Inline.php @@ -8,7 +8,24 @@ namespace Magento\Framework\Translate; -class Inline implements \Magento\Framework\Translate\InlineInterface +use Magento\Framework\App\Area; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\ScopeInterface; +use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\App\State; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Translate\Inline\ConfigInterface; +use Magento\Framework\Translate\Inline\ParserInterface; +use Magento\Framework\Translate\Inline\StateInterface; +use Magento\Framework\UrlInterface; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\LayoutInterface; + +/** + * Translate Inline Class + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ +class Inline implements InlineInterface { /** * Indicator to hold state of whether inline translation is allowed @@ -18,7 +35,7 @@ class Inline implements \Magento\Framework\Translate\InlineInterface protected $isAllowed; /** - * @var \Magento\Framework\Translate\Inline\ParserInterface + * @var ParserInterface */ protected $parser; @@ -30,22 +47,22 @@ class Inline implements \Magento\Framework\Translate\InlineInterface protected $isScriptInserted = false; /** - * @var \Magento\Framework\UrlInterface + * @var UrlInterface */ protected $url; /** - * @var \Magento\Framework\View\LayoutInterface + * @var LayoutInterface */ protected $layout; /** - * @var \Magento\Framework\Translate\Inline\ConfigInterface + * @var ConfigInterface */ protected $config; /** - * @var \Magento\Framework\App\ScopeResolverInterface + * @var ScopeResolverInterface */ protected $scopeResolver; @@ -70,28 +87,39 @@ class Inline implements \Magento\Framework\Translate\InlineInterface protected $state; /** - * Initialize inline translation model - * - * @param \Magento\Framework\App\ScopeResolverInterface $scopeResolver - * @param \Magento\Framework\UrlInterface $url - * @param \Magento\Framework\View\LayoutInterface $layout + * @var array + */ + private $allowedAreas = [Area::AREA_FRONTEND, Area::AREA_ADMINHTML]; + + /** + * @var State + */ + private $appState; + + /** + * @param ScopeResolverInterface $scopeResolver + * @param UrlInterface $url + * @param LayoutInterface $layout * @param Inline\ConfigInterface $config * @param Inline\ParserInterface $parser * @param Inline\StateInterface $state * @param string $templateFileName * @param string $translatorRoute - * @param null $scope + * @param string|null $scope + * @param State|null $appState + * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( - \Magento\Framework\App\ScopeResolverInterface $scopeResolver, - \Magento\Framework\UrlInterface $url, - \Magento\Framework\View\LayoutInterface $layout, - \Magento\Framework\Translate\Inline\ConfigInterface $config, - \Magento\Framework\Translate\Inline\ParserInterface $parser, - \Magento\Framework\Translate\Inline\StateInterface $state, + ScopeResolverInterface $scopeResolver, + UrlInterface $url, + LayoutInterface $layout, + ConfigInterface $config, + ParserInterface $parser, + StateInterface $state, $templateFileName = '', $translatorRoute = '', - $scope = null + $scope = null, + ?State $appState = null ) { $this->scopeResolver = $scopeResolver; $this->url = $url; @@ -102,6 +130,7 @@ public function __construct( $this->templateFileName = $templateFileName; $this->translatorRoute = $translatorRoute; $this->scope = $scope; + $this->appState = $appState ?: ObjectManager::getInstance()->get(State::class); } /** @@ -112,12 +141,13 @@ public function __construct( public function isAllowed() { if ($this->isAllowed === null) { - if (!$this->scope instanceof \Magento\Framework\App\ScopeInterface) { - $scope = $this->scopeResolver->getScope($this->scope); - } + $scope = $this->scope instanceof ScopeInterface ? null : $this->scopeResolver->getScope($this->scope); + $this->isAllowed = $this->config->isActive($scope) - && $this->config->isDevAllowed($scope); + && $this->config->isDevAllowed($scope) + && $this->isAreaAllowed(); } + return $this->state->isEnabled() && $this->isAllowed; } @@ -134,7 +164,7 @@ public function getParser() /** * Replace translation templates with HTML fragments * - * @param array|string &$body + * @param array|string $body * @param bool $isJson * @return $this */ @@ -189,7 +219,9 @@ protected function addInlineScript() return; } if (!$this->isScriptInserted) { - $this->getParser()->setContent(str_ireplace('</body>', $this->getInlineScript() . '</body>', $content)); + $this->getParser()->setContent( + str_ireplace('</body>', $this->getInlineScript() . '</body>', $content) + ); $this->isScriptInserted = true; } } @@ -204,8 +236,8 @@ protected function addInlineScript() */ protected function getInlineScript() { - /** @var $block \Magento\Framework\View\Element\Template */ - $block = $this->layout->createBlock(\Magento\Framework\View\Element\Template::class); + /** @var $block Template */ + $block = $this->layout->createBlock(Template::class); $block->setAjaxUrl($this->getAjaxUrl()); $block->setTemplate($this->templateFileName); @@ -238,15 +270,24 @@ protected function stripInlineTranslations(&$body) foreach ($body as &$part) { $this->stripInlineTranslations($part); } - } else { - if (is_string($body)) { - $body = preg_replace( - '#' . \Magento\Framework\Translate\Inline\ParserInterface::REGEXP_TOKEN . '#', - '$1', - $body - ); - } + } elseif (is_string($body)) { + $body = preg_replace('#' . ParserInterface::REGEXP_TOKEN . '#', '$1', $body); } + return $this; } + + /** + * Indicates whether the current area is valid for inline translation + * + * @return bool + */ + private function isAreaAllowed(): bool + { + try { + return in_array($this->appState->getAreaCode(), $this->allowedAreas, true); + } catch (LocalizedException $e) { + return false; + } + } } diff --git a/lib/internal/Magento/Framework/Translate/Inline/ConfigInterface.php b/lib/internal/Magento/Framework/Translate/Inline/ConfigInterface.php index 02ab3e92d1d16..dc78b0b49d26f 100644 --- a/lib/internal/Magento/Framework/Translate/Inline/ConfigInterface.php +++ b/lib/internal/Magento/Framework/Translate/Inline/ConfigInterface.php @@ -9,6 +9,7 @@ * Inline Translation config interface * * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php b/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php index 4c3bfc1a1964b..78093d99729b7 100644 --- a/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php +++ b/lib/internal/Magento/Framework/Translate/Inline/ParserInterface.php @@ -9,6 +9,7 @@ * Processes the content with the inline translation replacement so the inline translate JavaScript code will work. * * @api + * @since 100.0.2 */ interface ParserInterface { diff --git a/lib/internal/Magento/Framework/Translate/Inline/StateInterface.php b/lib/internal/Magento/Framework/Translate/Inline/StateInterface.php index cf56616ac4a99..e0994713a0219 100644 --- a/lib/internal/Magento/Framework/Translate/Inline/StateInterface.php +++ b/lib/internal/Magento/Framework/Translate/Inline/StateInterface.php @@ -10,6 +10,7 @@ * Controls and represents the state of the inline translation processing. * * @api + * @since 100.0.2 */ interface StateInterface { diff --git a/lib/internal/Magento/Framework/Translate/InlineInterface.php b/lib/internal/Magento/Framework/Translate/InlineInterface.php index 4669a536eedbf..2c8ac78bffcf0 100644 --- a/lib/internal/Magento/Framework/Translate/InlineInterface.php +++ b/lib/internal/Magento/Framework/Translate/InlineInterface.php @@ -9,6 +9,7 @@ * Inline translation interface * * @api + * @since 100.0.2 */ interface InlineInterface { diff --git a/lib/internal/Magento/Framework/Translate/ResourceInterface.php b/lib/internal/Magento/Framework/Translate/ResourceInterface.php index 51e2f430c7281..44a600a04deeb 100644 --- a/lib/internal/Magento/Framework/Translate/ResourceInterface.php +++ b/lib/internal/Magento/Framework/Translate/ResourceInterface.php @@ -9,6 +9,7 @@ * Returns the translation resource data. * * @api + * @since 100.0.2 */ interface ResourceInterface { diff --git a/lib/internal/Magento/Framework/Translate/Test/Unit/InlineTest.php b/lib/internal/Magento/Framework/Translate/Test/Unit/InlineTest.php index 8ef3d840568dc..1a5feb9506768 100644 --- a/lib/internal/Magento/Framework/Translate/Test/Unit/InlineTest.php +++ b/lib/internal/Magento/Framework/Translate/Test/Unit/InlineTest.php @@ -3,12 +3,16 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + declare(strict_types=1); namespace Magento\Framework\Translate\Test\Unit; +use Magento\Framework\App\Area; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ScopeResolverInterface; +use Magento\Framework\App\State as AppState; +use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; use Magento\Framework\Translate\Inline; use Magento\Framework\Translate\Inline\ConfigInterface; use Magento\Framework\Translate\Inline\ParserFactory; @@ -19,40 +23,64 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * Test for \Magento\Framework\Translate\Inline. + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class InlineTest extends TestCase { + /** + * @var ObjectManager + */ + private $objectManager; + + /** + * @var Inline + */ + private $model; + /** * @var ScopeResolverInterface|MockObject */ - protected $scopeResolverMock; + private $scopeResolverMock; /** * @var UrlInterface|MockObject */ - protected $urlMock; + private $urlMock; /** * @var LayoutInterface|MockObject */ - protected $layoutMock; + private $layoutMock; /** * @var ConfigInterface|MockObject */ - protected $configMock; + private $configMock; /** * @var ParserFactory|MockObject */ - protected $parserMock; + private $parserMock; /** * @var StateInterface|MockObject */ - protected $stateMock; + private $stateMock; + + /** + * @var AppState|MockObject + */ + private $appStateMock; + /** + * @inheritDoc + */ protected function setUp(): void { + $this->objectManager = new ObjectManager($this); + $this->scopeResolverMock = $this->getMockForAbstractClass(ScopeResolverInterface::class); $this->urlMock = $this->getMockForAbstractClass(UrlInterface::class); @@ -60,91 +88,111 @@ protected function setUp(): void $this->configMock = $this->getMockForAbstractClass(ConfigInterface::class); $this->parserMock = $this->getMockForAbstractClass(ParserInterface::class); $this->stateMock = $this->getMockForAbstractClass(StateInterface::class); + $this->appStateMock = $this->createMock(AppState::class); + $this->model = $this->objectManager->getObject( + Inline::class, + [ + 'scopeResolver' => $this->scopeResolverMock, + 'url' => $this->urlMock, + 'layout' => $this->layoutMock, + 'config' => $this->configMock, + 'parser' => $this->parserMock, + 'state' => $this->stateMock, + 'appState' => $this->appStateMock, + ] + ); } /** + * Is allowed test + * * @param bool $isEnabled * @param bool $isActive * @param bool $isDevAllowed + * @param string $area * @param bool $result * @dataProvider isAllowedDataProvider */ - public function testIsAllowed($isEnabled, $isActive, $isDevAllowed, $result) + public function testIsAllowed(bool $isEnabled, bool $isActive, bool $isDevAllowed, string $area, bool $result): void { - $this->prepareIsAllowed($isEnabled, $isActive, $isDevAllowed); - - $model = new Inline( - $this->scopeResolverMock, - $this->urlMock, - $this->layoutMock, - $this->configMock, - $this->parserMock, - $this->stateMock - ); + $this->prepareIsAllowed($isEnabled, $isActive, $isDevAllowed, null, $area); - $this->assertEquals($result, $model->isAllowed()); - $this->assertEquals($result, $model->isAllowed()); + $this->assertEquals($result, $this->model->isAllowed()); + $this->assertEquals($result, $this->model->isAllowed()); } /** + * Data provider for testIsAllowed + * * @return array */ - public function isAllowedDataProvider() + public function isAllowedDataProvider(): array { return [ - [true, true, true, true], - [true, false, true, false], - [true, true, false, false], - [true, false, false, false], - [false, true, true, false], - [false, false, true, false], - [false, true, false, false], - [false, false, false, false], + [true, true, true, Area::AREA_FRONTEND, true], + [true, false, true, Area::AREA_FRONTEND, false], + [true, true, false, Area::AREA_FRONTEND, false], + [true, false, false, Area::AREA_FRONTEND, false], + [false, true, true, Area::AREA_FRONTEND, false], + [false, false, true, Area::AREA_FRONTEND, false], + [false, true, false, Area::AREA_FRONTEND, false], + [false, false, false, Area::AREA_FRONTEND, false], + [true, true, true, Area::AREA_GLOBAL, false], + [true, true, true, Area::AREA_ADMINHTML, true], + [true, true, true, Area::AREA_DOC, false], + [true, true, true, Area::AREA_CRONTAB, false], + [true, true, true, Area::AREA_WEBAPI_REST, false], + [true, true, true, Area::AREA_WEBAPI_SOAP, false], + [true, true, true, Area::AREA_GRAPHQL, false] ]; } - public function testGetParser() + /** + * Get parser test + * + * @return void + */ + public function testGetParser(): void { - $model = new Inline( - $this->scopeResolverMock, - $this->urlMock, - $this->layoutMock, - $this->configMock, - $this->parserMock, - $this->stateMock - ); - $this->assertEquals($this->parserMock, $model->getParser()); + $this->assertEquals($this->parserMock, $this->model->getParser()); } /** + * Process response body strip inline + * * @param string|array $body - * @param string $expected + * @param string|array $expected + * @return void * @dataProvider processResponseBodyStripInlineDataProvider */ - public function testProcessResponseBodyStripInline($body, $expected) + public function testProcessResponseBodyStripInline($body, $expected): void { $scope = 'admin'; $this->prepareIsAllowed(false, true, true, $scope); - $model = new Inline( - $this->scopeResolverMock, - $this->urlMock, - $this->layoutMock, - $this->configMock, - $this->parserMock, - $this->stateMock, - '', - '', - $scope + $model = $this->objectManager->getObject( + Inline::class, + [ + 'scopeResolver' => $this->scopeResolverMock, + 'url' => $this->urlMock, + 'layout' => $this->layoutMock, + 'config' => $this->configMock, + 'parser' => $this->parserMock, + 'state' => $this->stateMock, + 'appState' => $this->appStateMock, + 'scope' => $scope, + ] ); $model->processResponseBody($body, true); $this->assertEquals($body, $expected); } /** + * Data provider for testProcessResponseBodyStripInline + * * @return array */ - public function processResponseBodyStripInlineDataProvider() + public function processResponseBodyStripInlineDataProvider(): array { return [ ['test', 'test'], @@ -157,63 +205,55 @@ public function processResponseBodyStripInlineDataProvider() } /** + * Process response body + * * @param string $scope - * @param array|string $body - * @param array|string $expected + * @param string $body + * @param string $expected + * @return void * @dataProvider processResponseBodyDataProvider * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function testProcessResponseBody($scope, $body, $expected) + public function testProcessResponseBody(string $scope, string $body, string $expected): void { $isJson = true; $this->prepareIsAllowed(true, true, true, $scope); $jsonCall = is_array($body) ? 2 * (count($body) + 1) : 2; - $this->parserMock->expects( - $this->exactly($jsonCall) - )->method( - 'setIsJson' - )->willReturnMap( + $this->parserMock->expects($this->exactly($jsonCall)) + ->method('setIsJson') + ->willReturnMap([[$isJson, $this->returnSelf()], [!$isJson, $this->returnSelf()]]); + $this->parserMock->expects($this->once()) + ->method('processResponseBodyString') + ->with(is_array($body) ? reset($body) : $body); + $this->parserMock->expects($this->exactly(2)) + ->method('getContent') + ->willReturn(is_array($body) ? reset($body) : $body); + + $model = $this->objectManager->getObject( + Inline::class, [ - [$isJson, $this->returnSelf()], - [!$isJson, $this->returnSelf()], + 'scopeResolver' => $this->scopeResolverMock, + 'url' => $this->urlMock, + 'layout' => $this->layoutMock, + 'config' => $this->configMock, + 'parser' => $this->parserMock, + 'state' => $this->stateMock, + 'appState' => $this->appStateMock, + 'scope' => $scope, ] ); - $this->parserMock->expects( - $this->exactly(1) - )->method( - 'processResponseBodyString' - )->with( - is_array($body) ? reset($body) : $body - ); - $this->parserMock->expects( - $this->exactly(2) - )->method( - 'getContent' - )->willReturn( - is_array($body) ? reset($body) : $body - ); - - $model = new Inline( - $this->scopeResolverMock, - $this->urlMock, - $this->layoutMock, - $this->configMock, - $this->parserMock, - $this->stateMock, - '', - '', - $scope - ); $model->processResponseBody($body, $isJson); $this->assertEquals($body, $expected); } /** + * Data provider for testProcessResponseBody + * * @return array */ - public function processResponseBodyDataProvider() + public function processResponseBodyDataProvider(): array { return [ ['admin', 'test', 'test'], @@ -222,63 +262,55 @@ public function processResponseBodyDataProvider() } /** - * @param $scope - * @param $body - * @param $expected + * Process response body get script + * + * @param string $scope + * @param string $body + * @param string $expected + * @return void * @dataProvider processResponseBodyGetInlineScriptDataProvider * @SuppressWarnings(PHPMD.NPathComplexity) */ - public function testProcessResponseBodyGetInlineScript($scope, $body, $expected) + public function testProcessResponseBodyGetInlineScript(string $scope, string $body, string $expected): void { $isJson = true; $this->prepareIsAllowed(true, true, true, $scope); $jsonCall = is_array($body) ? 2 * (count($body) + 1) : 2; - $this->parserMock->expects( - $this->exactly($jsonCall) - )->method( - 'setIsJson' - )->willReturnMap( + $this->parserMock->expects($this->exactly($jsonCall)) + ->method('setIsJson') + ->willReturnMap([[$isJson, $this->returnSelf()], [!$isJson, $this->returnSelf()]]); + $this->parserMock->expects($this->once()) + ->method('processResponseBodyString') + ->with(is_array($body) ? reset($body) : $body); + $this->parserMock->expects($this->exactly(2)) + ->method('getContent') + ->willReturn(is_array($body) ? reset($body) : $body); + + $model = $this->objectManager->getObject( + Inline::class, [ - [$isJson, $this->returnSelf()], - [!$isJson, $this->returnSelf()], + 'scopeResolver' => $this->scopeResolverMock, + 'url' => $this->urlMock, + 'layout' => $this->layoutMock, + 'config' => $this->configMock, + 'parser' => $this->parserMock, + 'state' => $this->stateMock, + 'appState' => $this->appStateMock, + 'scope' => $scope, ] ); - $this->parserMock->expects( - $this->exactly(1) - )->method( - 'processResponseBodyString' - )->with( - is_array($body) ? reset($body) : $body - ); - $this->parserMock->expects( - $this->exactly(2) - )->method( - 'getContent' - )->willReturn( - is_array($body) ? reset($body) : $body - ); - - $model = new Inline( - $this->scopeResolverMock, - $this->urlMock, - $this->layoutMock, - $this->configMock, - $this->parserMock, - $this->stateMock, - '', - '', - $scope - ); $model->processResponseBody($body, $isJson); $this->assertEquals($body, $expected); } /** + * Data provider for testProcessResponseBodyGetInlineScript + * * @return array */ - public function processResponseBodyGetInlineScriptDataProvider() + public function processResponseBodyGetInlineScriptDataProvider(): array { return [ ['admin', 'test', 'test'], @@ -287,41 +319,42 @@ public function processResponseBodyGetInlineScriptDataProvider() } /** + * Prepare is allowed + * * @param bool $isEnabled * @param bool $isActive * @param bool $isDevAllowed * @param null|string $scope + * @param string $area + * @return void */ - protected function prepareIsAllowed($isEnabled, $isActive, $isDevAllowed, $scope = null) - { + protected function prepareIsAllowed( + bool $isEnabled, + bool $isActive, + bool $isDevAllowed, + ?string $scope = null, + string $area = Area::AREA_FRONTEND + ): void { $scopeMock = $this->getMockForAbstractClass(ScopeConfigInterface::class); - $this->stateMock->expects($this->any())->method('isEnabled')->willReturn($isEnabled); - $this->scopeResolverMock->expects( - $this->once() - )->method( - 'getScope' - )->with( - $scope - )->willReturn( - $scopeMock - ); + $this->stateMock->expects($this->atLeastOnce()) + ->method('isEnabled') + ->willReturn($isEnabled); + $this->scopeResolverMock->expects($this->once()) + ->method('getScope') + ->with($scope) + ->willReturn($scopeMock); - $this->configMock->expects( - $this->once() - )->method( - 'isActive' - )->with( - $scopeMock - )->willReturn( - $isActive - ); + $this->configMock->expects($this->once()) + ->method('isActive') + ->with($scopeMock) + ->willReturn($isActive); - $this->configMock->expects( - $this->exactly((int)$isActive) - )->method( - 'isDevAllowed' - )->willReturn( - $isDevAllowed - ); + $this->configMock->expects($this->exactly((int)$isActive)) + ->method('isDevAllowed') + ->willReturn($isDevAllowed); + + $this->appStateMock->expects(($isActive && $isDevAllowed) ? $this->once() : $this->never()) + ->method('getAreaCode') + ->willReturn($area); } } diff --git a/lib/internal/Magento/Framework/Unserialize/Unserialize.php b/lib/internal/Magento/Framework/Unserialize/Unserialize.php index 56c3bb0d16864..7611e6c12b6d8 100644 --- a/lib/internal/Magento/Framework/Unserialize/Unserialize.php +++ b/lib/internal/Magento/Framework/Unserialize/Unserialize.php @@ -10,7 +10,7 @@ use Magento\Framework\Serialize\Serializer\Serialize; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ class Unserialize { diff --git a/lib/internal/Magento/Framework/Url.php b/lib/internal/Magento/Framework/Url.php index 567fd2a96c18a..1d00b732c5795 100644 --- a/lib/internal/Magento/Framework/Url.php +++ b/lib/internal/Magento/Framework/Url.php @@ -292,6 +292,8 @@ public function setUseSession($useSession) */ public function getUseSession() { + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return false; } @@ -756,12 +758,12 @@ public function getRouteUrl($routePath = null, $routeParams = null) } /** - * Add session param - * - * @return \Magento\Framework\UrlInterface + * @inheritDoc */ public function addSessionParam() { + trigger_error('Session ID is not used as URL parameter anymore.', E_USER_DEPRECATED); + return $this; } @@ -1005,7 +1007,7 @@ public function getRebuiltUrl($url) * * @param string $value * @return string - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function escape($value) { @@ -1152,7 +1154,7 @@ protected function getRouteParamsResolver() * Gets URL modifier. * * @return \Magento\Framework\Url\ModifierInterface - * @deprecated 100.1.0 + * @deprecated 101.0.0 */ private function getUrlModifier() { @@ -1169,7 +1171,7 @@ private function getUrlModifier() * Get escaper * * @return Escaper - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getEscaper() { diff --git a/lib/internal/Magento/Framework/Url/DecoderInterface.php b/lib/internal/Magento/Framework/Url/DecoderInterface.php index 523a402c1dc84..7a9f971202450 100644 --- a/lib/internal/Magento/Framework/Url/DecoderInterface.php +++ b/lib/internal/Magento/Framework/Url/DecoderInterface.php @@ -9,6 +9,7 @@ * Base64 decoder for URLs * * @api + * @since 100.0.2 */ interface DecoderInterface { diff --git a/lib/internal/Magento/Framework/Url/EncoderInterface.php b/lib/internal/Magento/Framework/Url/EncoderInterface.php index 9e5a999755063..02afdfcaefe9b 100644 --- a/lib/internal/Magento/Framework/Url/EncoderInterface.php +++ b/lib/internal/Magento/Framework/Url/EncoderInterface.php @@ -9,6 +9,7 @@ * Base64 encoder for URLs * * @api + * @since 100.0.2 */ interface EncoderInterface { diff --git a/lib/internal/Magento/Framework/Url/QueryParamsResolverInterface.php b/lib/internal/Magento/Framework/Url/QueryParamsResolverInterface.php index c9bd2412cad9c..1e7e8dff25456 100644 --- a/lib/internal/Magento/Framework/Url/QueryParamsResolverInterface.php +++ b/lib/internal/Magento/Framework/Url/QueryParamsResolverInterface.php @@ -9,6 +9,7 @@ * Resolves query parameters in a URL. * * @api + * @since 100.0.2 */ interface QueryParamsResolverInterface { diff --git a/lib/internal/Magento/Framework/Url/RouteParamsResolver.php b/lib/internal/Magento/Framework/Url/RouteParamsResolver.php index b46ba00d48023..eac9005e68548 100644 --- a/lib/internal/Magento/Framework/Url/RouteParamsResolver.php +++ b/lib/internal/Magento/Framework/Url/RouteParamsResolver.php @@ -154,7 +154,7 @@ public function getRouteParam($key) * Get escaper * * @return \Magento\Framework\Escaper - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getEscaper() { diff --git a/lib/internal/Magento/Framework/Url/RouteParamsResolverInterface.php b/lib/internal/Magento/Framework/Url/RouteParamsResolverInterface.php index 349f3d8359f7d..22d9aac7a2c32 100644 --- a/lib/internal/Magento/Framework/Url/RouteParamsResolverInterface.php +++ b/lib/internal/Magento/Framework/Url/RouteParamsResolverInterface.php @@ -9,6 +9,7 @@ * Route parameters resolver. * * @api + * @since 100.0.2 */ interface RouteParamsResolverInterface { diff --git a/lib/internal/Magento/Framework/Url/ScopeInterface.php b/lib/internal/Magento/Framework/Url/ScopeInterface.php index 821ae1814e9a3..648a33ca7b870 100644 --- a/lib/internal/Magento/Framework/Url/ScopeInterface.php +++ b/lib/internal/Magento/Framework/Url/ScopeInterface.php @@ -10,6 +10,7 @@ * determine scope based on URLs. * * @api + * @since 100.0.2 */ interface ScopeInterface extends \Magento\Framework\App\ScopeInterface { diff --git a/lib/internal/Magento/Framework/Url/ScopeResolverInterface.php b/lib/internal/Magento/Framework/Url/ScopeResolverInterface.php index 961ad6ecff8b5..b6861e1153f21 100644 --- a/lib/internal/Magento/Framework/Url/ScopeResolverInterface.php +++ b/lib/internal/Magento/Framework/Url/ScopeResolverInterface.php @@ -9,6 +9,7 @@ * This ScopeResolverInterface adds the ability to get the Magento area the code is executing in. * * @api + * @since 100.0.2 */ interface ScopeResolverInterface extends \Magento\Framework\App\ScopeResolverInterface { diff --git a/lib/internal/Magento/Framework/Url/SecurityInfoInterface.php b/lib/internal/Magento/Framework/Url/SecurityInfoInterface.php index c3cdab91d325f..8029538fbaeb4 100644 --- a/lib/internal/Magento/Framework/Url/SecurityInfoInterface.php +++ b/lib/internal/Magento/Framework/Url/SecurityInfoInterface.php @@ -9,6 +9,7 @@ * URL security information. Answers whether URL is secured. * * @api + * @since 100.0.2 */ interface SecurityInfoInterface { diff --git a/lib/internal/Magento/Framework/UrlInterface.php b/lib/internal/Magento/Framework/UrlInterface.php index 59806e14eb7c9..b25d6ab1a0b85 100644 --- a/lib/internal/Magento/Framework/UrlInterface.php +++ b/lib/internal/Magento/Framework/UrlInterface.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ interface UrlInterface { @@ -28,19 +29,10 @@ interface UrlInterface */ const DEFAULT_URL_TYPE = 'link'; - /** - * Default controller name - */ const DEFAULT_CONTROLLER_NAME = 'index'; - /** - * Default action name - */ const DEFAULT_ACTION_NAME = 'index'; - /** - * Rewrite request path alias - */ const REWRITE_REQUEST_PATH_ALIAS = 'rewrite_request_path'; /** @@ -52,6 +44,7 @@ interface UrlInterface * Retrieve use session rule * * @return bool + * @deprecated SID is not being passed in URLs anymore. * @SuppressWarnings(PHPMD.BooleanGetMethodName) */ public function getUseSession(); @@ -84,6 +77,7 @@ public function getRouteUrl($routePath = null, $routeParams = null); * Add session param * * @return \Magento\Framework\UrlInterface + * @deprecated SID is not being passed in URLs anymore. */ public function addSessionParam(); diff --git a/lib/internal/Magento/Framework/Validation/ValidationException.php b/lib/internal/Magento/Framework/Validation/ValidationException.php index 4f94bd8af80b5..ee98d08ae47f8 100644 --- a/lib/internal/Magento/Framework/Validation/ValidationException.php +++ b/lib/internal/Magento/Framework/Validation/ValidationException.php @@ -17,6 +17,7 @@ * to support Multi-Error response. * * @api + * @since 101.0.7 */ class ValidationException extends LocalizedException implements AggregateExceptionInterface { @@ -43,6 +44,7 @@ public function __construct( /** * @inheritdoc + * @since 101.0.7 */ public function getErrors(): array { diff --git a/lib/internal/Magento/Framework/Validation/ValidationResult.php b/lib/internal/Magento/Framework/Validation/ValidationResult.php index 60ff7ba5a4700..9e9cad26536fb 100644 --- a/lib/internal/Magento/Framework/Validation/ValidationResult.php +++ b/lib/internal/Magento/Framework/Validation/ValidationResult.php @@ -12,6 +12,7 @@ * ValidationResult represents a container storing all the validation errors that happened during the entity validation. * * @api + * @since 101.0.7 */ class ValidationResult { @@ -30,6 +31,7 @@ public function __construct(array $errors) /** * @return bool + * @since 101.0.7 */ public function isValid(): bool { @@ -38,6 +40,7 @@ public function isValid(): bool /** * @return array + * @since 101.0.7 */ public function getErrors(): array { diff --git a/lib/internal/Magento/Framework/Validator.php b/lib/internal/Magento/Framework/Validator.php index e15a1399a96a4..b42e486d00483 100644 --- a/lib/internal/Magento/Framework/Validator.php +++ b/lib/internal/Magento/Framework/Validator.php @@ -10,6 +10,7 @@ * Validator class that represents chain of validators. * * @api + * @since 100.0.2 */ class Validator extends \Magento\Framework\Validator\AbstractValidator { diff --git a/lib/internal/Magento/Framework/Validator/AbstractValidator.php b/lib/internal/Magento/Framework/Validator/AbstractValidator.php index db636516aacab..ade9fa230fbb8 100644 --- a/lib/internal/Magento/Framework/Validator/AbstractValidator.php +++ b/lib/internal/Magento/Framework/Validator/AbstractValidator.php @@ -9,6 +9,7 @@ * Abstract validator class. * * @api + * @since 100.0.2 */ abstract class AbstractValidator implements \Magento\Framework\Validator\ValidatorInterface { diff --git a/lib/internal/Magento/Framework/Validator/Constraint.php b/lib/internal/Magento/Framework/Validator/Constraint.php index d9be3c3d7ff9f..3326d42b7d24f 100644 --- a/lib/internal/Magento/Framework/Validator/Constraint.php +++ b/lib/internal/Magento/Framework/Validator/Constraint.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class Constraint extends \Magento\Framework\Validator\AbstractValidator { diff --git a/lib/internal/Magento/Framework/Validator/DataObject.php b/lib/internal/Magento/Framework/Validator/DataObject.php index 7348550ab3880..d4f4bbb45e7ec 100644 --- a/lib/internal/Magento/Framework/Validator/DataObject.php +++ b/lib/internal/Magento/Framework/Validator/DataObject.php @@ -12,6 +12,7 @@ /** * @api + * @since 100.0.2 */ class DataObject implements \Zend_Validate_Interface { diff --git a/lib/internal/Magento/Framework/Validator/Exception.php b/lib/internal/Magento/Framework/Validator/Exception.php index 370f66c424b01..837886e0ce9c9 100644 --- a/lib/internal/Magento/Framework/Validator/Exception.php +++ b/lib/internal/Magento/Framework/Validator/Exception.php @@ -16,6 +16,7 @@ * Exception to be thrown when data validation fails * * @api + * @since 100.0.2 */ class Exception extends InputException { diff --git a/lib/internal/Magento/Framework/Validator/ValidatorInterface.php b/lib/internal/Magento/Framework/Validator/ValidatorInterface.php index 92fb6395c1e4a..001582bd5b867 100644 --- a/lib/internal/Magento/Framework/Validator/ValidatorInterface.php +++ b/lib/internal/Magento/Framework/Validator/ValidatorInterface.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ interface ValidatorInterface extends \Zend_Validate_Interface { diff --git a/lib/internal/Magento/Framework/View/Asset/AssetInterface.php b/lib/internal/Magento/Framework/View/Asset/AssetInterface.php index 60dc997b9d6da..6b7c47816c6a8 100644 --- a/lib/internal/Magento/Framework/View/Asset/AssetInterface.php +++ b/lib/internal/Magento/Framework/View/Asset/AssetInterface.php @@ -9,6 +9,7 @@ * An abstraction for static view file (or resource) that may be embedded to a web page * * @api + * @since 100.0.2 */ interface AssetInterface { @@ -30,7 +31,7 @@ public function getContentType(); * Retrieve source content type * * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getSourceContentType(); } diff --git a/lib/internal/Magento/Framework/View/Asset/Bundle.php b/lib/internal/Magento/Framework/View/Asset/Bundle.php index 80f35f9d57075..5d918982412b7 100644 --- a/lib/internal/Magento/Framework/View/Asset/Bundle.php +++ b/lib/internal/Magento/Framework/View/Asset/Bundle.php @@ -13,7 +13,7 @@ /** * Bundle model - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Deploy\Package\Bundle */ class Bundle diff --git a/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php b/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php index 8c529e9a2bc1c..ede43c789416f 100644 --- a/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php +++ b/lib/internal/Magento/Framework/View/Asset/Bundle/Config.php @@ -15,7 +15,7 @@ /** * Class Config - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Deploy\Config\BundleConfig */ class Config implements Bundle\ConfigInterface diff --git a/lib/internal/Magento/Framework/View/Asset/Bundle/ConfigInterface.php b/lib/internal/Magento/Framework/View/Asset/Bundle/ConfigInterface.php index bea542aef2ea8..3acb49337a2e0 100644 --- a/lib/internal/Magento/Framework/View/Asset/Bundle/ConfigInterface.php +++ b/lib/internal/Magento/Framework/View/Asset/Bundle/ConfigInterface.php @@ -10,7 +10,7 @@ /** * Interface ConfigInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Deploy\Config\BundleConfig */ interface ConfigInterface diff --git a/lib/internal/Magento/Framework/View/Asset/Bundle/Manager.php b/lib/internal/Magento/Framework/View/Asset/Bundle/Manager.php index ee1368c0e6230..490c04004365d 100644 --- a/lib/internal/Magento/Framework/View/Asset/Bundle/Manager.php +++ b/lib/internal/Magento/Framework/View/Asset/Bundle/Manager.php @@ -15,7 +15,7 @@ /** * BundleService model * @SuppressWarnings(PHPMD.CouplingBetweenObjects) - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @see \Magento\Deploy\Service\Bundle */ class Manager diff --git a/lib/internal/Magento/Framework/View/Asset/ConfigInterface.php b/lib/internal/Magento/Framework/View/Asset/ConfigInterface.php index 21357911e4597..1b817acf84aeb 100644 --- a/lib/internal/Magento/Framework/View/Asset/ConfigInterface.php +++ b/lib/internal/Magento/Framework/View/Asset/ConfigInterface.php @@ -9,6 +9,7 @@ * View asset configuration interface * * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/View/Asset/ContentProcessorException.php b/lib/internal/Magento/Framework/View/Asset/ContentProcessorException.php index 86daba29e5be8..481968f7da360 100644 --- a/lib/internal/Magento/Framework/View/Asset/ContentProcessorException.php +++ b/lib/internal/Magento/Framework/View/Asset/ContentProcessorException.php @@ -9,6 +9,7 @@ /** * @api + * @since 100.0.2 */ class ContentProcessorException extends LocalizedException { diff --git a/lib/internal/Magento/Framework/View/Asset/File.php b/lib/internal/Magento/Framework/View/Asset/File.php index 2dd50224f78fa..7a3276b251a5a 100644 --- a/lib/internal/Magento/Framework/View/Asset/File.php +++ b/lib/internal/Magento/Framework/View/Asset/File.php @@ -12,6 +12,7 @@ * This class is a value object with lazy loading of some of its data (content, physical file path) * * @api + * @since 100.0.2 */ class File implements MergeableInterface { @@ -166,7 +167,7 @@ public function getSourceFile() * Get source content type * * @return string - * @since 100.2.0 + * @since 101.0.0 */ public function getSourceContentType() { diff --git a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php index 5f83358336ab6..ba6564cd7abb7 100644 --- a/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php +++ b/lib/internal/Magento/Framework/View/Asset/File/FallbackContext.php @@ -12,6 +12,7 @@ * An advanced context that contains information necessary for view files fallback system * * @api + * @since 100.0.2 */ class FallbackContext extends Context { diff --git a/lib/internal/Magento/Framework/View/Asset/File/NotFoundException.php b/lib/internal/Magento/Framework/View/Asset/File/NotFoundException.php index 93b88834edff9..ddecc9330617d 100644 --- a/lib/internal/Magento/Framework/View/Asset/File/NotFoundException.php +++ b/lib/internal/Magento/Framework/View/Asset/File/NotFoundException.php @@ -12,6 +12,7 @@ * Use this exception when file has not been found * * @api + * @since 100.0.2 */ class NotFoundException extends \LogicException { diff --git a/lib/internal/Magento/Framework/View/Asset/GroupedCollection.php b/lib/internal/Magento/Framework/View/Asset/GroupedCollection.php index a0942599480b4..a574b272a2876 100644 --- a/lib/internal/Magento/Framework/View/Asset/GroupedCollection.php +++ b/lib/internal/Magento/Framework/View/Asset/GroupedCollection.php @@ -9,6 +9,7 @@ * List of page assets that combines into groups ones having the same properties * * @api + * @since 100.0.2 */ class GroupedCollection extends Collection { diff --git a/lib/internal/Magento/Framework/View/Asset/LocalInterface.php b/lib/internal/Magento/Framework/View/Asset/LocalInterface.php index fd7d1ea473ecd..2bb53bcaa86b2 100644 --- a/lib/internal/Magento/Framework/View/Asset/LocalInterface.php +++ b/lib/internal/Magento/Framework/View/Asset/LocalInterface.php @@ -8,6 +8,7 @@ /** * Interface of an asset with locally accessible source file * @api + * @since 100.0.2 */ interface LocalInterface extends AssetInterface { diff --git a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Checksum.php b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Checksum.php index 7c7e8864151c0..c15fedeb8d799 100644 --- a/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Checksum.php +++ b/lib/internal/Magento/Framework/View/Asset/MergeStrategy/Checksum.php @@ -45,7 +45,7 @@ public function __construct( } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return Source */ private function getAssetSource() diff --git a/lib/internal/Magento/Framework/View/Asset/Minification.php b/lib/internal/Magento/Framework/View/Asset/Minification.php index 40ac4dd1f3ba3..b0a42fbd373da 100644 --- a/lib/internal/Magento/Framework/View/Asset/Minification.php +++ b/lib/internal/Magento/Framework/View/Asset/Minification.php @@ -11,6 +11,7 @@ /** * Helper class for static files minification related processes. * @api + * @since 100.0.2 */ class Minification { diff --git a/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSourceInterface.php b/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSourceInterface.php index 750c77e651dff..d208786673e04 100644 --- a/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSourceInterface.php +++ b/lib/internal/Magento/Framework/View/Asset/PreProcessor/AlternativeSourceInterface.php @@ -11,6 +11,7 @@ * Interface AlternativeSourceInterface * * @api + * @since 100.0.2 */ interface AlternativeSourceInterface extends PreProcessorInterface { diff --git a/lib/internal/Magento/Framework/View/Asset/PreProcessor/Chain.php b/lib/internal/Magento/Framework/View/Asset/PreProcessor/Chain.php index 4fc31c22cb2de..80ce1321ef91e 100644 --- a/lib/internal/Magento/Framework/View/Asset/PreProcessor/Chain.php +++ b/lib/internal/Magento/Framework/View/Asset/PreProcessor/Chain.php @@ -13,6 +13,7 @@ * Encapsulates complexity of all necessary context and parameters * * @api + * @since 100.0.2 */ class Chain { diff --git a/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactory.php b/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactory.php index 51957cdf4c789..70baf7622b138 100644 --- a/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactory.php +++ b/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactory.php @@ -11,6 +11,7 @@ * Factory for @see \Magento\Framework\View\Asset\PreProcessor\Chain * @codeCoverageIgnore * @api + * @since 100.0.2 */ class ChainFactory implements ChainFactoryInterface { diff --git a/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactoryInterface.php b/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactoryInterface.php index d81bc69158db9..10ae2f6d479b2 100644 --- a/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactoryInterface.php +++ b/lib/internal/Magento/Framework/View/Asset/PreProcessor/ChainFactoryInterface.php @@ -9,6 +9,7 @@ * Interface ChainFactoryInterface * * @api + * @since 100.0.2 */ interface ChainFactoryInterface { diff --git a/lib/internal/Magento/Framework/View/Asset/PreProcessorInterface.php b/lib/internal/Magento/Framework/View/Asset/PreProcessorInterface.php index 419b6cd8f612f..35c9aa6a18a41 100644 --- a/lib/internal/Magento/Framework/View/Asset/PreProcessorInterface.php +++ b/lib/internal/Magento/Framework/View/Asset/PreProcessorInterface.php @@ -9,6 +9,7 @@ * An interface for "preprocessing" asset contents * * @api + * @since 100.0.2 */ interface PreProcessorInterface { diff --git a/lib/internal/Magento/Framework/View/Asset/Repository.php b/lib/internal/Magento/Framework/View/Asset/Repository.php index 15b9a7130180a..6ec4d1219af63 100644 --- a/lib/internal/Magento/Framework/View/Asset/Repository.php +++ b/lib/internal/Magento/Framework/View/Asset/Repository.php @@ -17,6 +17,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api + * @since 100.0.2 */ class Repository { @@ -37,7 +38,7 @@ class Repository /** * @var \Magento\Framework\View\Design\Theme\ListInterface - * @deprecated 100.1.1 + * @deprecated 100.0.2 */ private $themeList; diff --git a/lib/internal/Magento/Framework/View/Asset/Source.php b/lib/internal/Magento/Framework/View/Asset/Source.php index 4afe1be4490bc..dc98a257500c2 100644 --- a/lib/internal/Magento/Framework/View/Asset/Source.php +++ b/lib/internal/Magento/Framework/View/Asset/Source.php @@ -47,7 +47,7 @@ class Source /** * @var \Magento\Framework\View\Design\Theme\ListInterface - * @deprecated 100.1.1 + * @deprecated 100.0.2 */ private $themeList; diff --git a/lib/internal/Magento/Framework/View/ConfigInterface.php b/lib/internal/Magento/Framework/View/ConfigInterface.php index 1b81f10f49d5a..e2a82d97ca1e8 100644 --- a/lib/internal/Magento/Framework/View/ConfigInterface.php +++ b/lib/internal/Magento/Framework/View/ConfigInterface.php @@ -9,6 +9,7 @@ * Config Interface * * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/View/Context.php b/lib/internal/Magento/Framework/View/Context.php index 508d63d158bd7..8503c48d135c2 100644 --- a/lib/internal/Magento/Framework/View/Context.php +++ b/lib/internal/Magento/Framework/View/Context.php @@ -28,6 +28,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * @since 100.0.2 */ class Context { diff --git a/lib/internal/Magento/Framework/View/Design/Fallback/Rule/Theme.php b/lib/internal/Magento/Framework/View/Design/Fallback/Rule/Theme.php index b1e0e117e6efd..543eb3727d7a3 100644 --- a/lib/internal/Magento/Framework/View/Design/Fallback/Rule/Theme.php +++ b/lib/internal/Magento/Framework/View/Design/Fallback/Rule/Theme.php @@ -108,7 +108,7 @@ private function getThemePubStaticDir(ThemeInterface $theme, $params = []) * Get DirectoryList instance * @return DirectoryList * - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getDirectoryList() { diff --git a/lib/internal/Magento/Framework/View/Design/Theme/FileProviderInterface.php b/lib/internal/Magento/Framework/View/Design/Theme/FileProviderInterface.php index bf431b085a106..19337ba60f5a5 100644 --- a/lib/internal/Magento/Framework/View/Design/Theme/FileProviderInterface.php +++ b/lib/internal/Magento/Framework/View/Design/Theme/FileProviderInterface.php @@ -9,6 +9,7 @@ * Theme files provider * * @api + * @since 100.0.2 */ interface FileProviderInterface { diff --git a/lib/internal/Magento/Framework/View/Design/Theme/Label/ListInterface.php b/lib/internal/Magento/Framework/View/Design/Theme/Label/ListInterface.php index 7003ee17dd763..5f8e8be4cf812 100644 --- a/lib/internal/Magento/Framework/View/Design/Theme/Label/ListInterface.php +++ b/lib/internal/Magento/Framework/View/Design/Theme/Label/ListInterface.php @@ -9,6 +9,7 @@ * Label list interface * * @api + * @since 100.0.2 */ interface ListInterface { diff --git a/lib/internal/Magento/Framework/View/Design/Theme/ListInterface.php b/lib/internal/Magento/Framework/View/Design/Theme/ListInterface.php index b3556a578db6a..c68f2fc23f3a8 100644 --- a/lib/internal/Magento/Framework/View/Design/Theme/ListInterface.php +++ b/lib/internal/Magento/Framework/View/Design/Theme/ListInterface.php @@ -9,6 +9,7 @@ * Theme list interface * * @api + * @since 100.0.2 */ interface ListInterface { diff --git a/lib/internal/Magento/Framework/View/Design/ThemeInterface.php b/lib/internal/Magento/Framework/View/Design/ThemeInterface.php index adcd29db708c9..fc1c5d8a22a47 100644 --- a/lib/internal/Magento/Framework/View/Design/ThemeInterface.php +++ b/lib/internal/Magento/Framework/View/Design/ThemeInterface.php @@ -9,6 +9,7 @@ * Interface ThemeInterface * * @api + * @since 100.0.2 */ interface ThemeInterface { diff --git a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php index 95ff209dd4571..8034e1592a232 100644 --- a/lib/internal/Magento/Framework/View/Element/AbstractBlock.php +++ b/lib/internal/Magento/Framework/View/Element/AbstractBlock.php @@ -24,6 +24,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * @SuppressWarnings(PHPMD.NumberOfChildren) + * @since 100.0.2 */ abstract class AbstractBlock extends \Magento\Framework\DataObject implements BlockInterface { @@ -55,7 +56,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl * SID Resolver * * @var \Magento\Framework\Session\SidResolverInterface - * @deprecated Not used anymore. + * @deprecated 102.0.5 Not used anymore. */ protected $_sidResolver; @@ -176,7 +177,7 @@ abstract class AbstractBlock extends \Magento\Framework\DataObject implements Bl /** * @var \Magento\Framework\App\CacheInterface - * @since 100.2.0 + * @since 101.0.0 */ protected $_cache; @@ -893,7 +894,7 @@ public static function extractModuleName($className) * @param string|array $data * @param array|null $allowedTags * @return string - * @deprecated Use $escaper directly in templates and in blocks. + * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. */ public function escapeHtml($data, $allowedTags = null) { @@ -905,8 +906,8 @@ public function escapeHtml($data, $allowedTags = null) * * @param string $string * @return string - * @since 100.2.0 - * @deprecated Use $escaper directly in templates and in blocks. + * @since 101.0.0 + * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. */ public function escapeJs($string) { @@ -919,8 +920,8 @@ public function escapeJs($string) * @param string $string * @param boolean $escapeSingleQuote * @return string - * @since 100.2.0 - * @deprecated Use $escaper directly in templates and in blocks. + * @since 101.0.0 + * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. */ public function escapeHtmlAttr($string, $escapeSingleQuote = true) { @@ -932,8 +933,8 @@ public function escapeHtmlAttr($string, $escapeSingleQuote = true) * * @param string $string * @return string - * @since 100.2.0 - * @deprecated Use $escaper directly in templates and in blocks. + * @since 101.0.0 + * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. */ public function escapeCss($string) { @@ -961,7 +962,7 @@ public function stripTags($data, $allowableTags = null, $allowHtmlEntities = fal * * @param string $string * @return string - * @deprecated Use $escaper directly in templates and in blocks. + * @deprecated 103.0.0 Use $escaper directly in templates and in blocks. */ public function escapeUrl($string) { @@ -973,7 +974,7 @@ public function escapeUrl($string) * * @param string $data * @return string - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function escapeXssInUrl($data) { @@ -988,7 +989,7 @@ public function escapeXssInUrl($data) * @param string $data * @param bool $addSlashes * @return string - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function escapeQuote($data, $addSlashes = false) { @@ -1002,7 +1003,7 @@ public function escapeQuote($data, $addSlashes = false) * @param string $quote * * @return string|array - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ public function escapeJsQuote($data, $quote = '\'') { diff --git a/lib/internal/Magento/Framework/View/Element/Block/ArgumentInterface.php b/lib/internal/Magento/Framework/View/Element/Block/ArgumentInterface.php index 29b5557c9fcb1..00992d28f4df1 100644 --- a/lib/internal/Magento/Framework/View/Element/Block/ArgumentInterface.php +++ b/lib/internal/Magento/Framework/View/Element/Block/ArgumentInterface.php @@ -10,6 +10,7 @@ * All objects that are injected to block arguments should implement this interface. * * @api + * @since 101.0.0 */ interface ArgumentInterface { diff --git a/lib/internal/Magento/Framework/View/Element/BlockFactory.php b/lib/internal/Magento/Framework/View/Element/BlockFactory.php index d691af8d98f53..f159a9f202928 100644 --- a/lib/internal/Magento/Framework/View/Element/BlockFactory.php +++ b/lib/internal/Magento/Framework/View/Element/BlockFactory.php @@ -11,6 +11,7 @@ * Creates Blocks * * @api + * @since 100.0.2 */ class BlockFactory { diff --git a/lib/internal/Magento/Framework/View/Element/BlockInterface.php b/lib/internal/Magento/Framework/View/Element/BlockInterface.php index 1b4326694df97..565fdcc68188b 100644 --- a/lib/internal/Magento/Framework/View/Element/BlockInterface.php +++ b/lib/internal/Magento/Framework/View/Element/BlockInterface.php @@ -11,6 +11,7 @@ * Used to present information to user * * @api + * @since 100.0.2 */ interface BlockInterface { diff --git a/lib/internal/Magento/Framework/View/Element/Context.php b/lib/internal/Magento/Framework/View/Element/Context.php index 0f8123745e7e8..ce804e2ace255 100644 --- a/lib/internal/Magento/Framework/View/Element/Context.php +++ b/lib/internal/Magento/Framework/View/Element/Context.php @@ -22,6 +22,7 @@ * @SuppressWarnings(PHPMD) * * @api + * @since 100.0.2 */ class Context implements \Magento\Framework\ObjectManager\ContextInterface { @@ -379,6 +380,7 @@ public function getLocaleDate() * Lock guarded cache loader. * * @return LockGuardedCacheLoader + * @since 102.0.2 */ public function getLockGuardedCacheLoader() { diff --git a/lib/internal/Magento/Framework/View/Element/FormKey.php b/lib/internal/Magento/Framework/View/Element/FormKey.php index e6ecd20435ce8..be661f47cf8e2 100644 --- a/lib/internal/Magento/Framework/View/Element/FormKey.php +++ b/lib/internal/Magento/Framework/View/Element/FormKey.php @@ -13,6 +13,7 @@ * Element with FormKey * * @api + * @since 100.0.2 */ class FormKey extends \Magento\Framework\View\Element\AbstractBlock { diff --git a/lib/internal/Magento/Framework/View/Element/Html/Calendar.php b/lib/internal/Magento/Framework/View/Element/Html/Calendar.php index 0c8187569cf28..884488d77a74f 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Calendar.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Calendar.php @@ -13,6 +13,7 @@ * Prepares localization data for calendar * * @api + * @since 100.0.2 */ class Calendar extends \Magento\Framework\View\Element\Template { @@ -107,9 +108,7 @@ protected function _toHtml() ] ); - // get "today" and "week" words - $this->assign('today', $this->encoder->encode($localeData['fields']['day']['relative']['0'])); - $this->assign('week', $this->encoder->encode($localeData['fields']['week']['dn'])); + $this->assignFieldsValues($localeData); // get "am" & "pm" words $this->assign('am', $this->encoder->encode($localeData['calendar']['gregorian']['AmPmMarkers']['0'])); @@ -189,4 +188,25 @@ public function getYearRange() return (new \DateTime())->modify('- 100 years')->format('Y') . ':' . (new \DateTime())->modify('+ 100 years')->format('Y'); } + + /** + * Assign "fields" values from the ICU data + * + * @param \ResourceBundle $localeData + */ + private function assignFieldsValues(\ResourceBundle $localeData): void + { + /** + * Fields value in the current position has been added to ICU Data tables + * starting with ICU library version 51.1. + * Due to fact that we do not use these variables in templates, we do not initialize them for older versions + * + * @see https://github.com/unicode-org/icu/blob/release-50-2/icu4c/source/data/locales/en.txt + * @see https://github.com/unicode-org/icu/blob/release-51-2/icu4c/source/data/locales/en.txt + */ + if ($localeData->get('fields')) { + $this->assign('today', $this->encoder->encode($localeData['fields']['day']['relative']['0'])); + $this->assign('week', $this->encoder->encode($localeData['fields']['week']['dn'])); + } + } } diff --git a/lib/internal/Magento/Framework/View/Element/Html/Link.php b/lib/internal/Magento/Framework/View/Element/Html/Link.php index 6c5761f8cea25..d45bcd373e7a5 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Link.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Link.php @@ -5,6 +5,12 @@ */ namespace Magento\Framework\View\Element\Html; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Element\Template; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\View\Element\Template\Context; + /** * HTML anchor element block * @@ -13,6 +19,7 @@ * @method string getTitle() * * @api + * @since 100.0.2 */ class Link extends \Magento\Framework\View\Element\Template { @@ -51,6 +58,33 @@ class Link extends \Magento\Framework\View\Element\Template 'onkeyup', // %events ]; + /** + * @var SecureHtmlRenderer + */ + private $secureRenderer; + + /** + * @var Random + */ + private $random; + + /** + * @param Context $context + * @param array $data + * @param SecureHtmlRenderer|null $secureRenderer + * @param Random|null $random + */ + public function __construct( + Context $context, + array $data = [], + ?SecureHtmlRenderer $secureRenderer = null, + ?Random $random = null + ) { + parent::__construct($context, $data); + $this->secureRenderer = $secureRenderer ?? ObjectManager::getInstance()->get(SecureHtmlRenderer::class); + $this->random = $random ?? ObjectManager::getInstance()->get(Random::class); + } + /** * Prepare link attributes as serialized and formatted string * @@ -60,6 +94,9 @@ public function getLinkAttributes() { $attributes = []; foreach ($this->allowedAttributes as $attribute) { + if ($attribute === 'style' || mb_strpos($attribute, 'on') === 0) { + continue; + } $value = $this->getDataUsingMethod($attribute); if ($value !== null) { $attributes[$attribute] = $this->escapeHtml($value); @@ -103,7 +140,12 @@ protected function _toHtml() return parent::_toHtml(); } - return '<li><a ' . $this->getLinkAttributes() . ' >' . $this->escapeHtml($this->getLabel()) . '</a></li>'; + if (!$this->getDataUsingMethod('id')) { + $this->setDataUsingMethod('id', 'id' .$this->random->getRandomString(8)); + } + + return '<li><a ' . $this->getLinkAttributes() . ' >' . $this->escapeHtml($this->getLabel()) . '</a></li>' + .$this->renderSpecialAttributes(); } /** @@ -115,4 +157,34 @@ public function getHref() { return $this->getUrl($this->getPath()); } + + /** + * Render attributes that require separate tags. + * + * @return string + */ + private function renderSpecialAttributes(): string + { + $id = $this->getDataUsingMethod('id'); + if (!$id) { + throw new \RuntimeException('ID is required to render the link'); + } + + $html = ''; + $style = $this->getDataUsingMethod('style'); + if ($style) { + $html .= $this->secureRenderer->renderStyleAsTag($style, "#$id"); + } + foreach ($this->allowedAttributes as $attribute) { + if (mb_strpos($attribute, 'on') === 0 && $value = $this->getDataUsingMethod($attribute)) { + $html .= $this->secureRenderer->renderEventListenerAsTag( + $attribute, + $value, + "#$id" + ); + } + } + + return $html; + } } diff --git a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php index 3bd0677c6a443..81c6edaa543d3 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Link/Current.php @@ -20,6 +20,7 @@ * @method null|array getAttributes() * @method null|bool getCurrent() * @method \Magento\Framework\View\Element\Html\Link\Current setCurrent(bool $value) + * @since 100.0.2 */ class Current extends Template { @@ -92,9 +93,31 @@ private function getMca() */ public function isCurrent() { + $urlByPath = preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getPath())); return $this->getCurrent() || - preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getPath())) - == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getMca())); + ($urlByPath == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getUrl($this->getMca()))) || + $this->isCurrentCmsUrl($urlByPath); + } + + /** + * Get Current displayed page url + * + * @return string + */ + private function getCurrentUrl() + { + return $this->getUrl('*/*/*', ['_current' => false, '_use_rewrite' => true]); + } + + /** + * Check if link URL equivalent to URL of currently displayed CMS page + * + * @param string $urlByPath + * @return bool + */ + private function isCurrentCmsUrl($urlByPath) + { + return ($urlByPath == preg_replace(self::REGEX_INDEX_URL_PATTERN, '', $this->getCurrentUrl())); } /** diff --git a/lib/internal/Magento/Framework/View/Element/Html/Links.php b/lib/internal/Magento/Framework/View/Element/Html/Links.php index 472e24d7f2bfa..8bca55fcbb61d 100644 --- a/lib/internal/Magento/Framework/View/Element/Html/Links.php +++ b/lib/internal/Magento/Framework/View/Element/Html/Links.php @@ -9,6 +9,7 @@ * Links list block * * @api + * @since 100.0.2 */ class Links extends \Magento\Framework\View\Element\Template { diff --git a/lib/internal/Magento/Framework/View/Element/Js/Components.php b/lib/internal/Magento/Framework/View/Element/Js/Components.php index 8e33ca5581960..6b5eb78f9994a 100644 --- a/lib/internal/Magento/Framework/View/Element/Js/Components.php +++ b/lib/internal/Magento/Framework/View/Element/Js/Components.php @@ -12,6 +12,7 @@ * Block for Components * * @api + * @since 100.0.2 */ class Components extends Template { diff --git a/lib/internal/Magento/Framework/View/Element/Js/Cookie.php b/lib/internal/Magento/Framework/View/Element/Js/Cookie.php index b37a9387c07f6..220318bf5392e 100644 --- a/lib/internal/Magento/Framework/View/Element/Js/Cookie.php +++ b/lib/internal/Magento/Framework/View/Element/Js/Cookie.php @@ -13,6 +13,7 @@ * Block passes configuration for cookies set by JS * * @api + * @since 100.0.2 */ class Cookie extends Template { diff --git a/lib/internal/Magento/Framework/View/Element/Message/InterpretationMediator.php b/lib/internal/Magento/Framework/View/Element/Message/InterpretationMediator.php index eb4312e523b45..306cf89c0bb01 100644 --- a/lib/internal/Magento/Framework/View/Element/Message/InterpretationMediator.php +++ b/lib/internal/Magento/Framework/View/Element/Message/InterpretationMediator.php @@ -3,10 +3,15 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Framework\View\Element\Message; use Magento\Framework\Message\MessageInterface; +/** + * Can try and interpret a given message or fall back to the message text if not possible + */ class InterpretationMediator implements InterpretationStrategyInterface { /** @@ -34,8 +39,8 @@ public function interpret(MessageInterface $message) if ($message->getIdentifier()) { try { return $this->interpretationStrategy->interpret($message); + // phpcs:ignore Magento2.CodeAnalysis.EmptyBlock.DetectedCatch } catch (\LogicException $e) { - // pass } } diff --git a/lib/internal/Magento/Framework/View/Element/Messages.php b/lib/internal/Magento/Framework/View/Element/Messages.php index 409e678d4e30a..0af9a915966bf 100644 --- a/lib/internal/Magento/Framework/View/Element/Messages.php +++ b/lib/internal/Magento/Framework/View/Element/Messages.php @@ -11,6 +11,7 @@ * Class Messages * * @api + * @since 100.0.2 */ class Messages extends Template { diff --git a/lib/internal/Magento/Framework/View/Element/RendererInterface.php b/lib/internal/Magento/Framework/View/Element/RendererInterface.php index e9be81b7f9d20..4137739c08757 100644 --- a/lib/internal/Magento/Framework/View/Element/RendererInterface.php +++ b/lib/internal/Magento/Framework/View/Element/RendererInterface.php @@ -9,6 +9,7 @@ * Magento Block interface * * @api + * @since 100.0.2 */ interface RendererInterface { diff --git a/lib/internal/Magento/Framework/View/Element/RendererList.php b/lib/internal/Magento/Framework/View/Element/RendererList.php index eba78867d7725..a0beb59b14fd9 100644 --- a/lib/internal/Magento/Framework/View/Element/RendererList.php +++ b/lib/internal/Magento/Framework/View/Element/RendererList.php @@ -9,6 +9,7 @@ * Get renderer by code * * @api + * @since 100.0.2 */ class RendererList extends AbstractBlock { diff --git a/lib/internal/Magento/Framework/View/Element/Template.php b/lib/internal/Magento/Framework/View/Element/Template.php index 99c876b440665..53355203213fa 100644 --- a/lib/internal/Magento/Framework/View/Element/Template.php +++ b/lib/internal/Magento/Framework/View/Element/Template.php @@ -28,6 +28,7 @@ * @api * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class Template extends AbstractBlock { diff --git a/lib/internal/Magento/Framework/View/Element/Template/Context.php b/lib/internal/Magento/Framework/View/Element/Template/Context.php index 4538fb33a9726..48c91ae29bfa9 100644 --- a/lib/internal/Magento/Framework/View/Element/Template/Context.php +++ b/lib/internal/Magento/Framework/View/Element/Template/Context.php @@ -20,6 +20,7 @@ * * @api * @SuppressWarnings(PHPMD) + * @since 100.0.2 */ class Context extends \Magento\Framework\View\Element\Context { diff --git a/lib/internal/Magento/Framework/View/Element/Text.php b/lib/internal/Magento/Framework/View/Element/Text.php index 687317576c358..b1197006349da 100644 --- a/lib/internal/Magento/Framework/View/Element/Text.php +++ b/lib/internal/Magento/Framework/View/Element/Text.php @@ -9,6 +9,7 @@ * Class Text * * @api + * @since 100.0.2 */ class Text extends \Magento\Framework\View\Element\AbstractBlock { diff --git a/lib/internal/Magento/Framework/View/Element/Text/ListText.php b/lib/internal/Magento/Framework/View/Element/Text/ListText.php index a18ed78967139..d1892b3e11bd7 100644 --- a/lib/internal/Magento/Framework/View/Element/Text/ListText.php +++ b/lib/internal/Magento/Framework/View/Element/Text/ListText.php @@ -11,6 +11,7 @@ * Class ListText * * @api + * @since 100.0.2 */ class ListText extends \Magento\Framework\View\Element\Text { diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/ManagerInterface.php b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/ManagerInterface.php index a66233f99f4d5..525e9ecb52b20 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/ManagerInterface.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/ManagerInterface.php @@ -7,7 +7,7 @@ /** * Interface ManagerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ interface ManagerInterface { diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Component/Definition.php b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Component/Definition.php index 6fd041d582495..56c945ab4fd28 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Component/Definition.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Component/Definition.php @@ -114,7 +114,7 @@ protected function prepareComponentData(array $componentsData) * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Template.php b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Template.php index 59da9bc8aa5b1..074e18738c3b8 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Template.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/Config/Provider/Template.php @@ -117,7 +117,7 @@ public function getTemplate($template) * Get serializer * * @return \Magento\Framework\Serialize\SerializerInterface - * @deprecated 100.2.0 + * @deprecated 101.0.0 */ private function getSerializer() { diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/DataProviderInterface.php b/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/DataProviderInterface.php index 9f3121f4e495d..d939a01648f6b 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/DataProviderInterface.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/DataProviderInterface.php @@ -11,6 +11,7 @@ * Interface DataProviderInterface * * @api + * @since 100.0.2 */ interface DataProviderInterface { diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/FilterPool.php b/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/FilterPool.php index 2c0b16f27df8b..2e22199773e90 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/FilterPool.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/FilterPool.php @@ -14,6 +14,7 @@ * Filter poll apply filters from search criteria * * @api + * @since 100.0.2 */ class FilterPool { diff --git a/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/SearchResult.php b/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/SearchResult.php index e1aa6a0605dab..32334e89c5876 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/SearchResult.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponent/DataProvider/SearchResult.php @@ -95,7 +95,7 @@ public function __construct( } /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @return ResourceConnection */ private function getResourceConnection() diff --git a/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php b/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php index 16c06fe3abf3a..5e1bc93b9c033 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponentFactory.php @@ -25,6 +25,7 @@ * * @api * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + * @since 100.0.2 */ class UiComponentFactory extends DataObject { @@ -50,7 +51,7 @@ class UiComponentFactory extends DataObject /** * UI component manager * - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var ManagerInterface */ protected $componentManager; diff --git a/lib/internal/Magento/Framework/View/Element/UiComponentInterface.php b/lib/internal/Magento/Framework/View/Element/UiComponentInterface.php index 3ae2627881101..19f06fd6b2ba4 100644 --- a/lib/internal/Magento/Framework/View/Element/UiComponentInterface.php +++ b/lib/internal/Magento/Framework/View/Element/UiComponentInterface.php @@ -11,6 +11,7 @@ * Interface UiComponentInterface * * @api + * @since 100.0.2 */ interface UiComponentInterface extends BlockInterface { diff --git a/lib/internal/Magento/Framework/View/File/CollectorInterface.php b/lib/internal/Magento/Framework/View/File/CollectorInterface.php index 31554d2a79238..dec8d6ea093b6 100644 --- a/lib/internal/Magento/Framework/View/File/CollectorInterface.php +++ b/lib/internal/Magento/Framework/View/File/CollectorInterface.php @@ -11,6 +11,7 @@ * Interface of locating view files in the file system * * @api + * @since 100.0.2 */ interface CollectorInterface { diff --git a/lib/internal/Magento/Framework/View/FileSystem.php b/lib/internal/Magento/Framework/View/FileSystem.php index db269345eab98..687b6adcb0f8e 100644 --- a/lib/internal/Magento/Framework/View/FileSystem.php +++ b/lib/internal/Magento/Framework/View/FileSystem.php @@ -9,6 +9,7 @@ * Model that finds file paths by their fileId * * @api + * @since 100.0.2 */ class FileSystem { diff --git a/lib/internal/Magento/Framework/View/Helper/Js.php b/lib/internal/Magento/Framework/View/Helper/Js.php index e2a30cdd1a527..c4b14ad88d45a 100644 --- a/lib/internal/Magento/Framework/View/Helper/Js.php +++ b/lib/internal/Magento/Framework/View/Helper/Js.php @@ -9,8 +9,27 @@ */ namespace Magento\Framework\View\Helper; +use Magento\Framework\App\ObjectManager; + +/** + * Class Js help render script. + */ class Js { + /** + * @var SecureHtmlRenderer + */ + protected $secureRenderer; + + /** + * @param SecureHtmlRenderer $htmlRenderer + */ + public function __construct( + SecureHtmlRenderer $htmlRenderer + ) { + $this->secureRenderer = $htmlRenderer; + } + /** * Retrieve framed javascript * @@ -19,6 +38,8 @@ class Js */ public function getScript($script) { - return '<script type="text/javascript">//<![CDATA[' . "\n{$script}\n" . '//]]></script>'; + $scriptString = '//<![CDATA[' . "\n{$script}\n" . '//]]>'; + + return /* @noEscape */ $this->secureRenderer->renderTag('script', [], $scriptString, false); } } diff --git a/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/EventHandlerData.php b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/EventHandlerData.php new file mode 100644 index 0000000000000..9d7afc5211cd2 --- /dev/null +++ b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/EventHandlerData.php @@ -0,0 +1,55 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper\SecureHtmlRender; + +/** + * JS event handler data. + */ +class EventHandlerData +{ + /** + * @var string + */ + private $event; + + /** + * @var string + */ + private $code; + + /** + * @param string $event + * @param string $code + */ + public function __construct(string $event, string $code) + { + $this->event = $event; + $this->code = $code; + } + + /** + * Full event name like "onclick" + * + * @return string + */ + public function getEvent(): string + { + return $this->event; + } + + /** + * JavaScript code. + * + * @return string + */ + public function getCode(): string + { + return $this->code; + } +} diff --git a/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/HtmlRenderer.php b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/HtmlRenderer.php new file mode 100644 index 0000000000000..d8705b63cd0a9 --- /dev/null +++ b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/HtmlRenderer.php @@ -0,0 +1,72 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +namespace Magento\Framework\View\Helper\SecureHtmlRender; + +use Magento\Framework\Escaper; + +/** + * Renders HTML based on provided data. + */ +class HtmlRenderer +{ + /** + * @var Escaper + */ + private $escaper; + + /** + * @param Escaper $escaper + */ + public function __construct(Escaper $escaper) + { + $this->escaper = $escaper; + } + + /** + * Render the tag. + * + * @param TagData $tagData + * @return string + */ + public function renderTag(TagData $tagData): string + { + $attributesHtmls = []; + foreach ($tagData->getAttributes() as $attribute => $value) { + $attributesHtmls[] = $attribute . '="' .$this->escaper->escapeHtmlAttr($value) .'"'; + } + $content = null; + if ($tagData->getContent() !== null) { + $content = $tagData->isTextContent() + ? $this->escaper->escapeHtml($tagData->getContent()) : $tagData->getContent(); + } + $attributesHtml = ''; + if ($attributesHtmls) { + $attributesHtml = ' ' .implode(' ', $attributesHtmls); + } + + $html = '<' .$tagData->getTag() .$attributesHtml; + if ($content) { + $html .= '>' .$content .'</' .$tagData->getTag() .'>'; + } else { + $html .= '/>'; + } + + return $html; + } + + /** + * Render the handler as an HTML attribute. + * + * @param EventHandlerData $eventHandlerData + * @return string + */ + public function renderEventHandler(EventHandlerData $eventHandlerData): string + { + return $eventHandlerData->getEvent() .'="' + .$this->escaper->escapeHtmlAttr($eventHandlerData->getCode()) .'"'; + } +} diff --git a/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/SecurityProcessorInterface.php b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/SecurityProcessorInterface.php new file mode 100644 index 0000000000000..c02394a17e1aa --- /dev/null +++ b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/SecurityProcessorInterface.php @@ -0,0 +1,31 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper\SecureHtmlRender; + +/** + * Perform security related modifications or logic for HTML being rendered. + */ +interface SecurityProcessorInterface +{ + /** + * Process a tag. + * + * @param TagData $tagData + * @return TagData + */ + public function processTag(TagData $tagData): TagData; + + /** + * Process an event handler. + * + * @param EventHandlerData $eventHandlerData + * @return EventHandlerData + */ + public function processEventHandler(EventHandlerData $eventHandlerData): EventHandlerData; +} diff --git a/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/TagData.php b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/TagData.php new file mode 100644 index 0000000000000..c856dae75d023 --- /dev/null +++ b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRender/TagData.php @@ -0,0 +1,89 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper\SecureHtmlRender; + +/** + * Tag data to render. + */ +class TagData +{ + /** + * @var string + */ + private $tag; + + /** + * @var string[] + */ + private $attributes; + + /** + * @var string|null + */ + private $content; + + /** + * @var bool + */ + private $textContent; + + /** + * @param string $tag + * @param string[] $attributes + * @param string|null $content + * @param bool $textContent + */ + public function __construct(string $tag, array $attributes, ?string $content, bool $textContent) + { + $this->tag = $tag; + $this->attributes = $attributes; + $this->content = $content; + $this->textContent = $textContent; + } + + /** + * Tag name (like "style", "script" etc) + * + * @return string + */ + public function getTag(): string + { + return $this->tag; + } + + /** + * Attributes list, not escaped. + * + * @return string[] + */ + public function getAttributes(): array + { + return $this->attributes; + } + + /** + * Text or HTML inner content. + * + * @return string|null + */ + public function getContent(): ?string + { + return $this->content; + } + + /** + * Is the content to be treated as text or HTML? + * + * @return bool + */ + public function isTextContent(): bool + { + return $this->textContent; + } +} diff --git a/lib/internal/Magento/Framework/View/Helper/SecureHtmlRenderer.php b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRenderer.php new file mode 100644 index 0000000000000..ae8ab3f15bc96 --- /dev/null +++ b/lib/internal/Magento/Framework/View/Helper/SecureHtmlRenderer.php @@ -0,0 +1,169 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +declare(strict_types=1); + +namespace Magento\Framework\View\Helper; + +use Magento\Framework\Api\SimpleDataObjectConverter; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRender\EventHandlerData; +use Magento\Framework\View\Helper\SecureHtmlRender\HtmlRenderer; +use Magento\Framework\View\Helper\SecureHtmlRender\SecurityProcessorInterface; +use Magento\Framework\View\Helper\SecureHtmlRender\TagData; + +/** + * Render HTML elements with consideration to application security. + */ +class SecureHtmlRenderer +{ + /** + * @var HtmlRenderer + */ + private $renderer; + + /** + * @var SecurityProcessorInterface[] + */ + private $processors; + + /** + * @var Random + */ + private $random; + + /** + * @param HtmlRenderer $renderer + * @param Random $random + * @param SecurityProcessorInterface[] $processors + */ + public function __construct(HtmlRenderer $renderer, Random $random, array $processors = []) + { + $this->renderer = $renderer; + $this->random = $random; + $this->processors = $processors; + } + + /** + * Renders HTML tag while possibly modifying or using it's attributes and content for security reasons. + * + * @param string $tagName Like "script" or "style" + * @param string[] $attributes Attributes map, values must not be escaped. + * @param string|null $content Tag's content. + * @param bool $textContent Whether to treat the tag's content as text or HTML. + * @return string + */ + public function renderTag( + string $tagName, + array $attributes, + ?string $content = null, + bool $textContent = true + ): string { + $tag = new TagData($tagName, $attributes, $content, $textContent); + foreach ($this->processors as $processor) { + $tag = $processor->processTag($tag); + } + + return $this->renderer->renderTag($tag); + } + + /** + * Render event listener as an HTML attribute while possibly modifying or using it's content for security reasons. + * + * @param string $eventName Full attribute name like "onclick". + * @param string $javascript + * @return string + */ + public function renderEventListener(string $eventName, string $javascript): string + { + $event = new EventHandlerData($eventName, $javascript); + foreach ($this->processors as $processor) { + $event = $processor->processEventHandler($event); + } + + return $this->renderer->renderEventHandler($event); + } + + /** + * Render event listener script as a separate tag instead of an attribute. + * + * @param string $eventName Full event name like "onclick". + * @param string $attributeJavascript JS that would've gone to an HTML attribute. + * @param string $elementSelector CSS selector for the element we handle the event for. + * @return string Result script tag. + */ + public function renderEventListenerAsTag( + string $eventName, + string $attributeJavascript, + string $elementSelector + ): string { + if (!$eventName || !$attributeJavascript || !$elementSelector || mb_strpos($eventName, 'on') !== 0) { + throw new \InvalidArgumentException('Invalid JS event handler data provided'); + } + + $random = $this->random->getRandomString(10); + $listenerFunction = 'eventListener' .$random; + $elementName = 'listenedElement' .$random; + $script = <<<script + function {$listenerFunction} () { + {$attributeJavascript}; + } + var {$elementName} = document.querySelector("{$elementSelector}"); + if ({$elementName}) { + {$elementName}.{$eventName} = function (event) { + var targetElement = {$elementName}; + if (event && event.target) { + targetElement = event.target; + } + {$listenerFunction}.apply(targetElement); + } + } +script; + + return $this->renderTag('script', ['type' => 'text/javascript'], $script, false); + } + + /** + * Render "style" attribute as a separate tag instead. + * + * @param string $style + * @param string $selector Must resolve to a single node. + * @return string + */ + public function renderStyleAsTag(string $style, string $selector): string + { + $stylePairs = array_filter(array_map('trim', explode(';', $style))); + if (!$stylePairs || !$selector) { + throw new \InvalidArgumentException('Invalid style data given'); + } + + $elementVariable = 'elem' .$this->random->getRandomString(8); + /** @var string[] $styles */ + $stylesAssignments = ''; + foreach ($stylePairs as $stylePair) { + $exploded = array_map('trim', explode(':', $stylePair)); + if (count($exploded) < 2) { + throw new \InvalidArgumentException('Invalid CSS given'); + } + //Converting to camelCase + $styleAttribute = lcfirst(str_replace(' ', '', ucwords(str_replace('-', ' ', $exploded[0])))); + if (count($exploded) > 2) { + //For cases when ":" is encountered in the style's value. + $exploded[1] = join('', array_slice($exploded, 1)); + } + $styleValue = str_replace('\'', '\\\'', trim($exploded[1])); + $stylesAssignments .= "$elementVariable.style.$styleAttribute = '$styleValue';\n"; + } + + return $this->renderTag( + 'script', + ['type' => 'text/javascript'], + "var $elementVariable = document.querySelector('$selector');\n" + ."if ($elementVariable) {\n{$stylesAssignments}}", + false + ); + } +} diff --git a/lib/internal/Magento/Framework/View/Layout/BuilderInterface.php b/lib/internal/Magento/Framework/View/Layout/BuilderInterface.php index f579a539a12a9..1d28952aaa0f7 100644 --- a/lib/internal/Magento/Framework/View/Layout/BuilderInterface.php +++ b/lib/internal/Magento/Framework/View/Layout/BuilderInterface.php @@ -11,6 +11,7 @@ * Interface BuilderInterface * * @api + * @since 100.0.2 */ interface BuilderInterface { diff --git a/lib/internal/Magento/Framework/View/Layout/Data/Structure.php b/lib/internal/Magento/Framework/View/Layout/Data/Structure.php index d9aa739fb04f9..3ec5f8b50c7da 100644 --- a/lib/internal/Magento/Framework/View/Layout/Data/Structure.php +++ b/lib/internal/Magento/Framework/View/Layout/Data/Structure.php @@ -12,6 +12,7 @@ * An associative data structure, that features "nested set" parent-child relations * * @api + * @since 100.0.2 */ class Structure extends DataStructure { diff --git a/lib/internal/Magento/Framework/View/Layout/Element.php b/lib/internal/Magento/Framework/View/Layout/Element.php index 203af4bf1553c..f8ce682dcded2 100644 --- a/lib/internal/Magento/Framework/View/Layout/Element.php +++ b/lib/internal/Magento/Framework/View/Layout/Element.php @@ -9,6 +9,7 @@ * Class Element * * @api + * @since 100.0.2 */ class Element extends \Magento\Framework\Simplexml\Element { diff --git a/lib/internal/Magento/Framework/View/Layout/Generator/Context.php b/lib/internal/Magento/Framework/View/Layout/Generator/Context.php index 951724b062829..606372d366722 100644 --- a/lib/internal/Magento/Framework/View/Layout/Generator/Context.php +++ b/lib/internal/Magento/Framework/View/Layout/Generator/Context.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class Context { diff --git a/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php b/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php index a585eda37df68..a8f929bc08084 100644 --- a/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php +++ b/lib/internal/Magento/Framework/View/Layout/GeneratorPool.php @@ -5,11 +5,16 @@ */ namespace Magento\Framework\View\Layout; +use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\State; use Magento\Framework\View\Layout\Condition\ConditionFactory; +use Psr\Log\LoggerInterface; /** * Pool of generators for structural elements * @api + * @since 100.0.2 + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class GeneratorPool { @@ -24,31 +29,39 @@ class GeneratorPool protected $generators = []; /** - * @var \Psr\Log\LoggerInterface + * @var LoggerInterface */ protected $logger; /** - * @var \Magento\Framework\View\Layout\Condition\ConditionFactory + * @var ConditionFactory */ private $conditionFactory; + /** + * @var State + */ + private $state; + /** * @param ScheduledStructure\Helper $helper * @param ConditionFactory $conditionFactory - * @param \Psr\Log\LoggerInterface $logger + * @param LoggerInterface $logger * @param array|null $generators + * @param State|null $state */ public function __construct( ScheduledStructure\Helper $helper, ConditionFactory $conditionFactory, - \Psr\Log\LoggerInterface $logger, - array $generators = null + LoggerInterface $logger, + array $generators = null, + ?State $state = null ) { $this->helper = $helper; $this->conditionFactory = $conditionFactory; $this->logger = $logger; $this->addGenerators($generators); + $this->state = $state ?? ObjectManager::getInstance()->get(State::class); } /** @@ -226,7 +239,9 @@ protected function moveElementInStructure( $structure->setAsChild($element, $destination, $alias); $structure->reorderChildElement($destination, $element, $siblingName, $isAfter); } catch (\OutOfBoundsException $e) { - $this->logger->warning('Broken reference: ' . $e->getMessage()); + if ($this->state->getMode() === State::MODE_DEVELOPER) { + $this->logger->warning('Broken reference: ' . $e->getMessage()); + } } $scheduledStructure->unsetElementFromBrokenParentList($element); return $this; @@ -238,7 +253,7 @@ protected function moveElementInStructure( * @param array $data * * @return bool - * @since 100.2.0 + * @since 101.0.0 */ protected function visibilityConditionsExistsIn(array $data) { diff --git a/lib/internal/Magento/Framework/View/Layout/Reader/Block.php b/lib/internal/Magento/Framework/View/Layout/Reader/Block.php index b287b517a454c..c24cda37defdb 100644 --- a/lib/internal/Magento/Framework/View/Layout/Reader/Block.php +++ b/lib/internal/Magento/Framework/View/Layout/Reader/Block.php @@ -81,7 +81,7 @@ class Block implements Layout\ReaderInterface private $conditionReader; /** - * @deprecated 100.2.0 + * @deprecated 101.0.0 * @var string */ private $deprecatedAttributeAcl = 'acl'; diff --git a/lib/internal/Magento/Framework/View/Layout/Reader/Context.php b/lib/internal/Magento/Framework/View/Layout/Reader/Context.php index 806c779186dc0..f9ca09478d600 100644 --- a/lib/internal/Magento/Framework/View/Layout/Reader/Context.php +++ b/lib/internal/Magento/Framework/View/Layout/Reader/Context.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class Context { diff --git a/lib/internal/Magento/Framework/View/Layout/ScheduledStructure.php b/lib/internal/Magento/Framework/View/Layout/ScheduledStructure.php index 3193e10282fd4..aae2e9a68fe3c 100644 --- a/lib/internal/Magento/Framework/View/Layout/ScheduledStructure.php +++ b/lib/internal/Magento/Framework/View/Layout/ScheduledStructure.php @@ -9,6 +9,7 @@ * Layout structure model * * @api + * @since 100.0.2 */ class ScheduledStructure { @@ -489,7 +490,7 @@ public function flushScheduledStructure() * Reformat 'Layout scheduled structure' to array. * * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function __toArray() { @@ -506,7 +507,7 @@ public function __toArray() * * @param array $data * @return void - * @since 100.2.0 + * @since 101.0.0 */ public function populateWithArray(array $data) { diff --git a/lib/internal/Magento/Framework/View/LayoutInterface.php b/lib/internal/Magento/Framework/View/LayoutInterface.php index a8ad5e28de2d6..3a63b5ccc9ea3 100644 --- a/lib/internal/Magento/Framework/View/LayoutInterface.php +++ b/lib/internal/Magento/Framework/View/LayoutInterface.php @@ -8,6 +8,7 @@ /** * Interface LayoutInterface * @api + * @since 100.0.2 */ interface LayoutInterface { diff --git a/lib/internal/Magento/Framework/View/Page/Builder.php b/lib/internal/Magento/Framework/View/Page/Builder.php index 66cc3f588a9a0..846ffd119dabd 100644 --- a/lib/internal/Magento/Framework/View/Page/Builder.php +++ b/lib/internal/Magento/Framework/View/Page/Builder.php @@ -6,11 +6,13 @@ namespace Magento\Framework\View\Page; use Magento\Framework\App; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Event; use Magento\Framework\View; +use Magento\Framework\View\Model\PageLayout\Config\BuilderInterface; /** - * Class Builder + * Page Layout Builder */ class Builder extends View\Layout\Builder { @@ -24,23 +26,31 @@ class Builder extends View\Layout\Builder */ protected $pageLayoutReader; + /** + * @var BuilderInterface|mixed + */ + private $pageLayoutBuilder; + /** * @param View\LayoutInterface $layout * @param App\Request\Http $request * @param Event\ManagerInterface $eventManager * @param Config $pageConfig * @param Layout\Reader $pageLayoutReader + * @param BuilderInterface|null $pageLayoutBuilder */ public function __construct( View\LayoutInterface $layout, App\Request\Http $request, Event\ManagerInterface $eventManager, Config $pageConfig, - Layout\Reader $pageLayoutReader + Layout\Reader $pageLayoutReader, + ?BuilderInterface $pageLayoutBuilder = null ) { parent::__construct($layout, $request, $eventManager); $this->pageConfig = $pageConfig; $this->pageLayoutReader = $pageLayoutReader; + $this->pageLayoutBuilder = $pageLayoutBuilder ?? ObjectManager::getInstance()->get(BuilderInterface::class); $this->pageConfig->setBuilder($this); } @@ -57,6 +67,7 @@ protected function generateLayoutBlocks() /** * Read page layout and write structure to ReadContext + * * @return void */ protected function readPageLayout() @@ -69,10 +80,16 @@ protected function readPageLayout() } /** + * Get current page layout or fallback to default + * * @return string */ protected function getPageLayout() { - return $this->pageConfig->getPageLayout() ?: $this->layout->getUpdate()->getPageLayout(); + $pageLayout = $this->pageConfig->getPageLayout(); + + return ($pageLayout && $this->pageLayoutBuilder->getPageLayoutsConfig()->hasPageLayout($pageLayout)) + ? $pageLayout + : $this->layout->getUpdate()->getPageLayout(); } } diff --git a/lib/internal/Magento/Framework/View/Page/Config.php b/lib/internal/Magento/Framework/View/Page/Config.php index 44f4038860cda..ea71e49615e47 100644 --- a/lib/internal/Magento/Framework/View/Page/Config.php +++ b/lib/internal/Magento/Framework/View/Page/Config.php @@ -26,6 +26,7 @@ * @SuppressWarnings(PHPMD.TooManyFields) * * @api + * @since 100.0.2 */ class Config { @@ -384,6 +385,7 @@ public function getDescription() * Set meta title * * @param string $title + * @since 101.0.6 */ public function setMetaTitle($title) { @@ -394,6 +396,7 @@ public function setMetaTitle($title) * Retrieve meta title * * @return string + * @since 101.0.6 */ public function getMetaTitle() { diff --git a/lib/internal/Magento/Framework/View/Page/Config/Structure.php b/lib/internal/Magento/Framework/View/Page/Config/Structure.php index 1a181952ed990..78e3cf9f58c85 100644 --- a/lib/internal/Magento/Framework/View/Page/Config/Structure.php +++ b/lib/internal/Magento/Framework/View/Page/Config/Structure.php @@ -10,6 +10,7 @@ * Page config structure model * * @api + * @since 100.0.2 */ class Structure { @@ -216,7 +217,7 @@ public function getAssets() * Reformat 'Page config structure' to array. * * @return array - * @since 100.2.0 + * @since 101.0.0 */ public function __toArray() { @@ -233,7 +234,7 @@ public function __toArray() * * @param array $data * @return void - * @since 100.2.0 + * @since 101.0.0 */ public function populateWithArray(array $data) { diff --git a/lib/internal/Magento/Framework/View/Page/FaviconInterface.php b/lib/internal/Magento/Framework/View/Page/FaviconInterface.php index d7286029c466c..a28506e3f5906 100644 --- a/lib/internal/Magento/Framework/View/Page/FaviconInterface.php +++ b/lib/internal/Magento/Framework/View/Page/FaviconInterface.php @@ -9,6 +9,7 @@ * Favicon interface * * @api + * @since 100.0.2 */ interface FaviconInterface { diff --git a/lib/internal/Magento/Framework/View/Page/Title.php b/lib/internal/Magento/Framework/View/Page/Title.php index b0c8b155c878e..17bc4cea1cc92 100644 --- a/lib/internal/Magento/Framework/View/Page/Title.php +++ b/lib/internal/Magento/Framework/View/Page/Title.php @@ -12,6 +12,7 @@ * Page title * * @api + * @since 100.0.2 */ class Title { diff --git a/lib/internal/Magento/Framework/View/Render/RenderFactory.php b/lib/internal/Magento/Framework/View/Render/RenderFactory.php index 3f90bc2e8f2bb..d150ede9a5b78 100644 --- a/lib/internal/Magento/Framework/View/Render/RenderFactory.php +++ b/lib/internal/Magento/Framework/View/Render/RenderFactory.php @@ -12,6 +12,7 @@ * Class RenderFactory * * @api + * @since 100.0.2 */ class RenderFactory { diff --git a/lib/internal/Magento/Framework/View/RenderInterface.php b/lib/internal/Magento/Framework/View/RenderInterface.php index 041893f81232a..5f456dd1987eb 100644 --- a/lib/internal/Magento/Framework/View/RenderInterface.php +++ b/lib/internal/Magento/Framework/View/RenderInterface.php @@ -9,6 +9,7 @@ * Interface RenderInterface * * @api + * @since 100.0.2 */ interface RenderInterface { diff --git a/lib/internal/Magento/Framework/View/Result/Layout.php b/lib/internal/Magento/Framework/View/Result/Layout.php index 83e0a991cb959..2b08ddf69e372 100644 --- a/lib/internal/Magento/Framework/View/Result/Layout.php +++ b/lib/internal/Magento/Framework/View/Result/Layout.php @@ -19,6 +19,7 @@ * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * * @api + * @since 100.0.2 */ class Layout extends AbstractResult { diff --git a/lib/internal/Magento/Framework/View/Result/LayoutFactory.php b/lib/internal/Magento/Framework/View/Result/LayoutFactory.php index 4104e19686244..4b857ad3cf66b 100644 --- a/lib/internal/Magento/Framework/View/Result/LayoutFactory.php +++ b/lib/internal/Magento/Framework/View/Result/LayoutFactory.php @@ -10,6 +10,7 @@ /** * @api + * @since 100.0.2 */ class LayoutFactory { diff --git a/lib/internal/Magento/Framework/View/Result/Page.php b/lib/internal/Magento/Framework/View/Result/Page.php index 39cca1a111906..14a83a8320330 100644 --- a/lib/internal/Magento/Framework/View/Result/Page.php +++ b/lib/internal/Magento/Framework/View/Result/Page.php @@ -25,6 +25,7 @@ * @SuppressWarnings(PHPMD.DepthOfInheritance) * * @api + * @since 100.0.2 */ class Page extends Layout { diff --git a/lib/internal/Magento/Framework/View/Result/PageFactory.php b/lib/internal/Magento/Framework/View/Result/PageFactory.php index ef234ca4d4429..4de4c097d16a2 100644 --- a/lib/internal/Magento/Framework/View/Result/PageFactory.php +++ b/lib/internal/Magento/Framework/View/Result/PageFactory.php @@ -14,6 +14,7 @@ * which is by convention is determined from the controller action class * * @api + * @since 100.0.2 */ class PageFactory { diff --git a/lib/internal/Magento/Framework/View/Template/Html/Minifier.php b/lib/internal/Magento/Framework/View/Template/Html/Minifier.php index 796cc8bef0f28..0a8db80cae349 100644 --- a/lib/internal/Magento/Framework/View/Template/Html/Minifier.php +++ b/lib/internal/Magento/Framework/View/Template/Html/Minifier.php @@ -114,6 +114,18 @@ public function minify($file) { $dir = dirname($file); $fileName = basename($file); + $content = $this->readFactory->create($dir)->readFile($fileName); + //Storing Heredocs + $heredocs = []; + $content = preg_replace_callback( + '/<<<([A-z]+).*?\1;/ims', + function ($match) use (&$heredocs) { + $heredocs[] = $match[0]; + + return '__MINIFIED_HEREDOC__' .(count($heredocs) - 1); + }, + $content + ); $content = preg_replace( '#(?<!]]>)\s+</#', '</', @@ -136,7 +148,7 @@ public function minify($file) preg_replace( '#(?<!:)//[^\n\r]*(\<\?php)[^\n\r]*(\s\?\>)[^\n\r]*#', '', - $this->readFactory->create($dir)->readFile($fileName) + $content ) ) ) @@ -145,6 +157,15 @@ public function minify($file) ) ); + //Restoring Heredocs + $content = preg_replace_callback( + '/__MINIFIED_HEREDOC__(\d+)/ims', + function ($match) use ($heredocs) { + return $heredocs[(int)$match[1]]; + }, + $content + ); + if (!$this->htmlDirectory->isExist()) { $this->htmlDirectory->create(); } diff --git a/lib/internal/Magento/Framework/View/Template/Html/MinifierInterface.php b/lib/internal/Magento/Framework/View/Template/Html/MinifierInterface.php index edd8caeb914b3..98fe13ab3a12c 100644 --- a/lib/internal/Magento/Framework/View/Template/Html/MinifierInterface.php +++ b/lib/internal/Magento/Framework/View/Template/Html/MinifierInterface.php @@ -10,6 +10,7 @@ * HTML minifier * * @api + * @since 100.0.2 */ interface MinifierInterface { diff --git a/lib/internal/Magento/Framework/View/TemplateEngine/Php.php b/lib/internal/Magento/Framework/View/TemplateEngine/Php.php index 0437386e4fc2b..0a4e90504a350 100644 --- a/lib/internal/Magento/Framework/View/TemplateEngine/Php.php +++ b/lib/internal/Magento/Framework/View/TemplateEngine/Php.php @@ -7,7 +7,6 @@ namespace Magento\Framework\View\TemplateEngine; -use Magento\Framework\Escaper; use Magento\Framework\View\Element\BlockInterface; use Magento\Framework\View\TemplateEngineInterface; @@ -31,22 +30,22 @@ class Php implements TemplateEngineInterface protected $_helperFactory; /** - * @var Escaper + * @var object[] */ - private $escaper; + private $blockVariables = []; /** * Constructor * * @param \Magento\Framework\ObjectManagerInterface $helperFactory - * @param Escaper|null $escaper + * @param object[] $blockVariables */ public function __construct( \Magento\Framework\ObjectManagerInterface $helperFactory, - ?Escaper $escaper = null + array $blockVariables = [] ) { $this->_helperFactory = $helperFactory; - $this->escaper = $escaper ?? $helperFactory->get(Escaper::class); + $this->blockVariables = $blockVariables; } /** @@ -55,12 +54,11 @@ public function __construct( * Include the named PHTML template using the given block as the $this * reference, though only public methods will be accessible. * - * @param BlockInterface $block - * @param string $fileName - * @param array $dictionary + * @param BlockInterface $block + * @param string $fileName + * @param array $dictionary * @return string - * @throws \Exception - * @SuppressWarnings(PHPMD.UnusedLocalVariable) + * @throws \Throwable */ public function render(BlockInterface $block, $fileName, array $dictionary = []) { @@ -68,10 +66,8 @@ public function render(BlockInterface $block, $fileName, array $dictionary = []) try { $tmpBlock = $this->_currentBlock; $this->_currentBlock = $block; + $dictionary = array_merge($this->blockVariables, $dictionary); extract($dictionary, EXTR_SKIP); - //So it can be used in the template. - $escaper = $this->escaper; - // phpcs:ignore include $fileName; $this->_currentBlock = $tmpBlock; } catch (\Exception $exception) { diff --git a/lib/internal/Magento/Framework/View/TemplateEngine/Xhtml/CompilerInterface.php b/lib/internal/Magento/Framework/View/TemplateEngine/Xhtml/CompilerInterface.php index 565bb7c92324c..8228013df40a4 100644 --- a/lib/internal/Magento/Framework/View/TemplateEngine/Xhtml/CompilerInterface.php +++ b/lib/internal/Magento/Framework/View/TemplateEngine/Xhtml/CompilerInterface.php @@ -11,6 +11,7 @@ * Interface CompilerInterface * * @api + * @since 100.0.2 */ interface CompilerInterface { diff --git a/lib/internal/Magento/Framework/View/TemplateEnginePool.php b/lib/internal/Magento/Framework/View/TemplateEnginePool.php index 1aec127a17bd1..0f8d2032af879 100644 --- a/lib/internal/Magento/Framework/View/TemplateEnginePool.php +++ b/lib/internal/Magento/Framework/View/TemplateEnginePool.php @@ -7,6 +7,7 @@ /** * @api + * @since 100.0.2 */ class TemplateEnginePool { diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php index b6bf31f580ab6..852cb901e430e 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/Link/CurrentTest.php @@ -14,109 +14,159 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +/** + * @covers \Magento\Framework\View\Element\Html\Link\Current + */ class CurrentTest extends TestCase { /** - * @var MockObject + * @var UrlInterface|MockObject */ - protected $_urlBuilderMock; + private $_urlBuilderMock; /** - * @var MockObject + * @var Http|MockObject */ - protected $_requestMock; + private $_requestMock; /** - * @var ObjectManager + * @var Current */ - protected $_objectManager; + private $currentLink; + /** + * @inheritDoc + */ protected function setUp(): void { - $this->_objectManager = new ObjectManager($this); - $this->_urlBuilderMock = $this->getMockForAbstractClass(UrlInterface::class); + $this->_urlBuilderMock = $this->createMock(UrlInterface::class); $this->_requestMock = $this->createMock(Http::class); + + $this->currentLink = (new ObjectManager($this))->getObject( + Current::class, + [ + 'urlBuilder' => $this->_urlBuilderMock, + 'request' => $this->_requestMock + ] + ); } - public function testGetUrl() + /** + * Test get Url + */ + public function testGetUrl(): void { - $path = 'test/path'; - $url = 'http://example.com/asdasd'; + $pathStub = 'test/path'; + $urlStub = 'http://example.com/asdasd'; - $this->_urlBuilderMock->expects($this->once())->method('getUrl')->with($path)->willReturn($url); + $this->_urlBuilderMock->expects($this->once()) + ->method('getUrl') + ->with($pathStub) + ->will($this->returnValue($urlStub)); - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - ['urlBuilder' => $this->_urlBuilderMock] - ); + $this->currentLink->setPath($pathStub); - $link->setPath($path); - $this->assertEquals($url, $link->getHref()); + $this->assertEquals($urlStub, $this->currentLink->getHref()); } - public function testIsCurrentIfIsset() + /** + * Test if set current + */ + public function testIsCurrentIfIsset(): void { - /** @var Current $link */ - $link = $this->_objectManager->getObject(Current::class); - $link->setCurrent(true); - $this->assertTrue($link->isCurrent()); + $this->currentLink->setCurrent(true); + $this->assertTrue($this->currentLink->isCurrent()); } /** * Test if the current url is the same as link path * - * @return void + * @param string $pathStub + * @param string $urlStub + * @param array $request + * @param bool $expected + * @dataProvider isCurrentDataProvider */ - public function testIsCurrent() + public function testIsCurrent($pathStub, $urlStub, $request, $expected): void { - $path = 'test/index'; - $url = 'http://example.com/test/index'; - - $this->_requestMock->expects($this->once()) + $this->_requestMock->expects($this->any()) ->method('getPathInfo') - ->willReturn('/test/index/'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['pathInfoStub'])); + $this->_requestMock->expects($this->any()) ->method('getModuleName') - ->willReturn('test'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['moduleStub'])); + $this->_requestMock->expects($this->any()) ->method('getControllerName') - ->willReturn('index'); - $this->_requestMock->expects($this->once()) + ->will($this->returnValue($request['controllerStub'])); + $this->_requestMock->expects($this->any()) ->method('getActionName') - ->willReturn('index'); + ->will($this->returnValue($request['actionStub'])); + $this->_urlBuilderMock->expects($this->at(0)) ->method('getUrl') - ->with($path) - ->willReturn($url); + ->with($pathStub) + ->will($this->returnValue($urlStub)); $this->_urlBuilderMock->expects($this->at(1)) ->method('getUrl') - ->with('test/index') - ->willReturn($url); - - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - [ - 'urlBuilder' => $this->_urlBuilderMock, - 'request' => $this->_requestMock - ] - ); - - $link->setPath($path); - $this->assertTrue($link->isCurrent()); + ->with($request['mcaStub']) + ->will($this->returnValue($request['getUrl'])); + + if ($request['mcaStub'] == '') { + $this->_urlBuilderMock->expects($this->at(2)) + ->method('getUrl') + ->with('*/*/*', ['_current' => false, '_use_rewrite' => true]) + ->will($this->returnValue($urlStub)); + } + + $this->currentLink->setPath($pathStub); + $this->assertEquals($expected, $this->currentLink->isCurrent()); } - public function testIsCurrentFalse() + /** + * Data provider for is current + */ + public function isCurrentDataProvider(): array { - $this->_urlBuilderMock->expects($this->at(0))->method('getUrl')->willReturn('1'); - $this->_urlBuilderMock->expects($this->at(1))->method('getUrl')->willReturn('2'); - - /** @var Current $link */ - $link = $this->_objectManager->getObject( - Current::class, - ['urlBuilder' => $this->_urlBuilderMock, 'request' => $this->_requestMock] - ); - $this->assertFalse($link->isCurrent()); + return [ + 'url with MCA' => [ + 'pathStub' => 'test/path', + 'urlStub' => 'http://example.com/asdasd', + 'requestStub' => [ + 'pathInfoStub' => '/test/index/', + 'moduleStub' => 'test', + 'controllerStub' => 'index', + 'actionStub' => 'index', + 'mcaStub' => 'test/index', + 'getUrl' => 'http://example.com/asdasd/' + ], + 'excepted' => true + ], + 'url with CMS' => [ + 'pathStub' => 'test', + 'urlStub' => 'http://example.com/test', + 'requestStub' => [ + 'pathInfoStub' => '//test//', + 'moduleStub' => 'cms', + 'controllerStub' => 'page', + 'actionStub' => 'view', + 'mcaStub' => '', + 'getUrl' => 'http://example.com/' + ], + 'excepted' => true + ], + 'Test if is current false' => [ + 'pathStub' => 'test/path', + 'urlStub' => 'http://example.com/tests', + 'requestStub' => [ + 'pathInfoStub' => '/test/index/', + 'moduleStub' => 'test', + 'controllerStub' => 'index', + 'actionStub' => 'index', + 'mcaStub' => 'test/index', + 'getUrl' => 'http://example.com/asdasd/' + ], + 'excepted' => false + ] + ]; } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php index d7b78490615a2..ac158be6f6fd7 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Element/Html/LinkTest.php @@ -16,16 +16,23 @@ use Magento\Framework\View\Element\Template\File\Resolver; use Magento\Framework\View\Element\Template\File\Validator; use PHPUnit\Framework\TestCase; +use Magento\Framework\Math\Random; +use Magento\Framework\View\Helper\SecureHtmlRenderer; +use Magento\Framework\DataObject; +use Magento\Framework\Event\ManagerInterface; +/** + * Test Link widget. + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class LinkTest extends TestCase { + /** + * @var ObjectManager + */ private $objectManager; - protected function setUp(): void - { - $this->objectManager = new ObjectManager($this); - } - /** * @var array */ @@ -43,20 +50,18 @@ protected function setUp(): void */ protected $link; - public function testGetLinkAttributes() + protected function setUp(): void { - $escaperMock = $this->getMockBuilder(Escaper::class) - ->setMethods(['escapeHtml'])->disableOriginalConstructor() - ->getMock(); + $this->objectManager = new ObjectManager($this); + $escaperMock = $this->getMockBuilder(Escaper::class) + ->setMethods(['escapeHtml'])->disableOriginalConstructor()->getMock(); $escaperMock->expects($this->any()) ->method('escapeHtml') ->willReturnArgument(0); $urlBuilderMock = $this->getMockBuilder(UrlInterface::class) - ->setMethods(['getUrl'])->disableOriginalConstructor() - ->getMockForAbstractClass(); - + ->setMethods(['getUrl'])->disableOriginalConstructor()->getMockForAbstractClass(); $urlBuilderMock->expects($this->any()) ->method('getUrl') ->willReturn('http://site.com/link.html'); @@ -69,7 +74,7 @@ public function testGetLinkAttributes() ->willReturn(false); $scopeConfigMock = $this->getMockBuilder(Config::class) - ->setMethods(['isSetFlag'])->disableOriginalConstructor() + ->disableOriginalConstructor() ->getMock(); $scopeConfigMock->expects($this->any()) ->method('isSetFlag') @@ -80,54 +85,88 @@ public function testGetLinkAttributes() ->getMock(); $contextMock = $this->getMockBuilder(Context::class) - ->setMethods(['getEscaper', 'getUrlBuilder', 'getValidator', 'getResolver', 'getScopeConfig']) ->disableOriginalConstructor() ->getMock(); - $contextMock->expects($this->any()) ->method('getValidator') ->willReturn($validtorMock); - $contextMock->expects($this->any()) ->method('getResolver') ->willReturn($resolverMock); - $contextMock->expects($this->any()) ->method('getEscaper') ->willReturn($escaperMock); - $contextMock->expects($this->any()) ->method('getUrlBuilder') ->willReturn($urlBuilderMock); - $contextMock->expects($this->any()) ->method('getScopeConfig') ->willReturn($scopeConfigMock); - - /** @var Link $linkWithAttributes */ - $linkWithAttributes = $this->objectManager->getObject( + $contextMock->method('getEventManager') + ->willReturn($this->createMock(ManagerInterface::class)); + $randomMock = $this->createMock(Random::class); + $randomMock->method('getRandomString')->willReturn('random'); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $secureRendererMock->method('renderEventListenerAsTag') + ->willReturnCallback( + function (string $event, string $js, string $selector): string { + return "<script>document.querySelector('$selector').$event = function () { $js };</script>"; + } + ); + $secureRendererMock->method('renderStyleAsTag') + ->willReturnCallback( + function (string $style, string $selector): string { + return "<style>$selector { $style }</style>"; + } + ); + + /** @var \Magento\Framework\View\Element\Html\Link $linkWithAttributes */ + $this->link = $this->objectManager->getObject( Link::class, - ['context' => $contextMock] + ['context' => $contextMock, 'random' => $randomMock, 'secureRenderer' => $secureRendererMock] ); + } + public function testGetLinkAttributes(): void + { + $linkWithAttributes = clone $this->link; $this->assertEquals( 'href="http://site.com/link.html"', $linkWithAttributes->getLinkAttributes() ); /** @var Link $linkWithoutAttributes */ - $linkWithoutAttributes = $this->objectManager->getObject( - Link::class, - ['context' => $contextMock] - ); + $linkWithoutAttributes = clone $this->link; foreach ($this->allowedAttributes as $attribute) { $linkWithoutAttributes->setDataUsingMethod($attribute, $attribute); } $this->assertEquals( 'href="http://site.com/link.html" shape="shape" tabindex="tabindex"' - . ' onfocus="onfocus" onblur="onblur" id="id"', + . ' id="id"', $linkWithoutAttributes->getLinkAttributes() ); } + + public function testLinkHtml(): void + { + $this->link->setDataUsingMethod('style', 'display: block;'); + $this->link->setDataUsingMethod('onclick', 'alert("clicked");'); + + $html = $this->link->toHtml(); + $this->assertEquals( + '<li><a href="http://site.com/link.html" id="idrandom" ></a></li>' + .'<style>#idrandom { display: block; }</style>' + .'<script>document.querySelector(\'#idrandom\').onclick = function () { alert("clicked"); };</script>', + $html + ); + } } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Helper/JsTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Helper/JsTest.php index ad616c8506fa9..0d442da2d3f6b 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Helper/JsTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Helper/JsTest.php @@ -9,6 +9,8 @@ use Magento\Framework\View\Helper\Js; use PHPUnit\Framework\TestCase; +use Magento\Framework\DataObject; +use Magento\Framework\View\Helper\SecureHtmlRenderer; class JsTest extends TestCase { @@ -17,9 +19,18 @@ class JsTest extends TestCase */ public function testGetScript() { - $helper = new Js(); + $secureRendererMock = $this->createMock(SecureHtmlRenderer::class); + $secureRendererMock->method('renderTag') + ->willReturnCallback( + function (string $tag, array $attributes, string $content): string { + $attributes = new DataObject($attributes); + + return "<$tag {$attributes->serialize()}>$content</$tag>"; + } + ); + $helper = new Js($secureRendererMock); $this->assertEquals( - "<script type=\"text/javascript\">//<![CDATA[\ntest\n//]]></script>", + "<script >//<![CDATA[\ntest\n//]]></script>", $helper->getScript('test') ); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Page/BuilderTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Page/BuilderTest.php index 77bdd91041ad2..0f94e32b0a707 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Page/BuilderTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Page/BuilderTest.php @@ -8,9 +8,11 @@ namespace Magento\Framework\View\Test\Unit\Page; use Magento\Framework\View\Layout\Reader\Context; +use Magento\Framework\View\Model\PageLayout\Config\BuilderInterface; use Magento\Framework\View\Page\Builder; use Magento\Framework\View\Page\Config; use Magento\Framework\View\Page\Layout\Reader; +use Magento\Framework\View\PageLayout\Config as PageLayoutConfig; use PHPUnit\Framework\MockObject\MockObject; /** @@ -22,7 +24,7 @@ class BuilderTest extends \Magento\Framework\View\Test\Unit\Layout\BuilderTest /** * @param array $arguments - * @return \Magento\Framework\View\Page\Builder + * @return \Magento\Framework\View\Layout\Builder */ protected function getBuilder($arguments) { @@ -39,6 +41,15 @@ protected function getBuilder($arguments) $arguments['pageLayoutReader'] = $this->createMock(Reader::class); $arguments['pageLayoutReader']->expects($this->once())->method('read')->with($readerContext, 'test_layout'); + $pageLayoutConfig = $this->createMock(PageLayoutConfig::class); + $arguments['pageLayoutBuilder'] = $this->getMockForAbstractClass(BuilderInterface::class); + $arguments['pageLayoutBuilder']->expects($this->once()) + ->method('getPageLayoutsConfig') + ->willReturn($pageLayoutConfig); + $pageLayoutConfig->expects($this->once()) + ->method('hasPageLayout') + ->with('test_layout') + ->willReturn(true); return parent::getBuilder($arguments); } diff --git a/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php b/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php index 3e9db46c354d7..6aafa5a46cf63 100644 --- a/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php +++ b/lib/internal/Magento/Framework/View/Test/Unit/Template/Html/MinifierTest.php @@ -113,6 +113,8 @@ public function testGetPathToMinified() /** * Covered method minify and test regular expressions * @test + * + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function testMinify() { @@ -167,6 +169,12 @@ public function testMinify() <?php // echo \$block->getChildHtml('anotherChildBlock'); ?> <?php // endif; ?> </body> + <?php + \$sometext = <<<SOMETEXT + mytext + mytextline2 +SOMETEXT; + ?> </html> TEXT; @@ -189,7 +197,10 @@ public function testMinify() } }); //]]> -</script><?php echo "http://some.link.com/" ?> <?php echo "//some.link.com/" ?> <?php echo '//some.link.com/' ?> <em>inline text</em> <a href="http://www.<?php echo 'hi' ?>"></a> <?php ?> <?php echo \$block->getChildHtml('someChildBlock'); ?> <?php ?> <?php ?> <?php ?></body></html> +</script><?php echo "http://some.link.com/" ?> <?php echo "//some.link.com/" ?> <?php echo '//some.link.com/' ?> <em>inline text</em> <a href="http://www.<?php echo 'hi' ?>"></a> <?php ?> <?php echo \$block->getChildHtml('someChildBlock'); ?> <?php ?> <?php ?> <?php ?></body><?php \$sometext = <<<SOMETEXT + mytext + mytextline2 +SOMETEXT; ?></html> TEXT; $this->appDirectoryMock->expects($this->once()) diff --git a/lib/internal/Magento/Framework/View/Url/ConfigInterface.php b/lib/internal/Magento/Framework/View/Url/ConfigInterface.php index d1076abdb0ec5..4cc0d86e619f2 100644 --- a/lib/internal/Magento/Framework/View/Url/ConfigInterface.php +++ b/lib/internal/Magento/Framework/View/Url/ConfigInterface.php @@ -8,6 +8,7 @@ /** * Url Config Interface * @api + * @since 100.0.2 */ interface ConfigInterface { diff --git a/lib/internal/Magento/Framework/View/Url/CssResolver.php b/lib/internal/Magento/Framework/View/Url/CssResolver.php index 8c73e2ab9bcf9..60f03d063c4a8 100644 --- a/lib/internal/Magento/Framework/View/Url/CssResolver.php +++ b/lib/internal/Magento/Framework/View/Url/CssResolver.php @@ -11,6 +11,7 @@ * CSS URLs resolver class. * This utility class provides a set of methods to work with CSS files. * @api + * @since 100.0.2 */ class CssResolver { diff --git a/lib/internal/Magento/Framework/Webapi/Authorization.php b/lib/internal/Magento/Framework/Webapi/Authorization.php index 03ee07d48b323..a1907dc46ab11 100644 --- a/lib/internal/Magento/Framework/Webapi/Authorization.php +++ b/lib/internal/Magento/Framework/Webapi/Authorization.php @@ -23,7 +23,6 @@ class Authorization * Initialize dependencies. * * @param \Magento\Framework\AuthorizationInterface $authorization - * @since 100.1.0 */ public function __construct(\Magento\Framework\AuthorizationInterface $authorization) { diff --git a/lib/internal/Magento/Framework/Webapi/CustomAttributeTypeLocatorInterface.php b/lib/internal/Magento/Framework/Webapi/CustomAttributeTypeLocatorInterface.php index 25d78672eef87..ac183bc3b1871 100644 --- a/lib/internal/Magento/Framework/Webapi/CustomAttributeTypeLocatorInterface.php +++ b/lib/internal/Magento/Framework/Webapi/CustomAttributeTypeLocatorInterface.php @@ -24,7 +24,7 @@ public function getType($attributeCode, $entityType); * Get list of all Data Interface corresponding to complex custom attribute types * * @return string[] array of Data Interface class names - * @deprecated + * @deprecated 102.0.0 * @see \Magento\Framework\Webapi\CustomAttribute\ServiceTypeListInterface::getDataTypes() */ public function getAllServiceDataInterfaces(); diff --git a/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php b/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php index f9b6a32fc5673..3737d86d2b1f6 100644 --- a/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ErrorProcessor.php @@ -24,6 +24,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * @since 100.0.2 */ class ErrorProcessor { diff --git a/lib/internal/Magento/Framework/Webapi/Rest/Request/Deserializer/Json.php b/lib/internal/Magento/Framework/Webapi/Rest/Request/Deserializer/Json.php index c2e7c324d1282..2673df519b064 100644 --- a/lib/internal/Magento/Framework/Webapi/Rest/Request/Deserializer/Json.php +++ b/lib/internal/Magento/Framework/Webapi/Rest/Request/Deserializer/Json.php @@ -14,7 +14,7 @@ class Json implements \Magento\Framework\Webapi\Rest\Request\DeserializerInterfa { /** * @var \Magento\Framework\Json\Decoder - * @deprecated + * @deprecated 101.0.0 */ protected $decoder; diff --git a/lib/internal/Magento/Framework/Webapi/Rest/Request/ParamOverriderInterface.php b/lib/internal/Magento/Framework/Webapi/Rest/Request/ParamOverriderInterface.php index 828022353e4fa..bc171b84ba3d9 100644 --- a/lib/internal/Magento/Framework/Webapi/Rest/Request/ParamOverriderInterface.php +++ b/lib/internal/Magento/Framework/Webapi/Rest/Request/ParamOverriderInterface.php @@ -27,6 +27,7 @@ * adding to the parameter list for ParamsOverrider's dependency injection configuration. * * @api + * @since 100.0.2 */ interface ParamOverriderInterface { diff --git a/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererInterface.php b/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererInterface.php index 9d905ce599779..4edbe4e6a5377 100644 --- a/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererInterface.php +++ b/lib/internal/Magento/Framework/Webapi/Rest/Response/RendererInterface.php @@ -11,6 +11,7 @@ * Renderer interface allows REST response data rendering in a specific format (e.g. Json or Xml) * * @api + * @since 100.0.2 */ interface RendererInterface { diff --git a/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php index 902e67bf015b7..c8955aa2a6998 100644 --- a/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ServiceInputProcessor.php @@ -28,6 +28,7 @@ * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @api + * @since 100.0.2 */ class ServiceInputProcessor implements ServicePayloadConverterInterface { diff --git a/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php b/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php index 25eacb00c23ae..8319a0398a825 100644 --- a/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php +++ b/lib/internal/Magento/Framework/Webapi/ServiceOutputProcessor.php @@ -17,6 +17,7 @@ * Data object converter * * @api + * @since 100.0.2 */ class ServiceOutputProcessor implements ServicePayloadConverterInterface { diff --git a/lib/internal/Magento/Framework/Webapi/ServicePayloadConverterInterface.php b/lib/internal/Magento/Framework/Webapi/ServicePayloadConverterInterface.php index 30f93efdb9ef9..da4fe0ad08143 100644 --- a/lib/internal/Magento/Framework/Webapi/ServicePayloadConverterInterface.php +++ b/lib/internal/Magento/Framework/Webapi/ServicePayloadConverterInterface.php @@ -11,6 +11,7 @@ * Interface for data conversion based on data type. * * @api + * @since 100.0.2 */ interface ServicePayloadConverterInterface { diff --git a/lib/web/css/source/lib/_navigation.less b/lib/web/css/source/lib/_navigation.less index 38cd042591722..551e138ea06ec 100644 --- a/lib/web/css/source/lib/_navigation.less +++ b/lib/web/css/source/lib/_navigation.less @@ -470,6 +470,8 @@ li { margin: 0; + position: relative; + &.parent { > a { > .ui-menu-icon { diff --git a/lib/web/mage/adminhtml/browser.js b/lib/web/mage/adminhtml/browser.js index 604680e0bf8d5..74984024b74a0 100644 --- a/lib/web/mage/adminhtml/browser.js +++ b/lib/web/mage/adminhtml/browser.js @@ -376,7 +376,7 @@ define([ * @param {*} folderName */ confirm: function (folderName) { - return $.ajax({ + $.ajax({ url: self.options.newFolderUrl, dataType: 'json', data: { @@ -399,6 +399,8 @@ define([ ); } }, this)); + + return true; } } }); diff --git a/lib/web/mage/adminhtml/form.js b/lib/web/mage/adminhtml/form.js index 4dfbde6afa9d7..eae359c4b26a4 100644 --- a/lib/web/mage/adminhtml/form.js +++ b/lib/web/mage/adminhtml/form.js @@ -133,6 +133,7 @@ define([ this.config = regions.config; delete regions.config; this.regions = regions; + this.sortedRegions = this.getSortedRegions(); this.disableAction = typeof disableAction === 'undefined' ? 'hide' : disableAction; this.clearRegionValueOnDisable = typeof clearRegionValueOnDisable === 'undefined' ? false : clearRegionValueOnDisable; @@ -246,11 +247,13 @@ define([ * Update. */ update: function () { - var option, region, def, regionId; + var option, selectElement, def, regionId, region; - if (this.regions[this.countryEl.value]) { + selectElement = this.regionSelectEl; + + if (this.sortedRegions[this.countryEl.value]) { if (this.lastCountryId != this.countryEl.value) { //eslint-disable-line eqeqeq - def = this.regionSelectEl.getAttribute('defaultValue'); + def = selectElement.getAttribute('defaultValue'); if (this.regionTextEl) { if (!def) { @@ -259,26 +262,27 @@ define([ this.regionTextEl.value = ''; } - this.regionSelectEl.options.length = 1; + selectElement.options.length = 1; - for (regionId in this.regions[this.countryEl.value]) { //eslint-disable-line guard-for-in - region = this.regions[this.countryEl.value][regionId]; + this.sortedRegions[this.countryEl.value].forEach(function (item) { + regionId = item[0]; + region = item[1]; option = document.createElement('OPTION'); option.value = regionId; option.text = region.name.stripTags(); option.title = region.name; - if (this.regionSelectEl.options.add) { - this.regionSelectEl.options.add(option); + if (selectElement.options.add) { + selectElement.options.add(option); } else { - this.regionSelectEl.appendChild(option); + selectElement.appendChild(option); } if (regionId == def || region.name.toLowerCase() == def || region.code.toLowerCase() == def) { //eslint-disable-line - this.regionSelectEl.value = regionId; + selectElement.value = regionId; } - } + }); } if (this.disableAction == 'hide') { //eslint-disable-line eqeqeq @@ -340,6 +344,28 @@ define([ display ? marks[0].show() : marks[0].hide(); } } + }, + + /** + * Sort regions from JSON by name + * + * @returns {*[]} + */ + getSortedRegions: function () { + var country, regionsEntries, regionsByCountry; + + regionsByCountry = []; + + for (country in this.regions) { //eslint-disable-line guard-for-in + regionsEntries = Object.entries(this.regions[country]); + regionsEntries.sort(function (a, b) { + return a[1].name > b[1].name ? 1 : -1; + }); + + regionsByCountry[country] = regionsEntries; + } + + return regionsByCountry; } }; diff --git a/lib/web/mage/ie-class-fixer.js b/lib/web/mage/ie-class-fixer.js index 683090b1d1386..fe07f273a0b58 100644 --- a/lib/web/mage/ie-class-fixer.js +++ b/lib/web/mage/ie-class-fixer.js @@ -3,18 +3,10 @@ * See COPYING.txt for license details. */ -/* eslint-disable strict */ -(function () { - var userAgent = navigator.userAgent, // user agent identifier - html = document.documentElement, // html tag - gap = ''; // gap between classes +define([], function () { + 'use strict'; - if (html.className) { // check if neighbour class exist in html tag - gap = ' '; - } // end if - - if (userAgent.match(/Trident.*rv[ :]*11\./)) { // Special case for IE11 - html.className += gap + 'ie11'; - } // end if - -})(); + if (navigator.userAgent.match(/Trident.*rv[ :]*11\./)) { + document.documentElement.classList.add('ie11'); + } +}); diff --git a/lib/web/mage/sticky.js b/lib/web/mage/sticky.js index b6e29bb3cae20..78ff63168b9ba 100644 --- a/lib/web/mage/sticky.js +++ b/lib/web/mage/sticky.js @@ -68,6 +68,9 @@ define([ this.element.on('dimensionsChanged', $.proxy(this.reset, this)); this.reset(); + + // Application of the workaround for IE11 and Edge + this.normalizeIE11AndEdgeScroll(); }, /** @@ -128,11 +131,27 @@ define([ }, /** - * Facade method that palces sticky element where it should be. + * Facade method that places sticky element where it should be. */ reset: function () { this._calculateDimens() ._stick(); + }, + + /** + * Workaround for IE11 and Edge that solves the IE known rendering issue + * that prevents sticky element from jumpy movement on scrolling the page. + * + * Alternatively, undesired jumpy movement can be eliminated by changing the setting in IE: + * Settings > Internet options > Advanced tab > inside 'Browsing' item > set 'Use smooth scrolling' to False + */ + normalizeIE11AndEdgeScroll: function () { + if (navigator.userAgent.match(/Trident.*rv[ :]*11\.|Edge\//)) { + document.body.addEventListener('mousewheel', function () { + event.preventDefault(); + window.scrollTo(0, window.pageYOffset - event.wheelDelta); + }); + } } }); diff --git a/lib/web/mage/touch-slider.js b/lib/web/mage/touch-slider.js new file mode 100644 index 0000000000000..6c468a832895b --- /dev/null +++ b/lib/web/mage/touch-slider.js @@ -0,0 +1,151 @@ +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ + +define([ + 'jquery', + 'underscore', + 'jquery-ui-modules/slider' +], function ($, _) { + 'use strict'; + + /** + * Adds support for touch events for regular jQuery UI slider. + */ + $.widget('mage.touchSlider', $.ui.slider, { + + /** + * Creates instance of widget. + * + * @override + */ + _create: function () { + _.bindAll( + this, + '_mouseDown', + '_mouseMove', + '_onTouchEnd' + ); + + return this._superApply(arguments); + }, + + /** + * Initializes mouse events on element. + * @override + */ + _mouseInit: function () { + var result = this._superApply(arguments); + + this.element + .off('mousedown.' + this.widgetName) + .on('touchstart.' + this.widgetName, this._mouseDown); + + return result; + }, + + /** + * Elements' 'mousedown' event handler polyfill. + * @override + */ + _mouseDown: function (event) { + var prevDelegate = this._mouseMoveDelegate, + result; + + event = this._touchToMouse(event); + result = this._super(event); + + if (prevDelegate === this._mouseMoveDelegate) { + return result; + } + + $(document) + .off('mousemove.' + this.widgetName) + .off('mouseup.' + this.widgetName); + + $(document) + .on('touchmove.' + this.widgetName, this._mouseMove) + .on('touchend.' + this.widgetName, this._onTouchEnd) + .on('tochleave.' + this.widgetName, this._onTouchEnd); + + return result; + }, + + /** + * Documents' 'mousemove' event handler polyfill. + * + * @override + * @param {Event} event - Touch event object. + */ + _mouseMove: function (event) { + event = this._touchToMouse(event); + + return this._super(event); + }, + + /** + * Documents' 'touchend' event handler. + */ + _onTouchEnd: function (event) { + $(document).trigger('mouseup'); + + return this._mouseUp(event); + }, + + /** + * Removes previously assigned touch handlers. + * + * @override + */ + _mouseUp: function () { + this._removeTouchHandlers(); + + return this._superApply(arguments); + }, + + /** + * Removes previously assigned touch handlers. + * + * @override + */ + _mouseDestroy: function () { + this._removeTouchHandlers(); + + return this._superApply(arguments); + }, + + /** + * Removes touch events from document object. + */ + _removeTouchHandlers: function () { + $(document) + .off('touchmove.' + this.widgetName) + .off('touchend.' + this.widgetName) + .off('touchleave.' + this.widgetName); + }, + + /** + * Adds properties to the touch event to mimic mouse event. + * + * @param {Event} event - Touch event object. + * @returns {Event} + */ + _touchToMouse: function (event) { + var orig = event.originalEvent, + touch = orig.touches[0]; + + return _.extend(event, { + which: 1, + pageX: touch.pageX, + pageY: touch.pageY, + clientX: touch.clientX, + clientY: touch.clientY, + screenX: touch.screenX, + screenY: touch.screenY + }); + } + }); + + return $.mage.touchSlider; +}); diff --git a/lib/web/mage/validation.js b/lib/web/mage/validation.js index 10f9dab6bdd9b..de40e3afa40ab 100644 --- a/lib/web/mage/validation.js +++ b/lib/web/mage/validation.js @@ -1972,7 +1972,7 @@ define([ if (firstActive.length) { $('html, body').stop().animate({ - scrollTop: firstActive.offset().top - windowHeight / 2 + scrollTop: firstActive.parent().offset().top - windowHeight / 2 }); firstActive.focus(); } diff --git a/lib/web/magnifier/magnifier.js b/lib/web/magnifier/magnifier.js index 06e41377ae33f..155b946b68467 100644 --- a/lib/web/magnifier/magnifier.js +++ b/lib/web/magnifier/magnifier.js @@ -577,8 +577,10 @@ isOverThumb = inBounds; } - if (inBounds && isOverThumb && gMode === 'outside') { - $magnifierPreview.removeClass(MagnifyCls.magnifyHidden); + if (inBounds && isOverThumb) { + if (gMode === 'outside') { + $magnifierPreview.removeClass(MagnifyCls.magnifyHidden); + } move(); } } diff --git a/lib/web/tiny_mce_4/plugins/help/plugin.min.js b/lib/web/tiny_mce_4/plugins/help/plugin.min.js index a71ff52f547dc..67cde482ab9bf 100644 --- a/lib/web/tiny_mce_4/plugins/help/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/help/plugin.min.js @@ -1 +1 @@ -!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return function(){return e}};function l(r){for(var o=[],e=1;e<arguments.length;e++)o[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=o.concat(e);return r.apply(null,n)}}var n,r,o,a,i,c,u=t(!1),s=t(!0),m=u,f=s,d=function(){return p},p=(a={fold:function(e,t){return e()},is:m,isSome:m,isNone:f,getOr:o=function(e){return e},getOrThunk:r=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:o,orThunk:r,map:d,ap:d,each:function(){},bind:d,flatten:d,exists:m,forall:f,filter:d,equals:n=function(e){return e.isNone()},equals_:n,toArray:function(){return[]},toString:t("none()")},Object.freeze&&Object.freeze(a),a),y=function(n){var e=function(){return n},t=function(){return o},r=function(e){return e(n)},o={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:f,isNone:m,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return y(e(n))},ap:function(e){return e.fold(d,function(e){return y(e(n))})},each:function(e){e(n)},bind:r,flatten:e,exists:r,forall:r,filter:function(e){return e(n)?o:p},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(m,function(e){return t(n,e)})},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},h={some:y,none:d,from:function(e){return null===e||e===undefined?p:y(e)}},g=(i="function",function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===i}),k=(Array.prototype.slice,(c=Array.prototype.indexOf)===undefined?function(e,t){return x(e,t)}:function(e,t){return c.call(e,t)}),v=function(e,t){return-1<k(e,t)},b=function(e,t){for(var n=e.length,r=new Array(n),o=0;o<n;o++){var a=e[o];r[o]=t(a,o,e)}return r},x=function(e,t){for(var n=0,r=e.length;n<r;++n)if(e[n]===t)return n;return-1},A=(g(Array.from)&&Array.from,tinymce.util.Tools.resolve("tinymce.util.I18n")),C=tinymce.util.Tools.resolve("tinymce.Env"),w=C.mac?"\u2318":"Ctrl",S=C.mac?"Ctrl + Alt":"Shift + Alt",O={shortcuts:[{shortcut:w+" + B",action:"Bold"},{shortcut:w+" + I",action:"Italic"},{shortcut:w+" + U",action:"Underline"},{shortcut:w+" + A",action:"Select all"},{shortcut:w+" + Y or "+w+" + Shift + Z",action:"Redo"},{shortcut:w+" + Z",action:"Undo"},{shortcut:S+" + 1",action:"Header 1"},{shortcut:S+" + 2",action:"Header 2"},{shortcut:S+" + 3",action:"Header 3"},{shortcut:S+" + 4",action:"Header 4"},{shortcut:S+" + 5",action:"Header 5"},{shortcut:S+" + 6",action:"Header 6"},{shortcut:S+" + 7",action:"Paragraph"},{shortcut:S+" + 8",action:"Div"},{shortcut:S+" + 9",action:"Address"},{shortcut:"Alt + F9",action:"Focus to menubar"},{shortcut:"Alt + F10",action:"Focus to toolbar"},{shortcut:"Alt + F11",action:"Focus to element path"},{shortcut:"Ctrl + F9",action:"Focus to contextual toolbar"},{shortcut:w+" + K",action:"Insert link (if link plugin activated)"},{shortcut:w+" + S",action:"Save (if save plugin activated)"},{shortcut:w+" + F",action:"Find (if searchreplace plugin activated)"}]},T=function(){var e=b(O.shortcuts,function(e){return'<tr data-mce-tabstop="1" tabindex="-1" aria-label="Action: '+(t=e).action+", Shortcut: "+t.shortcut.replace(/Ctrl/g,"Control")+'"><td>'+A.translate(e.action)+"</td><td>"+e.shortcut+"</td></tr>";var t}).join("");return{title:"Handy Shortcuts",type:"container",style:"overflow-y: auto; overflow-x: hidden; max-height: 250px",items:[{type:"container",html:'<div><table class="mce-table-striped"><thead><th>'+A.translate("Action")+"</th><th>"+A.translate("Shortcut")+"</th></thead>"+e+"</table></div>"}]}},P=Object.keys,_=[{key:"advlist",name:"Advanced List"},{key:"anchor",name:"Anchor"},{key:"autolink",name:"Autolink"},{key:"autoresize",name:"Autoresize"},{key:"autosave",name:"Autosave"},{key:"bbcode",name:"BBCode"},{key:"charmap",name:"Character Map"},{key:"code",name:"Code"},{key:"codesample",name:"Code Sample"},{key:"colorpicker",name:"Color Picker"},{key:"compat3x",name:"3.x Compatibility"},{key:"contextmenu",name:"Context Menu"},{key:"directionality",name:"Directionality"},{key:"emoticons",name:"Emoticons"},{key:"fullpage",name:"Full Page"},{key:"fullscreen",name:"Full Screen"},{key:"help",name:"Help"},{key:"hr",name:"Horizontal Rule"},{key:"image",name:"Image"},{key:"imagetools",name:"Image Tools"},{key:"importcss",name:"Import CSS"},{key:"insertdatetime",name:"Insert Date/Time"},{key:"legacyoutput",name:"Legacy Output"},{key:"link",name:"Link"},{key:"lists",name:"Lists"},{key:"media",name:"Media"},{key:"nonbreaking",name:"Nonbreaking"},{key:"noneditable",name:"Noneditable"},{key:"pagebreak",name:"Page Break"},{key:"paste",name:"Paste"},{key:"preview",name:"Preview"},{key:"print",name:"Print"},{key:"save",name:"Save"},{key:"searchreplace",name:"Search and Replace"},{key:"spellchecker",name:"Spell Checker"},{key:"tabfocus",name:"Tab Focus"},{key:"table",name:"Table"},{key:"template",name:"Template"},{key:"textcolor",name:"Text Color"},{key:"textpattern",name:"Text Pattern"},{key:"toc",name:"Table of Contents"},{key:"visualblocks",name:"Visual Blocks"},{key:"visualchars",name:"Visual Characters"},{key:"wordcount",name:"Word Count"}],H=l(function(e,o){return e.replace(/\$\{([^{}]*)\}/g,function(e,t){var n,r=o[t];return"string"==(n=typeof r)||"number"===n?r.toString():e})},'<a href="${url}" target="_blank" rel="noopener">${name}</a>'),F=function(t,n){return function(e,t){for(var n=0,r=e.length;n<r;n++){var o=e[n];if(t(o,n,e))return h.some(o)}return h.none()}(_,function(e){return e.key===n}).fold(function(){var e=t.plugins[n].getMetadata;return"function"==typeof e?H(e()):n},function(e){return H({name:e.name,url:"https://www.tinymce.com/docs/plugins/"+e.key})})},M=function(t){var e,n,r,o=(r=P((e=t).plugins),e.settings.forced_plugins===undefined?r:function(e,t){for(var n=[],r=0,o=e.length;r<o;r++){var a=e[r];t(a,r,e)&&n.push(a)}return n}(r,(n=l(v,e.settings.forced_plugins),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return!n.apply(null,e)}))),a=b(o,function(e){return"<li>"+F(t,e)+"</li>"}),i=a.length,c=a.join("");return"<p><b>"+A.translate(["Plugins installed ({0}):",i])+"</b></p><ul>"+c+"</ul>"},E=function(e){return{title:"Plugins",type:"container",style:"overflow-y: auto; overflow-x: hidden;",layout:"flex",padding:10,spacing:10,items:[(t=e,{type:"container",html:'<div style="overflow-y: auto; overflow-x: hidden; max-height: 230px; height: 230px;" data-mce-tabstop="1" tabindex="-1">'+M(t)+"</div>",flex:1}),{type:"container",html:'<div style="padding: 10px; background: #e3e7f4; height: 100%;" data-mce-tabstop="1" tabindex="-1"><p><b>'+A.translate("Premium plugins:")+'</b></p><ul><li>PowerPaste</li><li>Spell Checker Pro</li><li>Accessibility Checker</li><li>Advanced Code Editor</li><li>Enhanced Media Embed</li><li>Link Checker</li></ul><br /><p style="float: right;"><a href="https://www.tinymce.com/pricing/?utm_campaign=editor_referral&utm_medium=help_dialog&utm_source=tinymce" target="_blank">'+A.translate("Learn more...")+"</a></p></div>",flex:1}]};var t},I=tinymce.util.Tools.resolve("tinymce.EditorManager"),j=function(){var e,t,n='<a href="https://www.tinymce.com/docs/changelog/?utm_campaign=editor_referral&utm_medium=help_dialog&utm_source=tinymce" target="_blank">TinyMCE '+(e=I.majorVersion,t=I.minorVersion,0===e.indexOf("@")?"X.X.X":e+"."+t)+"</a>";return[{type:"label",html:A.translate(["You are using {0}",n])},{type:"spacer",flex:1},{text:"Close",onclick:function(){this.parent().parent().close()}}]},L=function(e,t){return function(){e.windowManager.open({title:"Help",bodyType:"tabpanel",layout:"flex",body:[T(),E(e)],buttons:j(),onPostRender:function(){this.getEl("title").innerHTML='<img src="'+t+'/img/logo.png" alt="TinyMCE Logo" style="display: inline-block; width: 200px; height: 50px">'}})}},B=function(e,t){e.addCommand("mceHelp",L(e,t))},N=function(e,t){e.addButton("help",{icon:"help",onclick:L(e,t)}),e.addMenuItem("help",{text:"Help",icon:"help",context:"help",onclick:L(e,t)})};e.add("help",function(e,t){N(e,t),B(e,t),e.shortcuts.add("Alt+0","Open help dialog","mceHelp")})}(); \ No newline at end of file +!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(){},a=function(e){return function(){return e}};function l(r){for(var o=[],e=1;e<arguments.length;e++)o[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=o.concat(e);return r.apply(null,n)}}var n,r,o,i,c,u=a(!1),s=a(!0),m=function(){return d},d=(n=function(e){return e.isNone()},i={fold:function(e,t){return e()},is:u,isSome:u,isNone:s,getOr:o=function(e){return e},getOrThunk:r=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:a(null),getOrUndefined:a(undefined),or:o,orThunk:r,map:m,each:t,bind:m,exists:u,forall:s,filter:m,equals:n,equals_:n,toArray:function(){return[]},toString:a("none()")},Object.freeze&&Object.freeze(i),i),f=function(n){var e=a(n),t=function(){return o},r=function(e){return e(n)},o={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:s,isNone:u,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return f(e(n))},each:function(e){e(n)},bind:r,exists:r,forall:r,filter:function(e){return e(n)?o:d},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(u,function(e){return t(n,e)})}};return o},p={some:f,none:m,from:function(e){return null===e||e===undefined?d:f(e)}},y=(c="function",function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===c}),h=(Array.prototype.slice,Array.prototype.indexOf),g=function(e,t){return n=e,r=t,-1<h.call(n,r);var n,r},k=function(e,t){for(var n=e.length,r=new Array(n),o=0;o<n;o++){var a=e[o];r[o]=t(a,o)}return r},v=(y(Array.from)&&Array.from,tinymce.util.Tools.resolve("tinymce.util.I18n")),b=tinymce.util.Tools.resolve("tinymce.Env"),x=b.mac?"\u2318":"Ctrl",A=b.mac?"Ctrl + Alt":"Shift + Alt",C={shortcuts:[{shortcut:x+" + B",action:"Bold"},{shortcut:x+" + I",action:"Italic"},{shortcut:x+" + U",action:"Underline"},{shortcut:x+" + A",action:"Select all"},{shortcut:x+" + Y or "+x+" + Shift + Z",action:"Redo"},{shortcut:x+" + Z",action:"Undo"},{shortcut:A+" + 1",action:"Header 1"},{shortcut:A+" + 2",action:"Header 2"},{shortcut:A+" + 3",action:"Header 3"},{shortcut:A+" + 4",action:"Header 4"},{shortcut:A+" + 5",action:"Header 5"},{shortcut:A+" + 6",action:"Header 6"},{shortcut:A+" + 7",action:"Paragraph"},{shortcut:A+" + 8",action:"Div"},{shortcut:A+" + 9",action:"Address"},{shortcut:"Alt + F9",action:"Focus to menubar"},{shortcut:"Alt + F10",action:"Focus to toolbar"},{shortcut:"Alt + F11",action:"Focus to element path"},{shortcut:"Ctrl + F9",action:"Focus to contextual toolbar"},{shortcut:x+" + K",action:"Insert link (if link plugin activated)"},{shortcut:x+" + S",action:"Save (if save plugin activated)"},{shortcut:x+" + F",action:"Find (if searchreplace plugin activated)"}]},w=function(){var e=k(C.shortcuts,function(e){return'<tr data-mce-tabstop="1" tabindex="-1" aria-label="Action: '+(t=e).action+", Shortcut: "+t.shortcut.replace(/Ctrl/g,"Control")+'"><td>'+v.translate(e.action)+"</td><td>"+e.shortcut+"</td></tr>";var t}).join("");return{title:"Handy Shortcuts",type:"container",style:"overflow-y: auto; overflow-x: hidden; max-height: 250px",items:[{type:"container",html:'<div><table class="mce-table-striped"><thead><th>'+v.translate("Action")+"</th><th>"+v.translate("Shortcut")+"</th></thead>"+e+"</table></div>"}]}},S=Object.keys,O=[{key:"advlist",name:"Advanced List"},{key:"anchor",name:"Anchor"},{key:"autolink",name:"Autolink"},{key:"autoresize",name:"Autoresize"},{key:"autosave",name:"Autosave"},{key:"bbcode",name:"BBCode"},{key:"charmap",name:"Character Map"},{key:"code",name:"Code"},{key:"codesample",name:"Code Sample"},{key:"colorpicker",name:"Color Picker"},{key:"compat3x",name:"3.x Compatibility"},{key:"contextmenu",name:"Context Menu"},{key:"directionality",name:"Directionality"},{key:"emoticons",name:"Emoticons"},{key:"fullpage",name:"Full Page"},{key:"fullscreen",name:"Full Screen"},{key:"help",name:"Help"},{key:"hr",name:"Horizontal Rule"},{key:"image",name:"Image"},{key:"imagetools",name:"Image Tools"},{key:"importcss",name:"Import CSS"},{key:"insertdatetime",name:"Insert Date/Time"},{key:"legacyoutput",name:"Legacy Output"},{key:"link",name:"Link"},{key:"lists",name:"Lists"},{key:"media",name:"Media"},{key:"nonbreaking",name:"Nonbreaking"},{key:"noneditable",name:"Noneditable"},{key:"pagebreak",name:"Page Break"},{key:"paste",name:"Paste"},{key:"preview",name:"Preview"},{key:"print",name:"Print"},{key:"save",name:"Save"},{key:"searchreplace",name:"Search and Replace"},{key:"spellchecker",name:"Spell Checker"},{key:"tabfocus",name:"Tab Focus"},{key:"table",name:"Table"},{key:"template",name:"Template"},{key:"textcolor",name:"Text Color"},{key:"textpattern",name:"Text Pattern"},{key:"toc",name:"Table of Contents"},{key:"visualblocks",name:"Visual Blocks"},{key:"visualchars",name:"Visual Characters"},{key:"wordcount",name:"Word Count"}],T=l(function(e,o){return e.replace(/\$\{([^{}]*)\}/g,function(e,t){var n,r=o[t];return"string"==(n=typeof r)||"number"===n?r.toString():e})},'<a href="${url}" target="_blank" rel="noopener">${name}</a>'),P=function(t,n){return function(e,t){for(var n=0,r=e.length;n<r;n++){var o=e[n];if(t(o,n))return p.some(o)}return p.none()}(O,function(e){return e.key===n}).fold(function(){var e=t.plugins[n].getMetadata;return"function"==typeof e?T(e()):n},function(e){return T({name:e.name,url:"https://www.tinymce.com/docs/plugins/"+e.key})})},_=function(t){var e,n,r,o=(r=S((e=t).plugins),e.settings.forced_plugins===undefined?r:function(e,t){for(var n=[],r=0,o=e.length;r<o;r++){var a=e[r];t(a,r)&&n.push(a)}return n}(r,(n=l(g,e.settings.forced_plugins),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return!n.apply(null,e)}))),a=k(o,function(e){return"<li>"+P(t,e)+"</li>"}),i=a.length,c=a.join("");return"<p><b>"+v.translate(["Plugins installed ({0}):",i])+"</b></p><ul>"+c+"</ul>"},H=function(e){return{title:"Plugins",type:"container",style:"overflow-y: auto; overflow-x: hidden;",layout:"flex",padding:10,spacing:10,items:[(t=e,{type:"container",html:'<div style="overflow-y: auto; overflow-x: hidden; max-height: 230px; height: 230px;" data-mce-tabstop="1" tabindex="-1">'+_(t)+"</div>",flex:1}),{type:"container",html:'<div style="padding: 10px; background: #e3e7f4; height: 100%;" data-mce-tabstop="1" tabindex="-1"><p><b>'+v.translate("Premium plugins:")+'</b></p><ul><li>PowerPaste</li><li>Spell Checker Pro</li><li>Accessibility Checker</li><li>Advanced Code Editor</li><li>Enhanced Media Embed</li><li>Link Checker</li></ul><br /><p style="float: right;"><a href="https://www.tinymce.com/pricing/?utm_campaign=editor_referral&utm_medium=help_dialog&utm_source=tinymce" target="_blank">'+v.translate("Learn more...")+"</a></p></div>",flex:1}]};var t},F=tinymce.util.Tools.resolve("tinymce.EditorManager"),M=function(){var e,t,n='<a href="https://www.tinymce.com/docs/changelog/?utm_campaign=editor_referral&utm_medium=help_dialog&utm_source=tinymce" target="_blank">TinyMCE '+(e=F.majorVersion,t=F.minorVersion,0===e.indexOf("@")?"X.X.X":e+"."+t)+"</a>";return[{type:"label",html:v.translate(["You are using {0}",n])},{type:"spacer",flex:1},{text:"Close",onclick:function(){this.parent().parent().close()}}]},E=function(e,t){return function(){e.windowManager.open({title:"Help",bodyType:"tabpanel",layout:"flex",body:[w(),H(e)],buttons:M(),onPostRender:function(){this.getEl("title").innerHTML='<img src="'+t+'/img/logo.png" alt="TinyMCE Logo" style="display: inline-block; width: 200px; height: 50px">'}})}},I=function(e,t){e.addCommand("mceHelp",E(e,t))},j=function(e,t){e.addButton("help",{icon:"help",onclick:E(e,t)}),e.addMenuItem("help",{text:"Help",icon:"help",context:"help",onclick:E(e,t)})};e.add("help",function(e,t){j(e,t),I(e,t),e.shortcuts.add("Alt+0","Open help dialog","mceHelp")})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/image/plugin.min.js b/lib/web/tiny_mce_4/plugins/image/plugin.min.js index d4764ad6251f5..23473aa76db46 100644 --- a/lib/web/tiny_mce_4/plugins/image/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/image/plugin.min.js @@ -1 +1 @@ -!function(l){"use strict";var i,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),d=function(e){return!1!==e.settings.image_dimensions},u=function(e){return!0===e.settings.image_advtab},m=function(e){return e.getParam("image_prepend_url","")},n=function(e){return e.getParam("image_class_list")},r=function(e){return!1!==e.settings.image_description},a=function(e){return!0===e.settings.image_title},o=function(e){return!0===e.settings.image_caption},c=function(e){return e.getParam("image_list",!1)},s=function(e){return e.getParam("images_upload_url",!1)},g=function(e){return e.getParam("images_upload_handler",!1)},f=function(e){return e.getParam("images_upload_url")},p=function(e){return e.getParam("images_upload_handler")},h=function(e){return e.getParam("images_upload_base_path")},v=function(e){return e.getParam("images_upload_credentials")},b="undefined"!=typeof l.window?l.window:Function("return this;")(),y=function(e,t){return function(e,t){for(var n=t!==undefined&&null!==t?t:b,r=0;r<e.length&&n!==undefined&&null!==n;++r)n=n[e[r]];return n}(e.split("."),t)},x={getOrDie:function(e,t){var n=y(e,t);if(n===undefined||null===n)throw e+" not available on this browser";return n}},w=tinymce.util.Tools.resolve("tinymce.util.Promise"),C=tinymce.util.Tools.resolve("tinymce.util.Tools"),S=tinymce.util.Tools.resolve("tinymce.util.XHR"),N=function(e,t){return Math.max(parseInt(e,10),parseInt(t,10))},_=function(e,n){var r=l.document.createElement("img");function t(e,t){r.parentNode&&r.parentNode.removeChild(r),n({width:e,height:t})}r.onload=function(){t(N(r.width,r.clientWidth),N(r.height,r.clientHeight))},r.onerror=function(){t(0,0)};var a=r.style;a.visibility="hidden",a.position="fixed",a.bottom=a.left="0px",a.width=a.height="auto",l.document.body.appendChild(r),r.src=e},T=function(e,a,t){return function n(e,r){return r=r||[],C.each(e,function(e){var t={text:e.text||e.title};e.menu?t.menu=n(e.menu):(t.value=e.value,a(t)),r.push(t)}),r}(e,t||[])},A=function(e){return e&&(e=e.replace(/px$/,"")),e},R=function(e){return 0<e.length&&/^[0-9]+$/.test(e)&&(e+="px"),e},I=function(e){if(e.margin){var t=e.margin.split(" ");switch(t.length){case 1:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[0],e["margin-bottom"]=e["margin-bottom"]||t[0],e["margin-left"]=e["margin-left"]||t[0];break;case 2:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[0],e["margin-left"]=e["margin-left"]||t[1];break;case 3:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[2],e["margin-left"]=e["margin-left"]||t[1];break;case 4:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[2],e["margin-left"]=e["margin-left"]||t[3]}delete e.margin}return e},t=function(e,t){var n=c(e);"string"==typeof n?S.send({url:n,success:function(e){t(JSON.parse(e))}}):"function"==typeof n?n(t):t(n)},O=function(e,t,n){function r(){n.onload=n.onerror=null,e.selection&&(e.selection.select(n),e.nodeChanged())}n.onload=function(){t.width||t.height||!d(e)||e.dom.setAttribs(n,{width:n.clientWidth,height:n.clientHeight}),r()},n.onerror=r},L=function(r){return new w(function(e,t){var n=new(x.getOrDie("FileReader"));n.onload=function(){e(n.result)},n.onerror=function(){t(n.error.message)},n.readAsDataURL(r)})},P=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),U=Object.prototype.hasOwnProperty,E=(i=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t<e.length;t++)e[t]=arguments[t];if(0===e.length)throw new Error("Can't merge zero objects");for(var n={},r=0;r<e.length;r++){var a=e[r];for(var o in a)U.call(a,o)&&(n[o]=i(n[o],a[o]))}return n}),k=P.DOM,M=function(e){return e.style.marginLeft&&e.style.marginRight&&e.style.marginLeft===e.style.marginRight?A(e.style.marginLeft):""},D=function(e){return e.style.marginTop&&e.style.marginBottom&&e.style.marginTop===e.style.marginBottom?A(e.style.marginTop):""},z=function(e){return e.style.borderWidth?A(e.style.borderWidth):""},B=function(e,t){return e.hasAttribute(t)?e.getAttribute(t):""},H=function(e,t){return e.style[t]?e.style[t]:""},j=function(e){return null!==e.parentNode&&"FIGURE"===e.parentNode.nodeName},F=function(e,t,n){e.setAttribute(t,n)},W=function(e){var t,n,r,a;j(e)?(a=(r=e).parentNode,k.insertAfter(r,a),k.remove(a)):(t=e,n=k.create("figure",{"class":"image"}),k.insertAfter(n,t),n.appendChild(t),n.appendChild(k.create("figcaption",{contentEditable:!0},"Caption")),n.contentEditable="false")},J=function(e,t){var n=e.getAttribute("style"),r=t(null!==n?n:"");0<r.length?(e.setAttribute("style",r),e.setAttribute("data-mce-style",r)):e.removeAttribute("style")},V=function(e,r){return function(e,t,n){e.style[t]?(e.style[t]=R(n),J(e,r)):F(e,t,n)}},G=function(e,t){return e.style[t]?A(e.style[t]):B(e,t)},$=function(e,t){var n=R(t);e.style.marginLeft=n,e.style.marginRight=n},X=function(e,t){var n=R(t);e.style.marginTop=n,e.style.marginBottom=n},q=function(e,t){var n=R(t);e.style.borderWidth=n},K=function(e,t){e.style.borderStyle=t},Q=function(e){return"FIGURE"===e.nodeName},Y=function(e,t){var n=l.document.createElement("img");return F(n,"style",t.style),(M(n)||""!==t.hspace)&&$(n,t.hspace),(D(n)||""!==t.vspace)&&X(n,t.vspace),(z(n)||""!==t.border)&&q(n,t.border),(H(n,"borderStyle")||""!==t.borderStyle)&&K(n,t.borderStyle),e(n.getAttribute("style"))},Z=function(e,t){return{src:B(t,"src"),alt:B(t,"alt"),title:B(t,"title"),width:G(t,"width"),height:G(t,"height"),"class":B(t,"class"),style:e(B(t,"style")),caption:j(t),hspace:M(t),vspace:D(t),border:z(t),borderStyle:H(t,"borderStyle")}},ee=function(e,t,n,r,a){n[r]!==t[r]&&a(e,r,n[r])},te=function(r,a){return function(e,t,n){r(e,n),J(e,a)}},ne=function(e,t,n){var r=Z(e,n);ee(n,r,t,"caption",function(e,t,n){return W(e)}),ee(n,r,t,"src",F),ee(n,r,t,"alt",F),ee(n,r,t,"title",F),ee(n,r,t,"width",V(0,e)),ee(n,r,t,"height",V(0,e)),ee(n,r,t,"class",F),ee(n,r,t,"style",te(function(e,t){return F(e,"style",t)},e)),ee(n,r,t,"hspace",te($,e)),ee(n,r,t,"vspace",te(X,e)),ee(n,r,t,"border",te(q,e)),ee(n,r,t,"borderStyle",te(K,e))},re=function(e,t){var n=e.dom.styles.parse(t),r=I(n),a=e.dom.styles.parse(e.dom.styles.serialize(r));return e.dom.styles.serialize(a)},ae=function(e){var t=e.selection.getNode(),n=e.dom.getParent(t,"figure.image");return n?e.dom.select("img",n)[0]:t&&("IMG"!==t.nodeName||t.getAttribute("data-mce-object")||t.getAttribute("data-mce-placeholder"))?null:t},oe=function(t,e){var n=t.dom,r=n.getParent(e.parentNode,function(e){return t.schema.getTextBlockElements()[e.nodeName]},t.getBody());return r?n.split(r,e):e},ie=function(t){var e=ae(t);return e?Z(function(e){return re(t,e)},e):{src:"",alt:"",title:"",width:"",height:"","class":"",style:"",caption:!1,hspace:"",vspace:"",border:"",borderStyle:""}},le=function(t,e){var n=function(e,t){var n=l.document.createElement("img");if(ne(e,E(t,{caption:!1}),n),F(n,"alt",t.alt),t.caption){var r=k.create("figure",{"class":"image"});return r.appendChild(n),r.appendChild(k.create("figcaption",{contentEditable:!0},"Caption")),r.contentEditable="false",r}return n}(function(e){return re(t,e)},e);t.dom.setAttrib(n,"data-mce-id","__mcenew"),t.focus(),t.selection.setContent(n.outerHTML);var r=t.dom.select('*[data-mce-id="__mcenew"]')[0];if(t.dom.setAttrib(r,"data-mce-id",null),Q(r)){var a=oe(t,r);t.selection.select(a)}else t.selection.select(r)},ue=function(e,t){var n=ae(e);n?t.src?function(t,e){var n,r=ae(t);if(ne(function(e){return re(t,e)},e,r),n=r,t.dom.setAttrib(n,"src",n.getAttribute("src")),Q(r.parentNode)){var a=r.parentNode;oe(t,a),t.selection.select(r.parentNode)}else t.selection.select(r),O(t,e,r)}(e,t):function(e,t){if(t){var n=e.dom.is(t.parentNode,"figure.image")?t.parentNode:t;e.dom.remove(n),e.focus(),e.nodeChanged(),e.dom.isEmpty(e.getBody())&&(e.setContent(""),e.selection.setCursorLocation())}}(e,n):t.src&&le(e,t)},ce=function(n,r){r.find("#style").each(function(e){var t=Y(function(e){return re(n,e)},E({src:"",alt:"",title:"",width:"",height:"","class":"",style:"",caption:!1,hspace:"",vspace:"",border:"",borderStyle:""},r.toJSON()));e.value(t)})},se=function(t){return{title:"Advanced",type:"form",pack:"start",items:[{label:"Style",name:"style",type:"textbox",onchange:(o=t,function(e){var t=o.dom,n=e.control.rootControl;if(u(o)){var r=n.toJSON(),a=t.parseStyle(r.style);n.find("#vspace").value(""),n.find("#hspace").value(""),((a=I(a))["margin-top"]&&a["margin-bottom"]||a["margin-right"]&&a["margin-left"])&&(a["margin-top"]===a["margin-bottom"]?n.find("#vspace").value(A(a["margin-top"])):n.find("#vspace").value(""),a["margin-right"]===a["margin-left"]?n.find("#hspace").value(A(a["margin-right"])):n.find("#hspace").value("")),a["border-width"]?n.find("#border").value(A(a["border-width"])):n.find("#border").value(""),a["border-style"]?n.find("#borderStyle").value(a["border-style"]):n.find("#borderStyle").value(""),n.find("#style").value(t.serializeStyle(t.parseStyle(t.serializeStyle(a))))}})},{type:"form",layout:"grid",packV:"start",columns:2,padding:0,defaults:{type:"textbox",maxWidth:50,onchange:function(e){ce(t,e.control.rootControl)}},items:[{label:"Vertical space",name:"vspace"},{label:"Border width",name:"border"},{label:"Horizontal space",name:"hspace"},{label:"Border style",type:"listbox",name:"borderStyle",width:90,maxWidth:90,onselect:function(e){ce(t,e.control.rootControl)},values:[{text:"Select...",value:""},{text:"Solid",value:"solid"},{text:"Dotted",value:"dotted"},{text:"Dashed",value:"dashed"},{text:"Double",value:"double"},{text:"Groove",value:"groove"},{text:"Ridge",value:"ridge"},{text:"Inset",value:"inset"},{text:"Outset",value:"outset"},{text:"None",value:"none"},{text:"Hidden",value:"hidden"}]}]}]};var o},de=function(e,t){e.state.set("oldVal",e.value()),t.state.set("oldVal",t.value())},me=function(e,t){var n=e.find("#width")[0],r=e.find("#height")[0],a=e.find("#constrain")[0];n&&r&&a&&t(n,r,a.checked())},ge=function(e,t,n){var r=e.state.get("oldVal"),a=t.state.get("oldVal"),o=e.value(),i=t.value();n&&r&&a&&o&&i&&(o!==r?(i=Math.round(o/r*i),isNaN(i)||t.value(i)):(o=Math.round(i/a*o),isNaN(o)||e.value(o))),de(e,t)},fe=function(e){me(e,ge)},pe=function(){var e=function(e){fe(e.control.rootControl)};return{type:"container",label:"Dimensions",layout:"flex",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:5,onchange:e,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:5,onchange:e,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}},he=function(e){me(e,de)},ve=fe,be=function(e){e.meta=e.control.rootControl.toJSON()},ye=function(s,e){var t=[{name:"src",type:"filepicker",filetype:"image",label:"Source",autofocus:!0,onchange:function(e){var t,n,r,a,o,i,l,u,c;n=s,i=(t=e).meta||{},l=t.control,u=l.rootControl,(c=u.find("#image-list")[0])&&c.value(n.convertURL(l.value(),"src")),C.each(i,function(e,t){u.find("#"+t).value(e)}),i.width||i.height||(r=n.convertURL(l.value(),"src"),a=m(n),o=new RegExp("^(?:[a-z]+:)?//","i"),a&&!o.test(r)&&r.substring(0,a.length)!==a&&(r=a+r),l.value(r),_(n.documentBaseURI.toAbsolute(l.value()),function(e){e.width&&e.height&&d(n)&&(u.find("#width").value(e.width),u.find("#height").value(e.height),he(u))}))},onbeforecall:be},e];return r(s)&&t.push({name:"alt",type:"textbox",label:"Image description"}),a(s)&&t.push({name:"title",type:"textbox",label:"Image Title"}),d(s)&&t.push(pe()),n(s)&&t.push({name:"class",type:"listbox",label:"Class",values:T(n(s),function(e){e.value&&(e.textStyle=function(){return s.formatter.getCssText({inline:"img",classes:[e.value]})})})}),o(s)&&t.push({name:"caption",type:"checkbox",label:"Caption"}),t},xe=function(e,t){return{title:"General",type:"form",items:ye(e,t)}},we=ye,Ce=function(){return x.getOrDie("URL")},Se=function(e){return Ce().createObjectURL(e)},Ne=function(e){Ce().revokeObjectURL(e)},_e=tinymce.util.Tools.resolve("tinymce.ui.Factory"),Te=function(){};function Ae(i){var t=function(e,r,a,t){var o,n;(o=new(x.getOrDie("XMLHttpRequest"))).open("POST",i.url),o.withCredentials=i.credentials,o.upload.onprogress=function(e){t(e.loaded/e.total*100)},o.onerror=function(){a("Image upload failed due to a XHR Transport error. Code: "+o.status)},o.onload=function(){var e,t,n;o.status<200||300<=o.status?a("HTTP Error: "+o.status):(e=JSON.parse(o.responseText))&&"string"==typeof e.location?r((t=i.basePath,n=e.location,t?t.replace(/\/$/,"")+"/"+n.replace(/^\//,""):n)):a("Invalid JSON: "+o.responseText)},(n=new l.FormData).append("file",e.blob(),e.filename()),o.send(n)};return i=C.extend({credentials:!1,handler:t},i),{upload:function(e){return i.url||i.handler!==t?(r=e,a=i.handler,new w(function(e,t){try{a(r,e,t,Te)}catch(n){t(n.message)}})):w.reject("Upload url missing from the settings.");var r,a}}}var Re=function(u){return function(e){var t=_e.get("Throbber"),n=e.control.rootControl,r=new t(n.getEl()),a=e.control.value(),o=Se(a),i=Ae({url:f(u),basePath:h(u),credentials:v(u),handler:p(u)}),l=function(){r.hide(),Ne(o)};return r.show(),L(a).then(function(e){var t=u.editorUpload.blobCache.create({blob:a,blobUri:o,name:a.name?a.name.replace(/\.[^\.]+$/,""):null,base64:e.split(",")[1]});return i.upload(t).then(function(e){var t=n.find("#src");return t.value(e),n.find("tabpanel")[0].activateTab(0),t.fire("change"),l(),e})})["catch"](function(e){u.windowManager.alert(e),l()})}},Ie=".jpg,.jpeg,.png,.gif",Oe=function(e){return{title:"Upload",type:"form",layout:"flex",direction:"column",align:"stretch",padding:"20 20 20 20",items:[{type:"container",layout:"flex",direction:"column",align:"center",spacing:10,items:[{text:"Browse for an image",type:"browsebutton",accept:Ie,onchange:Re(e)},{text:"OR",type:"label"}]},{text:"Drop an image here",type:"dropzone",accept:Ie,height:100,onchange:Re(e)}]}};function Le(r){for(var a=[],e=1;e<arguments.length;e++)a[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=a.concat(e);return r.apply(null,n)}}var Pe=function(t,e){var n=e.control.getRoot();ve(n),t.undoManager.transact(function(){var e=E(ie(t),n.toJSON());ue(t,e)}),t.editorUpload.uploadImagesAuto()};function Ue(o){function e(e){var n,t,r=ie(o);if(e&&(t={type:"listbox",label:"Image list",name:"image-list",values:T(e,function(e){e.value=o.convertURL(e.value||e.url,"src")},[{text:"None",value:""}]),value:r.src&&o.convertURL(r.src,"src"),onselect:function(e){var t=n.find("#alt");(!t.value()||e.lastControl&&t.value()===e.lastControl.text())&&t.value(e.control.text()),n.find("#src").value(e.control.value()).fire("change")},onPostRender:function(){t=this}}),u(o)||s(o)||g(o)){var a=[xe(o,t)];u(o)&&a.push(se(o)),(s(o)||g(o))&&a.push(Oe(o)),n=o.windowManager.open({title:"Insert/edit image",data:r,bodyType:"tabpanel",body:a,onSubmit:Le(Pe,o)})}else n=o.windowManager.open({title:"Insert/edit image",data:r,body:we(o,t),onSubmit:Le(Pe,o)});he(n)}return{open:function(){t(o,e)}}}var Ee=function(e){e.addCommand("mceImage",Ue(e).open)},ke=function(o){return function(e){for(var t,n,r=e.length,a=function(e){e.attr("contenteditable",o?"true":null)};r--;)t=e[r],(n=t.attr("class"))&&/\bimage\b/.test(n)&&(t.attr("contenteditable",o?"false":null),C.each(t.getAll("figcaption"),a))}},Me=function(e){e.on("preInit",function(){e.parser.addNodeFilter("figure",ke(!0)),e.serializer.addNodeFilter("figure",ke(!1))})},De=function(e){e.addButton("image",{icon:"image",tooltip:"Insert/edit image",onclick:Ue(e).open,stateSelector:"img:not([data-mce-object],[data-mce-placeholder]),figure.image"}),e.addMenuItem("image",{icon:"image",text:"Image",onclick:Ue(e).open,context:"insert",prependToContext:!0})};e.add("image",function(e){Me(e),De(e),Ee(e)})}(window); \ No newline at end of file +!function(l){"use strict";var i,e=tinymce.util.Tools.resolve("tinymce.PluginManager"),d=function(e){return!1!==e.settings.image_dimensions},u=function(e){return!0===e.settings.image_advtab},m=function(e){return e.getParam("image_prepend_url","")},n=function(e){return e.getParam("image_class_list")},r=function(e){return!1!==e.settings.image_description},a=function(e){return!0===e.settings.image_title},o=function(e){return!0===e.settings.image_caption},c=function(e){return e.getParam("image_list",!1)},s=function(e){return e.getParam("images_upload_url",!1)},g=function(e){return e.getParam("images_upload_handler",!1)},f=function(e){return e.getParam("images_upload_url")},p=function(e){return e.getParam("images_upload_handler")},h=function(e){return e.getParam("images_upload_base_path")},v=function(e){return e.getParam("images_upload_credentials")},b="undefined"!=typeof l.window?l.window:Function("return this;")(),y=function(e,t){return function(e,t){for(var n=t!==undefined&&null!==t?t:b,r=0;r<e.length&&n!==undefined&&null!==n;++r)n=n[e[r]];return n}(e.split("."),t)},x={getOrDie:function(e,t){var n=y(e,t);if(n===undefined||null===n)throw new Error(e+" not available on this browser");return n}},w=tinymce.util.Tools.resolve("tinymce.util.Promise"),C=tinymce.util.Tools.resolve("tinymce.util.Tools"),S=tinymce.util.Tools.resolve("tinymce.util.XHR"),N=function(e,t){return Math.max(parseInt(e,10),parseInt(t,10))},_=function(e,n){var r=l.document.createElement("img");function t(e,t){r.parentNode&&r.parentNode.removeChild(r),n({width:e,height:t})}r.onload=function(){t(N(r.width,r.clientWidth),N(r.height,r.clientHeight))},r.onerror=function(){t(0,0)};var a=r.style;a.visibility="hidden",a.position="fixed",a.bottom=a.left="0px",a.width=a.height="auto",l.document.body.appendChild(r),r.src=e},T=function(e,a,t){return function n(e,r){return r=r||[],C.each(e,function(e){var t={text:e.text||e.title};e.menu?t.menu=n(e.menu):(t.value=e.value,a(t)),r.push(t)}),r}(e,t||[])},A=function(e){return e&&(e=e.replace(/px$/,"")),e},R=function(e){return 0<e.length&&/^[0-9]+$/.test(e)&&(e+="px"),e},I=function(e){if(e.margin){var t=e.margin.split(" ");switch(t.length){case 1:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[0],e["margin-bottom"]=e["margin-bottom"]||t[0],e["margin-left"]=e["margin-left"]||t[0];break;case 2:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[0],e["margin-left"]=e["margin-left"]||t[1];break;case 3:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[2],e["margin-left"]=e["margin-left"]||t[1];break;case 4:e["margin-top"]=e["margin-top"]||t[0],e["margin-right"]=e["margin-right"]||t[1],e["margin-bottom"]=e["margin-bottom"]||t[2],e["margin-left"]=e["margin-left"]||t[3]}delete e.margin}return e},t=function(e,t){var n=c(e);"string"==typeof n?S.send({url:n,success:function(e){t(JSON.parse(e))}}):"function"==typeof n?n(t):t(n)},O=function(e,t,n){function r(){n.onload=n.onerror=null,e.selection&&(e.selection.select(n),e.nodeChanged())}n.onload=function(){t.width||t.height||!d(e)||e.dom.setAttribs(n,{width:n.clientWidth,height:n.clientHeight}),r()},n.onerror=r},L=function(r){return new w(function(e,t){var n=new(x.getOrDie("FileReader"));n.onload=function(){e(n.result)},n.onerror=function(){t(n.error.message)},n.readAsDataURL(r)})},P=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),U=Object.prototype.hasOwnProperty,E=(i=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t<e.length;t++)e[t]=arguments[t];if(0===e.length)throw new Error("Can't merge zero objects");for(var n={},r=0;r<e.length;r++){var a=e[r];for(var o in a)U.call(a,o)&&(n[o]=i(n[o],a[o]))}return n}),k=P.DOM,M=function(e){return e.style.marginLeft&&e.style.marginRight&&e.style.marginLeft===e.style.marginRight?A(e.style.marginLeft):""},D=function(e){return e.style.marginTop&&e.style.marginBottom&&e.style.marginTop===e.style.marginBottom?A(e.style.marginTop):""},z=function(e){return e.style.borderWidth?A(e.style.borderWidth):""},B=function(e,t){return e.hasAttribute(t)?e.getAttribute(t):""},H=function(e,t){return e.style[t]?e.style[t]:""},j=function(e){return null!==e.parentNode&&"FIGURE"===e.parentNode.nodeName},F=function(e,t,n){e.setAttribute(t,n)},W=function(e){var t,n,r,a;j(e)?(a=(r=e).parentNode,k.insertAfter(r,a),k.remove(a)):(t=e,n=k.create("figure",{"class":"image"}),k.insertAfter(n,t),n.appendChild(t),n.appendChild(k.create("figcaption",{contentEditable:!0},"Caption")),n.contentEditable="false")},J=function(e,t){var n=e.getAttribute("style"),r=t(null!==n?n:"");0<r.length?(e.setAttribute("style",r),e.setAttribute("data-mce-style",r)):e.removeAttribute("style")},V=function(e,r){return function(e,t,n){e.style[t]?(e.style[t]=R(n),J(e,r)):F(e,t,n)}},G=function(e,t){return e.style[t]?A(e.style[t]):B(e,t)},$=function(e,t){var n=R(t);e.style.marginLeft=n,e.style.marginRight=n},X=function(e,t){var n=R(t);e.style.marginTop=n,e.style.marginBottom=n},q=function(e,t){var n=R(t);e.style.borderWidth=n},K=function(e,t){e.style.borderStyle=t},Q=function(e){return"FIGURE"===e.nodeName},Y=function(e,t){var n=l.document.createElement("img");return F(n,"style",t.style),(M(n)||""!==t.hspace)&&$(n,t.hspace),(D(n)||""!==t.vspace)&&X(n,t.vspace),(z(n)||""!==t.border)&&q(n,t.border),(H(n,"borderStyle")||""!==t.borderStyle)&&K(n,t.borderStyle),e(n.getAttribute("style"))},Z=function(e,t){return{src:B(t,"src"),alt:B(t,"alt"),title:B(t,"title"),width:G(t,"width"),height:G(t,"height"),"class":B(t,"class"),style:e(B(t,"style")),caption:j(t),hspace:M(t),vspace:D(t),border:z(t),borderStyle:H(t,"borderStyle")}},ee=function(e,t,n,r,a){n[r]!==t[r]&&a(e,r,n[r])},te=function(r,a){return function(e,t,n){r(e,n),J(e,a)}},ne=function(e,t,n){var r=Z(e,n);ee(n,r,t,"caption",function(e,t,n){return W(e)}),ee(n,r,t,"src",F),ee(n,r,t,"alt",F),ee(n,r,t,"title",F),ee(n,r,t,"width",V(0,e)),ee(n,r,t,"height",V(0,e)),ee(n,r,t,"class",F),ee(n,r,t,"style",te(function(e,t){return F(e,"style",t)},e)),ee(n,r,t,"hspace",te($,e)),ee(n,r,t,"vspace",te(X,e)),ee(n,r,t,"border",te(q,e)),ee(n,r,t,"borderStyle",te(K,e))},re=function(e,t){var n=e.dom.styles.parse(t),r=I(n),a=e.dom.styles.parse(e.dom.styles.serialize(r));return e.dom.styles.serialize(a)},ae=function(e){var t=e.selection.getNode(),n=e.dom.getParent(t,"figure.image");return n?e.dom.select("img",n)[0]:t&&("IMG"!==t.nodeName||t.getAttribute("data-mce-object")||t.getAttribute("data-mce-placeholder"))?null:t},oe=function(t,e){var n=t.dom,r=n.getParent(e.parentNode,function(e){return t.schema.getTextBlockElements()[e.nodeName]},t.getBody());return r?n.split(r,e):e},ie=function(t){var e=ae(t);return e?Z(function(e){return re(t,e)},e):{src:"",alt:"",title:"",width:"",height:"","class":"",style:"",caption:!1,hspace:"",vspace:"",border:"",borderStyle:""}},le=function(t,e){var n=function(e,t){var n=l.document.createElement("img");if(ne(e,E(t,{caption:!1}),n),F(n,"alt",t.alt),t.caption){var r=k.create("figure",{"class":"image"});return r.appendChild(n),r.appendChild(k.create("figcaption",{contentEditable:!0},"Caption")),r.contentEditable="false",r}return n}(function(e){return re(t,e)},e);t.dom.setAttrib(n,"data-mce-id","__mcenew"),t.focus(),t.selection.setContent(n.outerHTML);var r=t.dom.select('*[data-mce-id="__mcenew"]')[0];if(t.dom.setAttrib(r,"data-mce-id",null),Q(r)){var a=oe(t,r);t.selection.select(a)}else t.selection.select(r)},ue=function(e,t){var n=ae(e);n?t.src?function(t,e){var n,r=ae(t);if(ne(function(e){return re(t,e)},e,r),n=r,t.dom.setAttrib(n,"src",n.getAttribute("src")),Q(r.parentNode)){var a=r.parentNode;oe(t,a),t.selection.select(r.parentNode)}else t.selection.select(r),O(t,e,r)}(e,t):function(e,t){if(t){var n=e.dom.is(t.parentNode,"figure.image")?t.parentNode:t;e.dom.remove(n),e.focus(),e.nodeChanged(),e.dom.isEmpty(e.getBody())&&(e.setContent(""),e.selection.setCursorLocation())}}(e,n):t.src&&le(e,t)},ce=function(n,r){r.find("#style").each(function(e){var t=Y(function(e){return re(n,e)},E({src:"",alt:"",title:"",width:"",height:"","class":"",style:"",caption:!1,hspace:"",vspace:"",border:"",borderStyle:""},r.toJSON()));e.value(t)})},se=function(t){return{title:"Advanced",type:"form",pack:"start",items:[{label:"Style",name:"style",type:"textbox",onchange:(o=t,function(e){var t=o.dom,n=e.control.rootControl;if(u(o)){var r=n.toJSON(),a=t.parseStyle(r.style);n.find("#vspace").value(""),n.find("#hspace").value(""),((a=I(a))["margin-top"]&&a["margin-bottom"]||a["margin-right"]&&a["margin-left"])&&(a["margin-top"]===a["margin-bottom"]?n.find("#vspace").value(A(a["margin-top"])):n.find("#vspace").value(""),a["margin-right"]===a["margin-left"]?n.find("#hspace").value(A(a["margin-right"])):n.find("#hspace").value("")),a["border-width"]?n.find("#border").value(A(a["border-width"])):n.find("#border").value(""),a["border-style"]?n.find("#borderStyle").value(a["border-style"]):n.find("#borderStyle").value(""),n.find("#style").value(t.serializeStyle(t.parseStyle(t.serializeStyle(a))))}})},{type:"form",layout:"grid",packV:"start",columns:2,padding:0,defaults:{type:"textbox",maxWidth:50,onchange:function(e){ce(t,e.control.rootControl)}},items:[{label:"Vertical space",name:"vspace"},{label:"Border width",name:"border"},{label:"Horizontal space",name:"hspace"},{label:"Border style",type:"listbox",name:"borderStyle",width:90,maxWidth:90,onselect:function(e){ce(t,e.control.rootControl)},values:[{text:"Select...",value:""},{text:"Solid",value:"solid"},{text:"Dotted",value:"dotted"},{text:"Dashed",value:"dashed"},{text:"Double",value:"double"},{text:"Groove",value:"groove"},{text:"Ridge",value:"ridge"},{text:"Inset",value:"inset"},{text:"Outset",value:"outset"},{text:"None",value:"none"},{text:"Hidden",value:"hidden"}]}]}]};var o},de=function(e,t){e.state.set("oldVal",e.value()),t.state.set("oldVal",t.value())},me=function(e,t){var n=e.find("#width")[0],r=e.find("#height")[0],a=e.find("#constrain")[0];n&&r&&a&&t(n,r,a.checked())},ge=function(e,t,n){var r=e.state.get("oldVal"),a=t.state.get("oldVal"),o=e.value(),i=t.value();n&&r&&a&&o&&i&&(o!==r?(i=Math.round(o/r*i),isNaN(i)||t.value(i)):(o=Math.round(i/a*o),isNaN(o)||e.value(o))),de(e,t)},fe=function(e){me(e,ge)},pe=function(){var e=function(e){fe(e.control.rootControl)};return{type:"container",label:"Dimensions",layout:"flex",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:5,onchange:e,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:5,onchange:e,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}},he=function(e){me(e,de)},ve=fe,be=function(e){e.meta=e.control.rootControl.toJSON()},ye=function(s,e){var t=[{name:"src",type:"filepicker",filetype:"image",label:"Source",autofocus:!0,onchange:function(e){var t,n,r,a,o,i,l,u,c;n=s,i=(t=e).meta||{},l=t.control,u=l.rootControl,(c=u.find("#image-list")[0])&&c.value(n.convertURL(l.value(),"src")),C.each(i,function(e,t){u.find("#"+t).value(e)}),i.width||i.height||(r=n.convertURL(l.value(),"src"),a=m(n),o=new RegExp("^(?:[a-z]+:)?//","i"),a&&!o.test(r)&&r.substring(0,a.length)!==a&&(r=a+r),l.value(r),_(n.documentBaseURI.toAbsolute(l.value()),function(e){e.width&&e.height&&d(n)&&(u.find("#width").value(e.width),u.find("#height").value(e.height),he(u))}))},onbeforecall:be},e];return r(s)&&t.push({name:"alt",type:"textbox",label:"Image description"}),a(s)&&t.push({name:"title",type:"textbox",label:"Image Title"}),d(s)&&t.push(pe()),n(s)&&t.push({name:"class",type:"listbox",label:"Class",values:T(n(s),function(e){e.value&&(e.textStyle=function(){return s.formatter.getCssText({inline:"img",classes:[e.value]})})})}),o(s)&&t.push({name:"caption",type:"checkbox",label:"Caption"}),t},xe=function(e,t){return{title:"General",type:"form",items:ye(e,t)}},we=ye,Ce=function(){return x.getOrDie("URL")},Se=function(e){return Ce().createObjectURL(e)},Ne=function(e){Ce().revokeObjectURL(e)},_e=tinymce.util.Tools.resolve("tinymce.ui.Factory"),Te=function(){};function Ae(i){var t=function(e,r,a,t){var o,n;(o=new(x.getOrDie("XMLHttpRequest"))).open("POST",i.url),o.withCredentials=i.credentials,o.upload.onprogress=function(e){t(e.loaded/e.total*100)},o.onerror=function(){a("Image upload failed due to a XHR Transport error. Code: "+o.status)},o.onload=function(){var e,t,n;o.status<200||300<=o.status?a("HTTP Error: "+o.status):(e=JSON.parse(o.responseText))&&"string"==typeof e.location?r((t=i.basePath,n=e.location,t?t.replace(/\/$/,"")+"/"+n.replace(/^\//,""):n)):a("Invalid JSON: "+o.responseText)},(n=new l.FormData).append("file",e.blob(),e.filename()),o.send(n)};return i=C.extend({credentials:!1,handler:t},i),{upload:function(e){return i.url||i.handler!==t?(r=e,a=i.handler,new w(function(e,t){try{a(r,e,t,Te)}catch(n){t(n.message)}})):w.reject("Upload url missing from the settings.");var r,a}}}var Re=function(u){return function(e){var t=_e.get("Throbber"),n=e.control.rootControl,r=new t(n.getEl()),a=e.control.value(),o=Se(a),i=Ae({url:f(u),basePath:h(u),credentials:v(u),handler:p(u)}),l=function(){r.hide(),Ne(o)};return r.show(),L(a).then(function(e){var t=u.editorUpload.blobCache.create({blob:a,blobUri:o,name:a.name?a.name.replace(/\.[^\.]+$/,""):null,base64:e.split(",")[1]});return i.upload(t).then(function(e){var t=n.find("#src");return t.value(e),n.find("tabpanel")[0].activateTab(0),t.fire("change"),l(),e})})["catch"](function(e){u.windowManager.alert(e),l()})}},Ie=".jpg,.jpeg,.png,.gif",Oe=function(e){return{title:"Upload",type:"form",layout:"flex",direction:"column",align:"stretch",padding:"20 20 20 20",items:[{type:"container",layout:"flex",direction:"column",align:"center",spacing:10,items:[{text:"Browse for an image",type:"browsebutton",accept:Ie,onchange:Re(e)},{text:"OR",type:"label"}]},{text:"Drop an image here",type:"dropzone",accept:Ie,height:100,onchange:Re(e)}]}};function Le(r){for(var a=[],e=1;e<arguments.length;e++)a[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=a.concat(e);return r.apply(null,n)}}var Pe=function(t,e){var n=e.control.getRoot();ve(n),t.undoManager.transact(function(){var e=E(ie(t),n.toJSON());ue(t,e)}),t.editorUpload.uploadImagesAuto()};function Ue(o){function e(e){var n,t,r=ie(o);if(e&&(t={type:"listbox",label:"Image list",name:"image-list",values:T(e,function(e){e.value=o.convertURL(e.value||e.url,"src")},[{text:"None",value:""}]),value:r.src&&o.convertURL(r.src,"src"),onselect:function(e){var t=n.find("#alt");(!t.value()||e.lastControl&&t.value()===e.lastControl.text())&&t.value(e.control.text()),n.find("#src").value(e.control.value()).fire("change")},onPostRender:function(){t=this}}),u(o)||s(o)||g(o)){var a=[xe(o,t)];u(o)&&a.push(se(o)),(s(o)||g(o))&&a.push(Oe(o)),n=o.windowManager.open({title:"Insert/edit image",data:r,bodyType:"tabpanel",body:a,onSubmit:Le(Pe,o)})}else n=o.windowManager.open({title:"Insert/edit image",data:r,body:we(o,t),onSubmit:Le(Pe,o)});he(n)}return{open:function(){t(o,e)}}}var Ee=function(e){e.addCommand("mceImage",Ue(e).open)},ke=function(o){return function(e){for(var t,n,r=e.length,a=function(e){e.attr("contenteditable",o?"true":null)};r--;)t=e[r],(n=t.attr("class"))&&/\bimage\b/.test(n)&&(t.attr("contenteditable",o?"false":null),C.each(t.getAll("figcaption"),a))}},Me=function(e){e.on("preInit",function(){e.parser.addNodeFilter("figure",ke(!0)),e.serializer.addNodeFilter("figure",ke(!1))})},De=function(e){e.addButton("image",{icon:"image",tooltip:"Insert/edit image",onclick:Ue(e).open,stateSelector:"img:not([data-mce-object],[data-mce-placeholder]),figure.image"}),e.addMenuItem("image",{icon:"image",text:"Image",onclick:Ue(e).open,context:"insert",prependToContext:!0})};e.add("image",function(e){Me(e),De(e),Ee(e)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/imagetools/plugin.min.js b/lib/web/tiny_mce_4/plugins/imagetools/plugin.min.js index c1551224be187..f1b6a11104b2b 100644 --- a/lib/web/tiny_mce_4/plugins/imagetools/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/imagetools/plugin.min.js @@ -1 +1 @@ -!function(s){"use strict";var r=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return r(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),G=tinymce.util.Tools.resolve("tinymce.util.Tools"),i=function(t){return function(){return t}};function a(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var e,n,o,u,c=i(!1),l=i(!0),f=c,d=l,h=function(){return p},p=(u={fold:function(t,e){return t()},is:f,isSome:f,isNone:d,getOr:o=function(t){return t},getOrThunk:n=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:o,orThunk:n,map:h,ap:h,each:function(){},bind:h,flatten:h,exists:f,forall:d,filter:h,equals:e=function(t){return t.isNone()},equals_:e,toArray:function(){return[]},toString:i("none()")},Object.freeze&&Object.freeze(u),u),m=function(n){var t=function(){return n},e=function(){return o},r=function(t){return t(n)},o={fold:function(t,e){return e(n)},is:function(t){return n===t},isSome:d,isNone:f,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:e,orThunk:e,map:function(t){return m(t(n))},ap:function(t){return t.fold(h,function(t){return m(t(n))})},each:function(t){t(n)},bind:r,flatten:t,exists:r,forall:r,filter:function(t){return t(n)?o:p},equals:function(t){return t.is(n)},equals_:function(t,e){return t.fold(f,function(t){return e(n,t)})},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},y={some:m,none:h,from:function(t){return null===t||t===undefined?p:m(t)}},g="undefined"!=typeof s.window?s.window:Function("return this;")(),v=function(t,e){return function(t,e){for(var n=e!==undefined&&null!==e?e:g,r=0;r<t.length&&n!==undefined&&null!==n;++r)n=n[t[r]];return n}(t.split("."),e)},w={getOrDie:function(t,e){var n=v(t,e);if(n===undefined||null===n)throw t+" not available on this browser";return n}};function b(){return new(w.getOrDie("FileReader"))}var x={atob:function(t){return w.getOrDie("atob")(t)},requestAnimationFrame:function(t){w.getOrDie("requestAnimationFrame")(t)}};function k(t,e){return M(s.document.createElement("canvas"),t,e)}function R(t){var e=k(t.width,t.height);return I(e).drawImage(t,0,0),e}function I(t){return t.getContext("2d")}function M(t,e,n){return t.width=e,t.height=n,t}function T(t){return t.naturalWidth||t.width}function O(t){return t.naturalHeight||t.height}var U=window.Promise?window.Promise:function(){var i=function(t){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof t)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],f(t,r(o,this),r(u,this))},t=i.immediateFn||"function"==typeof window.setImmediate&&window.setImmediate||function(t){s.setTimeout(t,1)};function r(t,e){return function(){return t.apply(e,arguments)}}var n=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function a(r){var o=this;null!==this._state?t(function(){var t=o._state?r.onFulfilled:r.onRejected;if(null!==t){var e;try{e=t(o._value)}catch(n){return void r.reject(n)}r.resolve(e)}else(o._state?r.resolve:r.reject)(o._value)}):this._deferreds.push(r)}function o(t){try{if(t===this)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var e=t.then;if("function"==typeof e)return void f(r(e,t),r(o,this),r(u,this))}this._state=!0,this._value=t,c.call(this)}catch(n){u.call(this,n)}}function u(t){this._state=!1,this._value=t,c.call(this)}function c(){for(var t=0,e=this._deferreds;t<e.length;t++){var n=e[t];a.call(this,n)}this._deferreds=[]}function l(t,e,n,r){this.onFulfilled="function"==typeof t?t:null,this.onRejected="function"==typeof e?e:null,this.resolve=n,this.reject=r}function f(t,e,n){var r=!1;try{t(function(t){r||(r=!0,e(t))},function(t){r||(r=!0,n(t))})}catch(o){if(r)return;r=!0,n(o)}}return i.prototype["catch"]=function(t){return this.then(null,t)},i.prototype.then=function(n,r){var o=this;return new i(function(t,e){a.call(o,new l(n,r,t,e))})},i.all=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var c=Array.prototype.slice.call(1===t.length&&n(t[0])?t[0]:t);return new i(function(o,i){if(0===c.length)return o([]);var a=c.length;function u(e,t){try{if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if("function"==typeof n)return void n.call(t,function(t){u(e,t)},i)}c[e]=t,0==--a&&o(c)}catch(r){i(r)}}for(var t=0;t<c.length;t++)u(t,c[t])})},i.resolve=function(e){return e&&"object"==typeof e&&e.constructor===i?e:new i(function(t){t(e)})},i.reject=function(n){return new i(function(t,e){e(n)})},i.race=function(o){return new i(function(t,e){for(var n=0,r=o;n<r.length;n++)r[n].then(t,e)})},i}();function C(t){var r,e=t.src;return 0===e.indexOf("data:")?E(e):(r=e,new U(function(t,n){var e=new s.XMLHttpRequest;e.open("GET",r,!0),e.responseType="blob",e.onload=function(){200===this.status&&t(this.response)},e.onerror=function(){var t,e=this;n(0===this.status?((t=new Error("No access to download image")).code=18,t.name="SecurityError",t):new Error("Error "+e.status+" downloading image"))},e.send()}))}function A(u){return new U(function(t,e){var n=s.URL.createObjectURL(u),r=new s.Image,o=function(){r.removeEventListener("load",i),r.removeEventListener("error",a)};function i(){o(),t(r)}function a(){o(),e("Unable to load data of type "+u.type+": "+n)}r.addEventListener("load",i),r.addEventListener("error",a),r.src=n,r.complete&&i()})}function _(t){var e=t.split(","),n=/data:([^;]+)/.exec(e[0]);if(!n)return y.none();for(var r,o,i,a=n[1],u=e[1],c=x.atob(u),l=c.length,f=Math.ceil(l/1024),s=new Array(f),d=0;d<f;++d){for(var h=1024*d,p=Math.min(h+1024,l),m=new Array(p-h),g=h,v=0;g<p;++v,++g)m[v]=c[g].charCodeAt(0);s[d]=(r=m,new(w.getOrDie("Uint8Array"))(r))}return y.some((o=s,i={type:a},new(w.getOrDie("Blob"))(o,i)))}function E(n){return new U(function(t,e){_(n).fold(function(){e("uri is not base64: "+n)},t)})}function j(t,r,o){return r=r||"image/png",s.HTMLCanvasElement.prototype.toBlob?new U(function(e,n){t.toBlob(function(t){t?e(t):n()},r,o)}):E(t.toDataURL(r,o))}function z(t){return A(t).then(function(t){var e;e=t,s.URL.revokeObjectURL(e.src);var n=k(T(t),O(t));return I(n).drawImage(t,0,0),n})}function D(t,e,n){var r=e.type;function o(r,o){return t.then(function(t){return n=o,e=(e=r)||"image/png",t.toDataURL(e,n);var e,n})}return{getType:i(r),toBlob:function(){return U.resolve(e)},toDataURL:function(){return n},toBase64:function(){return n.split(",")[1]},toAdjustedBlob:function(e,n){return t.then(function(t){return j(t,e,n)})},toAdjustedDataURL:o,toAdjustedBase64:function(t,e){return o(t,e).then(function(t){return t.split(",")[1]})},toCanvas:function(){return t.then(R)}}}function L(e){return(n=e,new U(function(t){var e=b();e.onloadend=function(){t(e.result)},e.readAsDataURL(n)})).then(function(t){return D(z(e),e,t)});var n}function B(e,t){return j(e,t).then(function(t){return D(U.resolve(e),t,e.toDataURL())})}function S(t,e,n){var r="string"==typeof t?parseFloat(t):t;return n<r?r=n:r<e&&(r=e),r}var P=[0,.01,.02,.04,.05,.06,.07,.08,.1,.11,.12,.14,.15,.16,.17,.18,.2,.21,.22,.24,.25,.27,.28,.3,.32,.34,.36,.38,.4,.42,.44,.46,.48,.5,.53,.56,.59,.62,.65,.68,.71,.74,.77,.8,.83,.86,.89,.92,.95,.98,1,1.06,1.12,1.18,1.24,1.3,1.36,1.42,1.48,1.54,1.6,1.66,1.72,1.78,1.84,1.9,1.96,2,2.12,2.25,2.37,2.5,2.62,2.75,2.87,3,3.2,3.4,3.6,3.8,4,4.3,4.7,4.9,5,5.5,6,6.5,6.8,7,7.3,7.5,7.8,8,8.4,8.7,9,9.4,9.6,9.8,10];function H(t,e){for(var n,r=[],o=new Array(25),i=0;i<5;i++){for(var a=0;a<5;a++)r[a]=e[a+5*i];for(a=0;a<5;a++){for(var u=n=0;u<5;u++)n+=t[a+5*u]*r[u];o[a+5*i]=n}}return o}function F(t,n){return n=S(n,0,1),t.map(function(t,e){return e%6==0?t=1-(1-t)*n:t*=n,S(t,0,1)})}function V(a,u){return a.toCanvas().then(function(t){return e=t,n=a.getType(),r=u,o=I(e),i=function(t,e){for(var n,r,o,i,a=t.data,u=e[0],c=e[1],l=e[2],f=e[3],s=e[4],d=e[5],h=e[6],p=e[7],m=e[8],g=e[9],v=e[10],y=e[11],w=e[12],b=e[13],x=e[14],k=e[15],R=e[16],I=e[17],M=e[18],T=e[19],O=0;O<a.length;O+=4)n=a[O],r=a[O+1],o=a[O+2],i=a[O+3],a[O]=n*u+r*c+o*l+i*f+s,a[O+1]=n*d+r*h+o*p+i*m+g,a[O+2]=n*v+r*y+o*w+i*b+x,a[O+3]=n*k+r*R+o*I+i*M+T;return t}(o.getImageData(0,0,e.width,e.height),r),o.putImageData(i,0,0),B(e,n);var e,n,r,o,i})}function W(u,c){return u.toCanvas().then(function(t){return e=t,n=u.getType(),r=c,o=I(e),i=o.getImageData(0,0,e.width,e.height),a=o.getImageData(0,0,e.width,e.height),a=function(t,e,n){function r(t,e,n){return n<t?t=n:t<e&&(t=e),t}for(var o=Math.round(Math.sqrt(n.length)),i=Math.floor(o/2),a=t.data,u=e.data,c=t.width,l=t.height,f=0;f<l;f++)for(var s=0;s<c;s++){for(var d=0,h=0,p=0,m=0;m<o;m++)for(var g=0;g<o;g++){var v=r(s+g-i,0,c-1),y=r(f+m-i,0,l-1),w=4*(y*c+v),b=n[m*o+g];d+=a[w]*b,h+=a[w+1]*b,p+=a[w+2]*b}var x=4*(f*c+s);u[x]=r(d,0,255),u[x+1]=r(h,0,255),u[x+2]=r(p,0,255)}return e}(i,a,r),o.putImageData(a,0,0),B(e,n);var e,n,r,o,i,a})}function N(u){return function(e,n){return e.toCanvas().then(function(t){return function(t,e,n){for(var r=I(t),o=new Array(256),i=0;i<o.length;i++)o[i]=u(i,n);var a=function(t,e){for(var n=t.data,r=0;r<n.length;r+=4)n[r]=e[n[r]],n[r+1]=e[n[r+1]],n[r+2]=e[n[r+2]];return t}(r.getImageData(0,0,t.width,t.height),o);return r.putImageData(a,0,0),B(t,e)}(t,e.getType(),n)})}}function q(n){return function(t,e){return V(t,n([1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1],e))}}function $(e){return function(t){return W(t,e)}}var X,Y=(X=[-1,0,0,0,255,0,-1,0,0,255,0,0,-1,0,255,0,0,0,1,0,0,0,0,0,1],function(t){return V(t,X)}),K=q(function(t,e){return H(t,[1,0,0,0,e=S(255*e,-255,255),0,1,0,0,e,0,0,1,0,e,0,0,0,1,0,0,0,0,0,1])}),J=q(function(t,e){e=S(e,-180,180)/180*Math.PI;var n=Math.cos(e),r=Math.sin(e),o=.213,i=.715,a=.072;return H(t,[o+.787*n+r*-o,i+n*-i+r*-i,a+n*-a+.928*r,0,0,o+n*-o+.143*r,i+n*(1-i)+.14*r,a+n*-a+-.283*r,0,0,o+n*-o+-.787*r,i+n*-i+r*i,a+.928*n+r*a,0,0,0,0,0,1,0,0,0,0,0,1])}),Z=q(function(t,e){var n=1+(0<(e=S(e,-1,1))?3*e:e);return H(t,[.3086*(1-n)+n,.6094*(1-n),.082*(1-n),0,0,.3086*(1-n),.6094*(1-n)+n,.082*(1-n),0,0,.3086*(1-n),.6094*(1-n),.082*(1-n)+n,0,0,0,0,0,1,0,0,0,0,0,1])}),Q=q(function(t,e){var n;return e=S(e,-1,1),H(t,[(n=(e*=100)<0?127+e/100*127:127*(n=0==(n=e%1)?P[e]:P[Math.floor(e)]*(1-n)+P[Math.floor(e)+1]*n)+127)/127,0,0,0,.5*(127-n),0,n/127,0,0,.5*(127-n),0,0,n/127,0,.5*(127-n),0,0,0,1,0,0,0,0,0,1])}),tt=q(function(t,e){return H(t,F([.33,.34,.33,0,0,.33,.34,.33,0,0,.33,.34,.33,0,0,0,0,0,1,0,0,0,0,0,1],e=S(e,0,1)))}),et=q(function(t,e){return H(t,F([.393,.769,.189,0,0,.349,.686,.168,0,0,.272,.534,.131,0,0,0,0,0,1,0,0,0,0,0,1],e=S(e,0,1)))}),nt=function(t,e,n,r){return V(t,(o=n,i=r,H([1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1],[S(e,0,2),0,0,0,0,0,o=S(o,0,2),0,0,0,0,0,i=S(i,0,2),0,0,0,0,0,1,0,0,0,0,0,1])));var o,i},rt=$([0,-1,0,-1,5,-1,0,-1,0]),ot=$([-2,-1,0,-1,1,1,0,1,2]),it=N(function(t,e){return 255*Math.pow(t/255,1-e)}),at=N(function(t,e){return 255*(1-Math.exp(-t/255*e))});function ut(t,e,n){var r=T(t),o=O(t),i=e/r,a=n/o,u=!1;(i<.5||2<i)&&(i=i<.5?.5:2,u=!0),(a<.5||2<a)&&(a=a<.5?.5:2,u=!0);var c,l,f,s=(c=t,l=i,f=a,new U(function(t){var e=T(c),n=O(c),r=Math.floor(e*l),o=Math.floor(n*f),i=k(r,o),a=I(i);a.drawImage(c,0,0,e,n,0,0,r,o),t(i)}));return u?s.then(function(t){return ut(t,e,n)}):s}function ct(c,l){return c.toCanvas().then(function(t){return e=t,n=c.getType(),r=l,o=k(e.width,e.height),i=I(o),90!==(r=r<(u=a=0)?360+r:r)&&270!==r||M(o,o.height,o.width),90!==r&&180!==r||(a=o.width),270!==r&&180!==r||(u=o.height),i.translate(a,u),i.rotate(r*Math.PI/180),i.drawImage(e,0,0),B(o,n);var e,n,r,o,i,a,u})}function lt(a,u){return a.toCanvas().then(function(t){return e=t,n=a.getType(),r=u,o=k(e.width,e.height),i=I(o),"v"===r?(i.scale(1,-1),i.drawImage(e,0,-o.height)):(i.scale(-1,1),i.drawImage(e,-o.width,0)),B(o,n);var e,n,r,o,i})}function ft(a,u,c,l,f){return a.toCanvas().then(function(t){return e=t,n=a.getType(),r=u,o=c,I(i=k(l,f)).drawImage(e,-r,-o),B(i,n);var e,n,r,o,i})}var st=function(t){return Y(t)},dt=function(t){return rt(t)},ht=function(t){return ot(t)},pt=function(t,e){return it(t,e)},mt=function(t,e){return at(t,e)},gt=function(t,e,n,r){return nt(t,e,n,r)},vt=function(t,e){return K(t,e)},yt=function(t,e){return J(t,e)},wt=function(t,e){return Z(t,e)},bt=function(t,e){return Q(t,e)},xt=function(t,e){return tt(t,e)},kt=function(t,e){return et(t,e)},Rt=function(t,e){return lt(t,e)},It=function(t,e,n,r,o){return ft(t,e,n,r,o)},Mt=function(t,e,n){return o=e,i=n,(r=t).toCanvas().then(function(t){return ut(t,o,i).then(function(t){return B(t,r.getType())})});var r,o,i},Tt=function(t,e){return ct(t,e)},Ot=function(t){return L(t)},Ut=function(){return w.getOrDie("URL")},Ct={createObjectURL:function(t){return Ut().createObjectURL(t)},revokeObjectURL:function(t){Ut().revokeObjectURL(t)}},At=tinymce.util.Tools.resolve("tinymce.util.Delay"),_t=tinymce.util.Tools.resolve("tinymce.util.Promise"),Et=tinymce.util.Tools.resolve("tinymce.util.URI"),jt=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),zt=tinymce.util.Tools.resolve("tinymce.ui.Factory"),Dt=tinymce.util.Tools.resolve("tinymce.geom.Rect"),Lt=function(n){return new _t(function(t){var e=function(){n.removeEventListener("load",e),t(n)};n.complete?t(n):n.addEventListener("load",e)})},Bt=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),St=tinymce.util.Tools.resolve("tinymce.util.Observable"),Pt=tinymce.util.Tools.resolve("tinymce.util.VK"),Ht=0,Ft={create:function(t){return new(zt.get("Control").extend({Defaults:{classes:"imagepanel"},selection:function(t){return arguments.length?(this.state.set("rect",t),this):this.state.get("rect")},imageSize:function(){var t=this.state.get("viewRect");return{w:t.w,h:t.h}},toggleCropRect:function(t){this.state.set("cropEnabled",t)},imageSrc:function(t){var o=this,i=new s.Image;i.src=t,Lt(i).then(function(){var t,e,n=o.state.get("viewRect");if((e=o.$el.find("img"))[0])e.replaceWith(i);else{var r=s.document.createElement("div");r.className="mce-imagepanel-bg",o.getEl().appendChild(r),o.getEl().appendChild(i)}t={x:0,y:0,w:i.naturalWidth,h:i.naturalHeight},o.state.set("viewRect",t),o.state.set("rect",Dt.inflate(t,-20,-20)),n&&n.w===t.w&&n.h===t.h||o.zoomFit(),o.repaintImage(),o.fire("load")})},zoom:function(t){return arguments.length?(this.state.set("zoom",t),this):this.state.get("zoom")},postRender:function(){return this.imageSrc(this.settings.imageSrc),this._super()},zoomFit:function(){var t,e,n,r,o,i;t=this.$el.find("img"),e=this.getEl().clientWidth,n=this.getEl().clientHeight,r=t[0].naturalWidth,o=t[0].naturalHeight,1<=(i=Math.min((e-10)/r,(n-10)/o))&&(i=1),this.zoom(i)},repaintImage:function(){var t,e,n,r,o,i,a,u,c,l,f;f=this.getEl(),c=this.zoom(),l=this.state.get("rect"),a=this.$el.find("img"),u=this.$el.find(".mce-imagepanel-bg"),o=f.offsetWidth,i=f.offsetHeight,n=a[0].naturalWidth*c,r=a[0].naturalHeight*c,t=Math.max(0,o/2-n/2),e=Math.max(0,i/2-r/2),a.css({left:t,top:e,width:n,height:r}),u.css({left:t,top:e,width:n,height:r}),this.cropRect&&(this.cropRect.setRect({x:l.x*c+t,y:l.y*c+e,w:l.w*c,h:l.h*c}),this.cropRect.setClampRect({x:t,y:e,w:n,h:r}),this.cropRect.setViewPortRect({x:0,y:0,w:o,h:i}))},bindStates:function(){var r=this;function n(t){r.cropRect=function(l,n,f,r,o){var s,a,t,i,e="mce-",u=e+"crid-"+Ht++;function d(t,e){return{x:e.x-t.x,y:e.y-t.y,w:e.w,h:e.h}}function c(t,e,n,r){var o,i,a,u,c;o=e.x,i=e.y,a=e.w,u=e.h,o+=n*t.deltaX,i+=r*t.deltaY,(a+=n*t.deltaW)<20&&(a=20),(u+=r*t.deltaH)<20&&(u=20),c=l=Dt.clamp({x:o,y:i,w:a,h:u},f,"move"===t.name),c=d(f,c),s.fire("updateRect",{rect:c}),m(c)}function h(e){function t(t,e){e.h<0&&(e.h=0),e.w<0&&(e.w=0),Bt("#"+u+"-"+t,r).css({left:e.x,top:e.y,width:e.w,height:e.h})}G.each(a,function(t){Bt("#"+u+"-"+t.name,r).css({left:e.w*t.xMul+e.x,top:e.h*t.yMul+e.y})}),t("top",{x:n.x,y:n.y,w:n.w,h:e.y-n.y}),t("right",{x:e.x+e.w,y:e.y,w:n.w-e.x-e.w+n.x,h:e.h}),t("bottom",{x:n.x,y:e.y+e.h,w:n.w,h:n.h-e.y-e.h+n.y}),t("left",{x:n.x,y:e.y,w:e.x-n.x,h:e.h}),t("move",e)}function p(t){h(l=t)}function m(t){var e,n;p((e=f,{x:(n=t).x+e.x,y:n.y+e.y,w:n.w,h:n.h}))}return a=[{name:"move",xMul:0,yMul:0,deltaX:1,deltaY:1,deltaW:0,deltaH:0,label:"Crop Mask"},{name:"nw",xMul:0,yMul:0,deltaX:1,deltaY:1,deltaW:-1,deltaH:-1,label:"Top Left Crop Handle"},{name:"ne",xMul:1,yMul:0,deltaX:0,deltaY:1,deltaW:1,deltaH:-1,label:"Top Right Crop Handle"},{name:"sw",xMul:0,yMul:1,deltaX:1,deltaY:0,deltaW:-1,deltaH:1,label:"Bottom Left Crop Handle"},{name:"se",xMul:1,yMul:1,deltaX:0,deltaY:0,deltaW:1,deltaH:1,label:"Bottom Right Crop Handle"}],i=["top","right","bottom","left"],Bt('<div id="'+u+'" class="'+e+'croprect-container" role="grid" aria-dropeffect="execute">').appendTo(r),G.each(i,function(t){Bt("#"+u,r).append('<div id="'+u+"-"+t+'"class="'+e+'croprect-block" style="display: none" data-mce-bogus="all">')}),G.each(a,function(t){Bt("#"+u,r).append('<div id="'+u+"-"+t.name+'" class="'+e+"croprect-handle "+e+"croprect-handle-"+t.name+'"style="display: none" data-mce-bogus="all" role="gridcell" tabindex="-1" aria-label="'+t.label+'" aria-grabbed="false">')}),t=G.map(a,function(e){var n;return new(zt.get("DragHelper"))(u,{document:r.ownerDocument,handle:u+"-"+e.name,start:function(){n=l},drag:function(t){c(e,n,t.deltaX,t.deltaY)}})}),h(l),Bt(r).on("focusin focusout",function(t){Bt(t.target).attr("aria-grabbed","focus"===t.type)}),Bt(r).on("keydown",function(e){var i;function t(t,e,n,r,o){t.stopPropagation(),t.preventDefault(),c(i,n,r,o)}switch(G.each(a,function(t){if(e.target.id===u+"-"+t.name)return i=t,!1}),e.keyCode){case Pt.LEFT:t(e,0,l,-10,0);break;case Pt.RIGHT:t(e,0,l,10,0);break;case Pt.UP:t(e,0,l,0,-10);break;case Pt.DOWN:t(e,0,l,0,10);break;case Pt.ENTER:case Pt.SPACEBAR:e.preventDefault(),o()}}),s=G.extend({toggleVisibility:function(t){var e;e=G.map(a,function(t){return"#"+u+"-"+t.name}).concat(G.map(i,function(t){return"#"+u+"-"+t})).join(","),t?Bt(e,r).show():Bt(e,r).hide()},setClampRect:function(t){f=t,h(l)},setRect:p,getInnerRect:function(){return d(f,l)},setInnerRect:m,setViewPortRect:function(t){n=t,h(l)},destroy:function(){G.each(t,function(t){t.destroy()}),t=[]}},St)}(t,r.state.get("viewRect"),r.state.get("viewRect"),r.getEl(),function(){r.fire("crop")}),r.cropRect.on("updateRect",function(t){var e=t.rect,n=r.zoom();e={x:Math.round(e.x/n),y:Math.round(e.y/n),w:Math.round(e.w/n),h:Math.round(e.h/n)},r.state.set("rect",e)}),r.on("remove",r.cropRect.destroy)}r.state.on("change:cropEnabled",function(t){r.cropRect.toggleVisibility(t.value),r.repaintImage()}),r.state.on("change:zoom",function(){r.repaintImage()}),r.state.on("change:rect",function(t){var e=t.value;r.cropRect||n(e),r.cropRect.setRect(e)})}}))(t)}};function Vt(t){return{blob:t,url:Ct.createObjectURL(t)}}function Wt(t){t&&Ct.revokeObjectURL(t.url)}function Nt(t){G.each(t,Wt)}function qt(i,a,t,e){var u,n,r,c,o,l,f,s,d,h,p,m,g,v,y,w,b,x,k,R,I,M,T,O,U,C,A,_=function(){var n=[],r=-1;function t(){return 0<r}function e(){return-1!==r&&r<n.length-1}return{data:n,add:function(t){var e;return e=n.splice(++r),n.push(t),{state:t,removed:e}},undo:function(){if(t())return n[--r]},redo:function(){if(e())return n[++r]},canUndo:t,canRedo:e}}(),E=function(t){return i.rtl?t.reverse():t};function j(t){var e,n,r,o;e=u.find("#w")[0],n=u.find("#h")[0],r=parseInt(e.value(),10),o=parseInt(n.value(),10),u.find("#constrain")[0].checked()&&O&&U&&r&&o&&("w"===t.control.settings.name?(o=Math.round(r*C),n.value(o)):(r=Math.round(o*A),e.value(r))),O=r,U=o}function z(t){return Math.round(100*t)+"%"}function D(){u.find("#undo").disabled(!_.canUndo()),u.find("#redo").disabled(!_.canRedo()),u.statusbar.find("#save").disabled(!_.canUndo())}function L(){u.find("#undo").disabled(!0),u.find("#redo").disabled(!0)}function B(t){t&&s.imageSrc(t.url)}function S(e){return function(){var t=G.grep(T,function(t){return t.settings.name!==e});G.each(t,function(t){t.hide()}),e.show(),e.focus()}}function P(t){B(c=Vt(t))}function H(t){B(a=Vt(t)),Nt(_.add(a).removed),D()}function F(){var e=s.selection();Ot(a.blob).then(function(t){It(t,e.x,e.y,e.w,e.h).then($).then(function(t){H(t),W()})})}var V=function(e){var n=[].slice.call(arguments,1);return function(){Ot((c||a).blob).then(function(t){e.apply(this,[t].concat(n)).then($).then(P)})}};function W(){B(a),Wt(c),S(n)(),D()}function N(){c?(H(c.blob),W()):function t(e,n){c?n():setTimeout(function(){0<e--?t(e,n):i.windowManager.alert("Error: failed to apply image operation.")},10)}(100,N)}function q(t){return zt.create("Form",{layout:"flex",direction:"row",labelGap:5,border:"0 0 1 0",align:"center",pack:"center",padding:"0 10 0 10",spacing:5,flex:0,minHeight:60,defaults:{classes:"imagetool",type:"button"},items:t})}var $=function(t){return t.toBlob()};function X(t,e){return q(E([{text:"Back",onclick:W},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",function(){L(),Ot(a.blob).then(function(t){return e(t)}).then($).then(function(t){var e=Vt(t);B(e),Wt(c),c=e})})}function Y(t,n,e,r,o){return q(E([{text:"Back",onclick:W},{type:"spacer",flex:1},{type:"slider",flex:1,ondragend:function(t){var e;e=t.value,Ot(a.blob).then(function(t){return n(t,e)}).then($).then(function(t){var e=Vt(t);B(e),Wt(c),c=e})},minValue:i.rtl?o:r,maxValue:i.rtl?r:o,value:e,previewFilter:z},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",function(){this.find("slider").value(e),L()})}o=q(E([{text:"Back",onclick:W},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:F}])).hide().on("show hide",function(t){s.toggleCropRect("show"===t.type)}).on("show",L),l=q(E([{text:"Back",onclick:W},{type:"spacer",flex:1},{type:"textbox",name:"w",label:"Width",size:4,onkeyup:j},{type:"textbox",name:"h",label:"Height",size:4,onkeyup:j},{type:"checkbox",name:"constrain",text:"Constrain proportions",checked:!0,onchange:function(t){!0===t.control.value()&&(C=U/O,A=O/U)}},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:"submit"}])).hide().on("submit",function(t){var e=parseInt(u.find("#w").value(),10),n=parseInt(u.find("#h").value(),10);t.preventDefault(),function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r=[].slice.call(arguments,1);return function(){Ot(a.blob).then(function(t){e.apply(this,[t].concat(r)).then($).then(H)})}}(Mt,e,n)(),W()}).on("show",L),f=q(E([{text:"Back",onclick:W},{type:"spacer",flex:1},{icon:"fliph",tooltip:"Flip horizontally",onclick:V(Rt,"h")},{icon:"flipv",tooltip:"Flip vertically",onclick:V(Rt,"v")},{icon:"rotateleft",tooltip:"Rotate counterclockwise",onclick:V(Tt,-90)},{icon:"rotateright",tooltip:"Rotate clockwise",onclick:V(Tt,90)},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",L),p=X(0,st),k=X(0,dt),R=X(0,ht),m=Y(0,vt,0,-1,1),g=Y(0,yt,180,0,360),v=Y(0,wt,0,-1,1),y=Y(0,bt,0,-1,1),w=Y(0,xt,0,0,1),b=Y(0,kt,0,0,1),x=function(t,o){function e(){var e,n,r;e=u.find("#r")[0].value(),n=u.find("#g")[0].value(),r=u.find("#b")[0].value(),Ot(a.blob).then(function(t){return o(t,e,n,r)}).then($).then(function(t){var e=Vt(t);B(e),Wt(c),c=e})}var n=i.rtl?2:0,r=i.rtl?0:2;return q(E([{text:"Back",onclick:W},{type:"spacer",flex:1},{type:"slider",label:"R",name:"r",minValue:n,value:1,maxValue:r,ondragend:e,previewFilter:z},{type:"slider",label:"G",name:"g",minValue:n,value:1,maxValue:r,ondragend:e,previewFilter:z},{type:"slider",label:"B",name:"b",minValue:n,value:1,maxValue:r,ondragend:e,previewFilter:z},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",function(){u.find("#r,#g,#b").value(1),L()})}(0,gt),I=Y(0,pt,0,-1,1),M=Y(0,mt,1,0,2),r=q(E([{text:"Back",onclick:W},{type:"spacer",flex:1},{text:"hue",icon:"hue",onclick:S(g)},{text:"saturate",icon:"saturate",onclick:S(v)},{text:"sepia",icon:"sepia",onclick:S(b)},{text:"emboss",icon:"emboss",onclick:S(R)},{text:"exposure",icon:"exposure",onclick:S(M)},{type:"spacer",flex:1}])).hide(),n=q(E([{tooltip:"Crop",icon:"crop",onclick:S(o)},{tooltip:"Resize",icon:"resize2",onclick:S(l)},{tooltip:"Orientation",icon:"orientation",onclick:S(f)},{tooltip:"Brightness",icon:"sun",onclick:S(m)},{tooltip:"Sharpen",icon:"sharpen",onclick:S(k)},{tooltip:"Contrast",icon:"contrast",onclick:S(y)},{tooltip:"Color levels",icon:"drop",onclick:S(x)},{tooltip:"Gamma",icon:"gamma",onclick:S(I)},{tooltip:"Invert",icon:"invert",onclick:S(p)}])),s=Ft.create({flex:1,imageSrc:a.url}),d=zt.create("Container",{layout:"flex",direction:"column",pack:"start",border:"0 1 0 0",padding:5,spacing:5,items:[{type:"button",icon:"undo",tooltip:"Undo",name:"undo",onclick:function(){B(a=_.undo()),D()}},{type:"button",icon:"redo",tooltip:"Redo",name:"redo",onclick:function(){B(a=_.redo()),D()}},{type:"button",icon:"zoomin",tooltip:"Zoom in",onclick:function(){var t=s.zoom();t<2&&(t+=.1),s.zoom(t)}},{type:"button",icon:"zoomout",tooltip:"Zoom out",onclick:function(){var t=s.zoom();.1<t&&(t-=.1),s.zoom(t)}}]}),h=zt.create("Container",{type:"container",layout:"flex",direction:"row",align:"stretch",flex:1,items:E([d,s])}),T=[n,o,l,f,r,p,m,g,v,y,w,b,x,k,R,I,M],(u=i.windowManager.open({layout:"flex",direction:"column",align:"stretch",minWidth:Math.min(jt.DOM.getViewPort().w,800),minHeight:Math.min(jt.DOM.getViewPort().h,650),title:"Edit image",items:T.concat([h]),buttons:E([{text:"Save",name:"save",subtype:"primary",onclick:function(){t(a.blob),u.close()}},{text:"Cancel",onclick:"close"}])})).on("close",function(){e(),Nt(_.data),c=_=null}),_.add(a),D(),s.on("load",function(){O=s.imageSize().w,U=s.imageSize().h,C=U/O,A=O/U,u.find("#w").value(O),u.find("#h").value(U)}),s.on("crop",F)}var $t,Xt={edit:function(r,t){return new _t(function(e,n){return t.toBlob().then(function(t){qt(r,Vt(t),e,n)})})}},Yt={getImageSize:function(t){var e,n;function r(t){return/^[0-9\.]+px$/.test(t)}return e=t.style.width,n=t.style.height,e||n?r(e)&&r(n)?{w:parseInt(e,10),h:parseInt(n,10)}:null:(e=t.width,n=t.height,e&&n?{w:parseInt(e,10),h:parseInt(n,10)}:null)},setImageSize:function(t,e){var n,r;e&&(n=t.style.width,r=t.style.height,(n||r)&&(t.style.width=e.w+"px",t.style.height=e.h+"px",t.removeAttribute("data-mce-style")),n=t.width,r=t.height,(n||r)&&(t.setAttribute("width",e.w),t.setAttribute("height",e.h)))},getNaturalImageSize:function(t){return{w:t.naturalWidth,h:t.naturalHeight}}},Gt=($t="function",function(t){return function(t){if(null===t)return"null";var e=typeof t;return"object"===e&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":e}(t)===$t}),Kt=(Array.prototype.slice,function(t,e){for(var n=0,r=t.length;n<r;n++){var o=t[n];if(e(o,n,t))return y.some(o)}return y.none()});Gt(Array.from)&&Array.from;var Jt=function(t){return null!==t&&t!==undefined},Zt=function(t,e){var n;return n=e.reduce(function(t,e){return Jt(t)?t[e]:undefined},t),Jt(n)?n:null},Qt=function(e){return new _t(function(n){var t=b();t.onload=function(t){var e=t.target;n(e.result)},t.readAsText(e)})},te=function(e,r,o){return new _t(function(t){var n;(n=new(w.getOrDie("XMLHttpRequest"))).onreadystatechange=function(){4===n.readyState&&t({status:n.status,blob:this.response})},n.open("GET",e,!0),n.withCredentials=o,G.each(r,function(t,e){n.setRequestHeader(e,t)}),n.responseType="blob",n.send()})},ee=function(t){var e;try{e=JSON.parse(t)}catch(n){}return e},ne=[{code:404,message:"Could not find Image Proxy"},{code:403,message:"Rejected request"},{code:0,message:"Incorrect Image Proxy URL"}],re=[{type:"key_missing",message:"The request did not include an api key."},{type:"key_not_found",message:"The provided api key could not be found."},{type:"domain_not_trusted",message:"The api key is not valid for the request origins."}],oe=function(e){return"ImageProxy HTTP error: "+Kt(ne,function(t){return e===t.code}).fold(i("Unknown ImageProxy error"),function(t){return t.message})},ie=function(t){var e=oe(t);return _t.reject(e)},ae=function(e){return Kt(re,function(t){return t.type===e}).fold(i("Unknown service error"),function(t){return t.message})},ue=function(t,e){return Qt(e).then(function(t){var e,n,r=(e=ee(t),"ImageProxy Service error: "+((n=Zt(e,["error","type"]))?ae(n):"Invalid JSON in service error message"));return _t.reject(r)})},ce=function(t,e){return 400===(n=t)||403===n||500===n?ue(0,e):ie(t);var n},le=ie,fe=function(t,e){var n,r,o,i={"Content-Type":"application/json;charset=UTF-8","tiny-api-key":e};return te((n=t,r=e,o=-1===n.indexOf("?")?"?":"&",/[?&]apiKey=/.test(n)||!r?n:n+o+"apiKey="+encodeURIComponent(r)),i,!1).then(function(t){return t.status<200||300<=t.status?ce(t.status,t.blob):_t.resolve(t.blob)})},se=function(t,e,n){return e?fe(t,e):te(t,{},n).then(function(t){return t.status<200||300<=t.status?le(t.status):_t.resolve(t.blob)})},de=0,he=function(t,e){t.notificationManager.open({text:e,type:"error"})},pe=function(t){return t.selection.getNode()},me=function(t,e){var n=e.src;return 0===n.indexOf("data:")||0===n.indexOf("blob:")||new Et(n).host===t.documentBaseURI.host},ge=function(t,e){return-1!==G.inArray(t.getParam("imagetools_cors_hosts",[],"string[]"),new Et(e.src).host)},ve=function(t,e){var n,r,o,i,a=e.src;return ge(t,e)?se(e.src,null,(r=t,o=e,-1!==G.inArray(r.getParam("imagetools_credentials_hosts",[],"string[]"),new Et(o.src).host))):me(t,e)?C(e):(a=t.getParam("imagetools_proxy"),a+=(-1===a.indexOf("?")?"?":"&")+"url="+encodeURIComponent(e.src),n=(i=t).getParam("api_key",i.getParam("imagetools_api_key","","string"),"string"),se(a,n,!1))},ye=function(t){var e;return(e=t.editorUpload.blobCache.getByUri(pe(t).src))?_t.resolve(e.blob()):ve(t,pe(t))},we=function(t){clearTimeout(t.get())},be=function(c,l,f,s,d){return l.toBlob().then(function(t){var e,n,r,o,i,a,u;return r=c.editorUpload.blobCache,e=(i=pe(c)).src,c.getParam("images_reuse_filename",!1,"boolean")&&((o=r.getByUri(e))?(e=o.uri(),n=o.name()):(a=c,n=(u=e.match(/\/([^\/\?]+)?\.(?:jpeg|jpg|png|gif)(?:\?|$)/i))?a.dom.encode(u[1]):null)),o=r.create({id:"imagetools"+de++,blob:t,base64:l.toBase64(),uri:e,name:n}),r.add(o),c.undoManager.transact(function(){c.$(i).on("load",function t(){var e,n,r;c.$(i).off("load",t),c.nodeChanged(),f?c.editorUpload.uploadImagesAuto():(we(s),e=c,n=s,r=At.setEditorTimeout(e,function(){e.editorUpload.uploadImagesAuto()},e.getParam("images_upload_timeout",3e4,"number")),n.set(r))}),d&&c.$(i).attr({width:d.w,height:d.h}),c.$(i).attr({src:o.blobUri()}).removeAttr("data-mce-src")}),o})},xe=function(e,n,t,r){return function(){return e._scanForImages().then(a(ye,e)).then(Ot).then(t).then(function(t){return be(e,t,!1,n,r)},function(t){he(e,t)})}},ke=function(n,r,o){return function(){var t=Yt.getImageSize(pe(n)),e=t?{w:t.h,h:t.w}:null;return xe(n,r,function(t){return Tt(t,o)},e)()}},Re=function(t,e,n){return function(){return xe(t,e,function(t){return Rt(t,n)})()}},Ie=function(e,r){return function(){var o=pe(e),i=Yt.getNaturalImageSize(o),n=function(r){return new _t(function(n){var t;(t=r,A(t)).then(function(t){var e=Yt.getNaturalImageSize(t);i.w===e.w&&i.h===e.h||Yt.getImageSize(o)&&Yt.setImageSize(o,e),Ct.revokeObjectURL(t.src),n(r)})})};ye(e).then(Ot).then(a(function(e,t){return Xt.edit(e,t).then(n).then(Ot).then(function(t){return be(e,t,!0,r)},function(){})},e),function(t){he(e,t)})}},Me=function(t,e){return t.dom.is(e,"img:not([data-mce-object],[data-mce-placeholder])")&&(me(t,e)||ge(t,e)||t.settings.imagetools_proxy)},Te=we,Oe=function(n,t){G.each({mceImageRotateLeft:ke(n,t,-90),mceImageRotateRight:ke(n,t,90),mceImageFlipVertical:Re(n,t,"v"),mceImageFlipHorizontal:Re(n,t,"h"),mceEditImage:Ie(n,t)},function(t,e){n.addCommand(e,t)})},Ue=function(n,r,o){n.on("NodeChange",function(t){var e=o.get();e&&e.src!==t.element.src&&(Te(r),n.editorUpload.uploadImagesAuto(),o.set(null)),Me(n,t.element)&&o.set(t.element)})},Ce=function(t){t.addButton("rotateleft",{title:"Rotate counterclockwise",cmd:"mceImageRotateLeft"}),t.addButton("rotateright",{title:"Rotate clockwise",cmd:"mceImageRotateRight"}),t.addButton("flipv",{title:"Flip vertically",cmd:"mceImageFlipVertical"}),t.addButton("fliph",{title:"Flip horizontally",cmd:"mceImageFlipHorizontal"}),t.addButton("editimage",{title:"Edit image",cmd:"mceEditImage"}),t.addButton("imageoptions",{title:"Image options",icon:"options",cmd:"mceImage"})},Ae=function(t){t.addContextToolbar(a(Me,t),t.getParam("imagetools_toolbar","rotateleft rotateright | flipv fliph | crop editimage imageoptions"))};t.add("imagetools",function(t){var e=r(0),n=r(null);Oe(t,e),Ce(t),Ae(t),Ue(t,e,n)})}(window); \ No newline at end of file +!function(m){"use strict";var r=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return r(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),G=tinymce.util.Tools.resolve("tinymce.util.Tools"),e=function(){},i=function(t){return function(){return t}};function a(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var n,o,c,u,l=i(!1),f=i(!0),s=function(){return d},d=(n=function(t){return t.isNone()},u={fold:function(t,e){return t()},is:l,isSome:l,isNone:f,getOr:c=function(t){return t},getOrThunk:o=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:i(null),getOrUndefined:i(undefined),or:c,orThunk:o,map:s,each:e,bind:s,exists:l,forall:f,filter:s,equals:n,equals_:n,toArray:function(){return[]},toString:i("none()")},Object.freeze&&Object.freeze(u),u),h=function(n){var t=i(n),e=function(){return o},r=function(t){return t(n)},o={fold:function(t,e){return e(n)},is:function(t){return n===t},isSome:f,isNone:l,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:e,orThunk:e,map:function(t){return h(t(n))},each:function(t){t(n)},bind:r,exists:r,forall:r,filter:function(t){return t(n)?o:d},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(t){return t.is(n)},equals_:function(t,e){return t.fold(l,function(t){return e(n,t)})}};return o},g={some:h,none:s,from:function(t){return null===t||t===undefined?d:h(t)}};function p(t,e){return w(m.document.createElement("canvas"),t,e)}function v(t){var e=p(t.width,t.height);return y(e).drawImage(t,0,0),e}function y(t){return t.getContext("2d")}function w(t,e,n){return t.width=e,t.height=n,t}function b(t){return t.naturalWidth||t.width}function x(t){return t.naturalHeight||t.height}var k=window.Promise?window.Promise:function(){var i=function(t){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof t)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],f(t,r(o,this),r(c,this))},t=i.immediateFn||"function"==typeof window.setImmediate&&window.setImmediate||function(t){m.setTimeout(t,1)};function r(t,e){return function(){return t.apply(e,arguments)}}var n=Array.isArray||function(t){return"[object Array]"===Object.prototype.toString.call(t)};function a(r){var o=this;null!==this._state?t(function(){var t=o._state?r.onFulfilled:r.onRejected;if(null!==t){var e;try{e=t(o._value)}catch(n){return void r.reject(n)}r.resolve(e)}else(o._state?r.resolve:r.reject)(o._value)}):this._deferreds.push(r)}function o(t){try{if(t===this)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var e=t.then;if("function"==typeof e)return void f(r(e,t),r(o,this),r(c,this))}this._state=!0,this._value=t,u.call(this)}catch(n){c.call(this,n)}}function c(t){this._state=!1,this._value=t,u.call(this)}function u(){for(var t=0,e=this._deferreds;t<e.length;t++){var n=e[t];a.call(this,n)}this._deferreds=[]}function l(t,e,n,r){this.onFulfilled="function"==typeof t?t:null,this.onRejected="function"==typeof e?e:null,this.resolve=n,this.reject=r}function f(t,e,n){var r=!1;try{t(function(t){r||(r=!0,e(t))},function(t){r||(r=!0,n(t))})}catch(o){if(r)return;r=!0,n(o)}}return i.prototype["catch"]=function(t){return this.then(null,t)},i.prototype.then=function(n,r){var o=this;return new i(function(t,e){a.call(o,new l(n,r,t,e))})},i.all=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var u=Array.prototype.slice.call(1===t.length&&n(t[0])?t[0]:t);return new i(function(o,i){if(0===u.length)return o([]);var a=u.length;function c(e,t){try{if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if("function"==typeof n)return void n.call(t,function(t){c(e,t)},i)}u[e]=t,0==--a&&o(u)}catch(r){i(r)}}for(var t=0;t<u.length;t++)c(t,u[t])})},i.resolve=function(e){return e&&"object"==typeof e&&e.constructor===i?e:new i(function(t){t(e)})},i.reject=function(n){return new i(function(t,e){e(n)})},i.race=function(o){return new i(function(t,e){for(var n=0,r=o;n<r.length;n++)r[n].then(t,e)})},i}();function R(t){var r,e=t.src;return 0===e.indexOf("data:")?M(e):(r=e,new k(function(t,n){var e=new m.XMLHttpRequest;e.open("GET",r,!0),e.responseType="blob",e.onload=function(){200===this.status&&t(this.response)},e.onerror=function(){var t,e=this;n(0===this.status?((t=new Error("No access to download image")).code=18,t.name="SecurityError",t):new Error("Error "+e.status+" downloading image"))},e.send()}))}function I(c){return new k(function(t,e){var n=m.URL.createObjectURL(c),r=new m.Image,o=function(){r.removeEventListener("load",i),r.removeEventListener("error",a)};function i(){o(),t(r)}function a(){o(),e("Unable to load data of type "+c.type+": "+n)}r.addEventListener("load",i),r.addEventListener("error",a),r.src=n,r.complete&&i()})}function M(n){return new k(function(t,e){(function(t){var e=t.split(","),n=/data:([^;]+)/.exec(e[0]);if(!n)return g.none();for(var r=n[1],o=e[1],i=m.atob(o),a=i.length,c=Math.ceil(a/1024),u=new Array(c),l=0;l<c;++l){for(var f=1024*l,s=Math.min(f+1024,a),d=new Array(s-f),h=f,p=0;h<s;++p,++h)d[p]=i[h].charCodeAt(0);u[l]=new Uint8Array(d)}return g.some(new m.Blob(u,{type:r}))})(n).fold(function(){e("uri is not base64: "+n)},t)})}function T(t,r,o){return r=r||"image/png",m.HTMLCanvasElement.prototype.toBlob?new k(function(e,n){t.toBlob(function(t){t?e(t):n()},r,o)}):M(t.toDataURL(r,o))}function U(t){return I(t).then(function(t){var e;e=t,m.URL.revokeObjectURL(e.src);var n=p(b(t),x(t));return y(n).drawImage(t,0,0),n})}function C(t,e,n){var r=e.type;function o(r,o){return t.then(function(t){return n=o,e=(e=r)||"image/png",t.toDataURL(e,n);var e,n})}return{getType:i(r),toBlob:function(){return k.resolve(e)},toDataURL:function(){return n},toBase64:function(){return n.split(",")[1]},toAdjustedBlob:function(e,n){return t.then(function(t){return T(t,e,n)})},toAdjustedDataURL:o,toAdjustedBase64:function(t,e){return o(t,e).then(function(t){return t.split(",")[1]})},toCanvas:function(){return t.then(v)}}}function A(e){return(n=e,new k(function(t){var e=new m.FileReader;e.onloadend=function(){t(e.result)},e.readAsDataURL(n)})).then(function(t){return C(U(e),e,t)});var n}function E(e,t){return T(e,t).then(function(t){return C(k.resolve(e),t,e.toDataURL())})}function O(t,e,n){var r="string"==typeof t?parseFloat(t):t;return n<r?r=n:r<e&&(r=e),r}var _=[0,.01,.02,.04,.05,.06,.07,.08,.1,.11,.12,.14,.15,.16,.17,.18,.2,.21,.22,.24,.25,.27,.28,.3,.32,.34,.36,.38,.4,.42,.44,.46,.48,.5,.53,.56,.59,.62,.65,.68,.71,.74,.77,.8,.83,.86,.89,.92,.95,.98,1,1.06,1.12,1.18,1.24,1.3,1.36,1.42,1.48,1.54,1.6,1.66,1.72,1.78,1.84,1.9,1.96,2,2.12,2.25,2.37,2.5,2.62,2.75,2.87,3,3.2,3.4,3.6,3.8,4,4.3,4.7,4.9,5,5.5,6,6.5,6.8,7,7.3,7.5,7.8,8,8.4,8.7,9,9.4,9.6,9.8,10];function j(t,e){for(var n,r=[],o=new Array(25),i=0;i<5;i++){for(var a=0;a<5;a++)r[a]=e[a+5*i];for(a=0;a<5;a++){for(var c=n=0;c<5;c++)n+=t[a+5*c]*r[c];o[a+5*i]=n}}return o}function z(t,n){return n=O(n,0,1),t.map(function(t,e){return e%6==0?t=1-(1-t)*n:t*=n,O(t,0,1)})}function L(a,c){return a.toCanvas().then(function(t){return e=t,n=a.getType(),r=c,o=y(e),i=function(t,e){for(var n,r,o,i,a=t.data,c=e[0],u=e[1],l=e[2],f=e[3],s=e[4],d=e[5],h=e[6],p=e[7],m=e[8],g=e[9],v=e[10],y=e[11],w=e[12],b=e[13],x=e[14],k=e[15],R=e[16],I=e[17],M=e[18],T=e[19],U=0;U<a.length;U+=4)n=a[U],r=a[U+1],o=a[U+2],i=a[U+3],a[U]=n*c+r*u+o*l+i*f+s,a[U+1]=n*d+r*h+o*p+i*m+g,a[U+2]=n*v+r*y+o*w+i*b+x,a[U+3]=n*k+r*R+o*I+i*M+T;return t}(o.getImageData(0,0,e.width,e.height),r),o.putImageData(i,0,0),E(e,n);var e,n,r,o,i})}function B(c,u){return c.toCanvas().then(function(t){return e=t,n=c.getType(),r=u,o=y(e),i=o.getImageData(0,0,e.width,e.height),a=o.getImageData(0,0,e.width,e.height),a=function(t,e,n){function r(t,e,n){return n<t?t=n:t<e&&(t=e),t}for(var o=Math.round(Math.sqrt(n.length)),i=Math.floor(o/2),a=t.data,c=e.data,u=t.width,l=t.height,f=0;f<l;f++)for(var s=0;s<u;s++){for(var d=0,h=0,p=0,m=0;m<o;m++)for(var g=0;g<o;g++){var v=r(s+g-i,0,u-1),y=r(f+m-i,0,l-1),w=4*(y*u+v),b=n[m*o+g];d+=a[w]*b,h+=a[w+1]*b,p+=a[w+2]*b}var x=4*(f*u+s);c[x]=r(d,0,255),c[x+1]=r(h,0,255),c[x+2]=r(p,0,255)}return e}(i,a,r),o.putImageData(a,0,0),E(e,n);var e,n,r,o,i,a})}function S(c){return function(e,n){return e.toCanvas().then(function(t){return function(t,e,n){for(var r=y(t),o=new Array(256),i=0;i<o.length;i++)o[i]=c(i,n);var a=function(t,e){for(var n=t.data,r=0;r<n.length;r+=4)n[r]=e[n[r]],n[r+1]=e[n[r+1]],n[r+2]=e[n[r+2]];return t}(r.getImageData(0,0,t.width,t.height),o);return r.putImageData(a,0,0),E(t,e)}(t,e.getType(),n)})}}function P(n){return function(t,e){return L(t,n([1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1],e))}}function H(e){return function(t){return B(t,e)}}var D,F=(D=[-1,0,0,0,255,0,-1,0,0,255,0,0,-1,0,255,0,0,0,1,0,0,0,0,0,1],function(t){return L(t,D)}),V=P(function(t,e){return j(t,[1,0,0,0,e=O(255*e,-255,255),0,1,0,0,e,0,0,1,0,e,0,0,0,1,0,0,0,0,0,1])}),W=P(function(t,e){e=O(e,-180,180)/180*Math.PI;var n=Math.cos(e),r=Math.sin(e),o=.213,i=.715,a=.072;return j(t,[o+.787*n+r*-o,i+n*-i+r*-i,a+n*-a+.928*r,0,0,o+n*-o+.143*r,i+n*(1-i)+.14*r,a+n*-a+-.283*r,0,0,o+n*-o+-.787*r,i+n*-i+r*i,a+.928*n+r*a,0,0,0,0,0,1,0,0,0,0,0,1])}),N=P(function(t,e){var n=1+(0<(e=O(e,-1,1))?3*e:e);return j(t,[.3086*(1-n)+n,.6094*(1-n),.082*(1-n),0,0,.3086*(1-n),.6094*(1-n)+n,.082*(1-n),0,0,.3086*(1-n),.6094*(1-n),.082*(1-n)+n,0,0,0,0,0,1,0,0,0,0,0,1])}),q=P(function(t,e){var n;return e=O(e,-1,1),j(t,[(n=(e*=100)<0?127+e/100*127:127*(n=0==(n=e%1)?_[e]:_[Math.floor(e)]*(1-n)+_[Math.floor(e)+1]*n)+127)/127,0,0,0,.5*(127-n),0,n/127,0,0,.5*(127-n),0,0,n/127,0,.5*(127-n),0,0,0,1,0,0,0,0,0,1])}),$=P(function(t,e){return j(t,z([.33,.34,.33,0,0,.33,.34,.33,0,0,.33,.34,.33,0,0,0,0,0,1,0,0,0,0,0,1],e=O(e,0,1)))}),X=P(function(t,e){return j(t,z([.393,.769,.189,0,0,.349,.686,.168,0,0,.272,.534,.131,0,0,0,0,0,1,0,0,0,0,0,1],e=O(e,0,1)))}),Y=function(t,e,n,r){return L(t,(o=n,i=r,j([1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1],[O(e,0,2),0,0,0,0,0,o=O(o,0,2),0,0,0,0,0,i=O(i,0,2),0,0,0,0,0,1,0,0,0,0,0,1])));var o,i},K=H([0,-1,0,-1,5,-1,0,-1,0]),J=H([-2,-1,0,-1,1,1,0,1,2]),Z=S(function(t,e){return 255*Math.pow(t/255,1-e)}),Q=S(function(t,e){return 255*(1-Math.exp(-t/255*e))});function tt(t,e,n){var r=b(t),o=x(t),i=e/r,a=n/o,c=!1;(i<.5||2<i)&&(i=i<.5?.5:2,c=!0),(a<.5||2<a)&&(a=a<.5?.5:2,c=!0);var u,l,f,s=(u=t,l=i,f=a,new k(function(t){var e=b(u),n=x(u),r=Math.floor(e*l),o=Math.floor(n*f),i=p(r,o),a=y(i);a.drawImage(u,0,0,e,n,0,0,r,o),t(i)}));return c?s.then(function(t){return tt(t,e,n)}):s}function et(u,l){return u.toCanvas().then(function(t){return e=t,n=u.getType(),r=l,o=p(e.width,e.height),i=y(o),90!==(r=r<(c=a=0)?360+r:r)&&270!==r||w(o,o.height,o.width),90!==r&&180!==r||(a=o.width),270!==r&&180!==r||(c=o.height),i.translate(a,c),i.rotate(r*Math.PI/180),i.drawImage(e,0,0),E(o,n);var e,n,r,o,i,a,c})}function nt(a,c){return a.toCanvas().then(function(t){return e=t,n=a.getType(),r=c,o=p(e.width,e.height),i=y(o),"v"===r?(i.scale(1,-1),i.drawImage(e,0,-o.height)):(i.scale(-1,1),i.drawImage(e,-o.width,0)),E(o,n);var e,n,r,o,i})}function rt(a,c,u,l,f){return a.toCanvas().then(function(t){return e=t,n=a.getType(),r=c,o=u,y(i=p(l,f)).drawImage(e,-r,-o),E(i,n);var e,n,r,o,i})}var ot=function(t){return F(t)},it=function(t){return K(t)},at=function(t){return J(t)},ct=function(t,e){return Z(t,e)},ut=function(t,e){return Q(t,e)},lt=function(t,e,n,r){return Y(t,e,n,r)},ft=function(t,e){return V(t,e)},st=function(t,e){return W(t,e)},dt=function(t,e){return N(t,e)},ht=function(t,e){return q(t,e)},pt=function(t,e){return $(t,e)},mt=function(t,e){return X(t,e)},gt=function(t,e){return nt(t,e)},vt=function(t,e,n,r,o){return rt(t,e,n,r,o)},yt=function(t,e,n){return o=e,i=n,(r=t).toCanvas().then(function(t){return tt(t,o,i).then(function(t){return E(t,r.getType())})});var r,o,i},wt=function(t,e){return et(t,e)},bt=function(t){return A(t)},xt="undefined"!=typeof m.window?m.window:Function("return this;")(),kt=function(t,e){return function(t,e){for(var n=e!==undefined&&null!==e?e:xt,r=0;r<t.length&&n!==undefined&&null!==n;++r)n=n[t[r]];return n}(t.split("."),e)},Rt=function(t,e){var n=kt(t,e);if(n===undefined||null===n)throw new Error(t+" not available on this browser");return n},It=function(){return Rt("URL")},Mt={createObjectURL:function(t){return It().createObjectURL(t)},revokeObjectURL:function(t){It().revokeObjectURL(t)}},Tt=tinymce.util.Tools.resolve("tinymce.util.Delay"),Ut=tinymce.util.Tools.resolve("tinymce.util.Promise"),Ct=tinymce.util.Tools.resolve("tinymce.util.URI"),At=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),Et=tinymce.util.Tools.resolve("tinymce.ui.Factory"),Ot=tinymce.util.Tools.resolve("tinymce.geom.Rect"),_t=function(n){return new Ut(function(t){var e=function(){n.removeEventListener("load",e),t(n)};n.complete?t(n):n.addEventListener("load",e)})},jt=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),zt=tinymce.util.Tools.resolve("tinymce.util.Observable"),Lt=tinymce.util.Tools.resolve("tinymce.util.VK"),Bt=0,St={create:function(t){return new(Et.get("Control").extend({Defaults:{classes:"imagepanel"},selection:function(t){return arguments.length?(this.state.set("rect",t),this):this.state.get("rect")},imageSize:function(){var t=this.state.get("viewRect");return{w:t.w,h:t.h}},toggleCropRect:function(t){this.state.set("cropEnabled",t)},imageSrc:function(t){var o=this,i=new m.Image;i.src=t,_t(i).then(function(){var t,e,n=o.state.get("viewRect");if((e=o.$el.find("img"))[0])e.replaceWith(i);else{var r=m.document.createElement("div");r.className="mce-imagepanel-bg",o.getEl().appendChild(r),o.getEl().appendChild(i)}t={x:0,y:0,w:i.naturalWidth,h:i.naturalHeight},o.state.set("viewRect",t),o.state.set("rect",Ot.inflate(t,-20,-20)),n&&n.w===t.w&&n.h===t.h||o.zoomFit(),o.repaintImage(),o.fire("load")})},zoom:function(t){return arguments.length?(this.state.set("zoom",t),this):this.state.get("zoom")},postRender:function(){return this.imageSrc(this.settings.imageSrc),this._super()},zoomFit:function(){var t,e,n,r,o,i;t=this.$el.find("img"),e=this.getEl().clientWidth,n=this.getEl().clientHeight,r=t[0].naturalWidth,o=t[0].naturalHeight,1<=(i=Math.min((e-10)/r,(n-10)/o))&&(i=1),this.zoom(i)},repaintImage:function(){var t,e,n,r,o,i,a,c,u,l,f;f=this.getEl(),u=this.zoom(),l=this.state.get("rect"),a=this.$el.find("img"),c=this.$el.find(".mce-imagepanel-bg"),o=f.offsetWidth,i=f.offsetHeight,n=a[0].naturalWidth*u,r=a[0].naturalHeight*u,t=Math.max(0,o/2-n/2),e=Math.max(0,i/2-r/2),a.css({left:t,top:e,width:n,height:r}),c.css({left:t,top:e,width:n,height:r}),this.cropRect&&(this.cropRect.setRect({x:l.x*u+t,y:l.y*u+e,w:l.w*u,h:l.h*u}),this.cropRect.setClampRect({x:t,y:e,w:n,h:r}),this.cropRect.setViewPortRect({x:0,y:0,w:o,h:i}))},bindStates:function(){var r=this;function n(t){r.cropRect=function(l,n,f,r,o){var s,a,t,i,e="mce-",c=e+"crid-"+Bt++;function d(t,e){return{x:e.x-t.x,y:e.y-t.y,w:e.w,h:e.h}}function u(t,e,n,r){var o,i,a,c,u;o=e.x,i=e.y,a=e.w,c=e.h,o+=n*t.deltaX,i+=r*t.deltaY,(a+=n*t.deltaW)<20&&(a=20),(c+=r*t.deltaH)<20&&(c=20),u=l=Ot.clamp({x:o,y:i,w:a,h:c},f,"move"===t.name),u=d(f,u),s.fire("updateRect",{rect:u}),m(u)}function h(e){function t(t,e){e.h<0&&(e.h=0),e.w<0&&(e.w=0),jt("#"+c+"-"+t,r).css({left:e.x,top:e.y,width:e.w,height:e.h})}G.each(a,function(t){jt("#"+c+"-"+t.name,r).css({left:e.w*t.xMul+e.x,top:e.h*t.yMul+e.y})}),t("top",{x:n.x,y:n.y,w:n.w,h:e.y-n.y}),t("right",{x:e.x+e.w,y:e.y,w:n.w-e.x-e.w+n.x,h:e.h}),t("bottom",{x:n.x,y:e.y+e.h,w:n.w,h:n.h-e.y-e.h+n.y}),t("left",{x:n.x,y:e.y,w:e.x-n.x,h:e.h}),t("move",e)}function p(t){h(l=t)}function m(t){var e,n;p((e=f,{x:(n=t).x+e.x,y:n.y+e.y,w:n.w,h:n.h}))}return a=[{name:"move",xMul:0,yMul:0,deltaX:1,deltaY:1,deltaW:0,deltaH:0,label:"Crop Mask"},{name:"nw",xMul:0,yMul:0,deltaX:1,deltaY:1,deltaW:-1,deltaH:-1,label:"Top Left Crop Handle"},{name:"ne",xMul:1,yMul:0,deltaX:0,deltaY:1,deltaW:1,deltaH:-1,label:"Top Right Crop Handle"},{name:"sw",xMul:0,yMul:1,deltaX:1,deltaY:0,deltaW:-1,deltaH:1,label:"Bottom Left Crop Handle"},{name:"se",xMul:1,yMul:1,deltaX:0,deltaY:0,deltaW:1,deltaH:1,label:"Bottom Right Crop Handle"}],i=["top","right","bottom","left"],jt('<div id="'+c+'" class="'+e+'croprect-container" role="grid" aria-dropeffect="execute">').appendTo(r),G.each(i,function(t){jt("#"+c,r).append('<div id="'+c+"-"+t+'"class="'+e+'croprect-block" style="display: none" data-mce-bogus="all">')}),G.each(a,function(t){jt("#"+c,r).append('<div id="'+c+"-"+t.name+'" class="'+e+"croprect-handle "+e+"croprect-handle-"+t.name+'"style="display: none" data-mce-bogus="all" role="gridcell" tabindex="-1" aria-label="'+t.label+'" aria-grabbed="false">')}),t=G.map(a,function(e){var n;return new(Et.get("DragHelper"))(c,{document:r.ownerDocument,handle:c+"-"+e.name,start:function(){n=l},drag:function(t){u(e,n,t.deltaX,t.deltaY)}})}),h(l),jt(r).on("focusin focusout",function(t){jt(t.target).attr("aria-grabbed","focus"===t.type)}),jt(r).on("keydown",function(e){var i;function t(t,e,n,r,o){t.stopPropagation(),t.preventDefault(),u(i,n,r,o)}switch(G.each(a,function(t){if(e.target.id===c+"-"+t.name)return i=t,!1}),e.keyCode){case Lt.LEFT:t(e,0,l,-10,0);break;case Lt.RIGHT:t(e,0,l,10,0);break;case Lt.UP:t(e,0,l,0,-10);break;case Lt.DOWN:t(e,0,l,0,10);break;case Lt.ENTER:case Lt.SPACEBAR:e.preventDefault(),o()}}),s=G.extend({toggleVisibility:function(t){var e;e=G.map(a,function(t){return"#"+c+"-"+t.name}).concat(G.map(i,function(t){return"#"+c+"-"+t})).join(","),t?jt(e,r).show():jt(e,r).hide()},setClampRect:function(t){f=t,h(l)},setRect:p,getInnerRect:function(){return d(f,l)},setInnerRect:m,setViewPortRect:function(t){n=t,h(l)},destroy:function(){G.each(t,function(t){t.destroy()}),t=[]}},zt)}(t,r.state.get("viewRect"),r.state.get("viewRect"),r.getEl(),function(){r.fire("crop")}),r.cropRect.on("updateRect",function(t){var e=t.rect,n=r.zoom();e={x:Math.round(e.x/n),y:Math.round(e.y/n),w:Math.round(e.w/n),h:Math.round(e.h/n)},r.state.set("rect",e)}),r.on("remove",r.cropRect.destroy)}r.state.on("change:cropEnabled",function(t){r.cropRect.toggleVisibility(t.value),r.repaintImage()}),r.state.on("change:zoom",function(){r.repaintImage()}),r.state.on("change:rect",function(t){var e=t.value;r.cropRect||n(e),r.cropRect.setRect(e)})}}))(t)}};function Pt(t){return{blob:t,url:Mt.createObjectURL(t)}}function Ht(t){t&&Mt.revokeObjectURL(t.url)}function Dt(t){G.each(t,Ht)}function Ft(i,a,t,e){var c,n,r,u,o,l,f,s,d,h,p,m,g,v,y,w,b,x,k,R,I,M,T,U,C,A,E,O=function(){var n=[],r=-1;function t(){return 0<r}function e(){return-1!==r&&r<n.length-1}return{data:n,add:function(t){var e;return e=n.splice(++r),n.push(t),{state:t,removed:e}},undo:function(){if(t())return n[--r]},redo:function(){if(e())return n[++r]},canUndo:t,canRedo:e}}(),_=function(t){return i.rtl?t.reverse():t};function j(t){var e,n,r,o;e=c.find("#w")[0],n=c.find("#h")[0],r=parseInt(e.value(),10),o=parseInt(n.value(),10),c.find("#constrain")[0].checked()&&U&&C&&r&&o&&("w"===t.control.settings.name?(o=Math.round(r*A),n.value(o)):(r=Math.round(o*E),e.value(r))),U=r,C=o}function z(t){return Math.round(100*t)+"%"}function L(){c.find("#undo").disabled(!O.canUndo()),c.find("#redo").disabled(!O.canRedo()),c.statusbar.find("#save").disabled(!O.canUndo())}function B(){c.find("#undo").disabled(!0),c.find("#redo").disabled(!0)}function S(t){t&&s.imageSrc(t.url)}function P(e){return function(){var t=G.grep(T,function(t){return t.settings.name!==e});G.each(t,function(t){t.hide()}),e.show(),e.focus()}}function H(t){S(u=Pt(t))}function D(t){S(a=Pt(t)),Dt(O.add(a).removed),L()}function F(){var e=s.selection();bt(a.blob).then(function(t){vt(t,e.x,e.y,e.w,e.h).then($).then(function(t){D(t),W()})})}var V=function(e){var n=[].slice.call(arguments,1);return function(){bt((u||a).blob).then(function(t){e.apply(this,[t].concat(n)).then($).then(H)})}};function W(){S(a),Ht(u),P(n)(),L()}function N(){u?(D(u.blob),W()):function t(e,n){u?n():setTimeout(function(){0<e--?t(e,n):i.windowManager.alert("Error: failed to apply image operation.")},10)}(100,N)}function q(t){return Et.create("Form",{layout:"flex",direction:"row",labelGap:5,border:"0 0 1 0",align:"center",pack:"center",padding:"0 10 0 10",spacing:5,flex:0,minHeight:60,defaults:{classes:"imagetool",type:"button"},items:t})}var $=function(t){return t.toBlob()};function X(t,e){return q(_([{text:"Back",onclick:W},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",function(){B(),bt(a.blob).then(function(t){return e(t)}).then($).then(function(t){var e=Pt(t);S(e),Ht(u),u=e})})}function Y(t,n,e,r,o){return q(_([{text:"Back",onclick:W},{type:"spacer",flex:1},{type:"slider",flex:1,ondragend:function(t){var e;e=t.value,bt(a.blob).then(function(t){return n(t,e)}).then($).then(function(t){var e=Pt(t);S(e),Ht(u),u=e})},minValue:i.rtl?o:r,maxValue:i.rtl?r:o,value:e,previewFilter:z},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",function(){this.find("slider").value(e),B()})}o=q(_([{text:"Back",onclick:W},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:F}])).hide().on("show hide",function(t){s.toggleCropRect("show"===t.type)}).on("show",B),l=q(_([{text:"Back",onclick:W},{type:"spacer",flex:1},{type:"textbox",name:"w",label:"Width",size:4,onkeyup:j},{type:"textbox",name:"h",label:"Height",size:4,onkeyup:j},{type:"checkbox",name:"constrain",text:"Constrain proportions",checked:!0,onchange:function(t){!0===t.control.value()&&(A=C/U,E=U/C)}},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:"submit"}])).hide().on("submit",function(t){var e=parseInt(c.find("#w").value(),10),n=parseInt(c.find("#h").value(),10);t.preventDefault(),function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r=[].slice.call(arguments,1);return function(){bt(a.blob).then(function(t){e.apply(this,[t].concat(r)).then($).then(D)})}}(yt,e,n)(),W()}).on("show",B),f=q(_([{text:"Back",onclick:W},{type:"spacer",flex:1},{icon:"fliph",tooltip:"Flip horizontally",onclick:V(gt,"h")},{icon:"flipv",tooltip:"Flip vertically",onclick:V(gt,"v")},{icon:"rotateleft",tooltip:"Rotate counterclockwise",onclick:V(wt,-90)},{icon:"rotateright",tooltip:"Rotate clockwise",onclick:V(wt,90)},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",B),p=X(0,ot),k=X(0,it),R=X(0,at),m=Y(0,ft,0,-1,1),g=Y(0,st,180,0,360),v=Y(0,dt,0,-1,1),y=Y(0,ht,0,-1,1),w=Y(0,pt,0,0,1),b=Y(0,mt,0,0,1),x=function(t,o){function e(){var e,n,r;e=c.find("#r")[0].value(),n=c.find("#g")[0].value(),r=c.find("#b")[0].value(),bt(a.blob).then(function(t){return o(t,e,n,r)}).then($).then(function(t){var e=Pt(t);S(e),Ht(u),u=e})}var n=i.rtl?2:0,r=i.rtl?0:2;return q(_([{text:"Back",onclick:W},{type:"spacer",flex:1},{type:"slider",label:"R",name:"r",minValue:n,value:1,maxValue:r,ondragend:e,previewFilter:z},{type:"slider",label:"G",name:"g",minValue:n,value:1,maxValue:r,ondragend:e,previewFilter:z},{type:"slider",label:"B",name:"b",minValue:n,value:1,maxValue:r,ondragend:e,previewFilter:z},{type:"spacer",flex:1},{text:"Apply",subtype:"primary",onclick:N}])).hide().on("show",function(){c.find("#r,#g,#b").value(1),B()})}(0,lt),I=Y(0,ct,0,-1,1),M=Y(0,ut,1,0,2),r=q(_([{text:"Back",onclick:W},{type:"spacer",flex:1},{text:"hue",icon:"hue",onclick:P(g)},{text:"saturate",icon:"saturate",onclick:P(v)},{text:"sepia",icon:"sepia",onclick:P(b)},{text:"emboss",icon:"emboss",onclick:P(R)},{text:"exposure",icon:"exposure",onclick:P(M)},{type:"spacer",flex:1}])).hide(),n=q(_([{tooltip:"Crop",icon:"crop",onclick:P(o)},{tooltip:"Resize",icon:"resize2",onclick:P(l)},{tooltip:"Orientation",icon:"orientation",onclick:P(f)},{tooltip:"Brightness",icon:"sun",onclick:P(m)},{tooltip:"Sharpen",icon:"sharpen",onclick:P(k)},{tooltip:"Contrast",icon:"contrast",onclick:P(y)},{tooltip:"Color levels",icon:"drop",onclick:P(x)},{tooltip:"Gamma",icon:"gamma",onclick:P(I)},{tooltip:"Invert",icon:"invert",onclick:P(p)}])),s=St.create({flex:1,imageSrc:a.url}),d=Et.create("Container",{layout:"flex",direction:"column",pack:"start",border:"0 1 0 0",padding:5,spacing:5,items:[{type:"button",icon:"undo",tooltip:"Undo",name:"undo",onclick:function(){S(a=O.undo()),L()}},{type:"button",icon:"redo",tooltip:"Redo",name:"redo",onclick:function(){S(a=O.redo()),L()}},{type:"button",icon:"zoomin",tooltip:"Zoom in",onclick:function(){var t=s.zoom();t<2&&(t+=.1),s.zoom(t)}},{type:"button",icon:"zoomout",tooltip:"Zoom out",onclick:function(){var t=s.zoom();.1<t&&(t-=.1),s.zoom(t)}}]}),h=Et.create("Container",{type:"container",layout:"flex",direction:"row",align:"stretch",flex:1,items:_([d,s])}),T=[n,o,l,f,r,p,m,g,v,y,w,b,x,k,R,I,M],(c=i.windowManager.open({layout:"flex",direction:"column",align:"stretch",minWidth:Math.min(At.DOM.getViewPort().w,800),minHeight:Math.min(At.DOM.getViewPort().h,650),title:"Edit image",items:T.concat([h]),buttons:_([{text:"Save",name:"save",subtype:"primary",onclick:function(){t(a.blob),c.close()}},{text:"Cancel",onclick:"close"}])})).on("close",function(){e(),Dt(O.data),u=O=null}),O.add(a),L(),s.on("load",function(){U=s.imageSize().w,C=s.imageSize().h,A=C/U,E=U/C,c.find("#w").value(U),c.find("#h").value(C)}),s.on("crop",F)}var Vt,Wt={edit:function(r,t){return new Ut(function(e,n){return t.toBlob().then(function(t){Ft(r,Pt(t),e,n)})})}},Nt={getImageSize:function(t){var e,n;function r(t){return/^[0-9\.]+px$/.test(t)}return e=t.style.width,n=t.style.height,e||n?r(e)&&r(n)?{w:parseInt(e,10),h:parseInt(n,10)}:null:(e=t.width,n=t.height,e&&n?{w:parseInt(e,10),h:parseInt(n,10)}:null)},setImageSize:function(t,e){var n,r;e&&(n=t.style.width,r=t.style.height,(n||r)&&(t.style.width=e.w+"px",t.style.height=e.h+"px",t.removeAttribute("data-mce-style")),n=t.width,r=t.height,(n||r)&&(t.setAttribute("width",e.w),t.setAttribute("height",e.h)))},getNaturalImageSize:function(t){return{w:t.naturalWidth,h:t.naturalHeight}}},qt=(Vt="function",function(t){return function(t){if(null===t)return"null";var e=typeof t;return"object"===e&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":e}(t)===Vt}),$t=(Array.prototype.slice,function(t,e){for(var n=0,r=t.length;n<r;n++){var o=t[n];if(e(o,n))return g.some(o)}return g.none()});qt(Array.from)&&Array.from;var Xt=function(t){return null!==t&&t!==undefined},Yt=function(t,e){var n;return n=e.reduce(function(t,e){return Xt(t)?t[e]:undefined},t),Xt(n)?n:null},Gt=function(e){return new Ut(function(n){var t=new(Rt("FileReader"));t.onload=function(t){var e=t.target;n(e.result)},t.readAsText(e)})},Kt=function(e,r,o){return new Ut(function(t){var n;(n=new(Rt("XMLHttpRequest"))).onreadystatechange=function(){4===n.readyState&&t({status:n.status,blob:this.response})},n.open("GET",e,!0),n.withCredentials=o,G.each(r,function(t,e){n.setRequestHeader(e,t)}),n.responseType="blob",n.send()})},Jt=function(t){var e;try{e=JSON.parse(t)}catch(n){}return e},Zt=[{code:404,message:"Could not find Image Proxy"},{code:403,message:"Rejected request"},{code:0,message:"Incorrect Image Proxy URL"}],Qt=[{type:"key_missing",message:"The request did not include an api key."},{type:"key_not_found",message:"The provided api key could not be found."},{type:"domain_not_trusted",message:"The api key is not valid for the request origins."}],te=function(e){return"ImageProxy HTTP error: "+$t(Zt,function(t){return e===t.code}).fold(i("Unknown ImageProxy error"),function(t){return t.message})},ee=function(t){var e=te(t);return Ut.reject(e)},ne=function(e){return $t(Qt,function(t){return t.type===e}).fold(i("Unknown service error"),function(t){return t.message})},re=function(t,e){return Gt(e).then(function(t){var e,n,r=(e=Jt(t),"ImageProxy Service error: "+((n=Yt(e,["error","type"]))?ne(n):"Invalid JSON in service error message"));return Ut.reject(r)})},oe=function(t,e){return 400===(n=t)||403===n||500===n?re(0,e):ee(t);var n},ie=ee,ae=function(t,e){var n,r,o,i={"Content-Type":"application/json;charset=UTF-8","tiny-api-key":e};return Kt((n=t,r=e,o=-1===n.indexOf("?")?"?":"&",/[?&]apiKey=/.test(n)||!r?n:n+o+"apiKey="+encodeURIComponent(r)),i,!1).then(function(t){return t.status<200||300<=t.status?oe(t.status,t.blob):Ut.resolve(t.blob)})},ce=function(t,e,n){return e?ae(t,e):Kt(t,{},n).then(function(t){return t.status<200||300<=t.status?ie(t.status):Ut.resolve(t.blob)})},ue=0,le=function(t,e){t.notificationManager.open({text:e,type:"error"})},fe=function(t){return t.selection.getNode()},se=function(t,e){var n=e.src;return 0===n.indexOf("data:")||0===n.indexOf("blob:")||new Ct(n).host===t.documentBaseURI.host},de=function(t,e){return-1!==G.inArray(t.getParam("imagetools_cors_hosts",[],"string[]"),new Ct(e.src).host)},he=function(t,e){var n,r,o,i,a=e.src;return de(t,e)?ce(e.src,null,(r=t,o=e,-1!==G.inArray(r.getParam("imagetools_credentials_hosts",[],"string[]"),new Ct(o.src).host))):se(t,e)?R(e):(a=t.getParam("imagetools_proxy"),a+=(-1===a.indexOf("?")?"?":"&")+"url="+encodeURIComponent(e.src),n=(i=t).getParam("api_key",i.getParam("imagetools_api_key","","string"),"string"),ce(a,n,!1))},pe=function(t){var e;return(e=t.editorUpload.blobCache.getByUri(fe(t).src))?Ut.resolve(e.blob()):he(t,fe(t))},me=function(t){clearTimeout(t.get())},ge=function(u,l,f,s,d){return l.toBlob().then(function(t){var e,n,r,o,i,a,c;return r=u.editorUpload.blobCache,e=(i=fe(u)).src,u.getParam("images_reuse_filename",!1,"boolean")&&((o=r.getByUri(e))?(e=o.uri(),n=o.name()):(a=u,n=(c=e.match(/\/([^\/\?]+)?\.(?:jpeg|jpg|png|gif)(?:\?|$)/i))?a.dom.encode(c[1]):null)),o=r.create({id:"imagetools"+ue++,blob:t,base64:l.toBase64(),uri:e,name:n}),r.add(o),u.undoManager.transact(function(){u.$(i).on("load",function t(){var e,n,r;u.$(i).off("load",t),u.nodeChanged(),f?u.editorUpload.uploadImagesAuto():(me(s),e=u,n=s,r=Tt.setEditorTimeout(e,function(){e.editorUpload.uploadImagesAuto()},e.getParam("images_upload_timeout",3e4,"number")),n.set(r))}),d&&u.$(i).attr({width:d.w,height:d.h}),u.$(i).attr({src:o.blobUri()}).removeAttr("data-mce-src")}),o})},ve=function(e,n,t,r){return function(){return e._scanForImages().then(a(pe,e)).then(bt).then(t).then(function(t){return ge(e,t,!1,n,r)},function(t){le(e,t)})}},ye=function(n,r,o){return function(){var t=Nt.getImageSize(fe(n)),e=t?{w:t.h,h:t.w}:null;return ve(n,r,function(t){return wt(t,o)},e)()}},we=function(t,e,n){return function(){return ve(t,e,function(t){return gt(t,n)})()}},be=function(e,r){return function(){var o=fe(e),i=Nt.getNaturalImageSize(o),n=function(r){return new Ut(function(n){var t;(t=r,I(t)).then(function(t){var e=Nt.getNaturalImageSize(t);i.w===e.w&&i.h===e.h||Nt.getImageSize(o)&&Nt.setImageSize(o,e),Mt.revokeObjectURL(t.src),n(r)})})};pe(e).then(bt).then(a(function(e,t){return Wt.edit(e,t).then(n).then(bt).then(function(t){return ge(e,t,!0,r)},function(){})},e),function(t){le(e,t)})}},xe=function(t,e){return t.dom.is(e,"img:not([data-mce-object],[data-mce-placeholder])")&&(se(t,e)||de(t,e)||t.settings.imagetools_proxy)},ke=me,Re=function(n,t){G.each({mceImageRotateLeft:ye(n,t,-90),mceImageRotateRight:ye(n,t,90),mceImageFlipVertical:we(n,t,"v"),mceImageFlipHorizontal:we(n,t,"h"),mceEditImage:be(n,t)},function(t,e){n.addCommand(e,t)})},Ie=function(n,r,o){n.on("NodeChange",function(t){var e=o.get();e&&e.src!==t.element.src&&(ke(r),n.editorUpload.uploadImagesAuto(),o.set(null)),xe(n,t.element)&&o.set(t.element)})},Me=function(t){t.addButton("rotateleft",{title:"Rotate counterclockwise",cmd:"mceImageRotateLeft"}),t.addButton("rotateright",{title:"Rotate clockwise",cmd:"mceImageRotateRight"}),t.addButton("flipv",{title:"Flip vertically",cmd:"mceImageFlipVertical"}),t.addButton("fliph",{title:"Flip horizontally",cmd:"mceImageFlipHorizontal"}),t.addButton("editimage",{title:"Edit image",cmd:"mceEditImage"}),t.addButton("imageoptions",{title:"Image options",icon:"options",cmd:"mceImage"})},Te=function(t){t.addContextToolbar(a(xe,t),t.getParam("imagetools_toolbar","rotateleft rotateright | flipv fliph | crop editimage imageoptions"))};t.add("imagetools",function(t){var e=r(0),n=r(null);Re(t,e),Me(t),Te(t),Ie(t,e,n)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/lists/plugin.min.js b/lib/web/tiny_mce_4/plugins/lists/plugin.min.js index aee814e9e6bff..d92fc6df35bdf 100644 --- a/lib/web/tiny_mce_4/plugins/lists/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/lists/plugin.min.js @@ -1 +1 @@ -!function(u){"use strict";var e,n,t,r,o,i,a,s,c=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),d=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),l=tinymce.util.Tools.resolve("tinymce.util.VK"),p=tinymce.util.Tools.resolve("tinymce.dom.BookmarkManager"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),m=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),g=function(e){return e&&"BR"===e.nodeName},h=function(e){return e&&3===e.nodeType},y=function(e){return e&&/^(OL|UL|DL)$/.test(e.nodeName)},N=function(e){return e&&/^(OL|UL)$/.test(e.nodeName)},S=function(e){return e&&/^(DT|DD)$/.test(e.nodeName)},C=function(e){return e&&/^(LI|DT|DD)$/.test(e.nodeName)},O=function(e){return e&&/^(TH|TD)$/.test(e.nodeName)},b=g,T=function(e,n){return n&&!!e.schema.getTextBlockElements()[n.nodeName]},D=function(e,n){return e&&e.nodeName in n},L=function(e,n){return!!g(n)&&!(!e.isBlock(n.nextSibling)||g(n.previousSibling))},E=function(e,n,t){var r=e.isEmpty(n);return!(t&&0<e.select("span[data-mce-type=bookmark]",n).length)&&r},w=function(e,n){return e.isChildOf(n,e.getRoot())},k=function(e,n){if(h(e))return{container:e,offset:n};var t=f.getNode(e,n);return h(t)?{container:t,offset:n>=e.childNodes.length?t.data.length:0}:t.previousSibling&&h(t.previousSibling)?{container:t.previousSibling,offset:t.previousSibling.data.length}:t.nextSibling&&h(t.nextSibling)?{container:t.nextSibling,offset:0}:{container:e,offset:n}},A=function(e){var n=e.cloneRange(),t=k(e.startContainer,e.startOffset);n.setStart(t.container,t.offset);var r=k(e.endContainer,e.endOffset);return n.setEnd(r.container,r.offset),n},x=m.DOM,R=function(o){var i={},e=function(e){var n,t,r;t=o[e?"startContainer":"endContainer"],r=o[e?"startOffset":"endOffset"],1===t.nodeType&&(n=x.create("span",{"data-mce-type":"bookmark"}),t.hasChildNodes()?(r=Math.min(r,t.childNodes.length-1),e?t.insertBefore(n,t.childNodes[r]):x.insertAfter(n,t.childNodes[r])):t.appendChild(n),t=n,r=0),i[e?"startContainer":"endContainer"]=t,i[e?"startOffset":"endOffset"]=r};return e(!0),o.collapsed||e(),i},I=function(o){function e(e){var n,t,r;n=r=o[e?"startContainer":"endContainer"],t=o[e?"startOffset":"endOffset"],n&&(1===n.nodeType&&(t=function(e){for(var n=e.parentNode.firstChild,t=0;n;){if(n===e)return t;1===n.nodeType&&"bookmark"===n.getAttribute("data-mce-type")||t++,n=n.nextSibling}return-1}(n),n=n.parentNode,x.remove(r),!n.hasChildNodes()&&x.isBlock(n)&&n.appendChild(x.create("br"))),o[e?"startContainer":"endContainer"]=n,o[e?"startOffset":"endOffset"]=t)}e(!0),e();var n=x.createRng();return n.setStart(o.startContainer,o.startOffset),o.endContainer&&n.setEnd(o.endContainer,o.endOffset),A(n)},_=function(e){return function(){return e}},B=function(t){return function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return!t.apply(null,e)}},P=_(!1),M=_(!0),U=P,F=M,j=function(){return H},H=(r={fold:function(e,n){return e()},is:U,isSome:U,isNone:F,getOr:t=function(e){return e},getOrThunk:n=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:t,orThunk:n,map:j,ap:j,each:function(){},bind:j,flatten:j,exists:U,forall:F,filter:j,equals:e=function(e){return e.isNone()},equals_:e,toArray:function(){return[]},toString:_("none()")},Object.freeze&&Object.freeze(r),r),$=function(t){var e=function(){return t},n=function(){return o},r=function(e){return e(t)},o={fold:function(e,n){return n(t)},is:function(e){return t===e},isSome:F,isNone:U,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:n,orThunk:n,map:function(e){return $(e(t))},ap:function(e){return e.fold(j,function(e){return $(e(t))})},each:function(e){e(t)},bind:r,flatten:e,exists:r,forall:r,filter:function(e){return e(t)?o:H},equals:function(e){return e.is(t)},equals_:function(e,n){return e.fold(U,function(e){return n(t,e)})},toArray:function(){return[t]},toString:function(){return"some("+t+")"}};return o},q={some:$,none:j,from:function(e){return null===e||e===undefined?H:$(e)}},W=function(n){return function(e){return function(e){if(null===e)return"null";var n=typeof e;return"object"===n&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===n&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":n}(e)===n}},V=W("string"),z=W("boolean"),K=W("function"),X=W("number"),Q=Array.prototype.slice,Y=function(e,n){for(var t=e.length,r=new Array(t),o=0;o<t;o++){var i=e[o];r[o]=n(i,o,e)}return r},G=function(e,n){for(var t=0,r=e.length;t<r;t++)n(e[t],t,e)},J=function(e,n){for(var t=[],r=0,o=e.length;r<o;r++){var i=e[r];n(i,r,e)&&t.push(i)}return t},Z=function(e,n,t){return G(e,function(e){t=n(t,e)}),t},ee=function(e,n){for(var t=0,r=e.length;t<r;t++){var o=e[t];if(n(o,t,e))return q.some(o)}return q.none()},ne=Array.prototype.push,te=function(e,n){return function(e){for(var n=[],t=0,r=e.length;t<r;++t){if(!Array.prototype.isPrototypeOf(e[t]))throw new Error("Arr.flatten item "+t+" was not an array, input: "+e);ne.apply(n,e[t])}return n}(Y(e,n))},re=function(e){return 0===e.length?q.none():q.some(e[0])},oe=function(e){return 0===e.length?q.none():q.some(e[e.length-1])},ie=(K(Array.from)&&Array.from,"undefined"!=typeof u.window?u.window:Function("return this;")()),ue=function(e,n){return function(e,n){for(var t=n!==undefined&&null!==n?n:ie,r=0;r<e.length&&t!==undefined&&null!==t;++r)t=t[e[r]];return t}(e.split("."),n)},ae=function(e,n){var t=ue(e,n);if(t===undefined||null===t)throw e+" not available on this browser";return t},se=function(e){var n,t=ue("ownerDocument.defaultView",e);return(n=t,ae("HTMLElement",n)).prototype.isPrototypeOf(e)},ce=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),fe=function(e){var n=e.selection.getStart(!0);return e.dom.getParent(n,"OL,UL,DL",le(e,n))},de=function(e){var t,n,r,o=e.selection.getSelectedBlocks();return v.grep((t=e,n=o,r=v.map(n,function(e){var n=t.dom.getParent(e,"li,dd,dt",le(t,e));return n||e}),ce.unique(r)),function(e){return C(e)})},le=function(e,n){var t=e.dom.getParents(n,"TD,TH");return 0<t.length?t[0]:e.getBody()},me=function(e,n){var t=e.dom.getParents(n,"ol,ul",le(e,n));return oe(t)},ge=function(n,e){var t=Y(e,function(e){return me(n,e).getOr(e)});return ce.unique(t)},pe={isList:function(e){var n=fe(e);return se(n)},getParentList:fe,getSelectedSubLists:function(e){var n,t,r,o=fe(e),i=e.selection.getSelectedBlocks();return r=i,(t=o)&&1===r.length&&r[0]===t?(n=o,v.grep(n.querySelectorAll("ol,ul,dl"),function(e){return y(e)})):v.grep(i,function(e){return y(e)&&o!==e})},getSelectedListItems:de,getClosestListRootElm:le,getSelectedDlItems:function(e){return J(de(e),S)},getSelectedListRoots:function(e){var n,t,r,o=(t=me(n=e,n.selection.getStart()),r=J(n.selection.getSelectedBlocks(),N),t.toArray().concat(r));return ge(e,o)}},ve=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:_(e)}},he={fromHtml:function(e,n){var t=(n||u.document).createElement("div");if(t.innerHTML=e,!t.hasChildNodes()||1<t.childNodes.length)throw u.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return ve(t.childNodes[0])},fromTag:function(e,n){var t=(n||u.document).createElement(e);return ve(t)},fromText:function(e,n){var t=(n||u.document).createTextNode(e);return ve(t)},fromDom:ve,fromPoint:function(e,n,t){var r=e.dom();return q.from(r.elementFromPoint(n,t)).map(ve)}},ye=function(e,n){for(var t=[],r=0;r<e.length;r++){var o=e[r];if(!o.isSome())return q.none();t.push(o.getOrDie())}return q.some(n.apply(null,t))},Ne=Object.keys,Se=function(){return ae("Node")},Ce=function(e,n,t){return 0!=(e.compareDocumentPosition(n)&t)},Oe=function(e,n){return Ce(e,n,Se().DOCUMENT_POSITION_CONTAINED_BY)},be=function(e,n){var t=function(e,n){for(var t=0;t<e.length;t++){var r=e[t];if(r.test(n))return r}return undefined}(e,n);if(!t)return{major:0,minor:0};var r=function(e){return Number(n.replace(t,"$"+e))};return De(r(1),r(2))},Te=function(){return De(0,0)},De=function(e,n){return{major:e,minor:n}},Le={nu:De,detect:function(e,n){var t=String(n).toLowerCase();return 0===e.length?Te():be(e,t)},unknown:Te},Ee="Firefox",we=function(e,n){return function(){return n===e}},ke=function(e){var n=e.current;return{current:n,version:e.version,isEdge:we("Edge",n),isChrome:we("Chrome",n),isIE:we("IE",n),isOpera:we("Opera",n),isFirefox:we(Ee,n),isSafari:we("Safari",n)}},Ae={unknown:function(){return ke({current:undefined,version:Le.unknown()})},nu:ke,edge:_("Edge"),chrome:_("Chrome"),ie:_("IE"),opera:_("Opera"),firefox:_(Ee),safari:_("Safari")},xe="Windows",Re="Android",Ie="Solaris",_e="FreeBSD",Be=function(e,n){return function(){return n===e}},Pe=function(e){var n=e.current;return{current:n,version:e.version,isWindows:Be(xe,n),isiOS:Be("iOS",n),isAndroid:Be(Re,n),isOSX:Be("OSX",n),isLinux:Be("Linux",n),isSolaris:Be(Ie,n),isFreeBSD:Be(_e,n)}},Me={unknown:function(){return Pe({current:undefined,version:Le.unknown()})},nu:Pe,windows:_(xe),ios:_("iOS"),android:_(Re),linux:_("Linux"),osx:_("OSX"),solaris:_(Ie),freebsd:_(_e)},Ue=function(e,n){var t=String(n).toLowerCase();return ee(e,function(e){return e.search(t)})},Fe=function(e,t){return Ue(e,t).map(function(e){var n=Le.detect(e.versionRegexes,t);return{current:e.name,version:n}})},je=function(e,t){return Ue(e,t).map(function(e){var n=Le.detect(e.versionRegexes,t);return{current:e.name,version:n}})},He=function(e,n){return-1!==e.indexOf(n)},$e=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,qe=function(n){return function(e){return He(e,n)}},We=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return He(e,"edge/")&&He(e,"chrome")&&He(e,"safari")&&He(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,$e],search:function(e){return He(e,"chrome")&&!He(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return He(e,"msie")||He(e,"trident")}},{name:"Opera",versionRegexes:[$e,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:qe("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:qe("firefox")},{name:"Safari",versionRegexes:[$e,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return(He(e,"safari")||He(e,"mobile/"))&&He(e,"applewebkit")}}],Ve=[{name:"Windows",search:qe("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return He(e,"iphone")||He(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:qe("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:qe("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:qe("linux"),versionRegexes:[]},{name:"Solaris",search:qe("sunos"),versionRegexes:[]},{name:"FreeBSD",search:qe("freebsd"),versionRegexes:[]}],ze={browsers:_(We),oses:_(Ve)},Ke=function(e){var n,t,r,o,i,u,a,s,c,f,d,l=ze.browsers(),m=ze.oses(),g=Fe(l,e).fold(Ae.unknown,Ae.nu),p=je(m,e).fold(Me.unknown,Me.nu);return{browser:g,os:p,deviceType:(t=g,r=e,o=(n=p).isiOS()&&!0===/ipad/i.test(r),i=n.isiOS()&&!o,u=n.isAndroid()&&3===n.version.major,a=n.isAndroid()&&4===n.version.major,s=o||u||a&&!0===/mobile/i.test(r),c=n.isiOS()||n.isAndroid(),f=c&&!s,d=t.isSafari()&&n.isiOS()&&!1===/safari/i.test(r),{isiPad:_(o),isiPhone:_(i),isTablet:_(s),isPhone:_(f),isTouch:_(c),isAndroid:n.isAndroid,isiOS:n.isiOS,isWebView:_(d)})}},Xe={detect:(o=function(){var e=u.navigator.userAgent;return Ke(e)},a=!1,function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return a||(a=!0,i=o.apply(null,e)),i})},Qe=(u.Node.ATTRIBUTE_NODE,u.Node.CDATA_SECTION_NODE,u.Node.COMMENT_NODE,u.Node.DOCUMENT_NODE,u.Node.DOCUMENT_TYPE_NODE,u.Node.DOCUMENT_FRAGMENT_NODE,u.Node.ELEMENT_NODE),Ye=(u.Node.TEXT_NODE,u.Node.PROCESSING_INSTRUCTION_NODE,u.Node.ENTITY_REFERENCE_NODE,u.Node.ENTITY_NODE,u.Node.NOTATION_NODE,Qe),Ge=function(e,n){return e.dom()===n.dom()},Je=Xe.detect().browser.isIE()?function(e,n){return Oe(e.dom(),n.dom())}:function(e,n){var t=e.dom(),r=n.dom();return t!==r&&t.contains(r)},Ze=function(e,n){var t=e.dom();if(t.nodeType!==Ye)return!1;if(t.matches!==undefined)return t.matches(n);if(t.msMatchesSelector!==undefined)return t.msMatchesSelector(n);if(t.webkitMatchesSelector!==undefined)return t.webkitMatchesSelector(n);if(t.mozMatchesSelector!==undefined)return t.mozMatchesSelector(n);throw new Error("Browser lacks native selectors")},en=function(e){var n=e.dom();return q.from(n.parentNode).map(he.fromDom)},nn=function(e){var n=e.dom();return Y(n.childNodes,he.fromDom)},tn=function(e,n){var t=e.dom().childNodes;return q.from(t[n]).map(he.fromDom)},rn=function(e){return tn(e,0)},on=function(e){return tn(e,e.dom().childNodes.length-1)},un=(function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n]}("element","offset"),function(n,t){en(n).each(function(e){e.dom().insertBefore(t.dom(),n.dom())})}),an=function(e,n){e.dom().appendChild(n.dom())},sn=function(n,e){G(e,function(e){an(n,e)})},cn=function(e){var n=e.dom();null!==n.parentNode&&n.parentNode.removeChild(n)},fn=function(e){return e.dom().nodeName.toLowerCase()},dn=function(e,n){var t=e.dom();!function(e,n){for(var t=Ne(e),r=0,o=t.length;r<o;r++){var i=t[r];n(e[i],i,e)}}(n,function(e,n){!function(e,n,t){if(!(V(t)||z(t)||X(t)))throw u.console.error("Invalid call to Attr.set. Key ",n,":: Value ",t,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(n,t+"")}(t,n,e)})},ln=function(e){return Z(e.dom().attributes,function(e,n){return e[n.name]=n.value,e},{})},mn=function(e,n,t){if(!V(t))throw u.console.error("Invalid call to CSS.set. Property ",n,":: Value ",t,":: Element ",e),new Error("CSS value must be a string: "+t);var r;(r=e).style!==undefined&&K(r.style.getPropertyValue)&&e.style.setProperty(n,t)},gn=function(e){return n=e,t=!0,he.fromDom(n.dom().cloneNode(t));var n,t},pn=function(e,n){var t,r,o,i,u=(t=e,r=n,o=he.fromTag(r),i=ln(t),dn(o,i),o);un(e,u);var a=nn(e);return sn(u,a),cn(e),u},vn=function(e,n){an(e.item,n.list)},hn=function(f,e,d){var n=e.slice(0,d.depth);return oe(n).each(function(e){var n,t,r,o,i,u,a,s,c=(n=f,t=d.itemAttributes,r=d.content,o=he.fromTag("li",n),dn(o,t),sn(o,r),o);u=c,an((i=e).list,u),i.item=u,s=d,fn((a=e).list)!==s.listType&&(a.list=pn(a.list,s.listType)),dn(a.list,s.listAttributes)}),n},yn=function(e,n,t){var r,o=function(e,n,t){for(var r,o,i,u=[],a=0;a<t;a++)u.push((r=e,o=n.listType,i={list:he.fromTag(o,r),item:he.fromTag("li",r)},an(i.list,i.item),i));return u}(e,t,t.depth-n.length);return function(e){for(var n=1;n<e.length;n++)vn(e[n-1],e[n])}(o),function(e,n){for(var t=0;t<e.length-1;t++)r=e[t].item,o="list-style-type",i="none",u=r.dom(),mn(u,o,i);var r,o,i,u;oe(e).each(function(e){dn(e.list,n.listAttributes),dn(e.item,n.itemAttributes),sn(e.item,n.content)})}(o,t),r=o,ye([oe(n),re(r)],vn),n.concat(o)},Nn=function(e){return Ze(e,"OL,UL")},Sn=function(e){return rn(e).map(Nn).getOr(!1)},Cn=function(e){return 0<e.depth},On=function(e){return e.isSelected},bn=function(e){var n=nn(e),t=on(e).map(Nn).getOr(!1)?n.slice(0,-1):n;return Y(t,gn)},Tn=Object.prototype.hasOwnProperty,Dn=(s=function(e,n){return n},function(){for(var e=new Array(arguments.length),n=0;n<e.length;n++)e[n]=arguments[n];if(0===e.length)throw new Error("Can't merge zero objects");for(var t={},r=0;r<e.length;r++){var o=e[r];for(var i in o)Tn.call(o,i)&&(t[i]=s(t[i],o[i]))}return t}),Ln=function(n){G(n,function(r,e){(function(e,n){for(var t=e[n].depth,r=n-1;0<=r;r--){if(e[r].depth===t)return q.some(e[r]);if(e[r].depth<t)break}return q.none()})(n,e).each(function(e){var n,t;t=e,(n=r).listType=t.listType,n.listAttributes=Dn({},t.listAttributes)})})},En=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return En(t())}}},wn=function(i,u,a,s){return rn(s).filter(Nn).fold(function(){u.each(function(e){Ge(e.start,s)&&a.set(!0)});var n,t,r,e=(n=s,t=i,r=a.get(),en(n).map(function(e){return{depth:t,isSelected:r,content:bn(n),itemAttributes:ln(n),listAttributes:ln(e),listType:fn(e)}}));u.each(function(e){Ge(e.end,s)&&a.set(!1)});var o=on(s).filter(Nn).map(function(e){return kn(i,u,a,e)}).getOr([]);return e.toArray().concat(o)},function(e){return kn(i,u,a,e)})},kn=function(n,t,r,e){return te(nn(e),function(e){return(Nn(e)?kn:wn)(n+1,t,r,e)})},An=tinymce.util.Tools.resolve("tinymce.Env"),xn=function(e,n){var t,r,o,i,u=e.dom,a=e.schema.getBlockElements(),s=u.createFragment();if(e.settings.forced_root_block&&(o=e.settings.forced_root_block),o&&((r=u.create(o)).tagName===e.settings.forced_root_block&&u.setAttribs(r,e.settings.forced_root_block_attrs),D(n.firstChild,a)||s.appendChild(r)),n)for(;t=n.firstChild;){var c=t.nodeName;i||"SPAN"===c&&"bookmark"===t.getAttribute("data-mce-type")||(i=!0),D(t,a)?(s.appendChild(t),r=null):o?(r||(r=u.create(o),s.appendChild(r)),r.appendChild(t)):s.appendChild(t)}return e.settings.forced_root_block?i||An.ie&&!(10<An.ie)||r.appendChild(u.create("br",{"data-mce-bogus":"1"})):s.appendChild(u.create("br")),s},Rn=function(i,e){return Y(e,function(e){var n,t,r,o=(n=e.content,r=(t||u.document).createDocumentFragment(),G(n,function(e){r.appendChild(e.dom())}),he.fromDom(r));return he.fromDom(xn(i,o.dom()))})},In=function(e,n){return Ln(n),(t=e.contentDocument,r=n,o=Z(r,function(e,n){return n.depth>e.length?yn(t,e,n):hn(t,e,n)},[]),re(o).map(function(e){return e.list})).toArray();var t,r,o},_n=function(e){var n,t,r=Y(pe.getSelectedListItems(e),he.fromDom);return ye([ee(r,B(Sn)),ee((n=r,t=Q.call(n,0),t.reverse(),t),B(Sn))],function(e,n){return{start:e,end:n}})},Bn=function(a,e,s){var n,t,r,o=(n=e,t=_n(a),r=En(!1),Y(n,function(e){return{sourceList:e,entries:kn(0,t,r,e)}}));G(o,function(e){var n,t,r,o,i,u;n=e.entries,t=s,G(J(n,On),function(e){return function(e,n){switch(e){case"Indent":n.depth++;break;case"Outdent":n.depth--;break;case"Flatten":n.depth=0}}(t,e)}),r=e.sourceList,i=a,u=e.entries,o=te(function(e,n){if(0===e.length)return[];for(var t=n(e[0]),r=[],o=[],i=0,u=e.length;i<u;i++){var a=e[i],s=n(a);s!==t&&(r.push(o),o=[]),t=s,o.push(a)}return 0!==o.length&&r.push(o),r}(u,Cn),function(e){return re(e).map(Cn).getOr(!1)?In(i,e):Rn(i,e)}),G(o,function(e){un(r,e)}),cn(e.sourceList)})},Pn=m.DOM,Mn=function(e,n,t){var r,o,i,u,a,s;for(i=Pn.select('span[data-mce-type="bookmark"]',n),a=xn(e,t),(r=Pn.createRng()).setStartAfter(t),r.setEndAfter(n),u=(o=r.extractContents()).firstChild;u;u=u.firstChild)if("LI"===u.nodeName&&e.dom.isEmpty(u)){Pn.remove(u);break}e.dom.isEmpty(o)||Pn.insertAfter(o,n),Pn.insertAfter(a,n),E(e.dom,t.parentNode)&&(s=t.parentNode,v.each(i,function(e){s.parentNode.insertBefore(e,t.parentNode)}),Pn.remove(s)),Pn.remove(t),E(e.dom,n)&&Pn.remove(n)},Un=function(e){Ze(e,"DT")&&pn(e,"DD")},Fn=function(r,e,n){G(n,"Indent"===e?Un:function(e){return n=r,void(Ze(t=e,"DD")?pn(t,"DT"):Ze(t,"DT")&&en(t).each(function(e){return Mn(n,e.dom(),t.dom())}));var n,t})},jn=function(e,n){var t=Y(pe.getSelectedListRoots(e),he.fromDom),r=Y(pe.getSelectedDlItems(e),he.fromDom),o=!1;if(t.length||r.length){var i=e.selection.getBookmark();Bn(e,t,n),Fn(e,n,r),e.selection.moveToBookmark(i),e.selection.setRng(A(e.selection.getRng())),e.nodeChanged(),o=!0}return o},Hn=function(e){return jn(e,"Indent")},$n=function(e){return jn(e,"Outdent")},qn=function(e){return jn(e,"Flatten")},Wn=function(t,e){v.each(e,function(e,n){t.setAttribute(n,e)})},Vn=function(e,n,t){var r,o,i,u,a,s,c;r=e,o=n,u=(i=t)["list-style-type"]?i["list-style-type"]:null,r.setStyle(o,"list-style-type",u),a=e,Wn(s=n,(c=t)["list-attributes"]),v.each(a.select("li",s),function(e){Wn(e,c["list-item-attributes"])})},zn=function(e,n,t,r){var o,i;for(o=n[t?"startContainer":"endContainer"],i=n[t?"startOffset":"endOffset"],1===o.nodeType&&(o=o.childNodes[Math.min(i,o.childNodes.length-1)]||o),!t&&b(o.nextSibling)&&(o=o.nextSibling);o.parentNode!==r;){if(T(e,o))return o;if(/^(TD|TH)$/.test(o.parentNode.nodeName))return o;o=o.parentNode}return o},Kn=function(f,d,l){void 0===l&&(l={});var e,n=f.selection.getRng(!0),m="LI",t=pe.getClosestListRootElm(f,f.selection.getStart(!0)),g=f.dom;"false"!==g.getContentEditable(f.selection.getNode())&&("DL"===(d=d.toUpperCase())&&(m="DT"),e=R(n),v.each(function(t,e,r){for(var o,i=[],u=t.dom,n=zn(t,e,!0,r),a=zn(t,e,!1,r),s=[],c=n;c&&(s.push(c),c!==a);c=c.nextSibling);return v.each(s,function(e){if(T(t,e))return i.push(e),void(o=null);if(u.isBlock(e)||b(e))return b(e)&&u.remove(e),void(o=null);var n=e.nextSibling;p.isBookmarkNode(e)&&(T(t,n)||!n&&e.parentNode===r)?o=null:(o||(o=u.create("p"),e.parentNode.insertBefore(o,e),i.push(o)),o.appendChild(e))}),i}(f,n,t),function(e){var n,t,r,o,i,u,a,s,c;(t=e.previousSibling)&&y(t)&&t.nodeName===d&&(r=t,o=l,i=g.getStyle(r,"list-style-type"),u=o?o["list-style-type"]:"",i===(u=null===u?"":u))?(n=t,e=g.rename(e,m),t.appendChild(e)):(n=g.create(d),e.parentNode.insertBefore(n,e),n.appendChild(e),e=g.rename(e,m)),a=g,s=e,c=["margin","margin-right","margin-bottom","margin-left","margin-top","padding","padding-right","padding-bottom","padding-left","padding-top"],v.each(c,function(e){var n;return a.setStyle(s,((n={})[e]="",n))}),Vn(g,n,l),Qn(f.dom,n)}),f.selection.setRng(I(e)))},Xn=function(e,n,t){return s=t,(a=n)&&s&&y(a)&&a.nodeName===s.nodeName&&(i=n,u=t,(o=e).getStyle(i,"list-style-type",!0)===o.getStyle(u,"list-style-type",!0))&&(r=t,n.className===r.className);var r,o,i,u,a,s},Qn=function(e,n){var t,r;if(t=n.nextSibling,Xn(e,n,t)){for(;r=t.firstChild;)n.appendChild(r);e.remove(t)}if(t=n.previousSibling,Xn(e,n,t)){for(;r=t.lastChild;)n.insertBefore(r,n.firstChild);e.remove(t)}},Yn=function(n,e,t,r,o){if(e.nodeName!==r||Gn(o)){var i=R(n.selection.getRng(!0));v.each([e].concat(t),function(e){!function(e,n,t,r){if(n.nodeName!==t){var o=e.rename(n,t);Vn(e,o,r)}else Vn(e,n,r)}(n.dom,e,r,o)}),n.selection.setRng(I(i))}else qn(n)},Gn=function(e){return"list-style-type"in e},Jn={toggleList:function(e,n,t){var r=pe.getParentList(e),o=pe.getSelectedSubLists(e);t=t||{},r&&0<o.length?Yn(e,r,o,n,t):function(e,n,t,r){if(n!==e.getBody())if(n)if(n.nodeName!==t||Gn(r)){var o=R(e.selection.getRng(!0));Vn(e.dom,n,r),Qn(e.dom,e.dom.rename(n,t)),e.selection.setRng(I(o))}else qn(e);else Kn(e,t,r)}(e,r,n,t)},mergeWithAdjacentLists:Qn},Zn=m.DOM,et=function(e,n){var t,r=n.parentNode;"LI"===r.nodeName&&r.firstChild===n&&((t=r.previousSibling)&&"LI"===t.nodeName?(t.appendChild(n),E(e,r)&&Zn.remove(r)):Zn.setStyle(r,"listStyleType","none")),y(r)&&(t=r.previousSibling)&&"LI"===t.nodeName&&t.appendChild(n)},nt=function(n,e){v.each(v.grep(n.select("ol,ul",e)),function(e){et(n,e)})},tt=function(e,n,t,r){var o,i,u=n.startContainer,a=n.startOffset;if(3===u.nodeType&&(t?a<u.data.length:0<a))return u;for(o=e.schema.getNonEmptyElements(),1===u.nodeType&&(u=f.getNode(u,a)),i=new d(u,r),t&&L(e.dom,u)&&i.next();u=i[t?"next":"prev2"]();){if("LI"===u.nodeName&&!u.hasChildNodes())return u;if(o[u.nodeName])return u;if(3===u.nodeType&&0<u.data.length)return u}},rt=function(e,n){var t=n.childNodes;return 1===t.length&&!y(t[0])&&e.isBlock(t[0])},ot=function(e,n,t){var r,o,i,u;if(o=rt(e,t)?t.firstChild:t,rt(i=e,u=n)&&i.remove(u.firstChild,!0),!E(e,n,!0))for(;r=n.firstChild;)o.appendChild(r)},it=function(n,e,t){var r,o,i=e.parentNode;if(w(n,e)&&w(n,t)){y(t.lastChild)&&(o=t.lastChild),i===t.lastChild&&b(i.previousSibling)&&n.remove(i.previousSibling),(r=t.lastChild)&&b(r)&&e.hasChildNodes()&&n.remove(r),E(n,t,!0)&&n.$(t).empty(),ot(n,e,t),o&&t.appendChild(o);var u=Je(he.fromDom(t),he.fromDom(e))?n.getParents(e,y,t):[];n.remove(e),G(u,function(e){E(n,e)&&e!==n.getRoot()&&n.remove(e)})}},ut=function(e,n,t,r){var o,i,u,a=e.dom;if(a.isEmpty(r))i=t,u=r,(o=e).dom.$(u).empty(),it(o.dom,i,u),o.selection.setCursorLocation(u);else{var s=R(n);it(a,t,r),e.selection.setRng(I(s))}},at=function(e,n){var t,r,o,i=e.dom,u=e.selection,a=u.getStart(),s=pe.getClosestListRootElm(e,a),c=i.getParent(u.getStart(),"LI",s);if(c){if((t=c.parentNode)===e.getBody()&&E(i,t))return!0;if(r=A(u.getRng(!0)),(o=i.getParent(tt(e,r,n,s),"LI",s))&&o!==c)return n?ut(e,r,o,c):function(e,n,t,r){var o=R(n);it(e.dom,t,r);var i=I(o);e.selection.setRng(i)}(e,r,c,o),!0;if(!o&&!n)return qn(e),!0}return!1},st=function(e,n){return at(e,n)||function(o,i){var u=o.dom,e=o.selection.getStart(),a=pe.getClosestListRootElm(o,e),s=u.getParent(e,u.isBlock,a);if(s&&u.isEmpty(s)){var n=A(o.selection.getRng(!0)),c=u.getParent(tt(o,n,i,a),"LI",a);if(c)return o.undoManager.transact(function(){var e,n,t,r;n=s,t=a,r=(e=u).getParent(n.parentNode,e.isBlock,t),e.remove(n),r&&e.isEmpty(r)&&e.remove(r),Jn.mergeWithAdjacentLists(u,c.parentNode),o.selection.select(c,!0),o.selection.collapse(i)}),!0}return!1}(e,n)},ct=function(e,n){return e.selection.isCollapsed()?st(e,n):(r=(t=e).selection.getStart(),o=pe.getClosestListRootElm(t,r),!!(t.dom.getParent(r,"LI,DT,DD",o)||0<pe.getSelectedListItems(t).length)&&(t.undoManager.transact(function(){t.execCommand("Delete"),nt(t.dom,t.getBody())}),!0));var t,r,o},ft=function(n){n.on("keydown",function(e){e.keyCode===l.BACKSPACE?ct(n,!1)&&e.preventDefault():e.keyCode===l.DELETE&&ct(n,!0)&&e.preventDefault()})},dt=ct,lt=function(n){return{backspaceDelete:function(e){dt(n,e)}}},mt=function(n,t){return function(){var e=n.dom.getParent(n.selection.getStart(),"UL,OL,DL");return e&&e.nodeName===t}},gt=function(t){t.on("BeforeExecCommand",function(e){var n=e.command.toLowerCase();"indent"===n?Hn(t):"outdent"===n&&$n(t)}),t.addCommand("InsertUnorderedList",function(e,n){Jn.toggleList(t,"UL",n)}),t.addCommand("InsertOrderedList",function(e,n){Jn.toggleList(t,"OL",n)}),t.addCommand("InsertDefinitionList",function(e,n){Jn.toggleList(t,"DL",n)}),t.addCommand("RemoveList",function(){qn(t)}),t.addQueryStateHandler("InsertUnorderedList",mt(t,"UL")),t.addQueryStateHandler("InsertOrderedList",mt(t,"OL")),t.addQueryStateHandler("InsertDefinitionList",mt(t,"DL"))},pt=function(e){return e.getParam("lists_indent_on_tab",!0)},vt=function(e){var n;pt(e)&&(n=e).on("keydown",function(e){e.keyCode!==l.TAB||l.metaKeyPressed(e)||n.undoManager.transact(function(){(e.shiftKey?$n(n):Hn(n))&&e.preventDefault()})}),ft(e)},ht=function(n,i){return function(e){var o=e.control;n.on("NodeChange",function(e){var n=function(e,n){for(var t=0;t<e.length;t++)if(n(e[t]))return t;return-1}(e.parents,O),t=-1!==n?e.parents.slice(0,n):e.parents,r=v.grep(t,y);o.active(0<r.length&&r[0].nodeName===i)})}},yt=function(e){var n,t,r;t="advlist",r=(n=e).settings.plugins?n.settings.plugins:"",-1===v.inArray(r.split(/[ ,]/),t)&&(e.addButton("numlist",{active:!1,title:"Numbered list",cmd:"InsertOrderedList",onPostRender:ht(e,"OL")}),e.addButton("bullist",{active:!1,title:"Bullet list",cmd:"InsertUnorderedList",onPostRender:ht(e,"UL")})),e.addButton("indent",{icon:"indent",title:"Increase indent",cmd:"Indent"})};c.add("lists",function(e){return vt(e),yt(e),gt(e),lt(e)})}(window); \ No newline at end of file +!function(u){"use strict";var e,n,t,r,o,i,s,a,c,f=tinymce.util.Tools.resolve("tinymce.PluginManager"),d=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),l=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),m=tinymce.util.Tools.resolve("tinymce.util.VK"),p=tinymce.util.Tools.resolve("tinymce.dom.BookmarkManager"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),g=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),h=function(e){return e&&"BR"===e.nodeName},y=function(e){return e&&3===e.nodeType},N=function(e){return e&&/^(OL|UL|DL)$/.test(e.nodeName)},S=function(e){return e&&/^(OL|UL)$/.test(e.nodeName)},C=function(e){return e&&/^(DT|DD)$/.test(e.nodeName)},O=function(e){return e&&/^(LI|DT|DD)$/.test(e.nodeName)},b=function(e){return e&&/^(TH|TD)$/.test(e.nodeName)},T=h,E=function(e,n){return n&&!!e.schema.getTextBlockElements()[n.nodeName]},L=function(e,n){return e&&e.nodeName in n},D=function(e,n){return!!h(n)&&!(!e.isBlock(n.nextSibling)||h(n.previousSibling))},w=function(e,n,t){var r=e.isEmpty(n);return!(t&&0<e.select("span[data-mce-type=bookmark]",n).length)&&r},k=function(e,n){return e.isChildOf(n,e.getRoot())},A=function(e,n){if(y(e))return{container:e,offset:n};var t=d.getNode(e,n);return y(t)?{container:t,offset:n>=e.childNodes.length?t.data.length:0}:t.previousSibling&&y(t.previousSibling)?{container:t.previousSibling,offset:t.previousSibling.data.length}:t.nextSibling&&y(t.nextSibling)?{container:t.nextSibling,offset:0}:{container:e,offset:n}},x=function(e){var n=e.cloneRange(),t=A(e.startContainer,e.startOffset);n.setStart(t.container,t.offset);var r=A(e.endContainer,e.endOffset);return n.setEnd(r.container,r.offset),n},R=g.DOM,I=function(o){var i={},e=function(e){var n,t,r;t=o[e?"startContainer":"endContainer"],r=o[e?"startOffset":"endOffset"],1===t.nodeType&&(n=R.create("span",{"data-mce-type":"bookmark"}),t.hasChildNodes()?(r=Math.min(r,t.childNodes.length-1),e?t.insertBefore(n,t.childNodes[r]):R.insertAfter(n,t.childNodes[r])):t.appendChild(n),t=n,r=0),i[e?"startContainer":"endContainer"]=t,i[e?"startOffset":"endOffset"]=r};return e(!0),o.collapsed||e(),i},_=function(o){function e(e){var n,t,r;n=r=o[e?"startContainer":"endContainer"],t=o[e?"startOffset":"endOffset"],n&&(1===n.nodeType&&(t=function(e){for(var n=e.parentNode.firstChild,t=0;n;){if(n===e)return t;1===n.nodeType&&"bookmark"===n.getAttribute("data-mce-type")||t++,n=n.nextSibling}return-1}(n),n=n.parentNode,R.remove(r),!n.hasChildNodes()&&R.isBlock(n)&&n.appendChild(R.create("br"))),o[e?"startContainer":"endContainer"]=n,o[e?"startOffset":"endOffset"]=t)}e(!0),e();var n=R.createRng();return n.setStart(o.startContainer,o.startOffset),o.endContainer&&n.setEnd(o.endContainer,o.endOffset),x(n)},B=function(){},P=function(e){return function(){return e}},M=function(t){return function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return!t.apply(null,e)}},U=P(!1),F=P(!0),j=function(){return H},H=(e=function(e){return e.isNone()},r={fold:function(e,n){return e()},is:U,isSome:U,isNone:F,getOr:t=function(e){return e},getOrThunk:n=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:P(null),getOrUndefined:P(undefined),or:t,orThunk:n,map:j,each:B,bind:j,exists:U,forall:F,filter:j,equals:e,equals_:e,toArray:function(){return[]},toString:P("none()")},Object.freeze&&Object.freeze(r),r),$=function(t){var e=P(t),n=function(){return o},r=function(e){return e(t)},o={fold:function(e,n){return n(t)},is:function(e){return t===e},isSome:F,isNone:U,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:n,orThunk:n,map:function(e){return $(e(t))},each:function(e){e(t)},bind:r,exists:r,forall:r,filter:function(e){return e(t)?o:H},toArray:function(){return[t]},toString:function(){return"some("+t+")"},equals:function(e){return e.is(t)},equals_:function(e,n){return e.fold(U,function(e){return n(t,e)})}};return o},q={some:$,none:j,from:function(e){return null===e||e===undefined?H:$(e)}},W=function(n){return function(e){return function(e){if(null===e)return"null";var n=typeof e;return"object"===n&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===n&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":n}(e)===n}},V=W("string"),z=W("array"),K=W("boolean"),X=W("function"),Q=W("number"),Y=Array.prototype.slice,G=Array.prototype.push,J=function(e,n){for(var t=e.length,r=new Array(t),o=0;o<t;o++){var i=e[o];r[o]=n(i,o)}return r},Z=function(e,n){for(var t=0,r=e.length;t<r;t++)n(e[t],t)},ee=function(e,n){for(var t=[],r=0,o=e.length;r<o;r++){var i=e[r];n(i,r)&&t.push(i)}return t},ne=function(e,n,t){return Z(e,function(e){t=n(t,e)}),t},te=function(e,n){for(var t=0,r=e.length;t<r;t++){var o=e[t];if(n(o,t))return q.some(o)}return q.none()},re=function(e,n){return function(e){for(var n=[],t=0,r=e.length;t<r;++t){if(!z(e[t]))throw new Error("Arr.flatten item "+t+" was not an array, input: "+e);G.apply(n,e[t])}return n}(J(e,n))},oe=function(e){return 0===e.length?q.none():q.some(e[0])},ie=function(e){return 0===e.length?q.none():q.some(e[e.length-1])},ue=(X(Array.from)&&Array.from,"undefined"!=typeof u.window?u.window:Function("return this;")()),se=function(e,n){return function(e,n){for(var t=n!==undefined&&null!==n?n:ue,r=0;r<e.length&&t!==undefined&&null!==t;++r)t=t[e[r]];return t}(e.split("."),n)},ae=function(e,n){var t=se(e,n);if(t===undefined||null===t)throw new Error(e+" not available on this browser");return t},ce=function(e){var n,t=se("ownerDocument.defaultView",e);return(n=t,ae("HTMLElement",n)).prototype.isPrototypeOf(e)},fe=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),de=function(e){var n=e.selection.getStart(!0);return e.dom.getParent(n,"OL,UL,DL",me(e,n))},le=function(e){var t,n,r,o=e.selection.getSelectedBlocks();return v.grep((t=e,n=o,r=v.map(n,function(e){var n=t.dom.getParent(e,"li,dd,dt",me(t,e));return n||e}),fe.unique(r)),function(e){return O(e)})},me=function(e,n){var t=e.dom.getParents(n,"TD,TH");return 0<t.length?t[0]:e.getBody()},ge=function(e,n){var t=e.dom.getParents(n,"ol,ul",me(e,n));return ie(t)},pe=function(n,e){var t=J(e,function(e){return ge(n,e).getOr(e)});return fe.unique(t)},ve={isList:function(e){var n=de(e);return ce(n)},getParentList:de,getSelectedSubLists:function(e){var n,t,r,o=de(e),i=e.selection.getSelectedBlocks();return r=i,(t=o)&&1===r.length&&r[0]===t?(n=o,v.grep(n.querySelectorAll("ol,ul,dl"),function(e){return N(e)})):v.grep(i,function(e){return N(e)&&o!==e})},getSelectedListItems:le,getClosestListRootElm:me,getSelectedDlItems:function(e){return ee(le(e),C)},getSelectedListRoots:function(e){var n,t,r,o=(t=ge(n=e,n.selection.getStart()),r=ee(n.selection.getSelectedBlocks(),S),t.toArray().concat(r));return pe(e,o)}},he=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:P(e)}},ye={fromHtml:function(e,n){var t=(n||u.document).createElement("div");if(t.innerHTML=e,!t.hasChildNodes()||1<t.childNodes.length)throw u.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return he(t.childNodes[0])},fromTag:function(e,n){var t=(n||u.document).createElement(e);return he(t)},fromText:function(e,n){var t=(n||u.document).createTextNode(e);return he(t)},fromDom:he,fromPoint:function(e,n,t){var r=e.dom();return q.from(r.elementFromPoint(n,t)).map(he)}},Ne=function(e,n,t){return e.isSome()&&n.isSome()?q.some(t(e.getOrDie(),n.getOrDie())):q.none()},Se=Object.keys,Ce=function(){return ae("Node")},Oe=function(e,n,t){return 0!=(e.compareDocumentPosition(n)&t)},be=function(e,n){return Oe(e,n,Ce().DOCUMENT_POSITION_CONTAINED_BY)},Te=function(e,n){var t=function(e,n){for(var t=0;t<e.length;t++){var r=e[t];if(r.test(n))return r}return undefined}(e,n);if(!t)return{major:0,minor:0};var r=function(e){return Number(n.replace(t,"$"+e))};return Le(r(1),r(2))},Ee=function(){return Le(0,0)},Le=function(e,n){return{major:e,minor:n}},De={nu:Le,detect:function(e,n){var t=String(n).toLowerCase();return 0===e.length?Ee():Te(e,t)},unknown:Ee},we="Firefox",ke=function(e,n){return function(){return n===e}},Ae=function(e){var n=e.current;return{current:n,version:e.version,isEdge:ke("Edge",n),isChrome:ke("Chrome",n),isIE:ke("IE",n),isOpera:ke("Opera",n),isFirefox:ke(we,n),isSafari:ke("Safari",n)}},xe={unknown:function(){return Ae({current:undefined,version:De.unknown()})},nu:Ae,edge:P("Edge"),chrome:P("Chrome"),ie:P("IE"),opera:P("Opera"),firefox:P(we),safari:P("Safari")},Re="Windows",Ie="Android",_e="Solaris",Be="FreeBSD",Pe=function(e,n){return function(){return n===e}},Me=function(e){var n=e.current;return{current:n,version:e.version,isWindows:Pe(Re,n),isiOS:Pe("iOS",n),isAndroid:Pe(Ie,n),isOSX:Pe("OSX",n),isLinux:Pe("Linux",n),isSolaris:Pe(_e,n),isFreeBSD:Pe(Be,n)}},Ue={unknown:function(){return Me({current:undefined,version:De.unknown()})},nu:Me,windows:P(Re),ios:P("iOS"),android:P(Ie),linux:P("Linux"),osx:P("OSX"),solaris:P(_e),freebsd:P(Be)},Fe=function(e,n){var t=String(n).toLowerCase();return te(e,function(e){return e.search(t)})},je=function(e,t){return Fe(e,t).map(function(e){var n=De.detect(e.versionRegexes,t);return{current:e.name,version:n}})},He=function(e,t){return Fe(e,t).map(function(e){var n=De.detect(e.versionRegexes,t);return{current:e.name,version:n}})},$e=function(e,n){return-1!==e.indexOf(n)},qe=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,We=function(n){return function(e){return $e(e,n)}},Ve=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return $e(e,"edge/")&&$e(e,"chrome")&&$e(e,"safari")&&$e(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,qe],search:function(e){return $e(e,"chrome")&&!$e(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return $e(e,"msie")||$e(e,"trident")}},{name:"Opera",versionRegexes:[qe,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:We("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:We("firefox")},{name:"Safari",versionRegexes:[qe,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return($e(e,"safari")||$e(e,"mobile/"))&&$e(e,"applewebkit")}}],ze=[{name:"Windows",search:We("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return $e(e,"iphone")||$e(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:We("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:We("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:We("linux"),versionRegexes:[]},{name:"Solaris",search:We("sunos"),versionRegexes:[]},{name:"FreeBSD",search:We("freebsd"),versionRegexes:[]}],Ke={browsers:P(Ve),oses:P(ze)},Xe=function(e){var n,t,r,o,i,u,s,a,c,f,d,l=Ke.browsers(),m=Ke.oses(),g=je(l,e).fold(xe.unknown,xe.nu),p=He(m,e).fold(Ue.unknown,Ue.nu);return{browser:g,os:p,deviceType:(t=g,r=e,o=(n=p).isiOS()&&!0===/ipad/i.test(r),i=n.isiOS()&&!o,u=n.isAndroid()&&3===n.version.major,s=n.isAndroid()&&4===n.version.major,a=o||u||s&&!0===/mobile/i.test(r),c=n.isiOS()||n.isAndroid(),f=c&&!a,d=t.isSafari()&&n.isiOS()&&!1===/safari/i.test(r),{isiPad:P(o),isiPhone:P(i),isTablet:P(a),isPhone:P(f),isTouch:P(c),isAndroid:n.isAndroid,isiOS:n.isiOS,isWebView:P(d)})}},Qe={detect:(o=function(){var e=u.navigator.userAgent;return Xe(e)},s=!1,function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return s||(s=!0,i=o.apply(null,e)),i})},Ye=(u.Node.ATTRIBUTE_NODE,u.Node.CDATA_SECTION_NODE,u.Node.COMMENT_NODE,u.Node.DOCUMENT_NODE,u.Node.DOCUMENT_TYPE_NODE,u.Node.DOCUMENT_FRAGMENT_NODE,u.Node.ELEMENT_NODE),Ge=(u.Node.TEXT_NODE,u.Node.PROCESSING_INSTRUCTION_NODE,u.Node.ENTITY_REFERENCE_NODE,u.Node.ENTITY_NODE,u.Node.NOTATION_NODE,Ye),Je=function(e,n){return e.dom()===n.dom()},Ze=Qe.detect().browser.isIE()?function(e,n){return be(e.dom(),n.dom())}:function(e,n){var t=e.dom(),r=n.dom();return t!==r&&t.contains(r)},en=function(e,n){var t=e.dom();if(t.nodeType!==Ge)return!1;var r=t;if(r.matches!==undefined)return r.matches(n);if(r.msMatchesSelector!==undefined)return r.msMatchesSelector(n);if(r.webkitMatchesSelector!==undefined)return r.webkitMatchesSelector(n);if(r.mozMatchesSelector!==undefined)return r.mozMatchesSelector(n);throw new Error("Browser lacks native selectors")},nn=function(e){return q.from(e.dom().parentNode).map(ye.fromDom)},tn=function(e){return J(e.dom().childNodes,ye.fromDom)},rn=function(e,n){var t=e.dom().childNodes;return q.from(t[n]).map(ye.fromDom)},on=function(e){return rn(e,0)},un=function(e){return rn(e,e.dom().childNodes.length-1)},sn=(function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n]}("element","offset"),function(n,t){nn(n).each(function(e){e.dom().insertBefore(t.dom(),n.dom())})}),an=function(e,n){e.dom().appendChild(n.dom())},cn=function(n,e){Z(e,function(e){an(n,e)})},fn=function(e){var n=e.dom();null!==n.parentNode&&n.parentNode.removeChild(n)},dn=function(e){return e.dom().nodeName.toLowerCase()},ln=(a=Ye,function(e){return e.dom().nodeType===a}),mn=function(e,n){var t=e.dom();!function(e,n){for(var t=Se(e),r=0,o=t.length;r<o;r++){var i=t[r];n(e[i],i)}}(n,function(e,n){!function(e,n,t){if(!(V(t)||K(t)||Q(t)))throw u.console.error("Invalid call to Attr.set. Key ",n,":: Value ",t,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(n,t+"")}(t,n,e)})},gn=function(e){return ne(e.dom().attributes,function(e,n){return e[n.name]=n.value,e},{})},pn=function(e,n,t){if(!V(t))throw u.console.error("Invalid call to CSS.set. Property ",n,":: Value ",t,":: Element ",e),new Error("CSS value must be a string: "+t);var r;(r=e).style!==undefined&&X(r.style.getPropertyValue)&&e.style.setProperty(n,t)},vn=function(e){return n=e,t=!0,ye.fromDom(n.dom().cloneNode(t));var n,t},hn=function(e,n){var t,r,o,i,u=(t=e,r=n,o=ye.fromTag(r),i=gn(t),mn(o,i),o);sn(e,u);var s=tn(e);return cn(u,s),fn(e),u},yn=function(e,n){an(e.item,n.list)},Nn=function(f,e,d){var n=e.slice(0,d.depth);return ie(n).each(function(e){var n,t,r,o,i,u,s,a,c=(n=f,t=d.itemAttributes,r=d.content,o=ye.fromTag("li",n),mn(o,t),cn(o,r),o);u=c,an((i=e).list,u),i.item=u,a=d,dn((s=e).list)!==a.listType&&(s.list=hn(s.list,a.listType)),mn(s.list,a.listAttributes)}),n},Sn=function(e,n,t){var r,o=function(e,n,t){for(var r,o,i,u=[],s=0;s<t;s++)u.push((r=e,o=n.listType,i={list:ye.fromTag(o,r),item:ye.fromTag("li",r)},an(i.list,i.item),i));return u}(e,t,t.depth-n.length);return function(e){for(var n=1;n<e.length;n++)yn(e[n-1],e[n])}(o),function(e,n){for(var t=0;t<e.length-1;t++)r=e[t].item,o="list-style-type",i="none",u=r.dom(),pn(u,o,i);var r,o,i,u;ie(e).each(function(e){mn(e.list,n.listAttributes),mn(e.item,n.itemAttributes),cn(e.item,n.content)})}(o,t),r=o,Ne(ie(n),oe(r),yn),n.concat(o)},Cn=function(e){return en(e,"OL,UL")},On=function(e){return on(e).map(Cn).getOr(!1)},bn=function(e){return 0<e.depth},Tn=function(e){return e.isSelected},En=function(e){var n=tn(e),t=un(e).map(Cn).getOr(!1)?n.slice(0,-1):n;return J(t,vn)},Ln=Object.prototype.hasOwnProperty,Dn=(c=function(e,n){return n},function(){for(var e=new Array(arguments.length),n=0;n<e.length;n++)e[n]=arguments[n];if(0===e.length)throw new Error("Can't merge zero objects");for(var t={},r=0;r<e.length;r++){var o=e[r];for(var i in o)Ln.call(o,i)&&(t[i]=c(t[i],o[i]))}return t}),wn=function(n){Z(n,function(r,e){(function(e,n){for(var t=e[n].depth,r=n-1;0<=r;r--){if(e[r].depth===t)return q.some(e[r]);if(e[r].depth<t)break}return q.none()})(n,e).each(function(e){var n,t;t=e,(n=r).listType=t.listType,n.listAttributes=Dn({},t.listAttributes)})})},kn=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return kn(t())}}},An=function(i,u,s,a){return on(a).filter(Cn).fold(function(){u.each(function(e){Je(e.start,a)&&s.set(!0)});var n,t,r,e=(n=a,t=i,r=s.get(),nn(n).filter(ln).map(function(e){return{depth:t,isSelected:r,content:En(n),itemAttributes:gn(n),listAttributes:gn(e),listType:dn(e)}}));u.each(function(e){Je(e.end,a)&&s.set(!1)});var o=un(a).filter(Cn).map(function(e){return xn(i,u,s,e)}).getOr([]);return e.toArray().concat(o)},function(e){return xn(i,u,s,e)})},xn=function(n,t,r,e){return re(tn(e),function(e){return(Cn(e)?xn:An)(n+1,t,r,e)})},Rn=tinymce.util.Tools.resolve("tinymce.Env"),In=function(e,n){var t,r,o,i,u=e.dom,s=e.schema.getBlockElements(),a=u.createFragment();if(e.settings.forced_root_block&&(o=e.settings.forced_root_block),o&&((r=u.create(o)).tagName===e.settings.forced_root_block&&u.setAttribs(r,e.settings.forced_root_block_attrs),L(n.firstChild,s)||a.appendChild(r)),n)for(;t=n.firstChild;){var c=t.nodeName;i||"SPAN"===c&&"bookmark"===t.getAttribute("data-mce-type")||(i=!0),L(t,s)?(a.appendChild(t),r=null):o?(r||(r=u.create(o),a.appendChild(r)),r.appendChild(t)):a.appendChild(t)}return e.settings.forced_root_block?i||Rn.ie&&!(10<Rn.ie)||r.appendChild(u.create("br",{"data-mce-bogus":"1"})):a.appendChild(u.create("br")),a},_n=function(i,e){return J(e,function(e){var n,t,r,o=(n=e.content,r=(t||u.document).createDocumentFragment(),Z(n,function(e){r.appendChild(e.dom())}),ye.fromDom(r));return ye.fromDom(In(i,o.dom()))})},Bn=function(e,n){return wn(n),(t=e.contentDocument,r=n,o=ne(r,function(e,n){return n.depth>e.length?Sn(t,e,n):Nn(t,e,n)},[]),oe(o).map(function(e){return e.list})).toArray();var t,r,o},Pn=function(e){var n,t,r=J(ve.getSelectedListItems(e),ye.fromDom);return Ne(te(r,M(On)),te((n=r,(t=Y.call(n,0)).reverse(),t),M(On)),function(e,n){return{start:e,end:n}})},Mn=function(s,e,a){var n,t,r,o=(n=e,t=Pn(s),r=kn(!1),J(n,function(e){return{sourceList:e,entries:xn(0,t,r,e)}}));Z(o,function(e){var n,t,r,o,i,u;n=e.entries,t=a,Z(ee(n,Tn),function(e){return function(e,n){switch(e){case"Indent":n.depth++;break;case"Outdent":n.depth--;break;case"Flatten":n.depth=0}}(t,e)}),r=e.sourceList,i=s,u=e.entries,o=re(function(e,n){if(0===e.length)return[];for(var t=n(e[0]),r=[],o=[],i=0,u=e.length;i<u;i++){var s=e[i],a=n(s);a!==t&&(r.push(o),o=[]),t=a,o.push(s)}return 0!==o.length&&r.push(o),r}(u,bn),function(e){return oe(e).map(bn).getOr(!1)?Bn(i,e):_n(i,e)}),Z(o,function(e){sn(r,e)}),fn(e.sourceList)})},Un=g.DOM,Fn=function(e,n,t){var r,o,i,u,s,a;for(i=Un.select('span[data-mce-type="bookmark"]',n),s=In(e,t),(r=Un.createRng()).setStartAfter(t),r.setEndAfter(n),u=(o=r.extractContents()).firstChild;u;u=u.firstChild)if("LI"===u.nodeName&&e.dom.isEmpty(u)){Un.remove(u);break}e.dom.isEmpty(o)||Un.insertAfter(o,n),Un.insertAfter(s,n),w(e.dom,t.parentNode)&&(a=t.parentNode,v.each(i,function(e){a.parentNode.insertBefore(e,t.parentNode)}),Un.remove(a)),Un.remove(t),w(e.dom,n)&&Un.remove(n)},jn=function(e){en(e,"dt")&&hn(e,"dd")},Hn=function(r,e,n){Z(n,"Indent"===e?jn:function(e){return n=r,void(en(t=e,"dd")?hn(t,"dt"):en(t,"dt")&&nn(t).each(function(e){return Fn(n,e.dom(),t.dom())}));var n,t})},$n=function(e,n){var t=J(ve.getSelectedListRoots(e),ye.fromDom),r=J(ve.getSelectedDlItems(e),ye.fromDom),o=!1;if(t.length||r.length){var i=e.selection.getBookmark();Mn(e,t,n),Hn(e,n,r),e.selection.moveToBookmark(i),e.selection.setRng(x(e.selection.getRng())),e.nodeChanged(),o=!0}return o},qn=function(e){return $n(e,"Indent")},Wn=function(e){return $n(e,"Outdent")},Vn=function(e){return $n(e,"Flatten")},zn=function(t,e){v.each(e,function(e,n){t.setAttribute(n,e)})},Kn=function(e,n,t){var r,o,i,u,s,a,c;r=e,o=n,u=(i=t)["list-style-type"]?i["list-style-type"]:null,r.setStyle(o,"list-style-type",u),s=e,zn(a=n,(c=t)["list-attributes"]),v.each(s.select("li",a),function(e){zn(e,c["list-item-attributes"])})},Xn=function(e,n,t,r){var o,i;for(o=n[t?"startContainer":"endContainer"],i=n[t?"startOffset":"endOffset"],1===o.nodeType&&(o=o.childNodes[Math.min(i,o.childNodes.length-1)]||o),!t&&T(o.nextSibling)&&(o=o.nextSibling);o.parentNode!==r;){if(E(e,o))return o;if(/^(TD|TH)$/.test(o.parentNode.nodeName))return o;o=o.parentNode}return o},Qn=function(f,d,l){void 0===l&&(l={});var e,n=f.selection.getRng(!0),m="LI",t=ve.getClosestListRootElm(f,f.selection.getStart(!0)),g=f.dom;"false"!==g.getContentEditable(f.selection.getNode())&&("DL"===(d=d.toUpperCase())&&(m="DT"),e=I(n),v.each(function(t,e,r){for(var o,i=[],u=t.dom,n=Xn(t,e,!0,r),s=Xn(t,e,!1,r),a=[],c=n;c&&(a.push(c),c!==s);c=c.nextSibling);return v.each(a,function(e){if(E(t,e))return i.push(e),void(o=null);if(u.isBlock(e)||T(e))return T(e)&&u.remove(e),void(o=null);var n=e.nextSibling;p.isBookmarkNode(e)&&(E(t,n)||!n&&e.parentNode===r)?o=null:(o||(o=u.create("p"),e.parentNode.insertBefore(o,e),i.push(o)),o.appendChild(e))}),i}(f,n,t),function(e){var n,t,r,o,i,u,s,a,c;(t=e.previousSibling)&&N(t)&&t.nodeName===d&&(r=t,o=l,i=g.getStyle(r,"list-style-type"),u=o?o["list-style-type"]:"",i===(u=null===u?"":u))?(n=t,e=g.rename(e,m),t.appendChild(e)):(n=g.create(d),e.parentNode.insertBefore(n,e),n.appendChild(e),e=g.rename(e,m)),s=g,a=e,c=["margin","margin-right","margin-bottom","margin-left","margin-top","padding","padding-right","padding-bottom","padding-left","padding-top"],v.each(c,function(e){var n;return s.setStyle(a,((n={})[e]="",n))}),Kn(g,n,l),Gn(f.dom,n)}),f.selection.setRng(_(e)))},Yn=function(e,n,t){return a=t,(s=n)&&a&&N(s)&&s.nodeName===a.nodeName&&(i=n,u=t,(o=e).getStyle(i,"list-style-type",!0)===o.getStyle(u,"list-style-type",!0))&&(r=t,n.className===r.className);var r,o,i,u,s,a},Gn=function(e,n){var t,r;if(t=n.nextSibling,Yn(e,n,t)){for(;r=t.firstChild;)n.appendChild(r);e.remove(t)}if(t=n.previousSibling,Yn(e,n,t)){for(;r=t.lastChild;)n.insertBefore(r,n.firstChild);e.remove(t)}},Jn=function(n,e,t,r,o){if(e.nodeName!==r||Zn(o)){var i=I(n.selection.getRng(!0));v.each([e].concat(t),function(e){!function(e,n,t,r){if(n.nodeName!==t){var o=e.rename(n,t);Kn(e,o,r)}else Kn(e,n,r)}(n.dom,e,r,o)}),n.selection.setRng(_(i))}else Vn(n)},Zn=function(e){return"list-style-type"in e},et={toggleList:function(e,n,t){var r=ve.getParentList(e),o=ve.getSelectedSubLists(e);t=t||{},r&&0<o.length?Jn(e,r,o,n,t):function(e,n,t,r){if(n!==e.getBody())if(n)if(n.nodeName!==t||Zn(r)){var o=I(e.selection.getRng(!0));Kn(e.dom,n,r),Gn(e.dom,e.dom.rename(n,t)),e.selection.setRng(_(o))}else Vn(e);else Qn(e,t,r)}(e,r,n,t)},mergeWithAdjacentLists:Gn},nt=g.DOM,tt=function(e,n){var t,r=n.parentNode;"LI"===r.nodeName&&r.firstChild===n&&((t=r.previousSibling)&&"LI"===t.nodeName?(t.appendChild(n),w(e,r)&&nt.remove(r)):nt.setStyle(r,"listStyleType","none")),N(r)&&(t=r.previousSibling)&&"LI"===t.nodeName&&t.appendChild(n)},rt=function(n,e){v.each(v.grep(n.select("ol,ul",e)),function(e){tt(n,e)})},ot=function(e,n,t,r){var o,i,u=n.startContainer,s=n.startOffset;if(3===u.nodeType&&(t?s<u.data.length:0<s))return u;for(o=e.schema.getNonEmptyElements(),1===u.nodeType&&(u=d.getNode(u,s)),i=new l(u,r),t&&D(e.dom,u)&&i.next();u=i[t?"next":"prev2"]();){if("LI"===u.nodeName&&!u.hasChildNodes())return u;if(o[u.nodeName])return u;if(3===u.nodeType&&0<u.data.length)return u}},it=function(e,n){var t=n.childNodes;return 1===t.length&&!N(t[0])&&e.isBlock(t[0])},ut=function(e,n,t){var r,o,i,u;if(o=it(e,t)?t.firstChild:t,it(i=e,u=n)&&i.remove(u.firstChild,!0),!w(e,n,!0))for(;r=n.firstChild;)o.appendChild(r)},st=function(n,e,t){var r,o,i=e.parentNode;if(k(n,e)&&k(n,t)){N(t.lastChild)&&(o=t.lastChild),i===t.lastChild&&T(i.previousSibling)&&n.remove(i.previousSibling),(r=t.lastChild)&&T(r)&&e.hasChildNodes()&&n.remove(r),w(n,t,!0)&&n.$(t).empty(),ut(n,e,t),o&&t.appendChild(o);var u=Ze(ye.fromDom(t),ye.fromDom(e))?n.getParents(e,N,t):[];n.remove(e),Z(u,function(e){w(n,e)&&e!==n.getRoot()&&n.remove(e)})}},at=function(e,n,t,r){var o,i,u,s=e.dom;if(s.isEmpty(r))i=t,u=r,(o=e).dom.$(u).empty(),st(o.dom,i,u),o.selection.setCursorLocation(u);else{var a=I(n);st(s,t,r),e.selection.setRng(_(a))}},ct=function(e,n){var t,r,o,i=e.dom,u=e.selection,s=u.getStart(),a=ve.getClosestListRootElm(e,s),c=i.getParent(u.getStart(),"LI",a);if(c){if((t=c.parentNode)===e.getBody()&&w(i,t))return!0;if(r=x(u.getRng(!0)),(o=i.getParent(ot(e,r,n,a),"LI",a))&&o!==c)return n?at(e,r,o,c):function(e,n,t,r){var o=I(n);st(e.dom,t,r);var i=_(o);e.selection.setRng(i)}(e,r,c,o),!0;if(!o&&!n)return Vn(e),!0}return!1},ft=function(e,n){return ct(e,n)||function(o,i){var u=o.dom,e=o.selection.getStart(),s=ve.getClosestListRootElm(o,e),a=u.getParent(e,u.isBlock,s);if(a&&u.isEmpty(a)){var n=x(o.selection.getRng(!0)),c=u.getParent(ot(o,n,i,s),"LI",s);if(c)return o.undoManager.transact(function(){var e,n,t,r;n=a,t=s,r=(e=u).getParent(n.parentNode,e.isBlock,t),e.remove(n),r&&e.isEmpty(r)&&e.remove(r),et.mergeWithAdjacentLists(u,c.parentNode),o.selection.select(c,!0),o.selection.collapse(i)}),!0}return!1}(e,n)},dt=function(e,n){return e.selection.isCollapsed()?ft(e,n):(r=(t=e).selection.getStart(),o=ve.getClosestListRootElm(t,r),!!(t.dom.getParent(r,"LI,DT,DD",o)||0<ve.getSelectedListItems(t).length)&&(t.undoManager.transact(function(){t.execCommand("Delete"),rt(t.dom,t.getBody())}),!0));var t,r,o},lt=function(n){n.on("keydown",function(e){e.keyCode===m.BACKSPACE?dt(n,!1)&&e.preventDefault():e.keyCode===m.DELETE&&dt(n,!0)&&e.preventDefault()})},mt=dt,gt=function(n){return{backspaceDelete:function(e){mt(n,e)}}},pt=function(n,t){return function(){var e=n.dom.getParent(n.selection.getStart(),"UL,OL,DL");return e&&e.nodeName===t}},vt=function(t){t.on("BeforeExecCommand",function(e){var n=e.command.toLowerCase();"indent"===n?qn(t):"outdent"===n&&Wn(t)}),t.addCommand("InsertUnorderedList",function(e,n){et.toggleList(t,"UL",n)}),t.addCommand("InsertOrderedList",function(e,n){et.toggleList(t,"OL",n)}),t.addCommand("InsertDefinitionList",function(e,n){et.toggleList(t,"DL",n)}),t.addCommand("RemoveList",function(){Vn(t)}),t.addQueryStateHandler("InsertUnorderedList",pt(t,"UL")),t.addQueryStateHandler("InsertOrderedList",pt(t,"OL")),t.addQueryStateHandler("InsertDefinitionList",pt(t,"DL"))},ht=function(e){return e.getParam("lists_indent_on_tab",!0)},yt=function(e){var n;ht(e)&&(n=e).on("keydown",function(e){e.keyCode!==m.TAB||m.metaKeyPressed(e)||n.undoManager.transact(function(){(e.shiftKey?Wn(n):qn(n))&&e.preventDefault()})}),lt(e)},Nt=function(n,i){return function(e){var o=e.control;n.on("NodeChange",function(e){var n=function(e,n){for(var t=0;t<e.length;t++)if(n(e[t]))return t;return-1}(e.parents,b),t=-1!==n?e.parents.slice(0,n):e.parents,r=v.grep(t,N);o.active(0<r.length&&r[0].nodeName===i)})}},St=function(e){var n,t,r;t="advlist",r=(n=e).settings.plugins?n.settings.plugins:"",-1===v.inArray(r.split(/[ ,]/),t)&&(e.addButton("numlist",{active:!1,title:"Numbered list",cmd:"InsertOrderedList",onPostRender:Nt(e,"OL")}),e.addButton("bullist",{active:!1,title:"Bullet list",cmd:"InsertUnorderedList",onPostRender:Nt(e,"UL")})),e.addButton("indent",{icon:"indent",title:"Increase indent",cmd:"Indent"})};f.add("lists",function(e){return yt(e),St(e),vt(e),gt(e)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/media/plugin.min.js b/lib/web/tiny_mce_4/plugins/media/plugin.min.js index 0dd06ba6816d9..e78d8efc11915 100644 --- a/lib/web/tiny_mce_4/plugins/media/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/media/plugin.min.js @@ -1 +1 @@ -!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),o=tinymce.util.Tools.resolve("tinymce.Env"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),w=function(e){return e.getParam("media_scripts")},b=function(e){return e.getParam("audio_template_callback")},y=function(e){return e.getParam("video_template_callback")},n=function(e){return e.getParam("media_live_embeds",!0)},t=function(e){return e.getParam("media_filter_html",!0)},s=function(e){return e.getParam("media_url_resolver")},m=function(e){return e.getParam("media_alt_source",!0)},d=function(e){return e.getParam("media_poster",!0)},h=function(e){return e.getParam("media_dimensions",!0)},p=tinymce.util.Tools.resolve("tinymce.html.SaxParser"),r=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),x=function(e,t){if(e)for(var r=0;r<e.length;r++)if(-1!==t.indexOf(e[r].filter))return e[r]},i=function(t){return function(e){return e?e.style[t].replace(/px$/,""):""}},a=function(i){return function(e,t){var r;e&&(e.style[i]=/^[0-9.]+$/.test(r=t)?r+"px":r)}},f={getMaxWidth:i("maxWidth"),getMaxHeight:i("maxHeight"),setMaxWidth:a("maxWidth"),setMaxHeight:a("maxHeight")},u=r.DOM,l=function(e){return u.getAttrib(e,"data-ephox-embed-iri")},j=function(e,t){return c=t,s=u.createFragment(c),""!==l(s.firstChild)?(o=t,n=u.createFragment(o).firstChild,{type:"ephox-embed-iri",source1:l(n),source2:"",poster:"",width:f.getMaxWidth(n),height:f.getMaxHeight(n)}):(i=e,r=t,p({validate:(a={},!1),allow_conditional_comments:!0,special:"script,noscript",start:function(e,t){if(a.source1||"param"!==e||(a.source1=t.map.movie),"iframe"!==e&&"object"!==e&&"embed"!==e&&"video"!==e&&"audio"!==e||(a.type||(a.type=e),a=v.extend(t.map,a)),"script"===e){var r=x(i,t.map.src);if(!r)return;a={type:"script",source1:t.map.src,width:r.width,height:r.height}}"source"===e&&(a.source1?a.source2||(a.source2=t.map.src):a.source1=t.map.src),"img"!==e||a.poster||(a.poster=t.map.src)}}).parse(r),a.source1=a.source1||a.src||a.data,a.source2=a.source2||"",a.poster=a.poster||"",a);var i,r,a,o,n,c,s},g=tinymce.util.Tools.resolve("tinymce.util.Promise"),M=function(e){var t={mp3:"audio/mpeg",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",swf:"application/x-shockwave-flash"}[e.toLowerCase().split(".").pop()];return t||""},_=tinymce.util.Tools.resolve("tinymce.html.Writer"),C=tinymce.util.Tools.resolve("tinymce.html.Schema"),S=r.DOM,F=function(e,t){var r,i,a,o;for(r in t)if(a=""+t[r],e.map[r])for(i=e.length;i--;)(o=e[i]).name===r&&(a?(e.map[r]=a,o.value=a):(delete e.map[r],e.splice(i,1)));else a&&(e.push({name:r,value:a}),e.map[r]=a)},k=function(e,t){var r,i,a=S.createFragment(e).firstChild;return f.setMaxWidth(a,t.width),f.setMaxHeight(a,t.height),r=a.outerHTML,i=_(),p(i).parse(r),i.getContent()},A=function(e,t,r){return u=e,l=S.createFragment(u),""!==S.getAttrib(l.firstChild,"data-ephox-embed-iri")?k(e,t):(i=e,a=t,o=r,c=_(),p({validate:!1,allow_conditional_comments:!(s=0),special:"script,noscript",comment:function(e){c.comment(e)},cdata:function(e){c.cdata(e)},text:function(e,t){c.text(e,t)},start:function(e,t,r){switch(e){case"video":case"object":case"embed":case"img":case"iframe":a.height!==undefined&&a.width!==undefined&&F(t,{width:a.width,height:a.height})}if(o)switch(e){case"video":F(t,{poster:a.poster,src:""}),a.source2&&F(t,{src:""});break;case"iframe":F(t,{src:a.source1});break;case"source":if(++s<=2&&(F(t,{src:a["source"+s],type:a["source"+s+"mime"]}),!a["source"+s]))return;break;case"img":if(!a.poster)return;n=!0}c.start(e,t,r)},end:function(e){if("video"===e&&o)for(var t=1;t<=2;t++)if(a["source"+t]){var r=[];r.map={},s<t&&(F(r,{src:a["source"+t],type:a["source"+t+"mime"]}),c.start("source",r,!0))}if(a.poster&&"object"===e&&o&&!n){var i=[];i.map={},F(i,{src:a.poster,width:a.width,height:a.height}),c.start("img",i,!0)}c.end(e)}},C({})).parse(i),c.getContent());var i,a,o,n,c,s,u,l},N=[{regex:/youtu\.be\/([\w\-_\?&=.]+)/i,type:"iframe",w:560,h:314,url:"//www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?/i,type:"iframe",w:560,h:314,url:"//www.youtube.com/embed/$2?$4",allowFullscreen:!0},{regex:/youtube.com\/embed\/([a-z0-9\?&=\-_]+)/i,type:"iframe",w:560,h:314,url:"//www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/vimeo\.com\/([0-9]+)/,type:"iframe",w:425,h:350,url:"//player.vimeo.com/video/$1?title=0&byline=0&portrait=0&color=8dc7dc",allowFullscreen:!0},{regex:/vimeo\.com\/(.*)\/([0-9]+)/,type:"iframe",w:425,h:350,url:"//player.vimeo.com/video/$2?title=0&byline=0",allowFullscreen:!0},{regex:/maps\.google\.([a-z]{2,3})\/maps\/(.+)msid=(.+)/,type:"iframe",w:425,h:350,url:'//maps.google.com/maps/ms?msid=$2&output=embed"',allowFullscreen:!1},{regex:/dailymotion\.com\/video\/([^_]+)/,type:"iframe",w:480,h:270,url:"//www.dailymotion.com/embed/video/$1",allowFullscreen:!0},{regex:/dai\.ly\/([^_]+)/,type:"iframe",w:480,h:270,url:"//www.dailymotion.com/embed/video/$1",allowFullscreen:!0}],c=function(r,e){var i=v.extend({},e);if(!i.source1&&(v.extend(i,j(w(r),i.embed)),!i.source1))return"";i.source2||(i.source2=""),i.poster||(i.poster=""),i.source1=r.convertURL(i.source1,"source"),i.source2=r.convertURL(i.source2,"source"),i.source1mime=M(i.source1),i.source2mime=M(i.source2),i.poster=r.convertURL(i.poster,"poster");var t,a,o=(t=i.source1,0<(a=N.filter(function(e){return e.regex.test(t)})).length?v.extend({},a[0],{url:function(e,t){for(var r=e.regex.exec(t),i=e.url,a=function(e){i=i.replace("$"+e,function(){return r[e]?r[e]:""})},o=0;o<r.length;o++)a(o);return i.replace(/\?$/,"")}(a[0],t)}):null);if(o&&(i.source1=o.url,i.type=o.type,i.allowFullscreen=o.allowFullscreen,i.width=i.width||o.w,i.height=i.height||o.h),i.embed)return A(i.embed,i,!0);var n=x(w(r),i.source1);n&&(i.type="script",i.width=n.width,i.height=n.height);var c,s,u,l,m,d,h,p,f=b(r),g=y(r);return i.width=i.width||300,i.height=i.height||150,v.each(i,function(e,t){i[t]=r.dom.encode(e)}),"iframe"===i.type?(p=(h=i).allowFullscreen?' allowFullscreen="1"':"",'<iframe src="'+h.source1+'" width="'+h.width+'" height="'+h.height+'"'+p+"></iframe>"):"application/x-shockwave-flash"===i.source1mime?(d='<object data="'+(m=i).source1+'" width="'+m.width+'" height="'+m.height+'" type="application/x-shockwave-flash">',m.poster&&(d+='<img src="'+m.poster+'" width="'+m.width+'" height="'+m.height+'" />'),d+="</object>"):-1!==i.source1mime.indexOf("audio")?(u=i,(l=f)?l(u):'<audio controls="controls" src="'+u.source1+'">'+(u.source2?'\n<source src="'+u.source2+'"'+(u.source2mime?' type="'+u.source2mime+'"':"")+" />\n":"")+"</audio>"):"script"===i.type?'<script src="'+i.source1+'"><\/script>':(c=i,(s=g)?s(c):'<video width="'+c.width+'" height="'+c.height+'"'+(c.poster?' poster="'+c.poster+'"':"")+' controls="controls">\n<source src="'+c.source1+'"'+(c.source1mime?' type="'+c.source1mime+'"':"")+" />\n"+(c.source2?'<source src="'+c.source2+'"'+(c.source2mime?' type="'+c.source2mime+'"':"")+" />\n":"")+"</video>")},O={},P=function(t){return function(e){return c(t,e)}},T=function(e,t){var r,i,a,o,n,c=s(e);return c?(a=t,o=P(e),n=c,new g(function(t,e){var r=function(e){return e.html&&(O[a.source1]=e),t({url:a.source1,html:e.html?e.html:o(a)})};O[a.source1]?r(O[a.source1]):n({url:a.source1},r,e)})):(r=t,i=P(e),new g(function(e){e({html:i(r),url:r.source1})}))},$=function(e){return O.hasOwnProperty(e)},z=function(e,t){e.state.set("oldVal",e.value()),t.state.set("oldVal",t.value())},L=function(e,t){var r=e.find("#width")[0],i=e.find("#height")[0],a=e.find("#constrain")[0];r&&i&&a&&t(r,i,a.checked())},H=function(e,t,r){var i=e.state.get("oldVal"),a=t.state.get("oldVal"),o=e.value(),n=t.value();r&&i&&a&&o&&n&&(o!==i?(n=Math.round(o/i*n),isNaN(n)||t.value(n)):(o=Math.round(n/a*o),isNaN(o)||e.value(o))),z(e,t)},W=function(e){L(e,H)},J=function(e){var t=function(){e(function(e){W(e)})};return{type:"container",label:"Dimensions",layout:"flex",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:5,onchange:t,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:5,onchange:t,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}},R=function(e){L(e,z)},D=W,E=o.ie&&o.ie<=8?"onChange":"onInput",I=function(r){return function(e){var t=e&&e.msg?"Media embed handler error: "+e.msg:"Media embed handler threw unknown error.";r.notificationManager.open({type:"error",text:t})}},U=function(a,o){return function(e){var t=e.html,r=a.find("#embed")[0],i=v.extend(j(w(o),t),{source1:e.url});a.fromJSON(i),r&&(r.value(t),D(a))}},V=function(e,t){var r=e.dom.select("img[data-mce-object]");e.insertContent(t),function(e,t){var r,i,a=e.dom.select("img[data-mce-object]");for(r=0;r<t.length;r++)for(i=a.length-1;0<=i;i--)t[r]===a[i]&&a.splice(i,1);e.selection.select(a[0])}(e,r),e.nodeChanged()},B=function(i){var a,t,e,r,o,n=[{name:"source1",type:"filepicker",filetype:"media",size:40,autofocus:!0,label:"Source",onpaste:function(){setTimeout(function(){T(i,a.toJSON()).then(U(a,i))["catch"](I(i))},1)},onchange:function(e){var r,t;T(i,a.toJSON()).then(U(a,i))["catch"](I(i)),r=a,t=e.meta,v.each(t,function(e,t){r.find("#"+t).value(e)})},onbeforecall:function(e){e.meta=a.toJSON()}}],c=[];if(m(i)&&c.push({name:"source2",type:"filepicker",filetype:"media",size:40,label:"Alternative source"}),d(i)&&c.push({name:"poster",type:"filepicker",filetype:"image",size:40,label:"Poster"}),h(i)){var s=J(function(e){e(a),t=a.toJSON(),a.find("#embed").value(A(t.embed,t))});n.push(s)}r=(e=i).selection.getNode(),o=r.getAttribute("data-ephox-embed-iri"),t=o?{source1:o,"data-ephox-embed-iri":o,width:f.getMaxWidth(r),height:f.getMaxHeight(r)}:r.getAttribute("data-mce-object")?j(w(e),e.serializer.serialize(r,{selection:!0})):{};var u={id:"mcemediasource",type:"textbox",flex:1,name:"embed",value:function(e){var t=e.selection.getNode();if(t.getAttribute("data-mce-object")||t.getAttribute("data-ephox-embed-iri"))return e.selection.getContent()}(i),multiline:!0,rows:5,label:"Source"};u[E]=function(){t=v.extend({},j(w(i),this.value())),this.parent().parent().fromJSON(t)};var l=[{title:"General",type:"form",items:n},{title:"Embed",type:"container",layout:"flex",direction:"column",align:"stretch",padding:10,spacing:10,items:[{type:"label",text:"Paste your embed code below:",forId:"mcemediasource"},u]}];0<c.length&&l.push({title:"Advanced",type:"form",items:c}),a=i.windowManager.open({title:"Insert/edit media",data:t,bodyType:"tabpanel",body:l,onSubmit:function(){var t,e;D(a),t=i,(e=a.toJSON()).embed=A(e.embed,e),e.embed&&$(e.source1)?V(t,e.embed):T(t,e).then(function(e){V(t,e.html)})["catch"](I(t))}}),R(a)},G=function(e){return{showDialog:function(){B(e)}}},q=function(e){e.addCommand("mceMedia",function(){B(e)})},K=tinymce.util.Tools.resolve("tinymce.html.Node"),Q=function(a,e){if(!1===t(a))return e;var o,n=_();return p({validate:!1,allow_conditional_comments:!1,special:"script,noscript",comment:function(e){n.comment(e)},cdata:function(e){n.cdata(e)},text:function(e,t){n.text(e,t)},start:function(e,t,r){if(o=!0,"script"!==e&&"noscript"!==e){for(var i=0;i<t.length;i++){if(0===t[i].name.indexOf("on"))return;"style"===t[i].name&&(t[i].value=a.dom.serializeStyle(a.dom.parseStyle(t[i].value),e))}n.start(e,t,r),o=!1}},end:function(e){o||n.end(e)}},C({})).parse(e),n.getContent()},X=function(e,t){var r,i=t.name;return(r=new K("img",1)).shortEnded=!0,Z(e,t,r),r.attr({width:t.attr("width")||"300",height:t.attr("height")||("audio"===i?"30":"150"),style:t.attr("style"),src:o.transparentSrc,"data-mce-object":i,"class":"mce-object mce-object-"+i}),r},Y=function(e,t){var r,i,a,o=t.name;return(r=new K("span",1)).attr({contentEditable:"false",style:t.attr("style"),"data-mce-object":o,"class":"mce-preview-object mce-object-"+o}),Z(e,t,r),(i=new K(o,1)).attr({src:t.attr("src"),allowfullscreen:t.attr("allowfullscreen"),style:t.attr("style"),"class":t.attr("class"),width:t.attr("width"),height:t.attr("height"),frameborder:"0"}),(a=new K("span",1)).attr("class","mce-shim"),r.append(i),r.append(a),r},Z=function(e,t,r){var i,a,o,n,c;for(n=(o=t.attributes).length;n--;)i=o[n].name,a=o[n].value,"width"!==i&&"height"!==i&&"style"!==i&&("data"!==i&&"src"!==i||(a=e.convertURL(a,i)),r.attr("data-mce-p-"+i,a));(c=t.firstChild&&t.firstChild.value)&&(r.attr("data-mce-html",escape(Q(e,c))),r.firstChild=null)},ee=function(e){for(;e=e.parent;)if(e.attr("data-ephox-embed-iri"))return!0;return!1},te=function(a){return function(e){for(var t,r,i=e.length;i--;)(t=e[i]).parent&&(t.parent.attr("data-mce-object")||("script"!==t.name||(r=x(w(a),t.attr("src"))))&&(r&&(r.width&&t.attr("width",r.width.toString()),r.height&&t.attr("height",r.height.toString())),"iframe"===t.name&&n(a)&&o.ceFalse?ee(t)||t.replace(Y(a,t)):ee(t)||t.replace(X(a,t))))}},re=function(d){d.on("preInit",function(){var t=d.schema.getSpecialElements();v.each("video audio iframe object".split(" "),function(e){t[e]=new RegExp("</"+e+"[^>]*>","gi")});var r=d.schema.getBoolAttrs();v.each("webkitallowfullscreen mozallowfullscreen allowfullscreen".split(" "),function(e){r[e]={}}),d.parser.addNodeFilter("iframe,video,audio,object,embed,script",te(d)),d.serializer.addAttributeFilter("data-mce-object",function(e,t){for(var r,i,a,o,n,c,s,u,l=e.length;l--;)if((r=e[l]).parent){for(s=r.attr(t),i=new K(s,1),"audio"!==s&&"script"!==s&&((u=r.attr("class"))&&-1!==u.indexOf("mce-preview-object")?i.attr({width:r.firstChild.attr("width"),height:r.firstChild.attr("height")}):i.attr({width:r.attr("width"),height:r.attr("height")})),i.attr({style:r.attr("style")}),a=(o=r.attributes).length;a--;){var m=o[a].name;0===m.indexOf("data-mce-p-")&&i.attr(m.substr(11),o[a].value)}"script"===s&&i.attr("type","text/javascript"),(n=r.attr("data-mce-html"))&&((c=new K("#text",3)).raw=!0,c.value=Q(d,unescape(n)),i.append(c)),r.replace(i)}})}),d.on("setContent",function(){d.$("span.mce-preview-object").each(function(e,t){var r=d.$(t);0===r.find("span.mce-shim",t).length&&r.append('<span class="mce-shim"></span>')})})},ie=function(e){e.on("ResolveName",function(e){var t;1===e.target.nodeType&&(t=e.target.getAttribute("data-mce-object"))&&(e.name=t)})},ae=function(t){t.on("click keyup",function(){var e=t.selection.getNode();e&&t.dom.hasClass(e,"mce-preview-object")&&t.dom.getAttrib(e,"data-mce-selected")&&e.setAttribute("data-mce-selected","2")}),t.on("ObjectSelected",function(e){var t=e.target.getAttribute("data-mce-object");"audio"!==t&&"script"!==t||e.preventDefault()}),t.on("objectResized",function(e){var t,r=e.target;r.getAttribute("data-mce-object")&&(t=r.getAttribute("data-mce-html"))&&(t=unescape(t),r.setAttribute("data-mce-html",escape(A(t,{width:e.width,height:e.height}))))})},oe=function(e){e.addButton("media",{tooltip:"Insert/edit media",cmd:"mceMedia",stateSelector:["img[data-mce-object]","span[data-mce-object]","div[data-ephox-embed-iri]"]}),e.addMenuItem("media",{icon:"media",text:"Media",cmd:"mceMedia",context:"insert",prependToContext:!0})};e.add("media",function(e){return q(e),oe(e),ie(e),re(e),ae(e),G(e)})}(); \ No newline at end of file +!function(){"use strict";var e,t,r,n,i=tinymce.util.Tools.resolve("tinymce.PluginManager"),o=tinymce.util.Tools.resolve("tinymce.Env"),v=tinymce.util.Tools.resolve("tinymce.util.Tools"),w=function(e){return e.getParam("media_scripts")},b=function(e){return e.getParam("audio_template_callback")},y=function(e){return e.getParam("video_template_callback")},a=function(e){return e.getParam("media_live_embeds",!0)},u=function(e){return e.getParam("media_filter_html",!0)},s=function(e){return e.getParam("media_url_resolver")},m=function(e){return e.getParam("media_alt_source",!0)},d=function(e){return e.getParam("media_poster",!0)},h=function(e){return e.getParam("media_dimensions",!0)},f=function(e){var t=e,r=function(){return t};return{get:r,set:function(e){t=e},clone:function(){return f(r())}}},c=function(){},l=function(e){return function(){return e}},p=l(!1),g=l(!0),x=function(){return O},O=(e=function(e){return e.isNone()},n={fold:function(e,t){return e()},is:p,isSome:p,isNone:g,getOr:r=function(e){return e},getOrThunk:t=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:l(null),getOrUndefined:l(undefined),or:r,orThunk:t,map:x,each:c,bind:x,exists:p,forall:g,filter:x,equals:e,equals_:e,toArray:function(){return[]},toString:l("none()")},Object.freeze&&Object.freeze(n),n),j=function(r){var e=l(r),t=function(){return i},n=function(e){return e(r)},i={fold:function(e,t){return t(r)},is:function(e){return r===e},isSome:g,isNone:p,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return j(e(r))},each:function(e){e(r)},bind:n,exists:n,forall:n,filter:function(e){return e(r)?i:O},toArray:function(){return[r]},toString:function(){return"some("+r+")"},equals:function(e){return e.is(r)},equals_:function(e,t){return e.fold(p,function(e){return t(r,e)})}};return i},_=x,S=function(e){return null===e||e===undefined?O:j(e)},k=Object.hasOwnProperty,N=function(e,t){return M(e,t)?S(e[t]):_()},M=function(e,t){return k.call(e,t)},T=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),z=tinymce.util.Tools.resolve("tinymce.html.SaxParser"),A=function(e,t){if(e)for(var r=0;r<e.length;r++)if(-1!==t.indexOf(e[r].filter))return e[r]},C=T.DOM,$=function(e){return e.replace(/px$/,"")},P=function(a,e){var c=f(!1),u={};return z({validate:!1,allow_conditional_comments:!0,special:"script,noscript",start:function(e,t){if(c.get());else if(M(t.map,"data-ephox-embed-iri"))c.set(!0),i=(n=t).map.style,o=i?C.parseStyle(i):{},u={type:"ephox-embed-iri",source1:n.map["data-ephox-embed-iri"],source2:"",poster:"",width:N(o,"max-width").map($).getOr(""),height:N(o,"max-height").map($).getOr("")};else{if(u.source1||"param"!==e||(u.source1=t.map.movie),"iframe"!==e&&"object"!==e&&"embed"!==e&&"video"!==e&&"audio"!==e||(u.type||(u.type=e),u=v.extend(t.map,u)),"script"===e){var r=A(a,t.map.src);if(!r)return;u={type:"script",source1:t.map.src,width:r.width,height:r.height}}"source"===e&&(u.source1?u.source2||(u.source2=t.map.src):u.source1=t.map.src),"img"!==e||u.poster||(u.poster=t.map.src)}var n,i,o}}).parse(e),u.source1=u.source1||u.src||u.data,u.source2=u.source2||"",u.poster=u.poster||"",u},F=tinymce.util.Tools.resolve("tinymce.util.Promise"),D=function(e){var t={mp3:"audio/mpeg",wav:"audio/wav",mp4:"video/mp4",webm:"video/webm",ogg:"video/ogg",swf:"application/x-shockwave-flash"}[e.toLowerCase().split(".").pop()];return t||""},L=tinymce.util.Tools.resolve("tinymce.html.Schema"),E=tinymce.util.Tools.resolve("tinymce.html.Writer"),J=T.DOM,R=function(e){return/^[0-9.]+$/.test(e)?e+"px":e},U=function(e,t){for(var r in t){var n=""+t[r];if(e.map[r])for(var i=e.length;i--;){var o=e[i];o.name===r&&(n?(e.map[r]=n,o.value=n):(delete e.map[r],e.splice(i,1)))}else n&&(e.push({name:r,value:n}),e.map[r]=n)}},W=function(e,c,u){var s,l=E(),m=f(!1),d=0;return z({validate:!1,allow_conditional_comments:!0,special:"script,noscript",comment:function(e){l.comment(e)},cdata:function(e){l.cdata(e)},text:function(e,t){l.text(e,t)},start:function(e,t,r){if(m.get());else if(M(t.map,"data-ephox-embed-iri"))m.set(!0),n=c,o=(i=t).map.style,(a=o?J.parseStyle(o):{})["max-width"]=R(n.width),a["max-height"]=R(n.height),U(i,{style:J.serializeStyle(a)});else{switch(e){case"video":case"object":case"embed":case"img":case"iframe":c.height!==undefined&&c.width!==undefined&&U(t,{width:c.width,height:c.height})}if(u)switch(e){case"video":U(t,{poster:c.poster,src:""}),c.source2&&U(t,{src:""});break;case"iframe":U(t,{src:c.source1});break;case"source":if(++d<=2&&(U(t,{src:c["source"+d],type:c["source"+d+"mime"]}),!c["source"+d]))return;break;case"img":if(!c.poster)return;s=!0}}var n,i,o,a;l.start(e,t,r)},end:function(e){if(!m.get()){if("video"===e&&u)for(var t=1;t<=2;t++)if(c["source"+t]){var r=[];r.map={},d<t&&(U(r,{src:c["source"+t],type:c["source"+t+"mime"]}),l.start("source",r,!0))}if(c.poster&&"object"===e&&u&&!s){var n=[];n.map={},U(n,{src:c.poster,width:c.width,height:c.height}),l.start("img",n,!0)}}l.end(e)}},L({})).parse(e),l.getContent()},H=[{regex:/youtu\.be\/([\w\-_\?&=.]+)/i,type:"iframe",w:560,h:314,url:"//www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/youtube\.com(.+)v=([^&]+)(&([a-z0-9&=\-_]+))?/i,type:"iframe",w:560,h:314,url:"//www.youtube.com/embed/$2?$4",allowFullscreen:!0},{regex:/youtube.com\/embed\/([a-z0-9\?&=\-_]+)/i,type:"iframe",w:560,h:314,url:"//www.youtube.com/embed/$1",allowFullscreen:!0},{regex:/vimeo\.com\/([0-9]+)/,type:"iframe",w:425,h:350,url:"//player.vimeo.com/video/$1?title=0&byline=0&portrait=0&color=8dc7dc",allowFullscreen:!0},{regex:/vimeo\.com\/(.*)\/([0-9]+)/,type:"iframe",w:425,h:350,url:"//player.vimeo.com/video/$2?title=0&byline=0",allowFullscreen:!0},{regex:/maps\.google\.([a-z]{2,3})\/maps\/(.+)msid=(.+)/,type:"iframe",w:425,h:350,url:'//maps.google.com/maps/ms?msid=$2&output=embed"',allowFullscreen:!1},{regex:/dailymotion\.com\/video\/([^_]+)/,type:"iframe",w:480,h:270,url:"//www.dailymotion.com/embed/video/$1",allowFullscreen:!0},{regex:/dai\.ly\/([^_]+)/,type:"iframe",w:480,h:270,url:"//www.dailymotion.com/embed/video/$1",allowFullscreen:!0}],I=function(r,e){var n=v.extend({},e);if(!n.source1&&(v.extend(n,P(w(r),n.embed)),!n.source1))return"";n.source2||(n.source2=""),n.poster||(n.poster=""),n.source1=r.convertURL(n.source1,"source"),n.source2=r.convertURL(n.source2,"source"),n.source1mime=D(n.source1),n.source2mime=D(n.source2),n.poster=r.convertURL(n.poster,"poster");var t,i,o=(t=n.source1,0<(i=H.filter(function(e){return e.regex.test(t)})).length?v.extend({},i[0],{url:function(e,t){for(var r=e.regex.exec(t),n=e.url,i=function(e){n=n.replace("$"+e,function(){return r[e]?r[e]:""})},o=0;o<r.length;o++)i(o);return n.replace(/\?$/,"")}(i[0],t)}):null);if(o&&(n.source1=o.url,n.type=o.type,n.allowFullscreen=o.allowFullscreen,n.width=n.width||o.w,n.height=n.height||o.h),n.embed)return W(n.embed,n,!0);var a=A(w(r),n.source1);a&&(n.type="script",n.width=a.width,n.height=a.height);var c,u,s,l,m,d,h,f,p=b(r),g=y(r);return n.width=n.width||300,n.height=n.height||150,v.each(n,function(e,t){n[t]=r.dom.encode(e)}),"iframe"===n.type?(f=(h=n).allowFullscreen?' allowFullscreen="1"':"",'<iframe src="'+h.source1+'" width="'+h.width+'" height="'+h.height+'"'+f+"></iframe>"):"application/x-shockwave-flash"===n.source1mime?(d='<object data="'+(m=n).source1+'" width="'+m.width+'" height="'+m.height+'" type="application/x-shockwave-flash">',m.poster&&(d+='<img src="'+m.poster+'" width="'+m.width+'" height="'+m.height+'" />'),d+="</object>"):-1!==n.source1mime.indexOf("audio")?(s=n,(l=p)?l(s):'<audio controls="controls" src="'+s.source1+'">'+(s.source2?'\n<source src="'+s.source2+'"'+(s.source2mime?' type="'+s.source2mime+'"':"")+" />\n":"")+"</audio>"):"script"===n.type?'<script src="'+n.source1+'"><\/script>':(c=n,(u=g)?u(c):'<video width="'+c.width+'" height="'+c.height+'"'+(c.poster?' poster="'+c.poster+'"':"")+' controls="controls">\n<source src="'+c.source1+'"'+(c.source1mime?' type="'+c.source1mime+'"':"")+" />\n"+(c.source2?'<source src="'+c.source2+'"'+(c.source2mime?' type="'+c.source2mime+'"':"")+" />\n":"")+"</video>")},q={},V=function(t){return function(e){return I(t,e)}},B=function(e,t){var r,n,i,o,a,c=s(e);return c?(i=t,o=V(e),a=c,new F(function(t,e){var r=function(e){return e.html&&(q[i.source1]=e),t({url:i.source1,html:e.html?e.html:o(i)})};q[i.source1]?r(q[i.source1]):a({url:i.source1},r,e)})):(r=t,n=V(e),new F(function(e){e({html:n(r),url:r.source1})}))},G=function(e){return q.hasOwnProperty(e)},K=function(t){return function(e){return e?e.style[t].replace(/px$/,""):""}},Q=function(n){return function(e,t){var r;e&&(e.style[n]=/^[0-9.]+$/.test(r=t)?r+"px":r)}},X={getMaxWidth:K("maxWidth"),getMaxHeight:K("maxHeight"),setMaxWidth:Q("maxWidth"),setMaxHeight:Q("maxHeight")},Y=function(e,t){e.state.set("oldVal",e.value()),t.state.set("oldVal",t.value())},Z=function(e,t){var r=e.find("#width")[0],n=e.find("#height")[0],i=e.find("#constrain")[0];r&&n&&i&&t(r,n,i.checked())},ee=function(e,t,r){var n=e.state.get("oldVal"),i=t.state.get("oldVal"),o=e.value(),a=t.value();r&&n&&i&&o&&a&&(o!==n?(a=Math.round(o/n*a),isNaN(a)||t.value(a)):(o=Math.round(a/i*o),isNaN(o)||e.value(o))),Y(e,t)},te=function(e){Z(e,ee)},re=function(e){var t=function(){e(function(e){te(e)})};return{type:"container",label:"Dimensions",layout:"flex",align:"center",spacing:5,items:[{name:"width",type:"textbox",maxLength:5,size:5,onchange:t,ariaLabel:"Width"},{type:"label",text:"x"},{name:"height",type:"textbox",maxLength:5,size:5,onchange:t,ariaLabel:"Height"},{name:"constrain",type:"checkbox",checked:!0,text:"Constrain proportions"}]}},ne=function(e){Z(e,Y)},ie=te,oe=o.ie&&o.ie<=8?"onChange":"onInput",ae=function(r){return function(e){var t=e&&e.msg?"Media embed handler error: "+e.msg:"Media embed handler threw unknown error.";r.notificationManager.open({type:"error",text:t})}},ce=function(i,o){return function(e){var t=e.html,r=i.find("#embed")[0],n=v.extend(P(w(o),t),{source1:e.url});i.fromJSON(n),r&&(r.value(t),ie(i))}},ue=function(e,t){var r=e.dom.select("img[data-mce-object]");e.insertContent(t),function(e,t){var r,n,i=e.dom.select("img[data-mce-object]");for(r=0;r<t.length;r++)for(n=i.length-1;0<=n;n--)t[r]===i[n]&&i.splice(n,1);e.selection.select(i[0])}(e,r),e.nodeChanged()},se=function(n){var i,t,e,r,o,a=[{name:"source1",type:"filepicker",filetype:"media",size:40,autofocus:!0,label:"Source",onpaste:function(){setTimeout(function(){B(n,i.toJSON()).then(ce(i,n))["catch"](ae(n))},1)},onchange:function(e){var r,t;B(n,i.toJSON()).then(ce(i,n))["catch"](ae(n)),r=i,t=e.meta,v.each(t,function(e,t){r.find("#"+t).value(e)})},onbeforecall:function(e){e.meta=i.toJSON()}}],c=[];if(m(n)&&c.push({name:"source2",type:"filepicker",filetype:"media",size:40,label:"Alternative source"}),d(n)&&c.push({name:"poster",type:"filepicker",filetype:"image",size:40,label:"Poster"}),h(n)){var u=re(function(e){e(i),t=i.toJSON(),i.find("#embed").value(W(t.embed,t))});a.push(u)}r=(e=n).selection.getNode(),o=r.getAttribute("data-ephox-embed-iri"),t=o?{source1:o,"data-ephox-embed-iri":o,width:X.getMaxWidth(r),height:X.getMaxHeight(r)}:r.getAttribute("data-mce-object")?P(w(e),e.serializer.serialize(r,{selection:!0})):{};var s={id:"mcemediasource",type:"textbox",flex:1,name:"embed",value:function(e){var t=e.selection.getNode();if(t.getAttribute("data-mce-object")||t.getAttribute("data-ephox-embed-iri"))return e.selection.getContent()}(n),multiline:!0,rows:5,label:"Source"};s[oe]=function(){t=v.extend({},P(w(n),this.value())),this.parent().parent().fromJSON(t)};var l=[{title:"General",type:"form",items:a},{title:"Embed",type:"container",layout:"flex",direction:"column",align:"stretch",padding:10,spacing:10,items:[{type:"label",text:"Paste your embed code below:",forId:"mcemediasource"},s]}];0<c.length&&l.push({title:"Advanced",type:"form",items:c}),i=n.windowManager.open({title:"Insert/edit media",data:t,bodyType:"tabpanel",body:l,onSubmit:function(){var t,e;ie(i),t=n,(e=i.toJSON()).embed=W(e.embed,e),e.embed&&G(e.source1)?ue(t,e.embed):B(t,e).then(function(e){ue(t,e.html)})["catch"](ae(t))}}),ne(i)},le=function(e){return{showDialog:function(){se(e)}}},me=function(e){e.addCommand("mceMedia",function(){se(e)})},de=tinymce.util.Tools.resolve("tinymce.html.Node"),he=function(o,e){if(!1===u(o))return e;var a,c=E();return z({validate:!1,allow_conditional_comments:!1,special:"script,noscript",comment:function(e){c.comment(e)},cdata:function(e){c.cdata(e)},text:function(e,t){c.text(e,t)},start:function(e,t,r){if(a=!0,"script"!==e&&"noscript"!==e&&"svg"!==e){for(var n=t.length-1;0<=n;n--){var i=t[n].name;0===i.indexOf("on")&&(delete t.map[i],t.splice(n,1)),"style"===i&&(t[n].value=o.dom.serializeStyle(o.dom.parseStyle(t[n].value),e))}c.start(e,t,r),a=!1}},end:function(e){a||c.end(e)}},L({})).parse(e),c.getContent()},fe=function(e,t){var r,n=t.name;return(r=new de("img",1)).shortEnded=!0,ge(e,t,r),r.attr({width:t.attr("width")||"300",height:t.attr("height")||("audio"===n?"30":"150"),style:t.attr("style"),src:o.transparentSrc,"data-mce-object":n,"class":"mce-object mce-object-"+n}),r},pe=function(e,t){var r,n,i,o=t.name;return(r=new de("span",1)).attr({contentEditable:"false",style:t.attr("style"),"data-mce-object":o,"class":"mce-preview-object mce-object-"+o}),ge(e,t,r),(n=new de(o,1)).attr({src:t.attr("src"),allowfullscreen:t.attr("allowfullscreen"),style:t.attr("style"),"class":t.attr("class"),width:t.attr("width"),height:t.attr("height"),frameborder:"0"}),(i=new de("span",1)).attr("class","mce-shim"),r.append(n),r.append(i),r},ge=function(e,t,r){var n,i,o,a,c;for(a=(o=t.attributes).length;a--;)n=o[a].name,i=o[a].value,"width"!==n&&"height"!==n&&"style"!==n&&("data"!==n&&"src"!==n||(i=e.convertURL(i,n)),r.attr("data-mce-p-"+n,i));(c=t.firstChild&&t.firstChild.value)&&(r.attr("data-mce-html",escape(he(e,c))),r.firstChild=null)},ve=function(e){for(;e=e.parent;)if(e.attr("data-ephox-embed-iri"))return!0;return!1},we=function(i){return function(e){for(var t,r,n=e.length;n--;)(t=e[n]).parent&&(t.parent.attr("data-mce-object")||("script"!==t.name||(r=A(w(i),t.attr("src"))))&&(r&&(r.width&&t.attr("width",r.width.toString()),r.height&&t.attr("height",r.height.toString())),"iframe"===t.name&&a(i)&&o.ceFalse?ve(t)||t.replace(pe(i,t)):ve(t)||t.replace(fe(i,t))))}},be=function(d){d.on("preInit",function(){var t=d.schema.getSpecialElements();v.each("video audio iframe object".split(" "),function(e){t[e]=new RegExp("</"+e+"[^>]*>","gi")});var r=d.schema.getBoolAttrs();v.each("webkitallowfullscreen mozallowfullscreen allowfullscreen".split(" "),function(e){r[e]={}}),d.parser.addNodeFilter("iframe,video,audio,object,embed,script",we(d)),d.serializer.addAttributeFilter("data-mce-object",function(e,t){for(var r,n,i,o,a,c,u,s,l=e.length;l--;)if((r=e[l]).parent){for(u=r.attr(t),n=new de(u,1),"audio"!==u&&"script"!==u&&((s=r.attr("class"))&&-1!==s.indexOf("mce-preview-object")?n.attr({width:r.firstChild.attr("width"),height:r.firstChild.attr("height")}):n.attr({width:r.attr("width"),height:r.attr("height")})),n.attr({style:r.attr("style")}),i=(o=r.attributes).length;i--;){var m=o[i].name;0===m.indexOf("data-mce-p-")&&n.attr(m.substr(11),o[i].value)}"script"===u&&n.attr("type","text/javascript"),(a=r.attr("data-mce-html"))&&((c=new de("#text",3)).raw=!0,c.value=he(d,unescape(a)),n.append(c)),r.replace(n)}})}),d.on("setContent",function(){d.$("span.mce-preview-object").each(function(e,t){var r=d.$(t);0===r.find("span.mce-shim",t).length&&r.append('<span class="mce-shim"></span>')})})},ye=function(e){e.on("ResolveName",function(e){var t;1===e.target.nodeType&&(t=e.target.getAttribute("data-mce-object"))&&(e.name=t)})},xe=function(t){t.on("click keyup",function(){var e=t.selection.getNode();e&&t.dom.hasClass(e,"mce-preview-object")&&t.dom.getAttrib(e,"data-mce-selected")&&e.setAttribute("data-mce-selected","2")}),t.on("ObjectSelected",function(e){var t=e.target.getAttribute("data-mce-object");"audio"!==t&&"script"!==t||e.preventDefault()}),t.on("objectResized",function(e){var t,r=e.target;r.getAttribute("data-mce-object")&&(t=r.getAttribute("data-mce-html"))&&(t=unescape(t),r.setAttribute("data-mce-html",escape(W(t,{width:e.width,height:e.height}))))})},Oe=function(e){e.addButton("media",{tooltip:"Insert/edit media",cmd:"mceMedia",stateSelector:["img[data-mce-object]","span[data-mce-object]","div[data-ephox-embed-iri]"]}),e.addMenuItem("media",{icon:"media",text:"Media",cmd:"mceMedia",context:"insert",prependToContext:!0})};i.add("media",function(e){return me(e),Oe(e),ye(e),be(e),xe(e),le(e)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/paste/plugin.min.js b/lib/web/tiny_mce_4/plugins/paste/plugin.min.js index 279f24c890308..db2393e70f0bf 100644 --- a/lib/web/tiny_mce_4/plugins/paste/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/paste/plugin.min.js @@ -1 +1 @@ -!function(v){"use strict";var g=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return g(n())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return!(!/(^|[ ,])powerpaste([, ]|$)/.test(t.settings.plugins)||!e.get("powerpaste")||("undefined"!=typeof v.window.console&&v.window.console.log&&v.window.console.log("PowerPaste is incompatible with Paste plugin! Remove 'paste' from the 'plugins' option."),0))},s=function(t,e){return{clipboard:t,quirks:e}},f=function(t,e,n,r){return t.fire("PastePreProcess",{content:e,internal:n,wordContent:r})},d=function(t,e,n,r){return t.fire("PastePostProcess",{node:e,internal:n,wordContent:r})},u=function(t,e){return t.fire("PastePlainTextToggle",{state:e})},n=function(t,e){return t.fire("paste",{ieFake:e})},m={shouldPlainTextInform:function(t){return t.getParam("paste_plaintext_inform",!0)},shouldBlockDrop:function(t){return t.getParam("paste_block_drop",!1)},shouldPasteDataImages:function(t){return t.getParam("paste_data_images",!1)},shouldFilterDrop:function(t){return t.getParam("paste_filter_drop",!0)},getPreProcess:function(t){return t.getParam("paste_preprocess")},getPostProcess:function(t){return t.getParam("paste_postprocess")},getWebkitStyles:function(t){return t.getParam("paste_webkit_styles")},shouldRemoveWebKitStyles:function(t){return t.getParam("paste_remove_styles_if_webkit",!0)},shouldMergeFormats:function(t){return t.getParam("paste_merge_formats",!0)},isSmartPasteEnabled:function(t){return t.getParam("smart_paste",!0)},isPasteAsTextEnabled:function(t){return t.getParam("paste_as_text",!1)},getRetainStyleProps:function(t){return t.getParam("paste_retain_style_properties")},getWordValidElements:function(t){return t.getParam("paste_word_valid_elements","-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody")},shouldConvertWordFakeLists:function(t){return t.getParam("paste_convert_word_fake_lists",!0)},shouldUseDefaultFilters:function(t){return t.getParam("paste_enable_default_filters",!0)}},r=function(t,e,n){var r,o,i;"text"===e.pasteFormat.get()?(e.pasteFormat.set("html"),u(t,!1)):(e.pasteFormat.set("text"),u(t,!0),i=t,!1===n.get()&&m.shouldPlainTextInform(i)&&(o="Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.",(r=t).notificationManager.open({text:r.translate(o),type:"info"}),n.set(!0))),t.focus()},l=function(t,n,e){t.addCommand("mceTogglePlainTextPaste",function(){r(t,n,e)}),t.addCommand("mceInsertClipboardContent",function(t,e){e.content&&n.pasteHtml(e.content,e.internal),e.text&&n.pasteText(e.text)})},p=tinymce.util.Tools.resolve("tinymce.Env"),h=tinymce.util.Tools.resolve("tinymce.util.Delay"),y=tinymce.util.Tools.resolve("tinymce.util.Tools"),o=tinymce.util.Tools.resolve("tinymce.util.VK"),t="x-tinymce/html",i="\x3c!-- "+t+" --\x3e",c=function(t){return i+t},b=function(t){return t.replace(i,"")},x=function(t){return-1!==t.indexOf(i)},P=function(){return t},w=tinymce.util.Tools.resolve("tinymce.html.Entities"),_=function(t){return t.replace(/\r?\n/g,"<br>")},T=function(t,e,n){var r=t.split(/\n\n/),o=function(t,e){var n,r=[],o="<"+t;if("object"==typeof e){for(n in e)e.hasOwnProperty(n)&&r.push(n+'="'+w.encodeAllRaw(e[n])+'"');r.length&&(o+=" "+r.join(" "))}return o+">"}(e,n),i="</"+e+">",a=y.map(r,function(t){return t.split(/\n/).join("<br />")});return 1===a.length?a[0]:y.map(a,function(t){return o+t+i}).join("")},D=function(t){return!/<(?:\/?(?!(?:div|p|br|span)>)\w+|(?:(?!(?:span style="white-space:\s?pre;?">)|br\s?\/>))\w+\s[^>]+)>/i.test(t)},C=function(t,e,n){return e?T(t,e,n):_(t)},k=tinymce.util.Tools.resolve("tinymce.html.DomParser"),S=tinymce.util.Tools.resolve("tinymce.html.Node"),O=tinymce.util.Tools.resolve("tinymce.html.Schema"),R=tinymce.util.Tools.resolve("tinymce.html.Serializer");function F(e,t){return y.each(t,function(t){e=t.constructor===RegExp?e.replace(t,""):e.replace(t[0],t[1])}),e}var E={filter:F,innerText:function(e){var n=O(),r=k({},n),o="",i=n.getShortEndedElements(),a=y.makeMap("script noscript style textarea video audio iframe object"," "),s=n.getBlockElements();return e=F(e,[/<!\[[^\]]+\]>/g]),function t(e){var n=e.name,r=e;if("br"!==n){if("wbr"!==n)if(i[n]&&(o+=" "),a[n])o+=" ";else{if(3===e.type&&(o+=e.value),!e.shortEnded&&(e=e.firstChild))for(;t(e),e=e.next;);s[n]&&r.next&&(o+="\n","p"===n&&(o+="\n"))}}else o+="\n"}(r.parse(e)),o},trimHtml:function(t){return t=F(t,[/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/gi,/<!--StartFragment-->|<!--EndFragment-->/g,[/( ?)<span class="Apple-converted-space">\u00a0<\/span>( ?)/g,function(t,e,n){return e||n?"\xa0":" "}],/<br class="Apple-interchange-newline">/g,/<br>$/i])},createIdGenerator:function(t){var e=0;return function(){return t+e++}},isMsEdge:function(){return-1!==v.navigator.userAgent.indexOf(" Edge/")}};function A(e){var n,t;return t=[/^[IVXLMCD]{1,2}\.[ \u00a0]/,/^[ivxlmcd]{1,2}\.[ \u00a0]/,/^[a-z]{1,2}[\.\)][ \u00a0]/,/^[A-Z]{1,2}[\.\)][ \u00a0]/,/^[0-9]+\.[ \u00a0]/,/^[\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d]+\.[ \u00a0]/,/^[\u58f1\u5f10\u53c2\u56db\u4f0d\u516d\u4e03\u516b\u4e5d\u62fe]+\.[ \u00a0]/],e=e.replace(/^[\u00a0 ]+/,""),y.each(t,function(t){if(t.test(e))return!(n=!0)}),n}function I(t){var i,a,s=1;function n(t){var e="";if(3===t.type)return t.value;if(t=t.firstChild)for(;e+=n(t),t=t.next;);return e}function u(t,e){if(3===t.type&&e.test(t.value))return t.value=t.value.replace(e,""),!1;if(t=t.firstChild)do{if(!u(t,e))return!1}while(t=t.next);return!0}function e(e,n,r){var o=e._listLevel||s;o!==s&&(o<s?i&&(i=i.parent.parent):(a=i,i=null)),i&&i.name===n?i.append(e):(a=a||i,i=new S(n,1),1<r&&i.attr("start",""+r),e.wrap(i)),e.name="li",s<o&&a&&a.lastChild.append(i),s=o,function t(e){if(e._listIgnore)e.remove();else if(e=e.firstChild)for(;t(e),e=e.next;);}(e),u(e,/^\u00a0+/),u(e,/^\s*([\u2022\u00b7\u00a7\u25CF]|\w+\.)/),u(e,/^\u00a0+/)}for(var r=[],o=t.firstChild;null!=o;)if(r.push(o),null!==(o=o.walk()))for(;void 0!==o&&o.parent!==t;)o=o.walk();for(var l=0;l<r.length;l++)if("p"===(t=r[l]).name&&t.firstChild){var c=n(t);if(/^[\s\u00a0]*[\u2022\u00b7\u00a7\u25CF]\s*/.test(c)){e(t,"ul");continue}if(A(c)){var f=/([0-9]+)\./.exec(c),d=1;f&&(d=parseInt(f[1],10)),e(t,"ol",d);continue}if(t._listLevel){e(t,"ul",1);continue}i=null}else a=i,i=null}function M(n,r,o,i){var a,s={},t=n.dom.parseStyle(i);return y.each(t,function(t,e){switch(e){case"mso-list":(a=/\w+ \w+([0-9]+)/i.exec(i))&&(o._listLevel=parseInt(a[1],10)),/Ignore/i.test(t)&&o.firstChild&&(o._listIgnore=!0,o.firstChild._listIgnore=!0);break;case"horiz-align":e="text-align";break;case"vert-align":e="vertical-align";break;case"font-color":case"mso-foreground":e="color";break;case"mso-background":case"mso-highlight":e="background";break;case"font-weight":case"font-style":return void("normal"!==t&&(s[e]=t));case"mso-element":if(/^(comment|comment-list)$/i.test(t))return void o.remove()}0!==e.indexOf("mso-comment")?0!==e.indexOf("mso-")&&("all"===m.getRetainStyleProps(n)||r&&r[e])&&(s[e]=t):o.remove()}),/(bold)/i.test(s["font-weight"])&&(delete s["font-weight"],o.wrap(new S("b",1))),/(italic)/i.test(s["font-style"])&&(delete s["font-style"],o.wrap(new S("i",1))),(s=n.dom.serializeStyle(s,o.name))||null}var B,H,j,L,N,$={preProcess:function(t,e){return m.shouldUseDefaultFilters(t)?function(r,t){var e,o;(e=m.getRetainStyleProps(r))&&(o=y.makeMap(e.split(/[, ]/))),t=E.filter(t,[/<br class="?Apple-interchange-newline"?>/gi,/<b[^>]+id="?docs-internal-[^>]*>/gi,/<!--[\s\S]+?-->/gi,/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,[/<(\/?)s>/gi,"<$1strike>"],[/ /gi,"\xa0"],[/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi,function(t,e){return 0<e.length?e.replace(/./," ").slice(Math.floor(e.length/2)).split("").join("\xa0"):""}]]);var n=m.getWordValidElements(r),i=O({valid_elements:n,valid_children:"-li[p]"});y.each(i.elements,function(t){t.attributes["class"]||(t.attributes["class"]={},t.attributesOrder.push("class")),t.attributes.style||(t.attributes.style={},t.attributesOrder.push("style"))});var a=k({},i);a.addAttributeFilter("style",function(t){for(var e,n=t.length;n--;)(e=t[n]).attr("style",M(r,o,e,e.attr("style"))),"span"===e.name&&e.parent&&!e.attributes.length&&e.unwrap()}),a.addAttributeFilter("class",function(t){for(var e,n,r=t.length;r--;)n=(e=t[r]).attr("class"),/^(MsoCommentReference|MsoCommentText|msoDel)$/i.test(n)&&e.remove(),e.attr("class",null)}),a.addNodeFilter("del",function(t){for(var e=t.length;e--;)t[e].remove()}),a.addNodeFilter("a",function(t){for(var e,n,r,o=t.length;o--;)if(n=(e=t[o]).attr("href"),r=e.attr("name"),n&&-1!==n.indexOf("#_msocom_"))e.remove();else if(n&&0===n.indexOf("file://")&&(n=n.split("#")[1])&&(n="#"+n),n||r){if(r&&!/^_?(?:toc|edn|ftn)/i.test(r)){e.unwrap();continue}e.attr({href:n,name:r})}else e.unwrap()});var s=a.parse(t);return m.shouldConvertWordFakeLists(r)&&I(s),t=R({validate:r.settings.validate},i).serialize(s)}(t,e):e},isWordContent:function(t){return/<font face="Times New Roman"|class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i.test(t)||/class="OutlineElement/.test(t)||/id="?docs\-internal\-guid\-/.test(t)}},W=function(t,e){return{content:t,cancelled:e}},z=function(t,e,n,r){var o,i,a,s,u,l,c=f(t,e,n,r);return t.hasEventListeners("PastePostProcess")&&!c.isDefaultPrevented()?(o=t,i=c.content,a=n,s=r,u=o.dom.create("div",{style:"display:none"},i),l=d(o,u,a,s),W(l.node.innerHTML,l.isDefaultPrevented())):W(c.content,c.isDefaultPrevented())},U=function(t,e,n){var r=$.isWordContent(e),o=r?$.preProcess(t,e):e;return z(t,o,n,r)},V=function(t,e){var n,r;return t.insertContent((n=e,r=t.dom.create("body",{},n),y.each(r.querySelectorAll("meta"),function(t){return t.parentNode.removeChild(t)}),r.innerHTML),{merge:m.shouldMergeFormats(t),paste:!0}),!0},q=function(t){return/^https?:\/\/[\w\?\-\/+=.&%@~#]+$/i.test(t)},K=function(t){return q(t)&&/.(gif|jpe?g|png)$/.test(t)},G=function(t,e,n){return!(!1!==t.selection.isCollapsed()||!q(e)||(o=e,i=n,(r=t).undoManager.extra(function(){i(r,o)},function(){r.execCommand("mceInsertLink",!1,o)}),0));var r,o,i},X=function(t,e,n){return!!K(e)&&(o=e,i=n,(r=t).undoManager.extra(function(){i(r,o)},function(){r.insertContent('<img src="'+o+'">')}),!0);var r,o,i},Y=function(t,e){var n,r;!1===m.isSmartPasteEnabled(t)?V(t,e):(n=t,r=e,y.each([G,X,V],function(t){return!0!==t(n,r,V)}))},Z=function(t){return function(){return t}},J=Z(!1),Q=Z(!0),tt=J,et=Q,nt=function(){return rt},rt=(L={fold:function(t,e){return t()},is:tt,isSome:tt,isNone:et,getOr:j=function(t){return t},getOrThunk:H=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:j,orThunk:H,map:nt,ap:nt,each:function(){},bind:nt,flatten:nt,exists:tt,forall:et,filter:nt,equals:B=function(t){return t.isNone()},equals_:B,toArray:function(){return[]},toString:Z("none()")},Object.freeze&&Object.freeze(L),L),ot=function(n){var t=function(){return n},e=function(){return o},r=function(t){return t(n)},o={fold:function(t,e){return e(n)},is:function(t){return n===t},isSome:et,isNone:tt,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:e,orThunk:e,map:function(t){return ot(t(n))},ap:function(t){return t.fold(nt,function(t){return ot(t(n))})},each:function(t){t(n)},bind:r,flatten:t,exists:r,forall:r,filter:function(t){return t(n)?o:rt},equals:function(t){return t.is(n)},equals_:function(t,e){return t.fold(tt,function(t){return e(n,t)})},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},it={some:ot,none:nt,from:function(t){return null===t||t===undefined?rt:ot(t)}},at=(N="function",function(t){return function(t){if(null===t)return"null";var e=typeof t;return"object"===e&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":e}(t)===N}),st=Array.prototype.slice,ut=function(t,e){for(var n=t.length,r=new Array(n),o=0;o<n;o++){var i=t[o];r[o]=e(i,o,t)}return r},lt=function(t,e){for(var n=0,r=t.length;n<r;n++)e(t[n],n,t)},ct=at(Array.from)?Array.from:function(t){return st.call(t)},ft=function(t){var n=it.none(),e=[],r=function(t){o()?a(t):e.push(t)},o=function(){return n.isSome()},i=function(t){lt(t,a)},a=function(e){n.each(function(t){v.setTimeout(function(){e(t)},0)})};return t(function(t){n=it.some(t),i(e),e=[]}),{get:r,map:function(n){return ft(function(e){r(function(t){e(n(t))})})},isReady:o}},dt={nu:ft,pure:function(e){return ft(function(t){t(e)})}},mt=function(e){var t=function(t){var r;e((r=t,function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=this;v.setTimeout(function(){r.apply(n,t)},0)}))},n=function(){return dt.nu(t)};return{map:function(r){return mt(function(n){t(function(t){var e=r(t);n(e)})})},bind:function(n){return mt(function(e){t(function(t){n(t).get(e)})})},anonBind:function(n){return mt(function(e){t(function(t){n.get(e)})})},toLazy:n,toCached:function(){var e=null;return mt(function(t){null===e&&(e=n()),e.get(t)})},get:t}},gt={nu:mt,pure:function(e){return mt(function(t){t(e)})}},pt=function(a,t){return t(function(r){var o=[],i=0;0===a.length?r([]):lt(a,function(t,e){var n;t.get((n=e,function(t){o[n]=t,++i>=a.length&&r(o)}))})})},vt=function(t,e){var n=ut(t,e);return pt(n,gt.nu)},ht=function(t,e,n){var r=n||x(e),o=U(t,b(e),r);!1===o.cancelled&&Y(t,o.content)},yt=function(t,e){e=t.dom.encode(e).replace(/\r\n/g,"\n"),e=C(e,t.settings.forced_root_block,t.settings.forced_root_block_attrs),ht(t,e,!1)},bt=function(t){var e={};if(t){if(t.getData){var n=t.getData("Text");n&&0<n.length&&-1===n.indexOf("data:text/mce-internal,")&&(e["text/plain"]=n)}if(t.types)for(var r=0;r<t.types.length;r++){var o=t.types[r];try{e[o]=t.getData(o)}catch(i){e[o]=""}}}return e},xt=function(t,e){return e in t&&0<t[e].length},Pt=function(t){return xt(t,"text/html")||xt(t,"text/plain")},wt=E.createIdGenerator("mceclip"),_t=function(e,t,n){var r,o,i,a,s="paste"===t.type?t.clipboardData:t.dataTransfer;if(e.settings.paste_data_images&&s){var u=(i=(o=s).items?ut(ct(o.items),function(t){return t.getAsFile()}):[],a=o.files?ct(o.files):[],function(t,e){for(var n=[],r=0,o=t.length;r<o;r++){var i=t[r];e(i,r,t)&&n.push(i)}return n}(0<i.length?i:a,function(t){return/^image\/(jpeg|png|gif|bmp)$/.test(t.type)}));if(0<u.length)return t.preventDefault(),(r=u,vt(r,function(r){return gt.nu(function(t){var e=r.getAsFile?r.getAsFile():r,n=new window.FileReader;n.onload=function(){t({blob:e,uri:n.result})},n.readAsDataURL(e)})})).get(function(t){n&&e.selection.setRng(n),lt(t,function(t){!function(t,e){var n,r,o,i,a,s,u,l=(n=e.uri,-1!==(r=n.indexOf(","))?n.substr(r+1):null),c=wt(),f=t.settings.images_reuse_filename&&e.blob.name?(o=t,i=e.blob.name,(a=i.match(/([\s\S]+?)\.(?:jpeg|jpg|png|gif)$/i))?o.dom.encode(a[1]):null):c,d=new v.Image;if(d.src=e.uri,s=t.settings,u=d,!s.images_dataimg_filter||s.images_dataimg_filter(u)){var m,g=t.editorUpload.blobCache,p=void 0;(m=g.findFirst(function(t){return t.base64()===l}))?p=m:(p=g.create(c,e.blob,l,f),g.add(p)),ht(t,'<img src="'+p.blobUri()+'">',!1)}else ht(t,'<img src="'+e.uri+'">',!1)}(e,t)})}),!0}return!1},Tt=function(t){return o.metaKeyPressed(t)&&86===t.keyCode||t.shiftKey&&45===t.keyCode},Dt=function(u,l,c){var e,f,d=(e=g(it.none()),{clear:function(){e.set(it.none())},set:function(t){e.set(it.some(t))},isSet:function(){return e.get().isSome()},on:function(t){e.get().each(t)}});function m(t,e,n,r){var o,i;xt(t,"text/html")?o=t["text/html"]:(o=l.getHtml(),r=r||x(o),l.isDefaultContent(o)&&(n=!0)),o=E.trimHtml(o),l.remove(),i=!1===r&&D(o),o.length&&!i||(n=!0),n&&(o=xt(t,"text/plain")&&i?t["text/plain"]:E.innerText(o)),l.isDefaultContent(o)?e||u.windowManager.alert("Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents."):n?yt(u,o):ht(u,o,r)}u.on("keydown",function(t){function e(t){Tt(t)&&!t.isDefaultPrevented()&&l.remove()}if(Tt(t)&&!t.isDefaultPrevented()){if((f=t.shiftKey&&86===t.keyCode)&&p.webkit&&-1!==v.navigator.userAgent.indexOf("Version/"))return;if(t.stopImmediatePropagation(),d.set(t),window.setTimeout(function(){d.clear()},100),p.ie&&f)return t.preventDefault(),void n(u,!0);l.remove(),l.create(),u.once("keyup",e),u.once("paste",function(){u.off("keyup",e)})}}),u.on("paste",function(t){var e,n,r,o=d.isSet(),i=(e=u,n=bt(t.clipboardData||e.getDoc().dataTransfer),E.isMsEdge()?y.extend(n,{"text/html":""}):n),a="text"===c.get()||f,s=xt(i,P());f=!1,t.isDefaultPrevented()||(r=t.clipboardData,-1!==v.navigator.userAgent.indexOf("Android")&&r&&r.items&&0===r.items.length)?l.remove():Pt(i)||!_t(u,t,l.getLastRng()||u.selection.getRng())?(o||t.preventDefault(),!p.ie||o&&!t.ieFake||xt(i,"text/html")||(l.create(),u.dom.bind(l.getEl(),"paste",function(t){t.stopPropagation()}),u.getDoc().execCommand("Paste",!1,null),i["text/html"]=l.getHtml()),xt(i,"text/html")?(t.preventDefault(),s||(s=x(i["text/html"])),m(i,o,a,s)):h.setEditorTimeout(u,function(){m(i,o,a,s)},0)):l.remove()})},Ct=function(t){return p.ie&&t.inline?v.document.body:t.getBody()},kt=function(e,t,n){var r;Ct(r=e)!==r.getBody()&&e.dom.bind(t,"paste keyup",function(t){Rt(e,n)||e.fire("paste")})},St=function(t){return t.dom.get("mcepastebin")},Ot=function(t,e){return e===t},Rt=function(t,e){var n,r=St(t);return(n=r)&&"mcepastebin"===n.id&&Ot(e,r.innerHTML)},Ft=function(a){var s=g(null),u="%MCEPASTEBIN%";return{create:function(){return e=s,n=u,o=(t=a).dom,i=t.getBody(),e.set(t.selection.getRng()),r=t.dom.add(Ct(t),"div",{id:"mcepastebin","class":"mce-pastebin",contentEditable:!0,"data-mce-bogus":"all",style:"position: fixed; top: 50%; width: 10px; height: 10px; overflow: hidden; opacity: 0"},n),(p.ie||p.gecko)&&o.setStyle(r,"left","rtl"===o.getStyle(i,"direction",!0)?65535:-65535),o.bind(r,"beforedeactivate focusin focusout",function(t){t.stopPropagation()}),kt(t,r,n),r.focus(),void t.selection.select(r,!0);var t,e,n,r,o,i},remove:function(){return function(t,e){if(St(t)){for(var n=void 0,r=e.get();n=t.dom.get("mcepastebin");)t.dom.remove(n),t.dom.unbind(n);r&&t.selection.setRng(r)}e.set(null)}(a,s)},getEl:function(){return St(a)},getHtml:function(){return function(n){var e,t,r,o,i,a=function(t,e){t.appendChild(e),n.dom.remove(e,!0)};for(t=y.grep(Ct(n).childNodes,function(t){return"mcepastebin"===t.id}),e=t.shift(),y.each(t,function(t){a(e,t)}),r=(o=n.dom.select("div[id=mcepastebin]",e)).length-1;0<=r;r--)i=n.dom.create("div"),e.insertBefore(i,o[r]),a(i,o[r]);return e?e.innerHTML:""}(a)},getLastRng:function(){return s.get()},isDefault:function(){return Rt(a,u)},isDefaultContent:function(t){return Ot(u,t)}}},Et=function(n,t){var e=Ft(n);return n.on("preInit",function(){return Dt(a=n,e,t),void a.parser.addNodeFilter("img",function(t,e,n){var r,o=function(t){t.attr("data-mce-object")||s===p.transparentSrc||t.remove()};if(!a.settings.paste_data_images&&(r=n).data&&!0===r.data.paste)for(var i=t.length;i--;)(s=t[i].attributes.map.src)&&(0===s.indexOf("webkit-fake-url")?o(t[i]):a.settings.allow_html_data_urls||0!==s.indexOf("data:")||o(t[i]))});var a,s}),{pasteFormat:t,pasteHtml:function(t,e){return ht(n,t,e)},pasteText:function(t){return yt(n,t)},pasteImageData:function(t,e){return _t(n,t,e)},getDataTransferItems:bt,hasHtmlOrText:Pt,hasContentType:xt}},At=function(){},It=function(t,e,n){if(r=t,!1!==p.iOS||r===undefined||"function"!=typeof r.setData||!0===E.isMsEdge())return!1;try{return t.clearData(),t.setData("text/html",e),t.setData("text/plain",n),t.setData(P(),e),!0}catch(o){return!1}var r},Mt=function(t,e,n,r){It(t.clipboardData,e.html,e.text)?(t.preventDefault(),r()):n(e.html,r)},Bt=function(s){return function(t,e){var n=c(t),r=s.dom.create("div",{contenteditable:"false","data-mce-bogus":"all"}),o=s.dom.create("div",{contenteditable:"true"},n);s.dom.setStyles(r,{position:"fixed",top:"0",left:"-3000px",width:"1000px",overflow:"hidden"}),r.appendChild(o),s.dom.add(s.getBody(),r);var i=s.selection.getRng();o.focus();var a=s.dom.createRng();a.selectNodeContents(o),s.selection.setRng(a),setTimeout(function(){s.selection.setRng(i),r.parentNode.removeChild(r),e()},0)}},Ht=function(t){return{html:t.selection.getContent({contextual:!0}),text:t.selection.getContent({format:"text"})}},jt=function(t){return!t.selection.isCollapsed()||!!(e=t).dom.getParent(e.selection.getStart(),"td[data-mce-selected],th[data-mce-selected]",e.getBody());var e},Lt=function(t){var e,n;t.on("cut",(e=t,function(t){jt(e)&&Mt(t,Ht(e),Bt(e),function(){setTimeout(function(){e.execCommand("Delete")},0)})})),t.on("copy",(n=t,function(t){jt(n)&&Mt(t,Ht(n),Bt(n),At)}))},Nt=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),$t=function(t,e){return Nt.getCaretRangeFromPoint(e.clientX,e.clientY,t.getDoc())},Wt=function(t,e){t.focus(),t.selection.setRng(e)},zt=function(a,s,u){m.shouldBlockDrop(a)&&a.on("dragend dragover draggesture dragdrop drop drag",function(t){t.preventDefault(),t.stopPropagation()}),m.shouldPasteDataImages(a)||a.on("drop",function(t){var e=t.dataTransfer;e&&e.files&&0<e.files.length&&t.preventDefault()}),a.on("drop",function(t){var e,n;if(n=$t(a,t),!t.isDefaultPrevented()&&!u.get()){e=s.getDataTransferItems(t.dataTransfer);var r,o=s.hasContentType(e,P());if((s.hasHtmlOrText(e)&&(!(r=e["text/plain"])||0!==r.indexOf("file://"))||!s.pasteImageData(t,n))&&n&&m.shouldFilterDrop(a)){var i=e["mce-internal"]||e["text/html"]||e["text/plain"];i&&(t.preventDefault(),h.setEditorTimeout(a,function(){a.undoManager.transact(function(){e["mce-internal"]&&a.execCommand("Delete"),Wt(a,n),i=E.trimHtml(i),e["text/html"]?s.pasteHtml(i,o):s.pasteText(i)})}))}}}),a.on("dragstart",function(t){u.set(!0)}),a.on("dragover dragend",function(t){m.shouldPasteDataImages(a)&&!1===u.get()&&(t.preventDefault(),Wt(a,$t(a,t))),"dragend"===t.type&&u.set(!1)})},Ut=function(t){var e=t.plugins.paste,n=m.getPreProcess(t);n&&t.on("PastePreProcess",function(t){n.call(e,e,t)});var r=m.getPostProcess(t);r&&t.on("PastePostProcess",function(t){r.call(e,e,t)})};function Vt(e,n){e.on("PastePreProcess",function(t){t.content=n(e,t.content,t.internal,t.wordContent)})}function qt(t,e){if(!$.isWordContent(e))return e;var n=[];y.each(t.schema.getBlockElements(),function(t,e){n.push(e)});var r=new RegExp("(?:<br> [\\s\\r\\n]+|<br>)*(<\\/?("+n.join("|")+")[^>]*>)(?:<br> [\\s\\r\\n]+|<br>)*","g");return e=E.filter(e,[[r,"$1"]]),e=E.filter(e,[[/<br><br>/g,"<BR><BR>"],[/<br>/g," "],[/<BR><BR>/g,"<br>"]])}function Kt(t,e,n,r){if(r||n)return e;var l,o=m.getWebkitStyles(t);if(!1===m.shouldRemoveWebKitStyles(t)||"all"===o)return e;if(o&&(l=o.split(/[, ]/)),l){var c=t.dom,f=t.selection.getNode();e=e.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi,function(t,e,n,r){var o=c.parseStyle(c.decode(n)),i={};if("none"===l)return e+r;for(var a=0;a<l.length;a++){var s=o[l[a]],u=c.getStyle(f,l[a],!0);/color/.test(l[a])&&(s=c.toHex(s),u=c.toHex(u)),u!==s&&(i[l[a]]=s)}return(i=c.serializeStyle(i,"span"))?e+' style="'+i+'"'+r:e+r})}else e=e.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi,"$1$3");return e=e.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi,function(t,e,n,r){return e+' style="'+n+'"'+r})}function Gt(n,t){n.$("a",t).find("font,u").each(function(t,e){n.dom.remove(e,!0)})}var Xt=function(t){var e,n;p.webkit&&Vt(t,Kt),p.ie&&(Vt(t,qt),n=Gt,(e=t).on("PastePostProcess",function(t){n(e,t.node)}))},Yt=function(t,e,n){var r=n.control;r.active("text"===e.pasteFormat.get()),t.on("PastePlainTextToggle",function(t){r.active(t.state)})},Zt=function(t,e){var n=function(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}(Yt,t,e);t.addButton("pastetext",{active:!1,icon:"pastetext",tooltip:"Paste as text",cmd:"mceTogglePlainTextPaste",onPostRender:n}),t.addMenuItem("pastetext",{text:"Paste as text",selectable:!0,active:e.pasteFormat,cmd:"mceTogglePlainTextPaste",onPostRender:n})};e.add("paste",function(t){if(!1===a(t)){var e=g(!1),n=g(!1),r=g(m.isPasteAsTextEnabled(t)?"text":"html"),o=Et(t,r),i=Xt(t);return Zt(t,o),l(t,o,e),Ut(t),Lt(t),zt(t,o,n),s(o,i)}})}(window); \ No newline at end of file +!function(v){"use strict";var p=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return p(n())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=function(t){return!(!/(^|[ ,])powerpaste([, ]|$)/.test(t.settings.plugins)||!e.get("powerpaste")||("undefined"!=typeof v.window.console&&v.window.console.log&&v.window.console.log("PowerPaste is incompatible with Paste plugin! Remove 'paste' from the 'plugins' option."),0))},u=function(t,e){return{clipboard:t,quirks:e}},d=function(t,e,n,r){return t.fire("PastePreProcess",{content:e,internal:n,wordContent:r})},m=function(t,e,n,r){return t.fire("PastePostProcess",{node:e,internal:n,wordContent:r})},s=function(t,e){return t.fire("PastePlainTextToggle",{state:e})},n=function(t,e){return t.fire("paste",{ieFake:e})},g={shouldPlainTextInform:function(t){return t.getParam("paste_plaintext_inform",!0)},shouldBlockDrop:function(t){return t.getParam("paste_block_drop",!1)},shouldPasteDataImages:function(t){return t.getParam("paste_data_images",!1)},shouldFilterDrop:function(t){return t.getParam("paste_filter_drop",!0)},getPreProcess:function(t){return t.getParam("paste_preprocess")},getPostProcess:function(t){return t.getParam("paste_postprocess")},getWebkitStyles:function(t){return t.getParam("paste_webkit_styles")},shouldRemoveWebKitStyles:function(t){return t.getParam("paste_remove_styles_if_webkit",!0)},shouldMergeFormats:function(t){return t.getParam("paste_merge_formats",!0)},isSmartPasteEnabled:function(t){return t.getParam("smart_paste",!0)},isPasteAsTextEnabled:function(t){return t.getParam("paste_as_text",!1)},getRetainStyleProps:function(t){return t.getParam("paste_retain_style_properties")},getWordValidElements:function(t){return t.getParam("paste_word_valid_elements","-strong/b,-em/i,-u,-span,-p,-ol,-ul,-li,-h1,-h2,-h3,-h4,-h5,-h6,-p/div,-a[href|name],sub,sup,strike,br,del,table[width],tr,td[colspan|rowspan|width],th[colspan|rowspan|width],thead,tfoot,tbody")},shouldConvertWordFakeLists:function(t){return t.getParam("paste_convert_word_fake_lists",!0)},shouldUseDefaultFilters:function(t){return t.getParam("paste_enable_default_filters",!0)}},r=function(t,e,n){var r,o,i;"text"===e.pasteFormat.get()?(e.pasteFormat.set("html"),s(t,!1)):(e.pasteFormat.set("text"),s(t,!0),i=t,!1===n.get()&&g.shouldPlainTextInform(i)&&(o="Paste is now in plain text mode. Contents will now be pasted as plain text until you toggle this option off.",(r=t).notificationManager.open({text:r.translate(o),type:"info"}),n.set(!0))),t.focus()},c=function(t,n,e){t.addCommand("mceTogglePlainTextPaste",function(){r(t,n,e)}),t.addCommand("mceInsertClipboardContent",function(t,e){e.content&&n.pasteHtml(e.content,e.internal),e.text&&n.pasteText(e.text)})},h=tinymce.util.Tools.resolve("tinymce.Env"),y=tinymce.util.Tools.resolve("tinymce.util.Delay"),b=tinymce.util.Tools.resolve("tinymce.util.Tools"),o=tinymce.util.Tools.resolve("tinymce.util.VK"),t="x-tinymce/html",i="\x3c!-- "+t+" --\x3e",l=function(t){return i+t},f=function(t){return t.replace(i,"")},w=function(t){return-1!==t.indexOf(i)},x=function(){return t},_=tinymce.util.Tools.resolve("tinymce.html.Entities"),P=function(t){return t.replace(/\r?\n/g,"<br>")},T=function(t,e,n){var r=t.split(/\n\n/),o=function(t,e){var n,r=[],o="<"+t;if("object"==typeof e){for(n in e)e.hasOwnProperty(n)&&r.push(n+'="'+_.encodeAllRaw(e[n])+'"');r.length&&(o+=" "+r.join(" "))}return o+">"}(e,n),i="</"+e+">",a=b.map(r,function(t){return t.split(/\n/).join("<br />")});return 1===a.length?a[0]:b.map(a,function(t){return o+t+i}).join("")},D=function(t){return!/<(?:\/?(?!(?:div|p|br|span)>)\w+|(?:(?!(?:span style="white-space:\s?pre;?">)|br\s?\/>))\w+\s[^>]+)>/i.test(t)},C=function(t,e,n){return e?T(t,e,n):P(t)},k=tinymce.util.Tools.resolve("tinymce.html.DomParser"),F=tinymce.util.Tools.resolve("tinymce.html.Serializer"),E=tinymce.util.Tools.resolve("tinymce.html.Node"),R=tinymce.util.Tools.resolve("tinymce.html.Schema");function I(e,t){return b.each(t,function(t){e=t.constructor===RegExp?e.replace(t,""):e.replace(t[0],t[1])}),e}var O={filter:I,innerText:function(e){var n=R(),r=k({},n),o="",i=n.getShortEndedElements(),a=b.makeMap("script noscript style textarea video audio iframe object"," "),u=n.getBlockElements();return e=I(e,[/<!\[[^\]]+\]>/g]),function t(e){var n=e.name,r=e;if("br"!==n){if("wbr"!==n)if(i[n]&&(o+=" "),a[n])o+=" ";else{if(3===e.type&&(o+=e.value),!e.shortEnded&&(e=e.firstChild))for(;t(e),e=e.next;);u[n]&&r.next&&(o+="\n","p"===n&&(o+="\n"))}}else o+="\n"}(r.parse(e)),o},trimHtml:function(t){return t=I(t,[/^[\s\S]*<body[^>]*>\s*|\s*<\/body[^>]*>[\s\S]*$/gi,/<!--StartFragment-->|<!--EndFragment-->/g,[/( ?)<span class="Apple-converted-space">\u00a0<\/span>( ?)/g,function(t,e,n){return e||n?"\xa0":" "}],/<br class="Apple-interchange-newline">/g,/<br>$/i])},createIdGenerator:function(t){var e=0;return function(){return t+e++}},isMsEdge:function(){return-1!==v.navigator.userAgent.indexOf(" Edge/")}};function S(e){var n,t;return t=[/^[IVXLMCD]{1,2}\.[ \u00a0]/,/^[ivxlmcd]{1,2}\.[ \u00a0]/,/^[a-z]{1,2}[\.\)][ \u00a0]/,/^[A-Z]{1,2}[\.\)][ \u00a0]/,/^[0-9]+\.[ \u00a0]/,/^[\u3007\u4e00\u4e8c\u4e09\u56db\u4e94\u516d\u4e03\u516b\u4e5d]+\.[ \u00a0]/,/^[\u58f1\u5f10\u53c2\u56db\u4f0d\u516d\u4e03\u516b\u4e5d\u62fe]+\.[ \u00a0]/],e=e.replace(/^[\u00a0 ]+/,""),b.each(t,function(t){if(t.test(e))return!(n=!0)}),n}function A(t){var i,a,u=1;function n(t){var e="";if(3===t.type)return t.value;if(t=t.firstChild)for(;e+=n(t),t=t.next;);return e}function s(t,e){if(3===t.type&&e.test(t.value))return t.value=t.value.replace(e,""),!1;if(t=t.firstChild)do{if(!s(t,e))return!1}while(t=t.next);return!0}function e(e,n,r){var o=e._listLevel||u;o!==u&&(o<u?i&&(i=i.parent.parent):(a=i,i=null)),i&&i.name===n?i.append(e):(a=a||i,i=new E(n,1),1<r&&i.attr("start",""+r),e.wrap(i)),e.name="li",u<o&&a&&a.lastChild.append(i),u=o,function t(e){if(e._listIgnore)e.remove();else if(e=e.firstChild)for(;t(e),e=e.next;);}(e),s(e,/^\u00a0+/),s(e,/^\s*([\u2022\u00b7\u00a7\u25CF]|\w+\.)/),s(e,/^\u00a0+/)}for(var r=[],o=t.firstChild;null!=o;)if(r.push(o),null!==(o=o.walk()))for(;void 0!==o&&o.parent!==t;)o=o.walk();for(var c=0;c<r.length;c++)if("p"===(t=r[c]).name&&t.firstChild){var l=n(t);if(/^[\s\u00a0]*[\u2022\u00b7\u00a7\u25CF]\s*/.test(l)){e(t,"ul");continue}if(S(l)){var f=/([0-9]+)\./.exec(l),d=1;f&&(d=parseInt(f[1],10)),e(t,"ol",d);continue}if(t._listLevel){e(t,"ul",1);continue}i=null}else a=i,i=null}function j(n,r,o,i){var a,u={},t=n.dom.parseStyle(i);return b.each(t,function(t,e){switch(e){case"mso-list":(a=/\w+ \w+([0-9]+)/i.exec(i))&&(o._listLevel=parseInt(a[1],10)),/Ignore/i.test(t)&&o.firstChild&&(o._listIgnore=!0,o.firstChild._listIgnore=!0);break;case"horiz-align":e="text-align";break;case"vert-align":e="vertical-align";break;case"font-color":case"mso-foreground":e="color";break;case"mso-background":case"mso-highlight":e="background";break;case"font-weight":case"font-style":return void("normal"!==t&&(u[e]=t));case"mso-element":if(/^(comment|comment-list)$/i.test(t))return void o.remove()}0!==e.indexOf("mso-comment")?0!==e.indexOf("mso-")&&("all"===g.getRetainStyleProps(n)||r&&r[e])&&(u[e]=t):o.remove()}),/(bold)/i.test(u["font-weight"])&&(delete u["font-weight"],o.wrap(new E("b",1))),/(italic)/i.test(u["font-style"])&&(delete u["font-style"],o.wrap(new E("i",1))),(u=n.dom.serializeStyle(u,o.name))||null}var M,L,N,B,H,$,W,U,z,V={preProcess:function(t,e){return g.shouldUseDefaultFilters(t)?function(r,t){var e,o;(e=g.getRetainStyleProps(r))&&(o=b.makeMap(e.split(/[, ]/))),t=O.filter(t,[/<br class="?Apple-interchange-newline"?>/gi,/<b[^>]+id="?docs-internal-[^>]*>/gi,/<!--[\s\S]+?-->/gi,/<(!|script[^>]*>.*?<\/script(?=[>\s])|\/?(\?xml(:\w+)?|img|meta|link|style|\w:\w+)(?=[\s\/>]))[^>]*>/gi,[/<(\/?)s>/gi,"<$1strike>"],[/ /gi,"\xa0"],[/<span\s+style\s*=\s*"\s*mso-spacerun\s*:\s*yes\s*;?\s*"\s*>([\s\u00a0]*)<\/span>/gi,function(t,e){return 0<e.length?e.replace(/./," ").slice(Math.floor(e.length/2)).split("").join("\xa0"):""}]]);var n=g.getWordValidElements(r),i=R({valid_elements:n,valid_children:"-li[p]"});b.each(i.elements,function(t){t.attributes["class"]||(t.attributes["class"]={},t.attributesOrder.push("class")),t.attributes.style||(t.attributes.style={},t.attributesOrder.push("style"))});var a=k({},i);a.addAttributeFilter("style",function(t){for(var e,n=t.length;n--;)(e=t[n]).attr("style",j(r,o,e,e.attr("style"))),"span"===e.name&&e.parent&&!e.attributes.length&&e.unwrap()}),a.addAttributeFilter("class",function(t){for(var e,n,r=t.length;r--;)n=(e=t[r]).attr("class"),/^(MsoCommentReference|MsoCommentText|msoDel)$/i.test(n)&&e.remove(),e.attr("class",null)}),a.addNodeFilter("del",function(t){for(var e=t.length;e--;)t[e].remove()}),a.addNodeFilter("a",function(t){for(var e,n,r,o=t.length;o--;)if(n=(e=t[o]).attr("href"),r=e.attr("name"),n&&-1!==n.indexOf("#_msocom_"))e.remove();else if(n&&0===n.indexOf("file://")&&(n=n.split("#")[1])&&(n="#"+n),n||r){if(r&&!/^_?(?:toc|edn|ftn)/i.test(r)){e.unwrap();continue}e.attr({href:n,name:r})}else e.unwrap()});var u=a.parse(t);return g.shouldConvertWordFakeLists(r)&&A(u),t=F({validate:r.settings.validate},i).serialize(u)}(t,e):e},isWordContent:function(t){return/<font face="Times New Roman"|class="?Mso|style="[^"]*\bmso-|style='[^'']*\bmso-|w:WordDocument/i.test(t)||/class="OutlineElement/.test(t)||/id="?docs\-internal\-guid\-/.test(t)}},K=function(t,e){return{content:t,cancelled:e}},q=function(t,e,n,r){var o,i,a,u,s,c,l=d(t,e,n,r),f=function(t,e){var n=k({},t.schema);n.addNodeFilter("meta",function(t){b.each(t,function(t){return t.remove()})});var r=n.parse(e,{forced_root_block:!1,isRootContent:!0});return F({validate:t.settings.validate},t.schema).serialize(r)}(t,l.content);return t.hasEventListeners("PastePostProcess")&&!l.isDefaultPrevented()?(i=f,a=n,u=r,s=(o=t).dom.create("div",{style:"display:none"},i),c=m(o,s,a,u),K(c.node.innerHTML,c.isDefaultPrevented())):K(f,l.isDefaultPrevented())},G=function(t,e,n){var r=V.isWordContent(e),o=r?V.preProcess(t,e):e;return q(t,o,n,r)},X=function(t,e){return t.insertContent(e,{merge:g.shouldMergeFormats(t),paste:!0}),!0},Y=function(t){return/^https?:\/\/[\w\?\-\/+=.&%@~#]+$/i.test(t)},Z=function(t){return Y(t)&&/.(gif|jpe?g|png)$/.test(t)},J=function(t,e,n){return!(!1!==t.selection.isCollapsed()||!Y(e)||(o=e,i=n,(r=t).undoManager.extra(function(){i(r,o)},function(){r.execCommand("mceInsertLink",!1,o)}),0));var r,o,i},Q=function(t,e,n){return!!Z(e)&&(o=e,i=n,(r=t).undoManager.extra(function(){i(r,o)},function(){r.insertContent('<img src="'+o+'">')}),!0);var r,o,i},tt=function(t,e){var n,r;!1===g.isSmartPasteEnabled(t)?X(t,e):(n=t,r=e,b.each([J,Q,X],function(t){return!0!==t(n,r,X)}))},et=function(){},nt=function(t){return function(){return t}},rt=nt(!1),ot=nt(!0),it=function(){return at},at=(M=function(t){return t.isNone()},B={fold:function(t,e){return t()},is:rt,isSome:rt,isNone:ot,getOr:N=function(t){return t},getOrThunk:L=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:nt(null),getOrUndefined:nt(undefined),or:N,orThunk:L,map:it,each:et,bind:it,exists:rt,forall:ot,filter:it,equals:M,equals_:M,toArray:function(){return[]},toString:nt("none()")},Object.freeze&&Object.freeze(B),B),ut=function(n){var t=nt(n),e=function(){return o},r=function(t){return t(n)},o={fold:function(t,e){return e(n)},is:function(t){return n===t},isSome:ot,isNone:rt,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:e,orThunk:e,map:function(t){return ut(t(n))},each:function(t){t(n)},bind:r,exists:r,forall:r,filter:function(t){return t(n)?o:at},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(t){return t.is(n)},equals_:function(t,e){return t.fold(rt,function(t){return e(n,t)})}};return o},st={some:ut,none:it,from:function(t){return null===t||t===undefined?at:ut(t)}},ct=(H="function",function(t){return function(t){if(null===t)return"null";var e=typeof t;return"object"===e&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":e}(t)===H}),lt=Array.prototype.slice,ft=function(t,e){for(var n=t.length,r=new Array(n),o=0;o<n;o++){var i=t[o];r[o]=e(i,o)}return r},dt=function(t,e){for(var n=0,r=t.length;n<r;n++)e(t[n],n)},mt=ct(Array.from)?Array.from:function(t){return lt.call(t)},pt={},gt={exports:pt};$=undefined,W=pt,U=gt,z=undefined,function(t){"object"==typeof W&&void 0!==U?U.exports=t():"function"==typeof $&&$.amd?$([],t):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).EphoxContactWrapper=t()}(function(){return function i(a,u,s){function c(e,t){if(!u[e]){if(!a[e]){var n="function"==typeof z&&z;if(!t&&n)return n(e,!0);if(l)return l(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var o=u[e]={exports:{}};a[e][0].call(o.exports,function(t){return c(a[e][1][t]||t)},o,o.exports,i,a,u,s)}return u[e].exports}for(var l="function"==typeof z&&z,t=0;t<s.length;t++)c(s[t]);return c}({1:[function(t,e,n){var r,o,i=e.exports={};function a(){throw new Error("setTimeout has not been defined")}function u(){throw new Error("clearTimeout has not been defined")}function s(t){if(r===setTimeout)return setTimeout(t,0);if((r===a||!r)&&setTimeout)return r=setTimeout,setTimeout(t,0);try{return r(t,0)}catch(e){try{return r.call(null,t,0)}catch(e){return r.call(this,t,0)}}}!function(){try{r="function"==typeof setTimeout?setTimeout:a}catch(t){r=a}try{o="function"==typeof clearTimeout?clearTimeout:u}catch(t){o=u}}();var c,l=[],f=!1,d=-1;function m(){f&&c&&(f=!1,c.length?l=c.concat(l):d=-1,l.length&&p())}function p(){if(!f){var t=s(m);f=!0;for(var e=l.length;e;){for(c=l,l=[];++d<e;)c&&c[d].run();d=-1,e=l.length}c=null,f=!1,function(t){if(o===clearTimeout)return clearTimeout(t);if((o===u||!o)&&clearTimeout)return o=clearTimeout,clearTimeout(t);try{o(t)}catch(e){try{return o.call(null,t)}catch(e){return o.call(this,t)}}}(t)}}function g(t,e){this.fun=t,this.array=e}function v(){}i.nextTick=function(t){var e=new Array(arguments.length-1);if(1<arguments.length)for(var n=1;n<arguments.length;n++)e[n-1]=arguments[n];l.push(new g(t,e)),1!==l.length||f||s(p)},g.prototype.run=function(){this.fun.apply(null,this.array)},i.title="browser",i.browser=!0,i.env={},i.argv=[],i.version="",i.versions={},i.on=v,i.addListener=v,i.once=v,i.off=v,i.removeListener=v,i.removeAllListeners=v,i.emit=v,i.prependListener=v,i.prependOnceListener=v,i.listeners=function(t){return[]},i.binding=function(t){throw new Error("process.binding is not supported")},i.cwd=function(){return"/"},i.chdir=function(t){throw new Error("process.chdir is not supported")},i.umask=function(){return 0}},{}],2:[function(t,f,e){(function(n){!function(t){var e=setTimeout;function r(){}function a(t){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof t)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],l(t,this)}function o(r,o){for(;3===r._state;)r=r._value;0!==r._state?(r._handled=!0,a._immediateFn(function(){var t=1===r._state?o.onFulfilled:o.onRejected;if(null!==t){var e;try{e=t(r._value)}catch(n){return void u(o.promise,n)}i(o.promise,e)}else(1===r._state?i:u)(o.promise,r._value)})):r._deferreds.push(o)}function i(t,e){try{if(e===t)throw new TypeError("A promise cannot be resolved with itself.");if(e&&("object"==typeof e||"function"==typeof e)){var n=e.then;if(e instanceof a)return t._state=3,t._value=e,void s(t);if("function"==typeof n)return void l((r=n,o=e,function(){r.apply(o,arguments)}),t)}t._state=1,t._value=e,s(t)}catch(i){u(t,i)}var r,o}function u(t,e){t._state=2,t._value=e,s(t)}function s(t){2===t._state&&0===t._deferreds.length&&a._immediateFn(function(){t._handled||a._unhandledRejectionFn(t._value)});for(var e=0,n=t._deferreds.length;e<n;e++)o(t,t._deferreds[e]);t._deferreds=null}function c(t,e,n){this.onFulfilled="function"==typeof t?t:null,this.onRejected="function"==typeof e?e:null,this.promise=n}function l(t,e){var n=!1;try{t(function(t){n||(n=!0,i(e,t))},function(t){n||(n=!0,u(e,t))})}catch(r){if(n)return;n=!0,u(e,r)}}a.prototype["catch"]=function(t){return this.then(null,t)},a.prototype.then=function(t,e){var n=new this.constructor(r);return o(this,new c(t,e,n)),n},a.all=function(t){var s=Array.prototype.slice.call(t);return new a(function(o,i){if(0===s.length)return o([]);var a=s.length;function u(e,t){try{if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if("function"==typeof n)return void n.call(t,function(t){u(e,t)},i)}s[e]=t,0==--a&&o(s)}catch(r){i(r)}}for(var t=0;t<s.length;t++)u(t,s[t])})},a.resolve=function(e){return e&&"object"==typeof e&&e.constructor===a?e:new a(function(t){t(e)})},a.reject=function(n){return new a(function(t,e){e(n)})},a.race=function(o){return new a(function(t,e){for(var n=0,r=o.length;n<r;n++)o[n].then(t,e)})},a._immediateFn="function"==typeof n?function(t){n(t)}:function(t){e(t,0)},a._unhandledRejectionFn=function(t){"undefined"!=typeof console&&console&&console.warn("Possible Unhandled Promise Rejection:",t)},a._setImmediateFn=function(t){a._immediateFn=t},a._setUnhandledRejectionFn=function(t){a._unhandledRejectionFn=t},void 0!==f&&f.exports?f.exports=a:t.Promise||(t.Promise=a)}(this)}).call(this,t("timers").setImmediate)},{timers:3}],3:[function(s,t,c){(function(t,e){var r=s("process/browser.js").nextTick,n=Function.prototype.apply,o=Array.prototype.slice,i={},a=0;function u(t,e){this._id=t,this._clearFn=e}c.setTimeout=function(){return new u(n.call(setTimeout,window,arguments),clearTimeout)},c.setInterval=function(){return new u(n.call(setInterval,window,arguments),clearInterval)},c.clearTimeout=c.clearInterval=function(t){t.close()},u.prototype.unref=u.prototype.ref=function(){},u.prototype.close=function(){this._clearFn.call(window,this._id)},c.enroll=function(t,e){clearTimeout(t._idleTimeoutId),t._idleTimeout=e},c.unenroll=function(t){clearTimeout(t._idleTimeoutId),t._idleTimeout=-1},c._unrefActive=c.active=function(t){clearTimeout(t._idleTimeoutId);var e=t._idleTimeout;0<=e&&(t._idleTimeoutId=setTimeout(function(){t._onTimeout&&t._onTimeout()},e))},c.setImmediate="function"==typeof t?t:function(t){var e=a++,n=!(arguments.length<2)&&o.call(arguments,1);return i[e]=!0,r(function(){i[e]&&(n?t.apply(null,n):t.call(null),c.clearImmediate(e))}),e},c.clearImmediate="function"==typeof e?e:function(t){delete i[t]}}).call(this,s("timers").setImmediate,s("timers").clearImmediate)},{"process/browser.js":1,timers:3}],4:[function(t,e,n){var r=t("promise-polyfill"),o="undefined"!=typeof window?window:Function("return this;")();e.exports={boltExport:o.Promise||r}},{"promise-polyfill":2}]},{},[4])(4)});var vt=gt.exports.boltExport,ht=function(t){var n=st.none(),e=[],r=function(t){o()?a(t):e.push(t)},o=function(){return n.isSome()},i=function(t){dt(t,a)},a=function(e){n.each(function(t){v.setTimeout(function(){e(t)},0)})};return t(function(t){n=st.some(t),i(e),e=[]}),{get:r,map:function(n){return ht(function(e){r(function(t){e(n(t))})})},isReady:o}},yt={nu:ht,pure:function(e){return ht(function(t){t(e)})}},bt=function(t){v.setTimeout(function(){throw t},0)},wt=function(n){var t=function(t){n().then(t,bt)};return{map:function(t){return wt(function(){return n().then(t)})},bind:function(e){return wt(function(){return n().then(function(t){return e(t).toPromise()})})},anonBind:function(t){return wt(function(){return n().then(function(){return t.toPromise()})})},toLazy:function(){return yt.nu(t)},toCached:function(){var t=null;return wt(function(){return null===t&&(t=n()),t})},toPromise:n,get:t}},xt=function(t){return wt(function(){return new vt(t)})},_t=function(a,t){return t(function(r){var o=[],i=0;0===a.length?r([]):dt(a,function(t,e){var n;t.get((n=e,function(t){o[n]=t,++i>=a.length&&r(o)}))})})},Pt=function(t,e){return n=ft(t,e),_t(n,xt);var n},Tt=function(t,e,n){var r=n||w(e),o=G(t,f(e),r);!1===o.cancelled&&tt(t,o.content)},Dt=function(t,e){e=t.dom.encode(e).replace(/\r\n/g,"\n"),e=C(e,t.settings.forced_root_block,t.settings.forced_root_block_attrs),Tt(t,e,!1)},Ct=function(t){var e={};if(t){if(t.getData){var n=t.getData("Text");n&&0<n.length&&-1===n.indexOf("data:text/mce-internal,")&&(e["text/plain"]=n)}if(t.types)for(var r=0;r<t.types.length;r++){var o=t.types[r];try{e[o]=t.getData(o)}catch(i){e[o]=""}}}return e},kt=function(t,e){return e in t&&0<t[e].length},Ft=function(t){return kt(t,"text/html")||kt(t,"text/plain")},Et=O.createIdGenerator("mceclip"),Rt=function(e,t,n){var r,o,i,a,u="paste"===t.type?t.clipboardData:t.dataTransfer;if(e.settings.paste_data_images&&u){var s=(i=(o=u).items?ft(mt(o.items),function(t){return t.getAsFile()}):[],a=o.files?mt(o.files):[],function(t,e){for(var n=[],r=0,o=t.length;r<o;r++){var i=t[r];e(i,r)&&n.push(i)}return n}(0<i.length?i:a,function(t){return/^image\/(jpeg|png|gif|bmp)$/.test(t.type)}));if(0<s.length)return t.preventDefault(),(r=s,Pt(r,function(r){return xt(function(t){var e=r.getAsFile?r.getAsFile():r,n=new window.FileReader;n.onload=function(){t({blob:e,uri:n.result})},n.readAsDataURL(e)})})).get(function(t){n&&e.selection.setRng(n),dt(t,function(t){!function(t,e){var n,r,o,i,a,u,s,c=(n=e.uri,-1!==(r=n.indexOf(","))?n.substr(r+1):null),l=Et(),f=t.settings.images_reuse_filename&&e.blob.name?(o=t,i=e.blob.name,(a=i.match(/([\s\S]+?)\.(?:jpeg|jpg|png|gif)$/i))?o.dom.encode(a[1]):null):l,d=new v.Image;if(d.src=e.uri,u=t.settings,s=d,!u.images_dataimg_filter||u.images_dataimg_filter(s)){var m,p=t.editorUpload.blobCache,g=void 0;(m=p.findFirst(function(t){return t.base64()===c}))?g=m:(g=p.create(l,e.blob,c,f),p.add(g)),Tt(t,'<img src="'+g.blobUri()+'">',!1)}else Tt(t,'<img src="'+e.uri+'">',!1)}(e,t)})}),!0}return!1},It=function(t){return o.metaKeyPressed(t)&&86===t.keyCode||t.shiftKey&&45===t.keyCode},Ot=function(s,c,l){var e,f,d=(e=p(st.none()),{clear:function(){e.set(st.none())},set:function(t){e.set(st.some(t))},isSet:function(){return e.get().isSome()},on:function(t){e.get().each(t)}});function m(t,e,n,r){var o,i;kt(t,"text/html")?o=t["text/html"]:(o=c.getHtml(),r=r||w(o),c.isDefaultContent(o)&&(n=!0)),o=O.trimHtml(o),c.remove(),i=!1===r&&D(o),o.length&&!i||(n=!0),n&&(o=kt(t,"text/plain")&&i?t["text/plain"]:O.innerText(o)),c.isDefaultContent(o)?e||s.windowManager.alert("Please use Ctrl+V/Cmd+V keyboard shortcuts to paste contents."):n?Dt(s,o):Tt(s,o,r)}s.on("keydown",function(t){function e(t){It(t)&&!t.isDefaultPrevented()&&c.remove()}if(It(t)&&!t.isDefaultPrevented()){if((f=t.shiftKey&&86===t.keyCode)&&h.webkit&&-1!==v.navigator.userAgent.indexOf("Version/"))return;if(t.stopImmediatePropagation(),d.set(t),window.setTimeout(function(){d.clear()},100),h.ie&&f)return t.preventDefault(),void n(s,!0);c.remove(),c.create(),s.once("keyup",e),s.once("paste",function(){s.off("keyup",e)})}}),s.on("paste",function(t){var e,n,r,o=d.isSet(),i=(e=s,n=Ct(t.clipboardData||e.getDoc().dataTransfer),O.isMsEdge()?b.extend(n,{"text/html":""}):n),a="text"===l.get()||f,u=kt(i,x());f=!1,t.isDefaultPrevented()||(r=t.clipboardData,-1!==v.navigator.userAgent.indexOf("Android")&&r&&r.items&&0===r.items.length)?c.remove():Ft(i)||!Rt(s,t,c.getLastRng()||s.selection.getRng())?(o||t.preventDefault(),!h.ie||o&&!t.ieFake||kt(i,"text/html")||(c.create(),s.dom.bind(c.getEl(),"paste",function(t){t.stopPropagation()}),s.getDoc().execCommand("Paste",!1,null),i["text/html"]=c.getHtml()),kt(i,"text/html")?(t.preventDefault(),u||(u=w(i["text/html"])),m(i,o,a,u)):y.setEditorTimeout(s,function(){m(i,o,a,u)},0)):c.remove()})},St=function(t){return h.ie&&t.inline?v.document.body:t.getBody()},At=function(e,t,n){var r;St(r=e)!==r.getBody()&&e.dom.bind(t,"paste keyup",function(t){Lt(e,n)||e.fire("paste")})},jt=function(t){return t.dom.get("mcepastebin")},Mt=function(t,e){return e===t},Lt=function(t,e){var n,r=jt(t);return(n=r)&&"mcepastebin"===n.id&&Mt(e,r.innerHTML)},Nt=function(a){var u=p(null),s="%MCEPASTEBIN%";return{create:function(){return e=u,n=s,o=(t=a).dom,i=t.getBody(),e.set(t.selection.getRng()),r=t.dom.add(St(t),"div",{id:"mcepastebin","class":"mce-pastebin",contentEditable:!0,"data-mce-bogus":"all",style:"position: fixed; top: 50%; width: 10px; height: 10px; overflow: hidden; opacity: 0"},n),(h.ie||h.gecko)&&o.setStyle(r,"left","rtl"===o.getStyle(i,"direction",!0)?65535:-65535),o.bind(r,"beforedeactivate focusin focusout",function(t){t.stopPropagation()}),At(t,r,n),r.focus(),void t.selection.select(r,!0);var t,e,n,r,o,i},remove:function(){return function(t,e){if(jt(t)){for(var n=void 0,r=e.get();n=t.dom.get("mcepastebin");)t.dom.remove(n),t.dom.unbind(n);r&&t.selection.setRng(r)}e.set(null)}(a,u)},getEl:function(){return jt(a)},getHtml:function(){return function(n){var e,t,r,o,i,a=function(t,e){t.appendChild(e),n.dom.remove(e,!0)};for(t=b.grep(St(n).childNodes,function(t){return"mcepastebin"===t.id}),e=t.shift(),b.each(t,function(t){a(e,t)}),r=(o=n.dom.select("div[id=mcepastebin]",e)).length-1;0<=r;r--)i=n.dom.create("div"),e.insertBefore(i,o[r]),a(i,o[r]);return e?e.innerHTML:""}(a)},getLastRng:function(){return u.get()},isDefault:function(){return Lt(a,s)},isDefaultContent:function(t){return Mt(s,t)}}},Bt=function(n,t){var e=Nt(n);return n.on("preInit",function(){return Ot(a=n,e,t),void a.parser.addNodeFilter("img",function(t,e,n){var r,o=function(t){t.attr("data-mce-object")||u===h.transparentSrc||t.remove()};if(!a.settings.paste_data_images&&(r=n).data&&!0===r.data.paste)for(var i=t.length;i--;)(u=t[i].attributes.map.src)&&(0===u.indexOf("webkit-fake-url")?o(t[i]):a.settings.allow_html_data_urls||0!==u.indexOf("data:")||o(t[i]))});var a,u}),{pasteFormat:t,pasteHtml:function(t,e){return Tt(n,t,e)},pasteText:function(t){return Dt(n,t)},pasteImageData:function(t,e){return Rt(n,t,e)},getDataTransferItems:Ct,hasHtmlOrText:Ft,hasContentType:kt}},Ht=function(){},$t=function(t,e,n){if(r=t,!1!==h.iOS||r===undefined||"function"!=typeof r.setData||!0===O.isMsEdge())return!1;try{return t.clearData(),t.setData("text/html",e),t.setData("text/plain",n),t.setData(x(),e),!0}catch(o){return!1}var r},Wt=function(t,e,n,r){$t(t.clipboardData,e.html,e.text)?(t.preventDefault(),r()):n(e.html,r)},Ut=function(u){return function(t,e){var n=l(t),r=u.dom.create("div",{contenteditable:"false","data-mce-bogus":"all"}),o=u.dom.create("div",{contenteditable:"true"},n);u.dom.setStyles(r,{position:"fixed",top:"0",left:"-3000px",width:"1000px",overflow:"hidden"}),r.appendChild(o),u.dom.add(u.getBody(),r);var i=u.selection.getRng();o.focus();var a=u.dom.createRng();a.selectNodeContents(o),u.selection.setRng(a),setTimeout(function(){u.selection.setRng(i),r.parentNode.removeChild(r),e()},0)}},zt=function(t){return{html:t.selection.getContent({contextual:!0}),text:t.selection.getContent({format:"text"})}},Vt=function(t){return!t.selection.isCollapsed()||!!(e=t).dom.getParent(e.selection.getStart(),"td[data-mce-selected],th[data-mce-selected]",e.getBody());var e},Kt=function(t){var e,n;t.on("cut",(e=t,function(t){Vt(e)&&Wt(t,zt(e),Ut(e),function(){setTimeout(function(){e.execCommand("Delete")},0)})})),t.on("copy",(n=t,function(t){Vt(n)&&Wt(t,zt(n),Ut(n),Ht)}))},qt=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),Gt=function(t,e){return qt.getCaretRangeFromPoint(e.clientX,e.clientY,t.getDoc())},Xt=function(t,e){t.focus(),t.selection.setRng(e)},Yt=function(a,u,s){g.shouldBlockDrop(a)&&a.on("dragend dragover draggesture dragdrop drop drag",function(t){t.preventDefault(),t.stopPropagation()}),g.shouldPasteDataImages(a)||a.on("drop",function(t){var e=t.dataTransfer;e&&e.files&&0<e.files.length&&t.preventDefault()}),a.on("drop",function(t){var e,n;if(n=Gt(a,t),!t.isDefaultPrevented()&&!s.get()){e=u.getDataTransferItems(t.dataTransfer);var r,o=u.hasContentType(e,x());if((u.hasHtmlOrText(e)&&(!(r=e["text/plain"])||0!==r.indexOf("file://"))||!u.pasteImageData(t,n))&&n&&g.shouldFilterDrop(a)){var i=e["mce-internal"]||e["text/html"]||e["text/plain"];i&&(t.preventDefault(),y.setEditorTimeout(a,function(){a.undoManager.transact(function(){e["mce-internal"]&&a.execCommand("Delete"),Xt(a,n),i=O.trimHtml(i),e["text/html"]?u.pasteHtml(i,o):u.pasteText(i)})}))}}}),a.on("dragstart",function(t){s.set(!0)}),a.on("dragover dragend",function(t){g.shouldPasteDataImages(a)&&!1===s.get()&&(t.preventDefault(),Xt(a,Gt(a,t))),"dragend"===t.type&&s.set(!1)})},Zt=function(t){var e=t.plugins.paste,n=g.getPreProcess(t);n&&t.on("PastePreProcess",function(t){n.call(e,e,t)});var r=g.getPostProcess(t);r&&t.on("PastePostProcess",function(t){r.call(e,e,t)})};function Jt(e,n){e.on("PastePreProcess",function(t){t.content=n(e,t.content,t.internal,t.wordContent)})}function Qt(t,e){if(!V.isWordContent(e))return e;var n=[];b.each(t.schema.getBlockElements(),function(t,e){n.push(e)});var r=new RegExp("(?:<br> [\\s\\r\\n]+|<br>)*(<\\/?("+n.join("|")+")[^>]*>)(?:<br> [\\s\\r\\n]+|<br>)*","g");return e=O.filter(e,[[r,"$1"]]),e=O.filter(e,[[/<br><br>/g,"<BR><BR>"],[/<br>/g," "],[/<BR><BR>/g,"<br>"]])}function te(t,e,n,r){if(r||n)return e;var c,o=g.getWebkitStyles(t);if(!1===g.shouldRemoveWebKitStyles(t)||"all"===o)return e;if(o&&(c=o.split(/[, ]/)),c){var l=t.dom,f=t.selection.getNode();e=e.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi,function(t,e,n,r){var o=l.parseStyle(l.decode(n)),i={};if("none"===c)return e+r;for(var a=0;a<c.length;a++){var u=o[c[a]],s=l.getStyle(f,c[a],!0);/color/.test(c[a])&&(u=l.toHex(u),s=l.toHex(s)),s!==u&&(i[c[a]]=u)}return(i=l.serializeStyle(i,"span"))?e+' style="'+i+'"'+r:e+r})}else e=e.replace(/(<[^>]+) style="([^"]*)"([^>]*>)/gi,"$1$3");return e=e.replace(/(<[^>]+) data-mce-style="([^"]+)"([^>]*>)/gi,function(t,e,n,r){return e+' style="'+n+'"'+r})}function ee(n,t){n.$("a",t).find("font,u").each(function(t,e){n.dom.remove(e,!0)})}var ne=function(t){var e,n;h.webkit&&Jt(t,te),h.ie&&(Jt(t,Qt),n=ee,(e=t).on("PastePostProcess",function(t){n(e,t.node)}))},re=function(t,e,n){var r=n.control;r.active("text"===e.pasteFormat.get()),t.on("PastePlainTextToggle",function(t){r.active(t.state)})},oe=function(t,e){var n=function(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}(re,t,e);t.addButton("pastetext",{active:!1,icon:"pastetext",tooltip:"Paste as text",cmd:"mceTogglePlainTextPaste",onPostRender:n}),t.addMenuItem("pastetext",{text:"Paste as text",selectable:!0,active:e.pasteFormat,cmd:"mceTogglePlainTextPaste",onPostRender:n})};e.add("paste",function(t){if(!1===a(t)){var e=p(!1),n=p(!1),r=p(g.isPasteAsTextEnabled(t)?"text":"html"),o=Bt(t,r),i=ne(t);return oe(t,o),c(t,o,e),Zt(t),Kt(t),Yt(t,o,n),u(o,i)}})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/table/plugin.min.js b/lib/web/tiny_mce_4/plugins/table/plugin.min.js index bdafd838616df..80d078b4e2a6b 100644 --- a/lib/web/tiny_mce_4/plugins/table/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/table/plugin.min.js @@ -1 +1 @@ -!function(m){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),y=function(){},x=function(n,r){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return n(r.apply(null,e))}},C=function(e){return function(){return e}},o=function(e){return e};function b(r){for(var o=[],e=1;e<arguments.length;e++)o[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=o.concat(e);return r.apply(null,n)}}var t,n,r,i,u,g=function(n){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return!n.apply(null,e)}},c=function(e){return e()},f=C(!1),a=C(!0),l=f,s=a,d=function(){return h},h=(i={fold:function(e,t){return e()},is:l,isSome:l,isNone:s,getOr:r=function(e){return e},getOrThunk:n=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:r,orThunk:n,map:d,ap:d,each:function(){},bind:d,flatten:d,exists:l,forall:s,filter:d,equals:t=function(e){return e.isNone()},equals_:t,toArray:function(){return[]},toString:C("none()")},Object.freeze&&Object.freeze(i),i),p=function(n){var e=function(){return n},t=function(){return o},r=function(e){return e(n)},o={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:s,isNone:l,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return p(e(n))},ap:function(e){return e.fold(d,function(e){return p(e(n))})},each:function(e){e(n)},bind:r,flatten:e,exists:r,forall:r,filter:function(e){return e(n)?o:h},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(l,function(e){return t(n,e)})},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},S={some:p,none:d,from:function(e){return null===e||e===undefined?h:p(e)}},v=function(t){return function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===t}},w=v("string"),R=v("array"),T=v("boolean"),D=v("function"),O=v("number"),E=Array.prototype.slice,N=(u=Array.prototype.indexOf)===undefined?function(e,t){return j(e,t)}:function(e,t){return u.call(e,t)},k=function(e,t){return-1<N(e,t)},A=function(e,t){return L(e,t).isSome()},P=function(e,t){for(var n=e.length,r=new Array(n),o=0;o<n;o++){var i=e[o];r[o]=t(i,o,e)}return r},I=function(e,t){for(var n=0,r=e.length;n<r;n++)t(e[n],n,e)},B=function(e,t){for(var n=[],r=0,o=e.length;r<o;r++){var i=e[r];t(i,r,e)&&n.push(i)}return n},W=function(e,t,n){return function(e,t){for(var n=e.length-1;0<=n;n--)t(e[n],n,e)}(e,function(e){n=t(n,e)}),n},M=function(e,t,n){return I(e,function(e){n=t(n,e)}),n},_=function(e,t){for(var n=0,r=e.length;n<r;n++){var o=e[n];if(t(o,n,e))return S.some(o)}return S.none()},L=function(e,t){for(var n=0,r=e.length;n<r;n++)if(t(e[n],n,e))return S.some(n);return S.none()},j=function(e,t){for(var n=0,r=e.length;n<r;++n)if(e[n]===t)return n;return-1},F=Array.prototype.push,z=function(e){for(var t=[],n=0,r=e.length;n<r;++n){if(!Array.prototype.isPrototypeOf(e[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+e);F.apply(t,e[n])}return t},H=function(e,t){var n=P(e,t);return z(n)},U=function(e,t){for(var n=0,r=e.length;n<r;++n)if(!0!==t(e[n],n,e))return!1;return!0},q=function(e){var t=E.call(e,0);return t.reverse(),t},V=(D(Array.from)&&Array.from,Object.keys),G=function(e,t){for(var n=V(e),r=0,o=n.length;r<o;r++){var i=n[r];t(e[i],i,e)}},Y=function(e,r){return X(e,function(e,t,n){return{k:t,v:r(e,t,n)}})},X=function(r,o){var i={};return G(r,function(e,t){var n=o(e,t,r);i[n.k]=n.v}),i},K=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];if(t.length!==n.length)throw new Error('Wrong number of arguments to struct. Expected "['+t.length+']", got '+n.length+" arguments");var r={};return I(t,function(e,t){r[e]=C(n[t])}),r}},J=function(e){return e.slice(0).sort()},$=function(e,t){throw new Error("All required keys ("+J(e).join(", ")+") were not specified. Specified keys were: "+J(t).join(", ")+".")},Q=function(e){throw new Error("Unsupported keys for object: "+J(e).join(", "))},Z=function(t,e){if(!R(e))throw new Error("The "+t+" fields must be an array. Was: "+e+".");I(e,function(e){if(!w(e))throw new Error("The value "+e+" in the "+t+" fields was not a string.")})},ee=function(e){var n=J(e);_(n,function(e,t){return t<n.length-1&&e===n[t+1]}).each(function(e){throw new Error("The field: "+e+" occurs more than once in the combined fields: ["+n.join(", ")+"].")})},te=function(o,i){var u=o.concat(i);if(0===u.length)throw new Error("You must specify at least one required or optional field.");return Z("required",o),Z("optional",i),ee(u),function(t){var n=V(t);U(o,function(e){return k(n,e)})||$(o,n);var e=B(n,function(e){return!k(u,e)});0<e.length&&Q(e);var r={};return I(o,function(e){r[e]=C(t[e])}),I(i,function(e){r[e]=C(Object.prototype.hasOwnProperty.call(t,e)?S.some(t[e]):S.none())}),r}},ne=(m.Node.ATTRIBUTE_NODE,m.Node.CDATA_SECTION_NODE,m.Node.COMMENT_NODE),re=m.Node.DOCUMENT_NODE,oe=(m.Node.DOCUMENT_TYPE_NODE,m.Node.DOCUMENT_FRAGMENT_NODE,m.Node.ELEMENT_NODE),ie=m.Node.TEXT_NODE,ue=(m.Node.PROCESSING_INSTRUCTION_NODE,m.Node.ENTITY_REFERENCE_NODE,m.Node.ENTITY_NODE,m.Node.NOTATION_NODE,function(e){return e.dom().nodeName.toLowerCase()}),ae=function(e){return e.dom().nodeType},ce=function(t){return function(e){return ae(e)===t}},le=function(e){return ae(e)===ne||"#comment"===ue(e)},fe=ce(oe),se=ce(ie),de=ce(re),me=function(e,t,n){if(!(w(n)||T(n)||O(n)))throw m.console.error("Invalid call to Attr.set. Key ",t,":: Value ",n,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,n+"")},ge=function(e,t,n){me(e.dom(),t,n)},he=function(e,t){var n=e.dom();G(t,function(e,t){me(n,t,e)})},pe=function(e,t){var n=e.dom().getAttribute(t);return null===n?undefined:n},ve=function(e,t){var n=e.dom();return!(!n||!n.hasAttribute)&&n.hasAttribute(t)},be=function(e,t){e.dom().removeAttribute(t)},we=function(e){return M(e.dom().attributes,function(e,t){return e[t.name]=t.value,e},{})},ye=function(e,t){return-1!==e.indexOf(t)},xe=function(e){return e.style!==undefined&&D(e.style.getPropertyValue)},Ce=function(n){var r,o=!1;return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return o||(o=!0,r=n.apply(null,e)),r}},Se=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:C(e)}},Re={fromHtml:function(e,t){var n=(t||m.document).createElement("div");if(n.innerHTML=e,!n.hasChildNodes()||1<n.childNodes.length)throw m.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return Se(n.childNodes[0])},fromTag:function(e,t){var n=(t||m.document).createElement(e);return Se(n)},fromText:function(e,t){var n=(t||m.document).createTextNode(e);return Se(n)},fromDom:Se,fromPoint:function(e,t,n){var r=e.dom();return S.from(r.elementFromPoint(t,n)).map(Se)}},Te=function(e){var t=se(e)?e.dom().parentNode:e.dom();return t!==undefined&&null!==t&&t.ownerDocument.body.contains(t)},De=Ce(function(){return Oe(Re.fromDom(m.document))}),Oe=function(e){var t=e.dom().body;if(null===t||t===undefined)throw new Error("Body is not available yet");return Re.fromDom(t)},Ee=function(e,t,n){if(!w(n))throw m.console.error("Invalid call to CSS.set. Property ",t,":: Value ",n,":: Element ",e),new Error("CSS value must be a string: "+n);xe(e)&&e.style.setProperty(t,n)},Ne=function(e,t,n){var r=e.dom();Ee(r,t,n)},ke=function(e,t){var n=e.dom();G(t,function(e,t){Ee(n,t,e)})},Ae=function(e,t){var n=e.dom(),r=m.window.getComputedStyle(n).getPropertyValue(t),o=""!==r||Te(e)?r:Pe(n,t);return null===o?undefined:o},Pe=function(e,t){return xe(e)?e.style.getPropertyValue(t):""},Ie=function(e,t){var n=e.dom(),r=Pe(n,t);return S.from(r).filter(function(e){return 0<e.length})},Be=function(e,t){var n,r,o=e.dom();r=t,xe(n=o)&&n.style.removeProperty(r),ve(e,"style")&&""===pe(e,"style").replace(/^\s+|\s+$/g,"")&&be(e,"style")},We="undefined"!=typeof m.window?m.window:Function("return this;")(),Me=function(e,t){return function(e,t){for(var n=t!==undefined&&null!==t?t:We,r=0;r<e.length&&n!==undefined&&null!==n;++r)n=n[e[r]];return n}(e.split("."),t)},_e=function(e,t){var n=Me(e,t);if(n===undefined||null===n)throw e+" not available on this browser";return n},Le=function(){return _e("Node")},je=function(e,t,n){return 0!=(e.compareDocumentPosition(t)&n)},Fe=function(e,t){return je(e,t,Le().DOCUMENT_POSITION_CONTAINED_BY)},ze=function(e,t){var n=function(e,t){for(var n=0;n<e.length;n++){var r=e[n];if(r.test(t))return r}return undefined}(e,t);if(!n)return{major:0,minor:0};var r=function(e){return Number(t.replace(n,"$"+e))};return Ue(r(1),r(2))},He=function(){return Ue(0,0)},Ue=function(e,t){return{major:e,minor:t}},qe={nu:Ue,detect:function(e,t){var n=String(t).toLowerCase();return 0===e.length?He():ze(e,n)},unknown:He},Ve="Firefox",Ge=function(e,t){return function(){return t===e}},Ye=function(e){var t=e.current;return{current:t,version:e.version,isEdge:Ge("Edge",t),isChrome:Ge("Chrome",t),isIE:Ge("IE",t),isOpera:Ge("Opera",t),isFirefox:Ge(Ve,t),isSafari:Ge("Safari",t)}},Xe={unknown:function(){return Ye({current:undefined,version:qe.unknown()})},nu:Ye,edge:C("Edge"),chrome:C("Chrome"),ie:C("IE"),opera:C("Opera"),firefox:C(Ve),safari:C("Safari")},Ke="Windows",Je="Android",$e="Solaris",Qe="FreeBSD",Ze=function(e,t){return function(){return t===e}},et=function(e){var t=e.current;return{current:t,version:e.version,isWindows:Ze(Ke,t),isiOS:Ze("iOS",t),isAndroid:Ze(Je,t),isOSX:Ze("OSX",t),isLinux:Ze("Linux",t),isSolaris:Ze($e,t),isFreeBSD:Ze(Qe,t)}},tt={unknown:function(){return et({current:undefined,version:qe.unknown()})},nu:et,windows:C(Ke),ios:C("iOS"),android:C(Je),linux:C("Linux"),osx:C("OSX"),solaris:C($e),freebsd:C(Qe)},nt=function(e,t){var n=String(t).toLowerCase();return _(e,function(e){return e.search(n)})},rt=function(e,n){return nt(e,n).map(function(e){var t=qe.detect(e.versionRegexes,n);return{current:e.name,version:t}})},ot=function(e,n){return nt(e,n).map(function(e){var t=qe.detect(e.versionRegexes,n);return{current:e.name,version:t}})},it=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,ut=function(t){return function(e){return ye(e,t)}},at=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return ye(e,"edge/")&&ye(e,"chrome")&&ye(e,"safari")&&ye(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,it],search:function(e){return ye(e,"chrome")&&!ye(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return ye(e,"msie")||ye(e,"trident")}},{name:"Opera",versionRegexes:[it,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:ut("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:ut("firefox")},{name:"Safari",versionRegexes:[it,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return(ye(e,"safari")||ye(e,"mobile/"))&&ye(e,"applewebkit")}}],ct=[{name:"Windows",search:ut("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return ye(e,"iphone")||ye(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:ut("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:ut("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:ut("linux"),versionRegexes:[]},{name:"Solaris",search:ut("sunos"),versionRegexes:[]},{name:"FreeBSD",search:ut("freebsd"),versionRegexes:[]}],lt={browsers:C(at),oses:C(ct)},ft=function(e){var t,n,r,o,i,u,a,c,l,f,s,d=lt.browsers(),m=lt.oses(),g=rt(d,e).fold(Xe.unknown,Xe.nu),h=ot(m,e).fold(tt.unknown,tt.nu);return{browser:g,os:h,deviceType:(n=g,r=e,o=(t=h).isiOS()&&!0===/ipad/i.test(r),i=t.isiOS()&&!o,u=t.isAndroid()&&3===t.version.major,a=t.isAndroid()&&4===t.version.major,c=o||u||a&&!0===/mobile/i.test(r),l=t.isiOS()||t.isAndroid(),f=l&&!c,s=n.isSafari()&&t.isiOS()&&!1===/safari/i.test(r),{isiPad:C(o),isiPhone:C(i),isTablet:C(c),isPhone:C(f),isTouch:C(l),isAndroid:t.isAndroid,isiOS:t.isiOS,isWebView:C(s)})}},st={detect:Ce(function(){var e=m.navigator.userAgent;return ft(e)})},dt=oe,mt=re,gt=function(e,t){var n=e.dom();if(n.nodeType!==dt)return!1;if(n.matches!==undefined)return n.matches(t);if(n.msMatchesSelector!==undefined)return n.msMatchesSelector(t);if(n.webkitMatchesSelector!==undefined)return n.webkitMatchesSelector(t);if(n.mozMatchesSelector!==undefined)return n.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")},ht=function(e){return e.nodeType!==dt&&e.nodeType!==mt||0===e.childElementCount},pt=function(e,t){return e.dom()===t.dom()},vt=st.detect().browser.isIE()?function(e,t){return Fe(e.dom(),t.dom())}:function(e,t){var n=e.dom(),r=t.dom();return n!==r&&n.contains(r)},bt=gt,wt=function(e){return Re.fromDom(e.dom().ownerDocument)},yt=function(e){var t=e.dom();return S.from(t.parentNode).map(Re.fromDom)},xt=function(e,t){for(var n=D(t)?t:C(!1),r=e.dom(),o=[];null!==r.parentNode&&r.parentNode!==undefined;){var i=r.parentNode,u=Re.fromDom(i);if(o.push(u),!0===n(u))break;r=i}return o},Ct=function(e){var t=e.dom();return S.from(t.previousSibling).map(Re.fromDom)},St=function(e){var t=e.dom();return S.from(t.nextSibling).map(Re.fromDom)},Rt=function(e){var t=e.dom();return P(t.childNodes,Re.fromDom)},Tt=function(e,t){var n=e.dom().childNodes;return S.from(n[t]).map(Re.fromDom)},Dt=(K("element","offset"),function(t,n){yt(t).each(function(e){e.dom().insertBefore(n.dom(),t.dom())})}),Ot=function(e,t){St(e).fold(function(){yt(e).each(function(e){Nt(e,t)})},function(e){Dt(e,t)})},Et=function(t,n){Tt(t,0).fold(function(){Nt(t,n)},function(e){t.dom().insertBefore(n.dom(),e.dom())})},Nt=function(e,t){e.dom().appendChild(t.dom())},kt=function(e,t){Dt(e,t),Nt(t,e)},At=function(r,o){I(o,function(e,t){var n=0===t?r:o[t-1];Ot(n,e)})},Pt=function(t,e){I(e,function(e){Nt(t,e)})},It=function(e){e.dom().textContent="",I(Rt(e),function(e){Bt(e)})},Bt=function(e){var t=e.dom();null!==t.parentNode&&t.parentNode.removeChild(t)},Wt=function(e){var t,n=Rt(e);0<n.length&&(t=e,I(n,function(e){Dt(t,e)})),Bt(e)},Mt=(K("width","height"),K("width","height"),K("rows","columns")),_t=K("row","column"),Lt=(K("x","y"),K("element","rowspan","colspan")),jt=K("element","rowspan","colspan","isNew"),Ft=K("element","rowspan","colspan","row","column"),zt=K("element","cells","section"),Ht=K("element","isNew"),Ut=K("element","cells","section","isNew"),qt=K("cells","section"),Vt=K("details","section"),Gt=K("startRow","startCol","finishRow","finishCol"),Yt=function(e,t){var n=[];return I(Rt(e),function(e){t(e)&&(n=n.concat([e])),n=n.concat(Yt(e,t))}),n},Xt=function(e,t,n){return r=function(e){return gt(e,t)},B(xt(e,n),r);var r},Kt=function(e,t){return n=function(e){return gt(e,t)},B(Rt(e),n);var n},Jt=function(e,t){return n=t,o=(r=e)===undefined?m.document:r.dom(),ht(o)?[]:P(o.querySelectorAll(n),Re.fromDom);var n,r,o};function $t(e,t,n,r,o){return e(n,r)?S.some(n):D(o)&&o(n)?S.none():t(n,r,o)}var Qt,Zt,en,tn,nn,rn=function(e,t,n){for(var r=e.dom(),o=D(n)?n:C(!1);r.parentNode;){r=r.parentNode;var i=Re.fromDom(r);if(t(i))return S.some(i);if(o(i))break}return S.none()},on=function(e,t,n){return rn(e,function(e){return gt(e,t)},n)},un=function(e,t){return n=function(e){return gt(e,t)},_(e.dom().childNodes,x(n,Re.fromDom)).map(Re.fromDom);var n},an=function(e,t){return n=t,o=(r=e)===undefined?m.document:r.dom(),ht(o)?S.none():S.from(o.querySelector(n)).map(Re.fromDom);var n,r,o},cn=function(e,t,n){return $t(gt,on,e,t,n)},ln=function(e,t,n){return H(Rt(e),function(e){return gt(e,t)?n(e)?[e]:[]:ln(e,t,n)})},fn={firstLayer:function(e,t){return ln(e,t,C(!0))},filterFirstLayer:ln},sn=function(e,t,n){return void 0===n&&(n=f),n(t)?S.none():k(e,ue(t))?S.some(t):on(t,e.join(","),function(e){return gt(e,"table")||n(e)})},dn=function(t,e){return yt(e).map(function(e){return Kt(e,t)})},mn=b(dn,"th,td"),gn=b(dn,"tr"),hn=function(e,t){return parseInt(pe(e,t),10)},pn={cell:function(e,t){return sn(["td","th"],e,t)},firstCell:function(e){return an(e,"th,td")},cells:function(e){return fn.firstLayer(e,"th,td")},neighbourCells:mn,table:function(e,t){return cn(e,"table",t)},row:function(e,t){return sn(["tr"],e,t)},rows:function(e){return fn.firstLayer(e,"tr")},notCell:function(e,t){return sn(["caption","tr","tbody","tfoot","thead"],e,t)},neighbourRows:gn,attr:hn,grid:function(e,t,n){var r=hn(e,t),o=hn(e,n);return Mt(r,o)}},vn=function(e){var t=pn.rows(e);return P(t,function(e){var t=e,n=yt(t).map(function(e){var t=ue(e);return"tfoot"===t||"thead"===t||"tbody"===t?t:"tbody"}).getOr("tbody"),r=P(pn.cells(e),function(e){var t=ve(e,"rowspan")?parseInt(pe(e,"rowspan"),10):1,n=ve(e,"colspan")?parseInt(pe(e,"colspan"),10):1;return Lt(e,t,n)});return zt(t,r,n)})},bn=function(e,n){return P(e,function(e){var t=P(pn.cells(e),function(e){var t=ve(e,"rowspan")?parseInt(pe(e,"rowspan"),10):1,n=ve(e,"colspan")?parseInt(pe(e,"colspan"),10):1;return Lt(e,t,n)});return zt(e,t,n.section())})},wn=function(e,t){return e+","+t},yn=function(e,t){var n=H(e.all(),function(e){return e.cells()});return B(n,t)},xn={generate:function(e){var l={},t=[],n=e.length,f=0;I(e,function(e,a){var c=[];I(e.cells(),function(e){for(var t=0;l[wn(a,t)]!==undefined;)t++;for(var n=Ft(e.element(),e.rowspan(),e.colspan(),a,t),r=0;r<e.colspan();r++)for(var o=0;o<e.rowspan();o++){var i=t+r,u=wn(a+o,i);l[u]=n,f=Math.max(f,i+1)}c.push(n)}),t.push(zt(e.element(),c,e.section()))});var r=Mt(n,f);return{grid:C(r),access:C(l),all:C(t)}},getAt:function(e,t,n){var r=e.access()[wn(t,n)];return r!==undefined?S.some(r):S.none()},findItem:function(e,t,n){var r=yn(e,function(e){return n(t,e.element())});return 0<r.length?S.some(r[0]):S.none()},filterItems:yn,justCells:function(e){var t=P(e.all(),function(e){return e.cells()});return z(t)}},Cn=K("minRow","minCol","maxRow","maxCol"),Sn=function(e,t){var n,i,r,u,a,c,l,o,f,s,d=function(e){return gt(e.element(),t)},m=vn(e),g=xn.generate(m),h=(i=d,r=(n=g).grid().columns(),u=n.grid().rows(),a=r,l=c=0,G(n.access(),function(e){if(i(e)){var t=e.row(),n=t+e.rowspan()-1,r=e.column(),o=r+e.colspan()-1;t<u?u=t:c<n&&(c=n),r<a?a=r:l<o&&(l=o)}}),Cn(u,a,c,l)),p="th:not("+t+"),td:not("+t+")",v=fn.filterFirstLayer(e,"th,td",function(e){return gt(e,p)});return I(v,Bt),function(e,t,n,r){for(var o,i,u,a=t.grid().columns(),c=t.grid().rows(),l=0;l<c;l++)for(var f=!1,s=0;s<a;s++)l<n.minRow()||l>n.maxRow()||s<n.minCol()||s>n.maxCol()||(xn.getAt(t,l,s).filter(r).isNone()?(o=f,i=e[l].element(),u=Re.fromTag("td"),Nt(u,Re.fromTag("br")),(o?Nt:Et)(i,u)):f=!0)}(m,g,h,d),o=e,f=h,s=B(fn.firstLayer(o,"tr"),function(e){return 0===e.dom().childElementCount}),I(s,Bt),f.minCol()!==f.maxCol()&&f.minRow()!==f.maxRow()||I(fn.firstLayer(o,"th,td"),function(e){be(e,"rowspan"),be(e,"colspan")}),be(o,"width"),be(o,"height"),Be(o,"width"),Be(o,"height"),e},Rn=(Qt=se,Zt="text",en=function(e){return Qt(e)?S.from(e.dom().nodeValue):S.none()},tn=st.detect().browser,{get:function(e){if(!Qt(e))throw new Error("Can only get "+Zt+" value of a "+Zt+" node");return nn(e).getOr("")},getOption:nn=tn.isIE()&&10===tn.version.major?function(e){try{return en(e)}catch(t){return S.none()}}:en,set:function(e,t){if(!Qt(e))throw new Error("Can only set raw "+Zt+" value of a "+Zt+" node");e.dom().nodeValue=t}}),Tn=function(e){return Rn.get(e)},Dn=function(e){return Rn.getOption(e)},On=function(e,t){Rn.set(e,t)},En=function(e){return"img"===ue(e)?1:Dn(e).fold(function(){return Rt(e).length},function(e){return e.length})},Nn=["img","br"],kn=function(e){return Dn(e).filter(function(e){return 0!==e.trim().length||-1<e.indexOf("\xa0")}).isSome()||k(Nn,ue(e))},An=function(e){return r=kn,(o=function(e){for(var t=0;t<e.childNodes.length;t++){if(r(Re.fromDom(e.childNodes[t])))return S.some(Re.fromDom(e.childNodes[t]));var n=o(e.childNodes[t]);if(n.isSome())return n}return S.none()})(e.dom());var r,o},Pn=function(e){return In(e,kn)},In=function(e,i){var u=function(e){for(var t=Rt(e),n=t.length-1;0<=n;n--){var r=t[n];if(i(r))return S.some(r);var o=u(r);if(o.isSome())return o}return S.none()};return u(e)},Bn=function(e,t){return Re.fromDom(e.dom().cloneNode(t))},Wn=function(e){return Bn(e,!1)},Mn=function(e){return Bn(e,!0)},_n=function(e,t){var n,r,o,i,u=(n=e,r=t,o=Re.fromTag(r),i=we(n),he(o,i),o),a=Rt(Mn(e));return Pt(u,a),u},Ln=function(){var e=Re.fromTag("td");return Nt(e,Re.fromTag("br")),e},jn=function(e,t,n){var r=_n(e,t);return G(n,function(e,t){null===e?be(r,t):ge(r,t,e)}),r},Fn=function(e){return e},zn=function(e){return function(){return Re.fromTag("tr",e.dom())}},Hn=function(d,e,m){return{row:zn(e),cell:function(e){var r,o,i,t,n,u,a,c=wt(e.element()),l=Re.fromTag(ue(e.element()),c.dom()),f=m.getOr(["strong","em","b","i","span","font","h1","h2","h3","h4","h5","h6","p","div"]),s=0<f.length?(r=e.element(),o=l,i=f,An(r).map(function(e){var t=i.join(","),n=Xt(e,t,function(e){return pt(e,r)});return W(n,function(e,t){var n=Wn(t);return be(n,"contenteditable"),Nt(e,n),n},o)}).getOr(o)):l;return Nt(s,Re.fromTag("br")),t=e.element(),n=l,u=t.dom(),a=n.dom(),xe(u)&&xe(a)&&(a.style.cssText=u.style.cssText),Be(l,"height"),1!==e.colspan()&&Be(e.element(),"width"),d(e.element(),l),l},replace:jn,gap:Ln}},Un=function(e){return{row:zn(e),cell:Ln,replace:Fn,gap:Ln}},qn=function(e,t){return t.column()>=e.startCol()&&t.column()+t.colspan()-1<=e.finishCol()&&t.row()>=e.startRow()&&t.row()+t.rowspan()-1<=e.finishRow()},Vn=function(e,t){var n=t.column(),r=t.column()+t.colspan()-1,o=t.row(),i=t.row()+t.rowspan()-1;return n<=e.finishCol()&&r>=e.startCol()&&o<=e.finishRow()&&i>=e.startRow()},Gn=function(e,t){for(var n=!0,r=b(qn,t),o=t.startRow();o<=t.finishRow();o++)for(var i=t.startCol();i<=t.finishCol();i++)n=n&&xn.getAt(e,o,i).exists(r);return n?S.some(t):S.none()},Yn=function(e,t,n){var r=xn.findItem(e,t,pt),o=xn.findItem(e,n,pt);return r.bind(function(r){return o.map(function(e){return t=r,n=e,Gt(Math.min(t.row(),n.row()),Math.min(t.column(),n.column()),Math.max(t.row()+t.rowspan()-1,n.row()+n.rowspan()-1),Math.max(t.column()+t.colspan()-1,n.column()+n.colspan()-1));var t,n})})},Xn=Yn,Kn=function(t,e,n){return Yn(t,e,n).bind(function(e){return Gn(t,e)})},Jn=function(r,e,o,i){return xn.findItem(r,e,pt).bind(function(e){var t=0<o?e.row()+e.rowspan()-1:e.row(),n=0<i?e.column()+e.colspan()-1:e.column();return xn.getAt(r,t+o,n+i).map(function(e){return e.element()})})},$n=function(n,e,t){return Xn(n,e,t).map(function(e){var t=xn.filterItems(n,b(Vn,e));return P(t,function(e){return e.element()})})},Qn=function(e,t){return xn.findItem(e,t,function(e,t){return vt(t,e)}).map(function(e){return e.element()})},Zn=function(e){var t=vn(e);return xn.generate(t)},er=function(n,r,o){return pn.table(n).bind(function(e){var t=Zn(e);return Jn(t,n,r,o)})},tr=function(e,t,n){var r=Zn(e);return $n(r,t,n)},nr=function(e,t,n,r,o){var i=Zn(e),u=pt(e,n)?S.some(t):Qn(i,t),a=pt(e,o)?S.some(r):Qn(i,r);return u.bind(function(t){return a.bind(function(e){return $n(i,t,e)})})},rr=function(e,t,n){var r=Zn(e);return Kn(r,t,n)},or=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","li","table","thead","tbody","tfoot","caption","tr","td","th","h1","h2","h3","h4","h5","h6","blockquote","pre","address"];function ir(){return{up:C({selector:on,closest:cn,predicate:rn,all:xt}),down:C({selector:Jt,predicate:Yt}),styles:C({get:Ae,getRaw:Ie,set:Ne,remove:Be}),attrs:C({get:pe,set:ge,remove:be,copyTo:function(e,t){var n=we(e);he(t,n)}}),insert:C({before:Dt,after:Ot,afterAll:At,append:Nt,appendAll:Pt,prepend:Et,wrap:kt}),remove:C({unwrap:Wt,remove:Bt}),create:C({nu:Re.fromTag,clone:function(e){return Re.fromDom(e.dom().cloneNode(!1))},text:Re.fromText}),query:C({comparePosition:function(e,t){return e.dom().compareDocumentPosition(t.dom())},prevSibling:Ct,nextSibling:St}),property:C({children:Rt,name:ue,parent:yt,document:function(e){return e.dom().ownerDocument},isText:se,isComment:le,isElement:fe,getText:Tn,setText:On,isBoundary:function(e){return!!fe(e)&&("body"===ue(e)||k(or,ue(e)))},isEmptyTag:function(e){return!!fe(e)&&k(["br","img","hr","input"],ue(e))}}),eq:pt,is:bt}}var ur=K("left","right"),ar=K("first","second","splits"),cr=function(e,t,n){var r=e.property().children(t);return L(r,b(e.eq,n)).map(function(e){return{before:C(r.slice(0,e)),after:C(r.slice(e+1))}})},lr=function(r,o,e,t){var n=o(r,e);return W(t,function(e,t){var n=o(r,t);return fr(r,e,n)},n)},fr=function(t,e,n){return e.bind(function(e){return n.filter(b(t.eq,e))})},sr=function(e,t){return b(e.eq,t)},dr=function(t,e,n,r){void 0===r&&(r=f);var o=[e].concat(t.up().all(e)),i=[n].concat(t.up().all(n)),u=function(t){return L(t,r).fold(function(){return t},function(e){return t.slice(0,e+1)})},a=u(o),c=u(i),l=_(a,function(e){return A(c,sr(t,e))});return{firstpath:C(a),secondpath:C(c),shared:C(l)}},mr={sharedOne:function(e,t,n){return 0<n.length?lr(e,t,(r=n)[0],r.slice(1)):S.none();var r},subset:function(t,e,n){var r=dr(t,e,n);return r.shared().bind(function(e){return function(o,i,e,t){var u=o.property().children(i);if(o.eq(i,e[0]))return S.some([e[0]]);if(o.eq(i,t[0]))return S.some([t[0]]);var n=function(e){var t=q(e),n=L(t,sr(o,i)).getOr(-1),r=n<t.length-1?t[n+1]:t[n];return L(u,sr(o,r))},r=n(e),a=n(t);return r.bind(function(r){return a.map(function(e){var t=Math.min(r,e),n=Math.max(r,e);return u.slice(t,n+1)})})}(t,e,r.firstpath(),r.secondpath())})},ancestors:dr,breakToLeft:function(n,r,o){return cr(n,r,o).map(function(e){var t=n.create().clone(r);return n.insert().appendAll(t,e.before().concat([o])),n.insert().appendAll(r,e.after()),n.insert().before(r,t),ur(t,r)})},breakToRight:function(n,r,e){return cr(n,r,e).map(function(e){var t=n.create().clone(r);return n.insert().appendAll(t,e.after()),n.insert().after(r,t),ur(r,t)})},breakPath:function(i,e,u,a){var c=function(e,t,o){var n=ar(e,S.none(),o);return u(e)?ar(e,t,o):i.property().parent(e).bind(function(r){return a(i,r,e).map(function(e){var t=[{first:e.left,second:e.right}],n=u(r)?r:e.left();return c(n,S.some(e.right()),o.concat(t))})}).getOr(n)};return c(e,S.none(),[])}},gr=ir(),hr={sharedOne:function(n,e){return mr.sharedOne(gr,function(e,t){return n(t)},e)},subset:function(e,t){return mr.subset(gr,e,t)},ancestors:function(e,t,n){return mr.ancestors(gr,e,t,n)},breakToLeft:function(e,t){return mr.breakToLeft(gr,e,t)},breakToRight:function(e,t){return mr.breakToRight(gr,e,t)},breakPath:function(e,t,r){return mr.breakPath(gr,e,t,function(e,t,n){return r(t,n)})}},pr={create:te(["boxes","start","finish"],[])},vr=function(e){return on(e,"table")},br=function(a,c,r){var l=function(t){return function(e){return r!==undefined&&r(e)||pt(e,t)}};return pt(a,c)?S.some(pr.create({boxes:S.some([a]),start:a,finish:c})):vr(a).bind(function(u){return vr(c).bind(function(i){if(pt(u,i))return S.some(pr.create({boxes:tr(u,a,c),start:a,finish:c}));if(vt(u,i)){var e=0<(t=Xt(c,"td,th",l(u))).length?t[t.length-1]:c;return S.some(pr.create({boxes:nr(u,a,u,c,i),start:a,finish:e}))}if(vt(i,u)){var t,n=0<(t=Xt(a,"td,th",l(i))).length?t[t.length-1]:a;return S.some(pr.create({boxes:nr(i,a,u,c,i),start:a,finish:n}))}return hr.ancestors(a,c).shared().bind(function(e){return cn(e,"table",r).bind(function(e){var t=Xt(c,"td,th",l(e)),n=0<t.length?t[t.length-1]:c,r=Xt(a,"td,th",l(e)),o=0<r.length?r[r.length-1]:a;return S.some(pr.create({boxes:nr(e,a,u,c,i),start:o,finish:n}))})})})})},wr=br,yr=function(e,t){var n=Jt(e,t);return 0<n.length?S.some(n):S.none()},xr=function(e,t,n,r,o){return(i=e,u=o,_(i,function(e){return gt(e,u)})).bind(function(e){return er(e,t,n).bind(function(e){return n=r,on(t=e,"table").bind(function(e){return an(e,n).bind(function(e){return br(e,t).bind(function(t){return t.boxes().map(function(e){return{boxes:C(e),start:C(t.start()),finish:C(t.finish())}})})})});var t,n})});var i,u},Cr=function(e,t,r){return an(e,t).bind(function(n){return an(e,r).bind(function(t){return hr.sharedOne(vr,[n,t]).map(function(e){return{first:C(n),last:C(t),table:C(e)}})})})},Sr=function(e,t){return yr(e,t)},Rr=function(o,e,t){return Cr(o,e,t).bind(function(n){var e=function(e){return pt(o,e)},t=on(n.first(),"thead,tfoot,tbody,table",e),r=on(n.last(),"thead,tfoot,tbody,table",e);return t.bind(function(t){return r.bind(function(e){return pt(t,e)?rr(n.table(),n.first(),n.last()):S.none()})})})},Tr="data-mce-selected",Dr="data-mce-first-selected",Or="data-mce-last-selected",Er={selected:C(Tr),selectedSelector:C("td[data-mce-selected],th[data-mce-selected]"),attributeSelector:C("[data-mce-selected]"),firstSelected:C(Dr),firstSelectedSelector:C("td[data-mce-first-selected],th[data-mce-first-selected]"),lastSelected:C(Or),lastSelectedSelector:C("td[data-mce-last-selected],th[data-mce-last-selected]")},Nr=function(u){if(!R(u))throw new Error("cases must be an array");if(0===u.length)throw new Error("there must be at least one case");var a=[],n={};return I(u,function(e,r){var t=V(e);if(1!==t.length)throw new Error("one and only one name per case");var o=t[0],i=e[o];if(n[o]!==undefined)throw new Error("duplicate key detected:"+o);if("cata"===o)throw new Error("cannot have a case named cata (sorry)");if(!R(i))throw new Error("case arguments must be an array");a.push(o),n[o]=function(){var e=arguments.length;if(e!==i.length)throw new Error("Wrong number of arguments to case "+o+". Expected "+i.length+" ("+i+"), got "+e);for(var n=new Array(e),t=0;t<n.length;t++)n[t]=arguments[t];return{fold:function(){if(arguments.length!==u.length)throw new Error("Wrong number of arguments to fold. Expected "+u.length+", got "+arguments.length);return arguments[r].apply(null,n)},match:function(e){var t=V(e);if(a.length!==t.length)throw new Error("Wrong number of arguments to match. Expected: "+a.join(",")+"\nActual: "+t.join(","));if(!U(a,function(e){return k(t,e)}))throw new Error("Not all branches were specified when using match. Specified: "+t.join(", ")+"\nRequired: "+a.join(", "));return e[o].apply(null,n)},log:function(e){m.console.log(e,{constructors:a,constructor:o,params:n})}}}}),n},kr=Nr([{none:[]},{multiple:["elements"]},{single:["selection"]}]),Ar={cata:function(e,t,n,r){return e.fold(t,n,r)},none:kr.none,multiple:kr.multiple,single:kr.single},Pr=function(e,t){return Ar.cata(t.get(),C([]),o,C([e]))},Ir=function(n,e){return Ar.cata(e.get(),S.none,function(t,e){return 0===t.length?S.none():Rr(n,Er.firstSelectedSelector(),Er.lastSelectedSelector()).bind(function(e){return 1<t.length?S.some({bounds:C(e),cells:C(t)}):S.none()})},S.none)},Br=function(e,t){var n=Pr(e,t);return 0<n.length&&U(n,function(e){return ve(e,"rowspan")&&1<parseInt(pe(e,"rowspan"),10)||ve(e,"colspan")&&1<parseInt(pe(e,"colspan"),10)})?S.some(n):S.none()},Wr=Pr,Mr=function(e){return{element:C(e),mergable:S.none,unmergable:S.none,selection:C([e])}},_r=K("element","clipboard","generators"),Lr={noMenu:Mr,forMenu:function(e,t,n){return{element:C(n),mergable:C(Ir(t,e)),unmergable:C(Br(n,e)),selection:C(Wr(n,e))}},notCell:function(e){return Mr(e)},paste:_r,pasteRows:function(e,t,n,r,o){return{element:C(n),mergable:S.none,unmergable:S.none,selection:C(Wr(n,e)),clipboard:C(r),generators:C(o)}}},jr=function(f,e,s,d){f.on("BeforeGetContent",function(n){!0===n.selection&&Ar.cata(e.get(),y,function(e){var t;n.preventDefault(),(t=e,pn.table(t[0]).map(Mn).map(function(e){return[Sn(e,Er.attributeSelector())]})).each(function(e){var t;n.content="text"===n.format?P(e,function(e){return e.dom().innerText}).join(""):(t=f,P(e,function(e){return t.selection.serializer.serialize(e.dom(),{})}).join(""))})},y)}),f.on("BeforeSetContent",function(l){!0===l.selection&&!0===l.paste&&S.from(f.dom.getParent(f.selection.getStart(),"th,td")).each(function(e){var c=Re.fromDom(e);pn.table(c).each(function(t){var e,n,r,o=B((e=l.content,(r=(n||m.document).createElement("div")).innerHTML=e,Rt(Re.fromDom(r))),function(e){return"meta"!==ue(e)});if(1===o.length&&"table"===ue(o[0])){l.preventDefault();var i=Re.fromDom(f.getDoc()),u=Un(i),a=Lr.paste(c,o[0],u);s.pasteCells(t,a).each(function(e){f.selection.setRng(e),f.focus(),d.clear(t)})}})})})};function Fr(r,o){var e=function(e){var t=o(e);if(t<=0||null===t){var n=Ae(e,r);return parseFloat(n)||0}return t},i=function(o,e){return M(e,function(e,t){var n=Ae(o,t),r=n===undefined?0:parseInt(n,10);return isNaN(r)?e:e+r},0)};return{set:function(e,t){if(!O(t)&&!t.match(/^[0-9]+$/))throw new Error(r+".set accepts only positive integer values. Value was "+t);var n=e.dom();xe(n)&&(n.style[r]=t+"px")},get:e,getOuter:e,aggregate:i,max:function(e,t,n){var r=i(e,n);return r<t?t-r:0}}}var zr=Fr("height",function(e){var t=e.dom();return Te(e)?t.getBoundingClientRect().height:t.offsetHeight}),Hr=function(e){return zr.get(e)},Ur=function(e){return zr.getOuter(e)},qr=Fr("width",function(e){return e.dom().offsetWidth}),Vr=function(e){return qr.get(e)},Gr=function(e){return qr.getOuter(e)},Yr=st.detect(),Xr=function(e,t,n){return r=Ae(e,t),o=n,i=parseFloat(r),isNaN(i)?o:i;var r,o,i},Kr=function(e){return Yr.browser.isIE()||Yr.browser.isEdge()?(n=Xr(t=e,"padding-top",0),r=Xr(t,"padding-bottom",0),o=Xr(t,"border-top-width",0),i=Xr(t,"border-bottom-width",0),u=t.dom().getBoundingClientRect().height,"border-box"===Ae(t,"box-sizing")?u:u-n-r-(o+i)):Xr(e,"height",Hr(e));var t,n,r,o,i,u},Jr=/(\d+(\.\d+)?)(\w|%)*/,$r=/(\d+(\.\d+)?)%/,Qr=/(\d+(\.\d+)?)px|em/,Zr=function(e,t){Ne(e,"height",t+"px")},eo=function(e,t,n,r){var o,i,u,a,c,l,f,s,d,m=parseInt(e,10);return s=l="%",d=(f=e).length-l.length,""!==s&&(f.length<s.length||f.substr(d,d+s.length)!==s)||"table"===ue(t)?m:(o=t,i=m,u=n,a=r,c=pn.table(o).map(function(e){var t=u(e);return Math.floor(i/100*t)}).getOr(i),a(o,c),c)},to=function(e){var t,n=Ie(t=e,"height").getOrThunk(function(){return Kr(t)+"px"});return n?eo(n,e,Hr,Zr):Hr(e)},no=function(e,t){return ve(e,t)?parseInt(pe(e,t),10):1},ro=function(e){return Ie(e,"width").fold(function(){return S.from(pe(e,"width"))},function(e){return S.some(e)})},oo=function(e,t){return e/t.pixelWidth()*100},io={percentageBasedSizeRegex:C($r),pixelBasedSizeRegex:C(Qr),setPixelWidth:function(e,t){Ne(e,"width",t+"px")},setPercentageWidth:function(e,t){Ne(e,"width",t+"%")},setHeight:Zr,getPixelWidth:function(t,n){return ro(t).fold(function(){return Vr(t)},function(e){return function(e,t,n){var r=Qr.exec(t);if(null!==r)return parseInt(r[1],10);var o=$r.exec(t);if(null!==o){var i=parseFloat(o[1]);return i/100*n.pixelWidth()}return Vr(e)}(t,e,n)})},getPercentageWidth:function(t,n){return ro(t).fold(function(){var e=Vr(t);return oo(e,n)},function(e){return function(e,t,n){var r=$r.exec(t);if(null!==r)return parseFloat(r[1]);var o=Vr(e);return oo(o,n)}(t,e,n)})},getGenericWidth:function(e){return ro(e).bind(function(e){var t=Jr.exec(e);return null!==t?S.some({width:C(parseFloat(t[1])),unit:C(t[3])}):S.none()})},setGenericWidth:function(e,t,n){Ne(e,"width",t+n)},getHeight:function(e){return n="rowspan",to(t=e)/no(t,n);var t,n},getRawWidth:ro},uo=function(n,r){io.getGenericWidth(n).each(function(e){var t=e.width()/2;io.setGenericWidth(n,t,e.unit()),io.setGenericWidth(r,t,e.unit())})},ao=function(n,r){return{left:C(n),top:C(r),translate:function(e,t){return ao(n+e,r+t)}}},co=ao,lo=function(e,t){return e!==undefined?e:t!==undefined?t:0},fo=function(e){var t,n,r=e.dom().ownerDocument,o=r.body,i=(t=Re.fromDom(r),(n=t.dom())===n.window&&t instanceof m.Window?t:de(t)?n.defaultView||n.parentWindow:null),u=r.documentElement,a=lo(i.pageYOffset,u.scrollTop),c=lo(i.pageXOffset,u.scrollLeft),l=lo(u.clientTop,o.clientTop),f=lo(u.clientLeft,o.clientLeft);return so(e).translate(c-f,a-l)},so=function(e){var t,n,r,o=e.dom(),i=o.ownerDocument,u=i.body,a=Re.fromDom(i.documentElement);return u===o?co(u.offsetLeft,u.offsetTop):(t=e,n=a||Re.fromDom(m.document.documentElement),rn(t,b(pt,n)).isSome()?(r=o.getBoundingClientRect(),co(r.left,r.top)):co(0,0))},mo=K("row","y"),go=K("col","x"),ho=function(e){return fo(e).left()+Gr(e)},po=function(e){return fo(e).left()},vo=function(e,t){return go(e,po(t))},bo=function(e,t){return go(e,ho(t))},wo=function(e){return fo(e).top()},yo=function(e,t){return mo(e,wo(t))},xo=function(e,t){return mo(e,wo(t)+Ur(t))},Co=function(n,t,r){if(0===r.length)return[];var e=P(r.slice(1),function(e,t){return e.map(function(e){return n(t,e)})}),o=r[r.length-1].map(function(e){return t(r.length-1,e)});return e.concat([o])},So={height:{delta:o,positions:function(e){return Co(yo,xo,e)},edge:wo},rtl:{delta:function(e){return-e},edge:ho,positions:function(e){return Co(bo,vo,e)}},ltr:{delta:o,edge:po,positions:function(e){return Co(vo,bo,e)}}},Ro={ltr:So.ltr,rtl:So.rtl};function To(t){var n=function(e){return t(e).isRtl()?Ro.rtl:Ro.ltr};return{delta:function(e,t){return n(t).delta(e,t)},edge:function(e){return n(e).edge(e)},positions:function(e,t){return n(t).positions(e,t)}}}var Do=function(e){var t=vn(e);return xn.generate(t).grid()},Oo=function(){return(Oo=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e}).apply(this,arguments)},Eo=function(e){for(var t=[],n=function(e){t.push(e)},r=0;r<e.length;r++)e[r].each(n);return t},No=function(e,t){for(var n=0;n<e.length;n++){var r=t(e[n],n);if(r.isSome())return r}return S.none()},ko=function(e,t,n,r){n===r?be(e,t):ge(e,t,n)},Ao=function(o,e){var i=[],u=[],t=function(e,t){0<e.length?function(e,t){var n=un(o,t).getOrThunk(function(){var e=Re.fromTag(t,wt(o).dom());return Nt(o,e),e});It(n);var r=P(e,function(e){e.isNew()&&i.push(e.element());var t=e.element();return It(t),I(e.cells(),function(e){e.isNew()&&u.push(e.element()),ko(e.element(),"colspan",e.colspan(),1),ko(e.element(),"rowspan",e.rowspan(),1),Nt(t,e.element())}),t});Pt(n,r)}(e,t):un(o,t).each(Bt)},n=[],r=[],a=[];return I(e,function(e){switch(e.section()){case"thead":n.push(e);break;case"tbody":r.push(e);break;case"tfoot":a.push(e)}}),t(n,"thead"),t(r,"tbody"),t(a,"tfoot"),{newRows:C(i),newCells:C(u)}},Po=function(e){return P(e,function(e){var n=Wn(e.element());return I(e.cells(),function(e){var t=Mn(e.element());ko(t,"colspan",e.colspan(),1),ko(t,"rowspan",e.rowspan(),1),Nt(n,t)}),n})},Io=function(e,t){var n=pe(e,t);return n===undefined||""===n?[]:n.split(" ")},Bo=function(e){return e.dom().classList!==undefined},Wo=function(e,t){return o=t,i=Io(n=e,r="class").concat([o]),ge(n,r,i.join(" ")),!0;var n,r,o,i},Mo=function(e,t){return o=t,0<(i=B(Io(n=e,r="class"),function(e){return e!==o})).length?ge(n,r,i.join(" ")):be(n,r),!1;var n,r,o,i},_o=function(e,t){Bo(e)?e.dom().classList.add(t):Wo(e,t)},Lo=function(e){0===(Bo(e)?e.dom().classList:Io(e,"class")).length&&be(e,"class")},jo=function(e,t){return Bo(e)&&e.dom().classList.contains(t)},Fo=function(e,t){for(var n=[],r=e;r<t;r++)n.push(r);return n},zo=function(t,n){if(n<0||n>=t.length-1)return S.none();var e=t[n].fold(function(){var e=q(t.slice(0,n));return No(e,function(e,t){return e.map(function(e){return{value:e,delta:t+1}})})},function(e){return S.some({value:e,delta:0})}),r=t[n+1].fold(function(){var e=t.slice(n+1);return No(e,function(e,t){return e.map(function(e){return{value:e,delta:t+1}})})},function(e){return S.some({value:e,delta:1})});return e.bind(function(n){return r.map(function(e){var t=e.delta+n.delta;return Math.abs(e.value-n.value)/t})})},Ho=function(e,t,n){var r=e();return _(r,t).orThunk(function(){return S.from(r[0]).orThunk(n)}).map(function(e){return e.element()})},Uo=function(n){var e=n.grid(),t=Fo(0,e.columns()),r=Fo(0,e.rows());return P(t,function(t){return Ho(function(){return H(r,function(e){return xn.getAt(n,e,t).filter(function(e){return e.column()===t}).fold(C([]),function(e){return[e]})})},function(e){return 1===e.colspan()},function(){return xn.getAt(n,0,t)})})},qo=function(n){var e=n.grid(),t=Fo(0,e.rows()),r=Fo(0,e.columns());return P(t,function(t){return Ho(function(){return H(r,function(e){return xn.getAt(n,t,e).filter(function(e){return e.row()===t}).fold(C([]),function(e){return[e]})})},function(e){return 1===e.rowspan()},function(){return xn.getAt(n,t,0)})})},Vo=function(e){var t=e.replace(/\./g,"-");return{resolve:function(e){return t+"-"+e}}},Go={resolve:Vo("ephox-snooker").resolve},Yo=function(e,t,n,r,o){var i=Re.fromTag("div");return ke(i,{position:"absolute",left:t-r/2+"px",top:n+"px",height:o+"px",width:r+"px"}),he(i,{"data-column":e,role:"presentation"}),i},Xo=function(e,t,n,r,o){var i=Re.fromTag("div");return ke(i,{position:"absolute",left:t+"px",top:n-o/2+"px",height:o+"px",width:r+"px"}),he(i,{"data-row":e,role:"presentation"}),i},Ko=Go.resolve("resizer-bar"),Jo=Go.resolve("resizer-rows"),$o=Go.resolve("resizer-cols"),Qo=function(e){var t=Jt(e.parent(),"."+Ko);I(t,Bt)},Zo=function(n,e,r){var o=n.origin();I(e,function(e,t){e.each(function(e){var t=r(o,e);_o(t,Ko),Nt(n.parent(),t)})})},ei=function(e,t,n,r,o,i){var u,a,c,l,f=fo(t),s=0<n.length?o.positions(n,t):[];u=e,a=s,c=f,l=Gr(t),Zo(u,a,function(e,t){var n=Xo(t.row(),c.left()-e.left(),t.y()-e.top(),l,7);return _o(n,Jo),n});var d,m,g,h,p=0<r.length?i.positions(r,t):[];d=e,m=p,g=f,h=Ur(t),Zo(d,m,function(e,t){var n=Yo(t.col(),t.x()-e.left(),g.top()-e.top(),7,h);return _o(n,$o),n})},ti=function(e,t){var n=Jt(e.parent(),"."+Ko);I(n,t)},ni=function(e,t,n,r){Qo(e);var o=vn(t),i=xn.generate(o),u=qo(i),a=Uo(i);ei(e,t,u,a,n,r)},ri=function(e){ti(e,function(e){Ne(e,"display","none")})},oi=function(e){ti(e,function(e){Ne(e,"display","block")})},ii=Qo,ui=function(e){return jo(e,Jo)},ai=function(e){return jo(e,$o)},ci=function(e,t){return qt(t,e.section())},li=function(e,t){return e.cells()[t]},fi={addCell:function(e,t,n){var r=e.cells(),o=r.slice(0,t),i=r.slice(t),u=o.concat([n]).concat(i);return ci(e,u)},setCells:ci,mutateCell:function(e,t,n){e.cells()[t]=n},getCell:li,getCellElement:function(e,t){return li(e,t).element()},mapCells:function(e,t){var n=e.cells(),r=P(n,t);return qt(r,e.section())},cellLength:function(e){return e.cells().length}},si=function(e,t){if(0===e.length)return 0;var n=e[0];return L(e,function(e){return!t(n.element(),e.element())}).fold(function(){return e.length},function(e){return e})},di=function(e,t,n,r){var o,i,u,a,c=(o=e,i=t,o[i]).cells().slice(n),l=si(c,r),f=(u=e,a=n,P(u,function(e){return fi.getCell(e,a)})).slice(t),s=si(f,r);return{colspan:C(l),rowspan:C(s)}},mi=function(o,i){var u=P(o,function(e,t){return P(e.cells(),function(e,t){return!1})});return P(o,function(e,r){var t=H(e.cells(),function(e,t){if(!1===u[r][t]){var n=di(o,r,t,i);return function(e,t,n,r){for(var o=e;o<e+n;o++)for(var i=t;i<t+r;i++)u[o][i]=!0}(r,t,n.rowspan(),n.colspan()),[jt(e.element(),n.rowspan(),n.colspan(),e.isNew())]}return[]});return Vt(t,e.section())})},gi=function(e,t,n){for(var r=[],o=0;o<e.grid().rows();o++){for(var i=[],u=0;u<e.grid().columns();u++){var a=xn.getAt(e,o,u).map(function(e){return Ht(e.element(),n)}).getOrThunk(function(){return Ht(t.gap(),!0)});i.push(a)}var c=qt(i,e.all()[o].section());r.push(c)}return r},hi=function(e,r){return P(e,function(e){var t,n=(t=e.details(),No(t,function(e){return yt(e.element()).map(function(e){var t=yt(e).isNone();return Ht(e,t)})}).getOrThunk(function(){return Ht(r.row(),!0)}));return Ut(n.element(),e.details(),e.section(),n.isNew())})},pi=function(e,t){var n=mi(e,pt);return hi(n,t)},vi=function(e,t){var n=z(P(e.all(),function(e){return e.cells()}));return _(n,function(e){return pt(t,e.element())})},bi=function(a,c,l,f,s){return function(n,r,e,o,i){var t=vn(r),u=xn.generate(t);return c(u,e).map(function(e){var t=gi(u,o,!1),n=a(t,e,pt,s(o)),r=pi(n.grid(),o);return{grid:C(r),cursor:n.cursor}}).fold(function(){return S.none()},function(e){var t=Ao(r,e.grid());return l(r,e.grid(),i),f(r),ni(n,r,So.height,i),S.some({cursor:e.cursor,newRows:t.newRows,newCells:t.newCells})})}},wi=function(t,e){return pn.cell(e.element()).bind(function(e){return vi(t,e)})},yi=function(t,e){var n=P(e.selection(),function(e){return pn.cell(e).bind(function(e){return vi(t,e)})}),r=Eo(n);return 0<r.length?S.some({cells:r,generators:e.generators,clipboard:e.clipboard}):S.none()},xi=function(t,e){var n=P(e.selection(),function(e){return pn.cell(e).bind(function(e){return vi(t,e)})}),r=Eo(n);return 0<r.length?S.some(r):S.none()},Ci=function(n){return{is:function(e){return n===e},isValue:a,isError:f,getOr:C(n),getOrThunk:C(n),getOrDie:C(n),or:function(e){return Ci(n)},orThunk:function(e){return Ci(n)},fold:function(e,t){return t(n)},map:function(e){return Ci(e(n))},mapError:function(e){return Ci(n)},each:function(e){e(n)},bind:function(e){return e(n)},exists:function(e){return e(n)},forall:function(e){return e(n)},toOption:function(){return S.some(n)}}},Si=function(n){return{is:f,isValue:f,isError:a,getOr:o,getOrThunk:function(e){return e()},getOrDie:function(){return e=String(n),function(){throw new Error(e)}();var e},or:function(e){return e},orThunk:function(e){return e()},fold:function(e,t){return e(n)},map:function(e){return Si(n)},mapError:function(e){return Si(e(n))},each:y,bind:function(e){return Si(n)},exists:f,forall:a,toOption:S.none}},Ri={value:Ci,error:Si,fromOption:function(e,t){return e.fold(function(){return Si(t)},Ci)}},Ti=function(e,t){return P(e,function(){return Ht(t.cell(),!0)})},Di=function(t,e,n){return t.concat(function(e,t){for(var n=[],r=0;r<e;r++)n.push(t(r));return n}(e,function(e){return fi.setCells(t[t.length-1],Ti(t[t.length-1].cells(),n))}))},Oi=function(e,t,n){return P(e,function(e){return fi.setCells(e,e.cells().concat(Ti(Fo(0,t),n)))})},Ei=function(e,t,n){if(e.row()>=t.length||e.column()>fi.cellLength(t[0]))return Ri.error("invalid start address out of table bounds, row: "+e.row()+", column: "+e.column());var r=t.slice(e.row()),o=r[0].cells().slice(e.column()),i=fi.cellLength(n[0]),u=n.length;return Ri.value({rowDelta:C(r.length-u),colDelta:C(o.length-i)})},Ni=function(e,t){var n=fi.cellLength(e[0]),r=fi.cellLength(t[0]);return{rowDelta:C(0),colDelta:C(n-r)}},ki=function(e,t,n){var r=t.colDelta()<0?Oi:o;return(t.rowDelta()<0?Di:o)(r(e,Math.abs(t.colDelta()),n),Math.abs(t.rowDelta()),n)},Ai=function(e,t,n,r){if(0===e.length)return e;for(var o=t.startRow();o<=t.finishRow();o++)for(var i=t.startCol();i<=t.finishCol();i++)fi.mutateCell(e[o],i,Ht(r(),!1));return e},Pi=function(e,t,n,r){for(var o=!0,i=0;i<e.length;i++)for(var u=0;u<fi.cellLength(e[0]);u++){var a=n(fi.getCellElement(e[i],u),t);!0===a&&!1===o?fi.mutateCell(e[i],u,Ht(r(),!0)):!0===a&&(o=!1)}return e},Ii=function(i,n,u,a){if(0<n&&n<i.length){var e=i[n-1].cells(),t=(r=u,M(e,function(e,t){return A(e,function(e){return r(e.element(),t.element())})?e:e.concat([t])},[]));I(t,function(r){for(var o=S.none(),e=function(n){for(var e=function(t){var e=i[n].cells()[t];u(e.element(),r.element())&&(o.isNone()&&(o=S.some(a())),o.each(function(e){fi.mutateCell(i[n],t,Ht(e,!0))}))},t=0;t<fi.cellLength(i[0]);t++)e(t)},t=n;t<i.length;t++)e(t)})}var r;return i},Bi=function(n,r,o,i,u){return Ei(n,r,o).map(function(e){var t=ki(r,e,i);return function(e,t,n,r,o){for(var i,u,a,c,l,f=e.row(),s=e.column(),d=f+n.length,m=s+fi.cellLength(n[0]),g=f;g<d;g++)for(var h=s;h<m;h++){i=t,u=g,a=h,l=c=void 0,c=b(o,fi.getCell(i[u],a).element()),l=i[u],1<i.length&&1<fi.cellLength(l)&&(0<a&&c(fi.getCellElement(l,a-1))||a<l.cells().length-1&&c(fi.getCellElement(l,a+1))||0<u&&c(fi.getCellElement(i[u-1],a))||u<i.length-1&&c(fi.getCellElement(i[u+1],a)))&&Pi(t,fi.getCellElement(t[g],h),o,r.cell);var p=fi.getCellElement(n[g-f],h-s),v=r.replace(p);fi.mutateCell(t[g],h,Ht(v,!0))}return t}(n,t,o,i,u)})},Wi=function(e,t,n,r,o){Ii(t,e,o,r.cell);var i=Ni(n,t),u=ki(n,i,r),a=Ni(t,u),c=ki(t,a,r);return c.slice(0,e).concat(u).concat(c.slice(e,c.length))},Mi=function(n,r,e,o,i){var t=n.slice(0,r),u=n.slice(r),a=fi.mapCells(n[e],function(e,t){return 0<r&&r<n.length&&o(fi.getCellElement(n[r-1],t),fi.getCellElement(n[r],t))?fi.getCell(n[r],t):Ht(i(e.element(),o),!0)});return t.concat([a]).concat(u)},_i=function(e,n,r,o,i){return P(e,function(e){var t=0<n&&n<fi.cellLength(e)&&o(fi.getCellElement(e,n-1),fi.getCellElement(e,n))?fi.getCell(e,n):Ht(i(fi.getCellElement(e,r),o),!0);return fi.addCell(e,n,t)})},Li=function(e,r,o,i,u){var a=o+1;return P(e,function(e,t){var n=t===r?Ht(u(fi.getCellElement(e,o),i),!0):fi.getCell(e,o);return fi.addCell(e,a,n)})},ji=function(e,t,n,r,o){var i=t+1,u=e.slice(0,i),a=e.slice(i),c=fi.mapCells(e[t],function(e,t){return t===n?Ht(o(e.element(),r),!0):e});return u.concat([c]).concat(a)},Fi=function(e,t,n){return e.slice(0,t).concat(e.slice(n+1))},zi=function(e,n,r){var t=P(e,function(e){var t=e.cells().slice(0,n).concat(e.cells().slice(r+1));return qt(t,e.section())});return B(t,function(e){return 0<e.cells().length})},Hi=function(e,n,r,o){return P(e,function(e){return fi.mapCells(e,function(e){return t=e,A(n,function(e){return r(t.element(),e.element())})?Ht(o(e.element(),r),!0):e;var t})})},Ui=function(e,t,n,r){return fi.getCellElement(e[t],n)!==undefined&&0<t&&r(fi.getCellElement(e[t-1],n),fi.getCellElement(e[t],n))},qi=function(e,t,n){return 0<t&&n(fi.getCellElement(e,t-1),fi.getCellElement(e,t))},Vi=function(n,r,o,e){var t=H(n,function(e,t){return Ui(n,t,r,o)||qi(e,r,o)?[]:[fi.getCell(e,r)]});return Hi(n,t,o,e)},Gi=function(n,r,o,e){var i=n[r],t=H(i.cells(),function(e,t){return Ui(n,r,t,o)||qi(i,t,o)?[]:[e]});return Hi(n,t,o,e)},Yi=Nr([{none:[]},{only:["index"]},{left:["index","next"]},{middle:["prev","index","next"]},{right:["prev","index"]}]),Xi=Oo({},Yi),Ki=function(e,t,i,u){var n,r,a=e.slice(0),o=(r=t,0===(n=e).length?Xi.none():1===n.length?Xi.only(0):0===r?Xi.left(0,1):r===n.length-1?Xi.right(r-1,r):0<r&&r<n.length-1?Xi.middle(r-1,r,r+1):Xi.none()),c=function(e){return P(e,C(0))},l=C(c(a)),f=function(e,t){if(0<=i){var n=Math.max(u.minCellWidth(),a[t]-i);return c(a.slice(0,e)).concat([i,n-a[t]]).concat(c(a.slice(t+1)))}var r=Math.max(u.minCellWidth(),a[e]+i),o=a[e]-r;return c(a.slice(0,e)).concat([r-a[e],o]).concat(c(a.slice(t+1)))},s=f;return o.fold(l,function(e){return u.singleColumnWidth(a[e],i)},s,function(e,t,n){return f(t,n)},function(e,t){if(0<=i)return c(a.slice(0,t)).concat([i]);var n=Math.max(u.minCellWidth(),a[t]+i);return c(a.slice(0,t)).concat([n-a[t]])})},Ji=function(e,t){return ve(e,t)&&1<parseInt(pe(e,t),10)},$i={hasColspan:function(e){return Ji(e,"colspan")},hasRowspan:function(e){return Ji(e,"rowspan")},minWidth:C(10),minHeight:C(10),getInt:function(e,t){return parseInt(Ae(e,t),10)}},Qi=function(e,t,n){return Ie(e,t).fold(function(){return n(e)+"px"},function(e){return e})},Zi=function(e,t){return Qi(e,"width",function(e){return io.getPixelWidth(e,t)})},eu=function(e){return Qi(e,"height",io.getHeight)},tu=function(e,t,n,r,o){var i=Uo(e),u=P(i,function(e){return e.map(t.edge)});return P(i,function(e,t){return e.filter(g($i.hasColspan)).fold(function(){var e=zo(u,t);return r(e)},function(e){return n(e,o)})})},nu=function(e){return e.map(function(e){return e+"px"}).getOr("")},ru=function(e,t,n,r){var o=qo(e),i=P(o,function(e){return e.map(t.edge)});return P(o,function(e,t){return e.filter(g($i.hasRowspan)).fold(function(){var e=zo(i,t);return r(e)},function(e){return n(e)})})},ou={getRawWidths:function(e,t,n){return tu(e,t,Zi,nu,n)},getPixelWidths:function(e,t,n){return tu(e,t,io.getPixelWidth,function(e){return e.getOrThunk(n.minCellWidth)},n)},getPercentageWidths:function(e,t,n){return tu(e,t,io.getPercentageWidth,function(e){return e.fold(function(){return n.minCellWidth()},function(e){return e/n.pixelWidth()*100})},n)},getPixelHeights:function(e,t){return ru(e,t,io.getHeight,function(e){return e.getOrThunk($i.minHeight)})},getRawHeights:function(e,t){return ru(e,t,eu,nu)}},iu=function(e,t,n){for(var r=0,o=e;o<t;o++)r+=n[o]!==undefined?n[o]:0;return r},uu=function(e,n){var t=xn.justCells(e);return P(t,function(e){var t=iu(e.column(),e.column()+e.colspan(),n);return{element:e.element,width:C(t),colspan:e.colspan}})},au=function(e,n){var t=xn.justCells(e);return P(t,function(e){var t=iu(e.row(),e.row()+e.rowspan(),n);return{element:e.element,height:C(t),rowspan:e.rowspan}})},cu=function(e,n){return P(e.all(),function(e,t){return{element:e.element,height:C(n[t])}})},lu=function(e){var t=o;return{width:C(e),pixelWidth:C(e),getWidths:ou.getPixelWidths,getCellDelta:t,singleColumnWidth:function(e,t){return[Math.max($i.minWidth(),e+t)-e]},minCellWidth:$i.minWidth,setElementWidth:io.setPixelWidth,setTableWidth:function(e,t,n){var r=W(t,function(e,t){return e+t},0);io.setPixelWidth(e,r)}}},fu=function(e,t){var n,r,o,i,u=io.percentageBasedSizeRegex().exec(t);if(null!==u)return n=u[1],r=e,o=parseFloat(n),i=Vr(r),{width:C(o),pixelWidth:C(i),getWidths:ou.getPercentageWidths,getCellDelta:function(e){return e/i*100},singleColumnWidth:function(e,t){return[100-e]},minCellWidth:function(){return $i.minWidth()/i*100},setElementWidth:io.setPercentageWidth,setTableWidth:function(e,t,n){var r=n/100*o;io.setPercentageWidth(e,o+r)}};var a=io.pixelBasedSizeRegex().exec(t);if(null!==a){var c=parseInt(a[1],10);return lu(c)}var l=Vr(e);return lu(l)},su=function(t){return io.getRawWidth(t).fold(function(){var e=Vr(t);return lu(e)},function(e){return fu(t,e)})},du=function(e){return xn.generate(e)},mu=function(e){var t=vn(e);return du(t)},gu=function(e,t,n,r){var o=su(e),i=o.getCellDelta(t),u=mu(e),a=o.getWidths(u,r,o),c=Ki(a,n,i,o),l=P(c,function(e,t){return e+a[t]}),f=uu(u,l);I(f,function(e){o.setElementWidth(e.element(),e.width())}),n===u.grid().columns()-1&&o.setTableWidth(e,l,i)},hu=function(e,n,r,t){var o=mu(e),i=ou.getPixelHeights(o,t),u=P(i,function(e,t){return r===t?Math.max(n+e,$i.minHeight()):e}),a=au(o,u),c=cu(o,u);I(c,function(e){io.setHeight(e.element(),e.height())}),I(a,function(e){io.setHeight(e.element(),e.height())});var l=W(u,function(e,t){return e+t},0);io.setHeight(e,l)},pu=function(e,t,n){var r=su(e),o=du(t),i=r.getWidths(o,n,r),u=uu(o,i);I(u,function(e){r.setElementWidth(e.element(),e.width())}),0<u.length&&r.setTableWidth(e,i,r.getCellDelta(0))},vu=function(e){var t=e,n=function(){return t};return{get:n,set:function(e){t=e},clone:function(){return vu(n())}}},bu=function(r,o,i){if(0===o.length)throw new Error("You must specify at least one required field.");return Z("required",o),ee(o),function(t){var n=V(t);U(o,function(e){return k(n,e)})||$(o,n),r(o,n);var e=B(o,function(e){return!i.validate(t[e],e)});return 0<e.length&&function(e,t){throw new Error("All values need to be of type: "+t+". Keys ("+J(e).join(", ")+") were not.")}(e,i.label),t}},wu=function(t,e){var n=B(e,function(e){return!k(t,e)});0<n.length&&Q(n)},yu=function(e){return bu(wu,e,{validate:D,label:"function"})},xu=yu(["cell","row","replace","gap"]),Cu=function(e){var t=ve(e,"colspan")?parseInt(pe(e,"colspan"),10):1,n=ve(e,"rowspan")?parseInt(pe(e,"rowspan"),10):1;return{element:C(e),colspan:C(t),rowspan:C(n)}},Su=function(r,o){void 0===o&&(o=Cu),xu(r);var n=vu(S.none()),i=function(e){var t,n=o(e);return t=n,r.cell(t)},u=function(e){var t=i(e);return n.get().isNone()&&n.set(S.some(t)),a=S.some({item:e,replacement:t}),t},a=S.none();return{getOrInit:function(t,n){return a.fold(function(){return u(t)},function(e){return n(t,e.item)?e.replacement:u(t)})},cursor:n.get}},Ru=function(a,c){return function(r){var o=vu(S.none());xu(r);var i=[],u=function(e){var t={scope:a},n=r.replace(e,c,t);return i.push({item:e,sub:n}),o.get().isNone()&&o.set(S.some(n)),n};return{replaceOrInit:function(t,n){return(r=t,o=n,_(i,function(e){return o(e.item,r)})).fold(function(){return u(t)},function(e){return n(t,e.item)?e.sub:u(t)});var r,o},cursor:o.get}}},Tu=function(n){xu(n);var e=vu(S.none());return{combine:function(t){return e.get().isNone()&&e.set(S.some(t)),function(){var e=n.cell({element:C(t),colspan:C(1),rowspan:C(1)});return Be(e,"width"),Be(t,"width"),e}},cursor:e.get}},Du=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","table","thead","tfoot","tbody","caption","tr","td","th","h1","h2","h3","h4","h5","h6","blockquote","pre","address"],Ou=function(e,t){var n=e.property().name(t);return k(Du,n)},Eu=function(e,t){return k(["br","img","hr","input"],e.property().name(t))},Nu=Ou,ku=function(e,t){var n=e.property().name(t);return k(["ol","ul"],n)},Au=Eu,Pu=ir(),Iu=function(e){return Nu(Pu,e)},Bu=function(e){return ku(Pu,e)},Wu=function(e){return Au(Pu,e)},Mu=function(e){var t,i=function(e){return"br"===ue(e)},n=function(o){return Pn(o).bind(function(n){var r=St(n).map(function(e){return!!Iu(e)||!!Wu(e)&&"img"!==ue(e)}).getOr(!1);return yt(n).map(function(e){return!0===r||"li"===ue(t=e)||rn(t,Bu).isSome()||i(n)||Iu(e)&&!pt(o,e)?[]:[Re.fromTag("br")];var t})}).getOr([])},r=0===(t=H(e,function(e){var t=Rt(e);return U(t,function(e){return i(e)||se(e)&&0===Tn(e).trim().length})?[]:t.concat(n(e))})).length?[Re.fromTag("br")]:t;It(e[0]),Pt(e[0],r)},_u=function(e){0===pn.cells(e).length&&Bt(e)},Lu=K("grid","cursor"),ju=function(e,t,n){return Fu(e,t,n).orThunk(function(){return Fu(e,0,0)})},Fu=function(e,t,n){return S.from(e[t]).bind(function(e){return S.from(e.cells()[n]).bind(function(e){return S.from(e.element())})})},zu=function(e,t,n){return Lu(e,Fu(e,t,n))},Hu=function(e){return M(e,function(e,t){return A(e,function(e){return e.row()===t.row()})?e:e.concat([t])},[]).sort(function(e,t){return e.row()-t.row()})},Uu=function(e){return M(e,function(e,t){return A(e,function(e){return e.column()===t.column()})?e:e.concat([t])},[]).sort(function(e,t){return e.column()-t.column()})},qu=function(e,t,n){var r=bn(e,n),o=xn.generate(r);return gi(o,t,!0)},Vu=pu,Gu={insertRowBefore:bi(function(e,t,n,r){var o=t.row(),i=t.row(),u=Mi(e,i,o,n,r.getOrInit);return zu(u,i,t.column())},wi,y,y,Su),insertRowsBefore:bi(function(e,t,n,r){var o=t[0].row(),i=t[0].row(),u=Hu(t),a=M(u,function(e,t){return Mi(e,i,o,n,r.getOrInit)},e);return zu(a,i,t[0].column())},xi,y,y,Su),insertRowAfter:bi(function(e,t,n,r){var o=t.row(),i=t.row()+t.rowspan(),u=Mi(e,i,o,n,r.getOrInit);return zu(u,i,t.column())},wi,y,y,Su),insertRowsAfter:bi(function(e,t,n,r){var o=Hu(t),i=o[o.length-1].row(),u=o[o.length-1].row()+o[o.length-1].rowspan(),a=M(o,function(e,t){return Mi(e,u,i,n,r.getOrInit)},e);return zu(a,u,t[0].column())},xi,y,y,Su),insertColumnBefore:bi(function(e,t,n,r){var o=t.column(),i=t.column(),u=_i(e,i,o,n,r.getOrInit);return zu(u,t.row(),i)},wi,Vu,y,Su),insertColumnsBefore:bi(function(e,t,n,r){var o=Uu(t),i=o[0].column(),u=o[0].column(),a=M(o,function(e,t){return _i(e,u,i,n,r.getOrInit)},e);return zu(a,t[0].row(),u)},xi,Vu,y,Su),insertColumnAfter:bi(function(e,t,n,r){var o=t.column(),i=t.column()+t.colspan(),u=_i(e,i,o,n,r.getOrInit);return zu(u,t.row(),i)},wi,Vu,y,Su),insertColumnsAfter:bi(function(e,t,n,r){var o=t[t.length-1].column(),i=t[t.length-1].column()+t[t.length-1].colspan(),u=Uu(t),a=M(u,function(e,t){return _i(e,i,o,n,r.getOrInit)},e);return zu(a,t[0].row(),i)},xi,Vu,y,Su),splitCellIntoColumns:bi(function(e,t,n,r){var o=Li(e,t.row(),t.column(),n,r.getOrInit);return zu(o,t.row(),t.column())},wi,Vu,y,Su),splitCellIntoRows:bi(function(e,t,n,r){var o=ji(e,t.row(),t.column(),n,r.getOrInit);return zu(o,t.row(),t.column())},wi,y,y,Su),eraseColumns:bi(function(e,t,n,r){var o=Uu(t),i=zi(e,o[0].column(),o[o.length-1].column()),u=ju(i,t[0].row(),t[0].column());return Lu(i,u)},xi,Vu,_u,Su),eraseRows:bi(function(e,t,n,r){var o=Hu(t),i=Fi(e,o[0].row(),o[o.length-1].row()),u=ju(i,t[0].row(),t[0].column());return Lu(i,u)},xi,y,_u,Su),makeColumnHeader:bi(function(e,t,n,r){var o=Vi(e,t.column(),n,r.replaceOrInit);return zu(o,t.row(),t.column())},wi,y,y,Ru("row","th")),unmakeColumnHeader:bi(function(e,t,n,r){var o=Vi(e,t.column(),n,r.replaceOrInit);return zu(o,t.row(),t.column())},wi,y,y,Ru(null,"td")),makeRowHeader:bi(function(e,t,n,r){var o=Gi(e,t.row(),n,r.replaceOrInit);return zu(o,t.row(),t.column())},wi,y,y,Ru("col","th")),unmakeRowHeader:bi(function(e,t,n,r){var o=Gi(e,t.row(),n,r.replaceOrInit);return zu(o,t.row(),t.column())},wi,y,y,Ru(null,"td")),mergeCells:bi(function(e,t,n,r){var o=t.cells();Mu(o);var i=Ai(e,t.bounds(),n,C(o[0]));return Lu(i,S.from(o[0]))},function(e,t){return t.mergable()},y,y,Tu),unmergeCells:bi(function(e,t,n,r){var o=W(t,function(e,t){return Pi(e,t,n,r.combine(t))},e);return Lu(o,S.from(t[0]))},function(e,t){return t.unmergable()},Vu,y,Tu),pasteCells:bi(function(e,n,t,r){var o,i,u,a,c=(o=n.clipboard(),i=n.generators(),u=vn(o),a=xn.generate(u),gi(a,i,!0)),l=_t(n.row(),n.column());return Bi(l,e,c,n.generators(),t).fold(function(){return Lu(e,S.some(n.element()))},function(e){var t=ju(e,n.row(),n.column());return Lu(e,t)})},function(t,n){return pn.cell(n.element()).bind(function(e){return vi(t,e).map(function(e){return Oo({},e,{generators:n.generators,clipboard:n.clipboard})})})},Vu,y,Su),pasteRowsBefore:bi(function(e,t,n,r){var o=e[t.cells[0].row()],i=t.cells[0].row(),u=qu(t.clipboard(),t.generators(),o),a=Wi(i,e,u,t.generators(),n),c=ju(a,t.cells[0].row(),t.cells[0].column());return Lu(a,c)},yi,y,y,Su),pasteRowsAfter:bi(function(e,t,n,r){var o=e[t.cells[0].row()],i=t.cells[t.cells.length-1].row()+t.cells[t.cells.length-1].rowspan(),u=qu(t.clipboard(),t.generators(),o),a=Wi(i,e,u,t.generators(),n),c=ju(a,t.cells[0].row(),t.cells[0].column());return Lu(a,c)},yi,y,y,Su)},Yu=function(e){return Re.fromDom(e.getBody())},Xu=function(e){return e.getBoundingClientRect().width},Ku=function(e){return e.getBoundingClientRect().height},Ju=function(t){return function(e){return pt(e,Yu(t))}},$u=function(e){return/^[0-9]+$/.test(e)&&(e+="px"),e},Qu=function(e){var t=Jt(e,"td[data-mce-style],th[data-mce-style]");be(e,"data-mce-style"),I(t,function(e){be(e,"data-mce-style")})},Zu={isRtl:C(!1)},ea={isRtl:C(!0)},ta={directionAt:function(e){return"rtl"==("rtl"===Ae(e,"direction")?"rtl":"ltr")?ea:Zu}},na=["tableprops","tabledelete","|","tableinsertrowbefore","tableinsertrowafter","tabledeleterow","|","tableinsertcolbefore","tableinsertcolafter","tabledeletecol"],ra={"border-collapse":"collapse",width:"100%"},oa={border:"1"},ia=function(e){return e.getParam("table_cell_advtab",!0,"boolean")},ua=function(e){return e.getParam("table_row_advtab",!0,"boolean")},aa=function(e){return e.getParam("table_advtab",!0,"boolean")},ca=function(e){return e.getParam("table_style_by_css",!1,"boolean")},la=function(e){return e.getParam("table_cell_class_list",[],"array")},fa=function(e){return e.getParam("table_row_class_list",[],"array")},sa=function(e){return e.getParam("table_class_list",[],"array")},da=function(e){return!1===e.getParam("table_responsive_width")},ma=function(e,t){return e.fire("newrow",{node:t})},ga=function(e,t){return e.fire("newcell",{node:t})},ha=function(e,t,n,r){e.fire("ObjectResizeStart",{target:t,width:n,height:r})},pa=function(e,t,n,r){e.fire("ObjectResized",{target:t,width:n,height:r})},va=function(f,e){var t,n=function(e){return"table"===ue(Yu(e))},s=(t=f.getParam("table_clone_elements"),w(t)?S.some(t.split(/[ ,]/)):Array.isArray(t)?S.some(t):S.none()),r=function(u,a,c,l){return function(e,t){Qu(e);var n=l(),r=Re.fromDom(f.getDoc()),o=To(ta.directionAt),i=Hn(c,r,s);return a(e)?u(n,e,t,i,o).bind(function(e){return I(e.newRows(),function(e){ma(f,e.dom())}),I(e.newCells(),function(e){ga(f,e.dom())}),e.cursor().map(function(e){var t=f.dom.createRng();return t.setStart(e.dom(),0),t.setEnd(e.dom(),0),t})}):S.none()}};return{deleteRow:r(Gu.eraseRows,function(e){var t=Do(e);return!1===n(f)||1<t.rows()},y,e),deleteColumn:r(Gu.eraseColumns,function(e){var t=Do(e);return!1===n(f)||1<t.columns()},y,e),insertRowsBefore:r(Gu.insertRowsBefore,a,y,e),insertRowsAfter:r(Gu.insertRowsAfter,a,y,e),insertColumnsBefore:r(Gu.insertColumnsBefore,a,uo,e),insertColumnsAfter:r(Gu.insertColumnsAfter,a,uo,e),mergeCells:r(Gu.mergeCells,a,y,e),unmergeCells:r(Gu.unmergeCells,a,y,e),pasteRowsBefore:r(Gu.pasteRowsBefore,a,y,e),pasteRowsAfter:r(Gu.pasteRowsAfter,a,y,e),pasteCells:r(Gu.pasteCells,a,y,e)}},ba=function(e,t,r){var n=vn(e),o=xn.generate(n);return xi(o,t).map(function(e){var t=gi(o,r,!1).slice(e[0].row(),e[e.length-1].row()+e[e.length-1].rowspan()),n=pi(t,r);return Po(n)})},wa=tinymce.util.Tools.resolve("tinymce.util.Tools"),ya=function(e,t,n){n&&e.formatter.apply("align"+n,{},t)},xa=function(e,t,n){n&&e.formatter.apply("valign"+n,{},t)},Ca=function(t,n){wa.each("left center right".split(" "),function(e){t.formatter.remove("align"+e,{},n)})},Sa=function(t,n){wa.each("top middle bottom".split(" "),function(e){t.formatter.remove("valign"+e,{},n)})},Ra=function(o,e,i){var t;return t=function(e,t){for(var n=0;n<t.length;n++){var r=o.getStyle(t[n],i);if(void 0===e&&(e=r),e!==r)return""}return e}(t,o.select("td,th",e))},Ta=function(e,t){var n=e.dom,r=t.control.rootControl,o=r.toJSON(),i=n.parseStyle(o.style);i["border-style"]=o.borderStyle,i["border-color"]=o.borderColor,i["background-color"]=o.backgroundColor,i.width=o.width?$u(o.width):"",i.height=o.height?$u(o.height):"",r.find("#style").value(n.serializeStyle(n.parseStyle(n.serializeStyle(i))))},Da=function(e,t){var n=e.dom,r=t.control.rootControl,o=r.toJSON(),i=n.parseStyle(o.style);r.find("#borderStyle").value(i["border-style"]||""),r.find("#borderColor").value(i["border-color"]||""),r.find("#backgroundColor").value(i["background-color"]||""),r.find("#width").value(i.width||""),r.find("#height").value(i.height||"")},Oa={createStyleForm:function(n){var e=function(){var e=n.getParam("color_picker_callback");if(e)return function(t){return e.call(n,function(e){t.control.value(e).fire("change")},t.control.value())}};return{title:"Advanced",type:"form",defaults:{onchange:b(Ta,n)},items:[{label:"Style",name:"style",type:"textbox",onchange:b(Da,n)},{type:"form",padding:0,formItemDefaults:{layout:"grid",alignH:["start","right"]},defaults:{size:7},items:[{label:"Border style",type:"listbox",name:"borderStyle",width:90,onselect:b(Ta,n),values:[{text:"Select...",value:""},{text:"Solid",value:"solid"},{text:"Dotted",value:"dotted"},{text:"Dashed",value:"dashed"},{text:"Double",value:"double"},{text:"Groove",value:"groove"},{text:"Ridge",value:"ridge"},{text:"Inset",value:"inset"},{text:"Outset",value:"outset"},{text:"None",value:"none"},{text:"Hidden",value:"hidden"}]},{label:"Border color",type:"colorbox",name:"borderColor",onaction:e()},{label:"Background color",type:"colorbox",name:"backgroundColor",onaction:e()}]}]}},buildListItems:function(e,r,t){var o=function(e,n){return n=n||[],wa.each(e,function(e){var t={text:e.text||e.title};e.menu?t.menu=o(e.menu):(t.value=e.value,r&&r(t)),n.push(t)}),n};return o(e,t||[])},updateStyleField:Ta,extractAdvancedStyles:function(e,t){var n=e.parseStyle(e.getAttrib(t,"style")),r={};return n["border-style"]&&(r.borderStyle=n["border-style"]),n["border-color"]&&(r.borderColor=n["border-color"]),n["background-color"]&&(r.backgroundColor=n["background-color"]),r.style=e.serializeStyle(n),r},updateAdvancedFields:Da,syncAdvancedStyleFields:function(e,t){t.control.rootControl.find("#style")[0].getEl().isEqualNode(m.document.activeElement)?Da(e,t):Ta(e,t)}},Ea=function(r,o,e){var i,u=r.dom;function a(e,t,n){(1===o.length||n)&&u.setAttrib(e,t,n)}function c(e,t,n){(1===o.length||n)&&u.setStyle(e,t,n)}ia(r)&&Oa.syncAdvancedStyleFields(r,e),i=e.control.rootControl.toJSON(),r.undoManager.transact(function(){wa.each(o,function(e){var t,n;a(e,"scope",i.scope),1===o.length?a(e,"style",i.style):(t=e,n=i.style,delete t.dataset.mceStyle,t.style.cssText+=";"+n),a(e,"class",i["class"]),c(e,"width",$u(i.width)),c(e,"height",$u(i.height)),i.type&&e.nodeName.toLowerCase()!==i.type&&(e=u.rename(e,i.type)),1===o.length&&(Ca(r,e),Sa(r,e)),i.align&&ya(r,e,i.align),i.valign&&xa(r,e,i.valign)}),r.focus()})},Na=function(t){var e,n,r,o=[];if(o=t.dom.select("td[data-mce-selected],th[data-mce-selected]"),e=t.dom.getParent(t.selection.getStart(),"td,th"),!o.length&&e&&o.push(e),e=e||o[0]){var i,u,a,c;1<o.length?n={width:"",height:"",scope:"","class":"",align:"",valign:"",style:"",type:e.nodeName.toLowerCase()}:(u=e,a=(i=t).dom,c={width:a.getStyle(u,"width")||a.getAttrib(u,"width"),height:a.getStyle(u,"height")||a.getAttrib(u,"height"),scope:a.getAttrib(u,"scope"),"class":a.getAttrib(u,"class"),type:u.nodeName.toLowerCase(),style:"",align:"",valign:""},wa.each("left center right".split(" "),function(e){i.formatter.matchNode(u,"align"+e)&&(c.align=e)}),wa.each("top middle bottom".split(" "),function(e){i.formatter.matchNode(u,"valign"+e)&&(c.valign=e)}),ia(i)&&wa.extend(c,Oa.extractAdvancedStyles(a,u)),n=c),0<la(t).length&&(r={name:"class",type:"listbox",label:"Class",values:Oa.buildListItems(la(t),function(e){e.value&&(e.textStyle=function(){return t.formatter.getCssText({block:"td",classes:[e.value]})})})});var l={type:"form",layout:"flex",direction:"column",labelGapCalc:"children",padding:0,items:[{type:"form",layout:"grid",columns:2,labelGapCalc:!1,padding:0,defaults:{type:"textbox",maxWidth:50},items:[{label:"Width",name:"width",onchange:b(Oa.updateStyleField,t)},{label:"Height",name:"height",onchange:b(Oa.updateStyleField,t)},{label:"Cell type",name:"type",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"Cell",value:"td"},{text:"Header cell",value:"th"}]},{label:"Scope",name:"scope",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Row",value:"row"},{text:"Column",value:"col"},{text:"Row group",value:"rowgroup"},{text:"Column group",value:"colgroup"}]},{label:"H Align",name:"align",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},{label:"V Align",name:"valign",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Top",value:"top"},{text:"Middle",value:"middle"},{text:"Bottom",value:"bottom"}]}]},r]};ia(t)?t.windowManager.open({title:"Cell properties",bodyType:"tabpanel",data:n,body:[{title:"General",type:"form",items:l},Oa.createStyleForm(t)],onsubmit:b(Ea,t,o)}):t.windowManager.open({title:"Cell properties",data:n,body:l,onsubmit:b(Ea,t,o)})}};function ka(f,s,d,e){var m=f.dom;function g(e,t,n){(1===s.length||n)&&m.setAttrib(e,t,n)}ua(f)&&Oa.syncAdvancedStyleFields(f,e);var h=e.control.rootControl.toJSON();f.undoManager.transact(function(){wa.each(s,function(e){var t,n,r,o,i,u,a,c,l;g(e,"scope",h.scope),g(e,"style",h.style),g(e,"class",h["class"]),t=e,n="height",r=$u(h.height),(1===s.length||r)&&m.setStyle(t,n,r),h.type!==e.parentNode.nodeName.toLowerCase()&&(o=f.dom,i=e,u=h.type,a=o.getParent(i,"table"),c=i.parentNode,(l=o.select(u,a)[0])||(l=o.create(u),a.firstChild?"CAPTION"===a.firstChild.nodeName?o.insertAfter(l,a.firstChild):a.insertBefore(l,a.firstChild):a.appendChild(l)),l.appendChild(i),c.hasChildNodes()||o.remove(c)),h.align!==d.align&&(Ca(f,e),ya(f,e,h.align))}),f.focus()})}var Aa=function(t){var e,n,r,o,i,u,a,c,l,f,s=t.dom,d=[];e=s.getParent(t.selection.getStart(),"table"),n=s.getParent(t.selection.getStart(),"td,th"),wa.each(e.rows,function(t){wa.each(t.cells,function(e){if(s.getAttrib(e,"data-mce-selected")||e===n)return d.push(t),!1})}),(r=d[0])&&(1<d.length?i={height:"",scope:"",style:"","class":"",align:"",type:r.parentNode.nodeName.toLowerCase()}:(c=r,l=(a=t).dom,f={height:l.getStyle(c,"height")||l.getAttrib(c,"height"),scope:l.getAttrib(c,"scope"),"class":l.getAttrib(c,"class"),align:"",style:"",type:c.parentNode.nodeName.toLowerCase()},wa.each("left center right".split(" "),function(e){a.formatter.matchNode(c,"align"+e)&&(f.align=e)}),ua(a)&&wa.extend(f,Oa.extractAdvancedStyles(l,c)),i=f),0<fa(t).length&&(o={name:"class",type:"listbox",label:"Class",values:Oa.buildListItems(fa(t),function(e){e.value&&(e.textStyle=function(){return t.formatter.getCssText({block:"tr",classes:[e.value]})})})}),u={type:"form",columns:2,padding:0,defaults:{type:"textbox"},items:[{type:"listbox",name:"type",label:"Row type",text:"Header",maxWidth:null,values:[{text:"Header",value:"thead"},{text:"Body",value:"tbody"},{text:"Footer",value:"tfoot"}]},{type:"listbox",name:"align",label:"Alignment",text:"None",maxWidth:null,values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},{label:"Height",name:"height"},o]},ua(t)?t.windowManager.open({title:"Row properties",data:i,bodyType:"tabpanel",body:[{title:"General",type:"form",items:u},Oa.createStyleForm(t)],onsubmit:b(ka,t,d,i)}):t.windowManager.open({title:"Row properties",data:i,body:u,onsubmit:b(ka,t,d,i)}))},Pa=tinymce.util.Tools.resolve("tinymce.Env"),Ia={styles:{"border-collapse":"collapse",width:"100%"},attributes:{border:"1"},percentages:!0},Ba=function(e,t,n,r,o){void 0===o&&(o=Ia);var i=Re.fromTag("table");ke(i,o.styles),he(i,o.attributes);var u=Re.fromTag("tbody");Nt(i,u);for(var a=[],c=0;c<e;c++){for(var l=Re.fromTag("tr"),f=0;f<t;f++){var s=c<n||f<r?Re.fromTag("th"):Re.fromTag("td");f<r&&ge(s,"scope","row"),c<n&&ge(s,"scope","col"),Nt(s,Re.fromTag("br")),o.percentages&&Ne(s,"width",100/t+"%"),Nt(l,s)}a.push(l)}return Pt(u,a),i},Wa=function(e,t){e.selection.select(t.dom(),!0),e.selection.collapse(!0)},Ma=function(r,e,t){var n,o,i=r.getParam("table_default_styles",ra,"object"),u={styles:i,attributes:(o=r,o.getParam("table_default_attributes",oa,"object")),percentages:(n=i.width,w(n)&&-1!==n.indexOf("%")&&!da(r))},a=Ba(t,e,0,0,u);ge(a,"data-mce-id","__mce");var c,l,f,s=(c=a,l=Re.fromTag("div"),f=Re.fromDom(c.dom().cloneNode(!0)),Nt(l,f),l.dom().innerHTML);return r.insertContent(s),an(Yu(r),'table[data-mce-id="__mce"]').map(function(e){var t,n;return da(r)&&Ne(e,"width",Ae(e,"width")),be(e,"data-mce-id"),t=r,I(Jt(e,"tr"),function(e){ma(t,e.dom()),I(Jt(e,"th,td"),function(e){ga(t,e.dom())})}),n=r,an(e,"td,th").each(b(Wa,n)),e.dom()}).getOr(null)};function _a(e,t,n,r){if("TD"===t.tagName||"TH"===t.tagName)e.setStyle(t,n,r);else if(t.children)for(var o=0;o<t.children.length;o++)_a(e,t.children[o],n,r)}var La,ja=function(e,t,n){var r,o,i=e.dom;aa(e)&&Oa.syncAdvancedStyleFields(e,n),!1===(o=n.control.rootControl.toJSON())["class"]&&delete o["class"],e.undoManager.transact(function(){t||(t=Ma(e,o.cols||1,o.rows||1)),function(e,t,n){var r,o=e.dom,i={},u={};if(i["class"]=n["class"],u.height=$u(n.height),o.getAttrib(t,"width")&&!ca(e)?i.width=(r=n.width)?r.replace(/px$/,""):"":u.width=$u(n.width),ca(e)?(u["border-width"]=$u(n.border),u["border-spacing"]=$u(n.cellspacing),wa.extend(i,{"data-mce-border-color":n.borderColor,"data-mce-cell-padding":n.cellpadding,"data-mce-border":n.border})):wa.extend(i,{border:n.border,cellpadding:n.cellpadding,cellspacing:n.cellspacing}),ca(e)&&t.children)for(var a=0;a<t.children.length;a++)_a(o,t.children[a],{"border-width":$u(n.border),"border-color":n.borderColor,padding:$u(n.cellpadding)});n.style?wa.extend(u,o.parseStyle(n.style)):u=wa.extend({},o.parseStyle(o.getAttrib(t,"style")),u),i.style=o.serializeStyle(u),o.setAttribs(t,i)}(e,t,o),(r=i.select("caption",t)[0])&&!o.caption&&i.remove(r),!r&&o.caption&&((r=i.create("caption")).innerHTML=Pa.ie?"\xa0":'<br data-mce-bogus="1"/>',t.insertBefore(r,t.firstChild)),Ca(e,t),o.align&&ya(e,t,o.align),e.focus(),e.addVisual()})},Fa=function(t,e){var n,r,o,i,u,a,c,l,f,s,d=t.dom,m={};!0===e?(n=d.getParent(t.selection.getStart(),"table"))&&(c=n,l=(a=t).dom,f={width:l.getStyle(c,"width")||l.getAttrib(c,"width"),height:l.getStyle(c,"height")||l.getAttrib(c,"height"),cellspacing:l.getStyle(c,"border-spacing")||l.getAttrib(c,"cellspacing"),cellpadding:l.getAttrib(c,"data-mce-cell-padding")||l.getAttrib(c,"cellpadding")||Ra(a.dom,c,"padding"),border:l.getAttrib(c,"data-mce-border")||l.getAttrib(c,"border")||Ra(a.dom,c,"border"),borderColor:l.getAttrib(c,"data-mce-border-color"),caption:!!l.select("caption",c)[0],"class":l.getAttrib(c,"class")},wa.each("left center right".split(" "),function(e){a.formatter.matchNode(c,"align"+e)&&(f.align=e)}),aa(a)&&wa.extend(f,Oa.extractAdvancedStyles(l,c)),m=f):(r={label:"Cols",name:"cols"},o={label:"Rows",name:"rows"}),0<sa(t).length&&(m["class"]&&(m["class"]=m["class"].replace(/\s*mce\-item\-table\s*/g,"")),i={name:"class",type:"listbox",label:"Class",values:Oa.buildListItems(sa(t),function(e){e.value&&(e.textStyle=function(){return t.formatter.getCssText({block:"table",classes:[e.value]})})})}),u={type:"form",layout:"flex",direction:"column",labelGapCalc:"children",padding:0,items:[{type:"form",labelGapCalc:!1,padding:0,layout:"grid",columns:2,defaults:{type:"textbox",maxWidth:50},items:(s=t,s.getParam("table_appearance_options",!0,"boolean")?[r,o,{label:"Width",name:"width",onchange:b(Oa.updateStyleField,t)},{label:"Height",name:"height",onchange:b(Oa.updateStyleField,t)},{label:"Cell spacing",name:"cellspacing"},{label:"Cell padding",name:"cellpadding"},{label:"Border",name:"border"},{label:"Caption",name:"caption",type:"checkbox"}]:[r,o,{label:"Width",name:"width",onchange:b(Oa.updateStyleField,t)},{label:"Height",name:"height",onchange:b(Oa.updateStyleField,t)}])},{label:"Alignment",name:"align",type:"listbox",text:"None",values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},i]},aa(t)?t.windowManager.open({title:"Table properties",data:m,bodyType:"tabpanel",body:[{title:"General",type:"form",items:u},Oa.createStyleForm(t)],onsubmit:b(ja,t,n)}):t.windowManager.open({title:"Table properties",data:m,body:u,onsubmit:b(ja,t,n)})},za=wa.each,Ha=function(a,t,c,l,n){var r=Ju(a),e=function(e){return function(){return S.from(a.dom.getParent(a.selection.getStart(),e)).map(Re.fromDom)}},o=e("caption"),f=e("th,td"),s=function(e){return pn.table(e,r)},d=function(e){return{width:Xu(e.dom()),height:Xu(e.dom())}},i=function(n){f().each(function(t){s(t).each(function(i){var e=Lr.forMenu(l,i,t),u=d(i);n(i,e).each(function(e){var t,n,r,o;t=a,n=u,o=d(r=i),n.width===o.width&&n.height===o.height||(ha(t,r.dom(),n.width,n.height),pa(t,r.dom(),o.width,o.height)),a.selection.setRng(e),a.focus(),c.clear(i),Qu(i)})})})},u=function(e){return f().bind(function(o){return s(o).bind(function(e){var t=Re.fromDom(a.getDoc()),n=Lr.forMenu(l,e,o),r=Hn(y,t,S.none());return ba(e,n,r)})})},m=function(u){n.get().each(function(e){var i=P(e,function(e){return Mn(e)});f().each(function(o){s(o).each(function(t){var e=Re.fromDom(a.getDoc()),n=Un(e),r=Lr.pasteRows(l,t,o,i,n);u(t,r).each(function(e){a.selection.setRng(e),a.focus(),c.clear(t)})})})})};za({mceTableSplitCells:function(){i(t.unmergeCells)},mceTableMergeCells:function(){i(t.mergeCells)},mceTableInsertRowBefore:function(){i(t.insertRowsBefore)},mceTableInsertRowAfter:function(){i(t.insertRowsAfter)},mceTableInsertColBefore:function(){i(t.insertColumnsBefore)},mceTableInsertColAfter:function(){i(t.insertColumnsAfter)},mceTableDeleteCol:function(){i(t.deleteColumn)},mceTableDeleteRow:function(){i(t.deleteRow)},mceTableCutRow:function(e){n.set(u()),i(t.deleteRow)},mceTableCopyRow:function(e){n.set(u())},mceTablePasteRowBefore:function(e){m(t.pasteRowsBefore)},mceTablePasteRowAfter:function(e){m(t.pasteRowsAfter)},mceTableDelete:function(){f().orThunk(o).each(function(e){pn.table(e,r).filter(g(r)).each(function(e){var t=Re.fromText("");Ot(e,t),Bt(e);var n=a.dom.createRng();n.setStart(t.dom(),0),n.setEnd(t.dom(),0),a.selection.setRng(n)})})}},function(e,t){a.addCommand(t,e)}),za({mceInsertTable:b(Fa,a),mceTableProps:b(Fa,a,!0),mceTableRowProps:b(Aa,a),mceTableCellProps:b(Na,a)},function(n,e){a.addCommand(e,function(e,t){n(t)})})},Ua=function(e){var t=S.from(e.dom().documentElement).map(Re.fromDom).getOr(e);return{parent:C(t),view:C(e),origin:C(co(0,0))}},qa=function(e,t){return{parent:C(t),view:C(e),origin:C(co(0,0))}},Va=function(e){var r=K.apply(null,e),o=[];return{bind:function(e){if(e===undefined)throw new Error("Event bind error: undefined handler");o.push(e)},unbind:function(t){o=B(o,function(e){return e!==t})},trigger:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=r.apply(null,e);I(o,function(e){e(n)})}}},Ga={create:function(e){return{registry:Y(e,function(e){return{bind:e.bind,unbind:e.unbind}}),trigger:Y(e,function(e){return e.trigger})}}},Ya=function(m,g){return function(e){if(m(e)){var t,n,r,o,i,u,a,c=Re.fromDom(e.target),l=function(){e.stopPropagation()},f=function(){e.preventDefault()},s=x(f,l),d=(t=c,n=e.clientX,r=e.clientY,o=l,i=f,u=s,a=e,{target:C(t),x:C(n),y:C(r),stop:o,prevent:i,kill:u,raw:C(a)});g(d)}}},Xa=function(e,t,n,r){return o=e,i=t,u=!1,a=Ya(n,r),o.dom().addEventListener(i,a,u),{unbind:b(Ka,o,i,a,u)};var o,i,u,a},Ka=function(e,t,n,r){e.dom().removeEventListener(t,n,r)},Ja=C(!0),$a=function(e,t,n){return Xa(e,t,Ja,n)},Qa=Object.prototype.hasOwnProperty,Za=(La=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t<e.length;t++)e[t]=arguments[t];if(0===e.length)throw new Error("Can't merge zero objects");for(var n={},r=0;r<e.length;r++){var o=e[r];for(var i in o)Qa.call(o,i)&&(n[i]=La(n[i],o[i]))}return n}),ec={resolve:Vo("ephox-dragster").resolve},tc=yu(["compare","extract","mutate","sink"]),nc=yu(["element","start","stop","destroy"]),rc=yu(["forceDrop","drop","move","delayDrop"]),oc=tc({compare:function(e,t){return co(t.left()-e.left(),t.top()-e.top())},extract:function(e){return S.some(co(e.x(),e.y()))},sink:function(e,t){var n,r,o,i=(n=t,r=Za({layerClass:ec.resolve("blocker")},n),o=Re.fromTag("div"),ge(o,"role","presentation"),ke(o,{position:"fixed",left:"0px",top:"0px",width:"100%",height:"100%"}),_o(o,ec.resolve("blocker")),_o(o,r.layerClass),{element:function(){return o},destroy:function(){Bt(o)}}),u=$a(i.element(),"mousedown",e.forceDrop),a=$a(i.element(),"mouseup",e.drop),c=$a(i.element(),"mousemove",e.move),l=$a(i.element(),"mouseout",e.delayDrop);return nc({element:i.element,start:function(e){Nt(e,i.element())},stop:function(){Bt(i.element())},destroy:function(){i.destroy(),a.unbind(),c.unbind(),l.unbind(),u.unbind()}})},mutate:function(e,t){e.mutate(t.left(),t.top())}});function ic(){var i=S.none(),u=Ga.create({move:Va(["info"])});return{onEvent:function(e,o){o.extract(e).each(function(e){var t,n,r;(t=o,n=e,r=i.map(function(e){return t.compare(e,n)}),i=S.some(n),r).each(function(e){u.trigger.move(e)})})},reset:function(){i=S.none()},events:u.registry}}function uc(){var e={onEvent:y,reset:y},t=ic(),n=e;return{on:function(){n.reset(),n=t},off:function(){n.reset(),n=e},isOn:function(){return n===t},onEvent:function(e,t){n.onEvent(e,t)},events:t.events}}var ac=function(t,n,e){var r,o,i,u=!1,a=Ga.create({start:Va([]),stop:Va([])}),c=uc(),l=function(){d.stop(),c.isOn()&&(c.off(),a.trigger.stop())},f=(r=l,o=200,i=null,{cancel:function(){null!==i&&(m.clearTimeout(i),i=null)},throttle:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];null!==i&&m.clearTimeout(i),i=m.setTimeout(function(){r.apply(null,e),i=null},o)}});c.events.move.bind(function(e){n.mutate(t,e.info())});var s=function(n){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];u&&n.apply(null,e)}},d=n.sink(rc({forceDrop:l,drop:s(l),move:s(function(e){f.cancel(),c.onEvent(e,n)}),delayDrop:s(f.throttle)}),e);return{element:d.element,go:function(e){d.start(e),c.on(),a.trigger.start()},on:function(){u=!0},off:function(){u=!1},destroy:function(){d.destroy()},events:a.registry}},cc=function(e,t){void 0===t&&(t={});var n=t.mode!==undefined?t.mode:oc;return ac(e,n,t)},lc=function(){var n,r=Ga.create({drag:Va(["xDelta","yDelta","target"])}),o=S.none(),e={mutate:function(e,t){n.trigger.drag(e,t)},events:(n=Ga.create({drag:Va(["xDelta","yDelta"])})).registry};return e.events.drag.bind(function(t){o.each(function(e){r.trigger.drag(t.xDelta(),t.yDelta(),e)})}),{assign:function(e){o=S.some(e)},get:function(){return o},mutate:e.mutate,events:r.registry}},fc=function(e){return"true"===pe(e,"contenteditable")},sc=Go.resolve("resizer-bar-dragging"),dc=function(o,t,i){var n=lc(),r=cc(n,{}),u=S.none(),e=function(e,t){return S.from(pe(e,t))};n.events.drag.bind(function(n){e(n.target(),"data-row").each(function(e){var t=$i.getInt(n.target(),"top");Ne(n.target(),"top",t+n.yDelta()+"px")}),e(n.target(),"data-column").each(function(e){var t=$i.getInt(n.target(),"left");Ne(n.target(),"left",t+n.xDelta()+"px")})});var a=function(e,t){return $i.getInt(e,t)-parseInt(pe(e,"data-initial-"+t),10)};r.events.stop.bind(function(){n.get().each(function(r){u.each(function(n){e(r,"data-row").each(function(e){var t=a(r,"top");be(r,"data-initial-top"),m.trigger.adjustHeight(n,t,parseInt(e,10))}),e(r,"data-column").each(function(e){var t=a(r,"left");be(r,"data-initial-left"),m.trigger.adjustWidth(n,t,parseInt(e,10))}),ni(o,n,i,t)})})});var c=function(e,t){m.trigger.startAdjust(),n.assign(e),ge(e,"data-initial-"+t,parseInt(Ae(e,t),10)),_o(e,sc),Ne(e,"opacity","0.2"),r.go(o.parent())},l=$a(o.parent(),"mousedown",function(e){ui(e.target())&&c(e.target(),"top"),ai(e.target())&&c(e.target(),"left")}),f=function(e){return pt(e,o.view())},s=function(e){return cn(e,"table",f).filter(function(e){return(t=e,n=f,cn(t,"[contenteditable]",n)).exists(fc);var t,n})},d=$a(o.view(),"mouseover",function(e){s(e.target()).fold(function(){Te(e.target())&&ii(o)},function(e){u=S.some(e),ni(o,e,i,t)})}),m=Ga.create({adjustHeight:Va(["table","delta","row"]),adjustWidth:Va(["table","delta","column"]),startAdjust:Va([])});return{destroy:function(){l.unbind(),d.unbind(),r.destroy(),ii(o)},refresh:function(e){ni(o,e,i,t)},on:r.on,off:r.off,hideBars:b(ri,o),showBars:b(oi,o),events:m.registry}},mc=function(e,t){return e.inline?qa(Yu(e),(n=Re.fromTag("div"),ke(n,{position:"static",height:"0",width:"0",padding:"0",margin:"0",border:"0"}),Nt(De(),n),n)):Ua(Re.fromDom(e.getDoc()));var n},gc=function(e,t){e.inline&&Bt(t.parent())},hc=function(c){var u,a,l=S.none(),f=S.none(),s=S.none(),d=/(\d+(\.\d+)?)%/,m=function(e){return"TABLE"===e.nodeName};return c.on("init",function(){var n,r,e,o,t,i=To(ta.directionAt),u=mc(c);if(s=S.some(u),("table"===(t=c.getParam("object_resizing",!0))||t)&&c.getParam("table_resize_bars",!0,"boolean")){var a=(e=dc(u,n=i,r=So.height),o=Ga.create({beforeResize:Va(["table"]),afterResize:Va(["table"]),startDrag:Va([])}),e.events.adjustHeight.bind(function(e){o.trigger.beforeResize(e.table());var t=r.delta(e.delta(),e.table());hu(e.table(),t,e.row(),r),o.trigger.afterResize(e.table())}),e.events.startAdjust.bind(function(e){o.trigger.startDrag()}),e.events.adjustWidth.bind(function(e){o.trigger.beforeResize(e.table());var t=n.delta(e.delta(),e.table());gu(e.table(),t,e.column(),n),o.trigger.afterResize(e.table())}),{on:e.on,off:e.off,hideBars:e.hideBars,showBars:e.showBars,destroy:e.destroy,events:o.registry});a.on(),a.events.startDrag.bind(function(e){l=S.some(c.selection.getRng())}),a.events.beforeResize.bind(function(e){var t=e.table().dom();ha(c,t,Xu(t),Ku(t))}),a.events.afterResize.bind(function(e){var t=e.table(),n=t.dom();Qu(t),l.each(function(e){c.selection.setRng(e),c.focus()}),pa(c,n,Xu(n),Ku(n)),c.undoManager.add()}),f=S.some(a)}}),c.on("ObjectResizeStart",function(e){var t,n=e.target;m(n)&&(u=e.width,t=n,a=c.dom.getStyle(t,"width")||c.dom.getAttrib(t,"width"))}),c.on("ObjectResized",function(e){var t=e.target;if(m(t)){var n=t;if(d.test(a)){var r=parseFloat(d.exec(a)[1]),o=e.width*r/u;c.dom.setStyle(n,"width",o+"%")}else{var i=[];wa.each(n.rows,function(e){wa.each(e.cells,function(e){var t=c.dom.getStyle(e,"width",!0);i.push({cell:e,width:t})})}),wa.each(i,function(e){c.dom.setStyle(e.cell,"width",e.width),c.dom.setAttrib(e.cell,"width",null)})}}}),{lazyResize:function(){return f},lazyWire:function(){return s.getOr(Ua(Re.fromDom(c.getBody())))},destroy:function(){f.each(function(e){e.destroy()}),s.each(function(e){gc(c,e)})}}},pc=Nr([{none:["current"]},{first:["current"]},{middle:["current","target"]},{last:["current"]}]),vc=Oo({},pc,{none:function(e){return void 0===e&&(e=undefined),pc.none(e)}}),bc=function(n,e){return pn.table(n,e).bind(function(e){var t=pn.cells(e);return L(t,function(e){return pt(n,e)}).map(function(e){return{index:C(e),all:C(t)}})})},wc=function(t,e){return bc(t,e).fold(function(){return vc.none(t)},function(e){return e.index()+1<e.all().length?vc.middle(t,e.all()[e.index()+1]):vc.last(t)})},yc=function(t,e){return bc(t,e).fold(function(){return vc.none()},function(e){return 0<=e.index()-1?vc.middle(t,e.all()[e.index()-1]):vc.first(t)})},xc={create:K("start","soffset","finish","foffset")},Cc=Nr([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),Sc={before:Cc.before,on:Cc.on,after:Cc.after,cata:function(e,t,n,r){return e.fold(t,n,r)},getStart:function(e){return e.fold(o,o,o)}},Rc=Nr([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),Tc={domRange:Rc.domRange,relative:Rc.relative,exact:Rc.exact,exactFromRange:function(e){return Rc.exact(e.start(),e.soffset(),e.finish(),e.foffset())},getWin:function(e){var t,n=e.match({domRange:function(e){return Re.fromDom(e.startContainer)},relative:function(e,t){return Sc.getStart(e)},exact:function(e,t,n,r){return e}});return t=n.dom().ownerDocument.defaultView,Re.fromDom(t)},range:xc.create},Dc=function(e,t){e.selectNodeContents(t.dom())},Oc=function(e,t,n){var r,o,i=e.document.createRange();return r=i,t.fold(function(e){r.setStartBefore(e.dom())},function(e,t){r.setStart(e.dom(),t)},function(e){r.setStartAfter(e.dom())}),o=i,n.fold(function(e){o.setEndBefore(e.dom())},function(e,t){o.setEnd(e.dom(),t)},function(e){o.setEndAfter(e.dom())}),i},Ec=function(e,t,n,r,o){var i=e.document.createRange();return i.setStart(t.dom(),n),i.setEnd(r.dom(),o),i},Nc=function(e){return{left:C(e.left),top:C(e.top),right:C(e.right),bottom:C(e.bottom),width:C(e.width),height:C(e.height)}},kc=Nr([{ltr:["start","soffset","finish","foffset"]},{rtl:["start","soffset","finish","foffset"]}]),Ac=function(e,t,n){return t(Re.fromDom(n.startContainer),n.startOffset,Re.fromDom(n.endContainer),n.endOffset)},Pc=function(e,t){var o,n,r,i=(o=e,t.match({domRange:function(e){return{ltr:C(e),rtl:S.none}},relative:function(e,t){return{ltr:Ce(function(){return Oc(o,e,t)}),rtl:Ce(function(){return S.some(Oc(o,t,e))})}},exact:function(e,t,n,r){return{ltr:Ce(function(){return Ec(o,e,t,n,r)}),rtl:Ce(function(){return S.some(Ec(o,n,r,e,t))})}}}));return(r=(n=i).ltr()).collapsed?n.rtl().filter(function(e){return!1===e.collapsed}).map(function(e){return kc.rtl(Re.fromDom(e.endContainer),e.endOffset,Re.fromDom(e.startContainer),e.startOffset)}).getOrThunk(function(){return Ac(0,kc.ltr,r)}):Ac(0,kc.ltr,r)},Ic=function(i,e){return Pc(i,e).match({ltr:function(e,t,n,r){var o=i.document.createRange();return o.setStart(e.dom(),t),o.setEnd(n.dom(),r),o},rtl:function(e,t,n,r){var o=i.document.createRange();return o.setStart(n.dom(),r),o.setEnd(e.dom(),t),o}})},Bc=function(e,t,n){return t>=e.left&&t<=e.right&&n>=e.top&&n<=e.bottom},Wc=function(n,r,e,t,o){var i=function(e){var t=n.dom().createRange();return t.setStart(r.dom(),e),t.collapse(!0),t},u=Tn(r).length,a=function(e,t,n,r,o){if(0===o)return 0;if(t===r)return o-1;for(var i=r,u=1;u<o;u++){var a=e(u),c=Math.abs(t-a.left);if(n<=a.bottom){if(n<a.top||i<c)return u-1;i=c}}return 0}(function(e){return i(e).getBoundingClientRect()},e,t,o.right,u);return i(a)},Mc=function(t,n,r,o){var e=t.dom().createRange();e.selectNode(n.dom());var i=e.getClientRects();return No(i,function(e){return Bc(e,r,o)?S.some(e):S.none()}).map(function(e){return Wc(t,n,r,o,e)})},_c=function(t,e,n,r){var o=t.dom().createRange(),i=Rt(e);return No(i,function(e){return o.selectNode(e.dom()),Bc(o.getBoundingClientRect(),n,r)?Lc(t,e,n,r):S.none()})},Lc=function(e,t,n,r){return(se(t)?Mc:_c)(e,t,n,r)},jc=function(e,t){return t-e.left<e.right-t},Fc=function(e,t,n){var r=e.dom().createRange();return r.selectNode(t.dom()),r.collapse(n),r},zc=function(t,e,n){var r=t.dom().createRange();r.selectNode(e.dom());var o=r.getBoundingClientRect(),i=jc(o,n);return(!0===i?An:Pn)(e).map(function(e){return Fc(t,e,i)})},Hc=function(e,t,n){var r=t.dom().getBoundingClientRect(),o=jc(r,n);return S.some(Fc(e,t,o))},Uc=function(e,t,n,r){var o=e.dom().createRange();o.selectNode(t.dom());var i=o.getBoundingClientRect();return function(e,t,n,r){var o=e.dom().createRange();o.selectNode(t.dom());var i=o.getBoundingClientRect(),u=Math.max(i.left,Math.min(i.right,n)),a=Math.max(i.top,Math.min(i.bottom,r));return Lc(e,t,u,a)}(e,t,Math.max(i.left,Math.min(i.right,n)),Math.max(i.top,Math.min(i.bottom,r)))},qc=document.caretPositionFromPoint?function(n,e,t){return S.from(n.dom().caretPositionFromPoint(e,t)).bind(function(e){if(null===e.offsetNode)return S.none();var t=n.dom().createRange();return t.setStart(e.offsetNode,e.offset),t.collapse(),S.some(t)})}:document.caretRangeFromPoint?function(e,t,n){return S.from(e.dom().caretRangeFromPoint(t,n))}:function(o,i,t){return Re.fromPoint(o,i,t).bind(function(r){var e=function(){return e=o,n=i,(0===Rt(t=r).length?Hc:zc)(e,t,n);var e,t,n};return 0===Rt(r).length?e():Uc(o,r,i,t).orThunk(e)})},Vc=function(e,t){var n=ue(e);return"input"===n?Sc.after(e):k(["br","img"],n)?0===t?Sc.before(e):Sc.after(e):Sc.on(e,t)},Gc=function(e,t){var n=e.fold(Sc.before,Vc,Sc.after),r=t.fold(Sc.before,Vc,Sc.after);return Tc.relative(n,r)},Yc=function(e,t,n,r){var o=Vc(e,t),i=Vc(n,r);return Tc.relative(o,i)},Xc=function(e,t,n,r){var o,i,u,a,c,l=(i=t,u=n,a=r,(c=wt(o=e).dom().createRange()).setStart(o.dom(),i),c.setEnd(u.dom(),a),c),f=pt(e,n)&&t===r;return l.collapsed&&!f},Kc=function(e,t){S.from(e.getSelection()).each(function(e){e.removeAllRanges(),e.addRange(t)})},Jc=function(e,t,n,r,o){var i=Ec(e,t,n,r,o);Kc(e,i)},$c=function(s,e){return Pc(s,e).match({ltr:function(e,t,n,r){Jc(s,e,t,n,r)},rtl:function(e,t,n,r){var o,i,u,a,c,l=s.getSelection();if(l.setBaseAndExtent)l.setBaseAndExtent(e.dom(),t,n.dom(),r);else if(l.extend)try{i=e,u=t,a=n,c=r,(o=l).collapse(i.dom(),u),o.extend(a.dom(),c)}catch(f){Jc(s,n,r,e,t)}else Jc(s,n,r,e,t)}})},Qc=function(e){var o=Tc.getWin(e).dom(),t=function(e,t,n,r){return Ec(o,e,t,n,r)},n=e.match({domRange:function(e){var t=Re.fromDom(e.startContainer),n=Re.fromDom(e.endContainer);return Yc(t,e.startOffset,n,e.endOffset)},relative:Gc,exact:Yc});return Pc(o,n).match({ltr:t,rtl:t})},Zc=function(e){var t=Re.fromDom(e.anchorNode),n=Re.fromDom(e.focusNode);return Xc(t,e.anchorOffset,n,e.focusOffset)?S.some(xc.create(t,e.anchorOffset,n,e.focusOffset)):function(e){if(0<e.rangeCount){var t=e.getRangeAt(0),n=e.getRangeAt(e.rangeCount-1);return S.some(xc.create(Re.fromDom(t.startContainer),t.startOffset,Re.fromDom(n.endContainer),n.endOffset))}return S.none()}(e)},el=function(e,t){var n,r,o=(n=t,r=e.document.createRange(),Dc(r,n),r);Kc(e,o)},tl=function(e){return(t=e,S.from(t.getSelection()).filter(function(e){return 0<e.rangeCount}).bind(Zc)).map(function(e){return Tc.exact(e.start(),e.soffset(),e.finish(),e.foffset())});var t},nl=function(e,t){var n,r,o,i=Ic(e,t);return r=(n=i).getClientRects(),0<(o=0<r.length?r[0]:n.getBoundingClientRect()).width||0<o.height?S.some(o).map(Nc):S.none()},rl=function(e,t,n){return r=e,o=t,i=n,u=Re.fromDom(r.document),qc(u,o,i).map(function(e){return xc.create(Re.fromDom(e.startContainer),e.startOffset,Re.fromDom(e.endContainer),e.endOffset)});var r,o,i,u},ol=tinymce.util.Tools.resolve("tinymce.util.VK"),il=function(e,t,n,r){return ll(e,t,wc(n),r)},ul=function(e,t,n,r){return ll(e,t,yc(n),r)},al=function(e,t){var n=Tc.exact(t,0,t,0);return Qc(n)},cl=function(e,t){var n,r=Jt(t,"tr");return(n=r,0===n.length?S.none():S.some(n[n.length-1])).bind(function(e){return an(e,"td,th").map(function(e){return al(0,e)})})},ll=function(r,e,t,o,n){return t.fold(S.none,S.none,function(e,t){return An(t).map(function(e){return al(0,e)})},function(n){return pn.table(n,e).bind(function(e){var t=Lr.noMenu(n);return r.undoManager.transact(function(){o.insertRowsAfter(e,t)}),cl(0,e)})})},fl=["table","li","dl"],sl=function(t,n,r,o){if(t.keyCode===ol.TAB){var i=Yu(n),u=function(e){var t=ue(e);return pt(e,i)||k(fl,t)},e=n.selection.getRng();if(e.collapsed){var a=Re.fromDom(e.startContainer);pn.cell(a,u).each(function(e){t.preventDefault(),(t.shiftKey?ul:il)(n,u,e,r,o).each(function(e){n.selection.setRng(e)})})}}},dl={create:K("selection","kill")},ml=function(e,t,n,r){return{start:C(Sc.on(e,t)),finish:C(Sc.on(n,r))}},gl={convertToRange:function(e,t){var n=Ic(e,t);return xc.create(Re.fromDom(n.startContainer),n.startOffset,Re.fromDom(n.endContainer),n.endOffset)},makeSitus:ml},hl=function(n,e,r,t,o){return pt(r,t)?S.none():wr(r,t,e).bind(function(e){var t=e.boxes().getOr([]);return 0<t.length?(o(n,t,e.start(),e.finish()),S.some(dl.create(S.some(gl.makeSitus(r,0,r,En(r))),!0))):S.none()})},pl={sync:function(n,r,e,t,o,i,u){return pt(e,o)&&t===i?S.none():cn(e,"td,th",r).bind(function(t){return cn(o,"td,th",r).bind(function(e){return hl(n,r,t,e,u)})})},detect:hl,update:function(e,t,n,r,o){return xr(r,e,t,o.firstSelectedSelector(),o.lastSelectedSelector()).map(function(e){return o.clear(n),o.selectRange(n,e.boxes(),e.start(),e.finish()),e.boxes()})}},vl=K("item","mode"),bl=function(e,t,n,r){return void 0===r&&(r=wl),e.property().parent(t).map(function(e){return vl(e,r)})},wl=function(e,t,n,r){return void 0===r&&(r=yl),n.sibling(e,t).map(function(e){return vl(e,r)})},yl=function(e,t,n,r){void 0===r&&(r=yl);var o=e.property().children(t);return n.first(o).map(function(e){return vl(e,r)})},xl=[{current:bl,next:wl,fallback:S.none()},{current:wl,next:yl,fallback:S.some(bl)},{current:yl,next:yl,fallback:S.some(wl)}],Cl=function(t,n,r,o,e){return void 0===e&&(e=xl),_(e,function(e){return e.current===r}).bind(function(e){return e.current(t,n,o,e.next).orThunk(function(){return e.fallback.bind(function(e){return Cl(t,n,e,o)})})})},Sl=function(){return{sibling:function(e,t){return e.query().prevSibling(t)},first:function(e){return 0<e.length?S.some(e[e.length-1]):S.none()}}},Rl=function(){return{sibling:function(e,t){return e.query().nextSibling(t)},first:function(e){return 0<e.length?S.some(e[0]):S.none()}}},Tl=function(t,e,n,r,o,i){return Cl(t,e,r,o).bind(function(e){return i(e.item())?S.none():n(e.item())?S.some(e.item()):Tl(t,e.item(),n,e.mode(),o,i)})},Dl=function(t){return function(e){return 0===t.property().children(e).length}},Ol=function(e,t,n,r){return Tl(e,t,n,wl,Sl(),r)},El=function(e,t,n,r){return Tl(e,t,n,wl,Rl(),r)},Nl=ir(),kl=function(e,t){return r=t,Ol(n=Nl,e,Dl(n),r);var n,r},Al=function(e,t){return r=t,El(n=Nl,e,Dl(n),r);var n,r},Pl=K("element","offset"),Il=(K("element","deltaOffset"),K("element","start","finish"),K("begin","end"),K("element","text"),Nr([{none:["message"]},{success:[]},{failedUp:["cell"]},{failedDown:["cell"]}])),Bl=function(e){return cn(e,"tr")},Wl=Oo({},Il,{verify:function(a,e,t,n,r,c,o){return cn(n,"td,th",o).bind(function(u){return cn(e,"td,th",o).map(function(i){return pt(u,i)?pt(n,u)&&En(u)===r?c(i):Il.none("in same cell"):hr.sharedOne(Bl,[u,i]).fold(function(){return t=i,n=u,r=(e=a).getRect(t),(o=e.getRect(n)).right>r.left&&o.left<r.right?Il.success():c(i);var e,t,n,r,o},function(e){return c(i)})})}).getOr(Il.none("default"))},cata:function(e,t,n,r,o){return e.fold(t,n,r,o)}}),Ml=(K("ancestor","descendants","element","index"),K("parent","children","element","index")),_l=function(e,t){return L(e,b(pt,t))},Ll=function(e){return"br"===ue(e)},jl=function(e,t,n){return t(e,n).bind(function(e){return se(e)&&0===Tn(e).trim().length?jl(e,t,n):S.some(e)})},Fl=function(t,e,n,r){return(o=e,i=n,Tt(o,i).filter(Ll).orThunk(function(){return Tt(o,i-1).filter(Ll)})).bind(function(e){return r.traverse(e).fold(function(){return jl(e,r.gather,t).map(r.relative)},function(e){return(r=e,yt(r).bind(function(t){var n=Rt(t);return _l(n,r).map(function(e){return Ml(t,n,r,e)})})).map(function(e){return Sc.on(e.parent(),e.index())});var r})});var o,i},zl=function(e,t,n,r){var o,i,u;return(Ll(t)?(o=e,i=t,(u=r).traverse(i).orThunk(function(){return jl(i,u.gather,o)}).map(u.relative)):Fl(e,t,n,r)).map(function(e){return{start:C(e),finish:C(e)}})},Hl=function(e){return Wl.cata(e,function(e){return S.none()},function(){return S.none()},function(e){return S.some(Pl(e,0))},function(e){return S.some(Pl(e,En(e)))})},Ul=te(["left","top","right","bottom"],[]),ql={nu:Ul,moveUp:function(e,t){return Ul({left:e.left(),top:e.top()-t,right:e.right(),bottom:e.bottom()-t})},moveDown:function(e,t){return Ul({left:e.left(),top:e.top()+t,right:e.right(),bottom:e.bottom()+t})},moveBottomTo:function(e,t){var n=e.bottom()-e.top();return Ul({left:e.left(),top:t-n,right:e.right(),bottom:t})},moveTopTo:function(e,t){var n=e.bottom()-e.top();return Ul({left:e.left(),top:t,right:e.right(),bottom:t+n})},getTop:function(e){return e.top()},getBottom:function(e){return e.bottom()},translate:function(e,t,n){return Ul({left:e.left()+t,top:e.top()+n,right:e.right()+t,bottom:e.bottom()+n})},toString:function(e){return"("+e.left()+", "+e.top()+") -> ("+e.right()+", "+e.bottom()+")"}},Vl=function(e){return ql.nu({left:e.left,top:e.top,right:e.right,bottom:e.bottom})},Gl=function(e,t){return S.some(e.getRect(t))},Yl=function(e,t,n){return fe(t)?Gl(e,t).map(Vl):se(t)?(r=e,o=t,i=n,0<=i&&i<En(o)?r.getRangedRect(o,i,o,i+1):0<i?r.getRangedRect(o,i-1,o,i):S.none()).map(Vl):S.none();var r,o,i},Xl=function(e,t){return fe(t)?Gl(e,t).map(Vl):se(t)?e.getRangedRect(t,0,t,En(t)).map(Vl):S.none()},Kl=Nr([{none:[]},{retry:["caret"]}]),Jl=function(t,e,r){return(n=e,o=Iu,$t(function(e){return o(e)},rn,n,o,i)).fold(C(!1),function(e){return Xl(t,e).exists(function(e){return n=e,(t=r).left()<n.left()||Math.abs(n.right()-t.left())<1||t.left()>n.right();var t,n})});var n,o,i},$l={point:ql.getTop,adjuster:function(e,t,n,r,o){var i=ql.moveUp(o,5);return Math.abs(n.top()-r.top())<1?Kl.retry(i):n.bottom()<o.top()?Kl.retry(i):n.bottom()===o.top()?Kl.retry(ql.moveUp(o,1)):Jl(e,t,o)?Kl.retry(ql.translate(i,5,0)):Kl.none()},move:ql.moveUp,gather:kl},Ql={point:ql.getBottom,adjuster:function(e,t,n,r,o){var i=ql.moveDown(o,5);return Math.abs(n.bottom()-r.bottom())<1?Kl.retry(i):n.top()>o.bottom()?Kl.retry(i):n.top()===o.bottom()?Kl.retry(ql.moveDown(o,1)):Jl(e,t,o)?Kl.retry(ql.translate(i,5,0)):Kl.none()},move:ql.moveDown,gather:Al},Zl=function(n,r,o,i,u){return 0===u?S.some(i):(c=n,l=i.left(),f=r.point(i),c.elementFromPoint(l,f).filter(function(e){return"table"===ue(e)}).isSome()?(t=i,a=u-1,Zl(n,e=r,o,e.move(t,5),a)):n.situsFromPoint(i.left(),r.point(i)).bind(function(e){return e.start().fold(S.none,function(t){return Xl(n,t).bind(function(e){return r.adjuster(n,t,e,o,i).fold(S.none,function(e){return Zl(n,r,o,e,u-1)})}).orThunk(function(){return S.some(i)})},S.none)}));var e,t,a,c,l,f},ef=function(t,n,e){var r,o,i,u=t.move(e,5),a=Zl(n,t,e,u,100).getOr(u);return(r=t,o=a,i=n,r.point(o)>i.getInnerHeight()?S.some(r.point(o)-i.getInnerHeight()):r.point(o)<0?S.some(-r.point(o)):S.none()).fold(function(){return n.situsFromPoint(a.left(),t.point(a))},function(e){return n.scrollBy(0,e),n.situsFromPoint(a.left(),t.point(a)-e)})},tf={tryUp:b(ef,$l),tryDown:b(ef,Ql),ieTryUp:function(e,t){return e.situsFromPoint(t.left(),t.top()-5)},ieTryDown:function(e,t){return e.situsFromPoint(t.left(),t.bottom()+5)},getJumpSize:C(5)},nf=st.detect(),rf=function(r,o,i,u,a,c){return 0===c?S.none():af(r,o,i,u,a).bind(function(e){var t=r.fromSitus(e),n=Wl.verify(r,i,u,t.finish(),t.foffset(),a.failure,o);return Wl.cata(n,function(){return S.none()},function(){return S.some(e)},function(e){return pt(i,e)&&0===u?of(r,i,u,ql.moveUp,a):rf(r,o,e,0,a,c-1)},function(e){return pt(i,e)&&u===En(e)?of(r,i,u,ql.moveDown,a):rf(r,o,e,En(e),a,c-1)})})},of=function(t,e,n,r,o){return Yl(t,e,n).bind(function(e){return uf(t,o,r(e,tf.getJumpSize()))})},uf=function(e,t,n){return nf.browser.isChrome()||nf.browser.isSafari()||nf.browser.isFirefox()||nf.browser.isEdge()?t.otherRetry(e,n):nf.browser.isIE()?t.ieRetry(e,n):S.none()},af=function(t,e,n,r,o){return Yl(t,n,r).bind(function(e){return uf(t,o,e)})},cf=function(t,n,r){return(o=t,i=n,u=r,o.getSelection().bind(function(r){return zl(i,r.finish(),r.foffset(),u).fold(function(){return S.some(Pl(r.finish(),r.foffset()))},function(e){var t=o.fromSitus(e),n=Wl.verify(o,r.finish(),r.foffset(),t.finish(),t.foffset(),u.failure,i);return Hl(n)})})).bind(function(e){return rf(t,n,e.element(),e.offset(),r,20).map(t.fromSitus)});var o,i,u},lf=st.detect(),ff=function(e,t){return rn(e,function(e){return yt(e).exists(function(e){return pt(e,t)})},n).isSome();var n},sf=function(t,r,o,e,i){return cn(e,"td,th",r).bind(function(n){return cn(n,"table",r).bind(function(e){return ff(i,e)?cf(t,r,o).bind(function(t){return cn(t.finish(),"td,th",r).map(function(e){return{start:C(n),finish:C(e),range:C(t)}})}):S.none()})})},df=function(e,t,n,r,o,i){return lf.browser.isIE()?S.none():i(r,t).orThunk(function(){return sf(e,t,n,r,o).map(function(e){var t=e.range();return dl.create(S.some(gl.makeSitus(t.start(),t.soffset(),t.finish(),t.foffset())),!0)})})},mf=function(e,t,n,r,o,i,u){return sf(e,n,r,o,i).bind(function(e){return pl.detect(t,n,e.start(),e.finish(),u)})},gf=function(e,u){return cn(e,"tr",u).bind(function(i){return cn(i,"table",u).bind(function(e){var t,n,r,o=Jt(e,"tr");return pt(i,o[0])?(t=e,n=function(e){return Pn(e).isSome()},r=u,Ol(Nl,t,n,r)).map(function(e){var t=En(e);return dl.create(S.some(gl.makeSitus(e,t,e,t)),!0)}):S.none()})})},hf=function(e,u){return cn(e,"tr",u).bind(function(i){return cn(i,"table",u).bind(function(e){var t,n,r,o=Jt(e,"tr");return pt(i,o[o.length-1])?(t=e,n=function(e){return An(e).isSome()},r=u,El(Nl,t,n,r)).map(function(e){return dl.create(S.some(gl.makeSitus(e,0,e,0)),!0)}):S.none()})})},pf=function(e,t){return cn(e,"td,th",t)},vf={down:{traverse:St,gather:Al,relative:Sc.before,otherRetry:tf.tryDown,ieRetry:tf.ieTryDown,failure:Wl.failedDown},up:{traverse:Ct,gather:kl,relative:Sc.before,otherRetry:tf.tryUp,ieRetry:tf.ieTryUp,failure:Wl.failedUp}},bf=function(t){return function(e){return e===t}},wf=bf(38),yf=bf(40),xf={ltr:{isBackward:bf(37),isForward:bf(39)},rtl:{isBackward:bf(39),isForward:bf(37)},isUp:wf,isDown:yf,isNavigation:function(e){return 37<=e&&e<=40}},Cf=(st.detect().browser.isSafari(),function(a){return{elementFromPoint:function(e,t){return Re.fromPoint(Re.fromDom(a.document),e,t)},getRect:function(e){return e.dom().getBoundingClientRect()},getRangedRect:function(e,t,n,r){var o=Tc.exact(e,t,n,r);return nl(a,o).map(function(e){return Y(e,c)})},getSelection:function(){return tl(a).map(function(e){return gl.convertToRange(a,e)})},fromSitus:function(e){var t=Tc.relative(e.start(),e.finish());return gl.convertToRange(a,t)},situsFromPoint:function(e,t){return rl(a,e,t).map(function(e){return ml(e.start(),e.soffset(),e.finish(),e.foffset())})},clearSelection:function(){a.getSelection().removeAllRanges()},setSelection:function(e){var t,n,r,o,i,u;t=a,n=e.start(),r=e.soffset(),o=e.finish(),i=e.foffset(),u=Yc(n,r,o,i),$c(t,u)},setRelativeSelection:function(e,t){var n,r;n=a,r=Gc(e,t),$c(n,r)},selectContents:function(e){el(a,e)},getInnerHeight:function(){return a.innerHeight},getScrollY:function(){var e,t,n,r;return(e=Re.fromDom(a.document),t=e!==undefined?e.dom():m.document,n=t.body.scrollLeft||t.documentElement.scrollLeft,r=t.body.scrollTop||t.documentElement.scrollTop,co(n,r)).top()},scrollBy:function(e,t){var n,r,o;n=e,r=t,((o=Re.fromDom(a.document))!==undefined?o.dom():m.document).defaultView.scrollBy(n,r)}}}),Sf=K("rows","cols"),Rf={mouse:function(e,t,n,r){var o,i,u,a,c,l,f=Cf(e),s=(o=f,i=t,u=n,a=r,c=S.none(),l=function(){c=S.none()},{mousedown:function(e){a.clear(i),c=pf(e.target(),u)},mouseover:function(e){c.each(function(r){a.clear(i),pf(e.target(),u).each(function(n){wr(r,n,u).each(function(e){var t=e.boxes().getOr([]);(1<t.length||1===t.length&&!pt(r,n))&&(a.selectRange(i,t,e.start(),e.finish()),o.selectContents(n))})})})},mouseup:function(e){c.each(l)}});return{mousedown:s.mousedown,mouseover:s.mouseover,mouseup:s.mouseup}},keyboard:function(e,l,f,s){var d=Cf(e),m=function(){return s.clear(l),S.none()};return{keydown:function(e,t,n,r,o,i){var u=e.raw(),a=u.which,c=!0===u.shiftKey;return yr(l,s.selectedSelector()).fold(function(){return xf.isDown(a)&&c?b(mf,d,l,f,vf.down,r,t,s.selectRange):xf.isUp(a)&&c?b(mf,d,l,f,vf.up,r,t,s.selectRange):xf.isDown(a)?b(df,d,f,vf.down,r,t,hf):xf.isUp(a)?b(df,d,f,vf.up,r,t,gf):S.none},function(t){var e=function(e){return function(){return No(e,function(e){return pl.update(e.rows(),e.cols(),l,t,s)}).fold(function(){return Cr(l,s.firstSelectedSelector(),s.lastSelectedSelector()).map(function(e){var t=xf.isDown(a)||i.isForward(a)?Sc.after:Sc.before;return d.setRelativeSelection(Sc.on(e.first(),0),t(e.table())),s.clear(l),dl.create(S.none(),!0)})},function(e){return S.some(dl.create(S.none(),!0))})}};return xf.isDown(a)&&c?e([Sf(1,0)]):xf.isUp(a)&&c?e([Sf(-1,0)]):i.isBackward(a)&&c?e([Sf(0,-1),Sf(-1,0)]):i.isForward(a)&&c?e([Sf(0,1),Sf(1,0)]):xf.isNavigation(a)&&!1===c?m:S.none})()},keyup:function(n,r,o,i,u){return yr(l,s.selectedSelector()).fold(function(){var e=n.raw(),t=e.which;return 0==(!0===e.shiftKey)?S.none():xf.isNavigation(t)?pl.sync(l,f,r,o,i,u,s.selectRange):S.none()},S.none)}}}},Tf=function(r,e){I(e,function(e){var t,n;n=e,Bo(t=r)?t.dom().classList.remove(n):Mo(t,n),Lo(t)})},Df={byClass:function(o){var t,n,i=(t=o.selected(),function(e){_o(e,t)}),r=(n=[o.selected(),o.lastSelected(),o.firstSelected()],function(e){Tf(e,n)}),u=function(e){var t=Jt(e,o.selectedSelector());I(t,r)};return{clear:u,selectRange:function(e,t,n,r){u(e),I(t,i),_o(n,o.firstSelected()),_o(r,o.lastSelected())},selectedSelector:o.selectedSelector,firstSelectedSelector:o.firstSelectedSelector,lastSelectedSelector:o.lastSelectedSelector}},byAttr:function(o){var n=function(e){be(e,o.selected()),be(e,o.firstSelected()),be(e,o.lastSelected())},i=function(e){ge(e,o.selected(),"1")},u=function(e){var t=Jt(e,o.selectedSelector());I(t,n)};return{clear:u,selectRange:function(e,t,n,r){u(e),I(t,i),ge(n,o.firstSelected(),"1"),ge(r,o.lastSelected(),"1")},selectedSelector:o.selectedSelector,firstSelectedSelector:o.firstSelectedSelector,lastSelectedSelector:o.lastSelectedSelector}}},Of=function(e){return!1===jo(Re.fromDom(e.target),"ephox-snooker-resizer-bar")};function Ef(h,p){var v=te(["mousedown","mouseover","mouseup","keyup","keydown"],[]),b=S.none(),w=Df.byAttr(Er);return h.on("init",function(e){var r=h.getWin(),o=Yu(h),t=Ju(h),n=Rf.mouse(r,o,t,w),a=Rf.keyboard(r,o,t,w),c=function(e,t){!0===e.raw().shiftKey&&(t.kill()&&e.kill(),t.selection().each(function(e){var t=Tc.relative(e.start(),e.finish()),n=Ic(r,t);h.selection.setRng(n)}))},i=function(e){var t=f(e);if(t.raw().shiftKey&&xf.isNavigation(t.raw().which)){var n=h.selection.getRng(),r=Re.fromDom(n.startContainer),o=Re.fromDom(n.endContainer);a.keyup(t,r,n.startOffset,o,n.endOffset).each(function(e){c(t,e)})}},u=function(e){var t=f(e);p().each(function(e){e.hideBars()});var n=h.selection.getRng(),r=Re.fromDom(h.selection.getStart()),o=Re.fromDom(n.startContainer),i=Re.fromDom(n.endContainer),u=ta.directionAt(r).isRtl()?xf.rtl:xf.ltr;a.keydown(t,o,n.startOffset,i,n.endOffset,u).each(function(e){c(t,e)}),p().each(function(e){e.showBars()})},l=function(e){return e.hasOwnProperty("x")&&e.hasOwnProperty("y")},f=function(e){var t=Re.fromDom(e.target),n=function(){e.stopPropagation()},r=function(){e.preventDefault()},o=x(r,n);return{target:C(t),x:C(l(e)?e.x:null),y:C(l(e)?e.y:null),stop:n,prevent:r,kill:o,raw:C(e)}},s=function(e){return 0===e.button},d=function(e){s(e)&&Of(e)&&n.mousedown(f(e))},m=function(e){var t;(t=e).buttons!==undefined&&0==(1&t.buttons)||!Of(e)||n.mouseover(f(e))},g=function(e){s(e)&&Of(e)&&n.mouseup(f(e))};h.on("mousedown",d),h.on("mouseover",m),h.on("mouseup",g),h.on("keyup",i),h.on("keydown",u),h.on("nodechange",function(){var e=h.selection,t=Re.fromDom(e.getStart()),n=Re.fromDom(e.getEnd());hr.sharedOne(pn.table,[t,n]).fold(function(){w.clear(o)},y)}),b=S.some(v({mousedown:d,mouseover:m,mouseup:g,keyup:i,keydown:u}))}),{clear:w.clear,destroy:function(){b.each(function(e){})}}}var Nf=wa.each,kf=function(t){var n=[];function e(e){return function(){t.execCommand(e)}}Nf("inserttable tableprops deletetable | cell row column".split(" "),function(e){"|"===e?n.push({text:"-"}):n.push(t.menuItems[e])}),t.addButton("table",{type:"menubutton",title:"Table",menu:n}),t.addButton("tableprops",{title:"Table properties",onclick:e("mceTableProps"),icon:"table"}),t.addButton("tabledelete",{title:"Delete table",onclick:e("mceTableDelete")}),t.addButton("tablecellprops",{title:"Cell properties",onclick:e("mceTableCellProps")}),t.addButton("tablemergecells",{title:"Merge cells",onclick:e("mceTableMergeCells")}),t.addButton("tablesplitcells",{title:"Split cell",onclick:e("mceTableSplitCells")}),t.addButton("tableinsertrowbefore",{title:"Insert row before",onclick:e("mceTableInsertRowBefore")}),t.addButton("tableinsertrowafter",{title:"Insert row after",onclick:e("mceTableInsertRowAfter")}),t.addButton("tabledeleterow",{title:"Delete row",onclick:e("mceTableDeleteRow")}),t.addButton("tablerowprops",{title:"Row properties",onclick:e("mceTableRowProps")}),t.addButton("tablecutrow",{title:"Cut row",onclick:e("mceTableCutRow")}),t.addButton("tablecopyrow",{title:"Copy row",onclick:e("mceTableCopyRow")}),t.addButton("tablepasterowbefore",{title:"Paste row before",onclick:e("mceTablePasteRowBefore")}),t.addButton("tablepasterowafter",{title:"Paste row after",onclick:e("mceTablePasteRowAfter")}),t.addButton("tableinsertcolbefore",{title:"Insert column before",onclick:e("mceTableInsertColBefore")}),t.addButton("tableinsertcolafter",{title:"Insert column after",onclick:e("mceTableInsertColAfter")}),t.addButton("tabledeletecol",{title:"Delete column",onclick:e("mceTableDeleteCol")})},Af=function(t){var e,n=""===(e=t.getParam("table_toolbar",na))||!1===e?[]:w(e)?e.split(/[ ,]/):R(e)?e:[];0<n.length&&t.addContextToolbar(function(e){return t.dom.is(e,"table")&&t.getBody().contains(e)},n.join(" "))},Pf=function(o,n){var r=S.none(),i=[],u=[],a=[],c=[],l=function(e){e.disabled(!0)},f=function(e){e.disabled(!1)},e=function(){var t=this;i.push(t),r.fold(function(){l(t)},function(e){f(t)})},t=function(){var t=this;u.push(t),r.fold(function(){l(t)},function(e){f(t)})};o.on("init",function(){o.on("nodechange",function(e){var t=S.from(o.dom.getParent(o.selection.getStart(),"th,td"));(r=t.bind(function(e){var t=Re.fromDom(e);return pn.table(t).map(function(e){return Lr.forMenu(n,e,t)})})).fold(function(){I(i,l),I(u,l),I(a,l),I(c,l)},function(t){I(i,f),I(u,f),I(a,function(e){e.disabled(t.mergable().isNone())}),I(c,function(e){e.disabled(t.unmergable().isNone())})})})});var s=function(e,t,n,r){var o,i,u,a,c,l=r.getEl().getElementsByTagName("table")[0],f=r.isRtl()||"tl-tr"===r.parent().rel;for(l.nextSibling.innerHTML=t+1+" x "+(n+1),f&&(t=9-t),i=0;i<10;i++)for(o=0;o<10;o++)a=l.rows[i].childNodes[o].firstChild,c=(f?t<=o:o<=t)&&i<=n,e.dom.toggleClass(a,"mce-active",c),c&&(u=a);return u.parentNode},d=!1===o.getParam("table_grid",!0,"boolean")?{text:"Table",icon:"table",context:"table",onclick:m("mceInsertTable")}:{text:"Table",icon:"table",context:"table",ariaHideMenu:!0,onclick:function(e){e.aria&&(this.parent().hideAll(),e.stopImmediatePropagation(),o.execCommand("mceInsertTable"))},onshow:function(){s(o,0,0,this.menu.items()[0])},onhide:function(){var e=this.menu.items()[0].getEl().getElementsByTagName("a");o.dom.removeClass(e,"mce-active"),o.dom.addClass(e[0],"mce-active")},menu:[{type:"container",html:function(){var e="";e='<table role="grid" class="mce-grid mce-grid-border" aria-readonly="true">';for(var t=0;t<10;t++){e+="<tr>";for(var n=0;n<10;n++)e+='<td role="gridcell" tabindex="-1"><a id="mcegrid'+(10*t+n)+'" href="#" data-mce-x="'+n+'" data-mce-y="'+t+'"></a></td>';e+="</tr>"}return e+="</table>",e+='<div class="mce-text-center" role="presentation">1 x 1</div>'}(),onPostRender:function(){this.lastX=this.lastY=0},onmousemove:function(e){var t,n,r=e.target;"A"===r.tagName.toUpperCase()&&(t=parseInt(r.getAttribute("data-mce-x"),10),n=parseInt(r.getAttribute("data-mce-y"),10),(this.isRtl()||"tl-tr"===this.parent().rel)&&(t=9-t),t===this.lastX&&n===this.lastY||(s(o,t,n,e.control),this.lastX=t,this.lastY=n))},onclick:function(e){var t=this;"A"===e.target.tagName.toUpperCase()&&(e.preventDefault(),e.stopPropagation(),t.parent().cancel(),o.undoManager.transact(function(){Ma(o,t.lastX+1,t.lastY+1)}),o.addVisual())}}]};function m(e){return function(){o.execCommand(e)}}var g={text:"Table properties",context:"table",onPostRender:e,onclick:m("mceTableProps")},h={text:"Delete table",context:"table",onPostRender:e,cmd:"mceTableDelete"},p={text:"Row",context:"table",menu:[{text:"Insert row before",onclick:m("mceTableInsertRowBefore"),onPostRender:t},{text:"Insert row after",onclick:m("mceTableInsertRowAfter"),onPostRender:t},{text:"Delete row",onclick:m("mceTableDeleteRow"),onPostRender:t},{text:"Row properties",onclick:m("mceTableRowProps"),onPostRender:t},{text:"-"},{text:"Cut row",onclick:m("mceTableCutRow"),onPostRender:t},{text:"Copy row",onclick:m("mceTableCopyRow"),onPostRender:t},{text:"Paste row before",onclick:m("mceTablePasteRowBefore"),onPostRender:t},{text:"Paste row after",onclick:m("mceTablePasteRowAfter"),onPostRender:t}]},v={text:"Column",context:"table",menu:[{text:"Insert column before",onclick:m("mceTableInsertColBefore"),onPostRender:t},{text:"Insert column after",onclick:m("mceTableInsertColAfter"),onPostRender:t},{text:"Delete column",onclick:m("mceTableDeleteCol"),onPostRender:t}]},b={separator:"before",text:"Cell",context:"table",menu:[{text:"Cell properties",onclick:m("mceTableCellProps"),onPostRender:t},{text:"Merge cells",onclick:m("mceTableMergeCells"),onPostRender:function(){var t=this;a.push(t),r.fold(function(){l(t)},function(e){t.disabled(e.mergable().isNone())})}},{text:"Split cell",onclick:m("mceTableSplitCells"),onPostRender:function(){var t=this;c.push(t),r.fold(function(){l(t)},function(e){t.disabled(e.unmergable().isNone())})}}]};o.addMenuItem("inserttable",d),o.addMenuItem("tableprops",g),o.addMenuItem("deletetable",h),o.addMenuItem("row",p),o.addMenuItem("column",v),o.addMenuItem("cell",b)},If=function(n,r){return{insertTable:function(e,t){return Ma(n,e,t)},setClipboardRows:function(e){return t=r,n=P(e,Re.fromDom),void t.set(S.from(n));var t,n},getClipboardRows:function(){return r.get().fold(function(){},function(e){return P(e,function(e){return e.dom()})})}}};e.add("table",function(t){var n,r=hc(t),e=Ef(t,r.lazyResize),o=va(t,r.lazyWire),i=(n=t,{get:function(){var e=Yu(n);return Sr(e,Er.selectedSelector()).fold(function(){return n.selection.getStart()===undefined?Ar.none():Ar.single(n.selection)},function(e){return Ar.multiple(e)})}}),u=vu(S.none());return Ha(t,o,e,i,u),jr(t,i,o,e),Pf(t,i),kf(t),Af(t),t.on("PreInit",function(){t.serializer.addTempAttr(Er.firstSelected()),t.serializer.addTempAttr(Er.lastSelected())}),t.getParam("table_tab_navigation",!0,"boolean")&&t.on("keydown",function(e){sl(e,t,o,r.lazyWire)}),t.on("remove",function(){r.destroy(),e.destroy()}),If(t,u)})}(window); \ No newline at end of file +!function(m){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),y=function(){},x=function(n,r){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return n(r.apply(null,e))}},C=function(e){return function(){return e}},o=function(e){return e};function b(r){for(var o=[],e=1;e<arguments.length;e++)o[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=o.concat(e);return r.apply(null,n)}}var t,n,r,i,g=function(n){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return!n.apply(null,e)}},f=C(!1),u=C(!0),a=function(){return c},c=(t=function(e){return e.isNone()},i={fold:function(e,t){return e()},is:f,isSome:f,isNone:u,getOr:r=function(e){return e},getOrThunk:n=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:C(null),getOrUndefined:C(undefined),or:r,orThunk:n,map:a,each:y,bind:a,exists:f,forall:u,filter:a,equals:t,equals_:t,toArray:function(){return[]},toString:C("none()")},Object.freeze&&Object.freeze(i),i),l=function(n){var e=C(n),t=function(){return o},r=function(e){return e(n)},o={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:u,isNone:f,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return l(e(n))},each:function(e){e(n)},bind:r,exists:r,forall:r,filter:function(e){return e(n)?o:c},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(f,function(e){return t(n,e)})}};return o},R={some:l,none:a,from:function(e){return null===e||e===undefined?c:l(e)}},s=function(t){return function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===t}},d=s("string"),h=s("array"),p=s("boolean"),v=s("function"),w=s("number"),S=Array.prototype.slice,T=Array.prototype.indexOf,D=Array.prototype.push,O=function(e,t){return n=e,r=t,-1<T.call(n,r);var n,r},N=function(e,t){for(var n=0,r=e.length;n<r;n++)if(t(e[n],n))return!0;return!1},E=function(e,t){for(var n=e.length,r=new Array(n),o=0;o<n;o++){var i=e[o];r[o]=t(i,o)}return r},k=function(e,t){for(var n=0,r=e.length;n<r;n++)t(e[n],n)},A=function(e,t){for(var n=[],r=0,o=e.length;r<o;r++){var i=e[r];t(i,r)&&n.push(i)}return n},P=function(e,t,n){return function(e,t){for(var n=e.length-1;0<=n;n--)t(e[n],n)}(e,function(e){n=t(n,e)}),n},I=function(e,t,n){return k(e,function(e){n=t(n,e)}),n},B=function(e,t){for(var n=0,r=e.length;n<r;n++){var o=e[n];if(t(o,n))return R.some(o)}return R.none()},W=function(e,t){for(var n=0,r=e.length;n<r;n++)if(t(e[n],n))return R.some(n);return R.none()},M=function(e){for(var t=[],n=0,r=e.length;n<r;++n){if(!h(e[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+e);D.apply(t,e[n])}return t},_=function(e,t){var n=E(e,t);return M(n)},L=function(e,t){for(var n=0,r=e.length;n<r;++n)if(!0!==t(e[n],n))return!1;return!0},F=function(e){var t=S.call(e,0);return t.reverse(),t},j=(v(Array.from)&&Array.from,Object.keys),z=function(e,t){for(var n=j(e),r=0,o=n.length;r<o;r++){var i=n[r];t(e[i],i)}},H=function(e,n){return U(e,function(e,t){return{k:t,v:n(e,t)}})},U=function(e,r){var o={};return z(e,function(e,t){var n=r(e,t);o[n.k]=n.v}),o},q=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];if(t.length!==n.length)throw new Error('Wrong number of arguments to struct. Expected "['+t.length+']", got '+n.length+" arguments");var r={};return k(t,function(e,t){r[e]=C(n[t])}),r}},V=function(e){return e.slice(0).sort()},G=function(e,t){throw new Error("All required keys ("+V(e).join(", ")+") were not specified. Specified keys were: "+V(t).join(", ")+".")},Y=function(e){throw new Error("Unsupported keys for object: "+V(e).join(", "))},X=function(t,e){if(!h(e))throw new Error("The "+t+" fields must be an array. Was: "+e+".");k(e,function(e){if(!d(e))throw new Error("The value "+e+" in the "+t+" fields was not a string.")})},K=function(e){var n=V(e);B(n,function(e,t){return t<n.length-1&&e===n[t+1]}).each(function(e){throw new Error("The field: "+e+" occurs more than once in the combined fields: ["+n.join(", ")+"].")})},J=function(o,i){var u=o.concat(i);if(0===u.length)throw new Error("You must specify at least one required or optional field.");return X("required",o),X("optional",i),K(u),function(t){var n=j(t);L(o,function(e){return O(n,e)})||G(o,n);var e=A(n,function(e){return!O(u,e)});0<e.length&&Y(e);var r={};return k(o,function(e){r[e]=C(t[e])}),k(i,function(e){r[e]=C(Object.prototype.hasOwnProperty.call(t,e)?R.some(t[e]):R.none())}),r}},$=(m.Node.ATTRIBUTE_NODE,m.Node.CDATA_SECTION_NODE,m.Node.COMMENT_NODE),Q=m.Node.DOCUMENT_NODE,Z=(m.Node.DOCUMENT_TYPE_NODE,m.Node.DOCUMENT_FRAGMENT_NODE,m.Node.ELEMENT_NODE),ee=m.Node.TEXT_NODE,te=(m.Node.PROCESSING_INSTRUCTION_NODE,m.Node.ENTITY_REFERENCE_NODE,m.Node.ENTITY_NODE,m.Node.NOTATION_NODE,"undefined"!=typeof m.window?m.window:Function("return this;")()),ne=function(e,t){return function(e,t){for(var n=t!==undefined&&null!==t?t:te,r=0;r<e.length&&n!==undefined&&null!==n;++r)n=n[e[r]];return n}(e.split("."),t)},re=function(e,t){var n=ne(e,t);if(n===undefined||null===n)throw new Error(e+" not available on this browser");return n},oe=function(e){return e.dom().nodeName.toLowerCase()},ie=function(e){return e.dom().nodeType},ue=function(t){return function(e){return ie(e)===t}},ae=function(e){return ie(e)===$||"#comment"===oe(e)},ce=ue(Z),le=ue(ee),fe=function(e,t,n){if(!(d(n)||p(n)||w(n)))throw m.console.error("Invalid call to Attr.set. Key ",t,":: Value ",n,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,n+"")},se=function(e,t,n){fe(e.dom(),t,n)},de=function(e,t){var n=e.dom();z(t,function(e,t){fe(n,t,e)})},me=function(e,t){var n=e.dom().getAttribute(t);return null===n?undefined:n},ge=function(e,t){var n=e.dom();return!(!n||!n.hasAttribute)&&n.hasAttribute(t)},he=function(e,t){e.dom().removeAttribute(t)},pe=function(e){return I(e.dom().attributes,function(e,t){return e[t.name]=t.value,e},{})},ve=function(e,t){return-1!==e.indexOf(t)},be=function(e){return e.style!==undefined&&v(e.style.getPropertyValue)},we=function(n){var r,o=!1;return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return o||(o=!0,r=n.apply(null,e)),r}},ye=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:C(e)}},xe={fromHtml:function(e,t){var n=(t||m.document).createElement("div");if(n.innerHTML=e,!n.hasChildNodes()||1<n.childNodes.length)throw m.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return ye(n.childNodes[0])},fromTag:function(e,t){var n=(t||m.document).createElement(e);return ye(n)},fromText:function(e,t){var n=(t||m.document).createTextNode(e);return ye(n)},fromDom:ye,fromPoint:function(e,t,n){var r=e.dom();return R.from(r.elementFromPoint(t,n)).map(ye)}},Ce=function(e){var t=le(e)?e.dom().parentNode:e.dom();return t!==undefined&&null!==t&&t.ownerDocument.body.contains(t)},Re=we(function(){return Se(xe.fromDom(m.document))}),Se=function(e){var t=e.dom().body;if(null===t||t===undefined)throw new Error("Body is not available yet");return xe.fromDom(t)},Te=function(e,t,n){if(!d(n))throw m.console.error("Invalid call to CSS.set. Property ",t,":: Value ",n,":: Element ",e),new Error("CSS value must be a string: "+n);be(e)&&e.style.setProperty(t,n)},De=function(e,t,n){var r=e.dom();Te(r,t,n)},Oe=function(e,t){var n=e.dom();z(t,function(e,t){Te(n,t,e)})},Ne=function(e,t){var n=e.dom(),r=m.window.getComputedStyle(n).getPropertyValue(t),o=""!==r||Ce(e)?r:Ee(n,t);return null===o?undefined:o},Ee=function(e,t){return be(e)?e.style.getPropertyValue(t):""},ke=function(e,t){var n=e.dom(),r=Ee(n,t);return R.from(r).filter(function(e){return 0<e.length})},Ae=function(e,t){var n,r,o=e.dom();r=t,be(n=o)&&n.style.removeProperty(r),ge(e,"style")&&""===me(e,"style").replace(/^\s+|\s+$/g,"")&&he(e,"style")},Pe=function(){return re("Node")},Ie=function(e,t,n){return 0!=(e.compareDocumentPosition(t)&n)},Be=function(e,t){return Ie(e,t,Pe().DOCUMENT_POSITION_CONTAINED_BY)},We=function(e,t){var n=function(e,t){for(var n=0;n<e.length;n++){var r=e[n];if(r.test(t))return r}return undefined}(e,t);if(!n)return{major:0,minor:0};var r=function(e){return Number(t.replace(n,"$"+e))};return _e(r(1),r(2))},Me=function(){return _e(0,0)},_e=function(e,t){return{major:e,minor:t}},Le={nu:_e,detect:function(e,t){var n=String(t).toLowerCase();return 0===e.length?Me():We(e,n)},unknown:Me},Fe="Firefox",je=function(e,t){return function(){return t===e}},ze=function(e){var t=e.current;return{current:t,version:e.version,isEdge:je("Edge",t),isChrome:je("Chrome",t),isIE:je("IE",t),isOpera:je("Opera",t),isFirefox:je(Fe,t),isSafari:je("Safari",t)}},He={unknown:function(){return ze({current:undefined,version:Le.unknown()})},nu:ze,edge:C("Edge"),chrome:C("Chrome"),ie:C("IE"),opera:C("Opera"),firefox:C(Fe),safari:C("Safari")},Ue="Windows",qe="Android",Ve="Solaris",Ge="FreeBSD",Ye=function(e,t){return function(){return t===e}},Xe=function(e){var t=e.current;return{current:t,version:e.version,isWindows:Ye(Ue,t),isiOS:Ye("iOS",t),isAndroid:Ye(qe,t),isOSX:Ye("OSX",t),isLinux:Ye("Linux",t),isSolaris:Ye(Ve,t),isFreeBSD:Ye(Ge,t)}},Ke={unknown:function(){return Xe({current:undefined,version:Le.unknown()})},nu:Xe,windows:C(Ue),ios:C("iOS"),android:C(qe),linux:C("Linux"),osx:C("OSX"),solaris:C(Ve),freebsd:C(Ge)},Je=function(e,t){var n=String(t).toLowerCase();return B(e,function(e){return e.search(n)})},$e=function(e,n){return Je(e,n).map(function(e){var t=Le.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Qe=function(e,n){return Je(e,n).map(function(e){var t=Le.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Ze=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,et=function(t){return function(e){return ve(e,t)}},tt=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return ve(e,"edge/")&&ve(e,"chrome")&&ve(e,"safari")&&ve(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,Ze],search:function(e){return ve(e,"chrome")&&!ve(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return ve(e,"msie")||ve(e,"trident")}},{name:"Opera",versionRegexes:[Ze,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:et("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:et("firefox")},{name:"Safari",versionRegexes:[Ze,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return(ve(e,"safari")||ve(e,"mobile/"))&&ve(e,"applewebkit")}}],nt=[{name:"Windows",search:et("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return ve(e,"iphone")||ve(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:et("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:et("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:et("linux"),versionRegexes:[]},{name:"Solaris",search:et("sunos"),versionRegexes:[]},{name:"FreeBSD",search:et("freebsd"),versionRegexes:[]}],rt={browsers:C(tt),oses:C(nt)},ot=function(e){var t,n,r,o,i,u,a,c,l,f,s,d=rt.browsers(),m=rt.oses(),g=$e(d,e).fold(He.unknown,He.nu),h=Qe(m,e).fold(Ke.unknown,Ke.nu);return{browser:g,os:h,deviceType:(n=g,r=e,o=(t=h).isiOS()&&!0===/ipad/i.test(r),i=t.isiOS()&&!o,u=t.isAndroid()&&3===t.version.major,a=t.isAndroid()&&4===t.version.major,c=o||u||a&&!0===/mobile/i.test(r),l=t.isiOS()||t.isAndroid(),f=l&&!c,s=n.isSafari()&&t.isiOS()&&!1===/safari/i.test(r),{isiPad:C(o),isiPhone:C(i),isTablet:C(c),isPhone:C(f),isTouch:C(l),isAndroid:t.isAndroid,isiOS:t.isiOS,isWebView:C(s)})}},it={detect:we(function(){var e=m.navigator.userAgent;return ot(e)})},ut=Z,at=Q,ct=function(e,t){var n=e.dom();if(n.nodeType!==ut)return!1;var r=n;if(r.matches!==undefined)return r.matches(t);if(r.msMatchesSelector!==undefined)return r.msMatchesSelector(t);if(r.webkitMatchesSelector!==undefined)return r.webkitMatchesSelector(t);if(r.mozMatchesSelector!==undefined)return r.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")},lt=function(e){return e.nodeType!==ut&&e.nodeType!==at||0===e.childElementCount},ft=function(e,t){return e.dom()===t.dom()},st=it.detect().browser.isIE()?function(e,t){return Be(e.dom(),t.dom())}:function(e,t){var n=e.dom(),r=t.dom();return n!==r&&n.contains(r)},dt=ct,mt=function(e){return xe.fromDom(e.dom().ownerDocument)},gt=function(e){return R.from(e.dom().parentNode).map(xe.fromDom)},ht=function(e,t){for(var n=v(t)?t:f,r=e.dom(),o=[];null!==r.parentNode&&r.parentNode!==undefined;){var i=r.parentNode,u=xe.fromDom(i);if(o.push(u),!0===n(u))break;r=i}return o},pt=function(e){return R.from(e.dom().previousSibling).map(xe.fromDom)},vt=function(e){return R.from(e.dom().nextSibling).map(xe.fromDom)},bt=function(e){return E(e.dom().childNodes,xe.fromDom)},wt=function(e,t){var n=e.dom().childNodes;return R.from(n[t]).map(xe.fromDom)},yt=(q("element","offset"),function(t,n){gt(t).each(function(e){e.dom().insertBefore(n.dom(),t.dom())})}),xt=function(e,t){vt(e).fold(function(){gt(e).each(function(e){Rt(e,t)})},function(e){yt(e,t)})},Ct=function(t,n){wt(t,0).fold(function(){Rt(t,n)},function(e){t.dom().insertBefore(n.dom(),e.dom())})},Rt=function(e,t){e.dom().appendChild(t.dom())},St=function(e,t){yt(e,t),Rt(t,e)},Tt=function(r,o){k(o,function(e,t){var n=0===t?r:o[t-1];xt(n,e)})},Dt=function(t,e){k(e,function(e){Rt(t,e)})},Ot=function(e){e.dom().textContent="",k(bt(e),function(e){Nt(e)})},Nt=function(e){var t=e.dom();null!==t.parentNode&&t.parentNode.removeChild(t)},Et=function(e){var t,n=bt(e);0<n.length&&(t=e,k(n,function(e){yt(t,e)})),Nt(e)},kt=(q("width","height"),q("width","height"),q("rows","columns")),At=q("row","column"),Pt=(q("x","y"),q("element","rowspan","colspan")),It=q("element","rowspan","colspan","isNew"),Bt=q("element","rowspan","colspan","row","column"),Wt=q("element","cells","section"),Mt=q("element","isNew"),_t=q("element","cells","section","isNew"),Lt=q("cells","section"),Ft=q("details","section"),jt=q("startRow","startCol","finishRow","finishCol"),zt=function(e,t){var n=[];return k(bt(e),function(e){t(e)&&(n=n.concat([e])),n=n.concat(zt(e,t))}),n},Ht=function(e,t,n){return r=function(e){return ct(e,t)},A(ht(e,n),r);var r},Ut=function(e,t){return n=function(e){return ct(e,t)},A(bt(e),n);var n},qt=function(e,t){return n=t,o=(r=e)===undefined?m.document:r.dom(),lt(o)?[]:E(o.querySelectorAll(n),xe.fromDom);var n,r,o};function Vt(e,t,n,r,o){return e(n,r)?R.some(n):v(o)&&o(n)?R.none():t(n,r,o)}var Gt,Yt,Xt,Kt=function(e,t,n){for(var r=e.dom(),o=v(n)?n:C(!1);r.parentNode;){r=r.parentNode;var i=xe.fromDom(r);if(t(i))return R.some(i);if(o(i))break}return R.none()},Jt=function(e,t,n){return Kt(e,function(e){return ct(e,t)},n)},$t=function(e,t){return n=function(e){return ct(e,t)},B(e.dom().childNodes,function(e){return n(xe.fromDom(e))}).map(xe.fromDom);var n},Qt=function(e,t){return n=t,o=(r=e)===undefined?m.document:r.dom(),lt(o)?R.none():R.from(o.querySelector(n)).map(xe.fromDom);var n,r,o},Zt=function(e,t,n){return Vt(ct,Jt,e,t,n)},en=function(e,t,n){return _(bt(e),function(e){return ct(e,t)?n(e)?[e]:[]:en(e,t,n)})},tn={firstLayer:function(e,t){return en(e,t,C(!0))},filterFirstLayer:en},nn=function(e,t,n){return void 0===n&&(n=f),n(t)?R.none():O(e,oe(t))?R.some(t):Jt(t,e.join(","),function(e){return ct(e,"table")||n(e)})},rn=function(t,e){return gt(e).map(function(e){return Ut(e,t)})},on=b(rn,"th,td"),un=b(rn,"tr"),an=function(e,t){return parseInt(me(e,t),10)},cn={cell:function(e,t){return nn(["td","th"],e,t)},firstCell:function(e){return Qt(e,"th,td")},cells:function(e){return tn.firstLayer(e,"th,td")},neighbourCells:on,table:function(e,t){return Zt(e,"table",t)},row:function(e,t){return nn(["tr"],e,t)},rows:function(e){return tn.firstLayer(e,"tr")},notCell:function(e,t){return nn(["caption","tr","tbody","tfoot","thead"],e,t)},neighbourRows:un,attr:an,grid:function(e,t,n){var r=an(e,t),o=an(e,n);return kt(r,o)}},ln=function(e){var t=cn.rows(e);return E(t,function(e){var t=e,n=gt(t).map(function(e){var t=oe(e);return"tfoot"===t||"thead"===t||"tbody"===t?t:"tbody"}).getOr("tbody"),r=E(cn.cells(e),function(e){var t=ge(e,"rowspan")?parseInt(me(e,"rowspan"),10):1,n=ge(e,"colspan")?parseInt(me(e,"colspan"),10):1;return Pt(e,t,n)});return Wt(t,r,n)})},fn=function(e,n){return E(e,function(e){var t=E(cn.cells(e),function(e){var t=ge(e,"rowspan")?parseInt(me(e,"rowspan"),10):1,n=ge(e,"colspan")?parseInt(me(e,"colspan"),10):1;return Pt(e,t,n)});return Wt(e,t,n.section())})},sn=function(e,t){return e+","+t},dn=function(e,t){var n=_(e.all(),function(e){return e.cells()});return A(n,t)},mn={generate:function(e){var l={},t=[],n=e.length,f=0;k(e,function(e,a){var c=[];k(e.cells(),function(e){for(var t=0;l[sn(a,t)]!==undefined;)t++;for(var n=Bt(e.element(),e.rowspan(),e.colspan(),a,t),r=0;r<e.colspan();r++)for(var o=0;o<e.rowspan();o++){var i=t+r,u=sn(a+o,i);l[u]=n,f=Math.max(f,i+1)}c.push(n)}),t.push(Wt(e.element(),c,e.section()))});var r=kt(n,f);return{grid:C(r),access:C(l),all:C(t)}},getAt:function(e,t,n){var r=e.access()[sn(t,n)];return r!==undefined?R.some(r):R.none()},findItem:function(e,t,n){var r=dn(e,function(e){return n(t,e.element())});return 0<r.length?R.some(r[0]):R.none()},filterItems:dn,justCells:function(e){var t=E(e.all(),function(e){return e.cells()});return M(t)}},gn=q("minRow","minCol","maxRow","maxCol"),hn=function(e,t){var n,i,r,u,a,c,l,o,f,s,d=function(e){return ct(e.element(),t)},m=ln(e),g=mn.generate(m),h=(i=d,r=(n=g).grid().columns(),u=n.grid().rows(),a=r,l=c=0,z(n.access(),function(e){if(i(e)){var t=e.row(),n=t+e.rowspan()-1,r=e.column(),o=r+e.colspan()-1;t<u?u=t:c<n&&(c=n),r<a?a=r:l<o&&(l=o)}}),gn(u,a,c,l)),p="th:not("+t+"),td:not("+t+")",v=tn.filterFirstLayer(e,"th,td",function(e){return ct(e,p)});return k(v,Nt),function(e,t,n,r){for(var o,i,u,a=t.grid().columns(),c=t.grid().rows(),l=0;l<c;l++)for(var f=!1,s=0;s<a;s++)l<n.minRow()||l>n.maxRow()||s<n.minCol()||s>n.maxCol()||(mn.getAt(t,l,s).filter(r).isNone()?(o=f,i=e[l].element(),u=xe.fromTag("td"),Rt(u,xe.fromTag("br")),(o?Rt:Ct)(i,u)):f=!0)}(m,g,h,d),o=e,f=h,s=A(tn.firstLayer(o,"tr"),function(e){return 0===e.dom().childElementCount}),k(s,Nt),f.minCol()!==f.maxCol()&&f.minRow()!==f.maxRow()||k(tn.firstLayer(o,"th,td"),function(e){he(e,"rowspan"),he(e,"colspan")}),he(o,"width"),he(o,"height"),Ae(o,"width"),Ae(o,"height"),e},pn=(Gt=le,Yt="text",{get:function(e){if(!Gt(e))throw new Error("Can only get "+Yt+" value of a "+Yt+" node");return Xt(e).getOr("")},getOption:Xt=function(e){return Gt(e)?R.from(e.dom().nodeValue):R.none()},set:function(e,t){if(!Gt(e))throw new Error("Can only set raw "+Yt+" value of a "+Yt+" node");e.dom().nodeValue=t}}),vn=function(e){return pn.get(e)},bn=function(e){return pn.getOption(e)},wn=function(e,t){pn.set(e,t)},yn=function(e){return"img"===oe(e)?1:bn(e).fold(function(){return bt(e).length},function(e){return e.length})},xn=["img","br"],Cn=function(e){return bn(e).filter(function(e){return 0!==e.trim().length||-1<e.indexOf("\xa0")}).isSome()||O(xn,oe(e))},Rn=function(e){return o=Cn,(i=function(e){for(var t=0;t<e.childNodes.length;t++){var n=xe.fromDom(e.childNodes[t]);if(o(n))return R.some(n);var r=i(e.childNodes[t]);if(r.isSome())return r}return R.none()})(e.dom());var o,i},Sn=function(e){return Tn(e,Cn)},Tn=function(e,i){var u=function(e){for(var t=bt(e),n=t.length-1;0<=n;n--){var r=t[n];if(i(r))return R.some(r);var o=u(r);if(o.isSome())return o}return R.none()};return u(e)},Dn=function(e,t){return xe.fromDom(e.dom().cloneNode(t))},On=function(e){return Dn(e,!1)},Nn=function(e){return Dn(e,!0)},En=function(e,t){var n,r,o,i,u=(n=e,r=t,o=xe.fromTag(r),i=pe(n),de(o,i),o),a=bt(Nn(e));return Dt(u,a),u},kn=function(){var e=xe.fromTag("td");return Rt(e,xe.fromTag("br")),e},An=function(e,t,n){var r=En(e,t);return z(n,function(e,t){null===e?he(r,t):se(r,t,e)}),r},Pn=function(e){return e},In=function(e){return function(){return xe.fromTag("tr",e.dom())}},Bn=function(d,e,m){return{row:In(e),cell:function(e){var r,o,i,t,n,u,a,c=mt(e.element()),l=xe.fromTag(oe(e.element()),c.dom()),f=m.getOr(["strong","em","b","i","span","font","h1","h2","h3","h4","h5","h6","p","div"]),s=0<f.length?(r=e.element(),o=l,i=f,Rn(r).map(function(e){var t=i.join(","),n=Ht(e,t,function(e){return ft(e,r)});return P(n,function(e,t){var n=On(t);return he(n,"contenteditable"),Rt(e,n),n},o)}).getOr(o)):l;return Rt(s,xe.fromTag("br")),t=e.element(),n=l,u=t.dom(),a=n.dom(),be(u)&&be(a)&&(a.style.cssText=u.style.cssText),Ae(l,"height"),1!==e.colspan()&&Ae(e.element(),"width"),d(e.element(),l),l},replace:An,gap:kn}},Wn=function(e){return{row:In(e),cell:kn,replace:Pn,gap:kn}},Mn=function(e,t){return t.column()>=e.startCol()&&t.column()+t.colspan()-1<=e.finishCol()&&t.row()>=e.startRow()&&t.row()+t.rowspan()-1<=e.finishRow()},_n=function(e,t){var n=t.column(),r=t.column()+t.colspan()-1,o=t.row(),i=t.row()+t.rowspan()-1;return n<=e.finishCol()&&r>=e.startCol()&&o<=e.finishRow()&&i>=e.startRow()},Ln=function(e,t){for(var n=!0,r=b(Mn,t),o=t.startRow();o<=t.finishRow();o++)for(var i=t.startCol();i<=t.finishCol();i++)n=n&&mn.getAt(e,o,i).exists(r);return n?R.some(t):R.none()},Fn=function(e,t,n){var r=mn.findItem(e,t,ft),o=mn.findItem(e,n,ft);return r.bind(function(r){return o.map(function(e){return t=r,n=e,jt(Math.min(t.row(),n.row()),Math.min(t.column(),n.column()),Math.max(t.row()+t.rowspan()-1,n.row()+n.rowspan()-1),Math.max(t.column()+t.colspan()-1,n.column()+n.colspan()-1));var t,n})})},jn=Fn,zn=function(t,e,n){return Fn(t,e,n).bind(function(e){return Ln(t,e)})},Hn=function(r,e,o,i){return mn.findItem(r,e,ft).bind(function(e){var t=0<o?e.row()+e.rowspan()-1:e.row(),n=0<i?e.column()+e.colspan()-1:e.column();return mn.getAt(r,t+o,n+i).map(function(e){return e.element()})})},Un=function(n,e,t){return jn(n,e,t).map(function(e){var t=mn.filterItems(n,b(_n,e));return E(t,function(e){return e.element()})})},qn=function(e,t){return mn.findItem(e,t,function(e,t){return st(t,e)}).map(function(e){return e.element()})},Vn=function(e){var t=ln(e);return mn.generate(t)},Gn=function(n,r,o){return cn.table(n).bind(function(e){var t=Vn(e);return Hn(t,n,r,o)})},Yn=function(e,t,n){var r=Vn(e);return Un(r,t,n)},Xn=function(e,t,n,r,o){var i=Vn(e),u=ft(e,n)?R.some(t):qn(i,t),a=ft(e,o)?R.some(r):qn(i,r);return u.bind(function(t){return a.bind(function(e){return Un(i,t,e)})})},Kn=function(e,t,n){var r=Vn(e);return zn(r,t,n)},Jn=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","li","table","thead","tbody","tfoot","caption","tr","td","th","h1","h2","h3","h4","h5","h6","blockquote","pre","address"];function $n(){return{up:C({selector:Jt,closest:Zt,predicate:Kt,all:ht}),down:C({selector:qt,predicate:zt}),styles:C({get:Ne,getRaw:ke,set:De,remove:Ae}),attrs:C({get:me,set:se,remove:he,copyTo:function(e,t){var n=pe(e);de(t,n)}}),insert:C({before:yt,after:xt,afterAll:Tt,append:Rt,appendAll:Dt,prepend:Ct,wrap:St}),remove:C({unwrap:Et,remove:Nt}),create:C({nu:xe.fromTag,clone:function(e){return xe.fromDom(e.dom().cloneNode(!1))},text:xe.fromText}),query:C({comparePosition:function(e,t){return e.dom().compareDocumentPosition(t.dom())},prevSibling:pt,nextSibling:vt}),property:C({children:bt,name:oe,parent:gt,document:function(e){return e.dom().ownerDocument},isText:le,isComment:ae,isElement:ce,getText:vn,setText:wn,isBoundary:function(e){return!!ce(e)&&("body"===oe(e)||O(Jn,oe(e)))},isEmptyTag:function(e){return!!ce(e)&&O(["br","img","hr","input"],oe(e))}}),eq:ft,is:dt}}var Qn=q("left","right"),Zn=q("first","second","splits"),er=function(e,t,n){var r=e.property().children(t);return W(r,b(e.eq,n)).map(function(e){return{before:C(r.slice(0,e)),after:C(r.slice(e+1))}})},tr=function(r,o,e,t){var n=o(r,e);return P(t,function(e,t){var n=o(r,t);return nr(r,e,n)},n)},nr=function(t,e,n){return e.bind(function(e){return n.filter(b(t.eq,e))})},rr=function(e,t){return b(e.eq,t)},or=function(t,e,n,r){void 0===r&&(r=f);var o=[e].concat(t.up().all(e)),i=[n].concat(t.up().all(n)),u=function(t){return W(t,r).fold(function(){return t},function(e){return t.slice(0,e+1)})},a=u(o),c=u(i),l=B(a,function(e){return N(c,rr(t,e))});return{firstpath:C(a),secondpath:C(c),shared:C(l)}},ir={sharedOne:function(e,t,n){return 0<n.length?tr(e,t,(r=n)[0],r.slice(1)):R.none();var r},subset:function(t,e,n){var r=or(t,e,n);return r.shared().bind(function(e){return function(o,i,e,t){var u=o.property().children(i);if(o.eq(i,e[0]))return R.some([e[0]]);if(o.eq(i,t[0]))return R.some([t[0]]);var n=function(e){var t=F(e),n=W(t,rr(o,i)).getOr(-1),r=n<t.length-1?t[n+1]:t[n];return W(u,rr(o,r))},r=n(e),a=n(t);return r.bind(function(r){return a.map(function(e){var t=Math.min(r,e),n=Math.max(r,e);return u.slice(t,n+1)})})}(t,e,r.firstpath(),r.secondpath())})},ancestors:or,breakToLeft:function(n,r,o){return er(n,r,o).map(function(e){var t=n.create().clone(r);return n.insert().appendAll(t,e.before().concat([o])),n.insert().appendAll(r,e.after()),n.insert().before(r,t),Qn(t,r)})},breakToRight:function(n,r,e){return er(n,r,e).map(function(e){var t=n.create().clone(r);return n.insert().appendAll(t,e.after()),n.insert().after(r,t),Qn(r,t)})},breakPath:function(i,e,u,a){var c=function(e,t,o){var n=Zn(e,R.none(),o);return u(e)?Zn(e,t,o):i.property().parent(e).bind(function(r){return a(i,r,e).map(function(e){var t=[{first:e.left,second:e.right}],n=u(r)?r:e.left();return c(n,R.some(e.right()),o.concat(t))})}).getOr(n)};return c(e,R.none(),[])}},ur=$n(),ar={sharedOne:function(n,e){return ir.sharedOne(ur,function(e,t){return n(t)},e)},subset:function(e,t){return ir.subset(ur,e,t)},ancestors:function(e,t,n){return ir.ancestors(ur,e,t,n)},breakToLeft:function(e,t){return ir.breakToLeft(ur,e,t)},breakToRight:function(e,t){return ir.breakToRight(ur,e,t)},breakPath:function(e,t,r){return ir.breakPath(ur,e,t,function(e,t,n){return r(t,n)})}},cr={create:J(["boxes","start","finish"],[])},lr=function(e){return Jt(e,"table")},fr=function(a,c,r){var l=function(t){return function(e){return r!==undefined&&r(e)||ft(e,t)}};return ft(a,c)?R.some(cr.create({boxes:R.some([a]),start:a,finish:c})):lr(a).bind(function(u){return lr(c).bind(function(i){if(ft(u,i))return R.some(cr.create({boxes:Yn(u,a,c),start:a,finish:c}));if(st(u,i)){var e=0<(t=Ht(c,"td,th",l(u))).length?t[t.length-1]:c;return R.some(cr.create({boxes:Xn(u,a,u,c,i),start:a,finish:e}))}if(st(i,u)){var t,n=0<(t=Ht(a,"td,th",l(i))).length?t[t.length-1]:a;return R.some(cr.create({boxes:Xn(i,a,u,c,i),start:a,finish:n}))}return ar.ancestors(a,c).shared().bind(function(e){return Zt(e,"table",r).bind(function(e){var t=Ht(c,"td,th",l(e)),n=0<t.length?t[t.length-1]:c,r=Ht(a,"td,th",l(e)),o=0<r.length?r[r.length-1]:a;return R.some(cr.create({boxes:Xn(e,a,u,c,i),start:o,finish:n}))})})})})},sr=fr,dr=function(e,t){var n=qt(e,t);return 0<n.length?R.some(n):R.none()},mr=function(e,t,n,r,o){return(i=e,u=o,B(i,function(e){return ct(e,u)})).bind(function(e){return Gn(e,t,n).bind(function(e){return n=r,Jt(t=e,"table").bind(function(e){return Qt(e,n).bind(function(e){return fr(e,t).bind(function(t){return t.boxes().map(function(e){return{boxes:C(e),start:C(t.start()),finish:C(t.finish())}})})})});var t,n})});var i,u},gr=function(e,t,r){return Qt(e,t).bind(function(n){return Qt(e,r).bind(function(t){return ar.sharedOne(lr,[n,t]).map(function(e){return{first:C(n),last:C(t),table:C(e)}})})})},hr=function(e,t){return dr(e,t)},pr=function(o,e,t){return gr(o,e,t).bind(function(n){var e=function(e){return ft(o,e)},t=Jt(n.first(),"thead,tfoot,tbody,table",e),r=Jt(n.last(),"thead,tfoot,tbody,table",e);return t.bind(function(t){return r.bind(function(e){return ft(t,e)?Kn(n.table(),n.first(),n.last()):R.none()})})})},vr="data-mce-selected",br="data-mce-first-selected",wr="data-mce-last-selected",yr={selected:C(vr),selectedSelector:C("td[data-mce-selected],th[data-mce-selected]"),attributeSelector:C("[data-mce-selected]"),firstSelected:C(br),firstSelectedSelector:C("td[data-mce-first-selected],th[data-mce-first-selected]"),lastSelected:C(wr),lastSelectedSelector:C("td[data-mce-last-selected],th[data-mce-last-selected]")},xr=function(u){if(!h(u))throw new Error("cases must be an array");if(0===u.length)throw new Error("there must be at least one case");var a=[],n={};return k(u,function(e,r){var t=j(e);if(1!==t.length)throw new Error("one and only one name per case");var o=t[0],i=e[o];if(n[o]!==undefined)throw new Error("duplicate key detected:"+o);if("cata"===o)throw new Error("cannot have a case named cata (sorry)");if(!h(i))throw new Error("case arguments must be an array");a.push(o),n[o]=function(){var e=arguments.length;if(e!==i.length)throw new Error("Wrong number of arguments to case "+o+". Expected "+i.length+" ("+i+"), got "+e);for(var n=new Array(e),t=0;t<n.length;t++)n[t]=arguments[t];return{fold:function(){if(arguments.length!==u.length)throw new Error("Wrong number of arguments to fold. Expected "+u.length+", got "+arguments.length);return arguments[r].apply(null,n)},match:function(e){var t=j(e);if(a.length!==t.length)throw new Error("Wrong number of arguments to match. Expected: "+a.join(",")+"\nActual: "+t.join(","));if(!L(a,function(e){return O(t,e)}))throw new Error("Not all branches were specified when using match. Specified: "+t.join(", ")+"\nRequired: "+a.join(", "));return e[o].apply(null,n)},log:function(e){m.console.log(e,{constructors:a,constructor:o,params:n})}}}}),n},Cr=xr([{none:[]},{multiple:["elements"]},{single:["selection"]}]),Rr={cata:function(e,t,n,r){return e.fold(t,n,r)},none:Cr.none,multiple:Cr.multiple,single:Cr.single},Sr=function(e,t){return Rr.cata(t.get(),C([]),o,C([e]))},Tr=function(n,e){return Rr.cata(e.get(),R.none,function(t,e){return 0===t.length?R.none():pr(n,yr.firstSelectedSelector(),yr.lastSelectedSelector()).bind(function(e){return 1<t.length?R.some({bounds:C(e),cells:C(t)}):R.none()})},R.none)},Dr=function(e,t){var n=Sr(e,t);return 0<n.length&&L(n,function(e){return ge(e,"rowspan")&&1<parseInt(me(e,"rowspan"),10)||ge(e,"colspan")&&1<parseInt(me(e,"colspan"),10)})?R.some(n):R.none()},Or=Sr,Nr=function(e){return{element:C(e),mergable:R.none,unmergable:R.none,selection:C([e])}},Er=q("element","clipboard","generators"),kr={noMenu:Nr,forMenu:function(e,t,n){return{element:C(n),mergable:C(Tr(t,e)),unmergable:C(Dr(n,e)),selection:C(Or(n,e))}},notCell:function(e){return Nr(e)},paste:Er,pasteRows:function(e,t,n,r,o){return{element:C(n),mergable:R.none,unmergable:R.none,selection:C(Or(n,e)),clipboard:C(r),generators:C(o)}}},Ar=function(f,e,s,d){f.on("BeforeGetContent",function(n){!0===n.selection&&Rr.cata(e.get(),y,function(e){var t;n.preventDefault(),(t=e,cn.table(t[0]).map(Nn).map(function(e){return[hn(e,yr.attributeSelector())]})).each(function(e){var t;n.content="text"===n.format?E(e,function(e){return e.dom().innerText}).join(""):(t=f,E(e,function(e){return t.selection.serializer.serialize(e.dom(),{})}).join(""))})},y)}),f.on("BeforeSetContent",function(l){!0===l.selection&&!0===l.paste&&R.from(f.dom.getParent(f.selection.getStart(),"th,td")).each(function(e){var c=xe.fromDom(e);cn.table(c).each(function(t){var e,n,r,o=A((e=l.content,(r=(n||m.document).createElement("div")).innerHTML=e,bt(xe.fromDom(r))),function(e){return"meta"!==oe(e)});if(1===o.length&&"table"===oe(o[0])){l.preventDefault();var i=xe.fromDom(f.getDoc()),u=Wn(i),a=kr.paste(c,o[0],u);s.pasteCells(t,a).each(function(e){f.selection.setRng(e),f.focus(),d.clear(t)})}})})})};function Pr(r,o){var e=function(e){var t=o(e);if(t<=0||null===t){var n=Ne(e,r);return parseFloat(n)||0}return t},i=function(o,e){return I(e,function(e,t){var n=Ne(o,t),r=n===undefined?0:parseInt(n,10);return isNaN(r)?e:e+r},0)};return{set:function(e,t){if(!w(t)&&!t.match(/^[0-9]+$/))throw new Error(r+".set accepts only positive integer values. Value was "+t);var n=e.dom();be(n)&&(n.style[r]=t+"px")},get:e,getOuter:e,aggregate:i,max:function(e,t,n){var r=i(e,n);return r<t?t-r:0}}}var Ir=Pr("height",function(e){var t=e.dom();return Ce(e)?t.getBoundingClientRect().height:t.offsetHeight}),Br=function(e){return Ir.get(e)},Wr=function(e){return Ir.getOuter(e)},Mr=Pr("width",function(e){return e.dom().offsetWidth}),_r=function(e){return Mr.get(e)},Lr=function(e){return Mr.getOuter(e)},Fr=it.detect(),jr=function(e,t,n){return r=Ne(e,t),o=n,i=parseFloat(r),isNaN(i)?o:i;var r,o,i},zr=function(e){return Fr.browser.isIE()||Fr.browser.isEdge()?(n=jr(t=e,"padding-top",0),r=jr(t,"padding-bottom",0),o=jr(t,"border-top-width",0),i=jr(t,"border-bottom-width",0),u=t.dom().getBoundingClientRect().height,"border-box"===Ne(t,"box-sizing")?u:u-n-r-(o+i)):jr(e,"height",Br(e));var t,n,r,o,i,u},Hr=/(\d+(\.\d+)?)(\w|%)*/,Ur=/(\d+(\.\d+)?)%/,qr=/(\d+(\.\d+)?)px|em/,Vr=function(e,t){De(e,"height",t+"px")},Gr=function(e,t,n,r){var o,i,u,a,c,l,f,s,d,m=parseInt(e,10);return s=l="%",d=(f=e).length-l.length,""!==s&&(f.length<s.length||f.substr(d,d+s.length)!==s)||"table"===oe(t)?m:(o=t,i=m,u=n,a=r,c=cn.table(o).map(function(e){var t=u(e);return Math.floor(i/100*t)}).getOr(i),a(o,c),c)},Yr=function(e){var t,n=ke(t=e,"height").getOrThunk(function(){return zr(t)+"px"});return n?Gr(n,e,Br,Vr):Br(e)},Xr=function(e,t){return ge(e,t)?parseInt(me(e,t),10):1},Kr=function(e){return ke(e,"width").fold(function(){return R.from(me(e,"width"))},function(e){return R.some(e)})},Jr=function(e,t){return e/t.pixelWidth()*100},$r={percentageBasedSizeRegex:C(Ur),pixelBasedSizeRegex:C(qr),setPixelWidth:function(e,t){De(e,"width",t+"px")},setPercentageWidth:function(e,t){De(e,"width",t+"%")},setHeight:Vr,getPixelWidth:function(t,n){return Kr(t).fold(function(){return _r(t)},function(e){return function(e,t,n){var r=qr.exec(t);if(null!==r)return parseInt(r[1],10);var o=Ur.exec(t);if(null!==o){var i=parseFloat(o[1]);return i/100*n.pixelWidth()}return _r(e)}(t,e,n)})},getPercentageWidth:function(t,n){return Kr(t).fold(function(){var e=_r(t);return Jr(e,n)},function(e){return function(e,t,n){var r=Ur.exec(t);if(null!==r)return parseFloat(r[1]);var o=_r(e);return Jr(o,n)}(t,e,n)})},getGenericWidth:function(e){return Kr(e).bind(function(e){var t=Hr.exec(e);return null!==t?R.some({width:C(parseFloat(t[1])),unit:C(t[3])}):R.none()})},setGenericWidth:function(e,t,n){De(e,"width",t+n)},getHeight:function(e){return n="rowspan",Yr(t=e)/Xr(t,n);var t,n},getRawWidth:Kr},Qr=function(n,r){$r.getGenericWidth(n).each(function(e){var t=e.width()/2;$r.setGenericWidth(n,t,e.unit()),$r.setGenericWidth(r,t,e.unit())})},Zr=function(n,r){return{left:C(n),top:C(r),translate:function(e,t){return Zr(n+e,r+t)}}},eo=Zr,to=function(e,t){return e!==undefined?e:t!==undefined?t:0},no=function(e){var t=e.dom().ownerDocument,n=t.body,r=t.defaultView,o=t.documentElement,i=to(r.pageYOffset,o.scrollTop),u=to(r.pageXOffset,o.scrollLeft),a=to(o.clientTop,n.clientTop),c=to(o.clientLeft,n.clientLeft);return ro(e).translate(u-c,i-a)},ro=function(e){var t,n=e.dom(),r=n.ownerDocument.body;return r===n?eo(r.offsetLeft,r.offsetTop):Ce(e)?(t=n.getBoundingClientRect(),eo(t.left,t.top)):eo(0,0)},oo=q("row","y"),io=q("col","x"),uo=function(e){return no(e).left()+Lr(e)},ao=function(e){return no(e).left()},co=function(e,t){return io(e,ao(t))},lo=function(e,t){return io(e,uo(t))},fo=function(e){return no(e).top()},so=function(e,t){return oo(e,fo(t))},mo=function(e,t){return oo(e,fo(t)+Wr(t))},go=function(n,t,r){if(0===r.length)return[];var e=E(r.slice(1),function(e,t){return e.map(function(e){return n(t,e)})}),o=r[r.length-1].map(function(e){return t(r.length-1,e)});return e.concat([o])},ho={height:{delta:o,positions:function(e){return go(so,mo,e)},edge:fo},rtl:{delta:function(e){return-e},edge:uo,positions:function(e){return go(lo,co,e)}},ltr:{delta:o,edge:ao,positions:function(e){return go(co,lo,e)}}},po={ltr:ho.ltr,rtl:ho.rtl};function vo(t){var n=function(e){return t(e).isRtl()?po.rtl:po.ltr};return{delta:function(e,t){return n(t).delta(e,t)},edge:function(e){return n(e).edge(e)},positions:function(e,t){return n(t).positions(e,t)}}}var bo=function(e){var t=ln(e);return mn.generate(t).grid()},wo=function(){return(wo=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e}).apply(this,arguments)},yo=function(e){for(var t=[],n=function(e){t.push(e)},r=0;r<e.length;r++)e[r].each(n);return t},xo=function(e,t){for(var n=0;n<e.length;n++){var r=t(e[n],n);if(r.isSome())return r}return R.none()},Co=function(e,t,n,r){n===r?he(e,t):se(e,t,n)},Ro=function(o,e){var i=[],u=[],t=function(e,t){0<e.length?function(e,t){var n=$t(o,t).getOrThunk(function(){var e=xe.fromTag(t,mt(o).dom());return Rt(o,e),e});Ot(n);var r=E(e,function(e){e.isNew()&&i.push(e.element());var t=e.element();return Ot(t),k(e.cells(),function(e){e.isNew()&&u.push(e.element()),Co(e.element(),"colspan",e.colspan(),1),Co(e.element(),"rowspan",e.rowspan(),1),Rt(t,e.element())}),t});Dt(n,r)}(e,t):$t(o,t).each(Nt)},n=[],r=[],a=[];return k(e,function(e){switch(e.section()){case"thead":n.push(e);break;case"tbody":r.push(e);break;case"tfoot":a.push(e)}}),t(n,"thead"),t(r,"tbody"),t(a,"tfoot"),{newRows:C(i),newCells:C(u)}},So=function(e){return E(e,function(e){var n=On(e.element());return k(e.cells(),function(e){var t=Nn(e.element());Co(t,"colspan",e.colspan(),1),Co(t,"rowspan",e.rowspan(),1),Rt(n,t)}),n})},To=function(e,t){var n=me(e,t);return n===undefined||""===n?[]:n.split(" ")},Do=function(e){return e.dom().classList!==undefined},Oo=function(e,t){return o=t,i=To(n=e,r="class").concat([o]),se(n,r,i.join(" ")),!0;var n,r,o,i},No=function(e,t){return o=t,0<(i=A(To(n=e,r="class"),function(e){return e!==o})).length?se(n,r,i.join(" ")):he(n,r),!1;var n,r,o,i},Eo=function(e,t){Do(e)?e.dom().classList.add(t):Oo(e,t)},ko=function(e){0===(Do(e)?e.dom().classList:To(e,"class")).length&&he(e,"class")},Ao=function(e,t){return Do(e)&&e.dom().classList.contains(t)},Po=function(e,t){for(var n=[],r=e;r<t;r++)n.push(r);return n},Io=function(t,n){if(n<0||n>=t.length-1)return R.none();var e=t[n].fold(function(){var e=F(t.slice(0,n));return xo(e,function(e,t){return e.map(function(e){return{value:e,delta:t+1}})})},function(e){return R.some({value:e,delta:0})}),r=t[n+1].fold(function(){var e=t.slice(n+1);return xo(e,function(e,t){return e.map(function(e){return{value:e,delta:t+1}})})},function(e){return R.some({value:e,delta:1})});return e.bind(function(n){return r.map(function(e){var t=e.delta+n.delta;return Math.abs(e.value-n.value)/t})})},Bo=function(e,t,n){var r=e();return B(r,t).orThunk(function(){return R.from(r[0]).orThunk(n)}).map(function(e){return e.element()})},Wo=function(n){var e=n.grid(),t=Po(0,e.columns()),r=Po(0,e.rows());return E(t,function(t){return Bo(function(){return _(r,function(e){return mn.getAt(n,e,t).filter(function(e){return e.column()===t}).fold(C([]),function(e){return[e]})})},function(e){return 1===e.colspan()},function(){return mn.getAt(n,0,t)})})},Mo=function(n){var e=n.grid(),t=Po(0,e.rows()),r=Po(0,e.columns());return E(t,function(t){return Bo(function(){return _(r,function(e){return mn.getAt(n,t,e).filter(function(e){return e.row()===t}).fold(C([]),function(e){return[e]})})},function(e){return 1===e.rowspan()},function(){return mn.getAt(n,t,0)})})},_o=function(e){var t=e.replace(/\./g,"-");return{resolve:function(e){return t+"-"+e}}},Lo={resolve:_o("ephox-snooker").resolve},Fo=function(e,t,n,r,o){var i=xe.fromTag("div");return Oe(i,{position:"absolute",left:t-r/2+"px",top:n+"px",height:o+"px",width:r+"px"}),de(i,{"data-column":e,role:"presentation"}),i},jo=function(e,t,n,r,o){var i=xe.fromTag("div");return Oe(i,{position:"absolute",left:t+"px",top:n-o/2+"px",height:o+"px",width:r+"px"}),de(i,{"data-row":e,role:"presentation"}),i},zo=Lo.resolve("resizer-bar"),Ho=Lo.resolve("resizer-rows"),Uo=Lo.resolve("resizer-cols"),qo=function(e){var t=qt(e.parent(),"."+zo);k(t,Nt)},Vo=function(n,e,r){var o=n.origin();k(e,function(e,t){e.each(function(e){var t=r(o,e);Eo(t,zo),Rt(n.parent(),t)})})},Go=function(e,t,n,r,o,i){var u,a,c,l,f=no(t),s=0<n.length?o.positions(n,t):[];u=e,a=s,c=f,l=Lr(t),Vo(u,a,function(e,t){var n=jo(t.row(),c.left()-e.left(),t.y()-e.top(),l,7);return Eo(n,Ho),n});var d,m,g,h,p=0<r.length?i.positions(r,t):[];d=e,m=p,g=f,h=Wr(t),Vo(d,m,function(e,t){var n=Fo(t.col(),t.x()-e.left(),g.top()-e.top(),7,h);return Eo(n,Uo),n})},Yo=function(e,t){var n=qt(e.parent(),"."+zo);k(n,t)},Xo=function(e,t,n,r){qo(e);var o=ln(t),i=mn.generate(o),u=Mo(i),a=Wo(i);Go(e,t,u,a,n,r)},Ko=function(e){Yo(e,function(e){De(e,"display","none")})},Jo=function(e){Yo(e,function(e){De(e,"display","block")})},$o=qo,Qo=function(e){return Ao(e,Ho)},Zo=function(e){return Ao(e,Uo)},ei=function(e,t){return Lt(t,e.section())},ti=function(e,t){return e.cells()[t]},ni={addCell:function(e,t,n){var r=e.cells(),o=r.slice(0,t),i=r.slice(t),u=o.concat([n]).concat(i);return ei(e,u)},setCells:ei,mutateCell:function(e,t,n){e.cells()[t]=n},getCell:ti,getCellElement:function(e,t){return ti(e,t).element()},mapCells:function(e,t){var n=e.cells(),r=E(n,t);return Lt(r,e.section())},cellLength:function(e){return e.cells().length}},ri=function(e,t){if(0===e.length)return 0;var n=e[0];return W(e,function(e){return!t(n.element(),e.element())}).fold(function(){return e.length},function(e){return e})},oi=function(e,t,n,r){var o,i,u,a,c=(o=e,i=t,o[i]).cells().slice(n),l=ri(c,r),f=(u=e,a=n,E(u,function(e){return ni.getCell(e,a)})).slice(t),s=ri(f,r);return{colspan:C(l),rowspan:C(s)}},ii=function(o,i){var u=E(o,function(e,t){return E(e.cells(),function(e,t){return!1})});return E(o,function(e,r){var t=_(e.cells(),function(e,t){if(!1===u[r][t]){var n=oi(o,r,t,i);return function(e,t,n,r){for(var o=e;o<e+n;o++)for(var i=t;i<t+r;i++)u[o][i]=!0}(r,t,n.rowspan(),n.colspan()),[It(e.element(),n.rowspan(),n.colspan(),e.isNew())]}return[]});return Ft(t,e.section())})},ui=function(e,t,n){for(var r=[],o=0;o<e.grid().rows();o++){for(var i=[],u=0;u<e.grid().columns();u++){var a=mn.getAt(e,o,u).map(function(e){return Mt(e.element(),n)}).getOrThunk(function(){return Mt(t.gap(),!0)});i.push(a)}var c=Lt(i,e.all()[o].section());r.push(c)}return r},ai=function(e,r){return E(e,function(e){var t,n=(t=e.details(),xo(t,function(e){return gt(e.element()).map(function(e){var t=gt(e).isNone();return Mt(e,t)})}).getOrThunk(function(){return Mt(r.row(),!0)}));return _t(n.element(),e.details(),e.section(),n.isNew())})},ci=function(e,t){var n=ii(e,ft);return ai(n,t)},li=function(e,t){var n=M(E(e.all(),function(e){return e.cells()}));return B(n,function(e){return ft(t,e.element())})},fi=function(a,c,l,f,s){return function(n,r,e,o,i){var t=ln(r),u=mn.generate(t);return c(u,e).map(function(e){var t=ui(u,o,!1),n=a(t,e,ft,s(o)),r=ci(n.grid(),o);return{grid:C(r),cursor:n.cursor}}).fold(function(){return R.none()},function(e){var t=Ro(r,e.grid());return l(r,e.grid(),i),f(r),Xo(n,r,ho.height,i),R.some({cursor:e.cursor,newRows:t.newRows,newCells:t.newCells})})}},si=function(t,e){return cn.cell(e.element()).bind(function(e){return li(t,e)})},di=function(t,e){var n=E(e.selection(),function(e){return cn.cell(e).bind(function(e){return li(t,e)})}),r=yo(n);return 0<r.length?R.some({cells:r,generators:e.generators,clipboard:e.clipboard}):R.none()},mi=function(t,e){var n=E(e.selection(),function(e){return cn.cell(e).bind(function(e){return li(t,e)})}),r=yo(n);return 0<r.length?R.some(r):R.none()},gi=function(n){return{is:function(e){return n===e},isValue:u,isError:f,getOr:C(n),getOrThunk:C(n),getOrDie:C(n),or:function(e){return gi(n)},orThunk:function(e){return gi(n)},fold:function(e,t){return t(n)},map:function(e){return gi(e(n))},mapError:function(e){return gi(n)},each:function(e){e(n)},bind:function(e){return e(n)},exists:function(e){return e(n)},forall:function(e){return e(n)},toOption:function(){return R.some(n)}}},hi=function(n){return{is:f,isValue:f,isError:u,getOr:o,getOrThunk:function(e){return e()},getOrDie:function(){return e=String(n),function(){throw new Error(e)}();var e},or:function(e){return e},orThunk:function(e){return e()},fold:function(e,t){return e(n)},map:function(e){return hi(n)},mapError:function(e){return hi(e(n))},each:y,bind:function(e){return hi(n)},exists:f,forall:u,toOption:R.none}},pi={value:gi,error:hi,fromOption:function(e,t){return e.fold(function(){return hi(t)},gi)}},vi=function(e,t){return E(e,function(){return Mt(t.cell(),!0)})},bi=function(t,e,n){return t.concat(function(e,t){for(var n=[],r=0;r<e;r++)n.push(t(r));return n}(e,function(e){return ni.setCells(t[t.length-1],vi(t[t.length-1].cells(),n))}))},wi=function(e,t,n){return E(e,function(e){return ni.setCells(e,e.cells().concat(vi(Po(0,t),n)))})},yi=function(e,t,n){if(e.row()>=t.length||e.column()>ni.cellLength(t[0]))return pi.error("invalid start address out of table bounds, row: "+e.row()+", column: "+e.column());var r=t.slice(e.row()),o=r[0].cells().slice(e.column()),i=ni.cellLength(n[0]),u=n.length;return pi.value({rowDelta:C(r.length-u),colDelta:C(o.length-i)})},xi=function(e,t){var n=ni.cellLength(e[0]),r=ni.cellLength(t[0]);return{rowDelta:C(0),colDelta:C(n-r)}},Ci=function(e,t,n){var r=t.colDelta()<0?wi:o;return(t.rowDelta()<0?bi:o)(r(e,Math.abs(t.colDelta()),n),Math.abs(t.rowDelta()),n)},Ri=function(e,t,n,r){if(0===e.length)return e;for(var o=t.startRow();o<=t.finishRow();o++)for(var i=t.startCol();i<=t.finishCol();i++)ni.mutateCell(e[o],i,Mt(r(),!1));return e},Si=function(e,t,n,r){for(var o=!0,i=0;i<e.length;i++)for(var u=0;u<ni.cellLength(e[0]);u++){var a=n(ni.getCellElement(e[i],u),t);!0===a&&!1===o?ni.mutateCell(e[i],u,Mt(r(),!0)):!0===a&&(o=!1)}return e},Ti=function(i,n,u,a){if(0<n&&n<i.length){var e=i[n-1].cells(),t=(r=u,I(e,function(e,t){return N(e,function(e){return r(e.element(),t.element())})?e:e.concat([t])},[]));k(t,function(r){for(var o=R.none(),e=function(n){for(var e=function(t){var e=i[n].cells()[t];u(e.element(),r.element())&&(o.isNone()&&(o=R.some(a())),o.each(function(e){ni.mutateCell(i[n],t,Mt(e,!0))}))},t=0;t<ni.cellLength(i[0]);t++)e(t)},t=n;t<i.length;t++)e(t)})}var r;return i},Di=function(n,r,o,i,u){return yi(n,r,o).map(function(e){var t=Ci(r,e,i);return function(e,t,n,r,o){for(var i,u,a,c,l,f=e.row(),s=e.column(),d=f+n.length,m=s+ni.cellLength(n[0]),g=f;g<d;g++)for(var h=s;h<m;h++){i=t,u=g,a=h,l=c=void 0,c=b(o,ni.getCell(i[u],a).element()),l=i[u],1<i.length&&1<ni.cellLength(l)&&(0<a&&c(ni.getCellElement(l,a-1))||a<l.cells().length-1&&c(ni.getCellElement(l,a+1))||0<u&&c(ni.getCellElement(i[u-1],a))||u<i.length-1&&c(ni.getCellElement(i[u+1],a)))&&Si(t,ni.getCellElement(t[g],h),o,r.cell);var p=ni.getCellElement(n[g-f],h-s),v=r.replace(p);ni.mutateCell(t[g],h,Mt(v,!0))}return t}(n,t,o,i,u)})},Oi=function(e,t,n,r,o){Ti(t,e,o,r.cell);var i=xi(n,t),u=Ci(n,i,r),a=xi(t,u),c=Ci(t,a,r);return c.slice(0,e).concat(u).concat(c.slice(e,c.length))},Ni=function(n,r,e,o,i){var t=n.slice(0,r),u=n.slice(r),a=ni.mapCells(n[e],function(e,t){return 0<r&&r<n.length&&o(ni.getCellElement(n[r-1],t),ni.getCellElement(n[r],t))?ni.getCell(n[r],t):Mt(i(e.element(),o),!0)});return t.concat([a]).concat(u)},Ei=function(e,n,r,o,i){return E(e,function(e){var t=0<n&&n<ni.cellLength(e)&&o(ni.getCellElement(e,n-1),ni.getCellElement(e,n))?ni.getCell(e,n):Mt(i(ni.getCellElement(e,r),o),!0);return ni.addCell(e,n,t)})},ki=function(e,r,o,i,u){var a=o+1;return E(e,function(e,t){var n=t===r?Mt(u(ni.getCellElement(e,o),i),!0):ni.getCell(e,o);return ni.addCell(e,a,n)})},Ai=function(e,t,n,r,o){var i=t+1,u=e.slice(0,i),a=e.slice(i),c=ni.mapCells(e[t],function(e,t){return t===n?Mt(o(e.element(),r),!0):e});return u.concat([c]).concat(a)},Pi=function(e,t,n){return e.slice(0,t).concat(e.slice(n+1))},Ii=function(e,n,r){var t=E(e,function(e){var t=e.cells().slice(0,n).concat(e.cells().slice(r+1));return Lt(t,e.section())});return A(t,function(e){return 0<e.cells().length})},Bi=function(e,n,r,o){return E(e,function(e){return ni.mapCells(e,function(e){return t=e,N(n,function(e){return r(t.element(),e.element())})?Mt(o(e.element(),r),!0):e;var t})})},Wi=function(e,t,n,r){return ni.getCellElement(e[t],n)!==undefined&&0<t&&r(ni.getCellElement(e[t-1],n),ni.getCellElement(e[t],n))},Mi=function(e,t,n){return 0<t&&n(ni.getCellElement(e,t-1),ni.getCellElement(e,t))},_i=function(n,r,o,e){var t=_(n,function(e,t){return Wi(n,t,r,o)||Mi(e,r,o)?[]:[ni.getCell(e,r)]});return Bi(n,t,o,e)},Li=function(n,r,o,e){var i=n[r],t=_(i.cells(),function(e,t){return Wi(n,r,t,o)||Mi(i,t,o)?[]:[e]});return Bi(n,t,o,e)},Fi=xr([{none:[]},{only:["index"]},{left:["index","next"]},{middle:["prev","index","next"]},{right:["prev","index"]}]),ji=wo({},Fi),zi=function(e,t,i,u){var n,r,a=e.slice(0),o=(r=t,0===(n=e).length?ji.none():1===n.length?ji.only(0):0===r?ji.left(0,1):r===n.length-1?ji.right(r-1,r):0<r&&r<n.length-1?ji.middle(r-1,r,r+1):ji.none()),c=function(e){return E(e,C(0))},l=C(c(a)),f=function(e,t){if(0<=i){var n=Math.max(u.minCellWidth(),a[t]-i);return c(a.slice(0,e)).concat([i,n-a[t]]).concat(c(a.slice(t+1)))}var r=Math.max(u.minCellWidth(),a[e]+i),o=a[e]-r;return c(a.slice(0,e)).concat([r-a[e],o]).concat(c(a.slice(t+1)))},s=f;return o.fold(l,function(e){return u.singleColumnWidth(a[e],i)},s,function(e,t,n){return f(t,n)},function(e,t){if(0<=i)return c(a.slice(0,t)).concat([i]);var n=Math.max(u.minCellWidth(),a[t]+i);return c(a.slice(0,t)).concat([n-a[t]])})},Hi=function(e,t){return ge(e,t)&&1<parseInt(me(e,t),10)},Ui={hasColspan:function(e){return Hi(e,"colspan")},hasRowspan:function(e){return Hi(e,"rowspan")},minWidth:C(10),minHeight:C(10),getInt:function(e,t){return parseInt(Ne(e,t),10)}},qi=function(e,t,n){return ke(e,t).fold(function(){return n(e)+"px"},function(e){return e})},Vi=function(e,t){return qi(e,"width",function(e){return $r.getPixelWidth(e,t)})},Gi=function(e){return qi(e,"height",$r.getHeight)},Yi=function(e,t,n,r,o){var i=Wo(e),u=E(i,function(e){return e.map(t.edge)});return E(i,function(e,t){return e.filter(g(Ui.hasColspan)).fold(function(){var e=Io(u,t);return r(e)},function(e){return n(e,o)})})},Xi=function(e){return e.map(function(e){return e+"px"}).getOr("")},Ki=function(e,t,n,r){var o=Mo(e),i=E(o,function(e){return e.map(t.edge)});return E(o,function(e,t){return e.filter(g(Ui.hasRowspan)).fold(function(){var e=Io(i,t);return r(e)},function(e){return n(e)})})},Ji={getRawWidths:function(e,t,n){return Yi(e,t,Vi,Xi,n)},getPixelWidths:function(e,t,n){return Yi(e,t,$r.getPixelWidth,function(e){return e.getOrThunk(n.minCellWidth)},n)},getPercentageWidths:function(e,t,n){return Yi(e,t,$r.getPercentageWidth,function(e){return e.fold(function(){return n.minCellWidth()},function(e){return e/n.pixelWidth()*100})},n)},getPixelHeights:function(e,t){return Ki(e,t,$r.getHeight,function(e){return e.getOrThunk(Ui.minHeight)})},getRawHeights:function(e,t){return Ki(e,t,Gi,Xi)}},$i=function(e,t,n){for(var r=0,o=e;o<t;o++)r+=n[o]!==undefined?n[o]:0;return r},Qi=function(e,n){var t=mn.justCells(e);return E(t,function(e){var t=$i(e.column(),e.column()+e.colspan(),n);return{element:e.element,width:C(t),colspan:e.colspan}})},Zi=function(e,n){var t=mn.justCells(e);return E(t,function(e){var t=$i(e.row(),e.row()+e.rowspan(),n);return{element:e.element,height:C(t),rowspan:e.rowspan}})},eu=function(e,n){return E(e.all(),function(e,t){return{element:e.element,height:C(n[t])}})},tu=function(e){var t=o;return{width:C(e),pixelWidth:C(e),getWidths:Ji.getPixelWidths,getCellDelta:t,singleColumnWidth:function(e,t){return[Math.max(Ui.minWidth(),e+t)-e]},minCellWidth:Ui.minWidth,setElementWidth:$r.setPixelWidth,setTableWidth:function(e,t,n){var r=P(t,function(e,t){return e+t},0);$r.setPixelWidth(e,r)}}},nu=function(e,t){var n,r,o,i,u=$r.percentageBasedSizeRegex().exec(t);if(null!==u)return n=u[1],r=e,o=parseFloat(n),i=_r(r),{width:C(o),pixelWidth:C(i),getWidths:Ji.getPercentageWidths,getCellDelta:function(e){return e/i*100},singleColumnWidth:function(e,t){return[100-e]},minCellWidth:function(){return Ui.minWidth()/i*100},setElementWidth:$r.setPercentageWidth,setTableWidth:function(e,t,n){var r=n/100*o;$r.setPercentageWidth(e,o+r)}};var a=$r.pixelBasedSizeRegex().exec(t);if(null!==a){var c=parseInt(a[1],10);return tu(c)}var l=_r(e);return tu(l)},ru=function(t){return $r.getRawWidth(t).fold(function(){var e=_r(t);return tu(e)},function(e){return nu(t,e)})},ou=function(e){return mn.generate(e)},iu=function(e){var t=ln(e);return ou(t)},uu=function(e,t,n,r){var o=ru(e),i=o.getCellDelta(t),u=iu(e),a=o.getWidths(u,r,o),c=zi(a,n,i,o),l=E(c,function(e,t){return e+a[t]}),f=Qi(u,l);k(f,function(e){o.setElementWidth(e.element(),e.width())}),n===u.grid().columns()-1&&o.setTableWidth(e,l,i)},au=function(e,n,r,t){var o=iu(e),i=Ji.getPixelHeights(o,t),u=E(i,function(e,t){return r===t?Math.max(n+e,Ui.minHeight()):e}),a=Zi(o,u),c=eu(o,u);k(c,function(e){$r.setHeight(e.element(),e.height())}),k(a,function(e){$r.setHeight(e.element(),e.height())});var l=P(u,function(e,t){return e+t},0);$r.setHeight(e,l)},cu=function(e,t,n){var r=ru(e),o=ou(t),i=r.getWidths(o,n,r),u=Qi(o,i);k(u,function(e){r.setElementWidth(e.element(),e.width())}),0<u.length&&r.setTableWidth(e,i,r.getCellDelta(0))},lu=function(e){var t=e,n=function(){return t};return{get:n,set:function(e){t=e},clone:function(){return lu(n())}}},fu=function(r,o,i){if(0===o.length)throw new Error("You must specify at least one required field.");return X("required",o),K(o),function(t){var n=j(t);L(o,function(e){return O(n,e)})||G(o,n),r(o,n);var e=A(o,function(e){return!i.validate(t[e],e)});return 0<e.length&&function(e,t){throw new Error("All values need to be of type: "+t+". Keys ("+V(e).join(", ")+") were not.")}(e,i.label),t}},su=function(t,e){var n=A(e,function(e){return!O(t,e)});0<n.length&&Y(n)},du=function(e){return fu(su,e,{validate:v,label:"function"})},mu=du(["cell","row","replace","gap"]),gu=function(e){var t=ge(e,"colspan")?parseInt(me(e,"colspan"),10):1,n=ge(e,"rowspan")?parseInt(me(e,"rowspan"),10):1;return{element:C(e),colspan:C(t),rowspan:C(n)}},hu=function(r,o){void 0===o&&(o=gu),mu(r);var n=lu(R.none()),i=function(e){var t,n=o(e);return t=n,r.cell(t)},u=function(e){var t=i(e);return n.get().isNone()&&n.set(R.some(t)),a=R.some({item:e,replacement:t}),t},a=R.none();return{getOrInit:function(t,n){return a.fold(function(){return u(t)},function(e){return n(t,e.item)?e.replacement:u(t)})},cursor:n.get}},pu=function(a,c){return function(r){var o=lu(R.none());mu(r);var i=[],u=function(e){var t={scope:a},n=r.replace(e,c,t);return i.push({item:e,sub:n}),o.get().isNone()&&o.set(R.some(n)),n};return{replaceOrInit:function(t,n){return(r=t,o=n,B(i,function(e){return o(e.item,r)})).fold(function(){return u(t)},function(e){return n(t,e.item)?e.sub:u(t)});var r,o},cursor:o.get}}},vu=function(n){mu(n);var e=lu(R.none());return{combine:function(t){return e.get().isNone()&&e.set(R.some(t)),function(){var e=n.cell({element:C(t),colspan:C(1),rowspan:C(1)});return Ae(e,"width"),Ae(t,"width"),e}},cursor:e.get}},bu=["body","p","div","article","aside","figcaption","figure","footer","header","nav","section","ol","ul","table","thead","tfoot","tbody","caption","tr","td","th","h1","h2","h3","h4","h5","h6","blockquote","pre","address"],wu=function(e,t){var n=e.property().name(t);return O(bu,n)},yu=function(e,t){return O(["br","img","hr","input"],e.property().name(t))},xu=wu,Cu=function(e,t){var n=e.property().name(t);return O(["ol","ul"],n)},Ru=yu,Su=$n(),Tu=function(e){return xu(Su,e)},Du=function(e){return Cu(Su,e)},Ou=function(e){return Ru(Su,e)},Nu=function(e){var t,i=function(e){return"br"===oe(e)},n=function(o){return Sn(o).bind(function(n){var r=vt(n).map(function(e){return!!Tu(e)||!!Ou(e)&&"img"!==oe(e)}).getOr(!1);return gt(n).map(function(e){return!0===r||"li"===oe(t=e)||Kt(t,Du).isSome()||i(n)||Tu(e)&&!ft(o,e)?[]:[xe.fromTag("br")];var t})}).getOr([])},r=0===(t=_(e,function(e){var t=bt(e);return L(t,function(e){return i(e)||le(e)&&0===vn(e).trim().length})?[]:t.concat(n(e))})).length?[xe.fromTag("br")]:t;Ot(e[0]),Dt(e[0],r)},Eu=function(e){0===cn.cells(e).length&&Nt(e)},ku=q("grid","cursor"),Au=function(e,t,n){return Pu(e,t,n).orThunk(function(){return Pu(e,0,0)})},Pu=function(e,t,n){return R.from(e[t]).bind(function(e){return R.from(e.cells()[n]).bind(function(e){return R.from(e.element())})})},Iu=function(e,t,n){return ku(e,Pu(e,t,n))},Bu=function(e){return I(e,function(e,t){return N(e,function(e){return e.row()===t.row()})?e:e.concat([t])},[]).sort(function(e,t){return e.row()-t.row()})},Wu=function(e){return I(e,function(e,t){return N(e,function(e){return e.column()===t.column()})?e:e.concat([t])},[]).sort(function(e,t){return e.column()-t.column()})},Mu=function(e,t,n){var r=fn(e,n),o=mn.generate(r);return ui(o,t,!0)},_u=cu,Lu={insertRowBefore:fi(function(e,t,n,r){var o=t.row(),i=t.row(),u=Ni(e,i,o,n,r.getOrInit);return Iu(u,i,t.column())},si,y,y,hu),insertRowsBefore:fi(function(e,t,n,r){var o=t[0].row(),i=t[0].row(),u=Bu(t),a=I(u,function(e,t){return Ni(e,i,o,n,r.getOrInit)},e);return Iu(a,i,t[0].column())},mi,y,y,hu),insertRowAfter:fi(function(e,t,n,r){var o=t.row(),i=t.row()+t.rowspan(),u=Ni(e,i,o,n,r.getOrInit);return Iu(u,i,t.column())},si,y,y,hu),insertRowsAfter:fi(function(e,t,n,r){var o=Bu(t),i=o[o.length-1].row(),u=o[o.length-1].row()+o[o.length-1].rowspan(),a=I(o,function(e,t){return Ni(e,u,i,n,r.getOrInit)},e);return Iu(a,u,t[0].column())},mi,y,y,hu),insertColumnBefore:fi(function(e,t,n,r){var o=t.column(),i=t.column(),u=Ei(e,i,o,n,r.getOrInit);return Iu(u,t.row(),i)},si,_u,y,hu),insertColumnsBefore:fi(function(e,t,n,r){var o=Wu(t),i=o[0].column(),u=o[0].column(),a=I(o,function(e,t){return Ei(e,u,i,n,r.getOrInit)},e);return Iu(a,t[0].row(),u)},mi,_u,y,hu),insertColumnAfter:fi(function(e,t,n,r){var o=t.column(),i=t.column()+t.colspan(),u=Ei(e,i,o,n,r.getOrInit);return Iu(u,t.row(),i)},si,_u,y,hu),insertColumnsAfter:fi(function(e,t,n,r){var o=t[t.length-1].column(),i=t[t.length-1].column()+t[t.length-1].colspan(),u=Wu(t),a=I(u,function(e,t){return Ei(e,i,o,n,r.getOrInit)},e);return Iu(a,t[0].row(),i)},mi,_u,y,hu),splitCellIntoColumns:fi(function(e,t,n,r){var o=ki(e,t.row(),t.column(),n,r.getOrInit);return Iu(o,t.row(),t.column())},si,_u,y,hu),splitCellIntoRows:fi(function(e,t,n,r){var o=Ai(e,t.row(),t.column(),n,r.getOrInit);return Iu(o,t.row(),t.column())},si,y,y,hu),eraseColumns:fi(function(e,t,n,r){var o=Wu(t),i=Ii(e,o[0].column(),o[o.length-1].column()),u=Au(i,t[0].row(),t[0].column());return ku(i,u)},mi,_u,Eu,hu),eraseRows:fi(function(e,t,n,r){var o=Bu(t),i=Pi(e,o[0].row(),o[o.length-1].row()),u=Au(i,t[0].row(),t[0].column());return ku(i,u)},mi,y,Eu,hu),makeColumnHeader:fi(function(e,t,n,r){var o=_i(e,t.column(),n,r.replaceOrInit);return Iu(o,t.row(),t.column())},si,y,y,pu("row","th")),unmakeColumnHeader:fi(function(e,t,n,r){var o=_i(e,t.column(),n,r.replaceOrInit);return Iu(o,t.row(),t.column())},si,y,y,pu(null,"td")),makeRowHeader:fi(function(e,t,n,r){var o=Li(e,t.row(),n,r.replaceOrInit);return Iu(o,t.row(),t.column())},si,y,y,pu("col","th")),unmakeRowHeader:fi(function(e,t,n,r){var o=Li(e,t.row(),n,r.replaceOrInit);return Iu(o,t.row(),t.column())},si,y,y,pu(null,"td")),mergeCells:fi(function(e,t,n,r){var o=t.cells();Nu(o);var i=Ri(e,t.bounds(),n,C(o[0]));return ku(i,R.from(o[0]))},function(e,t){return t.mergable()},y,y,vu),unmergeCells:fi(function(e,t,n,r){var o=P(t,function(e,t){return Si(e,t,n,r.combine(t))},e);return ku(o,R.from(t[0]))},function(e,t){return t.unmergable()},_u,y,vu),pasteCells:fi(function(e,n,t,r){var o,i,u,a,c=(o=n.clipboard(),i=n.generators(),u=ln(o),a=mn.generate(u),ui(a,i,!0)),l=At(n.row(),n.column());return Di(l,e,c,n.generators(),t).fold(function(){return ku(e,R.some(n.element()))},function(e){var t=Au(e,n.row(),n.column());return ku(e,t)})},function(t,n){return cn.cell(n.element()).bind(function(e){return li(t,e).map(function(e){return wo(wo({},e),{generators:n.generators,clipboard:n.clipboard})})})},_u,y,hu),pasteRowsBefore:fi(function(e,t,n,r){var o=e[t.cells[0].row()],i=t.cells[0].row(),u=Mu(t.clipboard(),t.generators(),o),a=Oi(i,e,u,t.generators(),n),c=Au(a,t.cells[0].row(),t.cells[0].column());return ku(a,c)},di,y,y,hu),pasteRowsAfter:fi(function(e,t,n,r){var o=e[t.cells[0].row()],i=t.cells[t.cells.length-1].row()+t.cells[t.cells.length-1].rowspan(),u=Mu(t.clipboard(),t.generators(),o),a=Oi(i,e,u,t.generators(),n),c=Au(a,t.cells[0].row(),t.cells[0].column());return ku(a,c)},di,y,y,hu)},Fu=function(e){return xe.fromDom(e.getBody())},ju=function(e){return e.getBoundingClientRect().width},zu=function(e){return e.getBoundingClientRect().height},Hu=function(t){return function(e){return ft(e,Fu(t))}},Uu=function(e){return/^[0-9]+$/.test(e)&&(e+="px"),e},qu=function(e){var t=qt(e,"td[data-mce-style],th[data-mce-style]");he(e,"data-mce-style"),k(t,function(e){he(e,"data-mce-style")})},Vu={isRtl:C(!1)},Gu={isRtl:C(!0)},Yu={directionAt:function(e){return"rtl"==("rtl"===Ne(e,"direction")?"rtl":"ltr")?Gu:Vu}},Xu=["tableprops","tabledelete","|","tableinsertrowbefore","tableinsertrowafter","tabledeleterow","|","tableinsertcolbefore","tableinsertcolafter","tabledeletecol"],Ku={"border-collapse":"collapse",width:"100%"},Ju={border:"1"},$u=function(e){return e.getParam("table_cell_advtab",!0,"boolean")},Qu=function(e){return e.getParam("table_row_advtab",!0,"boolean")},Zu=function(e){return e.getParam("table_advtab",!0,"boolean")},ea=function(e){return e.getParam("table_style_by_css",!1,"boolean")},ta=function(e){return e.getParam("table_cell_class_list",[],"array")},na=function(e){return e.getParam("table_row_class_list",[],"array")},ra=function(e){return e.getParam("table_class_list",[],"array")},oa=function(e){return!1===e.getParam("table_responsive_width")},ia=function(e,t){return e.fire("newrow",{node:t})},ua=function(e,t){return e.fire("newcell",{node:t})},aa=function(e,t,n,r){e.fire("ObjectResizeStart",{target:t,width:n,height:r})},ca=function(e,t,n,r){e.fire("ObjectResized",{target:t,width:n,height:r})},la=function(f,e){var t,n=function(e){return"table"===oe(Fu(e))},s=(t=f.getParam("table_clone_elements"),d(t)?R.some(t.split(/[ ,]/)):Array.isArray(t)?R.some(t):R.none()),r=function(u,a,c,l){return function(e,t){qu(e);var n=l(),r=xe.fromDom(f.getDoc()),o=vo(Yu.directionAt),i=Bn(c,r,s);return a(e)?u(n,e,t,i,o).bind(function(e){return k(e.newRows(),function(e){ia(f,e.dom())}),k(e.newCells(),function(e){ua(f,e.dom())}),e.cursor().map(function(e){var t=f.dom.createRng();return t.setStart(e.dom(),0),t.setEnd(e.dom(),0),t})}):R.none()}};return{deleteRow:r(Lu.eraseRows,function(e){var t=bo(e);return!1===n(f)||1<t.rows()},y,e),deleteColumn:r(Lu.eraseColumns,function(e){var t=bo(e);return!1===n(f)||1<t.columns()},y,e),insertRowsBefore:r(Lu.insertRowsBefore,u,y,e),insertRowsAfter:r(Lu.insertRowsAfter,u,y,e),insertColumnsBefore:r(Lu.insertColumnsBefore,u,Qr,e),insertColumnsAfter:r(Lu.insertColumnsAfter,u,Qr,e),mergeCells:r(Lu.mergeCells,u,y,e),unmergeCells:r(Lu.unmergeCells,u,y,e),pasteRowsBefore:r(Lu.pasteRowsBefore,u,y,e),pasteRowsAfter:r(Lu.pasteRowsAfter,u,y,e),pasteCells:r(Lu.pasteCells,u,y,e)}},fa=function(e,t,r){var n=ln(e),o=mn.generate(n);return mi(o,t).map(function(e){var t=ui(o,r,!1).slice(e[0].row(),e[e.length-1].row()+e[e.length-1].rowspan()),n=ci(t,r);return So(n)})},sa=tinymce.util.Tools.resolve("tinymce.util.Tools"),da=function(e,t,n){n&&e.formatter.apply("align"+n,{},t)},ma=function(e,t,n){n&&e.formatter.apply("valign"+n,{},t)},ga=function(t,n){sa.each("left center right".split(" "),function(e){t.formatter.remove("align"+e,{},n)})},ha=function(t,n){sa.each("top middle bottom".split(" "),function(e){t.formatter.remove("valign"+e,{},n)})},pa=function(o,e,i){var t;return t=function(e,t){for(var n=0;n<t.length;n++){var r=o.getStyle(t[n],i);if(void 0===e&&(e=r),e!==r)return""}return e}(t,o.select("td,th",e))},va=function(e,t){var n=e.dom,r=t.control.rootControl,o=r.toJSON(),i=n.parseStyle(o.style);i["border-style"]=o.borderStyle,i["border-color"]=o.borderColor,i["background-color"]=o.backgroundColor,i.width=o.width?Uu(o.width):"",i.height=o.height?Uu(o.height):"",r.find("#style").value(n.serializeStyle(n.parseStyle(n.serializeStyle(i))))},ba=function(e,t){var n=e.dom,r=t.control.rootControl,o=r.toJSON(),i=n.parseStyle(o.style);r.find("#borderStyle").value(i["border-style"]||""),r.find("#borderColor").value(i["border-color"]||""),r.find("#backgroundColor").value(i["background-color"]||""),r.find("#width").value(i.width||""),r.find("#height").value(i.height||"")},wa={createStyleForm:function(n){var e=function(){var e=n.getParam("color_picker_callback");if(e)return function(t){return e.call(n,function(e){t.control.value(e).fire("change")},t.control.value())}};return{title:"Advanced",type:"form",defaults:{onchange:b(va,n)},items:[{label:"Style",name:"style",type:"textbox",onchange:b(ba,n)},{type:"form",padding:0,formItemDefaults:{layout:"grid",alignH:["start","right"]},defaults:{size:7},items:[{label:"Border style",type:"listbox",name:"borderStyle",width:90,onselect:b(va,n),values:[{text:"Select...",value:""},{text:"Solid",value:"solid"},{text:"Dotted",value:"dotted"},{text:"Dashed",value:"dashed"},{text:"Double",value:"double"},{text:"Groove",value:"groove"},{text:"Ridge",value:"ridge"},{text:"Inset",value:"inset"},{text:"Outset",value:"outset"},{text:"None",value:"none"},{text:"Hidden",value:"hidden"}]},{label:"Border color",type:"colorbox",name:"borderColor",onaction:e()},{label:"Background color",type:"colorbox",name:"backgroundColor",onaction:e()}]}]}},buildListItems:function(e,r,t){var o=function(e,n){return n=n||[],sa.each(e,function(e){var t={text:e.text||e.title};e.menu?t.menu=o(e.menu):(t.value=e.value,r&&r(t)),n.push(t)}),n};return o(e,t||[])},updateStyleField:va,extractAdvancedStyles:function(e,t){var n=e.parseStyle(e.getAttrib(t,"style")),r={};return n["border-style"]&&(r.borderStyle=n["border-style"]),n["border-color"]&&(r.borderColor=n["border-color"]),n["background-color"]&&(r.backgroundColor=n["background-color"]),r.style=e.serializeStyle(n),r},updateAdvancedFields:ba,syncAdvancedStyleFields:function(e,t){t.control.rootControl.find("#style")[0].getEl().isEqualNode(m.document.activeElement)?ba(e,t):va(e,t)}},ya=function(r,o,e){var i,u=r.dom;function a(e,t,n){(1===o.length||n)&&u.setAttrib(e,t,n)}function c(e,t,n){(1===o.length||n)&&u.setStyle(e,t,n)}$u(r)&&wa.syncAdvancedStyleFields(r,e),i=e.control.rootControl.toJSON(),r.undoManager.transact(function(){sa.each(o,function(e){var t,n;a(e,"scope",i.scope),1===o.length?a(e,"style",i.style):(t=e,n=i.style,delete t.dataset.mceStyle,t.style.cssText+=";"+n),a(e,"class",i["class"]),c(e,"width",Uu(i.width)),c(e,"height",Uu(i.height)),i.type&&e.nodeName.toLowerCase()!==i.type&&(e=u.rename(e,i.type)),1===o.length&&(ga(r,e),ha(r,e)),i.align&&da(r,e,i.align),i.valign&&ma(r,e,i.valign)}),r.focus()})},xa=function(t){var e,n,r,o=[];if(o=t.dom.select("td[data-mce-selected],th[data-mce-selected]"),e=t.dom.getParent(t.selection.getStart(),"td,th"),!o.length&&e&&o.push(e),e=e||o[0]){var i,u,a,c;1<o.length?n={width:"",height:"",scope:"","class":"",align:"",valign:"",style:"",type:e.nodeName.toLowerCase()}:(u=e,a=(i=t).dom,c={width:a.getStyle(u,"width")||a.getAttrib(u,"width"),height:a.getStyle(u,"height")||a.getAttrib(u,"height"),scope:a.getAttrib(u,"scope"),"class":a.getAttrib(u,"class"),type:u.nodeName.toLowerCase(),style:"",align:"",valign:""},sa.each("left center right".split(" "),function(e){i.formatter.matchNode(u,"align"+e)&&(c.align=e)}),sa.each("top middle bottom".split(" "),function(e){i.formatter.matchNode(u,"valign"+e)&&(c.valign=e)}),$u(i)&&sa.extend(c,wa.extractAdvancedStyles(a,u)),n=c),0<ta(t).length&&(r={name:"class",type:"listbox",label:"Class",values:wa.buildListItems(ta(t),function(e){e.value&&(e.textStyle=function(){return t.formatter.getCssText({block:"td",classes:[e.value]})})})});var l={type:"form",layout:"flex",direction:"column",labelGapCalc:"children",padding:0,items:[{type:"form",layout:"grid",columns:2,labelGapCalc:!1,padding:0,defaults:{type:"textbox",maxWidth:50},items:[{label:"Width",name:"width",onchange:b(wa.updateStyleField,t)},{label:"Height",name:"height",onchange:b(wa.updateStyleField,t)},{label:"Cell type",name:"type",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"Cell",value:"td"},{text:"Header cell",value:"th"}]},{label:"Scope",name:"scope",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Row",value:"row"},{text:"Column",value:"col"},{text:"Row group",value:"rowgroup"},{text:"Column group",value:"colgroup"}]},{label:"H Align",name:"align",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},{label:"V Align",name:"valign",type:"listbox",text:"None",minWidth:90,maxWidth:null,values:[{text:"None",value:""},{text:"Top",value:"top"},{text:"Middle",value:"middle"},{text:"Bottom",value:"bottom"}]}]},r]};$u(t)?t.windowManager.open({title:"Cell properties",bodyType:"tabpanel",data:n,body:[{title:"General",type:"form",items:l},wa.createStyleForm(t)],onsubmit:b(ya,t,o)}):t.windowManager.open({title:"Cell properties",data:n,body:l,onsubmit:b(ya,t,o)})}};function Ca(f,s,d,e){var m=f.dom;function g(e,t,n){(1===s.length||n)&&m.setAttrib(e,t,n)}Qu(f)&&wa.syncAdvancedStyleFields(f,e);var h=e.control.rootControl.toJSON();f.undoManager.transact(function(){sa.each(s,function(e){var t,n,r,o,i,u,a,c,l;g(e,"scope",h.scope),g(e,"style",h.style),g(e,"class",h["class"]),t=e,n="height",r=Uu(h.height),(1===s.length||r)&&m.setStyle(t,n,r),h.type!==e.parentNode.nodeName.toLowerCase()&&(o=f.dom,i=e,u=h.type,a=o.getParent(i,"table"),c=i.parentNode,(l=o.select(u,a)[0])||(l=o.create(u),a.firstChild?"CAPTION"===a.firstChild.nodeName?o.insertAfter(l,a.firstChild):a.insertBefore(l,a.firstChild):a.appendChild(l)),l.appendChild(i),c.hasChildNodes()||o.remove(c)),h.align!==d.align&&(ga(f,e),da(f,e,h.align))}),f.focus()})}var Ra=function(t){var e,n,r,o,i,u,a,c,l,f,s=t.dom,d=[];e=s.getParent(t.selection.getStart(),"table"),n=s.getParent(t.selection.getStart(),"td,th"),sa.each(e.rows,function(t){sa.each(t.cells,function(e){if(s.getAttrib(e,"data-mce-selected")||e===n)return d.push(t),!1})}),(r=d[0])&&(1<d.length?i={height:"",scope:"",style:"","class":"",align:"",type:r.parentNode.nodeName.toLowerCase()}:(c=r,l=(a=t).dom,f={height:l.getStyle(c,"height")||l.getAttrib(c,"height"),scope:l.getAttrib(c,"scope"),"class":l.getAttrib(c,"class"),align:"",style:"",type:c.parentNode.nodeName.toLowerCase()},sa.each("left center right".split(" "),function(e){a.formatter.matchNode(c,"align"+e)&&(f.align=e)}),Qu(a)&&sa.extend(f,wa.extractAdvancedStyles(l,c)),i=f),0<na(t).length&&(o={name:"class",type:"listbox",label:"Class",values:wa.buildListItems(na(t),function(e){e.value&&(e.textStyle=function(){return t.formatter.getCssText({block:"tr",classes:[e.value]})})})}),u={type:"form",columns:2,padding:0,defaults:{type:"textbox"},items:[{type:"listbox",name:"type",label:"Row type",text:"Header",maxWidth:null,values:[{text:"Header",value:"thead"},{text:"Body",value:"tbody"},{text:"Footer",value:"tfoot"}]},{type:"listbox",name:"align",label:"Alignment",text:"None",maxWidth:null,values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},{label:"Height",name:"height"},o]},Qu(t)?t.windowManager.open({title:"Row properties",data:i,bodyType:"tabpanel",body:[{title:"General",type:"form",items:u},wa.createStyleForm(t)],onsubmit:b(Ca,t,d,i)}):t.windowManager.open({title:"Row properties",data:i,body:u,onsubmit:b(Ca,t,d,i)}))},Sa=tinymce.util.Tools.resolve("tinymce.Env"),Ta={styles:{"border-collapse":"collapse",width:"100%"},attributes:{border:"1"},percentages:!0},Da=function(e,t,n,r,o){void 0===o&&(o=Ta);var i=xe.fromTag("table");Oe(i,o.styles),de(i,o.attributes);var u=xe.fromTag("tbody");Rt(i,u);for(var a=[],c=0;c<e;c++){for(var l=xe.fromTag("tr"),f=0;f<t;f++){var s=c<n||f<r?xe.fromTag("th"):xe.fromTag("td");f<r&&se(s,"scope","row"),c<n&&se(s,"scope","col"),Rt(s,xe.fromTag("br")),o.percentages&&De(s,"width",100/t+"%"),Rt(l,s)}a.push(l)}return Dt(u,a),i},Oa=function(e,t){e.selection.select(t.dom(),!0),e.selection.collapse(!0)},Na=function(r,e,t){var n,o,i=r.getParam("table_default_styles",Ku,"object"),u={styles:i,attributes:(o=r,o.getParam("table_default_attributes",Ju,"object")),percentages:(n=i.width,d(n)&&-1!==n.indexOf("%")&&!oa(r))},a=Da(t,e,0,0,u);se(a,"data-mce-id","__mce");var c,l,f,s=(c=a,l=xe.fromTag("div"),f=xe.fromDom(c.dom().cloneNode(!0)),Rt(l,f),l.dom().innerHTML);return r.insertContent(s),Qt(Fu(r),'table[data-mce-id="__mce"]').map(function(e){var t,n;return oa(r)&&De(e,"width",Ne(e,"width")),he(e,"data-mce-id"),t=r,k(qt(e,"tr"),function(e){ia(t,e.dom()),k(qt(e,"th,td"),function(e){ua(t,e.dom())})}),n=r,Qt(e,"td,th").each(b(Oa,n)),e.dom()}).getOr(null)};function Ea(e,t,n,r){if("TD"===t.tagName||"TH"===t.tagName)e.setStyle(t,n,r);else if(t.children)for(var o=0;o<t.children.length;o++)Ea(e,t.children[o],n,r)}var ka,Aa=function(e,t,n){var r,o,i=e.dom;Zu(e)&&wa.syncAdvancedStyleFields(e,n),!1===(o=n.control.rootControl.toJSON())["class"]&&delete o["class"],e.undoManager.transact(function(){t||(t=Na(e,o.cols||1,o.rows||1)),function(e,t,n){var r,o=e.dom,i={},u={};if(i["class"]=n["class"],u.height=Uu(n.height),o.getAttrib(t,"width")&&!ea(e)?i.width=(r=n.width)?r.replace(/px$/,""):"":u.width=Uu(n.width),ea(e)?(u["border-width"]=Uu(n.border),u["border-spacing"]=Uu(n.cellspacing),sa.extend(i,{"data-mce-border-color":n.borderColor,"data-mce-cell-padding":n.cellpadding,"data-mce-border":n.border})):sa.extend(i,{border:n.border,cellpadding:n.cellpadding,cellspacing:n.cellspacing}),ea(e)&&t.children)for(var a=0;a<t.children.length;a++)Ea(o,t.children[a],{"border-width":Uu(n.border),"border-color":n.borderColor,padding:Uu(n.cellpadding)});n.style?sa.extend(u,o.parseStyle(n.style)):u=sa.extend({},o.parseStyle(o.getAttrib(t,"style")),u),i.style=o.serializeStyle(u),o.setAttribs(t,i)}(e,t,o),(r=i.select("caption",t)[0])&&!o.caption&&i.remove(r),!r&&o.caption&&((r=i.create("caption")).innerHTML=Sa.ie?"\xa0":'<br data-mce-bogus="1"/>',t.insertBefore(r,t.firstChild)),ga(e,t),o.align&&da(e,t,o.align),e.focus(),e.addVisual()})},Pa=function(t,e){var n,r,o,i,u,a,c,l,f,s,d=t.dom,m={};!0===e?(n=d.getParent(t.selection.getStart(),"table"))&&(c=n,l=(a=t).dom,f={width:l.getStyle(c,"width")||l.getAttrib(c,"width"),height:l.getStyle(c,"height")||l.getAttrib(c,"height"),cellspacing:l.getStyle(c,"border-spacing")||l.getAttrib(c,"cellspacing"),cellpadding:l.getAttrib(c,"data-mce-cell-padding")||l.getAttrib(c,"cellpadding")||pa(a.dom,c,"padding"),border:l.getAttrib(c,"data-mce-border")||l.getAttrib(c,"border")||pa(a.dom,c,"border"),borderColor:l.getAttrib(c,"data-mce-border-color"),caption:!!l.select("caption",c)[0],"class":l.getAttrib(c,"class")},sa.each("left center right".split(" "),function(e){a.formatter.matchNode(c,"align"+e)&&(f.align=e)}),Zu(a)&&sa.extend(f,wa.extractAdvancedStyles(l,c)),m=f):(r={label:"Cols",name:"cols"},o={label:"Rows",name:"rows"}),0<ra(t).length&&(m["class"]&&(m["class"]=m["class"].replace(/\s*mce\-item\-table\s*/g,"")),i={name:"class",type:"listbox",label:"Class",values:wa.buildListItems(ra(t),function(e){e.value&&(e.textStyle=function(){return t.formatter.getCssText({block:"table",classes:[e.value]})})})}),u={type:"form",layout:"flex",direction:"column",labelGapCalc:"children",padding:0,items:[{type:"form",labelGapCalc:!1,padding:0,layout:"grid",columns:2,defaults:{type:"textbox",maxWidth:50},items:(s=t,s.getParam("table_appearance_options",!0,"boolean")?[r,o,{label:"Width",name:"width",onchange:b(wa.updateStyleField,t)},{label:"Height",name:"height",onchange:b(wa.updateStyleField,t)},{label:"Cell spacing",name:"cellspacing"},{label:"Cell padding",name:"cellpadding"},{label:"Border",name:"border"},{label:"Caption",name:"caption",type:"checkbox"}]:[r,o,{label:"Width",name:"width",onchange:b(wa.updateStyleField,t)},{label:"Height",name:"height",onchange:b(wa.updateStyleField,t)}])},{label:"Alignment",name:"align",type:"listbox",text:"None",values:[{text:"None",value:""},{text:"Left",value:"left"},{text:"Center",value:"center"},{text:"Right",value:"right"}]},i]},Zu(t)?t.windowManager.open({title:"Table properties",data:m,bodyType:"tabpanel",body:[{title:"General",type:"form",items:u},wa.createStyleForm(t)],onsubmit:b(Aa,t,n)}):t.windowManager.open({title:"Table properties",data:m,body:u,onsubmit:b(Aa,t,n)})},Ia=sa.each,Ba=function(a,t,c,l,n){var r=Hu(a),e=function(e){return function(){return R.from(a.dom.getParent(a.selection.getStart(),e)).map(xe.fromDom)}},o=e("caption"),f=e("th,td"),s=function(e){return cn.table(e,r)},d=function(e){return{width:ju(e.dom()),height:ju(e.dom())}},i=function(n){f().each(function(t){s(t).each(function(i){var e=kr.forMenu(l,i,t),u=d(i);n(i,e).each(function(e){var t,n,r,o;t=a,n=u,o=d(r=i),n.width===o.width&&n.height===o.height||(aa(t,r.dom(),n.width,n.height),ca(t,r.dom(),o.width,o.height)),a.selection.setRng(e),a.focus(),c.clear(i),qu(i)})})})},u=function(e){return f().bind(function(o){return s(o).bind(function(e){var t=xe.fromDom(a.getDoc()),n=kr.forMenu(l,e,o),r=Bn(y,t,R.none());return fa(e,n,r)})})},m=function(u){n.get().each(function(e){var i=E(e,function(e){return Nn(e)});f().each(function(o){s(o).each(function(t){var e=xe.fromDom(a.getDoc()),n=Wn(e),r=kr.pasteRows(l,t,o,i,n);u(t,r).each(function(e){a.selection.setRng(e),a.focus(),c.clear(t)})})})})};Ia({mceTableSplitCells:function(){i(t.unmergeCells)},mceTableMergeCells:function(){i(t.mergeCells)},mceTableInsertRowBefore:function(){i(t.insertRowsBefore)},mceTableInsertRowAfter:function(){i(t.insertRowsAfter)},mceTableInsertColBefore:function(){i(t.insertColumnsBefore)},mceTableInsertColAfter:function(){i(t.insertColumnsAfter)},mceTableDeleteCol:function(){i(t.deleteColumn)},mceTableDeleteRow:function(){i(t.deleteRow)},mceTableCutRow:function(e){n.set(u()),i(t.deleteRow)},mceTableCopyRow:function(e){n.set(u())},mceTablePasteRowBefore:function(e){m(t.pasteRowsBefore)},mceTablePasteRowAfter:function(e){m(t.pasteRowsAfter)},mceTableDelete:function(){f().orThunk(o).each(function(e){cn.table(e,r).filter(g(r)).each(function(e){var t=xe.fromText("");xt(e,t),Nt(e);var n=a.dom.createRng();n.setStart(t.dom(),0),n.setEnd(t.dom(),0),a.selection.setRng(n)})})}},function(e,t){a.addCommand(t,e)}),Ia({mceInsertTable:b(Pa,a),mceTableProps:b(Pa,a,!0),mceTableRowProps:b(Ra,a),mceTableCellProps:b(xa,a)},function(n,e){a.addCommand(e,function(e,t){n(t)})})},Wa=function(e){var t=R.from(e.dom().documentElement).map(xe.fromDom).getOr(e);return{parent:C(t),view:C(e),origin:C(eo(0,0))}},Ma=function(e,t){return{parent:C(t),view:C(e),origin:C(eo(0,0))}},_a=function(e){var r=q.apply(null,e),o=[];return{bind:function(e){if(e===undefined)throw new Error("Event bind error: undefined handler");o.push(e)},unbind:function(t){o=A(o,function(e){return e!==t})},trigger:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=r.apply(null,e);k(o,function(e){e(n)})}}},La={create:function(e){return{registry:H(e,function(e){return{bind:e.bind,unbind:e.unbind}}),trigger:H(e,function(e){return e.trigger})}}},Fa=function(m,g){return function(e){if(m(e)){var t,n,r,o,i,u,a,c=xe.fromDom(e.target),l=function(){e.stopPropagation()},f=function(){e.preventDefault()},s=x(f,l),d=(t=c,n=e.clientX,r=e.clientY,o=l,i=f,u=s,a=e,{target:C(t),x:C(n),y:C(r),stop:o,prevent:i,kill:u,raw:C(a)});g(d)}}},ja=function(e,t,n,r){return o=e,i=t,u=!1,a=Fa(n,r),o.dom().addEventListener(i,a,u),{unbind:b(za,o,i,a,u)};var o,i,u,a},za=function(e,t,n,r){e.dom().removeEventListener(t,n,r)},Ha=C(!0),Ua=function(e,t,n){return ja(e,t,Ha,n)},qa=Object.prototype.hasOwnProperty,Va=(ka=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t<e.length;t++)e[t]=arguments[t];if(0===e.length)throw new Error("Can't merge zero objects");for(var n={},r=0;r<e.length;r++){var o=e[r];for(var i in o)qa.call(o,i)&&(n[i]=ka(n[i],o[i]))}return n}),Ga={resolve:_o("ephox-dragster").resolve},Ya=du(["compare","extract","mutate","sink"]),Xa=du(["element","start","stop","destroy"]),Ka=du(["forceDrop","drop","move","delayDrop"]),Ja=Ya({compare:function(e,t){return eo(t.left()-e.left(),t.top()-e.top())},extract:function(e){return R.some(eo(e.x(),e.y()))},sink:function(e,t){var n,r,o,i=(n=t,r=Va({layerClass:Ga.resolve("blocker")},n),o=xe.fromTag("div"),se(o,"role","presentation"),Oe(o,{position:"fixed",left:"0px",top:"0px",width:"100%",height:"100%"}),Eo(o,Ga.resolve("blocker")),Eo(o,r.layerClass),{element:function(){return o},destroy:function(){Nt(o)}}),u=Ua(i.element(),"mousedown",e.forceDrop),a=Ua(i.element(),"mouseup",e.drop),c=Ua(i.element(),"mousemove",e.move),l=Ua(i.element(),"mouseout",e.delayDrop);return Xa({element:i.element,start:function(e){Rt(e,i.element())},stop:function(){Nt(i.element())},destroy:function(){i.destroy(),a.unbind(),c.unbind(),l.unbind(),u.unbind()}})},mutate:function(e,t){e.mutate(t.left(),t.top())}});function $a(){var i=R.none(),u=La.create({move:_a(["info"])});return{onEvent:function(e,o){o.extract(e).each(function(e){var t,n,r;(t=o,n=e,r=i.map(function(e){return t.compare(e,n)}),i=R.some(n),r).each(function(e){u.trigger.move(e)})})},reset:function(){i=R.none()},events:u.registry}}function Qa(){var e={onEvent:y,reset:y},t=$a(),n=e;return{on:function(){n.reset(),n=t},off:function(){n.reset(),n=e},isOn:function(){return n===t},onEvent:function(e,t){n.onEvent(e,t)},events:t.events}}var Za=function(t,n,e){var r,o,i,u=!1,a=La.create({start:_a([]),stop:_a([])}),c=Qa(),l=function(){d.stop(),c.isOn()&&(c.off(),a.trigger.stop())},f=(r=l,o=200,i=null,{cancel:function(){null!==i&&(m.clearTimeout(i),i=null)},throttle:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];null!==i&&m.clearTimeout(i),i=m.setTimeout(function(){r.apply(null,e),i=null},o)}});c.events.move.bind(function(e){n.mutate(t,e.info())});var s=function(n){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];u&&n.apply(null,e)}},d=n.sink(Ka({forceDrop:l,drop:s(l),move:s(function(e){f.cancel(),c.onEvent(e,n)}),delayDrop:s(f.throttle)}),e);return{element:d.element,go:function(e){d.start(e),c.on(),a.trigger.start()},on:function(){u=!0},off:function(){u=!1},destroy:function(){d.destroy()},events:a.registry}},ec=function(e,t){void 0===t&&(t={});var n=t.mode!==undefined?t.mode:Ja;return Za(e,n,t)},tc=function(){var n,r=La.create({drag:_a(["xDelta","yDelta","target"])}),o=R.none(),e={mutate:function(e,t){n.trigger.drag(e,t)},events:(n=La.create({drag:_a(["xDelta","yDelta"])})).registry};return e.events.drag.bind(function(t){o.each(function(e){r.trigger.drag(t.xDelta(),t.yDelta(),e)})}),{assign:function(e){o=R.some(e)},get:function(){return o},mutate:e.mutate,events:r.registry}},nc=function(e){return"true"===me(e,"contenteditable")},rc=Lo.resolve("resizer-bar-dragging"),oc=function(o,t,i){var n=tc(),r=ec(n,{}),u=R.none(),e=function(e,t){return R.from(me(e,t))};n.events.drag.bind(function(n){e(n.target(),"data-row").each(function(e){var t=Ui.getInt(n.target(),"top");De(n.target(),"top",t+n.yDelta()+"px")}),e(n.target(),"data-column").each(function(e){var t=Ui.getInt(n.target(),"left");De(n.target(),"left",t+n.xDelta()+"px")})});var a=function(e,t){return Ui.getInt(e,t)-parseInt(me(e,"data-initial-"+t),10)};r.events.stop.bind(function(){n.get().each(function(r){u.each(function(n){e(r,"data-row").each(function(e){var t=a(r,"top");he(r,"data-initial-top"),m.trigger.adjustHeight(n,t,parseInt(e,10))}),e(r,"data-column").each(function(e){var t=a(r,"left");he(r,"data-initial-left"),m.trigger.adjustWidth(n,t,parseInt(e,10))}),Xo(o,n,i,t)})})});var c=function(e,t){m.trigger.startAdjust(),n.assign(e),se(e,"data-initial-"+t,parseInt(Ne(e,t),10)),Eo(e,rc),De(e,"opacity","0.2"),r.go(o.parent())},l=Ua(o.parent(),"mousedown",function(e){Qo(e.target())&&c(e.target(),"top"),Zo(e.target())&&c(e.target(),"left")}),f=function(e){return ft(e,o.view())},s=function(e){return Zt(e,"table",f).filter(function(e){return(t=e,n=f,Zt(t,"[contenteditable]",n)).exists(nc);var t,n})},d=Ua(o.view(),"mouseover",function(e){s(e.target()).fold(function(){Ce(e.target())&&$o(o)},function(e){u=R.some(e),Xo(o,e,i,t)})}),m=La.create({adjustHeight:_a(["table","delta","row"]),adjustWidth:_a(["table","delta","column"]),startAdjust:_a([])});return{destroy:function(){l.unbind(),d.unbind(),r.destroy(),$o(o)},refresh:function(e){Xo(o,e,i,t)},on:r.on,off:r.off,hideBars:b(Ko,o),showBars:b(Jo,o),events:m.registry}},ic=function(e,n){var r=ho.height,t=oc(e,n,r),o=La.create({beforeResize:_a(["table"]),afterResize:_a(["table"]),startDrag:_a([])});return t.events.adjustHeight.bind(function(e){o.trigger.beforeResize(e.table());var t=r.delta(e.delta(),e.table());au(e.table(),t,e.row(),r),o.trigger.afterResize(e.table())}),t.events.startAdjust.bind(function(e){o.trigger.startDrag()}),t.events.adjustWidth.bind(function(e){o.trigger.beforeResize(e.table());var t=n.delta(e.delta(),e.table());uu(e.table(),t,e.column(),n),o.trigger.afterResize(e.table())}),{on:t.on,off:t.off,hideBars:t.hideBars,showBars:t.showBars,destroy:t.destroy,events:o.registry}},uc=function(e,t){return e.inline?Ma(Fu(e),(n=xe.fromTag("div"),Oe(n,{position:"static",height:"0",width:"0",padding:"0",margin:"0",border:"0"}),Rt(Re(),n),n)):Wa(xe.fromDom(e.getDoc()));var n},ac=function(e,t){e.inline&&Nt(t.parent())},cc=function(u){var a,c,o=R.none(),i=R.none(),l=R.none(),f=/(\d+(\.\d+)?)%/,s=function(e){return"TABLE"===e.nodeName};return u.on("init",function(){var e,t=vo(Yu.directionAt),n=uc(u);if(l=R.some(n),("table"===(e=u.getParam("object_resizing",!0))||e)&&u.getParam("table_resize_bars",!0,"boolean")){var r=ic(n,t);r.on(),r.events.startDrag.bind(function(e){o=R.some(u.selection.getRng())}),r.events.beforeResize.bind(function(e){var t=e.table().dom();aa(u,t,ju(t),zu(t))}),r.events.afterResize.bind(function(e){var t=e.table(),n=t.dom();qu(t),o.each(function(e){u.selection.setRng(e),u.focus()}),ca(u,n,ju(n),zu(n)),u.undoManager.add()}),i=R.some(r)}}),u.on("ObjectResizeStart",function(e){var t,n=e.target;s(n)&&(a=e.width,t=n,c=u.dom.getStyle(t,"width")||u.dom.getAttrib(t,"width"))}),u.on("ObjectResized",function(e){var t=e.target;if(s(t)){var n=t;if(f.test(c)){var r=parseFloat(f.exec(c)[1]),o=e.width*r/a;u.dom.setStyle(n,"width",o+"%")}else{var i=[];sa.each(n.rows,function(e){sa.each(e.cells,function(e){var t=u.dom.getStyle(e,"width",!0);i.push({cell:e,width:t})})}),sa.each(i,function(e){u.dom.setStyle(e.cell,"width",e.width),u.dom.setAttrib(e.cell,"width",null)})}}}),{lazyResize:function(){return i},lazyWire:function(){return l.getOr(Wa(xe.fromDom(u.getBody())))},destroy:function(){i.each(function(e){e.destroy()}),l.each(function(e){ac(u,e)})}}},lc=xr([{none:["current"]},{first:["current"]},{middle:["current","target"]},{last:["current"]}]),fc=wo(wo({},lc),{none:function(e){return void 0===e&&(e=undefined),lc.none(e)}}),sc=function(n,e){return cn.table(n,e).bind(function(e){var t=cn.cells(e);return W(t,function(e){return ft(n,e)}).map(function(e){return{index:C(e),all:C(t)}})})},dc=function(t,e){return sc(t,e).fold(function(){return fc.none(t)},function(e){return e.index()+1<e.all().length?fc.middle(t,e.all()[e.index()+1]):fc.last(t)})},mc=function(t,e){return sc(t,e).fold(function(){return fc.none()},function(e){return 0<=e.index()-1?fc.middle(t,e.all()[e.index()-1]):fc.first(t)})},gc={create:q("start","soffset","finish","foffset")},hc=xr([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),pc={before:hc.before,on:hc.on,after:hc.after,cata:function(e,t,n,r){return e.fold(t,n,r)},getStart:function(e){return e.fold(o,o,o)}},vc=xr([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),bc={domRange:vc.domRange,relative:vc.relative,exact:vc.exact,exactFromRange:function(e){return vc.exact(e.start(),e.soffset(),e.finish(),e.foffset())},getWin:function(e){var t,n=e.match({domRange:function(e){return xe.fromDom(e.startContainer)},relative:function(e,t){return pc.getStart(e)},exact:function(e,t,n,r){return e}});return t=n,xe.fromDom(t.dom().ownerDocument.defaultView)},range:gc.create},wc=function(e,t){e.selectNodeContents(t.dom())},yc=function(e,t,n){var r,o,i=e.document.createRange();return r=i,t.fold(function(e){r.setStartBefore(e.dom())},function(e,t){r.setStart(e.dom(),t)},function(e){r.setStartAfter(e.dom())}),o=i,n.fold(function(e){o.setEndBefore(e.dom())},function(e,t){o.setEnd(e.dom(),t)},function(e){o.setEndAfter(e.dom())}),i},xc=function(e,t,n,r,o){var i=e.document.createRange();return i.setStart(t.dom(),n),i.setEnd(r.dom(),o),i},Cc=function(e){return{left:C(e.left),top:C(e.top),right:C(e.right),bottom:C(e.bottom),width:C(e.width),height:C(e.height)}},Rc=xr([{ltr:["start","soffset","finish","foffset"]},{rtl:["start","soffset","finish","foffset"]}]),Sc=function(e,t,n){return t(xe.fromDom(n.startContainer),n.startOffset,xe.fromDom(n.endContainer),n.endOffset)},Tc=function(e,t){var o,n,r,i=(o=e,t.match({domRange:function(e){return{ltr:C(e),rtl:R.none}},relative:function(e,t){return{ltr:we(function(){return yc(o,e,t)}),rtl:we(function(){return R.some(yc(o,t,e))})}},exact:function(e,t,n,r){return{ltr:we(function(){return xc(o,e,t,n,r)}),rtl:we(function(){return R.some(xc(o,n,r,e,t))})}}}));return(r=(n=i).ltr()).collapsed?n.rtl().filter(function(e){return!1===e.collapsed}).map(function(e){return Rc.rtl(xe.fromDom(e.endContainer),e.endOffset,xe.fromDom(e.startContainer),e.startOffset)}).getOrThunk(function(){return Sc(0,Rc.ltr,r)}):Sc(0,Rc.ltr,r)},Dc=function(i,e){return Tc(i,e).match({ltr:function(e,t,n,r){var o=i.document.createRange();return o.setStart(e.dom(),t),o.setEnd(n.dom(),r),o},rtl:function(e,t,n,r){var o=i.document.createRange();return o.setStart(n.dom(),r),o.setEnd(e.dom(),t),o}})},Oc=function(e,t,n){return t>=e.left&&t<=e.right&&n>=e.top&&n<=e.bottom},Nc=function(n,r,e,t,o){var i=function(e){var t=n.dom().createRange();return t.setStart(r.dom(),e),t.collapse(!0),t},u=vn(r).length,a=function(e,t,n,r,o){if(0===o)return 0;if(t===r)return o-1;for(var i=r,u=1;u<o;u++){var a=e(u),c=Math.abs(t-a.left);if(n<=a.bottom){if(n<a.top||i<c)return u-1;i=c}}return 0}(function(e){return i(e).getBoundingClientRect()},e,t,o.right,u);return i(a)},Ec=function(e,t,n,r){return le(t)?function(t,n,r,o){var e=t.dom().createRange();e.selectNode(n.dom());var i=e.getClientRects();return xo(i,function(e){return Oc(e,r,o)?R.some(e):R.none()}).map(function(e){return Nc(t,n,r,o,e)})}(e,t,n,r):(i=t,u=n,a=r,c=(o=e).dom().createRange(),l=bt(i),xo(l,function(e){return c.selectNode(e.dom()),Oc(c.getBoundingClientRect(),u,a)?Ec(o,e,u,a):R.none()}));var o,i,u,a,c,l},kc=function(e,t){return t-e.left<e.right-t},Ac=function(e,t,n){var r=e.dom().createRange();return r.selectNode(t.dom()),r.collapse(n),r},Pc=function(t,e,n){var r=t.dom().createRange();r.selectNode(e.dom());var o=r.getBoundingClientRect(),i=kc(o,n);return(!0===i?Rn:Sn)(e).map(function(e){return Ac(t,e,i)})},Ic=function(e,t,n){var r=t.dom().getBoundingClientRect(),o=kc(r,n);return R.some(Ac(e,t,o))},Bc=function(e,t,n,r){var o=e.dom().createRange();o.selectNode(t.dom());var i=o.getBoundingClientRect();return function(e,t,n,r){var o=e.dom().createRange();o.selectNode(t.dom());var i=o.getBoundingClientRect(),u=Math.max(i.left,Math.min(i.right,n)),a=Math.max(i.top,Math.min(i.bottom,r));return Ec(e,t,u,a)}(e,t,Math.max(i.left,Math.min(i.right,n)),Math.max(i.top,Math.min(i.bottom,r)))},Wc=document.caretPositionFromPoint?function(n,e,t){return R.from(n.dom().caretPositionFromPoint(e,t)).bind(function(e){if(null===e.offsetNode)return R.none();var t=n.dom().createRange();return t.setStart(e.offsetNode,e.offset),t.collapse(),R.some(t)})}:document.caretRangeFromPoint?function(e,t,n){return R.from(e.dom().caretRangeFromPoint(t,n))}:function(o,i,t){return xe.fromPoint(o,i,t).bind(function(r){var e=function(){return e=o,n=i,(0===bt(t=r).length?Ic:Pc)(e,t,n);var e,t,n};return 0===bt(r).length?e():Bc(o,r,i,t).orThunk(e)})},Mc=function(e,t){var n=oe(e);return"input"===n?pc.after(e):O(["br","img"],n)?0===t?pc.before(e):pc.after(e):pc.on(e,t)},_c=function(e,t){var n=e.fold(pc.before,Mc,pc.after),r=t.fold(pc.before,Mc,pc.after);return bc.relative(n,r)},Lc=function(e,t,n,r){var o=Mc(e,t),i=Mc(n,r);return bc.relative(o,i)},Fc=function(e,t,n,r){var o,i,u,a,c,l=(i=t,u=n,a=r,(c=mt(o=e).dom().createRange()).setStart(o.dom(),i),c.setEnd(u.dom(),a),c),f=ft(e,n)&&t===r;return l.collapsed&&!f},jc=function(e,t){R.from(e.getSelection()).each(function(e){e.removeAllRanges(),e.addRange(t)})},zc=function(e,t,n,r,o){var i=xc(e,t,n,r,o);jc(e,i)},Hc=function(s,e){return Tc(s,e).match({ltr:function(e,t,n,r){zc(s,e,t,n,r)},rtl:function(e,t,n,r){var o,i,u,a,c,l=s.getSelection();if(l.setBaseAndExtent)l.setBaseAndExtent(e.dom(),t,n.dom(),r);else if(l.extend)try{i=e,u=t,a=n,c=r,(o=l).collapse(i.dom(),u),o.extend(a.dom(),c)}catch(f){zc(s,n,r,e,t)}else zc(s,n,r,e,t)}})},Uc=function(e){var o=bc.getWin(e).dom(),t=function(e,t,n,r){return xc(o,e,t,n,r)},n=e.match({domRange:function(e){var t=xe.fromDom(e.startContainer),n=xe.fromDom(e.endContainer);return Lc(t,e.startOffset,n,e.endOffset)},relative:_c,exact:Lc});return Tc(o,n).match({ltr:t,rtl:t})},qc=function(e){var t=xe.fromDom(e.anchorNode),n=xe.fromDom(e.focusNode);return Fc(t,e.anchorOffset,n,e.focusOffset)?R.some(gc.create(t,e.anchorOffset,n,e.focusOffset)):function(e){if(0<e.rangeCount){var t=e.getRangeAt(0),n=e.getRangeAt(e.rangeCount-1);return R.some(gc.create(xe.fromDom(t.startContainer),t.startOffset,xe.fromDom(n.endContainer),n.endOffset))}return R.none()}(e)},Vc=function(e,t){var n,r,o=(n=t,r=e.document.createRange(),wc(r,n),r);jc(e,o)},Gc=function(e){return(t=e,R.from(t.getSelection()).filter(function(e){return 0<e.rangeCount}).bind(qc)).map(function(e){return bc.exact(e.start(),e.soffset(),e.finish(),e.foffset())});var t},Yc=function(e,t){var n,r,o,i=Dc(e,t);return r=(n=i).getClientRects(),0<(o=0<r.length?r[0]:n.getBoundingClientRect()).width||0<o.height?R.some(o).map(Cc):R.none()},Xc=function(e,t,n){return r=e,o=t,i=n,u=xe.fromDom(r.document),Wc(u,o,i).map(function(e){return gc.create(xe.fromDom(e.startContainer),e.startOffset,xe.fromDom(e.endContainer),e.endOffset)});var r,o,i,u},Kc=tinymce.util.Tools.resolve("tinymce.util.VK"),Jc=function(e,t,n,r){return el(e,t,dc(n),r)},$c=function(e,t,n,r){return el(e,t,mc(n),r)},Qc=function(e,t){var n=bc.exact(t,0,t,0);return Uc(n)},Zc=function(e,t){var n,r=qt(t,"tr");return(n=r,0===n.length?R.none():R.some(n[n.length-1])).bind(function(e){return Qt(e,"td,th").map(function(e){return Qc(0,e)})})},el=function(r,e,t,o,n){return t.fold(R.none,R.none,function(e,t){return Rn(t).map(function(e){return Qc(0,e)})},function(n){return cn.table(n,e).bind(function(e){var t=kr.noMenu(n);return r.undoManager.transact(function(){o.insertRowsAfter(e,t)}),Zc(0,e)})})},tl=["table","li","dl"],nl=function(t,n,r,o){if(t.keyCode===Kc.TAB){var i=Fu(n),u=function(e){var t=oe(e);return ft(e,i)||O(tl,t)},e=n.selection.getRng();if(e.collapsed){var a=xe.fromDom(e.startContainer);cn.cell(a,u).each(function(e){t.preventDefault(),(t.shiftKey?$c:Jc)(n,u,e,r,o).each(function(e){n.selection.setRng(e)})})}}},rl={create:q("selection","kill")},ol=function(e,t,n,r){return{start:C(pc.on(e,t)),finish:C(pc.on(n,r))}},il={convertToRange:function(e,t){var n=Dc(e,t);return gc.create(xe.fromDom(n.startContainer),n.startOffset,xe.fromDom(n.endContainer),n.endOffset)},makeSitus:ol},ul=function(n,e,r,t,o){return ft(r,t)?R.none():sr(r,t,e).bind(function(e){var t=e.boxes().getOr([]);return 0<t.length?(o(n,t,e.start(),e.finish()),R.some(rl.create(R.some(il.makeSitus(r,0,r,yn(r))),!0))):R.none()})},al={sync:function(n,r,e,t,o,i,u){return ft(e,o)&&t===i?R.none():Zt(e,"td,th",r).bind(function(t){return Zt(o,"td,th",r).bind(function(e){return ul(n,r,t,e,u)})})},detect:ul,update:function(e,t,n,r,o){return mr(r,e,t,o.firstSelectedSelector(),o.lastSelectedSelector()).map(function(e){return o.clear(n),o.selectRange(n,e.boxes(),e.start(),e.finish()),e.boxes()})}},cl=q("item","mode"),ll=function(e,t,n,r){return void 0===r&&(r=fl),e.property().parent(t).map(function(e){return cl(e,r)})},fl=function(e,t,n,r){return void 0===r&&(r=sl),n.sibling(e,t).map(function(e){return cl(e,r)})},sl=function(e,t,n,r){void 0===r&&(r=sl);var o=e.property().children(t);return n.first(o).map(function(e){return cl(e,r)})},dl=[{current:ll,next:fl,fallback:R.none()},{current:fl,next:sl,fallback:R.some(ll)},{current:sl,next:sl,fallback:R.some(fl)}],ml=function(t,n,r,o,e){return void 0===e&&(e=dl),B(e,function(e){return e.current===r}).bind(function(e){return e.current(t,n,o,e.next).orThunk(function(){return e.fallback.bind(function(e){return ml(t,n,e,o)})})})},gl=function(){return{sibling:function(e,t){return e.query().prevSibling(t)},first:function(e){return 0<e.length?R.some(e[e.length-1]):R.none()}}},hl=function(){return{sibling:function(e,t){return e.query().nextSibling(t)},first:function(e){return 0<e.length?R.some(e[0]):R.none()}}},pl=function(t,e,n,r,o,i){return ml(t,e,r,o).bind(function(e){return i(e.item())?R.none():n(e.item())?R.some(e.item()):pl(t,e.item(),n,e.mode(),o,i)})},vl=function(t){return function(e){return 0===t.property().children(e).length}},bl=function(e,t,n,r){return pl(e,t,n,fl,gl(),r)},wl=function(e,t,n,r){return pl(e,t,n,fl,hl(),r)},yl=$n(),xl=function(e,t){return r=t,bl(n=yl,e,vl(n),r);var n,r},Cl=function(e,t){return r=t,wl(n=yl,e,vl(n),r);var n,r},Rl=q("element","offset"),Sl=(q("element","deltaOffset"),q("element","start","finish"),q("begin","end"),q("element","text"),xr([{none:["message"]},{success:[]},{failedUp:["cell"]},{failedDown:["cell"]}])),Tl=function(e){return Zt(e,"tr")},Dl=wo(wo({},Sl),{verify:function(a,e,t,n,r,c,o){return Zt(n,"td,th",o).bind(function(u){return Zt(e,"td,th",o).map(function(i){return ft(u,i)?ft(n,u)&&yn(u)===r?c(i):Sl.none("in same cell"):ar.sharedOne(Tl,[u,i]).fold(function(){return t=i,n=u,r=(e=a).getRect(t),(o=e.getRect(n)).right>r.left&&o.left<r.right?Sl.success():c(i);var e,t,n,r,o},function(e){return c(i)})})}).getOr(Sl.none("default"))},cata:function(e,t,n,r,o){return e.fold(t,n,r,o)}}),Ol=(q("ancestor","descendants","element","index"),q("parent","children","element","index")),Nl=function(e,t){return W(e,b(ft,t))},El=function(e){return"br"===oe(e)},kl=function(e,t,n){return t(e,n).bind(function(e){return le(e)&&0===vn(e).trim().length?kl(e,t,n):R.some(e)})},Al=function(t,e,n,r){return(o=e,i=n,wt(o,i).filter(El).orThunk(function(){return wt(o,i-1).filter(El)})).bind(function(e){return r.traverse(e).fold(function(){return kl(e,r.gather,t).map(r.relative)},function(e){return(r=e,gt(r).bind(function(t){var n=bt(t);return Nl(n,r).map(function(e){return Ol(t,n,r,e)})})).map(function(e){return pc.on(e.parent(),e.index())});var r})});var o,i},Pl=function(e,t,n,r){var o,i,u;return(El(t)?(o=e,i=t,(u=r).traverse(i).orThunk(function(){return kl(i,u.gather,o)}).map(u.relative)):Al(e,t,n,r)).map(function(e){return{start:C(e),finish:C(e)}})},Il=function(e){return Dl.cata(e,function(e){return R.none()},function(){return R.none()},function(e){return R.some(Rl(e,0))},function(e){return R.some(Rl(e,yn(e)))})},Bl=J(["left","top","right","bottom"],[]),Wl={nu:Bl,moveUp:function(e,t){return Bl({left:e.left(),top:e.top()-t,right:e.right(),bottom:e.bottom()-t})},moveDown:function(e,t){return Bl({left:e.left(),top:e.top()+t,right:e.right(),bottom:e.bottom()+t})},moveBottomTo:function(e,t){var n=e.bottom()-e.top();return Bl({left:e.left(),top:t-n,right:e.right(),bottom:t})},moveTopTo:function(e,t){var n=e.bottom()-e.top();return Bl({left:e.left(),top:t,right:e.right(),bottom:t+n})},getTop:function(e){return e.top()},getBottom:function(e){return e.bottom()},translate:function(e,t,n){return Bl({left:e.left()+t,top:e.top()+n,right:e.right()+t,bottom:e.bottom()+n})},toString:function(e){return"("+e.left()+", "+e.top()+") -> ("+e.right()+", "+e.bottom()+")"}},Ml=function(e){return Wl.nu({left:e.left,top:e.top,right:e.right,bottom:e.bottom})},_l=function(e,t){return R.some(e.getRect(t))},Ll=function(e,t,n){return ce(t)?_l(e,t).map(Ml):le(t)?(r=e,o=t,i=n,0<=i&&i<yn(o)?r.getRangedRect(o,i,o,i+1):0<i?r.getRangedRect(o,i-1,o,i):R.none()).map(Ml):R.none();var r,o,i},Fl=function(e,t){return ce(t)?_l(e,t).map(Ml):le(t)?e.getRangedRect(t,0,t,yn(t)).map(Ml):R.none()},jl=xr([{none:[]},{retry:["caret"]}]),zl=function(t,e,r){return(n=e,o=Tu,Vt(function(e,t){return t(e)},Kt,n,o,i)).fold(C(!1),function(e){return Fl(t,e).exists(function(e){return n=e,(t=r).left()<n.left()||Math.abs(n.right()-t.left())<1||t.left()>n.right();var t,n})});var n,o,i},Hl={point:Wl.getTop,adjuster:function(e,t,n,r,o){var i=Wl.moveUp(o,5);return Math.abs(n.top()-r.top())<1?jl.retry(i):n.bottom()<o.top()?jl.retry(i):n.bottom()===o.top()?jl.retry(Wl.moveUp(o,1)):zl(e,t,o)?jl.retry(Wl.translate(i,5,0)):jl.none()},move:Wl.moveUp,gather:xl},Ul={point:Wl.getBottom,adjuster:function(e,t,n,r,o){var i=Wl.moveDown(o,5);return Math.abs(n.bottom()-r.bottom())<1?jl.retry(i):n.top()>o.bottom()?jl.retry(i):n.top()===o.bottom()?jl.retry(Wl.moveDown(o,1)):zl(e,t,o)?jl.retry(Wl.translate(i,5,0)):jl.none()},move:Wl.moveDown,gather:Cl},ql=function(n,r,o,i,u){return 0===u?R.some(i):(c=n,l=i.left(),f=r.point(i),c.elementFromPoint(l,f).filter(function(e){return"table"===oe(e)}).isSome()?(t=i,a=u-1,ql(n,e=r,o,e.move(t,5),a)):n.situsFromPoint(i.left(),r.point(i)).bind(function(e){return e.start().fold(R.none,function(t){return Fl(n,t).bind(function(e){return r.adjuster(n,t,e,o,i).fold(R.none,function(e){return ql(n,r,o,e,u-1)})}).orThunk(function(){return R.some(i)})},R.none)}));var e,t,a,c,l,f},Vl=function(t,n,e){var r,o,i,u=t.move(e,5),a=ql(n,t,e,u,100).getOr(u);return(r=t,o=a,i=n,r.point(o)>i.getInnerHeight()?R.some(r.point(o)-i.getInnerHeight()):r.point(o)<0?R.some(-r.point(o)):R.none()).fold(function(){return n.situsFromPoint(a.left(),t.point(a))},function(e){return n.scrollBy(0,e),n.situsFromPoint(a.left(),t.point(a)-e)})},Gl={tryUp:b(Vl,Hl),tryDown:b(Vl,Ul),ieTryUp:function(e,t){return e.situsFromPoint(t.left(),t.top()-5)},ieTryDown:function(e,t){return e.situsFromPoint(t.left(),t.bottom()+5)},getJumpSize:C(5)},Yl=it.detect(),Xl=function(r,o,i,u,a,c){return 0===c?R.none():$l(r,o,i,u,a).bind(function(e){var t=r.fromSitus(e),n=Dl.verify(r,i,u,t.finish(),t.foffset(),a.failure,o);return Dl.cata(n,function(){return R.none()},function(){return R.some(e)},function(e){return ft(i,e)&&0===u?Kl(r,i,u,Wl.moveUp,a):Xl(r,o,e,0,a,c-1)},function(e){return ft(i,e)&&u===yn(e)?Kl(r,i,u,Wl.moveDown,a):Xl(r,o,e,yn(e),a,c-1)})})},Kl=function(t,e,n,r,o){return Ll(t,e,n).bind(function(e){return Jl(t,o,r(e,Gl.getJumpSize()))})},Jl=function(e,t,n){return Yl.browser.isChrome()||Yl.browser.isSafari()||Yl.browser.isFirefox()||Yl.browser.isEdge()?t.otherRetry(e,n):Yl.browser.isIE()?t.ieRetry(e,n):R.none()},$l=function(t,e,n,r,o){return Ll(t,n,r).bind(function(e){return Jl(t,o,e)})},Ql=function(t,n,r){return(o=t,i=n,u=r,o.getSelection().bind(function(r){return Pl(i,r.finish(),r.foffset(),u).fold(function(){return R.some(Rl(r.finish(),r.foffset()))},function(e){var t=o.fromSitus(e),n=Dl.verify(o,r.finish(),r.foffset(),t.finish(),t.foffset(),u.failure,i);return Il(n)})})).bind(function(e){return Xl(t,n,e.element(),e.offset(),r,20).map(t.fromSitus)});var o,i,u},Zl=it.detect(),ef=function(e,t){return Kt(e,function(e){return gt(e).exists(function(e){return ft(e,t)})},n).isSome();var n},tf=function(t,r,o,e,i){return Zt(e,"td,th",r).bind(function(n){return Zt(n,"table",r).bind(function(e){return ef(i,e)?Ql(t,r,o).bind(function(t){return Zt(t.finish(),"td,th",r).map(function(e){return{start:C(n),finish:C(e),range:C(t)}})}):R.none()})})},nf=function(e,t,n,r,o,i){return Zl.browser.isIE()?R.none():i(r,t).orThunk(function(){return tf(e,t,n,r,o).map(function(e){var t=e.range();return rl.create(R.some(il.makeSitus(t.start(),t.soffset(),t.finish(),t.foffset())),!0)})})},rf=function(e,t,n,r,o,i,u){return tf(e,n,r,o,i).bind(function(e){return al.detect(t,n,e.start(),e.finish(),u)})},of=function(e,u){return Zt(e,"tr",u).bind(function(i){return Zt(i,"table",u).bind(function(e){var t,n,r,o=qt(e,"tr");return ft(i,o[0])?(t=e,n=function(e){return Sn(e).isSome()},r=u,bl(yl,t,n,r)).map(function(e){var t=yn(e);return rl.create(R.some(il.makeSitus(e,t,e,t)),!0)}):R.none()})})},uf=function(e,u){return Zt(e,"tr",u).bind(function(i){return Zt(i,"table",u).bind(function(e){var t,n,r,o=qt(e,"tr");return ft(i,o[o.length-1])?(t=e,n=function(e){return Rn(e).isSome()},r=u,wl(yl,t,n,r)).map(function(e){return rl.create(R.some(il.makeSitus(e,0,e,0)),!0)}):R.none()})})},af=function(e,t){return Zt(e,"td,th",t)},cf={down:{traverse:vt,gather:Cl,relative:pc.before,otherRetry:Gl.tryDown,ieRetry:Gl.ieTryDown,failure:Dl.failedDown},up:{traverse:pt,gather:xl,relative:pc.before,otherRetry:Gl.tryUp,ieRetry:Gl.ieTryUp,failure:Dl.failedUp}},lf=function(t){return function(e){return e===t}},ff=lf(38),sf=lf(40),df={ltr:{isBackward:lf(37),isForward:lf(39)},rtl:{isBackward:lf(39),isForward:lf(37)},isUp:ff,isDown:sf,isNavigation:function(e){return 37<=e&&e<=40}},mf=function(e){return{left:e.left(),top:e.top(),right:e.right(),bottom:e.bottom(),width:e.width(),height:e.height()}},gf=(it.detect().browser.isSafari(),function(a){return{elementFromPoint:function(e,t){return xe.fromPoint(xe.fromDom(a.document),e,t)},getRect:function(e){return e.dom().getBoundingClientRect()},getRangedRect:function(e,t,n,r){var o=bc.exact(e,t,n,r);return Yc(a,o).map(mf)},getSelection:function(){return Gc(a).map(function(e){return il.convertToRange(a,e)})},fromSitus:function(e){var t=bc.relative(e.start(),e.finish());return il.convertToRange(a,t)},situsFromPoint:function(e,t){return Xc(a,e,t).map(function(e){return ol(e.start(),e.soffset(),e.finish(),e.foffset())})},clearSelection:function(){a.getSelection().removeAllRanges()},setSelection:function(e){var t,n,r,o,i,u;t=a,n=e.start(),r=e.soffset(),o=e.finish(),i=e.foffset(),u=Lc(n,r,o,i),Hc(t,u)},setRelativeSelection:function(e,t){var n,r;n=a,r=_c(e,t),Hc(n,r)},selectContents:function(e){Vc(a,e)},getInnerHeight:function(){return a.innerHeight},getScrollY:function(){var e,t,n,r;return(e=xe.fromDom(a.document),t=e!==undefined?e.dom():m.document,n=t.body.scrollLeft||t.documentElement.scrollLeft,r=t.body.scrollTop||t.documentElement.scrollTop,eo(n,r)).top()},scrollBy:function(e,t){var n,r,o;n=e,r=t,((o=xe.fromDom(a.document))!==undefined?o.dom():m.document).defaultView.scrollBy(n,r)}}}),hf=q("rows","cols"),pf={mouse:function(e,t,n,r){var o,i,u,a,c,l,f=gf(e),s=(o=f,i=t,u=n,a=r,c=R.none(),l=function(){c=R.none()},{mousedown:function(e){a.clear(i),c=af(e.target(),u)},mouseover:function(e){c.each(function(r){a.clear(i),af(e.target(),u).each(function(n){sr(r,n,u).each(function(e){var t=e.boxes().getOr([]);(1<t.length||1===t.length&&!ft(r,n))&&(a.selectRange(i,t,e.start(),e.finish()),o.selectContents(n))})})})},mouseup:function(e){c.each(l)}});return{mousedown:s.mousedown,mouseover:s.mouseover,mouseup:s.mouseup}},keyboard:function(e,l,f,s){var d=gf(e),m=function(){return s.clear(l),R.none()};return{keydown:function(e,t,n,r,o,i){var u=e.raw(),a=u.which,c=!0===u.shiftKey;return dr(l,s.selectedSelector()).fold(function(){return df.isDown(a)&&c?b(rf,d,l,f,cf.down,r,t,s.selectRange):df.isUp(a)&&c?b(rf,d,l,f,cf.up,r,t,s.selectRange):df.isDown(a)?b(nf,d,f,cf.down,r,t,uf):df.isUp(a)?b(nf,d,f,cf.up,r,t,of):R.none},function(t){var e=function(e){return function(){return xo(e,function(e){return al.update(e.rows(),e.cols(),l,t,s)}).fold(function(){return gr(l,s.firstSelectedSelector(),s.lastSelectedSelector()).map(function(e){var t=df.isDown(a)||i.isForward(a)?pc.after:pc.before;return d.setRelativeSelection(pc.on(e.first(),0),t(e.table())),s.clear(l),rl.create(R.none(),!0)})},function(e){return R.some(rl.create(R.none(),!0))})}};return df.isDown(a)&&c?e([hf(1,0)]):df.isUp(a)&&c?e([hf(-1,0)]):i.isBackward(a)&&c?e([hf(0,-1),hf(-1,0)]):i.isForward(a)&&c?e([hf(0,1),hf(1,0)]):df.isNavigation(a)&&!1===c?m:R.none})()},keyup:function(n,r,o,i,u){return dr(l,s.selectedSelector()).fold(function(){var e=n.raw(),t=e.which;return 0==(!0===e.shiftKey)?R.none():df.isNavigation(t)?al.sync(l,f,r,o,i,u,s.selectRange):R.none()},R.none)}}}},vf=function(r,e){k(e,function(e){var t,n;n=e,Do(t=r)?t.dom().classList.remove(n):No(t,n),ko(t)})},bf={byClass:function(o){var t,n,i=(t=o.selected(),function(e){Eo(e,t)}),r=(n=[o.selected(),o.lastSelected(),o.firstSelected()],function(e){vf(e,n)}),u=function(e){var t=qt(e,o.selectedSelector());k(t,r)};return{clear:u,selectRange:function(e,t,n,r){u(e),k(t,i),Eo(n,o.firstSelected()),Eo(r,o.lastSelected())},selectedSelector:o.selectedSelector,firstSelectedSelector:o.firstSelectedSelector,lastSelectedSelector:o.lastSelectedSelector}},byAttr:function(o){var n=function(e){he(e,o.selected()),he(e,o.firstSelected()),he(e,o.lastSelected())},i=function(e){se(e,o.selected(),"1")},u=function(e){var t=qt(e,o.selectedSelector());k(t,n)};return{clear:u,selectRange:function(e,t,n,r){u(e),k(t,i),se(n,o.firstSelected(),"1"),se(r,o.lastSelected(),"1")},selectedSelector:o.selectedSelector,firstSelectedSelector:o.firstSelectedSelector,lastSelectedSelector:o.lastSelectedSelector}}},wf=function(e){return!1===Ao(xe.fromDom(e.target),"ephox-snooker-resizer-bar")};function yf(h,p){var v=J(["mousedown","mouseover","mouseup","keyup","keydown"],[]),b=R.none(),w=bf.byAttr(yr);return h.on("init",function(e){var r=h.getWin(),o=Fu(h),t=Hu(h),n=pf.mouse(r,o,t,w),a=pf.keyboard(r,o,t,w),c=function(e,t){!0===e.raw().shiftKey&&(t.kill()&&e.kill(),t.selection().each(function(e){var t=bc.relative(e.start(),e.finish()),n=Dc(r,t);h.selection.setRng(n)}))},i=function(e){var t=f(e);if(t.raw().shiftKey&&df.isNavigation(t.raw().which)){var n=h.selection.getRng(),r=xe.fromDom(n.startContainer),o=xe.fromDom(n.endContainer);a.keyup(t,r,n.startOffset,o,n.endOffset).each(function(e){c(t,e)})}},u=function(e){var t=f(e);p().each(function(e){e.hideBars()});var n=h.selection.getRng(),r=xe.fromDom(h.selection.getStart()),o=xe.fromDom(n.startContainer),i=xe.fromDom(n.endContainer),u=Yu.directionAt(r).isRtl()?df.rtl:df.ltr;a.keydown(t,o,n.startOffset,i,n.endOffset,u).each(function(e){c(t,e)}),p().each(function(e){e.showBars()})},l=function(e){return e.hasOwnProperty("x")&&e.hasOwnProperty("y")},f=function(e){var t=xe.fromDom(e.target),n=function(){e.stopPropagation()},r=function(){e.preventDefault()},o=x(r,n);return{target:C(t),x:C(l(e)?e.x:null),y:C(l(e)?e.y:null),stop:n,prevent:r,kill:o,raw:C(e)}},s=function(e){return 0===e.button},d=function(e){s(e)&&wf(e)&&n.mousedown(f(e))},m=function(e){var t;((t=e).buttons===undefined||Sa.ie&&12<=Sa.ie&&0===t.buttons||0!=(1&t.buttons))&&wf(e)&&n.mouseover(f(e))},g=function(e){s(e)&&wf(e)&&n.mouseup(f(e))};h.on("mousedown",d),h.on("mouseover",m),h.on("mouseup",g),h.on("keyup",i),h.on("keydown",u),h.on("nodechange",function(){var e=h.selection,t=xe.fromDom(e.getStart()),n=xe.fromDom(e.getEnd());ar.sharedOne(cn.table,[t,n]).fold(function(){w.clear(o)},y)}),b=R.some(v({mousedown:d,mouseover:m,mouseup:g,keyup:i,keydown:u}))}),{clear:w.clear,destroy:function(){b.each(function(e){})}}}var xf=sa.each,Cf=function(t){var n=[];function e(e){return function(){t.execCommand(e)}}xf("inserttable tableprops deletetable | cell row column".split(" "),function(e){"|"===e?n.push({text:"-"}):n.push(t.menuItems[e])}),t.addButton("table",{type:"menubutton",title:"Table",menu:n}),t.addButton("tableprops",{title:"Table properties",onclick:e("mceTableProps"),icon:"table"}),t.addButton("tabledelete",{title:"Delete table",onclick:e("mceTableDelete")}),t.addButton("tablecellprops",{title:"Cell properties",onclick:e("mceTableCellProps")}),t.addButton("tablemergecells",{title:"Merge cells",onclick:e("mceTableMergeCells")}),t.addButton("tablesplitcells",{title:"Split cell",onclick:e("mceTableSplitCells")}),t.addButton("tableinsertrowbefore",{title:"Insert row before",onclick:e("mceTableInsertRowBefore")}),t.addButton("tableinsertrowafter",{title:"Insert row after",onclick:e("mceTableInsertRowAfter")}),t.addButton("tabledeleterow",{title:"Delete row",onclick:e("mceTableDeleteRow")}),t.addButton("tablerowprops",{title:"Row properties",onclick:e("mceTableRowProps")}),t.addButton("tablecutrow",{title:"Cut row",onclick:e("mceTableCutRow")}),t.addButton("tablecopyrow",{title:"Copy row",onclick:e("mceTableCopyRow")}),t.addButton("tablepasterowbefore",{title:"Paste row before",onclick:e("mceTablePasteRowBefore")}),t.addButton("tablepasterowafter",{title:"Paste row after",onclick:e("mceTablePasteRowAfter")}),t.addButton("tableinsertcolbefore",{title:"Insert column before",onclick:e("mceTableInsertColBefore")}),t.addButton("tableinsertcolafter",{title:"Insert column after",onclick:e("mceTableInsertColAfter")}),t.addButton("tabledeletecol",{title:"Delete column",onclick:e("mceTableDeleteCol")})},Rf=function(t){var e,n=""===(e=t.getParam("table_toolbar",Xu))||!1===e?[]:d(e)?e.split(/[ ,]/):h(e)?e:[];0<n.length&&t.addContextToolbar(function(e){return t.dom.is(e,"table")&&t.getBody().contains(e)},n.join(" "))},Sf=function(o,n){var r=R.none(),i=[],u=[],a=[],c=[],l=function(e){e.disabled(!0)},f=function(e){e.disabled(!1)},e=function(){var t=this;i.push(t),r.fold(function(){l(t)},function(e){f(t)})},t=function(){var t=this;u.push(t),r.fold(function(){l(t)},function(e){f(t)})};o.on("init",function(){o.on("nodechange",function(e){var t=R.from(o.dom.getParent(o.selection.getStart(),"th,td"));(r=t.bind(function(e){var t=xe.fromDom(e);return cn.table(t).map(function(e){return kr.forMenu(n,e,t)})})).fold(function(){k(i,l),k(u,l),k(a,l),k(c,l)},function(t){k(i,f),k(u,f),k(a,function(e){e.disabled(t.mergable().isNone())}),k(c,function(e){e.disabled(t.unmergable().isNone())})})})});var s=function(e,t,n,r){var o,i,u,a,c,l=r.getEl().getElementsByTagName("table")[0],f=r.isRtl()||"tl-tr"===r.parent().rel;for(l.nextSibling.innerHTML=t+1+" x "+(n+1),f&&(t=9-t),i=0;i<10;i++)for(o=0;o<10;o++)a=l.rows[i].childNodes[o].firstChild,c=(f?t<=o:o<=t)&&i<=n,e.dom.toggleClass(a,"mce-active",c),c&&(u=a);return u.parentNode},d=!1===o.getParam("table_grid",!0,"boolean")?{text:"Table",icon:"table",context:"table",onclick:m("mceInsertTable")}:{text:"Table",icon:"table",context:"table",ariaHideMenu:!0,onclick:function(e){e.aria&&(this.parent().hideAll(),e.stopImmediatePropagation(),o.execCommand("mceInsertTable"))},onshow:function(){s(o,0,0,this.menu.items()[0])},onhide:function(){var e=this.menu.items()[0].getEl().getElementsByTagName("a");o.dom.removeClass(e,"mce-active"),o.dom.addClass(e[0],"mce-active")},menu:[{type:"container",html:function(){var e="";e='<table role="grid" class="mce-grid mce-grid-border" aria-readonly="true">';for(var t=0;t<10;t++){e+="<tr>";for(var n=0;n<10;n++)e+='<td role="gridcell" tabindex="-1"><a id="mcegrid'+(10*t+n)+'" href="#" data-mce-x="'+n+'" data-mce-y="'+t+'"></a></td>';e+="</tr>"}return e+="</table>",e+='<div class="mce-text-center" role="presentation">1 x 1</div>'}(),onPostRender:function(){this.lastX=this.lastY=0},onmousemove:function(e){var t,n,r=e.target;"A"===r.tagName.toUpperCase()&&(t=parseInt(r.getAttribute("data-mce-x"),10),n=parseInt(r.getAttribute("data-mce-y"),10),(this.isRtl()||"tl-tr"===this.parent().rel)&&(t=9-t),t===this.lastX&&n===this.lastY||(s(o,t,n,e.control),this.lastX=t,this.lastY=n))},onclick:function(e){var t=this;"A"===e.target.tagName.toUpperCase()&&(e.preventDefault(),e.stopPropagation(),t.parent().cancel(),o.undoManager.transact(function(){Na(o,t.lastX+1,t.lastY+1)}),o.addVisual())}}]};function m(e){return function(){o.execCommand(e)}}var g={text:"Table properties",context:"table",onPostRender:e,onclick:m("mceTableProps")},h={text:"Delete table",context:"table",onPostRender:e,cmd:"mceTableDelete"},p={text:"Row",context:"table",menu:[{text:"Insert row before",onclick:m("mceTableInsertRowBefore"),onPostRender:t},{text:"Insert row after",onclick:m("mceTableInsertRowAfter"),onPostRender:t},{text:"Delete row",onclick:m("mceTableDeleteRow"),onPostRender:t},{text:"Row properties",onclick:m("mceTableRowProps"),onPostRender:t},{text:"-"},{text:"Cut row",onclick:m("mceTableCutRow"),onPostRender:t},{text:"Copy row",onclick:m("mceTableCopyRow"),onPostRender:t},{text:"Paste row before",onclick:m("mceTablePasteRowBefore"),onPostRender:t},{text:"Paste row after",onclick:m("mceTablePasteRowAfter"),onPostRender:t}]},v={text:"Column",context:"table",menu:[{text:"Insert column before",onclick:m("mceTableInsertColBefore"),onPostRender:t},{text:"Insert column after",onclick:m("mceTableInsertColAfter"),onPostRender:t},{text:"Delete column",onclick:m("mceTableDeleteCol"),onPostRender:t}]},b={separator:"before",text:"Cell",context:"table",menu:[{text:"Cell properties",onclick:m("mceTableCellProps"),onPostRender:t},{text:"Merge cells",onclick:m("mceTableMergeCells"),onPostRender:function(){var t=this;a.push(t),r.fold(function(){l(t)},function(e){t.disabled(e.mergable().isNone())})}},{text:"Split cell",onclick:m("mceTableSplitCells"),onPostRender:function(){var t=this;c.push(t),r.fold(function(){l(t)},function(e){t.disabled(e.unmergable().isNone())})}}]};o.addMenuItem("inserttable",d),o.addMenuItem("tableprops",g),o.addMenuItem("deletetable",h),o.addMenuItem("row",p),o.addMenuItem("column",v),o.addMenuItem("cell",b)},Tf=function(n,r){return{insertTable:function(e,t){return Na(n,e,t)},setClipboardRows:function(e){return t=r,n=E(e,xe.fromDom),void t.set(R.from(n));var t,n},getClipboardRows:function(){return r.get().fold(function(){},function(e){return E(e,function(e){return e.dom()})})}}};e.add("table",function(t){var n,r=cc(t),e=yf(t,r.lazyResize),o=la(t,r.lazyWire),i=(n=t,{get:function(){var e=Fu(n);return hr(e,yr.selectedSelector()).fold(function(){return n.selection.getStart()===undefined?Rr.none():Rr.single(n.selection)},function(e){return Rr.multiple(e)})}}),u=lu(R.none());return Ba(t,o,e,i,u),Ar(t,i,o,e),Sf(t,i),Cf(t),Rf(t),t.on("PreInit",function(){t.serializer.addTempAttr(yr.firstSelected()),t.serializer.addTempAttr(yr.lastSelected())}),t.getParam("table_tab_navigation",!0,"boolean")&&t.on("keydown",function(e){nl(e,t,o,r.lazyWire)}),t.on("remove",function(){r.destroy(),e.destroy()}),Tf(t,u)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/textpattern/plugin.min.js b/lib/web/tiny_mce_4/plugins/textpattern/plugin.min.js index c6ce64d0c8574..03f872a74e3a6 100644 --- a/lib/web/tiny_mce_4/plugins/textpattern/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/textpattern/plugin.min.js @@ -1 +1 @@ -!function(l){"use strict";var t,n,e,r,a,o=function(t){var n=t,e=function(){return n};return{get:e,set:function(t){n=t},clone:function(){return o(e())}}},i=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=function(t){return function(){return t}},u=f(!1),s=f(!0),c=u,d=s,g=function(){return m},m=(r={fold:function(t,n){return t()},is:c,isSome:c,isNone:d,getOr:e=function(t){return t},getOrThunk:n=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:e,orThunk:n,map:g,ap:g,each:function(){},bind:g,flatten:g,exists:c,forall:d,filter:g,equals:t=function(t){return t.isNone()},equals_:t,toArray:function(){return[]},toString:f("none()")},Object.freeze&&Object.freeze(r),r),h=function(e){var t=function(){return e},n=function(){return a},r=function(t){return t(e)},a={fold:function(t,n){return n(e)},is:function(t){return e===t},isSome:d,isNone:c,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:n,orThunk:n,map:function(t){return h(t(e))},ap:function(t){return t.fold(g,function(t){return h(t(e))})},each:function(t){t(e)},bind:r,flatten:t,exists:r,forall:r,filter:function(t){return t(e)?a:m},equals:function(t){return t.is(e)},equals_:function(t,n){return t.fold(c,function(t){return n(e,t)})},toArray:function(){return[e]},toString:function(){return"some("+e+")"}};return a},p={some:h,none:g,from:function(t){return null===t||t===undefined?m:h(t)}},v=(a="function",function(t){return function(t){if(null===t)return"null";var n=typeof t;return"object"===n&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===n&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":n}(t)===a}),O=Array.prototype.slice,y=function(t,n){for(var e=[],r=0,a=t.length;r<a;r++){var o=t[r];n(o,r,t)&&e.push(o)}return e},P=(v(Array.from)&&Array.from,Object.hasOwnProperty),x=function(t,n){return P.call(t,n)},T=function(t){return x(t,"start")&&x(t,"end")},b=function(t){return!x(t,"end")&&!x(t,"replacement")},k=function(t){return x(t,"replacement")},C=function(t){return n=t,e=function(t,n){return t.start.length===n.start.length?0:t.start.length>n.start.length?-1:1},(r=O.call(n,0)).sort(e),r;var n,e,r},D=function(t){return{inlinePatterns:C(y(t,T)),blockPatterns:C(y(t,b)),replacementPatterns:y(t,k)}},S=function(n){return{setPatterns:function(t){n.set(D(t))},getPatterns:function(){return n.get().inlinePatterns.concat(n.get().blockPatterns,n.get().replacementPatterns)}}},A=[{start:"*",end:"*",format:"italic"},{start:"**",end:"**",format:"bold"},{start:"***",end:"***",format:["bold","italic"]},{start:"#",format:"h1"},{start:"##",format:"h2"},{start:"###",format:"h3"},{start:"####",format:"h4"},{start:"#####",format:"h5"},{start:"######",format:"h6"},{start:"1. ",cmd:"InsertOrderedList"},{start:"* ",cmd:"InsertUnorderedList"},{start:"- ",cmd:"InsertUnorderedList"}],N=function(t){var n,e,r=(n=t,e="textpattern_patterns",x(n,e)?p.from(n[e]):p.none()).getOr(A);return D(r)},R=tinymce.util.Tools.resolve("tinymce.util.Delay"),w=tinymce.util.Tools.resolve("tinymce.util.VK"),I=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),j=tinymce.util.Tools.resolve("tinymce.util.Tools"),E=function(t,n){for(var e=0;e<t.length;e++){var r=t[e];if(0===n.indexOf(r.start)&&(!r.end||n.lastIndexOf(r.end)===n.length-r.end.length))return r}},q=function(t,n,e){if(!1!==n.collapsed){var r=n.startContainer,a=r.data,o=!0===e?1:0;if(3===r.nodeType){var i=function(t,n,e,r){var a,o,i,f,u,s;for(o=0;o<t.length;o++)if((a=t[o]).end!==undefined&&(f=a,u=e,s=r,n.substr(u-f.end.length-s,f.end.length)===f.end)&&0<e-r-(i=a).end.length-i.start.length)return a}(t,a,n.startOffset,o);if(i!==undefined){var f=a.lastIndexOf(i.end,n.startOffset-o),u=a.lastIndexOf(i.start,f-i.end.length);if(f=a.indexOf(i.end,u+i.start.length),-1!==u){var s=l.document.createRange();s.setStart(r,u),s.setEnd(r,f+i.end.length);var c=E(t,s.toString());if(!(i===undefined||c!==i||r.data.length<=i.start.length+i.end.length))return{pattern:i,startOffset:u,endOffset:f}}}}}},L=function(t){return t&&3===t.nodeType},M=function(t,n,e){var r=t.dom.createRng();r.setStart(n,e),r.setEnd(n,e),t.selection.setRng(r)},U=function(n,t,e){var r=n.selection.getRng();return p.from(q(t,r,e)).map(function(t){return function(a,o,i,f){var u=j.isArray(i.pattern.format)?i.pattern.format:[i.pattern.format];if(0!==j.grep(u,function(t){var n=a.formatter.get(t);return n&&n[0].inline}).length)return a.undoManager.transact(function(){var t,n,e,r;t=o,n=i.pattern,e=i.endOffset,r=i.startOffset,(t=0<r?t.splitText(r):t).splitText(e-r+n.end.length),t.deleteData(0,n.start.length),t.deleteData(t.data.length-n.end.length,n.end.length),o=t,f&&a.selection.setCursorLocation(o.nextSibling,1),u.forEach(function(t){a.formatter.apply(t,{},o)})}),o}(n,r.startContainer,t,e)})},_=function(s,t,c){var n=s.selection.getRng(),l=n.startContainer;n.collapsed&&L(l)&&function(t,n,e){for(var r=0;r<t.length;r++){var a=e.lastIndexOf(t[r].start,n);if(-1!==a)return p.some({pattern:t[r],startOffset:a})}return p.none()}(t,n.startOffset,l.data).each(function(t){var n,e,r,a,o,i,f,u=c?p.some((n=l,r=(e=t).startOffset+e.pattern.start.length,a=n.data.slice(r,r+1),n.deleteData(r,1),a)):p.none();o=s,f=t,(i=l).deleteData(f.startOffset,f.pattern.start.length),o.insertContent(f.pattern.replacement),p.from(i.nextSibling).filter(L).each(function(t){t.insertData(0,i.data),o.dom.remove(i)}),u.each(function(t){return function(t,n){var e=t.selection.getRng(),r=e.startContainer;if(L(r)){var a=e.startOffset;r.insertData(a,n),M(t,r,a+n.length)}else{var o=t.dom.doc.createTextNode(n);e.insertNode(o),M(t,o,o.length)}}(s,t)})})},z=function(t,n,e){for(var r=0;r<t.length;r++)if(e(t[r],n))return!0},K=function(t,n){var e,r,a,o;e=t,r=n.replacementPatterns,_(e,r,!1),a=t,o=n.inlinePatterns,U(a,o,!1).each(function(t){M(a,t,t.data.length)}),function(t,n){var e,r,a,o,i,f,u,s,c,l,d;if(e=t.selection,r=t.dom,e.isCollapsed()&&(u=r.getParent(e.getStart(),"p"))){for(c=new I(u,u);i=c.next();)if(L(i)){o=i;break}if(o){if(!(s=E(n,o.data)))return;if(a=(l=e.getRng(!0)).startContainer,d=l.startOffset,o===a&&(d=Math.max(0,d-s.start.length)),j.trim(o.data).length===s.start.length)return;s.format&&(f=t.formatter.get(s.format))&&f[0].block&&(o.deleteData(0,s.start.length),t.formatter.apply(s.format,{},o),l.setStart(a,d),l.collapse(!0),e.setRng(l)),s.cmd&&t.undoManager.transact(function(){o.deleteData(0,s.start.length),t.execCommand(s.cmd)})}}}(t,n.blockPatterns)},V=function(t,n){var e,r,a,o;e=t,r=n.replacementPatterns,_(e,r,!0),a=t,o=n.inlinePatterns,U(a,o,!0).each(function(t){var n=t.data.slice(-1);if(/[\u00a0 ]/.test(n)){t.deleteData(t.data.length-1,1);var e=a.dom.doc.createTextNode(n);a.dom.insertAfter(e,t.parentNode),M(a,e,1)}})},W=function(t,n){return z(t,n,function(t,n){return t.charCodeAt(0)===n.charCode})},B=function(t,n){return z(t,n,function(t,n){return t===n.keyCode&&!1===w.modifierPressed(n)})},F=function(n,e){var r=[",",".",";",":","!","?"],a=[32];n.on("keydown",function(t){13!==t.keyCode||w.modifierPressed(t)||K(n,e.get())},!0),n.on("keyup",function(t){B(a,t)&&V(n,e.get())}),n.on("keypress",function(t){W(r,t)&&R.setEditorTimeout(n,function(){V(n,e.get())})})};i.add("textpattern",function(t){var n=o(N(t.settings));return F(t,n),S(n)})}(window); \ No newline at end of file +!function(l){"use strict";var t,n,e,r,a,o=function(t){var n=t,e=function(){return n};return{get:e,set:function(t){n=t},clone:function(){return o(e())}}},i=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=function(){},u=function(t){return function(){return t}},s=u(!1),c=u(!0),d=function(){return g},g=(t=function(t){return t.isNone()},r={fold:function(t,n){return t()},is:s,isSome:s,isNone:c,getOr:e=function(t){return t},getOrThunk:n=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:u(null),getOrUndefined:u(undefined),or:e,orThunk:n,map:d,each:f,bind:d,exists:s,forall:c,filter:d,equals:t,equals_:t,toArray:function(){return[]},toString:u("none()")},Object.freeze&&Object.freeze(r),r),m=function(e){var t=u(e),n=function(){return a},r=function(t){return t(e)},a={fold:function(t,n){return n(e)},is:function(t){return e===t},isSome:c,isNone:s,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:n,orThunk:n,map:function(t){return m(t(e))},each:function(t){t(e)},bind:r,exists:r,forall:r,filter:function(t){return t(e)?a:g},toArray:function(){return[e]},toString:function(){return"some("+e+")"},equals:function(t){return t.is(e)},equals_:function(t,n){return t.fold(s,function(t){return n(e,t)})}};return a},h={some:m,none:d,from:function(t){return null===t||t===undefined?g:m(t)}},p=(a="function",function(t){return function(t){if(null===t)return"null";var n=typeof t;return"object"===n&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===n&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":n}(t)===a}),v=Array.prototype.slice,y=function(t,n){for(var e=[],r=0,a=t.length;r<a;r++){var o=t[r];n(o,r)&&e.push(o)}return e},O=(p(Array.from)&&Array.from,Object.hasOwnProperty),P=function(t,n){return O.call(t,n)},x=function(t){return P(t,"start")&&P(t,"end")},T=function(t){return!P(t,"end")&&!P(t,"replacement")},b=function(t){return P(t,"replacement")},k=function(t){return n=t,e=function(t,n){return t.start.length===n.start.length?0:t.start.length>n.start.length?-1:1},(r=v.call(n,0)).sort(e),r;var n,e,r},C=function(t){return{inlinePatterns:k(y(t,x)),blockPatterns:k(y(t,T)),replacementPatterns:y(t,b)}},D=function(n){return{setPatterns:function(t){n.set(C(t))},getPatterns:function(){return function(){for(var t=0,n=0,e=arguments.length;n<e;n++)t+=arguments[n].length;var r=Array(t),a=0;for(n=0;n<e;n++)for(var o=arguments[n],i=0,f=o.length;i<f;i++,a++)r[a]=o[i];return r}(n.get().inlinePatterns,n.get().blockPatterns,n.get().replacementPatterns)}}},S=[{start:"*",end:"*",format:"italic"},{start:"**",end:"**",format:"bold"},{start:"***",end:"***",format:["bold","italic"]},{start:"#",format:"h1"},{start:"##",format:"h2"},{start:"###",format:"h3"},{start:"####",format:"h4"},{start:"#####",format:"h5"},{start:"######",format:"h6"},{start:"1. ",cmd:"InsertOrderedList"},{start:"* ",cmd:"InsertUnorderedList"},{start:"- ",cmd:"InsertUnorderedList"}],A=function(t){var n,e,r=(n=t,e="textpattern_patterns",P(n,e)?h.from(n[e]):h.none()).getOr(S);return C(r)},N=tinymce.util.Tools.resolve("tinymce.util.Delay"),R=tinymce.util.Tools.resolve("tinymce.util.VK"),w=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),I=tinymce.util.Tools.resolve("tinymce.util.Tools"),j=function(t,n){for(var e=0;e<t.length;e++){var r=t[e];if(0===n.indexOf(r.start)&&(!r.end||n.lastIndexOf(r.end)===n.length-r.end.length))return r}},E=function(t,n,e){if(!1!==n.collapsed){var r=n.startContainer,a=r.data,o=!0===e?1:0;if(3===r.nodeType){var i=function(t,n,e,r){var a,o,i,f,u,s;for(o=0;o<t.length;o++)if((a=t[o]).end!==undefined&&(f=a,u=e,s=r,n.substr(u-f.end.length-s,f.end.length)===f.end)&&0<e-r-(i=a).end.length-i.start.length)return a}(t,a,n.startOffset,o);if(i!==undefined){var f=a.lastIndexOf(i.end,n.startOffset-o),u=a.lastIndexOf(i.start,f-i.end.length);if(f=a.indexOf(i.end,u+i.start.length),-1!==u){var s=l.document.createRange();s.setStart(r,u),s.setEnd(r,f+i.end.length);var c=j(t,s.toString());if(!(i===undefined||c!==i||r.data.length<=i.start.length+i.end.length))return{pattern:i,startOffset:u,endOffset:f}}}}}},q=function(t){return t&&3===t.nodeType},L=function(t,n,e){var r=t.dom.createRng();r.setStart(n,e),r.setEnd(n,e),t.selection.setRng(r)},M=function(n,t,e){var r=n.selection.getRng();return h.from(E(t,r,e)).map(function(t){return function(a,o,i,f){var u=I.isArray(i.pattern.format)?i.pattern.format:[i.pattern.format];if(0!==I.grep(u,function(t){var n=a.formatter.get(t);return n&&n[0].inline}).length)return a.undoManager.transact(function(){var t,n,e,r;t=o,n=i.pattern,e=i.endOffset,r=i.startOffset,(t=0<r?t.splitText(r):t).splitText(e-r+n.end.length),t.deleteData(0,n.start.length),t.deleteData(t.data.length-n.end.length,n.end.length),o=t,f&&a.selection.setCursorLocation(o.nextSibling,1),u.forEach(function(t){a.formatter.apply(t,{},o)})}),o}(n,r.startContainer,t,e)})},U=function(s,t,c){var n=s.selection.getRng(),l=n.startContainer;n.collapsed&&q(l)&&function(t,n,e){for(var r=0;r<t.length;r++){var a=e.lastIndexOf(t[r].start,n);if(-1!==a)return h.some({pattern:t[r],startOffset:a})}return h.none()}(t,n.startOffset,l.data).each(function(t){var n,e,r,a,o,i,f,u=c?h.some((n=l,r=(e=t).startOffset+e.pattern.start.length,a=n.data.slice(r,r+1),n.deleteData(r,1),a)):h.none();o=s,f=t,(i=l).deleteData(f.startOffset,f.pattern.start.length),o.insertContent(f.pattern.replacement),h.from(i.nextSibling).filter(q).each(function(t){t.insertData(0,i.data),o.dom.remove(i)}),u.each(function(t){return function(t,n){var e=t.selection.getRng(),r=e.startContainer;if(q(r)){var a=e.startOffset;r.insertData(a,n),L(t,r,a+n.length)}else{var o=t.dom.doc.createTextNode(n);e.insertNode(o),L(t,o,o.length)}}(s,t)})})},_=function(t,n,e){for(var r=0;r<t.length;r++)if(e(t[r],n))return!0},z=function(t,n){var e,r,a,o;e=t,r=n.replacementPatterns,U(e,r,!1),a=t,o=n.inlinePatterns,M(a,o,!1).each(function(t){L(a,t,t.data.length)}),function(t,n){var e,r,a,o,i,f,u,s,c,l,d;if(e=t.selection,r=t.dom,e.isCollapsed()&&(u=r.getParent(e.getStart(),"p"))){for(c=new w(u,u);i=c.next();)if(q(i)){o=i;break}if(o){if(!(s=j(n,o.data)))return;if(a=(l=e.getRng(!0)).startContainer,d=l.startOffset,o===a&&(d=Math.max(0,d-s.start.length)),I.trim(o.data).length===s.start.length)return;s.format&&(f=t.formatter.get(s.format))&&f[0].block&&(o.deleteData(0,s.start.length),t.formatter.apply(s.format,{},o),l.setStart(a,d),l.collapse(!0),e.setRng(l)),s.cmd&&t.undoManager.transact(function(){o.deleteData(0,s.start.length),t.execCommand(s.cmd)})}}}(t,n.blockPatterns)},K=function(t,n){var e,r,a,o;e=t,r=n.replacementPatterns,U(e,r,!0),a=t,o=n.inlinePatterns,M(a,o,!0).each(function(t){var n=t.data.slice(-1);if(/[\u00a0 ]/.test(n)){t.deleteData(t.data.length-1,1);var e=a.dom.doc.createTextNode(n);a.dom.insertAfter(e,t.parentNode),L(a,e,1)}})},V=function(t,n){return _(t,n,function(t,n){return t.charCodeAt(0)===n.charCode})},W=function(t,n){return _(t,n,function(t,n){return t===n.keyCode&&!1===R.modifierPressed(n)})},B=function(n,e){var r=[",",".",";",":","!","?"],a=[32];n.on("keydown",function(t){13!==t.keyCode||R.modifierPressed(t)||z(n,e.get())},!0),n.on("keyup",function(t){W(a,t)&&K(n,e.get())}),n.on("keypress",function(t){V(r,t)&&N.setEditorTimeout(n,function(){K(n,e.get())})})};i.add("textpattern",function(t){var n=o(A(t.settings));return B(t,n),D(n)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/visualchars/plugin.min.js b/lib/web/tiny_mce_4/plugins/visualchars/plugin.min.js index 40995b37f500b..0a193452ebfdc 100644 --- a/lib/web/tiny_mce_4/plugins/visualchars/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/visualchars/plugin.min.js @@ -1 +1 @@ -!function(r){"use strict";var n,e,t,o,u,i,c=function(n){var e=n,t=function(){return e};return{get:t,set:function(n){e=n},clone:function(){return c(t())}}},a=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=function(n){return{isEnabled:function(){return n.get()}}},l=function(n,e){return n.fire("VisualChars",{state:e})},s={"\xa0":"nbsp","\xad":"shy"},d=function(n,e){var t,r="";for(t in n)r+=t;return new RegExp("["+r+"]",e?"g":"")},m=function(n){var e,t="";for(e in n)t&&(t+=","),t+="span.mce-"+n[e];return t},N={charMap:s,regExp:d(s),regExpGlobal:d(s,!0),selector:m(s),charMapToRegExp:d,charMapToSelector:m},g=function(n){return function(){return n}},E=g(!1),h=g(!0),v=E,T=h,p=function(){return O},O=(o={fold:function(n,e){return n()},is:v,isSome:v,isNone:T,getOr:t=function(n){return n},getOrThunk:e=function(n){return n()},getOrDie:function(n){throw new Error(n||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:t,orThunk:e,map:p,ap:p,each:function(){},bind:p,flatten:p,exists:v,forall:T,filter:p,equals:n=function(n){return n.isNone()},equals_:n,toArray:function(){return[]},toString:g("none()")},Object.freeze&&Object.freeze(o),o),y=function(t){var n=function(){return t},e=function(){return o},r=function(n){return n(t)},o={fold:function(n,e){return e(t)},is:function(n){return t===n},isSome:T,isNone:v,getOr:n,getOrThunk:n,getOrDie:n,getOrNull:n,getOrUndefined:n,or:e,orThunk:e,map:function(n){return y(n(t))},ap:function(n){return n.fold(p,function(n){return y(n(t))})},each:function(n){n(t)},bind:r,flatten:n,exists:r,forall:r,filter:function(n){return n(t)?o:O},equals:function(n){return n.is(t)},equals_:function(n,e){return n.fold(v,function(n){return e(t,n)})},toArray:function(){return[t]},toString:function(){return"some("+t+")"}};return o},D=function(n){return null===n||n===undefined?O:y(n)},_=(u="function",function(n){return function(n){if(null===n)return"null";var e=typeof n;return"object"===e&&(Array.prototype.isPrototypeOf(n)||n.constructor&&"Array"===n.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(n)||n.constructor&&"String"===n.constructor.name)?"string":e}(n)===u}),C=(Array.prototype.slice,function(n,e){for(var t=0,r=n.length;t<r;t++)e(n[t],t,n)}),M=(_(Array.from)&&Array.from,function(n){if(null===n||n===undefined)throw new Error("Node cannot be null or undefined");return{dom:g(n)}}),b={fromHtml:function(n,e){var t=(e||r.document).createElement("div");if(t.innerHTML=n,!t.hasChildNodes()||1<t.childNodes.length)throw r.console.error("HTML does not have a single root node",n),new Error("HTML must have a single root node");return M(t.childNodes[0])},fromTag:function(n,e){var t=(e||r.document).createElement(n);return M(t)},fromText:function(n,e){var t=(e||r.document).createTextNode(n);return M(t)},fromDom:M,fromPoint:function(n,e,t){var r=n.dom();return D(r.elementFromPoint(e,t)).map(M)}},k=(r.Node.ATTRIBUTE_NODE,r.Node.CDATA_SECTION_NODE,r.Node.COMMENT_NODE,r.Node.DOCUMENT_NODE,r.Node.DOCUMENT_TYPE_NODE,r.Node.DOCUMENT_FRAGMENT_NODE,r.Node.ELEMENT_NODE,r.Node.TEXT_NODE),S=(r.Node.PROCESSING_INSTRUCTION_NODE,r.Node.ENTITY_REFERENCE_NODE,r.Node.ENTITY_NODE,r.Node.NOTATION_NODE,function(n){return n.dom().nodeValue}),w=(i=k,function(n){return n.dom().nodeType===i}),A=function(n){return'<span data-mce-bogus="1" class="mce-'+N.charMap[n]+'">'+n+"</span>"},x=function(n,e){var t=[],r=function(n,e){for(var t=n.length,r=new Array(t),o=0;o<t;o++){var u=n[o];r[o]=e(u,o,n)}return r}(n.dom().childNodes,b.fromDom);return C(r,function(n){e(n)&&(t=t.concat([n])),t=t.concat(x(n,e))}),t},P={isMatch:function(n){return w(n)&&S(n)!==undefined&&N.regExp.test(S(n))},filterDescendants:x,findParentElm:function(n,e){for(;n.parentNode;){if(n.parentNode===e)return n;n=n.parentNode}},replaceWithSpans:function(n){return n.replace(N.regExpGlobal,A)}},R=function(t,n){var r,o,e=P.filterDescendants(b.fromDom(n),P.isMatch);C(e,function(n){var e=P.replaceWithSpans(S(n));for(o=t.dom.create("div",null,e);r=o.lastChild;)t.dom.insertAfter(r,n.dom());t.dom.remove(n.dom())})},I=function(e,n){var t=e.dom.select(N.selector,n);C(t,function(n){e.dom.remove(n,1)})},B=R,U=I,V=function(n){var e=n.getBody(),t=n.selection.getBookmark(),r=P.findParentElm(n.selection.getNode(),e);r=r!==undefined?r:e,I(n,r),R(n,r),n.selection.moveToBookmark(t)},j=function(n,e){var t,r=n.getBody(),o=n.selection;e.set(!e.get()),l(n,e.get()),t=o.getBookmark(),!0===e.get()?B(n,r):U(n,r),o.moveToBookmark(t)},q=function(n,e){n.addCommand("mceVisualChars",function(){j(n,e)})},G=tinymce.util.Tools.resolve("tinymce.util.Delay"),H=function(e,t){var r=G.debounce(function(){V(e)},300);!1!==e.settings.forced_root_block&&e.on("keydown",function(n){!0===t.get()&&(13===n.keyCode?V(e):r())})},L=function(n){return n.getParam("visualchars_default_state",!1)},F=function(e,t){e.on("init",function(){var n=!L(e);t.set(n),j(e,t)})},Y=function(t){return function(n){var e=n.control;t.on("VisualChars",function(n){e.active(n.state)})}};a.add("visualchars",function(n){var e,t=c(!1);return q(n,t),(e=n).addButton("visualchars",{active:!1,title:"Show invisible characters",cmd:"mceVisualChars",onPostRender:Y(e)}),e.addMenuItem("visualchars",{text:"Show invisible characters",cmd:"mceVisualChars",onPostRender:Y(e),selectable:!0,context:"view",prependToContext:!0}),H(n,t),F(n,t),f(t)})}(window); \ No newline at end of file +!function(r){"use strict";var n,e,t,o,i,u,c=function(n){var e=n,t=function(){return e};return{get:t,set:function(n){e=n},clone:function(){return c(t())}}},a=tinymce.util.Tools.resolve("tinymce.PluginManager"),f=function(n){return{isEnabled:function(){return n.get()}}},d=function(n,e){return n.fire("VisualChars",{state:e})},s=function(){},l=function(n){return function(){return n}},m=l(!1),N=l(!0),g=function(){return E},E=(n=function(n){return n.isNone()},o={fold:function(n,e){return n()},is:m,isSome:m,isNone:N,getOr:t=function(n){return n},getOrThunk:e=function(n){return n()},getOrDie:function(n){throw new Error(n||"error: getOrDie called on none.")},getOrNull:l(null),getOrUndefined:l(undefined),or:t,orThunk:e,map:g,each:s,bind:g,exists:m,forall:N,filter:g,equals:n,equals_:n,toArray:function(){return[]},toString:l("none()")},Object.freeze&&Object.freeze(o),o),h=function(t){var n=l(t),e=function(){return o},r=function(n){return n(t)},o={fold:function(n,e){return e(t)},is:function(n){return t===n},isSome:N,isNone:m,getOr:n,getOrThunk:n,getOrDie:n,getOrNull:n,getOrUndefined:n,or:e,orThunk:e,map:function(n){return h(n(t))},each:function(n){n(t)},bind:r,exists:r,forall:r,filter:function(n){return n(t)?o:E},toArray:function(){return[t]},toString:function(){return"some("+t+")"},equals:function(n){return n.is(t)},equals_:function(n,e){return n.fold(m,function(n){return e(t,n)})}};return o},v=function(n){return null===n||n===undefined?E:h(n)},T=(i="function",function(n){return function(n){if(null===n)return"null";var e=typeof n;return"object"===e&&(Array.prototype.isPrototypeOf(n)||n.constructor&&"Array"===n.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(n)||n.constructor&&"String"===n.constructor.name)?"string":e}(n)===i}),p=(Array.prototype.slice,function(n,e){for(var t=0,r=n.length;t<r;t++)e(n[t],t)}),O=(T(Array.from)&&Array.from,function(n){if(null===n||n===undefined)throw new Error("Node cannot be null or undefined");return{dom:l(n)}}),y={fromHtml:function(n,e){var t=(e||r.document).createElement("div");if(t.innerHTML=n,!t.hasChildNodes()||1<t.childNodes.length)throw r.console.error("HTML does not have a single root node",n),new Error("HTML must have a single root node");return O(t.childNodes[0])},fromTag:function(n,e){var t=(e||r.document).createElement(n);return O(t)},fromText:function(n,e){var t=(e||r.document).createTextNode(n);return O(t)},fromDom:O,fromPoint:function(n,e,t){var r=n.dom();return v(r.elementFromPoint(e,t)).map(O)}},D=(r.Node.ATTRIBUTE_NODE,r.Node.CDATA_SECTION_NODE,r.Node.COMMENT_NODE,r.Node.DOCUMENT_NODE,r.Node.DOCUMENT_TYPE_NODE,r.Node.DOCUMENT_FRAGMENT_NODE,r.Node.ELEMENT_NODE,r.Node.TEXT_NODE),_=(r.Node.PROCESSING_INSTRUCTION_NODE,r.Node.ENTITY_REFERENCE_NODE,r.Node.ENTITY_NODE,r.Node.NOTATION_NODE,"undefined"!=typeof r.window?r.window:Function("return this;")(),function(n){return n.dom().nodeValue}),C=(u=D,function(n){return n.dom().nodeType===u}),w={"\xa0":"nbsp","\xad":"shy"},M=function(n,e){var t,r="";for(t in n)r+=t;return new RegExp("["+r+"]",e?"g":"")},b=function(n){var e,t="";for(e in n)t&&(t+=","),t+="span.mce-"+n[e];return t},k={charMap:w,regExp:M(w),regExpGlobal:M(w,!0),selector:b(w),charMapToRegExp:M,charMapToSelector:b},S=function(n){return'<span data-mce-bogus="1" class="mce-'+k.charMap[n]+'">'+n+"</span>"},A=function(n,e){var t=[],r=function(n,e){for(var t=n.length,r=new Array(t),o=0;o<t;o++){var i=n[o];r[o]=e(i,o)}return r}(n.dom().childNodes,y.fromDom);return p(r,function(n){e(n)&&(t=t.concat([n])),t=t.concat(A(n,e))}),t},x={isMatch:function(n){var e=_(n);return C(n)&&e!==undefined&&k.regExp.test(e)},filterDescendants:A,findParentElm:function(n,e){for(;n.parentNode;){if(n.parentNode===e)return n;n=n.parentNode}},replaceWithSpans:function(n){return n.replace(k.regExpGlobal,S)}},P=function(t,n){var r,o,e=x.filterDescendants(y.fromDom(n),x.isMatch);p(e,function(n){var e=x.replaceWithSpans(t.dom.encode(_(n)));for(o=t.dom.create("div",null,e);r=o.lastChild;)t.dom.insertAfter(r,n.dom());t.dom.remove(n.dom())})},R=function(e,n){var t=e.dom.select(k.selector,n);p(t,function(n){e.dom.remove(n,1)})},I=P,B=R,U=function(n){var e=n.getBody(),t=n.selection.getBookmark(),r=x.findParentElm(n.selection.getNode(),e);r=r!==undefined?r:e,R(n,r),P(n,r),n.selection.moveToBookmark(t)},V=function(n,e){var t,r=n.getBody(),o=n.selection;e.set(!e.get()),d(n,e.get()),t=o.getBookmark(),!0===e.get()?I(n,r):B(n,r),o.moveToBookmark(t)},j=function(n,e){n.addCommand("mceVisualChars",function(){V(n,e)})},q=tinymce.util.Tools.resolve("tinymce.util.Delay"),F=function(e,t){var r=q.debounce(function(){U(e)},300);!1!==e.settings.forced_root_block&&e.on("keydown",function(n){!0===t.get()&&(13===n.keyCode?U(e):r())})},G=function(n){return n.getParam("visualchars_default_state",!1)},H=function(e,t){e.on("init",function(){var n=!G(e);t.set(n),V(e,t)})},L=function(t){return function(n){var e=n.control;t.on("VisualChars",function(n){e.active(n.state)})}};a.add("visualchars",function(n){var e,t=c(!1);return j(n,t),(e=n).addButton("visualchars",{active:!1,title:"Show invisible characters",cmd:"mceVisualChars",onPostRender:L(e)}),e.addMenuItem("visualchars",{text:"Show invisible characters",cmd:"mceVisualChars",onPostRender:L(e),selectable:!0,context:"view",prependToContext:!0}),F(n,t),H(n,t),f(t)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/plugins/wordcount/plugin.min.js b/lib/web/tiny_mce_4/plugins/wordcount/plugin.min.js index 6d04a7b355c76..a05c4c1b88921 100644 --- a/lib/web/tiny_mce_4/plugins/wordcount/plugin.min.js +++ b/lib/web/tiny_mce_4/plugins/wordcount/plugin.min.js @@ -1 +1 @@ -!function(){"use strict";var e,n,t,r,o,u=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),i=tinymce.util.Tools.resolve("tinymce.Env"),E="[-'\\.\u2018\u2019\u2024\ufe52\uff07\uff0e]",T="[:\xb7\xb7\u05f4\u2027\ufe13\ufe55\uff1a]",a="[\xb1+*/,;;\u0589\u060c\u060d\u066c\u07f8\u2044\ufe10\ufe14\ufe50\ufe54\uff0c\uff1b]",f="[0-9\u0660-\u0669\u066b\u06f0-\u06f9\u07c0-\u07c9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be6-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29\u1040-\u1049\u1090-\u1099\u17e0-\u17e9\u1810-\u1819\u1946-\u194f\u19d0-\u19d9\u1a80-\u1a89\u1a90-\u1a99\u1b50-\u1b59\u1bb0-\u1bb9\u1c40-\u1c49\u1c50-\u1c59\ua620-\ua629\ua8d0-\ua8d9\ua900-\ua909\ua9d0-\ua9d9\uaa50-\uaa59\uabf0-\uabf9]",s="\\r",l="\\n",A="[\x0B\f\x85\u2028\u2029]",N="[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d02\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f\u109a-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b6-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u192b\u1930-\u193b\u19b0-\u19c0\u19c8\u19c9\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f\u1b00-\u1b04\u1b34-\u1b44\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1baa\u1be6-\u1bf3\u1c24-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2\u1dc0-\u1de6\u1dfc-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa7b\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe3-\uabea\uabec\uabed\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]",R="[\xad\u0600-\u0603\u06dd\u070f\u17b4\u17b5\u200e\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff\ufff9-\ufffb]",d="[\u3031-\u3035\u309b\u309c\u30a0-\u30fa\u30fc-\u30ff\u31f0-\u31ff\u32d0-\u32fe\u3300-\u3357\uff66-\uff9d]",g="[=_\u203f\u2040\u2054\ufe33\ufe34\ufe4d-\ufe4f\uff3f\u2200-\u22ff<>]",p="[!-#%-*,-\\/:;?@\\[-\\]_{}\xa1\xab\xb7\xbb\xbf;\xb7\u055a-\u055f\u0589\u058a\u05be\u05c0\u05c3\u05c6\u05f3\u05f4\u0609\u060a\u060c\u060d\u061b\u061e\u061f\u066a-\u066d\u06d4\u0700-\u070d\u07f7-\u07f9\u0830-\u083e\u085e\u0964\u0965\u0970\u0df4\u0e4f\u0e5a\u0e5b\u0f04-\u0f12\u0f3a-\u0f3d\u0f85\u0fd0-\u0fd4\u0fd9\u0fda\u104a-\u104f\u10fb\u1361-\u1368\u1400\u166d\u166e\u169b\u169c\u16eb-\u16ed\u1735\u1736\u17d4-\u17d6\u17d8-\u17da\u1800-\u180a\u1944\u1945\u1a1e\u1a1f\u1aa0-\u1aa6\u1aa8-\u1aad\u1b5a-\u1b60\u1bfc-\u1bff\u1c3b-\u1c3f\u1c7e\u1c7f\u1cd3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205e\u207d\u207e\u208d\u208e\u3008\u3009\u2768-\u2775\u27c5\u27c6\u27e6-\u27ef\u2983-\u2998\u29d8-\u29db\u29fc\u29fd\u2cf9-\u2cfc\u2cfe\u2cff\u2d70\u2e00-\u2e2e\u2e30\u2e31\u3001-\u3003\u3008-\u3011\u3014-\u301f\u3030\u303d\u30a0\u30fb\ua4fe\ua4ff\ua60d-\ua60f\ua673\ua67e\ua6f2-\ua6f7\ua874-\ua877\ua8ce\ua8cf\ua8f8-\ua8fa\ua92e\ua92f\ua95f\ua9c1-\ua9cd\ua9de\ua9df\uaa5c-\uaa5f\uaade\uaadf\uabeb\ufd3e\ufd3f\ufe10-\ufe19\ufe30-\ufe52\ufe54-\ufe61\ufe63\ufe68\ufe6a\ufe6b\uff01-\uff03\uff05-\uff0a\uff0c-\uff0f\uff1a\uff1b\uff1f\uff20\uff3b-\uff3d\uff3f\uff5b\uff5d\uff5f-\uff65]",M={characterIndices:{ALETTER:0,MIDNUMLET:1,MIDLETTER:2,MIDNUM:3,NUMERIC:4,CR:5,LF:6,NEWLINE:7,EXTEND:8,FORMAT:9,KATAKANA:10,EXTENDNUMLET:11,AT:12,OTHER:13},SETS:[new RegExp("[A-Za-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f3\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bc0-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u24b6-\u24e9\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005\u303b\u303c\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790\ua791\ua7a0-\ua7a9\ua7fa-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uffa0-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]"),new RegExp(E),new RegExp(T),new RegExp(a),new RegExp(f),new RegExp(s),new RegExp(l),new RegExp(A),new RegExp(N),new RegExp(R),new RegExp(d),new RegExp(g),new RegExp("@")],EMPTY_STRING:"",PUNCTUATION:new RegExp("^"+p+"$"),WHITESPACE:/^\s+$/},I=function(e){return function(){return e}},L=I(!1),m=I(!0),y=function(){return h},h=(r={fold:function(e,n){return e()},is:L,isSome:L,isNone:m,getOr:t=function(e){return e},getOrThunk:n=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:t,orThunk:n,map:y,ap:y,each:function(){},bind:y,flatten:y,exists:L,forall:m,filter:y,equals:e=function(e){return e.isNone()},equals_:e,toArray:function(){return[]},toString:I("none()")},Object.freeze&&Object.freeze(r),r),w=(o="function",function(e){return function(e){if(null===e)return"null";var n=typeof e;return"object"===n&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===n&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":n}(e)===o}),U=(Array.prototype.slice,w(Array.from)&&Array.from,M.SETS),v=M.characterIndices.OTHER,x=function(e){var n,t,r=v,o=U.length;for(n=0;n<o;++n)if((t=U[n])&&t.test(e)){r=n;break}return r},D=function(e){var t,r,n=(t=x,r={},function(e){if(r[e])return r[e];var n=t(e);return r[e]=n});return function(e,n){for(var t=e.length,r=new Array(t),o=0;o<t;o++){var u=e[o];r[o]=n(u,o,e)}return r}(e.split(""),n)},C=M.characterIndices,O=function(e,n){var t,r,o=e[n],u=e[n+1];return!(n<0||n>e.length-1&&0!==n||o===C.ALETTER&&u===C.ALETTER||(r=e[n+2],o===C.ALETTER&&(u===C.MIDLETTER||u===C.MIDNUMLET||u===C.AT)&&r===C.ALETTER||(t=e[n-1],(o===C.MIDLETTER||o===C.MIDNUMLET||u===C.AT)&&u===C.ALETTER&&t===C.ALETTER||!(o!==C.NUMERIC&&o!==C.ALETTER||u!==C.NUMERIC&&u!==C.ALETTER)||(o===C.MIDNUM||o===C.MIDNUMLET)&&u===C.NUMERIC&&t===C.NUMERIC||o===C.NUMERIC&&(u===C.MIDNUM||u===C.MIDNUMLET)&&r===C.NUMERIC||o===C.EXTEND||o===C.FORMAT||t===C.EXTEND||t===C.FORMAT||u===C.EXTEND||u===C.FORMAT||o===C.CR&&u===C.LF||o!==C.NEWLINE&&o!==C.CR&&o!==C.LF&&u!==C.NEWLINE&&u!==C.CR&&u!==C.LF&&(o===C.KATAKANA&&u===C.KATAKANA||u===C.EXTENDNUMLET&&(o===C.ALETTER||o===C.NUMERIC||o===C.KATAKANA||o===C.EXTENDNUMLET)||o===C.EXTENDNUMLET&&(u===C.ALETTER||u===C.NUMERIC||u===C.KATAKANA)||o===C.AT))))},b=M.EMPTY_STRING,S=M.WHITESPACE,K=M.PUNCTUATION,P=function(e,n,t){var r=function(e,n){var t;for(t=n;t<e.length;++t){var r=e.charAt(t);if(S.test(r))break}return t}(n,t+1),o=n.substring(t+1,r);return"://"===o.substr(0,3)?{word:e+o,index:r}:{word:e,index:t}},F=function(e,n){return function(e,n){var t,r,o,u,i=0,E=D(e),c=E.length,T=[],a=[];for(n||(n={}),n.ignoreCase&&(e=e.toLowerCase()),r=n.includePunctuation,o=n.includeWhitespace;i<c;++i)if(t=e.charAt(i),T.push(t),O(E,i)){if((T=T.join(b))&&(o||!S.test(T))&&(r||!K.test(T)))if("http"===(u=T)||"https"===u){var f=P(T,e,i);a.push(f.word),i=f.index}else a.push(T);T=[]}return a}(e.replace(/\ufeff/g,""),n)},W=function(e,n){return i.ie?function(e,n){for(var t,r=n.getBlockElements(),o=n.getShortEndedElements(),u=n.getWhiteSpaceElements(),i="",E=new c(e,e);e=E.next();)3===e.nodeType?i+=e.data:(r[(t=e).nodeName]||o[t.nodeName]||u[t.nodeName])&&(i+=" ");return i}(e,n):e.innerText},X=function(e){return F((n=e,n.removed?"":W(n.getBody(),n.schema))).length;var n},k=function(e){return{getCount:function(){return X(e)}}},j=tinymce.util.Tools.resolve("tinymce.util.Delay"),_=tinymce.util.Tools.resolve("tinymce.util.I18n"),H=function(t){var r=function(e){return _.translate(["{0} words",X(e)])},o=function(){t.theme.panel.find("#wordcount").text(r(t))};t.on("init",function(){var e=t.theme.panel&&t.theme.panel.find("#statusbar")[0],n=j.debounce(o,300);e&&j.setEditorTimeout(t,function(){e.insert({type:"label",name:"wordcount",text:r(t),classes:"wordcount",disabled:t.settings.readonly},0),t.on("setcontent beforeaddundo undo redo keyup",n)},0)})};u.add("wordcount",function(e){return H(e),k(e)})}(); \ No newline at end of file +!function(){"use strict";var e,n,t,r,o,u=tinymce.util.Tools.resolve("tinymce.PluginManager"),c=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),i=tinymce.util.Tools.resolve("tinymce.Env"),E="[-'\\.\u2018\u2019\u2024\ufe52\uff07\uff0e]",T="[:\xb7\xb7\u05f4\u2027\ufe13\ufe55\uff1a]",a="[\xb1+*/,;;\u0589\u060c\u060d\u066c\u07f8\u2044\ufe10\ufe14\ufe50\ufe54\uff0c\uff1b]",f="[0-9\u0660-\u0669\u066b\u06f0-\u06f9\u07c0-\u07c9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be6-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29\u1040-\u1049\u1090-\u1099\u17e0-\u17e9\u1810-\u1819\u1946-\u194f\u19d0-\u19d9\u1a80-\u1a89\u1a90-\u1a99\u1b50-\u1b59\u1bb0-\u1bb9\u1c40-\u1c49\u1c50-\u1c59\ua620-\ua629\ua8d0-\ua8d9\ua900-\ua909\ua9d0-\ua9d9\uaa50-\uaa59\uabf0-\uabf9]",s="\\r",l="\\n",A="[\x0B\f\x85\u2028\u2029]",N="[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d02\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f\u109a-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b6-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u192b\u1930-\u193b\u19b0-\u19c0\u19c8\u19c9\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f\u1b00-\u1b04\u1b34-\u1b44\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1baa\u1be6-\u1bf3\u1c24-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2\u1dc0-\u1de6\u1dfc-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa7b\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe3-\uabea\uabec\uabed\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]",R="[\xad\u0600-\u0603\u06dd\u070f\u17b4\u17b5\u200e\u200f\u202a-\u202e\u2060-\u2064\u206a-\u206f\ufeff\ufff9-\ufffb]",d="[\u3031-\u3035\u309b\u309c\u30a0-\u30fa\u30fc-\u30ff\u31f0-\u31ff\u32d0-\u32fe\u3300-\u3357\uff66-\uff9d]",g="[=_\u203f\u2040\u2054\ufe33\ufe34\ufe4d-\ufe4f\uff3f\u2200-\u22ff<>]",p="[!-#%-*,-\\/:;?@\\[-\\]_{}\xa1\xab\xb7\xbb\xbf;\xb7\u055a-\u055f\u0589\u058a\u05be\u05c0\u05c3\u05c6\u05f3\u05f4\u0609\u060a\u060c\u060d\u061b\u061e\u061f\u066a-\u066d\u06d4\u0700-\u070d\u07f7-\u07f9\u0830-\u083e\u085e\u0964\u0965\u0970\u0df4\u0e4f\u0e5a\u0e5b\u0f04-\u0f12\u0f3a-\u0f3d\u0f85\u0fd0-\u0fd4\u0fd9\u0fda\u104a-\u104f\u10fb\u1361-\u1368\u1400\u166d\u166e\u169b\u169c\u16eb-\u16ed\u1735\u1736\u17d4-\u17d6\u17d8-\u17da\u1800-\u180a\u1944\u1945\u1a1e\u1a1f\u1aa0-\u1aa6\u1aa8-\u1aad\u1b5a-\u1b60\u1bfc-\u1bff\u1c3b-\u1c3f\u1c7e\u1c7f\u1cd3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205e\u207d\u207e\u208d\u208e\u3008\u3009\u2768-\u2775\u27c5\u27c6\u27e6-\u27ef\u2983-\u2998\u29d8-\u29db\u29fc\u29fd\u2cf9-\u2cfc\u2cfe\u2cff\u2d70\u2e00-\u2e2e\u2e30\u2e31\u3001-\u3003\u3008-\u3011\u3014-\u301f\u3030\u303d\u30a0\u30fb\ua4fe\ua4ff\ua60d-\ua60f\ua673\ua67e\ua6f2-\ua6f7\ua874-\ua877\ua8ce\ua8cf\ua8f8-\ua8fa\ua92e\ua92f\ua95f\ua9c1-\ua9cd\ua9de\ua9df\uaa5c-\uaa5f\uaade\uaadf\uabeb\ufd3e\ufd3f\ufe10-\ufe19\ufe30-\ufe52\ufe54-\ufe61\ufe63\ufe68\ufe6a\ufe6b\uff01-\uff03\uff05-\uff0a\uff0c-\uff0f\uff1a\uff1b\uff1f\uff20\uff3b-\uff3d\uff3f\uff5b\uff5d\uff5f-\uff65]",M={characterIndices:{ALETTER:0,MIDNUMLET:1,MIDLETTER:2,MIDNUM:3,NUMERIC:4,CR:5,LF:6,NEWLINE:7,EXTEND:8,FORMAT:9,KATAKANA:10,EXTENDNUMLET:11,AT:12,OTHER:13},SETS:[new RegExp("[A-Za-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05f3\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bc0-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u24b6-\u24e9\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005\u303b\u303c\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790\ua791\ua7a0-\ua7a9\ua7fa-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uffa0-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]"),new RegExp(E),new RegExp(T),new RegExp(a),new RegExp(f),new RegExp(s),new RegExp(l),new RegExp(A),new RegExp(N),new RegExp(R),new RegExp(d),new RegExp(g),new RegExp("@")],EMPTY_STRING:"",PUNCTUATION:new RegExp("^"+p+"$"),WHITESPACE:/^\s+$/},I=function(){},L=function(e){return function(){return e}},m=L(!1),y=L(!0),h=function(){return w},w=(e=function(e){return e.isNone()},r={fold:function(e,n){return e()},is:m,isSome:m,isNone:y,getOr:t=function(e){return e},getOrThunk:n=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:L(null),getOrUndefined:L(undefined),or:t,orThunk:n,map:h,each:I,bind:h,exists:m,forall:y,filter:h,equals:e,equals_:e,toArray:function(){return[]},toString:L("none()")},Object.freeze&&Object.freeze(r),r),U=(o="function",function(e){return function(e){if(null===e)return"null";var n=typeof e;return"object"===n&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===n&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":n}(e)===o}),v=(Array.prototype.slice,U(Array.from)&&Array.from,M.SETS),x=M.characterIndices.OTHER,D=function(e){var n,t,r=x,o=v.length;for(n=0;n<o;++n)if((t=v[n])&&t.test(e)){r=n;break}return r},C=function(e){var t,r,n=(t=D,r={},function(e){if(r[e])return r[e];var n=t(e);return r[e]=n});return function(e,n){for(var t=e.length,r=new Array(t),o=0;o<t;o++){var u=e[o];r[o]=n(u,o)}return r}(e.split(""),n)},O=M.characterIndices,b=function(e,n){var t,r,o=e[n],u=e[n+1];return!(n<0||n>e.length-1&&0!==n||o===O.ALETTER&&u===O.ALETTER||(r=e[n+2],o===O.ALETTER&&(u===O.MIDLETTER||u===O.MIDNUMLET||u===O.AT)&&r===O.ALETTER||(t=e[n-1],(o===O.MIDLETTER||o===O.MIDNUMLET||u===O.AT)&&u===O.ALETTER&&t===O.ALETTER||!(o!==O.NUMERIC&&o!==O.ALETTER||u!==O.NUMERIC&&u!==O.ALETTER)||(o===O.MIDNUM||o===O.MIDNUMLET)&&u===O.NUMERIC&&t===O.NUMERIC||o===O.NUMERIC&&(u===O.MIDNUM||u===O.MIDNUMLET)&&r===O.NUMERIC||o===O.EXTEND||o===O.FORMAT||t===O.EXTEND||t===O.FORMAT||u===O.EXTEND||u===O.FORMAT||o===O.CR&&u===O.LF||o!==O.NEWLINE&&o!==O.CR&&o!==O.LF&&u!==O.NEWLINE&&u!==O.CR&&u!==O.LF&&(o===O.KATAKANA&&u===O.KATAKANA||u===O.EXTENDNUMLET&&(o===O.ALETTER||o===O.NUMERIC||o===O.KATAKANA||o===O.EXTENDNUMLET)||o===O.EXTENDNUMLET&&(u===O.ALETTER||u===O.NUMERIC||u===O.KATAKANA)||o===O.AT))))},S=M.EMPTY_STRING,K=M.WHITESPACE,P=M.PUNCTUATION,F=function(e,n,t){var r=function(e,n){var t;for(t=n;t<e.length;++t){var r=e.charAt(t);if(K.test(r))break}return t}(n,t+1),o=n.substring(t+1,r);return"://"===o.substr(0,3)?{word:e+o,index:r}:{word:e,index:t}},W=function(e,n){return function(e,n){var t,r,o,u,i=0,E=C(e),c=E.length,T=[],a=[];for(n||(n={}),n.ignoreCase&&(e=e.toLowerCase()),r=n.includePunctuation,o=n.includeWhitespace;i<c;++i)if(t=e.charAt(i),T.push(t),b(E,i)){if((T=T.join(S))&&(o||!K.test(T))&&(r||!P.test(T)))if("http"===(u=T)||"https"===u){var f=F(T,e,i);a.push(f.word),i=f.index}else a.push(T);T=[]}return a}(e.replace(/\ufeff/g,""),n)},X=function(e,n){return i.ie?function(e,n){for(var t,r=n.getBlockElements(),o=n.getShortEndedElements(),u=n.getWhiteSpaceElements(),i="",E=new c(e,e);e=E.next();)3===e.nodeType?i+=e.data:(r[(t=e).nodeName]||o[t.nodeName]||u[t.nodeName])&&(i+=" ");return i}(e,n):e.innerText},k=function(e){return W((n=e,n.removed?"":X(n.getBody(),n.schema))).length;var n},j=function(e){return{getCount:function(){return k(e)}}},_=tinymce.util.Tools.resolve("tinymce.util.Delay"),H=tinymce.util.Tools.resolve("tinymce.util.I18n"),z=function(t){var r=function(e){return H.translate(["{0} words",k(e)])},o=function(){t.theme.panel.find("#wordcount").text(r(t))};t.on("init",function(){var e=t.theme.panel&&t.theme.panel.find("#statusbar")[0],n=_.debounce(o,300);e&&_.setEditorTimeout(t,function(){e.insert({type:"label",name:"wordcount",text:r(t),classes:"wordcount",disabled:t.settings.readonly},0),t.on("setcontent beforeaddundo undo redo keyup",n)},0)})};u.add("wordcount",function(e){return z(e),j(e)})}(); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/skins/lightgray/content.inline.min.css b/lib/web/tiny_mce_4/skins/lightgray/content.inline.min.css index e4a77ff459acd..aa3697c6f7603 100644 --- a/lib/web/tiny_mce_4/skins/lightgray/content.inline.min.css +++ b/lib/web/tiny_mce_4/skins/lightgray/content.inline.min.css @@ -1 +1 @@ -.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid rgba(208,2,27,0.5);cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#2276d2 !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2276d2}.mce-content-body.mce-content-readonly *[contentEditable=true]:focus,.mce-content-body.mce-content-readonly *[contentEditable=true]:hover{outline:none}.mce-content-body *[data-mce-selected="inline-boundary"]{background:#bfe6ff}.mce-content-body .mce-item-anchor[data-mce-selected]{background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-content-body hr{cursor:default}.mce-content-body table{-webkit-nbsp-mode:normal}.ephox-snooker-resizer-bar{background-color:#2276d2;opacity:0}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:.2}.mce-content-body{line-height:1.3} \ No newline at end of file +.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid rgba(208,2,27,0.5);cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#2276d2 !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2276d2}.mce-content-body.mce-content-readonly *[contentEditable=true]:focus,.mce-content-body.mce-content-readonly *[contentEditable=true]:hover{outline:none}.mce-content-body *[data-mce-selected="inline-boundary"]{background:#bfe6ff}.mce-content-body .mce-item-anchor[data-mce-selected]{background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-content-body hr{cursor:default}.mce-content-body table{-webkit-nbsp-mode:normal}.ephox-snooker-resizer-bar{background-color:#2276d2;opacity:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:.2}.mce-content-body{line-height:1.3} \ No newline at end of file diff --git a/lib/web/tiny_mce_4/skins/lightgray/content.min.css b/lib/web/tiny_mce_4/skins/lightgray/content.min.css index 1434177df569b..c04313684de9b 100644 --- a/lib/web/tiny_mce_4/skins/lightgray/content.min.css +++ b/lib/web/tiny_mce_4/skins/lightgray/content.min.css @@ -1 +1 @@ -body{background-color:#FFFFFF;color:#000000;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px;line-height:1.3;scrollbar-3dlight-color:#F0F0EE;scrollbar-arrow-color:#676662;scrollbar-base-color:#F0F0EE;scrollbar-darkshadow-color:#DDDDDD;scrollbar-face-color:#E0E0DD;scrollbar-highlight-color:#F0F0EE;scrollbar-shadow-color:#F0F0EE;scrollbar-track-color:#F5F5F5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px}.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid rgba(208,2,27,0.5);cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#2276d2 !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2276d2}.mce-content-body.mce-content-readonly *[contentEditable=true]:focus,.mce-content-body.mce-content-readonly *[contentEditable=true]:hover{outline:none}.mce-content-body *[data-mce-selected="inline-boundary"]{background:#bfe6ff}.mce-content-body .mce-item-anchor[data-mce-selected]{background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-content-body hr{cursor:default}.mce-content-body table{-webkit-nbsp-mode:normal}.ephox-snooker-resizer-bar{background-color:#2276d2;opacity:0}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:.2} \ No newline at end of file +body{background-color:#FFFFFF;color:#000000;font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px;line-height:1.3;scrollbar-3dlight-color:#F0F0EE;scrollbar-arrow-color:#676662;scrollbar-base-color:#F0F0EE;scrollbar-darkshadow-color:#DDDDDD;scrollbar-face-color:#E0E0DD;scrollbar-highlight-color:#F0F0EE;scrollbar-shadow-color:#F0F0EE;scrollbar-track-color:#F5F5F5}td,th{font-family:Verdana,Arial,Helvetica,sans-serif;font-size:14px}.word-wrap{word-wrap:break-word;-ms-word-break:break-all;word-break:break-all;word-break:break-word;-ms-hyphens:auto;-moz-hyphens:auto;-webkit-hyphens:auto;hyphens:auto}.mce-content-body .mce-reset{margin:0;padding:0;border:0;outline:0;vertical-align:top;background:transparent;text-decoration:none;color:black;font-family:Arial;font-size:11px;text-shadow:none;float:none;position:static;width:auto;height:auto;white-space:nowrap;cursor:inherit;line-height:normal;font-weight:normal;text-align:left;-webkit-tap-highlight-color:transparent;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box;direction:ltr;max-width:none}.mce-object{border:1px dotted #3A3A3A;background:#D5D5D5 url(img/object.gif) no-repeat center}.mce-preview-object{display:inline-block;position:relative;margin:0 2px 0 2px;line-height:0;border:1px solid gray}.mce-preview-object[data-mce-selected="2"] .mce-shim{display:none}.mce-preview-object .mce-shim{position:absolute;top:0;left:0;width:100%;height:100%;background:url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)}figure.align-left{float:left}figure.align-right{float:right}figure.image.align-center{display:table;margin-left:auto;margin-right:auto}figure.image{display:inline-block;border:1px solid gray;margin:0 2px 0 1px;background:#f5f2f0}figure.image img{margin:8px 8px 0 8px}figure.image figcaption{margin:6px 8px 6px 8px;text-align:center}.mce-toc{border:1px solid gray}.mce-toc h2{margin:4px}.mce-toc li{list-style-type:none}.mce-pagebreak{cursor:default;display:block;border:0;width:100%;height:5px;border:1px dashed #666;margin-top:15px;page-break-before:always}@media print{.mce-pagebreak{border:0}}.mce-item-anchor{cursor:default;display:inline-block;-webkit-user-select:all;-webkit-user-modify:read-only;-moz-user-select:all;-moz-user-modify:read-only;user-select:all;user-modify:read-only;width:9px !important;height:9px !important;border:1px dotted #3A3A3A;background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-nbsp,.mce-shy{background:#AAA}.mce-shy::after{content:'-'}.mce-match-marker{background:#AAA;color:#fff}.mce-match-marker-selected{background:#3399ff;color:#fff}.mce-spellchecker-word{border-bottom:2px solid rgba(208,2,27,0.5);cursor:default}.mce-spellchecker-grammar{border-bottom:2px solid #008000;cursor:default}.mce-item-table,.mce-item-table td,.mce-item-table th,.mce-item-table caption{border:1px dashed #BBB}td[data-mce-selected],th[data-mce-selected]{background-color:#2276d2 !important}.mce-edit-focus{outline:1px dotted #333}.mce-content-body *[contentEditable=false] *[contentEditable=true]:focus{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false] *[contentEditable=true]:hover{outline:2px solid #2276d2}.mce-content-body *[contentEditable=false][data-mce-selected]{outline:2px solid #2276d2}.mce-content-body.mce-content-readonly *[contentEditable=true]:focus,.mce-content-body.mce-content-readonly *[contentEditable=true]:hover{outline:none}.mce-content-body *[data-mce-selected="inline-boundary"]{background:#bfe6ff}.mce-content-body .mce-item-anchor[data-mce-selected]{background:#D5D5D5 url(img/anchor.gif) no-repeat center}.mce-content-body hr{cursor:default}.mce-content-body table{-webkit-nbsp-mode:normal}.ephox-snooker-resizer-bar{background-color:#2276d2;opacity:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ephox-snooker-resizer-cols{cursor:col-resize}.ephox-snooker-resizer-rows{cursor:row-resize}.ephox-snooker-resizer-bar.ephox-snooker-resizer-bar-dragging{opacity:.2} \ No newline at end of file diff --git a/lib/web/tiny_mce_4/themes/inlite/theme.min.js b/lib/web/tiny_mce_4/themes/inlite/theme.min.js index 3fd22f2c19d71..8eba581ec3b99 100644 --- a/lib/web/tiny_mce_4/themes/inlite/theme.min.js +++ b/lib/web/tiny_mce_4/themes/inlite/theme.min.js @@ -1 +1 @@ -!function(_){"use strict";var u,t,e,n,i,r,o=tinymce.util.Tools.resolve("tinymce.ThemeManager"),h=tinymce.util.Tools.resolve("tinymce.Env"),v=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),c=tinymce.util.Tools.resolve("tinymce.util.Delay"),s=function(t){return t.reduce(function(t,e){return Array.isArray(e)?t.concat(s(e)):t.concat(e)},[])},a={flatten:s},l=function(t,e){for(var n=0;n<e.length;n++){var i=(0,e[n])(t);if(i)return i}return null},d=function(t,e){return{id:t,rect:e}},f=function(t){return{x:t.left,y:t.top,w:t.width,h:t.height}},m=function(t){return{left:t.x,top:t.y,width:t.w,height:t.h,right:t.x+t.w,bottom:t.y+t.h}},g=function(t){var e=v.DOM.getViewPort();return{x:t.x+e.x,y:t.y+e.y,w:t.w,h:t.h}},p=function(t){var e=t.getBoundingClientRect();return g({x:e.left,y:e.top,w:Math.max(t.clientWidth,t.offsetWidth),h:Math.max(t.clientHeight,t.offsetHeight)})},y=function(t,e){return p(e)},b=function(t){return p(t.getContentAreaContainer()||t.getBody())},x=function(t){var e=t.selection.getBoundingClientRect();return e?g(f(e)):null},w=function(n,i){return function(t){for(var e=0;e<i.length;e++)if(i[e].predicate(n))return d(i[e].id,y(t,n));return null}},R=function(i,r){return function(t){for(var e=0;e<i.length;e++)for(var n=0;n<r.length;n++)if(r[n].predicate(i[e]))return d(r[n].id,y(t,i[e]));return null}},C=tinymce.util.Tools.resolve("tinymce.util.Tools"),k=function(t,e){return{id:t,predicate:e}},E=function(t){return C.map(t,function(t){return k(t.id,t.predicate)})},H=function(e){return function(t){return t.selection.isCollapsed()?null:d(e,x(t))}},T=function(i,r){return function(t){var e,n=t.schema.getTextBlockElements();for(e=0;e<i.length;e++)if("TABLE"===i[e].nodeName)return null;for(e=0;e<i.length;e++)if(i[e].nodeName in n)return t.dom.isEmpty(i[e])?d(r,x(t)):null;return null}},S=function(t){t.fire("SkinLoaded")},M=function(t){return t.fire("BeforeRenderUI")},N=tinymce.util.Tools.resolve("tinymce.EditorManager"),O=function(e){return function(t){return typeof t===e}},P=function(t){return Array.isArray(t)},W=function(t){return O("string")(t)},D=function(t){return O("number")(t)},A=function(t){return O("boolean")(t)},B=function(t){return O("function")(t)},L=(O("object"),P),I=function(t,e){if(e(t))return!0;throw new Error("Default value doesn't match requested type.")},z=function(r){return function(t,e,n){var i=t.settings;return I(n,r),e in i&&r(i[e])?i[e]:n}},F={getStringOr:z(W),getBoolOr:z(A),getNumberOr:z(D),getHandlerOr:z(B),getToolbarItemsOr:(u=L,function(t,e,n){var i,r,o,s,a,l=e in t.settings?t.settings[e]:n;return I(n,u),r=n,L(i=l)?i:W(i)?"string"==typeof(s=i)?(a=/[ ,]/,s.split(a).filter(function(t){return 0<t.length})):s:A(i)?(o=r,!1===i?[]:o):r})},U=tinymce.util.Tools.resolve("tinymce.geom.Rect"),V=function(t,e){return{rect:t,position:e}},q=function(t,e){return{x:e.x,y:e.y,w:t.w,h:t.h}},Y=function(t,e,n,i,r){var o,s,a,l={x:i.x,y:i.y,w:i.w+(i.w<r.w+n.w?r.w:0),h:i.h+(i.h<r.h+n.h?r.h:0)};return o=U.findBestRelativePosition(r,n,l,t),n=U.clamp(n,l),o?(s=U.relativePosition(r,n,o),a=q(r,s),V(a,o)):(n=U.intersect(l,n))?((o=U.findBestRelativePosition(r,n,l,e))?(s=U.relativePosition(r,n,o),a=q(r,s)):a=q(r,n),V(a,o)):null},$=function(t,e,n){return Y(["cr-cl","cl-cr"],["bc-tc","bl-tl","br-tr"],t,e,n)},X=function(t,e,n){return Y(["tc-bc","bc-tc","tl-bl","bl-tl","tr-br","br-tr","cr-cl","cl-cr"],["bc-tc","bl-tl","br-tr","cr-cl"],t,e,n)},j=function(t,e,n,i){var r;return"function"==typeof t?(r=t({elementRect:m(e),contentAreaRect:m(n),panelRect:m(i)}),f(r)):i},J=function(t){return t.panelRect},G=function(t){return F.getToolbarItemsOr(t,"selection_toolbar",["bold","italic","|","quicklink","h2","h3","blockquote"])},K=function(t){return F.getToolbarItemsOr(t,"insert_toolbar",["quickimage","quicktable"])},Z=function(t){return F.getHandlerOr(t,"inline_toolbar_position_handler",J)},Q=function(t){var e,n,i,r,o=t.settings;return o.skin_url?(i=t,r=o.skin_url,i.documentBaseURI.toAbsolute(r)):(e=o.skin,n=N.baseURL+"/skins/",e?n+e:n+"lightgray")},tt=function(t){return!1===t.settings.skin},et=function(i,r){var t=Q(i),e=function(){var t,e,n;e=r,n=function(){t._skinLoaded=!0,S(t),e()},(t=i).initialized?n():t.on("init",n)};tt(i)?e():(v.DOM.styleSheetLoader.load(t+"/skin.min.css",e),i.contentCSS.push(t+"/content.inline.min.css"))},nt=function(t){var e,n,i,r,o=t.contextToolbars;return a.flatten([o||[],(e=t,n="img",i="image",r="alignleft aligncenter alignright",{predicate:function(t){return e.dom.is(t,n)},id:i,items:r})])},it=function(t,e){var n,i,r,o,s;return s=(o=t).selection.getNode(),i=o.dom.getParents(s,"*"),r=E(e),(n=l(t,[w(i[0],r),H("text"),T(i,"insert"),R(i,r)]))&&n.rect?n:null},rt=function(i,r){return function(){var t,e,n;i.removed||(n=i,_.document.activeElement!==n.getBody())||(t=nt(i),(e=it(i,t))?r.show(i,e.id,e.rect,t):r.hide())}},ot=function(t,e){var n,i,r,o,s,a=c.throttle(rt(t,e),0),l=c.throttle((r=rt(n=t,i=e),function(){n.removed||i.inForm()||r()}),0),u=(o=t,s=e,function(){var t=nt(o),e=it(o,t);e&&s.reposition(o,e.id,e.rect)});t.on("blur hide ObjectResizeStart",e.hide),t.on("click",a),t.on("nodeChange mouseup",l),t.on("ResizeEditor keyup",a),t.on("ResizeWindow",u),v.DOM.bind(h.container,"scroll",u),t.on("remove",function(){v.DOM.unbind(h.container,"scroll",u),e.remove()}),t.shortcuts.add("Alt+F10,F10","",e.focus)},st=function(t,e){return et(t,function(){var n,i;ot(t,e),i=e,(n=t).shortcuts.remove("meta+k"),n.shortcuts.add("meta+k","",function(){var t=nt(n),e=l(n,[H("quicklink")]);e&&i.show(n,e.id,e.rect,t)})}),{}},at=function(t,e){return t.inline?st(t,e):function(t){throw new Error(t)}("inlite theme only supports inline mode.")},lt=function(){},ut=function(t){return function(){return t}},ct=ut(!1),dt=ut(!0),ft=ct,ht=dt,mt=function(){return gt},gt=(i={fold:function(t,e){return t()},is:ft,isSome:ft,isNone:ht,getOr:n=function(t){return t},getOrThunk:e=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:n,orThunk:e,map:mt,ap:mt,each:function(){},bind:mt,flatten:mt,exists:ft,forall:ht,filter:mt,equals:t=function(t){return t.isNone()},equals_:t,toArray:function(){return[]},toString:ut("none()")},Object.freeze&&Object.freeze(i),i),pt=function(n){var t=function(){return n},e=function(){return r},i=function(t){return t(n)},r={fold:function(t,e){return e(n)},is:function(t){return n===t},isSome:ht,isNone:ft,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:e,orThunk:e,map:function(t){return pt(t(n))},ap:function(t){return t.fold(mt,function(t){return pt(t(n))})},each:function(t){t(n)},bind:i,flatten:t,exists:i,forall:i,filter:function(t){return t(n)?r:gt},equals:function(t){return t.is(n)},equals_:function(t,e){return t.fold(ft,function(t){return e(n,t)})},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return r},vt={some:pt,none:mt,from:function(t){return null===t||t===undefined?gt:pt(t)}},yt=function(e){return function(t){return function(t){if(null===t)return"null";var e=typeof t;return"object"===e&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":e}(t)===e}},bt=yt("function"),xt=yt("number"),wt=(Array.prototype.slice,(r=Array.prototype.indexOf)===undefined?function(t,e){return Ht(t,e)}:function(t,e){return r.call(t,e)}),_t=function(t,e){return Et(t,e).isSome()},Rt=function(t,e){for(var n=t.length,i=new Array(n),r=0;r<n;r++){var o=t[r];i[r]=e(o,r,t)}return i},Ct=function(t,e){for(var n=0,i=t.length;n<i;n++)e(t[n],n,t)},kt=function(t,e){for(var n=[],i=0,r=t.length;i<r;i++){var o=t[i];e(o,i,t)&&n.push(o)}return n},Et=function(t,e){for(var n=0,i=t.length;n<i;n++)if(e(t[n],n,t))return vt.some(n);return vt.none()},Ht=function(t,e){for(var n=0,i=t.length;n<i;++n)if(t[n]===e)return n;return-1},Tt=Array.prototype.push,St=(bt(Array.from)&&Array.from,0),Mt={id:function(){return"mceu_"+St++},create:function(t,e,n){var i=_.document.createElement(t);return v.DOM.setAttribs(i,e),"string"==typeof n?i.innerHTML=n:C.each(n,function(t){t.nodeType&&i.appendChild(t)}),i},createFragment:function(t){return v.DOM.createFragment(t)},getWindowSize:function(){return v.DOM.getViewPort()},getSize:function(t){var e,n;if(t.getBoundingClientRect){var i=t.getBoundingClientRect();e=Math.max(i.width||i.right-i.left,t.offsetWidth),n=Math.max(i.height||i.bottom-i.bottom,t.offsetHeight)}else e=t.offsetWidth,n=t.offsetHeight;return{width:e,height:n}},getPos:function(t,e){return v.DOM.getPos(t,e||Mt.getContainer())},getContainer:function(){return h.container?h.container:_.document.body},getViewPort:function(t){return v.DOM.getViewPort(t)},get:function(t){return _.document.getElementById(t)},addClass:function(t,e){return v.DOM.addClass(t,e)},removeClass:function(t,e){return v.DOM.removeClass(t,e)},hasClass:function(t,e){return v.DOM.hasClass(t,e)},toggleClass:function(t,e,n){return v.DOM.toggleClass(t,e,n)},css:function(t,e,n){return v.DOM.setStyle(t,e,n)},getRuntimeStyle:function(t,e){return v.DOM.getStyle(t,e,!0)},on:function(t,e,n,i){return v.DOM.bind(t,e,n,i)},off:function(t,e,n){return v.DOM.unbind(t,e,n)},fire:function(t,e,n){return v.DOM.fire(t,e,n)},innerHtml:function(t,e){v.DOM.setHTML(t,e)}},Nt=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),Ot=tinymce.util.Tools.resolve("tinymce.util.Class"),Pt=tinymce.util.Tools.resolve("tinymce.util.EventDispatcher"),Wt=function(t){var e;if(t)return"number"==typeof t?{top:t=t||0,left:t,bottom:t,right:t}:(1===(e=(t=t.split(" ")).length)?t[1]=t[2]=t[3]=t[0]:2===e?(t[2]=t[0],t[3]=t[1]):3===e&&(t[3]=t[1]),{top:parseInt(t[0],10)||0,right:parseInt(t[1],10)||0,bottom:parseInt(t[2],10)||0,left:parseInt(t[3],10)||0})},Dt=function(i,t){function e(t){var e=parseFloat(function(t){var e=i.ownerDocument.defaultView;if(e){var n=e.getComputedStyle(i,null);return n?(t=t.replace(/[A-Z]/g,function(t){return"-"+t}),n.getPropertyValue(t)):null}return i.currentStyle[t]}(t));return isNaN(e)?0:e}return{top:e(t+"TopWidth"),right:e(t+"RightWidth"),bottom:e(t+"BottomWidth"),left:e(t+"LeftWidth")}};function At(){}function Bt(t){this.cls=[],this.cls._map={},this.onchange=t||At,this.prefix=""}C.extend(Bt.prototype,{add:function(t){return t&&!this.contains(t)&&(this.cls._map[t]=!0,this.cls.push(t),this._change()),this},remove:function(t){if(this.contains(t)){var e=void 0;for(e=0;e<this.cls.length&&this.cls[e]!==t;e++);this.cls.splice(e,1),delete this.cls._map[t],this._change()}return this},toggle:function(t,e){var n=this.contains(t);return n!==e&&(n?this.remove(t):this.add(t),this._change()),this},contains:function(t){return!!this.cls._map[t]},_change:function(){delete this.clsValue,this.onchange.call(this)}}),Bt.prototype.toString=function(){var t;if(this.clsValue)return this.clsValue;t="";for(var e=0;e<this.cls.length;e++)0<e&&(t+=" "),t+=this.prefix+this.cls[e];return t};var Lt,It,zt,Ft=/^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i,Ut=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,Vt=/^\s*|\s*$/g,qt=Ot.extend({init:function(t){var o=this.match;function s(t,e,n){var i;function r(t){t&&e.push(t)}return r(function(e){if(e)return e=e.toLowerCase(),function(t){return"*"===e||t.type===e}}((i=Ft.exec(t.replace(Vt,"")))[1])),r(function(e){if(e)return function(t){return t._name===e}}(i[2])),r(function(n){if(n)return n=n.split("."),function(t){for(var e=n.length;e--;)if(!t.classes.contains(n[e]))return!1;return!0}}(i[3])),r(function(n,i,r){if(n)return function(t){var e=t[n]?t[n]():"";return i?"="===i?e===r:"*="===i?0<=e.indexOf(r):"~="===i?0<=(" "+e+" ").indexOf(" "+r+" "):"!="===i?e!==r:"^="===i?0===e.indexOf(r):"$="===i&&e.substr(e.length-r.length)===r:!!r}}(i[4],i[5],i[6])),r(function(i){var e;if(i)return(i=/(?:not\((.+)\))|(.+)/i.exec(i))[1]?(e=a(i[1],[]),function(t){return!o(t,e)}):(i=i[2],function(t,e,n){return"first"===i?0===e:"last"===i?e===n-1:"even"===i?e%2==0:"odd"===i?e%2==1:!!t[i]&&t[i]()})}(i[7])),e.pseudo=!!i[7],e.direct=n,e}function a(t,e){var n,i,r,o=[];do{if(Ut.exec(""),(i=Ut.exec(t))&&(t=i[3],o.push(i[1]),i[2])){n=i[3];break}}while(i);for(n&&a(n,e),t=[],r=0;r<o.length;r++)">"!==o[r]&&t.push(s(o[r],[],">"===o[r-1]));return e.push(t),e}this._selectors=a(t,[])},match:function(t,e){var n,i,r,o,s,a,l,u,c,d,f,h,m;for(n=0,i=(e=e||this._selectors).length;n<i;n++){for(m=t,h=0,r=(o=(s=e[n]).length)-1;0<=r;r--)for(u=s[r];m;){if(u.pseudo)for(c=d=(f=m.parent().items()).length;c--&&f[c]!==m;);for(a=0,l=u.length;a<l;a++)if(!u[a](m,c,d)){a=l+1;break}if(a===l){h++;break}if(r===o-1)break;m=m.parent()}if(h===o)return!0}return!1},find:function(t){var e,n,u=[],i=this._selectors;function c(t,e,n){var i,r,o,s,a,l=e[n];for(i=0,r=t.length;i<r;i++){for(a=t[i],o=0,s=l.length;o<s;o++)if(!l[o](a,i,r)){o=s+1;break}if(o===s)n===e.length-1?u.push(a):a.items&&c(a.items(),e,n+1);else if(l.direct)return;a.items&&c(a.items(),e,n)}}if(t.items){for(e=0,n=i.length;e<n;e++)c(t.items(),i[e],0);1<n&&(u=function(t){for(var e,n=[],i=t.length;i--;)(e=t[i]).__checked||(n.push(e),e.__checked=1);for(i=n.length;i--;)delete n[i].__checked;return n}(u))}return Lt||(Lt=qt.Collection),new Lt(u)}}),Yt=Array.prototype.push,$t=Array.prototype.slice;zt={length:0,init:function(t){t&&this.add(t)},add:function(t){return C.isArray(t)?Yt.apply(this,t):t instanceof It?this.add(t.toArray()):Yt.call(this,t),this},set:function(t){var e,n=this,i=n.length;for(n.length=0,n.add(t),e=n.length;e<i;e++)delete n[e];return n},filter:function(e){var t,n,i,r,o=[];for("string"==typeof e?(e=new qt(e),r=function(t){return e.match(t)}):r=e,t=0,n=this.length;t<n;t++)r(i=this[t])&&o.push(i);return new It(o)},slice:function(){return new It($t.apply(this,arguments))},eq:function(t){return-1===t?this.slice(t):this.slice(t,+t+1)},each:function(t){return C.each(this,t),this},toArray:function(){return C.toArray(this)},indexOf:function(t){for(var e=this.length;e--&&this[e]!==t;);return e},reverse:function(){return new It(C.toArray(this).reverse())},hasClass:function(t){return!!this[0]&&this[0].classes.contains(t)},prop:function(e,n){var t;return n!==undefined?(this.each(function(t){t[e]&&t[e](n)}),this):(t=this[0])&&t[e]?t[e]():void 0},exec:function(e){var n=C.toArray(arguments).slice(1);return this.each(function(t){t[e]&&t[e].apply(t,n)}),this},remove:function(){for(var t=this.length;t--;)this[t].remove();return this},addClass:function(e){return this.each(function(t){t.classes.add(e)})},removeClass:function(e){return this.each(function(t){t.classes.remove(e)})}},C.each("fire on off show hide append prepend before after reflow".split(" "),function(n){zt[n]=function(){var e=C.toArray(arguments);return this.each(function(t){n in t&&t[n].apply(t,e)}),this}}),C.each("text name disabled active selected checked visible parent value data".split(" "),function(e){zt[e]=function(t){return this.prop(e,t)}}),It=Ot.extend(zt);var Xt=qt.Collection=It,jt=function(t){this.create=t.create};jt.create=function(r,o){return new jt({create:function(e,n){var i,t=function(t){e.set(n,t.value)};return e.on("change:"+n,function(t){r.set(o,t.value)}),r.on("change:"+o,t),(i=e._bindings)||(i=e._bindings=[],e.on("destroy",function(){for(var t=i.length;t--;)i[t]()})),i.push(function(){r.off("change:"+o,t)}),r.get(o)}})};var Jt=tinymce.util.Tools.resolve("tinymce.util.Observable");function Gt(t){return 0<t.nodeType}var Kt,Zt,Qt=Ot.extend({Mixins:[Jt],init:function(t){var e,n;for(e in t=t||{})(n=t[e])instanceof jt&&(t[e]=n.create(this,e));this.data=t},set:function(e,n){var i,r,o=this.data[e];if(n instanceof jt&&(n=n.create(this,e)),"object"==typeof e){for(i in e)this.set(i,e[i]);return this}return function t(e,n){var i,r;if(e===n)return!0;if(null===e||null===n)return e===n;if("object"!=typeof e||"object"!=typeof n)return e===n;if(C.isArray(n)){if(e.length!==n.length)return!1;for(i=e.length;i--;)if(!t(e[i],n[i]))return!1}if(Gt(e)||Gt(n))return e===n;for(i in r={},n){if(!t(e[i],n[i]))return!1;r[i]=!0}for(i in e)if(!r[i]&&!t(e[i],n[i]))return!1;return!0}(o,n)||(this.data[e]=n,r={target:this,name:e,value:n,oldValue:o},this.fire("change:"+e,r),this.fire("change",r)),this},get:function(t){return this.data[t]},has:function(t){return t in this.data},bind:function(t){return jt.create(this,t)},destroy:function(){this.fire("destroy")}}),te={},ee={add:function(t){var e=t.parent();if(e){if(!e._layout||e._layout.isNative())return;te[e._id]||(te[e._id]=e),Kt||(Kt=!0,c.requestAnimationFrame(function(){var t,e;for(t in Kt=!1,te)(e=te[t]).state.get("rendered")&&e.reflow();te={}},_.document.body))}},remove:function(t){te[t._id]&&delete te[t._id]}},ne=function(t){return t?t.getRoot().uiContainer:null},ie={getUiContainerDelta:function(t){var e=ne(t);if(e&&"static"!==v.DOM.getStyle(e,"position",!0)){var n=v.DOM.getPos(e),i=e.scrollLeft-n.x,r=e.scrollTop-n.y;return vt.some({x:i,y:r})}return vt.none()},setUiContainer:function(t,e){var n=v.DOM.select(t.settings.ui_container)[0];e.getRoot().uiContainer=n},getUiContainer:ne,inheritUiContainer:function(t,e){return e.uiContainer=ne(t)}},re="onmousewheel"in _.document,oe=!1,se=0,ae={Statics:{classPrefix:"mce-"},isRtl:function(){return Zt.rtl},classPrefix:"mce-",init:function(e){var t,n,i=this;function r(t){var e;for(t=t.split(" "),e=0;e<t.length;e++)i.classes.add(t[e])}i.settings=e=C.extend({},i.Defaults,e),i._id=e.id||"mceu_"+se++,i._aria={role:e.role},i._elmCache={},i.$=Nt,i.state=new Qt({visible:!0,active:!1,disabled:!1,value:""}),i.data=new Qt(e.data),i.classes=new Bt(function(){i.state.get("rendered")&&(i.getEl().className=this.toString())}),i.classes.prefix=i.classPrefix,(t=e.classes)&&(i.Defaults&&(n=i.Defaults.classes)&&t!==n&&r(n),r(t)),C.each("title text name visible disabled active value".split(" "),function(t){t in e&&i[t](e[t])}),i.on("click",function(){if(i.disabled())return!1}),i.settings=e,i.borderBox=Wt(e.border),i.paddingBox=Wt(e.padding),i.marginBox=Wt(e.margin),e.hidden&&i.hide()},Properties:"parent,name",getContainerElm:function(){var t=ie.getUiContainer(this);return t||Mt.getContainer()},getParentCtrl:function(t){for(var e,n=this.getRoot().controlIdLookup;t&&n&&!(e=n[t.id]);)t=t.parentNode;return e},initLayoutRect:function(){var t,e,n,i,r,o,s,a,l,u,c=this,d=c.settings,f=c.getEl();t=c.borderBox=c.borderBox||Dt(f,"border"),c.paddingBox=c.paddingBox||Dt(f,"padding"),c.marginBox=c.marginBox||Dt(f,"margin"),u=Mt.getSize(f),a=d.minWidth,l=d.minHeight,r=a||u.width,o=l||u.height,n=d.width,i=d.height,s=void 0!==(s=d.autoResize)?s:!n&&!i,n=n||r,i=i||o;var h=t.left+t.right,m=t.top+t.bottom,g=d.maxWidth||65535,p=d.maxHeight||65535;return c._layoutRect=e={x:d.x||0,y:d.y||0,w:n,h:i,deltaW:h,deltaH:m,contentW:n-h,contentH:i-m,innerW:n-h,innerH:i-m,startMinWidth:a||0,startMinHeight:l||0,minW:Math.min(r,g),minH:Math.min(o,p),maxW:g,maxH:p,autoResize:s,scrollW:0},c._lastLayoutRect={},e},layoutRect:function(t){var e,n,i,r,o,s=this,a=s._layoutRect;return a||(a=s.initLayoutRect()),t?(i=a.deltaW,r=a.deltaH,t.x!==undefined&&(a.x=t.x),t.y!==undefined&&(a.y=t.y),t.minW!==undefined&&(a.minW=t.minW),t.minH!==undefined&&(a.minH=t.minH),(n=t.w)!==undefined&&(n=(n=n<a.minW?a.minW:n)>a.maxW?a.maxW:n,a.w=n,a.innerW=n-i),(n=t.h)!==undefined&&(n=(n=n<a.minH?a.minH:n)>a.maxH?a.maxH:n,a.h=n,a.innerH=n-r),(n=t.innerW)!==undefined&&(n=(n=n<a.minW-i?a.minW-i:n)>a.maxW-i?a.maxW-i:n,a.innerW=n,a.w=n+i),(n=t.innerH)!==undefined&&(n=(n=n<a.minH-r?a.minH-r:n)>a.maxH-r?a.maxH-r:n,a.innerH=n,a.h=n+r),t.contentW!==undefined&&(a.contentW=t.contentW),t.contentH!==undefined&&(a.contentH=t.contentH),(e=s._lastLayoutRect).x===a.x&&e.y===a.y&&e.w===a.w&&e.h===a.h||((o=Zt.repaintControls)&&o.map&&!o.map[s._id]&&(o.push(s),o.map[s._id]=!0),e.x=a.x,e.y=a.y,e.w=a.w,e.h=a.h),s):a},repaint:function(){var t,e,n,i,r,o,s,a,l,u,c=this;l=_.document.createRange?function(t){return t}:Math.round,t=c.getEl().style,i=c._layoutRect,a=c._lastRepaintRect||{},o=(r=c.borderBox).left+r.right,s=r.top+r.bottom,i.x!==a.x&&(t.left=l(i.x)+"px",a.x=i.x),i.y!==a.y&&(t.top=l(i.y)+"px",a.y=i.y),i.w!==a.w&&(u=l(i.w-o),t.width=(0<=u?u:0)+"px",a.w=i.w),i.h!==a.h&&(u=l(i.h-s),t.height=(0<=u?u:0)+"px",a.h=i.h),c._hasBody&&i.innerW!==a.innerW&&(u=l(i.innerW),(n=c.getEl("body"))&&((e=n.style).width=(0<=u?u:0)+"px"),a.innerW=i.innerW),c._hasBody&&i.innerH!==a.innerH&&(u=l(i.innerH),(n=n||c.getEl("body"))&&((e=e||n.style).height=(0<=u?u:0)+"px"),a.innerH=i.innerH),c._lastRepaintRect=a,c.fire("repaint",{},!1)},updateLayoutRect:function(){var t=this;t.parent()._lastRect=null,Mt.css(t.getEl(),{width:"",height:""}),t._layoutRect=t._lastRepaintRect=t._lastLayoutRect=null,t.initLayoutRect()},on:function(t,e){var n,i,r,o=this;return le(o).on(t,"string"!=typeof(n=e)?n:function(t){return i||o.parentsAndSelf().each(function(t){var e=t.settings.callbacks;if(e&&(i=e[n]))return r=t,!1}),i?i.call(r,t):(t.action=n,void this.fire("execute",t))}),o},off:function(t,e){return le(this).off(t,e),this},fire:function(t,e,n){if((e=e||{}).control||(e.control=this),e=le(this).fire(t,e),!1!==n&&this.parent)for(var i=this.parent();i&&!e.isPropagationStopped();)i.fire(t,e,!1),i=i.parent();return e},hasEventListeners:function(t){return le(this).has(t)},parents:function(t){var e,n=new Xt;for(e=this.parent();e;e=e.parent())n.add(e);return t&&(n=n.filter(t)),n},parentsAndSelf:function(t){return new Xt(this).add(this.parents(t))},next:function(){var t=this.parent().items();return t[t.indexOf(this)+1]},prev:function(){var t=this.parent().items();return t[t.indexOf(this)-1]},innerHtml:function(t){return this.$el.html(t),this},getEl:function(t){var e=t?this._id+"-"+t:this._id;return this._elmCache[e]||(this._elmCache[e]=Nt("#"+e)[0]),this._elmCache[e]},show:function(){return this.visible(!0)},hide:function(){return this.visible(!1)},focus:function(){try{this.getEl().focus()}catch(t){}return this},blur:function(){return this.getEl().blur(),this},aria:function(t,e){var n=this,i=n.getEl(n.ariaTarget);return void 0===e?n._aria[t]:(n._aria[t]=e,n.state.get("rendered")&&i.setAttribute("role"===t?t:"aria-"+t,e),n)},encode:function(t,e){return!1!==e&&(t=this.translate(t)),(t||"").replace(/[&<>"]/g,function(t){return"&#"+t.charCodeAt(0)+";"})},translate:function(t){return Zt.translate?Zt.translate(t):t},before:function(t){var e=this.parent();return e&&e.insert(t,e.items().indexOf(this),!0),this},after:function(t){var e=this.parent();return e&&e.insert(t,e.items().indexOf(this)),this},remove:function(){var e,t,n=this,i=n.getEl(),r=n.parent();if(n.items){var o=n.items().toArray();for(t=o.length;t--;)o[t].remove()}r&&r.items&&(e=[],r.items().each(function(t){t!==n&&e.push(t)}),r.items().set(e),r._lastRect=null),n._eventsRoot&&n._eventsRoot===n&&Nt(i).off();var s=n.getRoot().controlIdLookup;return s&&delete s[n._id],i&&i.parentNode&&i.parentNode.removeChild(i),n.state.set("rendered",!1),n.state.destroy(),n.fire("remove"),n},renderBefore:function(t){return Nt(t).before(this.renderHtml()),this.postRender(),this},renderTo:function(t){return Nt(t||this.getContainerElm()).append(this.renderHtml()),this.postRender(),this},preRender:function(){},render:function(){},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'"></div>'},postRender:function(){var t,e,n,i,r,o=this,s=o.settings;for(i in o.$el=Nt(o.getEl()),o.state.set("rendered",!0),s)0===i.indexOf("on")&&o.on(i.substr(2),s[i]);if(o._eventsRoot){for(n=o.parent();!r&&n;n=n.parent())r=n._eventsRoot;if(r)for(i in r._nativeEvents)o._nativeEvents[i]=!0}ue(o),s.style&&(t=o.getEl())&&(t.setAttribute("style",s.style),t.style.cssText=s.style),o.settings.border&&(e=o.borderBox,o.$el.css({"border-top-width":e.top,"border-right-width":e.right,"border-bottom-width":e.bottom,"border-left-width":e.left}));var a=o.getRoot();for(var l in a.controlIdLookup||(a.controlIdLookup={}),(a.controlIdLookup[o._id]=o)._aria)o.aria(l,o._aria[l]);!1===o.state.get("visible")&&(o.getEl().style.display="none"),o.bindStates(),o.state.on("change:visible",function(t){var e,n=t.value;o.state.get("rendered")&&(o.getEl().style.display=!1===n?"none":"",o.getEl().getBoundingClientRect()),(e=o.parent())&&(e._lastRect=null),o.fire(n?"show":"hide"),ee.add(o)}),o.fire("postrender",{},!1)},bindStates:function(){},scrollIntoView:function(t){var e,n,i,r,o,s,a=this.getEl(),l=a.parentNode,u=function(t,e){var n,i,r=t;for(n=i=0;r&&r!==e&&r.nodeType;)n+=r.offsetLeft||0,i+=r.offsetTop||0,r=r.offsetParent;return{x:n,y:i}}(a,l);return e=u.x,n=u.y,i=a.offsetWidth,r=a.offsetHeight,o=l.clientWidth,s=l.clientHeight,"end"===t?(e-=o-i,n-=s-r):"center"===t&&(e-=o/2-i/2,n-=s/2-r/2),l.scrollLeft=e,l.scrollTop=n,this},getRoot:function(){for(var t,e=this,n=[];e;){if(e.rootControl){t=e.rootControl;break}n.push(e),e=(t=e).parent()}t||(t=this);for(var i=n.length;i--;)n[i].rootControl=t;return t},reflow:function(){ee.remove(this);var t=this.parent();return t&&t._layout&&!t._layout.isNative()&&t.reflow(),this}};function le(n){return n._eventDispatcher||(n._eventDispatcher=new Pt({scope:n,toggleEvent:function(t,e){e&&Pt.isNative(t)&&(n._nativeEvents||(n._nativeEvents={}),n._nativeEvents[t]=!0,n.state.get("rendered")&&ue(n))}})),n._eventDispatcher}function ue(a){var t,e,n,l,i,r;function o(t){var e=a.getParentCtrl(t.target);e&&e.fire(t.type,t)}function s(){var t=l._lastHoverCtrl;t&&(t.fire("mouseleave",{target:t.getEl()}),t.parents().each(function(t){t.fire("mouseleave",{target:t.getEl()})}),l._lastHoverCtrl=null)}function u(t){var e,n,i,r=a.getParentCtrl(t.target),o=l._lastHoverCtrl,s=0;if(r!==o){if((n=(l._lastHoverCtrl=r).parents().toArray().reverse()).push(r),o){for((i=o.parents().toArray().reverse()).push(o),s=0;s<i.length&&n[s]===i[s];s++);for(e=i.length-1;s<=e;e--)(o=i[e]).fire("mouseleave",{target:o.getEl()})}for(e=s;e<n.length;e++)(r=n[e]).fire("mouseenter",{target:r.getEl()})}}function c(t){t.preventDefault(),"mousewheel"===t.type?(t.deltaY=-.025*t.wheelDelta,t.wheelDeltaX&&(t.deltaX=-.025*t.wheelDeltaX)):(t.deltaX=0,t.deltaY=t.detail),t=a.fire("wheel",t)}if(i=a._nativeEvents){for((n=a.parents().toArray()).unshift(a),t=0,e=n.length;!l&&t<e;t++)l=n[t]._eventsRoot;for(l||(l=n[n.length-1]||a),a._eventsRoot=l,e=t,t=0;t<e;t++)n[t]._eventsRoot=l;var d=l._delegates;for(r in d||(d=l._delegates={}),i){if(!i)return!1;"wheel"!==r||oe?("mouseenter"===r||"mouseleave"===r?l._hasMouseEnter||(Nt(l.getEl()).on("mouseleave",s).on("mouseover",u),l._hasMouseEnter=1):d[r]||(Nt(l.getEl()).on(r,o),d[r]=!0),i[r]=!1):re?Nt(a.getEl()).on("mousewheel",c):Nt(a.getEl()).on("DOMMouseScroll",c)}}}C.each("text title visible disabled active value".split(" "),function(e){ae[e]=function(t){return 0===arguments.length?this.state.get(e):(void 0!==t&&this.state.set(e,t),this)}});var ce=Zt=Ot.extend(ae),de=function(t){return"static"===Mt.getRuntimeStyle(t,"position")},fe=function(t){return t.state.get("fixed")};function he(t,e,n){var i,r,o,s,a,l,u,c,d,f;return d=me(),o=(r=Mt.getPos(e,ie.getUiContainer(t))).x,s=r.y,fe(t)&&de(_.document.body)&&(o-=d.x,s-=d.y),i=t.getEl(),a=(f=Mt.getSize(i)).width,l=f.height,u=(f=Mt.getSize(e)).width,c=f.height,"b"===(n=(n||"").split(""))[0]&&(s+=c),"r"===n[1]&&(o+=u),"c"===n[0]&&(s+=Math.round(c/2)),"c"===n[1]&&(o+=Math.round(u/2)),"b"===n[3]&&(s-=l),"r"===n[4]&&(o-=a),"c"===n[3]&&(s-=Math.round(l/2)),"c"===n[4]&&(o-=Math.round(a/2)),{x:o,y:s,w:a,h:l}}var me=function(){var t=_.window;return{x:Math.max(t.pageXOffset,_.document.body.scrollLeft,_.document.documentElement.scrollLeft),y:Math.max(t.pageYOffset,_.document.body.scrollTop,_.document.documentElement.scrollTop),w:t.innerWidth||_.document.documentElement.clientWidth,h:t.innerHeight||_.document.documentElement.clientHeight}},ge=function(t){var e,n=ie.getUiContainer(t);return n&&!fe(t)?{x:0,y:0,w:(e=n).scrollWidth-1,h:e.scrollHeight-1}:me()},pe={testMoveRel:function(t,e){for(var n=ge(this),i=0;i<e.length;i++){var r=he(this,t,e[i]);if(fe(this)){if(0<r.x&&r.x+r.w<n.w&&0<r.y&&r.y+r.h<n.h)return e[i]}else if(r.x>n.x&&r.x+r.w<n.w+n.x&&r.y>n.y&&r.y+r.h<n.h+n.y)return e[i]}return e[0]},moveRel:function(t,e){"string"!=typeof e&&(e=this.testMoveRel(t,e));var n=he(this,t,e);return this.moveTo(n.x,n.y)},moveBy:function(t,e){var n=this.layoutRect();return this.moveTo(n.x+t,n.y+e),this},moveTo:function(t,e){var n=this;function i(t,e,n){return t<0?0:e<t+n&&(t=e-n)<0?0:t}if(n.settings.constrainToViewport){var r=ge(this),o=n.layoutRect();t=i(t,r.w+r.x,o.w),e=i(e,r.h+r.y,o.h)}var s=ie.getUiContainer(n);return s&&de(s)&&!fe(n)&&(t-=s.scrollLeft,e-=s.scrollTop),s&&(t+=1,e+=1),n.state.get("rendered")?n.layoutRect({x:t,y:e}).repaint():(n.settings.x=t,n.settings.y=e),n.fire("move",{x:t,y:e}),n}},ve=ce.extend({Mixins:[pe],Defaults:{classes:"widget tooltip tooltip-n"},renderHtml:function(){var t=this,e=t.classPrefix;return'<div id="'+t._id+'" class="'+t.classes+'" role="presentation"><div class="'+e+'tooltip-arrow"></div><div class="'+e+'tooltip-inner">'+t.encode(t.state.get("text"))+"</div></div>"},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl().lastChild.innerHTML=e.encode(t.value)}),e._super()},repaint:function(){var t,e;t=this.getEl().style,e=this._layoutRect,t.left=e.x+"px",t.top=e.y+"px",t.zIndex=131070}}),ye=ce.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.canFocus=!0,i.tooltip&&!1!==ye.tooltips&&(r.on("mouseenter",function(t){var e=r.tooltip().moveTo(-65535);if(t.control===r){var n=e.text(i.tooltip).show().testMoveRel(r.getEl(),["bc-tc","bc-tl","bc-tr"]);e.classes.toggle("tooltip-n","bc-tc"===n),e.classes.toggle("tooltip-nw","bc-tl"===n),e.classes.toggle("tooltip-ne","bc-tr"===n),e.moveRel(r.getEl(),n)}else e.hide()}),r.on("mouseleave mousedown click",function(){r.tooltip().remove(),r._tooltip=null})),r.aria("label",i.ariaLabel||i.tooltip)},tooltip:function(){return this._tooltip||(this._tooltip=new ve({type:"tooltip"}),ie.inheritUiContainer(this,this._tooltip),this._tooltip.renderTo()),this._tooltip},postRender:function(){var t=this,e=t.settings;t._super(),t.parent()||!e.width&&!e.height||(t.initLayoutRect(),t.repaint()),e.autofocus&&t.focus()},bindStates:function(){var e=this;function n(t){e.aria("disabled",t),e.classes.toggle("disabled",t)}function i(t){e.aria("pressed",t),e.classes.toggle("active",t)}return e.state.on("change:disabled",function(t){n(t.value)}),e.state.on("change:active",function(t){i(t.value)}),e.state.get("disabled")&&n(!0),e.state.get("active")&&i(!0),e._super()},remove:function(){this._super(),this._tooltip&&(this._tooltip.remove(),this._tooltip=null)}}),be=ye.extend({Defaults:{value:0},init:function(t){this._super(t),this.classes.add("progress"),this.settings.filter||(this.settings.filter=function(t){return Math.round(t)})},renderHtml:function(){var t=this._id,e=this.classPrefix;return'<div id="'+t+'" class="'+this.classes+'"><div class="'+e+'bar-container"><div class="'+e+'bar"></div></div><div class="'+e+'text">0%</div></div>'},postRender:function(){return this._super(),this.value(this.settings.value),this},bindStates:function(){var e=this;function n(t){t=e.settings.filter(t),e.getEl().lastChild.innerHTML=t+"%",e.getEl().firstChild.firstChild.style.width=t+"%"}return e.state.on("change:value",function(t){n(t.value)}),n(e.state.get("value")),e._super()}}),xe=function(t,e){t.getEl().lastChild.textContent=e+(t.progressBar?" "+t.progressBar.value()+"%":"")},we=ce.extend({Mixins:[pe],Defaults:{classes:"widget notification"},init:function(t){var e=this;e._super(t),e.maxWidth=t.maxWidth,t.text&&e.text(t.text),t.icon&&(e.icon=t.icon),t.color&&(e.color=t.color),t.type&&e.classes.add("notification-"+t.type),t.timeout&&(t.timeout<0||0<t.timeout)&&!t.closeButton?e.closeButton=!1:(e.classes.add("has-close"),e.closeButton=!0),t.progressBar&&(e.progressBar=new be),e.on("click",function(t){-1!==t.target.className.indexOf(e.classPrefix+"close")&&e.close()})},renderHtml:function(){var t,e=this,n=e.classPrefix,i="",r="",o="";return e.icon&&(i='<i class="'+n+"ico "+n+"i-"+e.icon+'"></i>'),t=' style="max-width: '+e.maxWidth+"px;"+(e.color?"background-color: "+e.color+';"':'"'),e.closeButton&&(r='<button type="button" class="'+n+'close" aria-hidden="true">\xd7</button>'),e.progressBar&&(o=e.progressBar.renderHtml()),'<div id="'+e._id+'" class="'+e.classes+'"'+t+' role="presentation">'+i+'<div class="'+n+'notification-inner">'+e.state.get("text")+"</div>"+o+r+'<div style="clip: rect(1px, 1px, 1px, 1px);height: 1px;overflow: hidden;position: absolute;width: 1px;" aria-live="assertive" aria-relevant="additions" aria-atomic="true"></div></div>'},postRender:function(){var t=this;return c.setTimeout(function(){t.$el.addClass(t.classPrefix+"in"),xe(t,t.state.get("text"))},100),t._super()},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl().firstChild.innerHTML=t.value,xe(e,t.value)}),e.progressBar&&(e.progressBar.bindStates(),e.progressBar.state.on("change:value",function(t){xe(e,e.state.get("text"))})),e._super()},close:function(){return this.fire("close").isDefaultPrevented()||this.remove(),this},repaint:function(){var t,e;t=this.getEl().style,e=this._layoutRect,t.left=e.x+"px",t.top=e.y+"px",t.zIndex=65534}});function _e(o){var s=function(t){return t.inline?t.getElement():t.getContentAreaContainer()};return{open:function(t,e){var n,i=C.extend(t,{maxWidth:(n=s(o),Mt.getSize(n).width)}),r=new we(i);return 0<(r.args=i).timeout&&(r.timer=setTimeout(function(){r.close(),e()},i.timeout)),r.on("close",function(){e()}),r.renderTo(),r},close:function(t){t.close()},reposition:function(t){Ct(t,function(t){t.moveTo(0,0)}),function(n){if(0<n.length){var t=n.slice(0,1)[0],e=s(o);t.moveRel(e,"tc-tc"),Ct(n,function(t,e){0<e&&t.moveRel(n[e-1].getEl(),"bc-tc")})}}(t)},getArgs:function(t){return t.args}}}function Re(t){var e,n;if(t.changedTouches)for(e="screenX screenY pageX pageY clientX clientY".split(" "),n=0;n<e.length;n++)t[e[n]]=t.changedTouches[0][e[n]]}function Ce(t,h){var m,g,e,p,v,y,b,x=h.document||_.document;h=h||{};var w=x.getElementById(h.handle||t);e=function(t){var e,n,i,r,o,s,a,l,u,c,d,f=(e=x,u=Math.max,n=e.documentElement,i=e.body,r=u(n.scrollWidth,i.scrollWidth),o=u(n.clientWidth,i.clientWidth),s=u(n.offsetWidth,i.offsetWidth),a=u(n.scrollHeight,i.scrollHeight),l=u(n.clientHeight,i.clientHeight),{width:r<s?o:r,height:a<u(n.offsetHeight,i.offsetHeight)?l:a});Re(t),t.preventDefault(),g=t.button,c=w,y=t.screenX,b=t.screenY,d=_.window.getComputedStyle?_.window.getComputedStyle(c,null).getPropertyValue("cursor"):c.runtimeStyle.cursor,m=Nt("<div></div>").css({position:"absolute",top:0,left:0,width:f.width,height:f.height,zIndex:2147483647,opacity:1e-4,cursor:d}).appendTo(x.body),Nt(x).on("mousemove touchmove",v).on("mouseup touchend",p),h.start(t)},v=function(t){if(Re(t),t.button!==g)return p(t);t.deltaX=t.screenX-y,t.deltaY=t.screenY-b,t.preventDefault(),h.drag(t)},p=function(t){Re(t),Nt(x).off("mousemove touchmove",v).off("mouseup touchend",p),m.remove(),h.stop&&h.stop(t)},this.destroy=function(){Nt(w).off()},Nt(w).on("mousedown touchstart",e)}var ke=tinymce.util.Tools.resolve("tinymce.ui.Factory"),Ee=function(t){return!!t.getAttribute("data-mce-tabstop")};function He(t){var o,r,n=t.root;function i(t){return t&&1===t.nodeType}try{o=_.document.activeElement}catch(e){o=_.document.body}function s(t){return i(t=t||o)?t.getAttribute("role"):null}function a(t){for(var e,n=t||o;n=n.parentNode;)if(e=s(n))return e}function l(t){var e=o;if(i(e))return e.getAttribute("aria-"+t)}function u(t){var e=t.tagName.toUpperCase();return"INPUT"===e||"TEXTAREA"===e||"SELECT"===e}function c(e){var r=[];return function t(e){if(1===e.nodeType&&"none"!==e.style.display&&!e.disabled){var n;(u(n=e)&&!n.hidden||Ee(n)||/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(s(n)))&&r.push(e);for(var i=0;i<e.childNodes.length;i++)t(e.childNodes[i])}}(e||n.getEl()),r}function d(t){var e,n;(n=(t=t||r).parents().toArray()).unshift(t);for(var i=0;i<n.length&&!(e=n[i]).settings.ariaRoot;i++);return e}function f(t,e){return t<0?t=e.length-1:t>=e.length&&(t=0),e[t]&&e[t].focus(),t}function h(t,e){var n=-1,i=d();e=e||c(i.getEl());for(var r=0;r<e.length;r++)e[r]===o&&(n=r);n+=t,i.lastAriaIndex=f(n,e)}function m(){"tablist"===a()?h(-1,c(o.parentNode)):r.parent().submenu?y():h(-1)}function g(){var t=s(),e=a();"tablist"===e?h(1,c(o.parentNode)):"menuitem"===t&&"menu"===e&&l("haspopup")?b():h(1)}function p(){h(-1)}function v(){var t=s(),e=a();"menuitem"===t&&"menubar"===e?b():"button"===t&&l("haspopup")?b({key:"down"}):h(1)}function y(){r.fire("cancel")}function b(t){t=t||{},r.fire("click",{target:o,aria:t})}return r=n.getParentCtrl(o),n.on("keydown",function(t){function e(t,e){u(o)||Ee(o)||"slider"!==s(o)&&!1!==e(t)&&t.preventDefault()}if(!t.isDefaultPrevented())switch(t.keyCode){case 37:e(t,m);break;case 39:e(t,g);break;case 38:e(t,p);break;case 40:e(t,v);break;case 27:y();break;case 14:case 13:case 32:e(t,b);break;case 9:!function(t){if("tablist"===a()){var e=c(r.getEl("body"))[0];e&&e.focus()}else h(t.shiftKey?-1:1)}(t),t.preventDefault()}}),n.on("focusin",function(t){o=t.target,r=t.control}),{focusFirst:function(t){var e=d(t),n=c(e.getEl());e.settings.ariaRemember&&"lastAriaIndex"in e?f(e.lastAriaIndex,n):f(0,n)}}}var Te,Se,Me,Ne,Oe={},Pe=ce.extend({init:function(t){var e=this;e._super(t),(t=e.settings).fixed&&e.state.set("fixed",!0),e._items=new Xt,e.isRtl()&&e.classes.add("rtl"),e.bodyClasses=new Bt(function(){e.state.get("rendered")&&(e.getEl("body").className=this.toString())}),e.bodyClasses.prefix=e.classPrefix,e.classes.add("container"),e.bodyClasses.add("container-body"),t.containerCls&&e.classes.add(t.containerCls),e._layout=ke.create((t.layout||"")+"layout"),e.settings.items?e.add(e.settings.items):e.add(e.render()),e._hasBody=!0},items:function(){return this._items},find:function(t){return(t=Oe[t]=Oe[t]||new qt(t)).find(this)},add:function(t){return this.items().add(this.create(t)).parent(this),this},focus:function(t){var e,n,i,r=this;if(!t||!(n=r.keyboardNav||r.parents().eq(-1)[0].keyboardNav))return i=r.find("*"),r.statusbar&&i.add(r.statusbar.items()),i.each(function(t){if(t.settings.autofocus)return e=null,!1;t.canFocus&&(e=e||t)}),e&&e.focus(),r;n.focusFirst(r)},replace:function(t,e){for(var n,i=this.items(),r=i.length;r--;)if(i[r]===t){i[r]=e;break}0<=r&&((n=e.getEl())&&n.parentNode.removeChild(n),(n=t.getEl())&&n.parentNode.removeChild(n)),e.parent(this)},create:function(t){var e,n=this,i=[];return C.isArray(t)||(t=[t]),C.each(t,function(t){t&&(t instanceof ce||("string"==typeof t&&(t={type:t}),e=C.extend({},n.settings.defaults,t),t.type=e.type=e.type||t.type||n.settings.defaultType||(e.defaults?e.defaults.type:null),t=ke.create(e)),i.push(t))}),i},renderNew:function(){var i=this;return i.items().each(function(t,e){var n;t.parent(i),t.state.get("rendered")||((n=i.getEl("body")).hasChildNodes()&&e<=n.childNodes.length-1?Nt(n.childNodes[e]).before(t.renderHtml()):Nt(n).append(t.renderHtml()),t.postRender(),ee.add(t))}),i._layout.applyClasses(i.items().filter(":visible")),i._lastRect=null,i},append:function(t){return this.add(t).renderNew()},prepend:function(t){return this.items().set(this.create(t).concat(this.items().toArray())),this.renderNew()},insert:function(t,e,n){var i,r,o;return t=this.create(t),i=this.items(),!n&&e<i.length-1&&(e+=1),0<=e&&e<i.length&&(r=i.slice(0,e).toArray(),o=i.slice(e).toArray(),i.set(r.concat(t,o))),this.renderNew()},fromJSON:function(t){for(var e in t)this.find("#"+e).value(t[e]);return this},toJSON:function(){var i={};return this.find("*").each(function(t){var e=t.name(),n=t.value();e&&void 0!==n&&(i[e]=n)}),i},renderHtml:function(){var t=this,e=t._layout,n=this.settings.role;return t.preRender(),e.preRender(t),'<div id="'+t._id+'" class="'+t.classes+'"'+(n?' role="'+this.settings.role+'"':"")+'><div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+(t.settings.html||"")+e.renderHtml(t)+"</div></div>"},postRender:function(){var t,e=this;return e.items().exec("postRender"),e._super(),e._layout.postRender(e),e.state.set("rendered",!0),e.settings.style&&e.$el.css(e.settings.style),e.settings.border&&(t=e.borderBox,e.$el.css({"border-top-width":t.top,"border-right-width":t.right,"border-bottom-width":t.bottom,"border-left-width":t.left})),e.parent()||(e.keyboardNav=He({root:e})),e},initLayoutRect:function(){var t=this._super();return this._layout.recalc(this),t},recalc:function(){var t=this,e=t._layoutRect,n=t._lastRect;if(!n||n.w!==e.w||n.h!==e.h)return t._layout.recalc(t),e=t.layoutRect(),t._lastRect={x:e.x,y:e.y,w:e.w,h:e.h},!0},reflow:function(){var t;if(ee.remove(this),this.visible()){for(ce.repaintControls=[],ce.repaintControls.map={},this.recalc(),t=ce.repaintControls.length;t--;)ce.repaintControls[t].repaint();"flow"!==this.settings.layout&&"stack"!==this.settings.layout&&this.repaint(),ce.repaintControls=[]}return this}}),We={init:function(){this.on("repaint",this.renderScroll)},renderScroll:function(){var p=this,v=2;function n(){var m,g,t;function e(t,e,n,i,r,o){var s,a,l,u,c,d,f,h;if(a=p.getEl("scroll"+t)){if(f=e.toLowerCase(),h=n.toLowerCase(),Nt(p.getEl("absend")).css(f,p.layoutRect()[i]-1),!r)return void Nt(a).css("display","none");Nt(a).css("display","block"),s=p.getEl("body"),l=p.getEl("scroll"+t+"t"),u=s["client"+n]-2*v,c=(u-=m&&g?a["client"+o]:0)/s["scroll"+n],(d={})[f]=s["offset"+e]+v,d[h]=u,Nt(a).css(d),(d={})[f]=s["scroll"+e]*c,d[h]=u*c,Nt(l).css(d)}}t=p.getEl("body"),m=t.scrollWidth>t.clientWidth,g=t.scrollHeight>t.clientHeight,e("h","Left","Width","contentW",m,"Height"),e("v","Top","Height","contentH",g,"Width")}p.settings.autoScroll&&(p._hasScroll||(p._hasScroll=!0,function(){function t(s,a,l,u,c){var d,t=p._id+"-scroll"+s,e=p.classPrefix;Nt(p.getEl()).append('<div id="'+t+'" class="'+e+"scrollbar "+e+"scrollbar-"+s+'"><div id="'+t+'t" class="'+e+'scrollbar-thumb"></div></div>'),p.draghelper=new Ce(t+"t",{start:function(){d=p.getEl("body")["scroll"+a],Nt("#"+t).addClass(e+"active")},drag:function(t){var e,n,i,r,o=p.layoutRect();n=o.contentW>o.innerW,i=o.contentH>o.innerH,r=p.getEl("body")["client"+l]-2*v,e=(r-=n&&i?p.getEl("scroll"+s)["client"+c]:0)/p.getEl("body")["scroll"+l],p.getEl("body")["scroll"+a]=d+t["delta"+u]/e},stop:function(){Nt("#"+t).removeClass(e+"active")}})}p.classes.add("scroll"),t("v","Top","Height","Y","Width"),t("h","Left","Width","X","Height")}(),p.on("wheel",function(t){var e=p.getEl("body");e.scrollLeft+=10*(t.deltaX||0),e.scrollTop+=10*t.deltaY,n()}),Nt(p.getEl("body")).on("scroll",n)),n())}},De=Pe.extend({Defaults:{layout:"fit",containerCls:"panel"},Mixins:[We],renderHtml:function(){var t=this,e=t._layout,n=t.settings.html;return t.preRender(),e.preRender(t),void 0===n?n='<div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+e.renderHtml(t)+"</div>":("function"==typeof n&&(n=n.call(t)),t._hasBody=!1),'<div id="'+t._id+'" class="'+t.classes+'" hidefocus="1" tabindex="-1" role="group">'+(t._preBodyHtml||"")+n+"</div>"}}),Ae={resizeToContent:function(){this._layoutRect.autoResize=!0,this._lastRect=null,this.reflow()},resizeTo:function(t,e){if(t<=1||e<=1){var n=Mt.getWindowSize();t=t<=1?t*n.w:t,e=e<=1?e*n.h:e}return this._layoutRect.autoResize=!1,this.layoutRect({minW:t,minH:e,w:t,h:e}).reflow()},resizeBy:function(t,e){var n=this.layoutRect();return this.resizeTo(n.w+t,n.h+e)}},Be=[],Le=[];function Ie(t,e){for(;t;){if(t===e)return!0;t=t.parent()}}function ze(){Te||(Te=function(t){2!==t.button&&function(t){for(var e=Be.length;e--;){var n=Be[e],i=n.getParentCtrl(t.target);if(n.settings.autohide){if(i&&(Ie(i,n)||n.parent()===i))continue;(t=n.fire("autohide",{target:t.target})).isDefaultPrevented()||n.hide()}}}(t)},Nt(_.document).on("click touchstart",Te))}function Fe(r){var t=Mt.getViewPort().y;function e(t,e){for(var n,i=0;i<Be.length;i++)if(Be[i]!==r)for(n=Be[i].parent();n&&(n=n.parent());)n===r&&Be[i].fixed(t).moveBy(0,e).repaint()}r.settings.autofix&&(r.state.get("fixed")?r._autoFixY>t&&(r.fixed(!1).layoutRect({y:r._autoFixY}).repaint(),e(!1,r._autoFixY-t)):(r._autoFixY=r.layoutRect().y,r._autoFixY<t&&(r.fixed(!0).layoutRect({y:0}).repaint(),e(!0,t-r._autoFixY))))}function Ue(t,e){var n,i,r=Ve.zIndex||65535;if(t)Le.push(e);else for(n=Le.length;n--;)Le[n]===e&&Le.splice(n,1);if(Le.length)for(n=0;n<Le.length;n++)Le[n].modal&&(r++,i=Le[n]),Le[n].getEl().style.zIndex=r,Le[n].zIndex=r,r++;var o=Nt("#"+e.classPrefix+"modal-block",e.getContainerElm())[0];i?Nt(o).css("z-index",i.zIndex-1):o&&(o.parentNode.removeChild(o),Ne=!1),Ve.currentZIndex=r}var Ve=De.extend({Mixins:[pe,Ae],init:function(t){var i=this;i._super(t),(i._eventsRoot=i).classes.add("floatpanel"),t.autohide&&(ze(),function(){if(!Me){var t=_.document.documentElement,e=t.clientWidth,n=t.clientHeight;Me=function(){_.document.all&&e===t.clientWidth&&n===t.clientHeight||(e=t.clientWidth,n=t.clientHeight,Ve.hideAll())},Nt(_.window).on("resize",Me)}}(),Be.push(i)),t.autofix&&(Se||(Se=function(){var t;for(t=Be.length;t--;)Fe(Be[t])},Nt(_.window).on("scroll",Se)),i.on("move",function(){Fe(this)})),i.on("postrender show",function(t){if(t.control===i){var e,n=i.classPrefix;i.modal&&!Ne&&((e=Nt("#"+n+"modal-block",i.getContainerElm()))[0]||(e=Nt('<div id="'+n+'modal-block" class="'+n+"reset "+n+'fade"></div>').appendTo(i.getContainerElm())),c.setTimeout(function(){e.addClass(n+"in"),Nt(i.getEl()).addClass(n+"in")}),Ne=!0),Ue(!0,i)}}),i.on("show",function(){i.parents().each(function(t){if(t.state.get("fixed"))return i.fixed(!0),!1})}),t.popover&&(i._preBodyHtml='<div class="'+i.classPrefix+'arrow"></div>',i.classes.add("popover").add("bottom").add(i.isRtl()?"end":"start")),i.aria("label",t.ariaLabel),i.aria("labelledby",i._id),i.aria("describedby",i.describedBy||i._id+"-none")},fixed:function(t){var e=this;if(e.state.get("fixed")!==t){if(e.state.get("rendered")){var n=Mt.getViewPort();t?e.layoutRect().y-=n.y:e.layoutRect().y+=n.y}e.classes.toggle("fixed",t),e.state.set("fixed",t)}return e},show:function(){var t,e=this._super();for(t=Be.length;t--&&Be[t]!==this;);return-1===t&&Be.push(this),e},hide:function(){return qe(this),Ue(!1,this),this._super()},hideAll:function(){Ve.hideAll()},close:function(){return this.fire("close").isDefaultPrevented()||(this.remove(),Ue(!1,this)),this},remove:function(){qe(this),this._super()},postRender:function(){return this.settings.bodyRole&&this.getEl("body").setAttribute("role",this.settings.bodyRole),this._super()}});function qe(t){var e;for(e=Be.length;e--;)Be[e]===t&&Be.splice(e,1);for(e=Le.length;e--;)Le[e]===t&&Le.splice(e,1)}Ve.hideAll=function(){for(var t=Be.length;t--;){var e=Be[t];e&&e.settings.autohide&&(e.hide(),Be.splice(t,1))}};var Ye=[],$e="";function Xe(t){var e,n=Nt("meta[name=viewport]")[0];!1!==h.overrideViewPort&&(n||((n=_.document.createElement("meta")).setAttribute("name","viewport"),_.document.getElementsByTagName("head")[0].appendChild(n)),(e=n.getAttribute("content"))&&void 0!==$e&&($e=e),n.setAttribute("content",t?"width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0":$e))}function je(t,e){(function(){for(var t=0;t<Ye.length;t++)if(Ye[t]._fullscreen)return!0;return!1})()&&!1===e&&Nt([_.document.documentElement,_.document.body]).removeClass(t+"fullscreen")}var Je=Ve.extend({modal:!0,Defaults:{border:1,layout:"flex",containerCls:"panel",role:"dialog",callbacks:{submit:function(){this.fire("submit",{data:this.toJSON()})},close:function(){this.close()}}},init:function(t){var n=this;n._super(t),n.isRtl()&&n.classes.add("rtl"),n.classes.add("window"),n.bodyClasses.add("window-body"),n.state.set("fixed",!0),t.buttons&&(n.statusbar=new De({layout:"flex",border:"1 0 0 0",spacing:3,padding:10,align:"center",pack:n.isRtl()?"start":"end",defaults:{type:"button"},items:t.buttons}),n.statusbar.classes.add("foot"),n.statusbar.parent(n)),n.on("click",function(t){var e=n.classPrefix+"close";(Mt.hasClass(t.target,e)||Mt.hasClass(t.target.parentNode,e))&&n.close()}),n.on("cancel",function(){n.close()}),n.on("move",function(t){t.control===n&&Ve.hideAll()}),n.aria("describedby",n.describedBy||n._id+"-none"),n.aria("label",t.title),n._fullscreen=!1},recalc:function(){var t,e,n,i,r=this,o=r.statusbar;r._fullscreen&&(r.layoutRect(Mt.getWindowSize()),r.layoutRect().contentH=r.layoutRect().innerH),r._super(),t=r.layoutRect(),r.settings.title&&!r._fullscreen&&(e=t.headerW)>t.w&&(n=t.x-Math.max(0,e/2),r.layoutRect({w:e,x:n}),i=!0),o&&(o.layoutRect({w:r.layoutRect().innerW}).recalc(),(e=o.layoutRect().minW+t.deltaW)>t.w&&(n=t.x-Math.max(0,e-t.w),r.layoutRect({w:e,x:n}),i=!0)),i&&r.recalc()},initLayoutRect:function(){var t,e=this,n=e._super(),i=0;if(e.settings.title&&!e._fullscreen){t=e.getEl("head");var r=Mt.getSize(t);n.headerW=r.width,n.headerH=r.height,i+=n.headerH}e.statusbar&&(i+=e.statusbar.layoutRect().h),n.deltaH+=i,n.minH+=i,n.h+=i;var o=Mt.getWindowSize();return n.x=e.settings.x||Math.max(0,o.w/2-n.w/2),n.y=e.settings.y||Math.max(0,o.h/2-n.h/2),n},renderHtml:function(){var t=this,e=t._layout,n=t._id,i=t.classPrefix,r=t.settings,o="",s="",a=r.html;return t.preRender(),e.preRender(t),r.title&&(o='<div id="'+n+'-head" class="'+i+'window-head"><div id="'+n+'-title" class="'+i+'title">'+t.encode(r.title)+'</div><div id="'+n+'-dragh" class="'+i+'dragh"></div><button type="button" class="'+i+'close" aria-hidden="true"><i class="mce-ico mce-i-remove"></i></button></div>'),r.url&&(a='<iframe src="'+r.url+'" tabindex="-1"></iframe>'),void 0===a&&(a=e.renderHtml(t)),t.statusbar&&(s=t.statusbar.renderHtml()),'<div id="'+n+'" class="'+t.classes+'" hidefocus="1"><div class="'+t.classPrefix+'reset" role="application">'+o+'<div id="'+n+'-body" class="'+t.bodyClasses+'">'+a+"</div>"+s+"</div></div>"},fullscreen:function(t){var n,e,i=this,r=_.document.documentElement,o=i.classPrefix;if(t!==i._fullscreen)if(Nt(_.window).on("resize",function(){var t;if(i._fullscreen)if(n)i._timer||(i._timer=c.setTimeout(function(){var t=Mt.getWindowSize();i.moveTo(0,0).resizeTo(t.w,t.h),i._timer=0},50));else{t=(new Date).getTime();var e=Mt.getWindowSize();i.moveTo(0,0).resizeTo(e.w,e.h),50<(new Date).getTime()-t&&(n=!0)}}),e=i.layoutRect(),i._fullscreen=t){i._initial={x:e.x,y:e.y,w:e.w,h:e.h},i.borderBox=Wt("0"),i.getEl("head").style.display="none",e.deltaH-=e.headerH+2,Nt([r,_.document.body]).addClass(o+"fullscreen"),i.classes.add("fullscreen");var s=Mt.getWindowSize();i.moveTo(0,0).resizeTo(s.w,s.h)}else i.borderBox=Wt(i.settings.border),i.getEl("head").style.display="",e.deltaH+=e.headerH,Nt([r,_.document.body]).removeClass(o+"fullscreen"),i.classes.remove("fullscreen"),i.moveTo(i._initial.x,i._initial.y).resizeTo(i._initial.w,i._initial.h);return i.reflow()},postRender:function(){var e,n=this;setTimeout(function(){n.classes.add("in"),n.fire("open")},0),n._super(),n.statusbar&&n.statusbar.postRender(),n.focus(),this.dragHelper=new Ce(n._id+"-dragh",{start:function(){e={x:n.layoutRect().x,y:n.layoutRect().y}},drag:function(t){n.moveTo(e.x+t.deltaX,e.y+t.deltaY)}}),n.on("submit",function(t){t.isDefaultPrevented()||n.close()}),Ye.push(n),Xe(!0)},submit:function(){return this.fire("submit",{data:this.toJSON()})},remove:function(){var t,e=this;for(e.dragHelper.destroy(),e._super(),e.statusbar&&this.statusbar.remove(),je(e.classPrefix,!1),t=Ye.length;t--;)Ye[t]===e&&Ye.splice(t,1);Xe(0<Ye.length)},getContentWindow:function(){var t=this.getEl().getElementsByTagName("iframe")[0];return t?t.contentWindow:null}});!function(){if(!h.desktop){var n={w:_.window.innerWidth,h:_.window.innerHeight};c.setInterval(function(){var t=_.window.innerWidth,e=_.window.innerHeight;n.w===t&&n.h===e||(n={w:t,h:e},Nt(_.window).trigger("resize"))},100)}Nt(_.window).on("resize",function(){var t,e,n=Mt.getWindowSize();for(t=0;t<Ye.length;t++)e=Ye[t].layoutRect(),Ye[t].moveTo(Ye[t].settings.x||Math.max(0,n.w/2-e.w/2),Ye[t].settings.y||Math.max(0,n.h/2-e.h/2))})}();var Ge,Ke,Ze,Qe=Je.extend({init:function(t){t={border:1,padding:20,layout:"flex",pack:"center",align:"center",containerCls:"panel",autoScroll:!0,buttons:{type:"button",text:"Ok",action:"ok"},items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200}},this._super(t)},Statics:{OK:1,OK_CANCEL:2,YES_NO:3,YES_NO_CANCEL:4,msgBox:function(t){var e,i=t.callback||function(){};function n(t,e,n){return{type:"button",text:t,subtype:n?"primary":"",onClick:function(t){t.control.parents()[1].close(),i(e)}}}switch(t.buttons){case Qe.OK_CANCEL:e=[n("Ok",!0,!0),n("Cancel",!1)];break;case Qe.YES_NO:case Qe.YES_NO_CANCEL:e=[n("Yes",1,!0),n("No",0)],t.buttons===Qe.YES_NO_CANCEL&&e.push(n("Cancel",-1));break;default:e=[n("Ok",!0,!0)]}return new Je({padding:20,x:t.x,y:t.y,minWidth:300,minHeight:100,layout:"flex",pack:"center",align:"center",buttons:e,title:t.title,role:"alertdialog",items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200,text:t.text},onPostRender:function(){this.aria("describedby",this.items()[0]._id)},onClose:t.onClose,onCancel:function(){i(!1)}}).renderTo(_.document.body).reflow()},alert:function(t,e){return"string"==typeof t&&(t={text:t}),t.callback=e,Qe.msgBox(t)},confirm:function(t,e){return"string"==typeof t&&(t={text:t}),t.callback=e,t.buttons=Qe.OK_CANCEL,Qe.msgBox(t)}}}),tn=function(t,e){return{renderUI:function(){return at(t,e)},getNotificationManagerImpl:function(){return _e(t)},getWindowManagerImpl:function(){return{open:function(n,t,e){var i;return n.title=n.title||" ",n.url=n.url||n.file,n.url&&(n.width=parseInt(n.width||320,10),n.height=parseInt(n.height||240,10)),n.body&&(n.items={defaults:n.defaults,type:n.bodyType||"form",items:n.body,data:n.data,callbacks:n.commands}),n.url||n.buttons||(n.buttons=[{text:"Ok",subtype:"primary",onclick:function(){i.find("form")[0].submit()}},{text:"Cancel",onclick:function(){i.close()}}]),(i=new Je(n)).on("close",function(){e(i)}),n.data&&i.on("postRender",function(){this.find("*").each(function(t){var e=t.name();e in n.data&&t.value(n.data[e])})}),i.features=n||{},i.params=t||{},i=i.renderTo(_.document.body).reflow()},alert:function(t,e,n){var i;return(i=Qe.alert(t,function(){e()})).on("close",function(){n(i)}),i},confirm:function(t,e,n){var i;return(i=Qe.confirm(t,function(t){e(t)})).on("close",function(){n(i)}),i},close:function(t){t.close()},getParams:function(t){return t.params},setParams:function(t,e){t.params=e}}}}},en="undefined"!=typeof _.window?_.window:Function("return this;")(),nn=function(t,e){return function(t,e){for(var n=e!==undefined&&null!==e?e:en,i=0;i<t.length&&n!==undefined&&null!==n;++i)n=n[t[i]];return n}(t.split("."),e)},rn=function(t,e){var n=nn(t,e);if(n===undefined||null===n)throw t+" not available on this browser";return n},on=tinymce.util.Tools.resolve("tinymce.util.Promise"),sn=function(n){return new on(function(t){var e=new(rn("FileReader"));e.onloadend=function(){t(e.result.split(",")[1])},e.readAsDataURL(n)})},an=function(){return new on(function(e){var t;(t=_.document.createElement("input")).type="file",t.style.position="fixed",t.style.left=0,t.style.top=0,t.style.opacity=.001,_.document.body.appendChild(t),t.onchange=function(t){e(Array.prototype.slice.call(t.target.files))},t.click(),t.parentNode.removeChild(t)})},ln=0,un=function(t){return t+ln+++(e=function(){return Math.round(4294967295*Math.random()).toString(36)},"s"+Date.now().toString(36)+e()+e()+e());var e},cn=function(r,o){var s={};function t(t){var e,n,i;n=o[t?"startContainer":"endContainer"],i=o[t?"startOffset":"endOffset"],1===n.nodeType&&(e=r.create("span",{"data-mce-type":"bookmark"}),n.hasChildNodes()?(i=Math.min(i,n.childNodes.length-1),t?n.insertBefore(e,n.childNodes[i]):r.insertAfter(e,n.childNodes[i])):n.appendChild(e),n=e,i=0),s[t?"startContainer":"endContainer"]=n,s[t?"startOffset":"endOffset"]=i}return t(!0),o.collapsed||t(),s},dn=function(r,o){function t(t){var e,n,i;e=i=o[t?"startContainer":"endContainer"],n=o[t?"startOffset":"endOffset"],e&&(1===e.nodeType&&(n=function(t){for(var e=t.parentNode.firstChild,n=0;e;){if(e===t)return n;1===e.nodeType&&"bookmark"===e.getAttribute("data-mce-type")||n++,e=e.nextSibling}return-1}(e),e=e.parentNode,r.remove(i)),o[t?"startContainer":"endContainer"]=e,o[t?"startOffset":"endOffset"]=n)}t(!0),t();var e=r.createRng();return e.setStart(o.startContainer,o.startOffset),o.endContainer&&e.setEnd(o.endContainer,o.endOffset),e},fn=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),hn=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),mn=function(t){return"A"===t.nodeName&&t.hasAttribute("href")},gn=function(t){var e,n,i,r,o,s,a,l;return r=t.selection,o=t.dom,s=r.getRng(),a=o,l=hn.getNode(s.startContainer,s.startOffset),e=a.getParent(l,mn)||l,n=hn.getNode(s.endContainer,s.endOffset),i=t.getBody(),C.grep(function(t,e,n){var i,r,o=[];for(i=new fn(e,t),r=e;r&&(1===r.nodeType&&o.push(r),r!==n);r=i.next());return o}(i,e,n),mn)},pn=function(t){var e,n,i,r,o;n=gn(e=t),r=e.dom,o=e.selection,i=cn(r,o.getRng()),C.each(n,function(t){e.dom.remove(t,!0)}),o.setRng(dn(r,i))},vn=function(t){t.selection.collapse(!1)},yn=function(t){t.focus(),pn(t),vn(t)},bn=function(t,e){var n,i,r,o,s,a=t.dom.getParent(t.selection.getStart(),"a[href]");a?(o=a,s=e,(r=t).focus(),r.dom.setAttrib(o,"href",s),vn(r)):(i=e,(n=t).execCommand("mceInsertLink",!1,{href:i}),vn(n))},xn=function(t,e,n){var i,r,o;t.plugins.table?t.plugins.table.insertTable(e,n):(r=e,o=n,(i=t).undoManager.transact(function(){var t,e;i.insertContent(function(t,e){var n,i,r;for(r='<table data-mce-id="mce" style="width: 100%">',r+="<tbody>",i=0;i<e;i++){for(r+="<tr>",n=0;n<t;n++)r+="<td><br></td>";r+="</tr>"}return r+="</tbody>",r+="</table>"}(r,o)),(t=i.dom.select("*[data-mce-id]")[0]).removeAttribute("data-mce-id"),e=i.dom.select("td,th",t),i.selection.setCursorLocation(e[0],0)}))},wn=function(t,e){t.execCommand("FormatBlock",!1,e)},_n=function(t,e,n){var i,r;r=(i=t.editorUpload.blobCache).create(un("mceu"),n,e),i.add(r),t.insertContent(t.dom.createHTML("img",{src:r.blobUri()}))},Rn=function(t,e){0===e.trim().length?yn(t):bn(t,e)},Cn=yn,kn=function(n,t){n.addButton("quicklink",{icon:"link",tooltip:"Insert/Edit link",stateSelector:"a[href]",onclick:function(){t.showForm(n,"quicklink")}}),n.addButton("quickimage",{icon:"image",tooltip:"Insert image",onclick:function(){an().then(function(t){var e=t[0];sn(e).then(function(t){_n(n,t,e)})})}}),n.addButton("quicktable",{icon:"table",tooltip:"Insert table",onclick:function(){t.hide(),xn(n,2,2)}}),function(e){for(var t=function(t){return function(){wn(e,t)}},n=1;n<6;n++){var i="h"+n;e.addButton(i,{text:i.toUpperCase(),tooltip:"Heading "+n,stateSelector:i,onclick:t(i),onPostRender:function(){this.getEl().firstChild.firstChild.style.fontWeight="bold"}})}}(n)},En=function(){var t=h.container;if(t&&"static"!==v.DOM.getStyle(t,"position",!0)){var e=v.DOM.getPos(t),n=e.x-t.scrollLeft,i=e.y-t.scrollTop;return vt.some({x:n,y:i})}return vt.none()},Hn=function(t){return/^www\.|\.(com|org|edu|gov|uk|net|ca|de|jp|fr|au|us|ru|ch|it|nl|se|no|es|mil)$/i.test(t.trim())},Tn=function(t){return/^https?:\/\//.test(t.trim())},Sn=function(t,e){return!Tn(e)&&Hn(e)?(n=t,i=e,new on(function(e){n.windowManager.confirm("The URL you entered seems to be an external link. Do you want to add the required http:// prefix?",function(t){e(!0===t?"http://"+i:i)})})):on.resolve(e);var n,i},Mn=function(r,e){var t,n,i,o={};return t="quicklink",n={items:[{type:"button",name:"unlink",icon:"unlink",onclick:function(){r.focus(),Cn(r),e()},tooltip:"Remove link"},{type:"filepicker",name:"linkurl",placeholder:"Paste or type a link",filetype:"file",onchange:function(t){var e=t.meta;e&&e.attach&&(o={href:this.value(),attach:e.attach})}},{type:"button",icon:"checkmark",subtype:"primary",tooltip:"Ok",onclick:"submit"}],onshow:function(t){if(t.control===this){var e,n="";(e=r.dom.getParent(r.selection.getStart(),"a[href]"))&&(n=r.dom.getAttrib(e,"href")),this.fromJSON({linkurl:n}),i=this.find("#unlink"),e?i.show():i.hide(),this.find("#linkurl")[0].focus()}var i},onsubmit:function(t){Sn(r,t.data.linkurl).then(function(t){r.undoManager.transact(function(){t===o.href&&(o.attach(),o={}),Rn(r,t)}),e()})}},(i=ke.create(C.extend({type:"form",layout:"flex",direction:"row",padding:5,name:t,spacing:3},n))).on("show",function(){i.find("textbox").eq(0).each(function(t){t.focus()})}),i},Nn=function(n,t,e){var o,i,s=[];if(e)return C.each(L(i=e)?i:W(i)?i.split(/[ ,]/):[],function(t){if("|"===t)o=null;else if(n.buttons[t]){o||(o={type:"buttongroup",items:[]},s.push(o));var e=n.buttons[t];B(e)&&(e=e()),e.type=e.type||"button",(e=ke.create(e)).on("postRender",(i=n,r=e,function(){var e,t,n=(t=function(t,e){return{selector:t,handler:e}},(e=r).settings.stateSelector?t(e.settings.stateSelector,function(t){e.active(t)}):e.settings.disabledStateSelector?t(e.settings.disabledStateSelector,function(t){e.disabled(t)}):null);null!==n&&i.selection.selectorChanged(n.selector,n.handler)})),o.items.push(e)}var i,r}),ke.create({type:"toolbar",layout:"flow",name:t,items:s})},On=function(){var l,c,o=function(t){return 0<t.items().length},u=function(t,e){var n,i,r=(n=t,i=e,C.map(i,function(t){return Nn(n,t.id,t.items)})).concat([Nn(t,"text",G(t)),Nn(t,"insert",K(t)),Mn(t,p)]);return ke.create({type:"floatpanel",role:"dialog",classes:"tinymce tinymce-inline arrow",ariaLabel:"Inline toolbar",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:!0,border:1,items:C.grep(r,o),oncancel:function(){t.focus()}})},d=function(t){t&&t.show()},f=function(t,e){t.moveTo(e.x,e.y)},h=function(n,i){i=i?i.substr(0,2):"",C.each({t:"down",b:"up",c:"center"},function(t,e){n.classes.toggle("arrow-"+t,e===i.substr(0,1))}),"cr"===i?(n.classes.toggle("arrow-left",!0),n.classes.toggle("arrow-right",!1)):"cl"===i?(n.classes.toggle("arrow-left",!1),n.classes.toggle("arrow-right",!0)):C.each({l:"left",r:"right"},function(t,e){n.classes.toggle("arrow-"+t,e===i.substr(1,1))})},m=function(t,e){var n=t.items().filter("#"+e);return 0<n.length&&(n[0].show(),t.reflow(),!0)},g=function(t,e,n,i){var r,o,s,a;if(a=Z(n),r=b(n),o=v.DOM.getRect(t.getEl()),s="insert"===e?$(i,r,o):X(i,r,o)){var l=En().getOr({x:0,y:0}),u={x:s.rect.x-l.x,y:s.rect.y-l.y,w:s.rect.w,h:s.rect.h};return f(t,j(a,c=i,r,u)),h(t,s.position),!0}return!1},p=function(){l&&l.hide()};return{show:function(t,e,n,i){var r,o,s,a;l||(M(t),(l=u(t,i)).renderTo().reflow().moveTo(n.x,n.y),t.nodeChanged()),o=e,s=t,a=n,d(r=l),r.items().hide(),m(r,o)?!1===g(r,o,s,a)&&p():p()},showForm:function(t,e){if(l){if(l.items().hide(),!m(l,e))return void p();var n,i,r,o=void 0;d(l),l.items().hide(),m(l,e),r=Z(t),n=b(t),o=v.DOM.getRect(l.getEl()),(i=X(c,n,o))&&(o=i.rect,f(l,j(r,c,n,o)),h(l,i.position))}},reposition:function(t,e,n){l&&g(l,e,t,n)},inForm:function(){return l&&l.visible()&&0<l.items().filter("form:visible").length},hide:p,focus:function(){l&&l.find("toolbar:visible").eq(0).each(function(t){t.focus(!0)})},remove:function(){l&&(l.remove(),l=null)}}},Pn=Ot.extend({Defaults:{firstControlClass:"first",lastControlClass:"last"},init:function(t){this.settings=C.extend({},this.Defaults,t)},preRender:function(t){t.bodyClasses.add(this.settings.containerClass)},applyClasses:function(t){var e,n,i,r,o=this.settings;e=o.firstControlClass,n=o.lastControlClass,t.each(function(t){t.classes.remove(e).remove(n).add(o.controlClass),t.visible()&&(i||(i=t),r=t)}),i&&i.classes.add(e),r&&r.classes.add(n)},renderHtml:function(t){var e="";return this.applyClasses(t.items()),t.items().each(function(t){e+=t.renderHtml()}),e},recalc:function(){},postRender:function(){},isNative:function(){return!1}}),Wn=Pn.extend({Defaults:{containerClass:"abs-layout",controlClass:"abs-layout-item"},recalc:function(t){t.items().filter(":visible").each(function(t){var e=t.settings;t.layoutRect({x:e.x,y:e.y,w:e.w,h:e.h}),t.recalc&&t.recalc()})},renderHtml:function(t){return'<div id="'+t._id+'-absend" class="'+t.classPrefix+'abs-end"></div>'+this._super(t)}}),Dn=ye.extend({Defaults:{classes:"widget btn",role:"button"},init:function(t){var e,n=this;n._super(t),t=n.settings,e=n.settings.size,n.on("click mousedown",function(t){t.preventDefault()}),n.on("touchstart",function(t){n.fire("click",t),t.preventDefault()}),t.subtype&&n.classes.add(t.subtype),e&&n.classes.add("btn-"+e),t.icon&&n.icon(t.icon)},icon:function(t){return arguments.length?(this.state.set("icon",t),this):this.state.get("icon")},repaint:function(){var t,e=this.getEl().firstChild;e&&((t=e.style).width=t.height="100%"),this._super()},renderHtml:function(){var t,e,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a="",l=n.settings;return(t=l.image)?(o="none","string"!=typeof t&&(t=_.window.getSelection?t[0]:t[1]),t=" style=\"background-image: url('"+t+"')\""):t="",s&&(n.classes.add("btn-has-text"),a='<span class="'+r+'txt">'+n.encode(s)+"</span>"),o=o?r+"ico "+r+"i-"+o:"",e="boolean"==typeof l.active?' aria-pressed="'+l.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" tabindex="-1"'+e+'><button id="'+i+'-button" role="presentation" type="button" tabindex="-1">'+(o?'<i class="'+o+'"'+t+"></i>":"")+a+"</button></div>"},bindStates:function(){var o=this,n=o.$,i=o.classPrefix+"txt";function s(t){var e=n("span."+i,o.getEl());t?(e[0]||(n("button:first",o.getEl()).append('<span class="'+i+'"></span>'),e=n("span."+i,o.getEl())),e.html(o.encode(t))):e.remove(),o.classes.toggle("btn-has-text",!!t)}return o.state.on("change:text",function(t){s(t.value)}),o.state.on("change:icon",function(t){var e=t.value,n=o.classPrefix;e=(o.settings.icon=e)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];e?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=e):r&&i.removeChild(r),s(o.state.get("text"))}),o._super()}}),An=Dn.extend({init:function(t){t=C.extend({text:"Browse...",multiple:!1,accept:null},t),this._super(t),this.classes.add("browsebutton"),t.multiple&&this.classes.add("multiple")},postRender:function(){var n=this,e=Mt.create("input",{type:"file",id:n._id+"-browse",accept:n.settings.accept});n._super(),Nt(e).on("change",function(t){var e=t.target.files;n.value=function(){return e.length?n.settings.multiple?e:e[0]:null},t.preventDefault(),e.length&&n.fire("change",t)}),Nt(e).on("click",function(t){t.stopPropagation()}),Nt(n.getEl("button")).on("click touchstart",function(t){t.stopPropagation(),e.click()}),n.getEl().appendChild(e)},remove:function(){Nt(this.getEl("button")).off(),Nt(this.getEl("input")).off(),this._super()}}),Bn=Pe.extend({Defaults:{defaultType:"button",role:"group"},renderHtml:function(){var t=this,e=t._layout;return t.classes.add("btn-group"),t.preRender(),e.preRender(t),'<div id="'+t._id+'" class="'+t.classes+'"><div id="'+t._id+'-body">'+(t.settings.html||"")+e.renderHtml(t)+"</div></div>"}}),Ln=ye.extend({Defaults:{classes:"checkbox",role:"checkbox",checked:!1},init:function(t){var e=this;e._super(t),e.on("click mousedown",function(t){t.preventDefault()}),e.on("click",function(t){t.preventDefault(),e.disabled()||e.checked(!e.checked())}),e.checked(e.settings.checked)},checked:function(t){return arguments.length?(this.state.set("checked",t),this):this.state.get("checked")},value:function(t){return arguments.length?this.checked(t):this.checked()},renderHtml:function(){var t=this,e=t._id,n=t.classPrefix;return'<div id="'+e+'" class="'+t.classes+'" unselectable="on" aria-labelledby="'+e+'-al" tabindex="-1"><i class="'+n+"ico "+n+'i-checkbox"></i><span id="'+e+'-al" class="'+n+'label">'+t.encode(t.state.get("text"))+"</span></div>"},bindStates:function(){var o=this;function e(t){o.classes.toggle("checked",t),o.aria("checked",t)}return o.state.on("change:text",function(t){o.getEl("al").firstChild.data=o.translate(t.value)}),o.state.on("change:checked change:value",function(t){o.fire("change"),e(t.value)}),o.state.on("change:icon",function(t){var e=t.value,n=o.classPrefix;if(void 0===e)return o.settings.icon;e=(o.settings.icon=e)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];e?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=e):r&&i.removeChild(r)}),o.state.get("checked")&&e(!0),o._super()}}),In=tinymce.util.Tools.resolve("tinymce.util.VK"),zn=ye.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.classes.add("combobox"),r.subinput=!0,r.ariaTarget="inp",i.menu=i.menu||i.values,i.menu&&(i.icon="caret"),r.on("click",function(t){var e=t.target,n=r.getEl();if(Nt.contains(n,e)||e===n)for(;e&&e!==n;)e.id&&-1!==e.id.indexOf("-open")&&(r.fire("action"),i.menu&&(r.showMenu(),t.aria&&r.menu.items()[0].focus())),e=e.parentNode}),r.on("keydown",function(t){var e;13===t.keyCode&&"INPUT"===t.target.nodeName&&(t.preventDefault(),r.parents().reverse().each(function(t){if(t.toJSON)return e=t,!1}),r.fire("submit",{data:e.toJSON()}))}),r.on("keyup",function(t){if("INPUT"===t.target.nodeName){var e=r.state.get("value"),n=t.target.value;n!==e&&(r.state.set("value",n),r.fire("autocomplete",t))}}),r.on("mouseover",function(t){var e=r.tooltip().moveTo(-65535);if(r.statusLevel()&&-1!==t.target.className.indexOf(r.classPrefix+"status")){var n=r.statusMessage()||"Ok",i=e.text(n).show().testMoveRel(t.target,["bc-tc","bc-tl","bc-tr"]);e.classes.toggle("tooltip-n","bc-tc"===i),e.classes.toggle("tooltip-nw","bc-tl"===i),e.classes.toggle("tooltip-ne","bc-tr"===i),e.moveRel(t.target,i)}})},statusLevel:function(t){return 0<arguments.length&&this.state.set("statusLevel",t),this.state.get("statusLevel")},statusMessage:function(t){return 0<arguments.length&&this.state.set("statusMessage",t),this.state.get("statusMessage")},showMenu:function(){var t,e=this,n=e.settings;e.menu||((t=n.menu||[]).length?t={type:"menu",items:t}:t.type=t.type||"menu",e.menu=ke.create(t).parent(e).renderTo(e.getContainerElm()),e.fire("createmenu"),e.menu.reflow(),e.menu.on("cancel",function(t){t.control===e.menu&&e.focus()}),e.menu.on("show hide",function(t){t.control.items().each(function(t){t.active(t.value()===e.value())})}).fire("show"),e.menu.on("select",function(t){e.value(t.control.value())}),e.on("focusin",function(t){"INPUT"===t.target.tagName.toUpperCase()&&e.menu.hide()}),e.aria("expanded",!0)),e.menu.show(),e.menu.layoutRect({w:e.layoutRect().w}),e.menu.moveRel(e.getEl(),e.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},focus:function(){this.getEl("inp").focus()},repaint:function(){var t,e,n=this,i=n.getEl(),r=n.getEl("open"),o=n.layoutRect(),s=0,a=i.firstChild;n.statusLevel()&&"none"!==n.statusLevel()&&(s=parseInt(Mt.getRuntimeStyle(a,"padding-right"),10)-parseInt(Mt.getRuntimeStyle(a,"padding-left"),10)),t=r?o.w-Mt.getSize(r).width-10:o.w-10;var l=_.document;return l.all&&(!l.documentMode||l.documentMode<=8)&&(e=n.layoutRect().h-2+"px"),Nt(a).css({width:t-s,lineHeight:e}),n._super(),n},postRender:function(){var e=this;return Nt(this.getEl("inp")).on("change",function(t){e.state.set("value",t.target.value),e.fire("change",t)}),e._super()},renderHtml:function(){var t,e,n,i=this,r=i._id,o=i.settings,s=i.classPrefix,a=i.state.get("value")||"",l="",u="";return"spellcheck"in o&&(u+=' spellcheck="'+o.spellcheck+'"'),o.maxLength&&(u+=' maxlength="'+o.maxLength+'"'),o.size&&(u+=' size="'+o.size+'"'),o.subtype&&(u+=' type="'+o.subtype+'"'),n='<i id="'+r+'-status" class="mce-status mce-ico" style="display: none"></i>',i.disabled()&&(u+=' disabled="disabled"'),(t=o.icon)&&"caret"!==t&&(t=s+"ico "+s+"i-"+o.icon),e=i.state.get("text"),(t||e)&&(l='<div id="'+r+'-open" class="'+s+"btn "+s+'open" tabIndex="-1" role="button"><button id="'+r+'-action" type="button" hidefocus="1" tabindex="-1">'+("caret"!==t?'<i class="'+t+'"></i>':'<i class="'+s+'caret"></i>')+(e?(t?" ":"")+e:"")+"</button></div>",i.classes.add("has-open")),'<div id="'+r+'" class="'+i.classes+'"><input id="'+r+'-inp" class="'+s+'textbox" value="'+i.encode(a,!1)+'" hidefocus="1"'+u+' placeholder="'+i.encode(o.placeholder)+'" />'+n+l+"</div>"},value:function(t){return arguments.length?(this.state.set("value",t),this):(this.state.get("rendered")&&this.state.set("value",this.getEl("inp").value),this.state.get("value"))},showAutoComplete:function(t,i){var r=this;if(0!==t.length){r.menu?r.menu.items().remove():r.menu=ke.create({type:"menu",classes:"combobox-menu",layout:"flow"}).parent(r).renderTo(),C.each(t,function(t){var e,n;r.menu.add({text:t.title,url:t.previewUrl,match:i,classes:"menu-item-ellipsis",onclick:(e=t.value,n=t.title,function(){r.fire("selectitem",{title:n,value:e})})})}),r.menu.renderNew(),r.hideMenu(),r.menu.on("cancel",function(t){t.control.parent()===r.menu&&(t.stopPropagation(),r.focus(),r.hideMenu())}),r.menu.on("select",function(){r.focus()});var e=r.layoutRect().w;r.menu.layoutRect({w:e,minW:0,maxW:e}),r.menu.repaint(),r.menu.reflow(),r.menu.show(),r.menu.moveRel(r.getEl(),r.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])}else r.hideMenu()},hideMenu:function(){this.menu&&this.menu.hide()},bindStates:function(){var r=this;r.state.on("change:value",function(t){r.getEl("inp").value!==t.value&&(r.getEl("inp").value=t.value)}),r.state.on("change:disabled",function(t){r.getEl("inp").disabled=t.value}),r.state.on("change:statusLevel",function(t){var e=r.getEl("status"),n=r.classPrefix,i=t.value;Mt.css(e,"display","none"===i?"none":""),Mt.toggleClass(e,n+"i-checkmark","ok"===i),Mt.toggleClass(e,n+"i-warning","warn"===i),Mt.toggleClass(e,n+"i-error","error"===i),r.classes.toggle("has-status","none"!==i),r.repaint()}),Mt.on(r.getEl("status"),"mouseleave",function(){r.tooltip().hide()}),r.on("cancel",function(t){r.menu&&r.menu.visible()&&(t.stopPropagation(),r.hideMenu())});var n=function(t,e){e&&0<e.items().length&&e.items().eq(t)[0].focus()};return r.on("keydown",function(t){var e=t.keyCode;"INPUT"===t.target.nodeName&&(e===In.DOWN?(t.preventDefault(),r.fire("autocomplete"),n(0,r.menu)):e===In.UP&&(t.preventDefault(),n(-1,r.menu)))}),r._super()},remove:function(){Nt(this.getEl("inp")).off(),this.menu&&this.menu.remove(),this._super()}}),Fn=zn.extend({init:function(t){var e=this;t.spellcheck=!1,t.onaction&&(t.icon="none"),e._super(t),e.classes.add("colorbox"),e.on("change keyup postrender",function(){e.repaintColor(e.value())})},repaintColor:function(t){var e=this.getEl("open"),n=e?e.getElementsByTagName("i")[0]:null;if(n)try{n.style.background=t}catch(i){}},bindStates:function(){var e=this;return e.state.on("change:value",function(t){e.state.get("rendered")&&e.repaintColor(t.value)}),e._super()}}),Un=Dn.extend({showPanel:function(){var e=this,t=e.settings;if(e.classes.add("opened"),e.panel)e.panel.show();else{var n=t.panel;n.type&&(n={layout:"grid",items:n}),n.role=n.role||"dialog",n.popover=!0,n.autohide=!0,n.ariaRoot=!0,e.panel=new Ve(n).on("hide",function(){e.classes.remove("opened")}).on("cancel",function(t){t.stopPropagation(),e.focus(),e.hidePanel()}).parent(e).renderTo(e.getContainerElm()),e.panel.fire("show"),e.panel.reflow()}var i=e.panel.testMoveRel(e.getEl(),t.popoverAlign||(e.isRtl()?["bc-tc","bc-tl","bc-tr"]:["bc-tc","bc-tr","bc-tl","tc-bc","tc-br","tc-bl"]));e.panel.classes.toggle("start","l"===i.substr(-1)),e.panel.classes.toggle("end","r"===i.substr(-1));var r="t"===i.substr(0,1);e.panel.classes.toggle("bottom",!r),e.panel.classes.toggle("top",r),e.panel.moveRel(e.getEl(),i)},hidePanel:function(){this.panel&&this.panel.hide()},postRender:function(){var e=this;return e.aria("haspopup",!0),e.on("click",function(t){t.control===e&&(e.panel&&e.panel.visible()?e.hidePanel():(e.showPanel(),e.panel.focus(!!t.aria)))}),e._super()},remove:function(){return this.panel&&(this.panel.remove(),this.panel=null),this._super()}}),Vn=v.DOM,qn=Un.extend({init:function(t){this._super(t),this.classes.add("splitbtn"),this.classes.add("colorbutton")},color:function(t){return t?(this._color=t,this.getEl("preview").style.backgroundColor=t,this):this._color},resetColor:function(){return this._color=null,this.getEl("preview").style.backgroundColor=null,this},renderHtml:function(){var t=this,e=t._id,n=t.classPrefix,i=t.state.get("text"),r=t.settings.icon?n+"ico "+n+"i-"+t.settings.icon:"",o=t.settings.image?" style=\"background-image: url('"+t.settings.image+"')\"":"",s="";return i&&(t.classes.add("btn-has-text"),s='<span class="'+n+'txt">'+t.encode(i)+"</span>"),'<div id="'+e+'" class="'+t.classes+'" role="button" tabindex="-1" aria-haspopup="true"><button role="presentation" hidefocus="1" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+o+"></i>":"")+'<span id="'+e+'-preview" class="'+n+'preview"></span>'+s+'</button><button type="button" class="'+n+'open" hidefocus="1" tabindex="-1"> <i class="'+n+'caret"></i></button></div>'},postRender:function(){var e=this,n=e.settings.onclick;return e.on("click",function(t){t.aria&&"down"===t.aria.key||t.control!==e||Vn.getParent(t.target,"."+e.classPrefix+"open")||(t.stopImmediatePropagation(),n.call(e,t))}),delete e.settings.onclick,e._super()}}),Yn=tinymce.util.Tools.resolve("tinymce.util.Color"),$n=ye.extend({Defaults:{classes:"widget colorpicker"},init:function(t){this._super(t)},postRender:function(){var n,i,r,o,s,a=this,l=a.color();function u(t,e){var n,i,r=Mt.getPos(t);return n=e.pageX-r.x,i=e.pageY-r.y,{x:n=Math.max(0,Math.min(n/t.clientWidth,1)),y:i=Math.max(0,Math.min(i/t.clientHeight,1))}}function c(t,e){var n=(360-t.h)/360;Mt.css(r,{top:100*n+"%"}),e||Mt.css(s,{left:t.s+"%",top:100-t.v+"%"}),o.style.background=Yn({s:100,v:100,h:t.h}).toHex(),a.color().parse({s:t.s,v:t.v,h:t.h})}function t(t){var e;e=u(o,t),n.s=100*e.x,n.v=100*(1-e.y),c(n),a.fire("change")}function e(t){var e;e=u(i,t),(n=l.toHsv()).h=360*(1-e.y),c(n,!0),a.fire("change")}i=a.getEl("h"),r=a.getEl("hp"),o=a.getEl("sv"),s=a.getEl("svp"),a._repaint=function(){c(n=l.toHsv())},a._super(),a._svdraghelper=new Ce(a._id+"-sv",{start:t,drag:t}),a._hdraghelper=new Ce(a._id+"-h",{start:e,drag:e}),a._repaint()},rgb:function(){return this.color().toRgb()},value:function(t){if(!arguments.length)return this.color().toHex();this.color().parse(t),this._rendered&&this._repaint()},color:function(){return this._color||(this._color=Yn()),this._color},renderHtml:function(){var t,e=this._id,o=this.classPrefix,s="#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000";return t='<div id="'+e+'-h" class="'+o+'colorpicker-h" style="background: -ms-linear-gradient(top,'+s+");background: linear-gradient(to bottom,"+s+');">'+function(){var t,e,n,i,r="";for(n="filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=",t=0,e=(i=s.split(",")).length-1;t<e;t++)r+='<div class="'+o+'colorpicker-h-chunk" style="height:'+100/e+"%;"+n+i[t]+",endColorstr="+i[t+1]+");-ms-"+n+i[t]+",endColorstr="+i[t+1]+')"></div>';return r}()+'<div id="'+e+'-hp" class="'+o+'colorpicker-h-marker"></div></div>','<div id="'+e+'" class="'+this.classes+'"><div id="'+e+'-sv" class="'+o+'colorpicker-sv"><div class="'+o+'colorpicker-overlay1"><div class="'+o+'colorpicker-overlay2"><div id="'+e+'-svp" class="'+o+'colorpicker-selector1"><div class="'+o+'colorpicker-selector2"></div></div></div></div></div>'+t+"</div>"}}),Xn=ye.extend({init:function(t){t=C.extend({height:100,text:"Drop an image here",multiple:!1,accept:null},t),this._super(t),this.classes.add("dropzone"),t.multiple&&this.classes.add("multiple")},renderHtml:function(){var t,e,n=this.settings;return t={id:this._id,hidefocus:"1"},e=Mt.create("div",t,"<span>"+this.translate(n.text)+"</span>"),n.height&&Mt.css(e,"height",n.height+"px"),n.width&&Mt.css(e,"width",n.width+"px"),e.className=this.classes,e.outerHTML},postRender:function(){var i=this,t=function(t){t.preventDefault(),i.classes.toggle("dragenter"),i.getEl().className=i.classes};i._super(),i.$el.on("dragover",function(t){t.preventDefault()}),i.$el.on("dragenter",t),i.$el.on("dragleave",t),i.$el.on("drop",function(t){if(t.preventDefault(),!i.state.get("disabled")){var e=function(t){var e=i.settings.accept;if("string"!=typeof e)return t;var n=new RegExp("("+e.split(/\s*,\s*/).join("|")+")$","i");return C.grep(t,function(t){return n.test(t.name)})}(t.dataTransfer.files);i.value=function(){return e.length?i.settings.multiple?e:e[0]:null},e.length&&i.fire("change",t)}})},remove:function(){this.$el.off(),this._super()}}),jn=ye.extend({init:function(t){var n=this;t.delimiter||(t.delimiter="\xbb"),n._super(t),n.classes.add("path"),n.canFocus=!0,n.on("click",function(t){var e;(e=t.target.getAttribute("data-index"))&&n.fire("select",{value:n.row()[e],index:e})}),n.row(n.settings.row)},focus:function(){return this.getEl().firstChild.focus(),this},row:function(t){return arguments.length?(this.state.set("row",t),this):this.state.get("row")},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'">'+this._getDataPathHtml(this.state.get("row"))+"</div>"},bindStates:function(){var e=this;return e.state.on("change:row",function(t){e.innerHtml(e._getDataPathHtml(t.value))}),e._super()},_getDataPathHtml:function(t){var e,n,i=t||[],r="",o=this.classPrefix;for(e=0,n=i.length;e<n;e++)r+=(0<e?'<div class="'+o+'divider" aria-hidden="true"> '+this.settings.delimiter+" </div>":"")+'<div role="button" class="'+o+"path-item"+(e===n-1?" "+o+"last":"")+'" data-index="'+e+'" tabindex="-1" id="'+this._id+"-"+e+'" aria-level="'+(e+1)+'">'+i[e].name+"</div>";return r||(r='<div class="'+o+'path-item">\xa0</div>'),r}}),Jn=jn.extend({postRender:function(){var o=this,s=o.settings.editor;function a(t){if(1===t.nodeType){if("BR"===t.nodeName||t.getAttribute("data-mce-bogus"))return!0;if("bookmark"===t.getAttribute("data-mce-type"))return!0}return!1}return!1!==s.settings.elementpath&&(o.on("select",function(t){s.focus(),s.selection.select(this.row()[t.index].element),s.nodeChanged()}),s.on("nodeChange",function(t){for(var e=[],n=t.parents,i=n.length;i--;)if(1===n[i].nodeType&&!a(n[i])){var r=s.fire("ResolveName",{name:n[i].nodeName.toLowerCase(),target:n[i]});if(r.isDefaultPrevented()||e.push({name:r.name,element:n[i]}),r.isPropagationStopped())break}o.row(e)})),o._super()}}),Gn=Pe.extend({Defaults:{layout:"flex",align:"center",defaults:{flex:1}},renderHtml:function(){var t=this,e=t._layout,n=t.classPrefix;return t.classes.add("formitem"),e.preRender(t),'<div id="'+t._id+'" class="'+t.classes+'" hidefocus="1" tabindex="-1">'+(t.settings.title?'<div id="'+t._id+'-title" class="'+n+'title">'+t.settings.title+"</div>":"")+'<div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+(t.settings.html||"")+e.renderHtml(t)+"</div></div>"}}),Kn=Pe.extend({Defaults:{containerCls:"form",layout:"flex",direction:"column",align:"stretch",flex:1,padding:15,labelGap:30,spacing:10,callbacks:{submit:function(){this.submit()}}},preRender:function(){var i=this,t=i.items();i.settings.formItemDefaults||(i.settings.formItemDefaults={layout:"flex",autoResize:"overflow",defaults:{flex:1}}),t.each(function(t){var e,n=t.settings.label;n&&((e=new Gn(C.extend({items:{type:"label",id:t._id+"-l",text:n,flex:0,forId:t._id,disabled:t.disabled()}},i.settings.formItemDefaults))).type="formitem",t.aria("labelledby",t._id+"-l"),"undefined"==typeof t.settings.flex&&(t.settings.flex=1),i.replace(t,e),e.add(t))})},submit:function(){return this.fire("submit",{data:this.toJSON()})},postRender:function(){this._super(),this.fromJSON(this.settings.data)},bindStates:function(){var n=this;function t(){var t,e,i=0,r=[];if(!1!==n.settings.labelGapCalc)for(("children"===n.settings.labelGapCalc?n.find("formitem"):n.items()).filter("formitem").each(function(t){var e=t.items()[0],n=e.getEl().clientWidth;i=i<n?n:i,r.push(e)}),e=n.settings.labelGap||0,t=r.length;t--;)r[t].settings.minWidth=i+e}n._super(),n.on("show",t),t()}}),Zn=Kn.extend({Defaults:{containerCls:"fieldset",layout:"flex",direction:"column",align:"stretch",flex:1,padding:"25 15 5 15",labelGap:30,spacing:10,border:1},renderHtml:function(){var t=this,e=t._layout,n=t.classPrefix;return t.preRender(),e.preRender(t),'<fieldset id="'+t._id+'" class="'+t.classes+'" hidefocus="1" tabindex="-1">'+(t.settings.title?'<legend id="'+t._id+'-title" class="'+n+'fieldset-title">'+t.settings.title+"</legend>":"")+'<div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+(t.settings.html||"")+e.renderHtml(t)+"</div></fieldset>"}}),Qn=0,ti=function(t){if(null===t||t===undefined)throw new Error("Node cannot be null or undefined");return{dom:ut(t)}},ei={fromHtml:function(t,e){var n=(e||_.document).createElement("div");if(n.innerHTML=t,!n.hasChildNodes()||1<n.childNodes.length)throw _.console.error("HTML does not have a single root node",t),new Error("HTML must have a single root node");return ti(n.childNodes[0])},fromTag:function(t,e){var n=(e||_.document).createElement(t);return ti(n)},fromText:function(t,e){var n=(e||_.document).createTextNode(t);return ti(n)},fromDom:ti,fromPoint:function(t,e,n){var i=t.dom();return vt.from(i.elementFromPoint(e,n)).map(ti)}},ni=(_.Node.ATTRIBUTE_NODE,_.Node.CDATA_SECTION_NODE,_.Node.COMMENT_NODE,_.Node.DOCUMENT_NODE),ii=(_.Node.DOCUMENT_TYPE_NODE,_.Node.DOCUMENT_FRAGMENT_NODE,_.Node.ELEMENT_NODE),ri=(_.Node.TEXT_NODE,_.Node.PROCESSING_INSTRUCTION_NODE,_.Node.ENTITY_REFERENCE_NODE,_.Node.ENTITY_NODE,_.Node.NOTATION_NODE,function(t,e){var n=function(t,e){for(var n=0;n<t.length;n++){var i=t[n];if(i.test(e))return i}return undefined}(t,e);if(!n)return{major:0,minor:0};var i=function(t){return Number(e.replace(n,"$"+t))};return si(i(1),i(2))}),oi=function(){return si(0,0)},si=function(t,e){return{major:t,minor:e}},ai={nu:si,detect:function(t,e){var n=String(e).toLowerCase();return 0===t.length?oi():ri(t,n)},unknown:oi},li="Firefox",ui=function(t,e){return function(){return e===t}},ci=function(t){var e=t.current;return{current:e,version:t.version,isEdge:ui("Edge",e),isChrome:ui("Chrome",e),isIE:ui("IE",e),isOpera:ui("Opera",e),isFirefox:ui(li,e),isSafari:ui("Safari",e)}},di={unknown:function(){return ci({current:undefined,version:ai.unknown()})},nu:ci,edge:ut("Edge"),chrome:ut("Chrome"),ie:ut("IE"),opera:ut("Opera"),firefox:ut(li),safari:ut("Safari")},fi="Windows",hi="Android",mi="Solaris",gi="FreeBSD",pi=function(t,e){return function(){return e===t}},vi=function(t){var e=t.current;return{current:e,version:t.version,isWindows:pi(fi,e),isiOS:pi("iOS",e),isAndroid:pi(hi,e),isOSX:pi("OSX",e),isLinux:pi("Linux",e),isSolaris:pi(mi,e),isFreeBSD:pi(gi,e)}},yi={unknown:function(){return vi({current:undefined,version:ai.unknown()})},nu:vi,windows:ut(fi),ios:ut("iOS"),android:ut(hi),linux:ut("Linux"),osx:ut("OSX"),solaris:ut(mi),freebsd:ut(gi)},bi=function(t,e){var n=String(e).toLowerCase();return function(t,e){for(var n=0,i=t.length;n<i;n++){var r=t[n];if(e(r,n,t))return vt.some(r)}return vt.none()}(t,function(t){return t.search(n)})},xi=function(t,n){return bi(t,n).map(function(t){var e=ai.detect(t.versionRegexes,n);return{current:t.name,version:e}})},wi=function(t,n){return bi(t,n).map(function(t){var e=ai.detect(t.versionRegexes,n);return{current:t.name,version:e}})},_i=function(t,e){return-1!==t.indexOf(e)},Ri=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,Ci=function(e){return function(t){return _i(t,e)}},ki=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(t){return _i(t,"edge/")&&_i(t,"chrome")&&_i(t,"safari")&&_i(t,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,Ri],search:function(t){return _i(t,"chrome")&&!_i(t,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(t){return _i(t,"msie")||_i(t,"trident")}},{name:"Opera",versionRegexes:[Ri,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:Ci("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:Ci("firefox")},{name:"Safari",versionRegexes:[Ri,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(t){return(_i(t,"safari")||_i(t,"mobile/"))&&_i(t,"applewebkit")}}],Ei=[{name:"Windows",search:Ci("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(t){return _i(t,"iphone")||_i(t,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:Ci("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:Ci("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:Ci("linux"),versionRegexes:[]},{name:"Solaris",search:Ci("sunos"),versionRegexes:[]},{name:"FreeBSD",search:Ci("freebsd"),versionRegexes:[]}],Hi={browsers:ut(ki),oses:ut(Ei)},Ti=function(t){var e,n,i,r,o,s,a,l,u,c,d,f=Hi.browsers(),h=Hi.oses(),m=xi(f,t).fold(di.unknown,di.nu),g=wi(h,t).fold(yi.unknown,yi.nu);return{browser:m,os:g,deviceType:(n=m,i=t,r=(e=g).isiOS()&&!0===/ipad/i.test(i),o=e.isiOS()&&!r,s=e.isAndroid()&&3===e.version.major,a=e.isAndroid()&&4===e.version.major,l=r||s||a&&!0===/mobile/i.test(i),u=e.isiOS()||e.isAndroid(),c=u&&!l,d=n.isSafari()&&e.isiOS()&&!1===/safari/i.test(i),{isiPad:ut(r),isiPhone:ut(o),isTablet:ut(l),isPhone:ut(c),isTouch:ut(u),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:ut(d)})}},Si=(Ze=!(Ge=function(){var t=_.navigator.userAgent;return Ti(t)}),function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];return Ze||(Ze=!0,Ke=Ge.apply(null,t)),Ke}),Mi=ii,Ni=ni,Oi=function(t){return t.nodeType!==Mi&&t.nodeType!==Ni||0===t.childElementCount},Pi=(Si().browser.isIE(),function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e]}("element","offset"),C.trim),Wi=function(e){return function(t){if(t&&1===t.nodeType){if(t.contentEditable===e)return!0;if(t.getAttribute("data-mce-contenteditable")===e)return!0}return!1}},Di=Wi("true"),Ai=Wi("false"),Bi=function(t,e,n,i,r){return{type:t,title:e,url:n,level:i,attach:r}},Li=function(t){return t.innerText||t.textContent},Ii=function(t){return t.id?t.id:(e="h",n=(new Date).getTime(),e+"_"+Math.floor(1e9*Math.random())+ ++Qn+String(n));var e,n},zi=function(t){return(e=t)&&"A"===e.nodeName&&(e.id||e.name)&&Ui(t);var e},Fi=function(t){return t&&/^(H[1-6])$/.test(t.nodeName)},Ui=function(t){return function(t){for(;t=t.parentNode;){var e=t.contentEditable;if(e&&"inherit"!==e)return Di(t)}return!1}(t)&&!Ai(t)},Vi=function(t){return Fi(t)&&Ui(t)},qi=function(t){var e,n=Ii(t);return Bi("header",Li(t),"#"+n,Fi(e=t)?parseInt(e.nodeName.substr(1),10):0,function(){t.id=n})},Yi=function(t){var e=t.id||t.name,n=Li(t);return Bi("anchor",n||"#"+e,"#"+e,0,lt)},$i=function(t){var e,n,i,r,o,s;return e="h1,h2,h3,h4,h5,h6,a:not([href])",n=t,Rt((Si().browser.isIE(),function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e]}("element","offset"),i=ei.fromDom(n),r=e,s=(o=i)===undefined?_.document:o.dom(),Oi(s)?[]:Rt(s.querySelectorAll(r),ei.fromDom)),function(t){return t.dom()})},Xi=function(t){return 0<Pi(t.title).length},ji=function(t){var e,n=$i(t);return kt((e=n,Rt(kt(e,Vi),qi)).concat(Rt(kt(n,zi),Yi)),Xi)},Ji={},Gi=function(t){return{title:t.title,value:{title:{raw:t.title},url:t.url,attach:t.attach}}},Ki=function(t,e){return{title:t,value:{title:t,url:e,attach:lt}}},Zi=function(t,e,n){var i=e in t?t[e]:n;return!1===i?null:i},Qi=function(t,i,r,e){var n,o,s,a,l,u,c={title:"-"},d=function(t){var e=t.hasOwnProperty(r)?t[r]:[],n=kt(e,function(t){return e=t,!_t(i,function(t){return t.url===e});var e});return C.map(n,function(t){return{title:t,value:{title:t,url:t,attach:lt}}})},f=function(e){var t,n=kt(i,function(t){return t.type===e});return t=n,C.map(t,Gi)};return!1===e.typeahead_urls?[]:"file"===r?(n=[er(t,d(Ji)),er(t,f("header")),er(t,(a=f("anchor"),l=Zi(e,"anchor_top","#top"),u=Zi(e,"anchor_bottom","#bottom"),null!==l&&a.unshift(Ki("<top>",l)),null!==u&&a.push(Ki("<bottom>",u)),a))],o=function(t,e){return 0===t.length||0===e.length?t.concat(e):t.concat(c,e)},s=[],Ct(n,function(t){s=o(s,t)}),s):er(t,d(Ji))},tr=function(t,e){var n,i,r,o=Ji[e];/^https?/.test(t)&&(o?(n=o,i=t,r=wt(n,i),-1===r?vt.none():vt.some(r)).isNone()&&(Ji[e]=o.slice(0,5).concat(t)):Ji[e]=[t])},er=function(t,e){var n=t.toLowerCase(),i=C.grep(e,function(t){return-1!==t.title.toLowerCase().indexOf(n)});return 1===i.length&&i[0].title===t?[]:i},nr=function(o,t,n){var i=t.filepicker_validator_handler;i&&o.state.on("change:value",function(t){var e;0!==(e=t.value).length?i({url:e,type:n},function(t){var e,n,i,r=(n=(e=t).status,i=e.message,"valid"===n?{status:"ok",message:i}:"unknown"===n?{status:"warn",message:i}:"invalid"===n?{status:"warn",message:i}:{status:"none",message:""});o.statusMessage(r.message),o.statusLevel(r.status)}):o.statusLevel("none")})},ir=zn.extend({Statics:{clearHistory:function(){Ji={}}},init:function(t){var e,n,i,r,o,s,a,l,u=this,c=window.tinymce?window.tinymce.activeEditor:N.activeEditor,d=c.settings,f=t.filetype;t.spellcheck=!1,(i=d.file_picker_types||d.file_browser_callback_types)&&(i=C.makeMap(i,/[, ]/)),i&&!i[f]||(!(n=d.file_picker_callback)||i&&!i[f]?!(n=d.file_browser_callback)||i&&!i[f]||(e=function(){n(u.getEl("inp").id,u.value(),f,window)}):e=function(){var t=u.fire("beforecall").meta;t=C.extend({filetype:f},t),n.call(c,function(t,e){u.value(t).fire("change",{meta:e})},u.value(),t)}),e&&(t.icon="browse",t.onaction=e),u._super(t),u.classes.add("filepicker"),r=u,o=d,s=c.getBody(),a=f,l=function(t){var e=ji(s),n=Qi(t,e,a,o);r.showAutoComplete(n,t)},r.on("autocomplete",function(){l(r.value())}),r.on("selectitem",function(t){var e=t.value;r.value(e.url);var n,i=(n=e.title).raw?n.raw:n;"image"===a?r.fire("change",{meta:{alt:i,attach:e.attach}}):r.fire("change",{meta:{text:i,attach:e.attach}}),r.focus()}),r.on("click",function(t){0===r.value().length&&"INPUT"===t.target.nodeName&&l("")}),r.on("PostRender",function(){r.getRoot().on("submit",function(t){t.isDefaultPrevented()||tr(r.value(),a)})}),nr(u,d,f)}}),rr=Wn.extend({recalc:function(t){var e=t.layoutRect(),n=t.paddingBox;t.items().filter(":visible").each(function(t){t.layoutRect({x:n.left,y:n.top,w:e.innerW-n.right-n.left,h:e.innerH-n.top-n.bottom}),t.recalc&&t.recalc()})}}),or=Wn.extend({recalc:function(t){var e,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,y,b,x,w,_,R,C,k,E,H,T,S,M,N,O,P,W,D,A,B,L=[],I=Math.max,z=Math.min;for(i=t.items().filter(":visible"),r=t.layoutRect(),o=t.paddingBox,s=t.settings,f=t.isRtl()?s.direction||"row-reversed":s.direction,a=s.align,l=t.isRtl()?s.pack||"end":s.pack,u=s.spacing||0,"row-reversed"!==f&&"column-reverse"!==f||(i=i.set(i.toArray().reverse()),f=f.split("-")[0]),"column"===f?(C="y",_="h",R="minH",k="maxH",H="innerH",E="top",T="deltaH",S="contentH",W="left",O="w",M="x",N="innerW",P="minW",D="right",A="deltaW",B="contentW"):(C="x",_="w",R="minW",k="maxW",H="innerW",E="left",T="deltaW",S="contentW",W="top",O="h",M="y",N="innerH",P="minH",D="bottom",A="deltaH",B="contentH"),d=r[H]-o[E]-o[E],w=c=0,e=0,n=i.length;e<n;e++)m=(h=i[e]).layoutRect(),d-=e<n-1?u:0,0<(g=h.settings.flex)&&(c+=g,m[k]&&L.push(h),m.flex=g),d-=m[R],w<(p=o[W]+m[P]+o[D])&&(w=p);if((b={})[R]=d<0?r[R]-d+r[T]:r[H]-d+r[T],b[P]=w+r[A],b[S]=r[H]-d,b[B]=w,b.minW=z(b.minW,r.maxW),b.minH=z(b.minH,r.maxH),b.minW=I(b.minW,r.startMinWidth),b.minH=I(b.minH,r.startMinHeight),!r.autoResize||b.minW===r.minW&&b.minH===r.minH){for(y=d/c,e=0,n=L.length;e<n;e++)(v=(m=(h=L[e]).layoutRect())[k])<(p=m[R]+m.flex*y)?(d-=m[k]-m[R],c-=m.flex,m.flex=0,m.maxFlexSize=v):m.maxFlexSize=0;for(y=d/c,x=o[E],b={},0===c&&("end"===l?x=d+o[E]:"center"===l?(x=Math.round(r[H]/2-(r[H]-d)/2)+o[E])<0&&(x=o[E]):"justify"===l&&(x=o[E],u=Math.floor(d/(i.length-1)))),b[M]=o[W],e=0,n=i.length;e<n;e++)p=(m=(h=i[e]).layoutRect()).maxFlexSize||m[R],"center"===a?b[M]=Math.round(r[N]/2-m[O]/2):"stretch"===a?(b[O]=I(m[P]||0,r[N]-o[W]-o[D]),b[M]=o[W]):"end"===a&&(b[M]=r[N]-m[O]-o.top),0<m.flex&&(p+=m.flex*y),b[_]=p,b[C]=x,h.layoutRect(b),h.recalc&&h.recalc(),x+=p+u}else if(b.w=b.minW,b.h=b.minH,t.layoutRect(b),this.recalc(t),null===t._lastRect){var F=t.parent();F&&(F._lastRect=null,F.recalc())}}}),sr=Pn.extend({Defaults:{containerClass:"flow-layout",controlClass:"flow-layout-item",endClass:"break"},recalc:function(t){t.items().filter(":visible").each(function(t){t.recalc&&t.recalc()})},isNative:function(){return!0}}),ar=function(t,e){return n=e,r=(i=t)===undefined?_.document:i.dom(),Oi(r)?vt.none():vt.from(r.querySelector(n)).map(ei.fromDom);var n,i,r},lr=function(t,e){return function(){t.execCommand("mceToggleFormat",!1,e)}},ur=function(t,e,n){var i=function(t){n(t,e)};t.formatter?t.formatter.formatChanged(e,i):t.on("init",function(){t.formatter.formatChanged(e,i)})},cr=function(t,n){return function(e){ur(t,n,function(t){e.control.active(t)})}},dr=function(i){var e=["alignleft","aligncenter","alignright","alignjustify"],r="alignleft",t=[{text:"Left",icon:"alignleft",onclick:lr(i,"alignleft")},{text:"Center",icon:"aligncenter",onclick:lr(i,"aligncenter")},{text:"Right",icon:"alignright",onclick:lr(i,"alignright")},{text:"Justify",icon:"alignjustify",onclick:lr(i,"alignjustify")}];i.addMenuItem("align",{text:"Align",menu:t}),i.addButton("align",{type:"menubutton",icon:r,menu:t,onShowMenu:function(t){var n=t.control.menu;C.each(e,function(e,t){n.items().eq(t).each(function(t){return t.active(i.formatter.match(e))})})},onPostRender:function(t){var n=t.control;C.each(e,function(e,t){ur(i,e,function(t){n.icon(r),t&&n.icon(e)})})}}),C.each({alignleft:["Align left","JustifyLeft"],aligncenter:["Align center","JustifyCenter"],alignright:["Align right","JustifyRight"],alignjustify:["Justify","JustifyFull"],alignnone:["No alignment","JustifyNone"]},function(t,e){i.addButton(e,{active:!1,tooltip:t[0],cmd:t[1],onPostRender:cr(i,e)})})},fr=function(t){return t?t.split(",")[0]:""},hr=function(l,u){return function(){var a=this;a.state.set("value",null),l.on("init nodeChange",function(t){var e,n,i,r,o=l.queryCommandValue("FontName"),s=(e=u,r=(n=o)?n.toLowerCase():"",C.each(e,function(t){t.value.toLowerCase()===r&&(i=t.value)}),C.each(e,function(t){i||fr(t.value).toLowerCase()!==fr(r).toLowerCase()||(i=t.value)}),i);a.value(s||null),!s&&o&&a.text(fr(o))})}},mr=function(n){n.addButton("fontselect",function(){var t,e=(t=function(t){for(var e=(t=t.replace(/;$/,"").split(";")).length;e--;)t[e]=t[e].split("=");return t}(n.settings.font_formats||"Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats"),C.map(t,function(t){return{text:{raw:t[0]},value:t[1],textStyle:-1===t[1].indexOf("dings")?"font-family:"+t[1]:""}}));return{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:hr(n,e),onselect:function(t){t.control.settings.value&&n.execCommand("FontName",!1,t.control.settings.value)}}})},gr=function(t){mr(t)},pr=function(t,e){return/[0-9.]+px$/.test(t)?(n=72*parseInt(t,10)/96,i=e||0,r=Math.pow(10,i),Math.round(n*r)/r+"pt"):t;var n,i,r},vr=function(t,e,n){var i;return C.each(t,function(t){t.value===n?i=n:t.value===e&&(i=e)}),i},yr=function(n){n.addButton("fontsizeselect",function(){var t,s,a,e=(t=n.settings.fontsize_formats||"8pt 10pt 12pt 14pt 18pt 24pt 36pt",C.map(t.split(" "),function(t){var e=t,n=t,i=t.split("=");return 1<i.length&&(e=i[0],n=i[1]),{text:e,value:n}}));return{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:e,fixedWidth:!0,onPostRender:(s=n,a=e,function(){var o=this;s.on("init nodeChange",function(t){var e,n,i,r;if(e=s.queryCommandValue("FontSize"))for(i=3;!r&&0<=i;i--)n=pr(e,i),r=vr(a,n,e);o.value(r||null),r||o.text(n)})}),onclick:function(t){t.control.settings.value&&n.execCommand("FontSize",!1,t.control.settings.value)}}})},br=function(t){yr(t)},xr=function(n,t){var i=t.length;return C.each(t,function(t){t.menu&&(t.hidden=0===xr(n,t.menu));var e=t.format;e&&(t.hidden=!n.formatter.canApply(e)),t.hidden&&i--}),i},wr=function(n,t){var i=t.items().length;return t.items().each(function(t){t.menu&&t.visible(0<wr(n,t.menu)),!t.menu&&t.settings.menu&&t.visible(0<xr(n,t.settings.menu));var e=t.settings.format;e&&t.visible(n.formatter.canApply(e)),t.visible()||i--}),i},_r=function(t){var i,r,o,e,s,n,a,l,u=(r=0,o=[],e=[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],s=function(t){var i=[];if(t)return C.each(t,function(t){var e={text:t.title,icon:t.icon};if(t.items)e.menu=s(t.items);else{var n=t.format||"custom"+r++;t.format||(t.name=n,o.push(t)),e.format=n,e.cmd=t.cmd}i.push(e)}),i},(i=t).on("init",function(){C.each(o,function(t){i.formatter.register(t.name,t)})}),{type:"menu",items:i.settings.style_formats_merge?i.settings.style_formats?s(e.concat(i.settings.style_formats)):s(e):s(i.settings.style_formats||e),onPostRender:function(t){i.fire("renderFormatsMenu",{control:t.control})},itemDefaults:{preview:!0,textStyle:function(){if(this.settings.format)return i.formatter.getCssText(this.settings.format)},onPostRender:function(){var n=this;n.parent().on("show",function(){var t,e;(t=n.settings.format)&&(n.disabled(!i.formatter.canApply(t)),n.active(i.formatter.match(t))),(e=n.settings.cmd)&&n.active(i.queryCommandState(e))})},onclick:function(){this.settings.format&&lr(i,this.settings.format)(),this.settings.cmd&&i.execCommand(this.settings.cmd)}}});n=u,t.addMenuItem("formats",{text:"Formats",menu:n}),l=u,(a=t).addButton("styleselect",{type:"menubutton",text:"Formats",menu:l,onShowMenu:function(){a.settings.style_formats_autohide&&wr(a,this.menu)}})},Rr=function(n,t){return function(){var r,o,s,e=[];return C.each(t,function(t){e.push({text:t[0],value:t[1],textStyle:function(){return n.formatter.getCssText(t[1])}})}),{type:"listbox",text:t[0][0],values:e,fixedWidth:!0,onselect:function(t){if(t.control){var e=t.control.value();lr(n,e)()}},onPostRender:(r=n,o=e,function(){var e=this;r.on("nodeChange",function(t){var n=r.formatter,i=null;C.each(t.parents,function(e){if(C.each(o,function(t){if(s?n.matchNode(e,s,{value:t.value})&&(i=t.value):n.matchNode(e,t.value)&&(i=t.value),i)return!1}),i)return!1}),e.value(i)})})}}},Cr=function(t){var e,n,i=function(t){for(var e=(t=t.replace(/;$/,"").split(";")).length;e--;)t[e]=t[e].split("=");return t}(t.settings.block_formats||"Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre");t.addMenuItem("blockformats",{text:"Blocks",menu:(e=t,n=i,C.map(n,function(t){return{text:t[0],onclick:lr(e,t[1]),textStyle:function(){return e.formatter.getCssText(t[1])}}}))}),t.addButton("formatselect",Rr(t,i))},kr=function(e,t){var n,i;if("string"==typeof t)i=t.split(" ");else if(C.isArray(t))return function(t){for(var e=[],n=0,i=t.length;n<i;++n){if(!Array.prototype.isPrototypeOf(t[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+t);Tt.apply(e,t[n])}return e}(C.map(t,function(t){return kr(e,t)}));return n=C.grep(i,function(t){return"|"===t||t in e.menuItems}),C.map(n,function(t){return"|"===t?{text:"-"}:e.menuItems[t]})},Er=function(t){return t&&"-"===t.text},Hr=function(t){var e=kt(t,function(t,e,n){return!Er(t)||!Er(n[e-1])});return kt(e,function(t,e,n){return!Er(t)||0<e&&e<n.length-1})},Tr=function(t){var e,n,i,r,o=t.settings.insert_button_items;return Hr(o?kr(t,o):(e=t,n="insert",i=[{text:"-"}],r=C.grep(e.menuItems,function(t){return t.context===n}),C.each(r,function(t){"before"===t.separator&&i.push({text:"|"}),t.prependToContext?i.unshift(t):i.push(t),"after"===t.separator&&i.push({text:"|"})}),i))},Sr=function(t){var e;(e=t).addButton("insert",{type:"menubutton",icon:"insert",menu:[],oncreatemenu:function(){this.menu.add(Tr(e)),this.menu.renderNew()}})},Mr=function(t){var n,i,r;n=t,C.each({bold:"Bold",italic:"Italic",underline:"Underline",strikethrough:"Strikethrough",subscript:"Subscript",superscript:"Superscript"},function(t,e){n.addButton(e,{active:!1,tooltip:t,onPostRender:cr(n,e),onclick:lr(n,e)})}),i=t,C.each({outdent:["Decrease indent","Outdent"],indent:["Increase indent","Indent"],cut:["Cut","Cut"],copy:["Copy","Copy"],paste:["Paste","Paste"],help:["Help","mceHelp"],selectall:["Select all","SelectAll"],visualaid:["Visual aids","mceToggleVisualAid"],newdocument:["New document","mceNewDocument"],removeformat:["Clear formatting","RemoveFormat"],remove:["Remove","Delete"]},function(t,e){i.addButton(e,{tooltip:t[0],cmd:t[1]})}),r=t,C.each({blockquote:["Blockquote","mceBlockQuote"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"]},function(t,e){r.addButton(e,{active:!1,tooltip:t[0],cmd:t[1],onPostRender:cr(r,e)})})},Nr=function(t){var n;Mr(t),n=t,C.each({bold:["Bold","Bold","Meta+B"],italic:["Italic","Italic","Meta+I"],underline:["Underline","Underline","Meta+U"],strikethrough:["Strikethrough","Strikethrough"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"],removeformat:["Clear formatting","RemoveFormat"],newdocument:["New document","mceNewDocument"],cut:["Cut","Cut","Meta+X"],copy:["Copy","Copy","Meta+C"],paste:["Paste","Paste","Meta+V"],selectall:["Select all","SelectAll","Meta+A"]},function(t,e){n.addMenuItem(e,{text:t[0],icon:e,shortcut:t[2],cmd:t[1]})}),n.addMenuItem("codeformat",{text:"Code",icon:"code",onclick:lr(n,"code")})},Or=function(n,i){return function(){var t=this,e=function(){var t="redo"===i?"hasRedo":"hasUndo";return!!n.undoManager&&n.undoManager[t]()};t.disabled(!e()),n.on("Undo Redo AddUndo TypingUndo ClearUndos SwitchMode",function(){t.disabled(n.readonly||!e())})}},Pr=function(t){var e,n;(e=t).addMenuItem("undo",{text:"Undo",icon:"undo",shortcut:"Meta+Z",onPostRender:Or(e,"undo"),cmd:"undo"}),e.addMenuItem("redo",{text:"Redo",icon:"redo",shortcut:"Meta+Y",onPostRender:Or(e,"redo"),cmd:"redo"}),(n=t).addButton("undo",{tooltip:"Undo",onPostRender:Or(n,"undo"),cmd:"undo"}),n.addButton("redo",{tooltip:"Redo",onPostRender:Or(n,"redo"),cmd:"redo"})},Wr=function(t){var e,n;(e=t).addMenuItem("visualaid",{text:"Visual aids",selectable:!0,onPostRender:(n=e,function(){var e=this;n.on("VisualAid",function(t){e.active(t.hasVisual)}),e.active(n.hasVisual)}),cmd:"mceToggleVisualAid"})},Dr={setup:function(t){var e;t.rtl&&(ce.rtl=!0),t.on("mousedown progressstate",function(){Ve.hideAll()}),(e=t).settings.ui_container&&(h.container=ar(ei.fromDom(_.document.body),e.settings.ui_container).fold(ut(null),function(t){return t.dom()})),ye.tooltips=!h.iOS,ce.translate=function(t){return N.translate(t)},Cr(t),dr(t),Nr(t),Pr(t),br(t),gr(t),_r(t),Wr(t),Sr(t)}},Ar=Wn.extend({recalc:function(t){var e,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,y,b,x,w,_,R,C,k,E,H,T,S=[],M=[];e=t.settings,r=t.items().filter(":visible"),o=t.layoutRect(),i=e.columns||Math.ceil(Math.sqrt(r.length)),n=Math.ceil(r.length/i),y=e.spacingH||e.spacing||0,b=e.spacingV||e.spacing||0,x=e.alignH||e.align,w=e.alignV||e.align,p=t.paddingBox,T="reverseRows"in e?e.reverseRows:t.isRtl(),x&&"string"==typeof x&&(x=[x]),w&&"string"==typeof w&&(w=[w]);for(d=0;d<i;d++)S.push(0);for(f=0;f<n;f++)M.push(0);for(f=0;f<n;f++)for(d=0;d<i&&(c=r[f*i+d]);d++)C=(u=c.layoutRect()).minW,k=u.minH,S[d]=C>S[d]?C:S[d],M[f]=k>M[f]?k:M[f];for(E=o.innerW-p.left-p.right,d=_=0;d<i;d++)_+=S[d]+(0<d?y:0),E-=(0<d?y:0)+S[d];for(H=o.innerH-p.top-p.bottom,f=R=0;f<n;f++)R+=M[f]+(0<f?b:0),H-=(0<f?b:0)+M[f];if(_+=p.left+p.right,R+=p.top+p.bottom,(l={}).minW=_+(o.w-o.innerW),l.minH=R+(o.h-o.innerH),l.contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH,l.minW=Math.min(l.minW,o.maxW),l.minH=Math.min(l.minH,o.maxH),l.minW=Math.max(l.minW,o.startMinWidth),l.minH=Math.max(l.minH,o.startMinHeight),!o.autoResize||l.minW===o.minW&&l.minH===o.minH){var N;o.autoResize&&((l=t.layoutRect(l)).contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH),N="start"===e.packV?0:0<H?Math.floor(H/n):0;var O=0,P=e.flexWidths;if(P)for(d=0;d<P.length;d++)O+=P[d];else O=i;var W=E/O;for(d=0;d<i;d++)S[d]+=P?P[d]*W:W;for(m=p.top,f=0;f<n;f++){for(h=p.left,a=M[f]+N,d=0;d<i&&(c=r[T?f*i+i-1-d:f*i+d]);d++)g=c.settings,u=c.layoutRect(),s=Math.max(S[d],u.startMinWidth),u.x=h,u.y=m,"center"===(v=g.alignH||(x?x[d]||x[0]:null))?u.x=h+s/2-u.w/2:"right"===v?u.x=h+s-u.w:"stretch"===v&&(u.w=s),"center"===(v=g.alignV||(w?w[d]||w[0]:null))?u.y=m+a/2-u.h/2:"bottom"===v?u.y=m+a-u.h:"stretch"===v&&(u.h=a),c.layoutRect(u),h+=s+y,c.recalc&&c.recalc();m+=a+b}}else if(l.w=l.minW,l.h=l.minH,t.layoutRect(l),this.recalc(t),null===t._lastRect){var D=t.parent();D&&(D._lastRect=null,D.recalc())}}}),Br=ye.extend({renderHtml:function(){var t=this;return t.classes.add("iframe"),t.canFocus=!1,'<iframe id="'+t._id+'" class="'+t.classes+'" tabindex="-1" src="'+(t.settings.url||"javascript:''")+'" frameborder="0"></iframe>'},src:function(t){this.getEl().src=t},html:function(t,e){var n=this,i=this.getEl().contentWindow.document.body;return i?(i.innerHTML=t,e&&e()):c.setTimeout(function(){n.html(t)}),this}}),Lr=ye.extend({init:function(t){this._super(t),this.classes.add("widget").add("infobox"),this.canFocus=!1},severity:function(t){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(t)},help:function(t){this.state.set("help",t)},renderHtml:function(){var t=this,e=t.classPrefix;return'<div id="'+t._id+'" class="'+t.classes+'"><div id="'+t._id+'-body">'+t.encode(t.state.get("text"))+'<button role="button" tabindex="-1"><i class="'+e+"ico "+e+'i-help"></i></button></div></div>'},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl("body").firstChild.data=e.encode(t.value),e.state.get("rendered")&&e.updateLayoutRect()}),e.state.on("change:help",function(t){e.classes.toggle("has-help",t.value),e.state.get("rendered")&&e.updateLayoutRect()}),e._super()}}),Ir=ye.extend({init:function(t){var e=this;e._super(t),e.classes.add("widget").add("label"),e.canFocus=!1,t.multiline&&e.classes.add("autoscroll"),t.strong&&e.classes.add("strong")},initLayoutRect:function(){var t=this,e=t._super();return t.settings.multiline&&(Mt.getSize(t.getEl()).width>e.maxW&&(e.minW=e.maxW,t.classes.add("multiline")),t.getEl().style.width=e.minW+"px",e.startMinH=e.h=e.minH=Math.min(e.maxH,Mt.getSize(t.getEl()).height)),e},repaint:function(){return this.settings.multiline||(this.getEl().style.lineHeight=this.layoutRect().h+"px"),this._super()},severity:function(t){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(t)},renderHtml:function(){var t,e,n=this,i=n.settings.forId,r=n.settings.html?n.settings.html:n.encode(n.state.get("text"));return!i&&(e=n.settings.forName)&&(t=n.getRoot().find("#"+e)[0])&&(i=t._id),i?'<label id="'+n._id+'" class="'+n.classes+'"'+(i?' for="'+i+'"':"")+">"+r+"</label>":'<span id="'+n._id+'" class="'+n.classes+'">'+r+"</span>"},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.innerHtml(e.encode(t.value)),e.state.get("rendered")&&e.updateLayoutRect()}),e._super()}}),zr=Pe.extend({Defaults:{role:"toolbar",layout:"flow"},init:function(t){this._super(t),this.classes.add("toolbar")},postRender:function(){return this.items().each(function(t){t.classes.add("toolbar-item")}),this._super()}}),Fr=zr.extend({Defaults:{role:"menubar",containerCls:"menubar",ariaRoot:!0,defaults:{type:"menubutton"}}}),Ur=Dn.extend({init:function(t){var e=this;e._renderOpen=!0,e._super(t),t=e.settings,e.classes.add("menubtn"),t.fixedWidth&&e.classes.add("fixed-width"),e.aria("haspopup",!0),e.state.set("menu",t.menu||e.render())},showMenu:function(t){var e,n=this;if(n.menu&&n.menu.visible()&&!1!==t)return n.hideMenu();n.menu||(e=n.state.get("menu")||[],n.classes.add("opened"),e.length?e={type:"menu",animate:!0,items:e}:(e.type=e.type||"menu",e.animate=!0),e.renderTo?n.menu=e.parent(n).show().renderTo():n.menu=ke.create(e).parent(n).renderTo(),n.fire("createmenu"),n.menu.reflow(),n.menu.on("cancel",function(t){t.control.parent()===n.menu&&(t.stopPropagation(),n.focus(),n.hideMenu())}),n.menu.on("select",function(){n.focus()}),n.menu.on("show hide",function(t){"hide"===t.type&&t.control.parent()===n&&n.classes.remove("opened-under"),t.control===n.menu&&(n.activeMenu("show"===t.type),n.classes.toggle("opened","show"===t.type)),n.aria("expanded","show"===t.type)}).fire("show")),n.menu.show(),n.menu.layoutRect({w:n.layoutRect().w}),n.menu.repaint(),n.menu.moveRel(n.getEl(),n.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"]);var i=n.menu.layoutRect(),r=n.$el.offset().top+n.layoutRect().h;r>i.y&&r<i.y+i.h&&n.classes.add("opened-under"),n.fire("showmenu")},hideMenu:function(){this.menu&&(this.menu.items().each(function(t){t.hideMenu&&t.hideMenu()}),this.menu.hide())},activeMenu:function(t){this.classes.toggle("active",t)},renderHtml:function(){var t,e=this,n=e._id,i=e.classPrefix,r=e.settings.icon,o=e.state.get("text"),s="";return(t=e.settings.image)?(r="none","string"!=typeof t&&(t=_.window.getSelection?t[0]:t[1]),t=" style=\"background-image: url('"+t+"')\""):t="",o&&(e.classes.add("btn-has-text"),s='<span class="'+i+'txt">'+e.encode(o)+"</span>"),r=e.settings.icon?i+"ico "+i+"i-"+r:"",e.aria("role",e.parent()instanceof Fr?"menuitem":"button"),'<div id="'+n+'" class="'+e.classes+'" tabindex="-1" aria-labelledby="'+n+'"><button id="'+n+'-open" role="presentation" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+t+"></i>":"")+s+' <i class="'+i+'caret"></i></button></div>'},postRender:function(){var r=this;return r.on("click",function(t){t.control===r&&function(t,e){for(;t;){if(e===t)return!0;t=t.parentNode}return!1}(t.target,r.getEl())&&(r.focus(),r.showMenu(!t.aria),t.aria&&r.menu.items().filter(":visible")[0].focus())}),r.on("mouseenter",function(t){var e,n=t.control,i=r.parent();n&&i&&n instanceof Ur&&n.parent()===i&&(i.items().filter("MenuButton").each(function(t){t.hideMenu&&t!==n&&(t.menu&&t.menu.visible()&&(e=!0),t.hideMenu())}),e&&(n.focus(),n.showMenu()))}),r._super()},bindStates:function(){var t=this;return t.state.on("change:menu",function(){t.menu&&t.menu.remove(),t.menu=null}),t._super()},remove:function(){this._super(),this.menu&&this.menu.remove()}});function Vr(i,r){var o,s,a=this,l=ce.classPrefix;a.show=function(t,e){function n(){o&&(Nt(i).append('<div class="'+l+"throbber"+(r?" "+l+"throbber-inline":"")+'"></div>'),e&&e())}return a.hide(),o=!0,t?s=c.setTimeout(n,t):n(),a},a.hide=function(){var t=i.lastChild;return c.clearTimeout(s),t&&-1!==t.className.indexOf("throbber")&&t.parentNode.removeChild(t),o=!1,a}}var qr=Ve.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(t){if(t.autohide=!0,t.constrainToViewport=!0,"function"==typeof t.items&&(t.itemsFactory=t.items,t.items=[]),t.itemDefaults)for(var e=t.items,n=e.length;n--;)e[n]=C.extend({},t.itemDefaults,e[n]);this._super(t),this.classes.add("menu"),t.animate&&11!==h.ie&&this.classes.add("animate")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){this.hideAll(),this.fire("select")},load:function(){var e,n=this;function i(){n.throbber&&(n.throbber.hide(),n.throbber=null)}n.settings.itemsFactory&&(n.throbber||(n.throbber=new Vr(n.getEl("body"),!0),0===n.items().length?(n.throbber.show(),n.fire("loading")):n.throbber.show(100,function(){n.items().remove(),n.fire("loading")}),n.on("hide close",i)),n.requestTime=e=(new Date).getTime(),n.settings.itemsFactory(function(t){0!==t.length?n.requestTime===e&&(n.getEl().style.width="",n.getEl("body").style.width="",i(),n.items().remove(),n.getEl("body").innerHTML="",n.add(t),n.renderNew(),n.fire("loaded")):n.hide()}))},hideAll:function(){return this.find("menuitem").exec("hideMenu"),this._super()},preRender:function(){var n=this;return n.items().each(function(t){var e=t.settings;if(e.icon||e.image||e.selectable)return!(n._hasIcons=!0)}),n.settings.itemsFactory&&n.on("postrender",function(){n.settings.itemsFactory&&n.load()}),n.on("show hide",function(t){t.control===n&&("show"===t.type?c.setTimeout(function(){n.classes.add("in")},0):n.classes.remove("in"))}),n._super()}}),Yr=Ur.extend({init:function(i){var e,r,o,n,s=this;s._super(i),i=s.settings,s._values=e=i.values,e&&("undefined"!=typeof i.value&&function t(e){for(var n=0;n<e.length;n++){if(r=e[n].selected||i.value===e[n].value)return o=o||e[n].text,s.state.set("value",e[n].value),!0;if(e[n].menu&&t(e[n].menu))return!0}}(e),!r&&0<e.length&&(o=e[0].text,s.state.set("value",e[0].value)),s.state.set("menu",e)),s.state.set("text",i.text||o),s.classes.add("listbox"),s.on("select",function(t){var e=t.control;n&&(t.lastControl=n),i.multiple?e.active(!e.active()):s.value(t.control.value()),n=e})},value:function(n){return 0===arguments.length?this.state.get("value"):(void 0===n||(this.settings.values&&!function e(t){return _t(t,function(t){return t.menu?e(t.menu):t.value===n})}(this.settings.values)?null===n&&this.state.set("value",null):this.state.set("value",n)),this)},bindStates:function(){var i=this;return i.on("show",function(t){var e,n;e=t.control,n=i.value(),e instanceof qr&&e.items().each(function(t){t.hasMenus()||t.active(t.value()===n)})}),i.state.on("change:value",function(e){var n=function t(e,n){var i;if(e)for(var r=0;r<e.length;r++){if(e[r].value===n)return e[r];if(e[r].menu&&(i=t(e[r].menu,n)))return i}}(i.state.get("menu"),e.value);n?i.text(n.text):i.text(i.settings.text)}),i._super()}}),$r=ye.extend({Defaults:{border:0,role:"menuitem"},init:function(t){var e,n=this;n._super(t),t=n.settings,n.classes.add("menu-item"),t.menu&&n.classes.add("menu-item-expand"),t.preview&&n.classes.add("menu-item-preview"),"-"!==(e=n.state.get("text"))&&"|"!==e||(n.classes.add("menu-item-sep"),n.aria("role","separator"),n.state.set("text","-")),t.selectable&&(n.aria("role","menuitemcheckbox"),n.classes.add("menu-item-checkbox"),t.icon="selected"),t.preview||t.selectable||n.classes.add("menu-item-normal"),n.on("mousedown",function(t){t.preventDefault()}),t.menu&&!t.ariaHideMenu&&n.aria("haspopup",!0)},hasMenus:function(){return!!this.settings.menu},showMenu:function(){var e,n=this,t=n.settings,i=n.parent();if(i.items().each(function(t){t!==n&&t.hideMenu()}),t.menu){(e=n.menu)?e.show():((e=t.menu).length?e={type:"menu",items:e}:e.type=e.type||"menu",i.settings.itemDefaults&&(e.itemDefaults=i.settings.itemDefaults),(e=n.menu=ke.create(e).parent(n).renderTo()).reflow(),e.on("cancel",function(t){t.stopPropagation(),n.focus(),e.hide()}),e.on("show hide",function(t){t.control.items&&t.control.items().each(function(t){t.active(t.settings.selected)})}).fire("show"),e.on("hide",function(t){t.control===e&&n.classes.remove("selected")}),e.submenu=!0),e._parentMenu=i,e.classes.add("menu-sub");var r=e.testMoveRel(n.getEl(),n.isRtl()?["tl-tr","bl-br","tr-tl","br-bl"]:["tr-tl","br-bl","tl-tr","bl-br"]);e.moveRel(n.getEl(),r),r="menu-sub-"+(e.rel=r),e.classes.remove(e._lastRel).add(r),e._lastRel=r,n.classes.add("selected"),n.aria("expanded",!0)}},hideMenu:function(){var t=this;return t.menu&&(t.menu.items().each(function(t){t.hideMenu&&t.hideMenu()}),t.menu.hide(),t.aria("expanded",!1)),t},renderHtml:function(){var t,e=this,n=e._id,i=e.settings,r=e.classPrefix,o=e.state.get("text"),s=e.settings.icon,a="",l=i.shortcut,u=e.encode(i.url);function c(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function d(t){var e=i.match||"";return e?t.replace(new RegExp(c(e),"gi"),function(t){return"!mce~match["+t+"]mce~match!"}):t}function f(t){return t.replace(new RegExp(c("!mce~match["),"g"),"<b>").replace(new RegExp(c("]mce~match!"),"g"),"</b>")}return s&&e.parent().classes.add("menu-has-icons"),i.image&&(a=" style=\"background-image: url('"+i.image+"')\""),l&&(l=function(t){var e,n,i={};for(i=h.mac?{alt:"⌥",ctrl:"⌘",shift:"⇧",meta:"⌘"}:{meta:"Ctrl"},t=t.split("+"),e=0;e<t.length;e++)(n=i[t[e].toLowerCase()])&&(t[e]=n);return t.join("+")}(l)),s=r+"ico "+r+"i-"+(e.settings.icon||"none"),t="-"!==o?'<i class="'+s+'"'+a+"></i>\xa0":"",o=f(e.encode(d(o))),u=f(e.encode(d(u))),'<div id="'+n+'" class="'+e.classes+'" tabindex="-1">'+t+("-"!==o?'<span id="'+n+'-text" class="'+r+'text">'+o+"</span>":"")+(l?'<div id="'+n+'-shortcut" class="'+r+'menu-shortcut">'+l+"</div>":"")+(i.menu?'<div class="'+r+'caret"></div>':"")+(u?'<div class="'+r+'menu-item-link">'+u+"</div>":"")+"</div>"},postRender:function(){var e=this,n=e.settings,t=n.textStyle;if("function"==typeof t&&(t=t.call(this)),t){var i=e.getEl("text");i&&(i.setAttribute("style",t),e._textStyle=t)}return e.on("mouseenter click",function(t){t.control===e&&(n.menu||"click"!==t.type?(e.showMenu(),t.aria&&e.menu.focus(!0)):(e.fire("select"),c.requestAnimationFrame(function(){e.parent().hideAll()})))}),e._super(),e},hover:function(){return this.parent().items().each(function(t){t.classes.remove("selected")}),this.classes.toggle("selected",!0),this},active:function(t){return function(t,e){var n=t._textStyle;if(n){var i=t.getEl("text");i.setAttribute("style",n),e&&(i.style.color="",i.style.backgroundColor="")}}(this,t),void 0!==t&&this.aria("checked",t),this._super(t)},remove:function(){this._super(),this.menu&&this.menu.remove()}}),Xr=Ln.extend({Defaults:{classes:"radio",role:"radio"}}),jr=ye.extend({renderHtml:function(){var t=this,e=t.classPrefix;return t.classes.add("resizehandle"),"both"===t.settings.direction&&t.classes.add("resizehandle-both"),t.canFocus=!1,'<div id="'+t._id+'" class="'+t.classes+'"><i class="'+e+"ico "+e+'i-resize"></i></div>'},postRender:function(){var e=this;e._super(),e.resizeDragHelper=new Ce(this._id,{start:function(){e.fire("ResizeStart")},drag:function(t){"both"!==e.settings.direction&&(t.deltaX=0),e.fire("Resize",t)},stop:function(){e.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}});function Jr(t){var e="";if(t)for(var n=0;n<t.length;n++)e+='<option value="'+t[n]+'">'+t[n]+"</option>";return e}var Gr=ye.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(t){var n=this;n._super(t),n.settings.size&&(n.size=n.settings.size),n.settings.options&&(n._options=n.settings.options),n.on("keydown",function(t){var e;13===t.keyCode&&(t.preventDefault(),n.parents().reverse().each(function(t){if(t.toJSON)return e=t,!1}),n.fire("submit",{data:e.toJSON()}))})},options:function(t){return arguments.length?(this.state.set("options",t),this):this.state.get("options")},renderHtml:function(){var t,e=this,n="";return t=Jr(e._options),e.size&&(n=' size = "'+e.size+'"'),'<select id="'+e._id+'" class="'+e.classes+'"'+n+">"+t+"</select>"},bindStates:function(){var e=this;return e.state.on("change:options",function(t){e.getEl().innerHTML=Jr(t.value)}),e._super()}});function Kr(t,e,n){return t<e&&(t=e),n<t&&(t=n),t}function Zr(t,e,n){t.setAttribute("aria-"+e,n)}function Qr(t,e){var n,i,r,o,s;"v"===t.settings.orientation?(r="top",i="height",n="h"):(r="left",i="width",n="w"),s=t.getEl("handle"),o=((t.layoutRect()[n]||100)-Mt.getSize(s)[i])*((e-t._minValue)/(t._maxValue-t._minValue))+"px",s.style[r]=o,s.style.height=t.layoutRect().h+"px",Zr(s,"valuenow",e),Zr(s,"valuetext",""+t.settings.previewFilter(e)),Zr(s,"valuemin",t._minValue),Zr(s,"valuemax",t._maxValue)}var to=ye.extend({init:function(t){var e=this;t.previewFilter||(t.previewFilter=function(t){return Math.round(100*t)/100}),e._super(t),e.classes.add("slider"),"v"===t.orientation&&e.classes.add("vertical"),e._minValue=xt(t.minValue)?t.minValue:0,e._maxValue=xt(t.maxValue)?t.maxValue:100,e._initValue=e.state.get("value")},renderHtml:function(){var t=this._id,e=this.classPrefix;return'<div id="'+t+'" class="'+this.classes+'"><div id="'+t+'-handle" class="'+e+'slider-handle" role="slider" tabindex="-1"></div></div>'},reset:function(){this.value(this._initValue).repaint()},postRender:function(){var t,e,n,i,r,o,s,a,l,u,c,d,f,h,m=this;t=m._minValue,e=m._maxValue,"v"===m.settings.orientation?(n="screenY",i="top",r="height",o="h"):(n="screenX",i="left",r="width",o="w"),m._super(),function(o,s){function e(t){var e,n,i,r;e=Kr(e=(((e=m.value())+(r=n=o))/((i=s)-r)+.05*t)*(i-n)-n,o,s),m.value(e),m.fire("dragstart",{value:e}),m.fire("drag",{value:e}),m.fire("dragend",{value:e})}m.on("keydown",function(t){switch(t.keyCode){case 37:case 38:e(-1);break;case 39:case 40:e(1)}})}(t,e),s=t,a=e,l=m.getEl("handle"),m._dragHelper=new Ce(m._id,{handle:m._id+"-handle",start:function(t){u=t[n],c=parseInt(m.getEl("handle").style[i],10),d=(m.layoutRect()[o]||100)-Mt.getSize(l)[r],m.fire("dragstart",{value:h})},drag:function(t){var e=t[n]-u;f=Kr(c+e,0,d),l.style[i]=f+"px",h=s+f/d*(a-s),m.value(h),m.tooltip().text(""+m.settings.previewFilter(h)).show().moveRel(l,"bc tc"),m.fire("drag",{value:h})},stop:function(){m.tooltip().hide(),m.fire("dragend",{value:h})}})},repaint:function(){this._super(),Qr(this,this.value())},bindStates:function(){var e=this;return e.state.on("change:value",function(t){Qr(e,t.value)}),e._super()}}),eo=ye.extend({renderHtml:function(){return this.classes.add("spacer"),this.canFocus=!1,'<div id="'+this._id+'" class="'+this.classes+'"></div>'}}),no=Ur.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var t,e,n=this.getEl(),i=this.layoutRect();return this._super(),t=n.firstChild,e=n.lastChild,Nt(t).css({width:i.w-Mt.getSize(e).width,height:i.h-2}),Nt(e).css({height:i.h-2}),this},activeMenu:function(t){Nt(this.getEl().lastChild).toggleClass(this.classPrefix+"active",t)},renderHtml:function(){var t,e,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a=n.settings,l="";return(t=a.image)?(o="none","string"!=typeof t&&(t=_.window.getSelection?t[0]:t[1]),t=" style=\"background-image: url('"+t+"')\""):t="",o=a.icon?r+"ico "+r+"i-"+o:"",s&&(n.classes.add("btn-has-text"),l='<span class="'+r+'txt">'+n.encode(s)+"</span>"),e="boolean"==typeof a.active?' aria-pressed="'+a.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" role="button"'+e+' tabindex="-1"><button type="button" hidefocus="1" tabindex="-1">'+(o?'<i class="'+o+'"'+t+"></i>":"")+l+'</button><button type="button" class="'+r+'open" hidefocus="1" tabindex="-1">'+(n._menuBtnText?(o?"\xa0":"")+n._menuBtnText:"")+' <i class="'+r+'caret"></i></button></div>'},postRender:function(){var n=this.settings.onclick;return this.on("click",function(t){var e=t.target;if(t.control===this)for(;e;){if(t.aria&&"down"!==t.aria.key||"BUTTON"===e.nodeName&&-1===e.className.indexOf("open"))return t.stopImmediatePropagation(),void(n&&n.call(this,t));e=e.parentNode}}),delete this.settings.onclick,this._super()}}),io=sr.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}}),ro=De.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(n){var t;this.activeTabId&&(t=this.getEl(this.activeTabId),Nt(t).removeClass(this.classPrefix+"active"),t.setAttribute("aria-selected","false")),this.activeTabId="t"+n,(t=this.getEl("t"+n)).setAttribute("aria-selected","true"),Nt(t).addClass(this.classPrefix+"active"),this.items()[n].show().fire("showtab"),this.reflow(),this.items().each(function(t,e){n!==e&&t.hide()})},renderHtml:function(){var i=this,t=i._layout,r="",o=i.classPrefix;return i.preRender(),t.preRender(i),i.items().each(function(t,e){var n=i._id+"-t"+e;t.aria("role","tabpanel"),t.aria("labelledby",n),r+='<div id="'+n+'" class="'+o+'tab" unselectable="on" role="tab" aria-controls="'+t._id+'" aria-selected="false" tabIndex="-1">'+i.encode(t.settings.title)+"</div>"}),'<div id="'+i._id+'" class="'+i.classes+'" hidefocus="1" tabindex="-1"><div id="'+i._id+'-head" class="'+o+'tabs" role="tablist">'+r+'</div><div id="'+i._id+'-body" class="'+i.bodyClasses+'">'+t.renderHtml(i)+"</div></div>"},postRender:function(){var i=this;i._super(),i.settings.activeTab=i.settings.activeTab||0,i.activateTab(i.settings.activeTab),this.on("click",function(t){var e=t.target.parentNode;if(e&&e.id===i._id+"-head")for(var n=e.childNodes.length;n--;)e.childNodes[n]===t.target&&i.activateTab(n)})},initLayoutRect:function(){var t,e,n,i=this;e=(e=Mt.getSize(i.getEl("head")).width)<0?0:e,n=0,i.items().each(function(t){e=Math.max(e,t.layoutRect().minW),n=Math.max(n,t.layoutRect().minH)}),i.items().each(function(t){t.settings.x=0,t.settings.y=0,t.settings.w=e,t.settings.h=n,t.layoutRect({x:0,y:0,w:e,h:n})});var r=Mt.getSize(i.getEl("head")).height;return i.settings.minWidth=e,i.settings.minHeight=n+r,(t=i._super()).deltaH+=r,t.innerH=t.h-t.deltaH,t}}),oo=ye.extend({init:function(t){var n=this;n._super(t),n.classes.add("textbox"),t.multiline?n.classes.add("multiline"):(n.on("keydown",function(t){var e;13===t.keyCode&&(t.preventDefault(),n.parents().reverse().each(function(t){if(t.toJSON)return e=t,!1}),n.fire("submit",{data:e.toJSON()}))}),n.on("keyup",function(t){n.state.set("value",t.target.value)}))},repaint:function(){var t,e,n,i,r,o=this,s=0;t=o.getEl().style,e=o._layoutRect,r=o._lastRepaintRect||{};var a=_.document;return!o.settings.multiline&&a.all&&(!a.documentMode||a.documentMode<=8)&&(t.lineHeight=e.h-s+"px"),i=(n=o.borderBox).left+n.right+8,s=n.top+n.bottom+(o.settings.multiline?8:0),e.x!==r.x&&(t.left=e.x+"px",r.x=e.x),e.y!==r.y&&(t.top=e.y+"px",r.y=e.y),e.w!==r.w&&(t.width=e.w-i+"px",r.w=e.w),e.h!==r.h&&(t.height=e.h-s+"px",r.h=e.h),o._lastRepaintRect=r,o.fire("repaint",{},!1),o},renderHtml:function(){var e,t,n=this,i=n.settings;return e={id:n._id,hidefocus:"1"},C.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(t){e[t]=i[t]}),n.disabled()&&(e.disabled="disabled"),i.subtype&&(e.type=i.subtype),(t=Mt.create(i.multiline?"textarea":"input",e)).value=n.state.get("value"),t.className=n.classes.toString(),t.outerHTML},value:function(t){return arguments.length?(this.state.set("value",t),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var e=this;e.getEl().value=e.state.get("value"),e._super(),e.$el.on("change",function(t){e.state.set("value",t.target.value),e.fire("change",t)})},bindStates:function(){var e=this;return e.state.on("change:value",function(t){e.getEl().value!==t.value&&(e.getEl().value=t.value)}),e.state.on("change:disabled",function(t){e.getEl().disabled=t.value}),e._super()},remove:function(){this.$el.off(),this._super()}}),so=function(){return{Selector:qt,Collection:Xt,ReflowQueue:ee,Control:ce,Factory:ke,KeyboardNavigation:He,Container:Pe,DragHelper:Ce,Scrollable:We,Panel:De,Movable:pe,Resizable:Ae,FloatPanel:Ve,Window:Je,MessageBox:Qe,Tooltip:ve,Widget:ye,Progress:be,Notification:we,Layout:Pn,AbsoluteLayout:Wn,Button:Dn,ButtonGroup:Bn,Checkbox:Ln,ComboBox:zn,ColorBox:Fn,PanelButton:Un,ColorButton:qn,ColorPicker:$n,Path:jn,ElementPath:Jn,FormItem:Gn,Form:Kn,FieldSet:Zn,FilePicker:ir,FitLayout:rr,FlexLayout:or,FlowLayout:sr,FormatControls:Dr,GridLayout:Ar,Iframe:Br,InfoBox:Lr,Label:Ir,Toolbar:zr,MenuBar:Fr,MenuButton:Ur,MenuItem:$r,Throbber:Vr,Menu:qr,ListBox:Yr,Radio:Xr,ResizeHandle:jr,SelectBox:Gr,Slider:to,Spacer:eo,SplitButton:no,StackLayout:io,TabPanel:ro,TextBox:oo,DropZone:Xn,BrowseButton:An}},ao=function(n){n.ui?C.each(so(),function(t,e){n.ui[e]=t}):n.ui=so()};C.each(so(),function(t,e){ke.add(e,t)}),ao(window.tinymce?window.tinymce:{}),o.add("inlite",function(t){var e=On();return Dr.setup(t),kn(t,e),tn(t,e)})}(window); \ No newline at end of file +!function(_){"use strict";var u,t,e,n,i,r=tinymce.util.Tools.resolve("tinymce.ThemeManager"),h=tinymce.util.Tools.resolve("tinymce.Env"),v=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),c=tinymce.util.Tools.resolve("tinymce.util.Delay"),o=function(t){return t.reduce(function(t,e){return Array.isArray(e)?t.concat(o(e)):t.concat(e)},[])},s={flatten:o},a=function(t,e){for(var n=0;n<e.length;n++){var i=(0,e[n])(t);if(i)return i}return null},l=function(t,e){return{id:t,rect:e}},d=function(t){return{x:t.left,y:t.top,w:t.width,h:t.height}},f=function(t){return{left:t.x,top:t.y,width:t.w,height:t.h,right:t.x+t.w,bottom:t.y+t.h}},m=function(t){var e=v.DOM.getViewPort();return{x:t.x+e.x,y:t.y+e.y,w:t.w,h:t.h}},g=function(t){var e=t.getBoundingClientRect();return m({x:e.left,y:e.top,w:Math.max(t.clientWidth,t.offsetWidth),h:Math.max(t.clientHeight,t.offsetHeight)})},p=function(t,e){return g(e)},b=function(t){return g(t.getContentAreaContainer()||t.getBody())},y=function(t){var e=t.selection.getBoundingClientRect();return e?m(d(e)):null},x=function(n,i){return function(t){for(var e=0;e<i.length;e++)if(i[e].predicate(n))return l(i[e].id,p(t,n));return null}},w=function(i,r){return function(t){for(var e=0;e<i.length;e++)for(var n=0;n<r.length;n++)if(r[n].predicate(i[e]))return l(r[n].id,p(t,i[e]));return null}},R=tinymce.util.Tools.resolve("tinymce.util.Tools"),C=function(t,e){return{id:t,predicate:e}},k=function(t){return R.map(t,function(t){return C(t.id,t.predicate)})},E=function(e){return function(t){return t.selection.isCollapsed()?null:l(e,y(t))}},H=function(i,r){return function(t){var e,n=t.schema.getTextBlockElements();for(e=0;e<i.length;e++)if("TABLE"===i[e].nodeName)return null;for(e=0;e<i.length;e++)if(i[e].nodeName in n)return t.dom.isEmpty(i[e])?l(r,y(t)):null;return null}},T=function(t){t.fire("SkinLoaded")},S=function(t){return t.fire("BeforeRenderUI")},M=tinymce.util.Tools.resolve("tinymce.EditorManager"),N=function(e){return function(t){return typeof t===e}},O=function(t){return Array.isArray(t)},W=function(t){return N("string")(t)},P=function(t){return N("number")(t)},D=function(t){return N("boolean")(t)},A=function(t){return N("function")(t)},B=(N("object"),O),L=function(t,e){if(e(t))return!0;throw new Error("Default value doesn't match requested type.")},I=function(r){return function(t,e,n){var i=t.settings;return L(n,r),e in i&&r(i[e])?i[e]:n}},z={getStringOr:I(W),getBoolOr:I(D),getNumberOr:I(P),getHandlerOr:I(A),getToolbarItemsOr:(u=B,function(t,e,n){var i,r,o,s,a,l=e in t.settings?t.settings[e]:n;return L(n,u),r=n,B(i=l)?i:W(i)?"string"==typeof(s=i)?(a=/[ ,]/,s.split(a).filter(function(t){return 0<t.length})):s:D(i)?(o=r,!1===i?[]:o):r})},F=tinymce.util.Tools.resolve("tinymce.geom.Rect"),U=function(t,e){return{rect:t,position:e}},V=function(t,e){return{x:e.x,y:e.y,w:t.w,h:t.h}},q=function(t,e,n,i,r){var o,s,a,l={x:i.x,y:i.y,w:i.w+(i.w<r.w+n.w?r.w:0),h:i.h+(i.h<r.h+n.h?r.h:0)};return o=F.findBestRelativePosition(r,n,l,t),n=F.clamp(n,l),o?(s=F.relativePosition(r,n,o),a=V(r,s),U(a,o)):(n=F.intersect(l,n))?((o=F.findBestRelativePosition(r,n,l,e))?(s=F.relativePosition(r,n,o),a=V(r,s)):a=V(r,n),U(a,o)):null},Y=function(t,e,n){return q(["cr-cl","cl-cr"],["bc-tc","bl-tl","br-tr"],t,e,n)},$=function(t,e,n){return q(["tc-bc","bc-tc","tl-bl","bl-tl","tr-br","br-tr","cr-cl","cl-cr"],["bc-tc","bl-tl","br-tr","cr-cl"],t,e,n)},X=function(t,e,n,i){var r;return"function"==typeof t?(r=t({elementRect:f(e),contentAreaRect:f(n),panelRect:f(i)}),d(r)):i},j=function(t){return t.panelRect},J=function(t){return z.getToolbarItemsOr(t,"selection_toolbar",["bold","italic","|","quicklink","h2","h3","blockquote"])},G=function(t){return z.getToolbarItemsOr(t,"insert_toolbar",["quickimage","quicktable"])},K=function(t){return z.getHandlerOr(t,"inline_toolbar_position_handler",j)},Z=function(t){var e,n,i,r,o=t.settings;return o.skin_url?(i=t,r=o.skin_url,i.documentBaseURI.toAbsolute(r)):(e=o.skin,n=M.baseURL+"/skins/",e?n+e:n+"lightgray")},Q=function(t){return!1===t.settings.skin},tt=function(i,r){var t=Z(i),e=function(){var t,e,n;e=r,n=function(){t._skinLoaded=!0,T(t),e()},(t=i).initialized?n():t.on("init",n)};Q(i)?e():(v.DOM.styleSheetLoader.load(t+"/skin.min.css",e),i.contentCSS.push(t+"/content.inline.min.css"))},et=function(t){var e,n,i,r,o=t.contextToolbars;return s.flatten([o||[],(e=t,n="img",i="image",r="alignleft aligncenter alignright",{predicate:function(t){return e.dom.is(t,n)},id:i,items:r})])},nt=function(t,e){var n,i,r,o,s;return s=(o=t).selection.getNode(),i=o.dom.getParents(s,"*"),r=k(e),(n=a(t,[x(i[0],r),E("text"),H(i,"insert"),w(i,r)]))&&n.rect?n:null},it=function(i,r){return function(){var t,e,n;i.removed||(n=i,_.document.activeElement!==n.getBody())||(t=et(i),(e=nt(i,t))?r.show(i,e.id,e.rect,t):r.hide())}},rt=function(t,e){var n,i,r,o,s,a=c.throttle(it(t,e),0),l=c.throttle((r=it(n=t,i=e),function(){n.removed||i.inForm()||r()}),0),u=(o=t,s=e,function(){var t=et(o),e=nt(o,t);e&&s.reposition(o,e.id,e.rect)});t.on("blur hide ObjectResizeStart",e.hide),t.on("click",a),t.on("nodeChange mouseup",l),t.on("ResizeEditor keyup",a),t.on("ResizeWindow",u),v.DOM.bind(h.container,"scroll",u),t.on("remove",function(){v.DOM.unbind(h.container,"scroll",u),e.remove()}),t.shortcuts.add("Alt+F10,F10","",e.focus)},ot=function(t,e){return tt(t,function(){var n,i;rt(t,e),i=e,(n=t).shortcuts.remove("meta+k"),n.shortcuts.add("meta+k","",function(){var t=et(n),e=a(n,[E("quicklink")]);e&&i.show(n,e.id,e.rect,t)})}),{}},st=function(t,e){return t.inline?ot(t,e):function(t){throw new Error(t)}("inlite theme only supports inline mode.")},at=function(){},lt=function(t){return function(){return t}},ut=lt(!1),ct=lt(!0),dt=function(){return ft},ft=(t=function(t){return t.isNone()},i={fold:function(t,e){return t()},is:ut,isSome:ut,isNone:ct,getOr:n=function(t){return t},getOrThunk:e=function(t){return t()},getOrDie:function(t){throw new Error(t||"error: getOrDie called on none.")},getOrNull:lt(null),getOrUndefined:lt(undefined),or:n,orThunk:e,map:dt,each:at,bind:dt,exists:ut,forall:ct,filter:dt,equals:t,equals_:t,toArray:function(){return[]},toString:lt("none()")},Object.freeze&&Object.freeze(i),i),ht=function(n){var t=lt(n),e=function(){return r},i=function(t){return t(n)},r={fold:function(t,e){return e(n)},is:function(t){return n===t},isSome:ct,isNone:ut,getOr:t,getOrThunk:t,getOrDie:t,getOrNull:t,getOrUndefined:t,or:e,orThunk:e,map:function(t){return ht(t(n))},each:function(t){t(n)},bind:i,exists:i,forall:i,filter:function(t){return t(n)?r:ft},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(t){return t.is(n)},equals_:function(t,e){return t.fold(ut,function(t){return e(n,t)})}};return r},mt={some:ht,none:dt,from:function(t){return null===t||t===undefined?ft:ht(t)}},gt=function(e){return function(t){return function(t){if(null===t)return"null";var e=typeof t;return"object"===e&&(Array.prototype.isPrototypeOf(t)||t.constructor&&"Array"===t.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(t)||t.constructor&&"String"===t.constructor.name)?"string":e}(t)===e}},pt=gt("array"),vt=gt("function"),bt=gt("number"),yt=(Array.prototype.slice,Array.prototype.indexOf),xt=Array.prototype.push,wt=function(t,e){var n,i,r=(n=t,i=e,yt.call(n,i));return-1===r?mt.none():mt.some(r)},_t=function(t,e){for(var n=0,i=t.length;n<i;n++)if(e(t[n],n))return!0;return!1},Rt=function(t,e){for(var n=t.length,i=new Array(n),r=0;r<n;r++){var o=t[r];i[r]=e(o,r)}return i},Ct=function(t,e){for(var n=0,i=t.length;n<i;n++)e(t[n],n)},kt=function(t,e){for(var n=[],i=0,r=t.length;i<r;i++){var o=t[i];e(o,i)&&n.push(o)}return n},Et=(vt(Array.from)&&Array.from,0),Ht={id:function(){return"mceu_"+Et++},create:function(t,e,n){var i=_.document.createElement(t);return v.DOM.setAttribs(i,e),"string"==typeof n?i.innerHTML=n:R.each(n,function(t){t.nodeType&&i.appendChild(t)}),i},createFragment:function(t){return v.DOM.createFragment(t)},getWindowSize:function(){return v.DOM.getViewPort()},getSize:function(t){var e,n;if(t.getBoundingClientRect){var i=t.getBoundingClientRect();e=Math.max(i.width||i.right-i.left,t.offsetWidth),n=Math.max(i.height||i.bottom-i.bottom,t.offsetHeight)}else e=t.offsetWidth,n=t.offsetHeight;return{width:e,height:n}},getPos:function(t,e){return v.DOM.getPos(t,e||Ht.getContainer())},getContainer:function(){return h.container?h.container:_.document.body},getViewPort:function(t){return v.DOM.getViewPort(t)},get:function(t){return _.document.getElementById(t)},addClass:function(t,e){return v.DOM.addClass(t,e)},removeClass:function(t,e){return v.DOM.removeClass(t,e)},hasClass:function(t,e){return v.DOM.hasClass(t,e)},toggleClass:function(t,e,n){return v.DOM.toggleClass(t,e,n)},css:function(t,e,n){return v.DOM.setStyle(t,e,n)},getRuntimeStyle:function(t,e){return v.DOM.getStyle(t,e,!0)},on:function(t,e,n,i){return v.DOM.bind(t,e,n,i)},off:function(t,e,n){return v.DOM.unbind(t,e,n)},fire:function(t,e,n){return v.DOM.fire(t,e,n)},innerHtml:function(t,e){v.DOM.setHTML(t,e)}},Tt=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),St=tinymce.util.Tools.resolve("tinymce.util.Class"),Mt=tinymce.util.Tools.resolve("tinymce.util.EventDispatcher"),Nt=function(t){var e;if(t)return"number"==typeof t?{top:t=t||0,left:t,bottom:t,right:t}:(1===(e=(t=t.split(" ")).length)?t[1]=t[2]=t[3]=t[0]:2===e?(t[2]=t[0],t[3]=t[1]):3===e&&(t[3]=t[1]),{top:parseInt(t[0],10)||0,right:parseInt(t[1],10)||0,bottom:parseInt(t[2],10)||0,left:parseInt(t[3],10)||0})},Ot=function(i,t){function e(t){var e=parseFloat(function(t){var e=i.ownerDocument.defaultView;if(e){var n=e.getComputedStyle(i,null);return n?(t=t.replace(/[A-Z]/g,function(t){return"-"+t}),n.getPropertyValue(t)):null}return i.currentStyle[t]}(t));return isNaN(e)?0:e}return{top:e(t+"TopWidth"),right:e(t+"RightWidth"),bottom:e(t+"BottomWidth"),left:e(t+"LeftWidth")}};function Wt(){}function Pt(t){this.cls=[],this.cls._map={},this.onchange=t||Wt,this.prefix=""}R.extend(Pt.prototype,{add:function(t){return t&&!this.contains(t)&&(this.cls._map[t]=!0,this.cls.push(t),this._change()),this},remove:function(t){if(this.contains(t)){var e=void 0;for(e=0;e<this.cls.length&&this.cls[e]!==t;e++);this.cls.splice(e,1),delete this.cls._map[t],this._change()}return this},toggle:function(t,e){var n=this.contains(t);return n!==e&&(n?this.remove(t):this.add(t),this._change()),this},contains:function(t){return!!this.cls._map[t]},_change:function(){delete this.clsValue,this.onchange.call(this)}}),Pt.prototype.toString=function(){var t;if(this.clsValue)return this.clsValue;t="";for(var e=0;e<this.cls.length;e++)0<e&&(t+=" "),t+=this.prefix+this.cls[e];return t};var Dt,At,Bt,Lt=/^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i,It=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,zt=/^\s*|\s*$/g,Ft=St.extend({init:function(t){var o=this.match;function s(t,e,n){var i;function r(t){t&&e.push(t)}return r(function(e){if(e)return e=e.toLowerCase(),function(t){return"*"===e||t.type===e}}((i=Lt.exec(t.replace(zt,"")))[1])),r(function(e){if(e)return function(t){return t._name===e}}(i[2])),r(function(n){if(n)return n=n.split("."),function(t){for(var e=n.length;e--;)if(!t.classes.contains(n[e]))return!1;return!0}}(i[3])),r(function(n,i,r){if(n)return function(t){var e=t[n]?t[n]():"";return i?"="===i?e===r:"*="===i?0<=e.indexOf(r):"~="===i?0<=(" "+e+" ").indexOf(" "+r+" "):"!="===i?e!==r:"^="===i?0===e.indexOf(r):"$="===i&&e.substr(e.length-r.length)===r:!!r}}(i[4],i[5],i[6])),r(function(i){var e;if(i)return(i=/(?:not\((.+)\))|(.+)/i.exec(i))[1]?(e=a(i[1],[]),function(t){return!o(t,e)}):(i=i[2],function(t,e,n){return"first"===i?0===e:"last"===i?e===n-1:"even"===i?e%2==0:"odd"===i?e%2==1:!!t[i]&&t[i]()})}(i[7])),e.pseudo=!!i[7],e.direct=n,e}function a(t,e){var n,i,r,o=[];do{if(It.exec(""),(i=It.exec(t))&&(t=i[3],o.push(i[1]),i[2])){n=i[3];break}}while(i);for(n&&a(n,e),t=[],r=0;r<o.length;r++)">"!==o[r]&&t.push(s(o[r],[],">"===o[r-1]));return e.push(t),e}this._selectors=a(t,[])},match:function(t,e){var n,i,r,o,s,a,l,u,c,d,f,h,m;for(n=0,i=(e=e||this._selectors).length;n<i;n++){for(m=t,h=0,r=(o=(s=e[n]).length)-1;0<=r;r--)for(u=s[r];m;){if(u.pseudo)for(c=d=(f=m.parent().items()).length;c--&&f[c]!==m;);for(a=0,l=u.length;a<l;a++)if(!u[a](m,c,d)){a=l+1;break}if(a===l){h++;break}if(r===o-1)break;m=m.parent()}if(h===o)return!0}return!1},find:function(t){var e,n,u=[],i=this._selectors;function c(t,e,n){var i,r,o,s,a,l=e[n];for(i=0,r=t.length;i<r;i++){for(a=t[i],o=0,s=l.length;o<s;o++)if(!l[o](a,i,r)){o=s+1;break}if(o===s)n===e.length-1?u.push(a):a.items&&c(a.items(),e,n+1);else if(l.direct)return;a.items&&c(a.items(),e,n)}}if(t.items){for(e=0,n=i.length;e<n;e++)c(t.items(),i[e],0);1<n&&(u=function(t){for(var e,n=[],i=t.length;i--;)(e=t[i]).__checked||(n.push(e),e.__checked=1);for(i=n.length;i--;)delete n[i].__checked;return n}(u))}return Dt||(Dt=Ft.Collection),new Dt(u)}}),Ut=Array.prototype.push,Vt=Array.prototype.slice;Bt={length:0,init:function(t){t&&this.add(t)},add:function(t){return R.isArray(t)?Ut.apply(this,t):t instanceof At?this.add(t.toArray()):Ut.call(this,t),this},set:function(t){var e,n=this,i=n.length;for(n.length=0,n.add(t),e=n.length;e<i;e++)delete n[e];return n},filter:function(e){var t,n,i,r,o=[];for("string"==typeof e?(e=new Ft(e),r=function(t){return e.match(t)}):r=e,t=0,n=this.length;t<n;t++)r(i=this[t])&&o.push(i);return new At(o)},slice:function(){return new At(Vt.apply(this,arguments))},eq:function(t){return-1===t?this.slice(t):this.slice(t,+t+1)},each:function(t){return R.each(this,t),this},toArray:function(){return R.toArray(this)},indexOf:function(t){for(var e=this.length;e--&&this[e]!==t;);return e},reverse:function(){return new At(R.toArray(this).reverse())},hasClass:function(t){return!!this[0]&&this[0].classes.contains(t)},prop:function(e,n){var t;return n!==undefined?(this.each(function(t){t[e]&&t[e](n)}),this):(t=this[0])&&t[e]?t[e]():void 0},exec:function(e){var n=R.toArray(arguments).slice(1);return this.each(function(t){t[e]&&t[e].apply(t,n)}),this},remove:function(){for(var t=this.length;t--;)this[t].remove();return this},addClass:function(e){return this.each(function(t){t.classes.add(e)})},removeClass:function(e){return this.each(function(t){t.classes.remove(e)})}},R.each("fire on off show hide append prepend before after reflow".split(" "),function(n){Bt[n]=function(){var e=R.toArray(arguments);return this.each(function(t){n in t&&t[n].apply(t,e)}),this}}),R.each("text name disabled active selected checked visible parent value data".split(" "),function(e){Bt[e]=function(t){return this.prop(e,t)}}),At=St.extend(Bt);var qt=Ft.Collection=At,Yt=function(t){this.create=t.create};Yt.create=function(r,o){return new Yt({create:function(e,n){var i,t=function(t){e.set(n,t.value)};return e.on("change:"+n,function(t){r.set(o,t.value)}),r.on("change:"+o,t),(i=e._bindings)||(i=e._bindings=[],e.on("destroy",function(){for(var t=i.length;t--;)i[t]()})),i.push(function(){r.off("change:"+o,t)}),r.get(o)}})};var $t=tinymce.util.Tools.resolve("tinymce.util.Observable");function Xt(t){return 0<t.nodeType}var jt,Jt,Gt=St.extend({Mixins:[$t],init:function(t){var e,n;for(e in t=t||{})(n=t[e])instanceof Yt&&(t[e]=n.create(this,e));this.data=t},set:function(e,n){var i,r,o=this.data[e];if(n instanceof Yt&&(n=n.create(this,e)),"object"==typeof e){for(i in e)this.set(i,e[i]);return this}return function t(e,n){var i,r;if(e===n)return!0;if(null===e||null===n)return e===n;if("object"!=typeof e||"object"!=typeof n)return e===n;if(R.isArray(n)){if(e.length!==n.length)return!1;for(i=e.length;i--;)if(!t(e[i],n[i]))return!1}if(Xt(e)||Xt(n))return e===n;for(i in r={},n){if(!t(e[i],n[i]))return!1;r[i]=!0}for(i in e)if(!r[i]&&!t(e[i],n[i]))return!1;return!0}(o,n)||(this.data[e]=n,r={target:this,name:e,value:n,oldValue:o},this.fire("change:"+e,r),this.fire("change",r)),this},get:function(t){return this.data[t]},has:function(t){return t in this.data},bind:function(t){return Yt.create(this,t)},destroy:function(){this.fire("destroy")}}),Kt={},Zt={add:function(t){var e=t.parent();if(e){if(!e._layout||e._layout.isNative())return;Kt[e._id]||(Kt[e._id]=e),jt||(jt=!0,c.requestAnimationFrame(function(){var t,e;for(t in jt=!1,Kt)(e=Kt[t]).state.get("rendered")&&e.reflow();Kt={}},_.document.body))}},remove:function(t){Kt[t._id]&&delete Kt[t._id]}},Qt=function(t){return t?t.getRoot().uiContainer:null},te={getUiContainerDelta:function(t){var e=Qt(t);if(e&&"static"!==v.DOM.getStyle(e,"position",!0)){var n=v.DOM.getPos(e),i=e.scrollLeft-n.x,r=e.scrollTop-n.y;return mt.some({x:i,y:r})}return mt.none()},setUiContainer:function(t,e){var n=v.DOM.select(t.settings.ui_container)[0];e.getRoot().uiContainer=n},getUiContainer:Qt,inheritUiContainer:function(t,e){return e.uiContainer=Qt(t)}},ee="onmousewheel"in _.document,ne=!1,ie=0,re={Statics:{classPrefix:"mce-"},isRtl:function(){return Jt.rtl},classPrefix:"mce-",init:function(e){var t,n,i=this;function r(t){var e;for(t=t.split(" "),e=0;e<t.length;e++)i.classes.add(t[e])}i.settings=e=R.extend({},i.Defaults,e),i._id=e.id||"mceu_"+ie++,i._aria={role:e.role},i._elmCache={},i.$=Tt,i.state=new Gt({visible:!0,active:!1,disabled:!1,value:""}),i.data=new Gt(e.data),i.classes=new Pt(function(){i.state.get("rendered")&&(i.getEl().className=this.toString())}),i.classes.prefix=i.classPrefix,(t=e.classes)&&(i.Defaults&&(n=i.Defaults.classes)&&t!==n&&r(n),r(t)),R.each("title text name visible disabled active value".split(" "),function(t){t in e&&i[t](e[t])}),i.on("click",function(){if(i.disabled())return!1}),i.settings=e,i.borderBox=Nt(e.border),i.paddingBox=Nt(e.padding),i.marginBox=Nt(e.margin),e.hidden&&i.hide()},Properties:"parent,name",getContainerElm:function(){var t=te.getUiContainer(this);return t||Ht.getContainer()},getParentCtrl:function(t){for(var e,n=this.getRoot().controlIdLookup;t&&n&&!(e=n[t.id]);)t=t.parentNode;return e},initLayoutRect:function(){var t,e,n,i,r,o,s,a,l,u,c=this,d=c.settings,f=c.getEl();t=c.borderBox=c.borderBox||Ot(f,"border"),c.paddingBox=c.paddingBox||Ot(f,"padding"),c.marginBox=c.marginBox||Ot(f,"margin"),u=Ht.getSize(f),a=d.minWidth,l=d.minHeight,r=a||u.width,o=l||u.height,n=d.width,i=d.height,s=void 0!==(s=d.autoResize)?s:!n&&!i,n=n||r,i=i||o;var h=t.left+t.right,m=t.top+t.bottom,g=d.maxWidth||65535,p=d.maxHeight||65535;return c._layoutRect=e={x:d.x||0,y:d.y||0,w:n,h:i,deltaW:h,deltaH:m,contentW:n-h,contentH:i-m,innerW:n-h,innerH:i-m,startMinWidth:a||0,startMinHeight:l||0,minW:Math.min(r,g),minH:Math.min(o,p),maxW:g,maxH:p,autoResize:s,scrollW:0},c._lastLayoutRect={},e},layoutRect:function(t){var e,n,i,r,o,s=this,a=s._layoutRect;return a||(a=s.initLayoutRect()),t?(i=a.deltaW,r=a.deltaH,t.x!==undefined&&(a.x=t.x),t.y!==undefined&&(a.y=t.y),t.minW!==undefined&&(a.minW=t.minW),t.minH!==undefined&&(a.minH=t.minH),(n=t.w)!==undefined&&(n=(n=n<a.minW?a.minW:n)>a.maxW?a.maxW:n,a.w=n,a.innerW=n-i),(n=t.h)!==undefined&&(n=(n=n<a.minH?a.minH:n)>a.maxH?a.maxH:n,a.h=n,a.innerH=n-r),(n=t.innerW)!==undefined&&(n=(n=n<a.minW-i?a.minW-i:n)>a.maxW-i?a.maxW-i:n,a.innerW=n,a.w=n+i),(n=t.innerH)!==undefined&&(n=(n=n<a.minH-r?a.minH-r:n)>a.maxH-r?a.maxH-r:n,a.innerH=n,a.h=n+r),t.contentW!==undefined&&(a.contentW=t.contentW),t.contentH!==undefined&&(a.contentH=t.contentH),(e=s._lastLayoutRect).x===a.x&&e.y===a.y&&e.w===a.w&&e.h===a.h||((o=Jt.repaintControls)&&o.map&&!o.map[s._id]&&(o.push(s),o.map[s._id]=!0),e.x=a.x,e.y=a.y,e.w=a.w,e.h=a.h),s):a},repaint:function(){var t,e,n,i,r,o,s,a,l,u,c=this;l=_.document.createRange?function(t){return t}:Math.round,t=c.getEl().style,i=c._layoutRect,a=c._lastRepaintRect||{},o=(r=c.borderBox).left+r.right,s=r.top+r.bottom,i.x!==a.x&&(t.left=l(i.x)+"px",a.x=i.x),i.y!==a.y&&(t.top=l(i.y)+"px",a.y=i.y),i.w!==a.w&&(u=l(i.w-o),t.width=(0<=u?u:0)+"px",a.w=i.w),i.h!==a.h&&(u=l(i.h-s),t.height=(0<=u?u:0)+"px",a.h=i.h),c._hasBody&&i.innerW!==a.innerW&&(u=l(i.innerW),(n=c.getEl("body"))&&((e=n.style).width=(0<=u?u:0)+"px"),a.innerW=i.innerW),c._hasBody&&i.innerH!==a.innerH&&(u=l(i.innerH),(n=n||c.getEl("body"))&&((e=e||n.style).height=(0<=u?u:0)+"px"),a.innerH=i.innerH),c._lastRepaintRect=a,c.fire("repaint",{},!1)},updateLayoutRect:function(){var t=this;t.parent()._lastRect=null,Ht.css(t.getEl(),{width:"",height:""}),t._layoutRect=t._lastRepaintRect=t._lastLayoutRect=null,t.initLayoutRect()},on:function(t,e){var n,i,r,o=this;return oe(o).on(t,"string"!=typeof(n=e)?n:function(t){return i||o.parentsAndSelf().each(function(t){var e=t.settings.callbacks;if(e&&(i=e[n]))return r=t,!1}),i?i.call(r,t):(t.action=n,void this.fire("execute",t))}),o},off:function(t,e){return oe(this).off(t,e),this},fire:function(t,e,n){if((e=e||{}).control||(e.control=this),e=oe(this).fire(t,e),!1!==n&&this.parent)for(var i=this.parent();i&&!e.isPropagationStopped();)i.fire(t,e,!1),i=i.parent();return e},hasEventListeners:function(t){return oe(this).has(t)},parents:function(t){var e,n=new qt;for(e=this.parent();e;e=e.parent())n.add(e);return t&&(n=n.filter(t)),n},parentsAndSelf:function(t){return new qt(this).add(this.parents(t))},next:function(){var t=this.parent().items();return t[t.indexOf(this)+1]},prev:function(){var t=this.parent().items();return t[t.indexOf(this)-1]},innerHtml:function(t){return this.$el.html(t),this},getEl:function(t){var e=t?this._id+"-"+t:this._id;return this._elmCache[e]||(this._elmCache[e]=Tt("#"+e)[0]),this._elmCache[e]},show:function(){return this.visible(!0)},hide:function(){return this.visible(!1)},focus:function(){try{this.getEl().focus()}catch(t){}return this},blur:function(){return this.getEl().blur(),this},aria:function(t,e){var n=this,i=n.getEl(n.ariaTarget);return void 0===e?n._aria[t]:(n._aria[t]=e,n.state.get("rendered")&&i.setAttribute("role"===t?t:"aria-"+t,e),n)},encode:function(t,e){return!1!==e&&(t=this.translate(t)),(t||"").replace(/[&<>"]/g,function(t){return"&#"+t.charCodeAt(0)+";"})},translate:function(t){return Jt.translate?Jt.translate(t):t},before:function(t){var e=this.parent();return e&&e.insert(t,e.items().indexOf(this),!0),this},after:function(t){var e=this.parent();return e&&e.insert(t,e.items().indexOf(this)),this},remove:function(){var e,t,n=this,i=n.getEl(),r=n.parent();if(n.items){var o=n.items().toArray();for(t=o.length;t--;)o[t].remove()}r&&r.items&&(e=[],r.items().each(function(t){t!==n&&e.push(t)}),r.items().set(e),r._lastRect=null),n._eventsRoot&&n._eventsRoot===n&&Tt(i).off();var s=n.getRoot().controlIdLookup;return s&&delete s[n._id],i&&i.parentNode&&i.parentNode.removeChild(i),n.state.set("rendered",!1),n.state.destroy(),n.fire("remove"),n},renderBefore:function(t){return Tt(t).before(this.renderHtml()),this.postRender(),this},renderTo:function(t){return Tt(t||this.getContainerElm()).append(this.renderHtml()),this.postRender(),this},preRender:function(){},render:function(){},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'"></div>'},postRender:function(){var t,e,n,i,r,o=this,s=o.settings;for(i in o.$el=Tt(o.getEl()),o.state.set("rendered",!0),s)0===i.indexOf("on")&&o.on(i.substr(2),s[i]);if(o._eventsRoot){for(n=o.parent();!r&&n;n=n.parent())r=n._eventsRoot;if(r)for(i in r._nativeEvents)o._nativeEvents[i]=!0}se(o),s.style&&(t=o.getEl())&&(t.setAttribute("style",s.style),t.style.cssText=s.style),o.settings.border&&(e=o.borderBox,o.$el.css({"border-top-width":e.top,"border-right-width":e.right,"border-bottom-width":e.bottom,"border-left-width":e.left}));var a=o.getRoot();for(var l in a.controlIdLookup||(a.controlIdLookup={}),(a.controlIdLookup[o._id]=o)._aria)o.aria(l,o._aria[l]);!1===o.state.get("visible")&&(o.getEl().style.display="none"),o.bindStates(),o.state.on("change:visible",function(t){var e,n=t.value;o.state.get("rendered")&&(o.getEl().style.display=!1===n?"none":"",o.getEl().getBoundingClientRect()),(e=o.parent())&&(e._lastRect=null),o.fire(n?"show":"hide"),Zt.add(o)}),o.fire("postrender",{},!1)},bindStates:function(){},scrollIntoView:function(t){var e,n,i,r,o,s,a=this.getEl(),l=a.parentNode,u=function(t,e){var n,i,r=t;for(n=i=0;r&&r!==e&&r.nodeType;)n+=r.offsetLeft||0,i+=r.offsetTop||0,r=r.offsetParent;return{x:n,y:i}}(a,l);return e=u.x,n=u.y,i=a.offsetWidth,r=a.offsetHeight,o=l.clientWidth,s=l.clientHeight,"end"===t?(e-=o-i,n-=s-r):"center"===t&&(e-=o/2-i/2,n-=s/2-r/2),l.scrollLeft=e,l.scrollTop=n,this},getRoot:function(){for(var t,e=this,n=[];e;){if(e.rootControl){t=e.rootControl;break}n.push(e),e=(t=e).parent()}t||(t=this);for(var i=n.length;i--;)n[i].rootControl=t;return t},reflow:function(){Zt.remove(this);var t=this.parent();return t&&t._layout&&!t._layout.isNative()&&t.reflow(),this}};function oe(n){return n._eventDispatcher||(n._eventDispatcher=new Mt({scope:n,toggleEvent:function(t,e){e&&Mt.isNative(t)&&(n._nativeEvents||(n._nativeEvents={}),n._nativeEvents[t]=!0,n.state.get("rendered")&&se(n))}})),n._eventDispatcher}function se(a){var t,e,n,l,i,r;function o(t){var e=a.getParentCtrl(t.target);e&&e.fire(t.type,t)}function s(){var t=l._lastHoverCtrl;t&&(t.fire("mouseleave",{target:t.getEl()}),t.parents().each(function(t){t.fire("mouseleave",{target:t.getEl()})}),l._lastHoverCtrl=null)}function u(t){var e,n,i,r=a.getParentCtrl(t.target),o=l._lastHoverCtrl,s=0;if(r!==o){if((n=(l._lastHoverCtrl=r).parents().toArray().reverse()).push(r),o){for((i=o.parents().toArray().reverse()).push(o),s=0;s<i.length&&n[s]===i[s];s++);for(e=i.length-1;s<=e;e--)(o=i[e]).fire("mouseleave",{target:o.getEl()})}for(e=s;e<n.length;e++)(r=n[e]).fire("mouseenter",{target:r.getEl()})}}function c(t){t.preventDefault(),"mousewheel"===t.type?(t.deltaY=-.025*t.wheelDelta,t.wheelDeltaX&&(t.deltaX=-.025*t.wheelDeltaX)):(t.deltaX=0,t.deltaY=t.detail),t=a.fire("wheel",t)}if(i=a._nativeEvents){for((n=a.parents().toArray()).unshift(a),t=0,e=n.length;!l&&t<e;t++)l=n[t]._eventsRoot;for(l||(l=n[n.length-1]||a),a._eventsRoot=l,e=t,t=0;t<e;t++)n[t]._eventsRoot=l;var d=l._delegates;for(r in d||(d=l._delegates={}),i){if(!i)return!1;"wheel"!==r||ne?("mouseenter"===r||"mouseleave"===r?l._hasMouseEnter||(Tt(l.getEl()).on("mouseleave",s).on("mouseover",u),l._hasMouseEnter=1):d[r]||(Tt(l.getEl()).on(r,o),d[r]=!0),i[r]=!1):ee?Tt(a.getEl()).on("mousewheel",c):Tt(a.getEl()).on("DOMMouseScroll",c)}}}R.each("text title visible disabled active value".split(" "),function(e){re[e]=function(t){return 0===arguments.length?this.state.get(e):(void 0!==t&&this.state.set(e,t),this)}});var ae=Jt=St.extend(re),le=function(t){return"static"===Ht.getRuntimeStyle(t,"position")},ue=function(t){return t.state.get("fixed")};function ce(t,e,n){var i,r,o,s,a,l,u,c,d,f;return d=de(),o=(r=Ht.getPos(e,te.getUiContainer(t))).x,s=r.y,ue(t)&&le(_.document.body)&&(o-=d.x,s-=d.y),i=t.getEl(),a=(f=Ht.getSize(i)).width,l=f.height,u=(f=Ht.getSize(e)).width,c=f.height,"b"===(n=(n||"").split(""))[0]&&(s+=c),"r"===n[1]&&(o+=u),"c"===n[0]&&(s+=Math.round(c/2)),"c"===n[1]&&(o+=Math.round(u/2)),"b"===n[3]&&(s-=l),"r"===n[4]&&(o-=a),"c"===n[3]&&(s-=Math.round(l/2)),"c"===n[4]&&(o-=Math.round(a/2)),{x:o,y:s,w:a,h:l}}var de=function(){var t=_.window;return{x:Math.max(t.pageXOffset,_.document.body.scrollLeft,_.document.documentElement.scrollLeft),y:Math.max(t.pageYOffset,_.document.body.scrollTop,_.document.documentElement.scrollTop),w:t.innerWidth||_.document.documentElement.clientWidth,h:t.innerHeight||_.document.documentElement.clientHeight}},fe=function(t){var e,n=te.getUiContainer(t);return n&&!ue(t)?{x:0,y:0,w:(e=n).scrollWidth-1,h:e.scrollHeight-1}:de()},he={testMoveRel:function(t,e){for(var n=fe(this),i=0;i<e.length;i++){var r=ce(this,t,e[i]);if(ue(this)){if(0<r.x&&r.x+r.w<n.w&&0<r.y&&r.y+r.h<n.h)return e[i]}else if(r.x>n.x&&r.x+r.w<n.w+n.x&&r.y>n.y&&r.y+r.h<n.h+n.y)return e[i]}return e[0]},moveRel:function(t,e){"string"!=typeof e&&(e=this.testMoveRel(t,e));var n=ce(this,t,e);return this.moveTo(n.x,n.y)},moveBy:function(t,e){var n=this.layoutRect();return this.moveTo(n.x+t,n.y+e),this},moveTo:function(t,e){var n=this;function i(t,e,n){return t<0?0:e<t+n&&(t=e-n)<0?0:t}if(n.settings.constrainToViewport){var r=fe(this),o=n.layoutRect();t=i(t,r.w+r.x,o.w),e=i(e,r.h+r.y,o.h)}var s=te.getUiContainer(n);return s&&le(s)&&!ue(n)&&(t-=s.scrollLeft,e-=s.scrollTop),s&&(t+=1,e+=1),n.state.get("rendered")?n.layoutRect({x:t,y:e}).repaint():(n.settings.x=t,n.settings.y=e),n.fire("move",{x:t,y:e}),n}},me=ae.extend({Mixins:[he],Defaults:{classes:"widget tooltip tooltip-n"},renderHtml:function(){var t=this,e=t.classPrefix;return'<div id="'+t._id+'" class="'+t.classes+'" role="presentation"><div class="'+e+'tooltip-arrow"></div><div class="'+e+'tooltip-inner">'+t.encode(t.state.get("text"))+"</div></div>"},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl().lastChild.innerHTML=e.encode(t.value)}),e._super()},repaint:function(){var t,e;t=this.getEl().style,e=this._layoutRect,t.left=e.x+"px",t.top=e.y+"px",t.zIndex=131070}}),ge=ae.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.canFocus=!0,i.tooltip&&!1!==ge.tooltips&&(r.on("mouseenter",function(t){var e=r.tooltip().moveTo(-65535);if(t.control===r){var n=e.text(i.tooltip).show().testMoveRel(r.getEl(),["bc-tc","bc-tl","bc-tr"]);e.classes.toggle("tooltip-n","bc-tc"===n),e.classes.toggle("tooltip-nw","bc-tl"===n),e.classes.toggle("tooltip-ne","bc-tr"===n),e.moveRel(r.getEl(),n)}else e.hide()}),r.on("mouseleave mousedown click",function(){r.tooltip().remove(),r._tooltip=null})),r.aria("label",i.ariaLabel||i.tooltip)},tooltip:function(){return this._tooltip||(this._tooltip=new me({type:"tooltip"}),te.inheritUiContainer(this,this._tooltip),this._tooltip.renderTo()),this._tooltip},postRender:function(){var t=this,e=t.settings;t._super(),t.parent()||!e.width&&!e.height||(t.initLayoutRect(),t.repaint()),e.autofocus&&t.focus()},bindStates:function(){var e=this;function n(t){e.aria("disabled",t),e.classes.toggle("disabled",t)}function i(t){e.aria("pressed",t),e.classes.toggle("active",t)}return e.state.on("change:disabled",function(t){n(t.value)}),e.state.on("change:active",function(t){i(t.value)}),e.state.get("disabled")&&n(!0),e.state.get("active")&&i(!0),e._super()},remove:function(){this._super(),this._tooltip&&(this._tooltip.remove(),this._tooltip=null)}}),pe=ge.extend({Defaults:{value:0},init:function(t){this._super(t),this.classes.add("progress"),this.settings.filter||(this.settings.filter=function(t){return Math.round(t)})},renderHtml:function(){var t=this._id,e=this.classPrefix;return'<div id="'+t+'" class="'+this.classes+'"><div class="'+e+'bar-container"><div class="'+e+'bar"></div></div><div class="'+e+'text">0%</div></div>'},postRender:function(){return this._super(),this.value(this.settings.value),this},bindStates:function(){var e=this;function n(t){t=e.settings.filter(t),e.getEl().lastChild.innerHTML=t+"%",e.getEl().firstChild.firstChild.style.width=t+"%"}return e.state.on("change:value",function(t){n(t.value)}),n(e.state.get("value")),e._super()}}),ve=function(t,e){t.getEl().lastChild.textContent=e+(t.progressBar?" "+t.progressBar.value()+"%":"")},be=ae.extend({Mixins:[he],Defaults:{classes:"widget notification"},init:function(t){var e=this;e._super(t),e.maxWidth=t.maxWidth,t.text&&e.text(t.text),t.icon&&(e.icon=t.icon),t.color&&(e.color=t.color),t.type&&e.classes.add("notification-"+t.type),t.timeout&&(t.timeout<0||0<t.timeout)&&!t.closeButton?e.closeButton=!1:(e.classes.add("has-close"),e.closeButton=!0),t.progressBar&&(e.progressBar=new pe),e.on("click",function(t){-1!==t.target.className.indexOf(e.classPrefix+"close")&&e.close()})},renderHtml:function(){var t,e=this,n=e.classPrefix,i="",r="",o="";return e.icon&&(i='<i class="'+n+"ico "+n+"i-"+e.icon+'"></i>'),t=' style="max-width: '+e.maxWidth+"px;"+(e.color?"background-color: "+e.color+';"':'"'),e.closeButton&&(r='<button type="button" class="'+n+'close" aria-hidden="true">\xd7</button>'),e.progressBar&&(o=e.progressBar.renderHtml()),'<div id="'+e._id+'" class="'+e.classes+'"'+t+' role="presentation">'+i+'<div class="'+n+'notification-inner">'+e.state.get("text")+"</div>"+o+r+'<div style="clip: rect(1px, 1px, 1px, 1px);height: 1px;overflow: hidden;position: absolute;width: 1px;" aria-live="assertive" aria-relevant="additions" aria-atomic="true"></div></div>'},postRender:function(){var t=this;return c.setTimeout(function(){t.$el.addClass(t.classPrefix+"in"),ve(t,t.state.get("text"))},100),t._super()},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl().firstChild.innerHTML=t.value,ve(e,t.value)}),e.progressBar&&(e.progressBar.bindStates(),e.progressBar.state.on("change:value",function(t){ve(e,e.state.get("text"))})),e._super()},close:function(){return this.fire("close").isDefaultPrevented()||this.remove(),this},repaint:function(){var t,e;t=this.getEl().style,e=this._layoutRect,t.left=e.x+"px",t.top=e.y+"px",t.zIndex=65534}});function ye(o){var s=function(t){return t.inline?t.getElement():t.getContentAreaContainer()};return{open:function(t,e){var n,i=R.extend(t,{maxWidth:(n=s(o),Ht.getSize(n).width)}),r=new be(i);return 0<(r.args=i).timeout&&(r.timer=setTimeout(function(){r.close(),e()},i.timeout)),r.on("close",function(){e()}),r.renderTo(),r},close:function(t){t.close()},reposition:function(t){Ct(t,function(t){t.moveTo(0,0)}),function(n){if(0<n.length){var t=n.slice(0,1)[0],e=s(o);t.moveRel(e,"tc-tc"),Ct(n,function(t,e){0<e&&t.moveRel(n[e-1].getEl(),"bc-tc")})}}(t)},getArgs:function(t){return t.args}}}function xe(t){var e,n;if(t.changedTouches)for(e="screenX screenY pageX pageY clientX clientY".split(" "),n=0;n<e.length;n++)t[e[n]]=t.changedTouches[0][e[n]]}function we(t,h){var m,g,e,p,v,b,y,x=h.document||_.document;h=h||{};var w=x.getElementById(h.handle||t);e=function(t){var e,n,i,r,o,s,a,l,u,c,d,f=(e=x,u=Math.max,n=e.documentElement,i=e.body,r=u(n.scrollWidth,i.scrollWidth),o=u(n.clientWidth,i.clientWidth),s=u(n.offsetWidth,i.offsetWidth),a=u(n.scrollHeight,i.scrollHeight),l=u(n.clientHeight,i.clientHeight),{width:r<s?o:r,height:a<u(n.offsetHeight,i.offsetHeight)?l:a});xe(t),t.preventDefault(),g=t.button,c=w,b=t.screenX,y=t.screenY,d=_.window.getComputedStyle?_.window.getComputedStyle(c,null).getPropertyValue("cursor"):c.runtimeStyle.cursor,m=Tt("<div></div>").css({position:"absolute",top:0,left:0,width:f.width,height:f.height,zIndex:2147483647,opacity:1e-4,cursor:d}).appendTo(x.body),Tt(x).on("mousemove touchmove",v).on("mouseup touchend",p),h.start(t)},v=function(t){if(xe(t),t.button!==g)return p(t);t.deltaX=t.screenX-b,t.deltaY=t.screenY-y,t.preventDefault(),h.drag(t)},p=function(t){xe(t),Tt(x).off("mousemove touchmove",v).off("mouseup touchend",p),m.remove(),h.stop&&h.stop(t)},this.destroy=function(){Tt(w).off()},Tt(w).on("mousedown touchstart",e)}var _e=tinymce.util.Tools.resolve("tinymce.ui.Factory"),Re=function(t){return!!t.getAttribute("data-mce-tabstop")};function Ce(t){var o,r,n=t.root;function i(t){return t&&1===t.nodeType}try{o=_.document.activeElement}catch(e){o=_.document.body}function s(t){return i(t=t||o)?t.getAttribute("role"):null}function a(t){for(var e,n=t||o;n=n.parentNode;)if(e=s(n))return e}function l(t){var e=o;if(i(e))return e.getAttribute("aria-"+t)}function u(t){var e=t.tagName.toUpperCase();return"INPUT"===e||"TEXTAREA"===e||"SELECT"===e}function c(e){var r=[];return function t(e){if(1===e.nodeType&&"none"!==e.style.display&&!e.disabled){var n;(u(n=e)&&!n.hidden||Re(n)||/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(s(n)))&&r.push(e);for(var i=0;i<e.childNodes.length;i++)t(e.childNodes[i])}}(e||n.getEl()),r}function d(t){var e,n;(n=(t=t||r).parents().toArray()).unshift(t);for(var i=0;i<n.length&&!(e=n[i]).settings.ariaRoot;i++);return e}function f(t,e){return t<0?t=e.length-1:t>=e.length&&(t=0),e[t]&&e[t].focus(),t}function h(t,e){var n=-1,i=d();e=e||c(i.getEl());for(var r=0;r<e.length;r++)e[r]===o&&(n=r);n+=t,i.lastAriaIndex=f(n,e)}function m(){"tablist"===a()?h(-1,c(o.parentNode)):r.parent().submenu?b():h(-1)}function g(){var t=s(),e=a();"tablist"===e?h(1,c(o.parentNode)):"menuitem"===t&&"menu"===e&&l("haspopup")?y():h(1)}function p(){h(-1)}function v(){var t=s(),e=a();"menuitem"===t&&"menubar"===e?y():"button"===t&&l("haspopup")?y({key:"down"}):h(1)}function b(){r.fire("cancel")}function y(t){t=t||{},r.fire("click",{target:o,aria:t})}return r=n.getParentCtrl(o),n.on("keydown",function(t){function e(t,e){u(o)||Re(o)||"slider"!==s(o)&&!1!==e(t)&&t.preventDefault()}if(!t.isDefaultPrevented())switch(t.keyCode){case 37:e(t,m);break;case 39:e(t,g);break;case 38:e(t,p);break;case 40:e(t,v);break;case 27:b();break;case 14:case 13:case 32:e(t,y);break;case 9:!function(t){if("tablist"===a()){var e=c(r.getEl("body"))[0];e&&e.focus()}else h(t.shiftKey?-1:1)}(t),t.preventDefault()}}),n.on("focusin",function(t){o=t.target,r=t.control}),{focusFirst:function(t){var e=d(t),n=c(e.getEl());e.settings.ariaRemember&&"lastAriaIndex"in e?f(e.lastAriaIndex,n):f(0,n)}}}var ke,Ee,He,Te,Se={},Me=ae.extend({init:function(t){var e=this;e._super(t),(t=e.settings).fixed&&e.state.set("fixed",!0),e._items=new qt,e.isRtl()&&e.classes.add("rtl"),e.bodyClasses=new Pt(function(){e.state.get("rendered")&&(e.getEl("body").className=this.toString())}),e.bodyClasses.prefix=e.classPrefix,e.classes.add("container"),e.bodyClasses.add("container-body"),t.containerCls&&e.classes.add(t.containerCls),e._layout=_e.create((t.layout||"")+"layout"),e.settings.items?e.add(e.settings.items):e.add(e.render()),e._hasBody=!0},items:function(){return this._items},find:function(t){return(t=Se[t]=Se[t]||new Ft(t)).find(this)},add:function(t){return this.items().add(this.create(t)).parent(this),this},focus:function(t){var e,n,i,r=this;if(!t||!(n=r.keyboardNav||r.parents().eq(-1)[0].keyboardNav))return i=r.find("*"),r.statusbar&&i.add(r.statusbar.items()),i.each(function(t){if(t.settings.autofocus)return e=null,!1;t.canFocus&&(e=e||t)}),e&&e.focus(),r;n.focusFirst(r)},replace:function(t,e){for(var n,i=this.items(),r=i.length;r--;)if(i[r]===t){i[r]=e;break}0<=r&&((n=e.getEl())&&n.parentNode.removeChild(n),(n=t.getEl())&&n.parentNode.removeChild(n)),e.parent(this)},create:function(t){var e,n=this,i=[];return R.isArray(t)||(t=[t]),R.each(t,function(t){t&&(t instanceof ae||("string"==typeof t&&(t={type:t}),e=R.extend({},n.settings.defaults,t),t.type=e.type=e.type||t.type||n.settings.defaultType||(e.defaults?e.defaults.type:null),t=_e.create(e)),i.push(t))}),i},renderNew:function(){var i=this;return i.items().each(function(t,e){var n;t.parent(i),t.state.get("rendered")||((n=i.getEl("body")).hasChildNodes()&&e<=n.childNodes.length-1?Tt(n.childNodes[e]).before(t.renderHtml()):Tt(n).append(t.renderHtml()),t.postRender(),Zt.add(t))}),i._layout.applyClasses(i.items().filter(":visible")),i._lastRect=null,i},append:function(t){return this.add(t).renderNew()},prepend:function(t){return this.items().set(this.create(t).concat(this.items().toArray())),this.renderNew()},insert:function(t,e,n){var i,r,o;return t=this.create(t),i=this.items(),!n&&e<i.length-1&&(e+=1),0<=e&&e<i.length&&(r=i.slice(0,e).toArray(),o=i.slice(e).toArray(),i.set(r.concat(t,o))),this.renderNew()},fromJSON:function(t){for(var e in t)this.find("#"+e).value(t[e]);return this},toJSON:function(){var i={};return this.find("*").each(function(t){var e=t.name(),n=t.value();e&&void 0!==n&&(i[e]=n)}),i},renderHtml:function(){var t=this,e=t._layout,n=this.settings.role;return t.preRender(),e.preRender(t),'<div id="'+t._id+'" class="'+t.classes+'"'+(n?' role="'+this.settings.role+'"':"")+'><div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+(t.settings.html||"")+e.renderHtml(t)+"</div></div>"},postRender:function(){var t,e=this;return e.items().exec("postRender"),e._super(),e._layout.postRender(e),e.state.set("rendered",!0),e.settings.style&&e.$el.css(e.settings.style),e.settings.border&&(t=e.borderBox,e.$el.css({"border-top-width":t.top,"border-right-width":t.right,"border-bottom-width":t.bottom,"border-left-width":t.left})),e.parent()||(e.keyboardNav=Ce({root:e})),e},initLayoutRect:function(){var t=this._super();return this._layout.recalc(this),t},recalc:function(){var t=this,e=t._layoutRect,n=t._lastRect;if(!n||n.w!==e.w||n.h!==e.h)return t._layout.recalc(t),e=t.layoutRect(),t._lastRect={x:e.x,y:e.y,w:e.w,h:e.h},!0},reflow:function(){var t;if(Zt.remove(this),this.visible()){for(ae.repaintControls=[],ae.repaintControls.map={},this.recalc(),t=ae.repaintControls.length;t--;)ae.repaintControls[t].repaint();"flow"!==this.settings.layout&&"stack"!==this.settings.layout&&this.repaint(),ae.repaintControls=[]}return this}}),Ne={init:function(){this.on("repaint",this.renderScroll)},renderScroll:function(){var p=this,v=2;function n(){var m,g,t;function e(t,e,n,i,r,o){var s,a,l,u,c,d,f,h;if(a=p.getEl("scroll"+t)){if(f=e.toLowerCase(),h=n.toLowerCase(),Tt(p.getEl("absend")).css(f,p.layoutRect()[i]-1),!r)return void Tt(a).css("display","none");Tt(a).css("display","block"),s=p.getEl("body"),l=p.getEl("scroll"+t+"t"),u=s["client"+n]-2*v,c=(u-=m&&g?a["client"+o]:0)/s["scroll"+n],(d={})[f]=s["offset"+e]+v,d[h]=u,Tt(a).css(d),(d={})[f]=s["scroll"+e]*c,d[h]=u*c,Tt(l).css(d)}}t=p.getEl("body"),m=t.scrollWidth>t.clientWidth,g=t.scrollHeight>t.clientHeight,e("h","Left","Width","contentW",m,"Height"),e("v","Top","Height","contentH",g,"Width")}p.settings.autoScroll&&(p._hasScroll||(p._hasScroll=!0,function(){function t(s,a,l,u,c){var d,t=p._id+"-scroll"+s,e=p.classPrefix;Tt(p.getEl()).append('<div id="'+t+'" class="'+e+"scrollbar "+e+"scrollbar-"+s+'"><div id="'+t+'t" class="'+e+'scrollbar-thumb"></div></div>'),p.draghelper=new we(t+"t",{start:function(){d=p.getEl("body")["scroll"+a],Tt("#"+t).addClass(e+"active")},drag:function(t){var e,n,i,r,o=p.layoutRect();n=o.contentW>o.innerW,i=o.contentH>o.innerH,r=p.getEl("body")["client"+l]-2*v,e=(r-=n&&i?p.getEl("scroll"+s)["client"+c]:0)/p.getEl("body")["scroll"+l],p.getEl("body")["scroll"+a]=d+t["delta"+u]/e},stop:function(){Tt("#"+t).removeClass(e+"active")}})}p.classes.add("scroll"),t("v","Top","Height","Y","Width"),t("h","Left","Width","X","Height")}(),p.on("wheel",function(t){var e=p.getEl("body");e.scrollLeft+=10*(t.deltaX||0),e.scrollTop+=10*t.deltaY,n()}),Tt(p.getEl("body")).on("scroll",n)),n())}},Oe=Me.extend({Defaults:{layout:"fit",containerCls:"panel"},Mixins:[Ne],renderHtml:function(){var t=this,e=t._layout,n=t.settings.html;return t.preRender(),e.preRender(t),void 0===n?n='<div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+e.renderHtml(t)+"</div>":("function"==typeof n&&(n=n.call(t)),t._hasBody=!1),'<div id="'+t._id+'" class="'+t.classes+'" hidefocus="1" tabindex="-1" role="group">'+(t._preBodyHtml||"")+n+"</div>"}}),We={resizeToContent:function(){this._layoutRect.autoResize=!0,this._lastRect=null,this.reflow()},resizeTo:function(t,e){if(t<=1||e<=1){var n=Ht.getWindowSize();t=t<=1?t*n.w:t,e=e<=1?e*n.h:e}return this._layoutRect.autoResize=!1,this.layoutRect({minW:t,minH:e,w:t,h:e}).reflow()},resizeBy:function(t,e){var n=this.layoutRect();return this.resizeTo(n.w+t,n.h+e)}},Pe=[],De=[];function Ae(t,e){for(;t;){if(t===e)return!0;t=t.parent()}}function Be(){ke||(ke=function(t){2!==t.button&&function(t){for(var e=Pe.length;e--;){var n=Pe[e],i=n.getParentCtrl(t.target);if(n.settings.autohide){if(i&&(Ae(i,n)||n.parent()===i))continue;(t=n.fire("autohide",{target:t.target})).isDefaultPrevented()||n.hide()}}}(t)},Tt(_.document).on("click touchstart",ke))}function Le(r){var t=Ht.getViewPort().y;function e(t,e){for(var n,i=0;i<Pe.length;i++)if(Pe[i]!==r)for(n=Pe[i].parent();n&&(n=n.parent());)n===r&&Pe[i].fixed(t).moveBy(0,e).repaint()}r.settings.autofix&&(r.state.get("fixed")?r._autoFixY>t&&(r.fixed(!1).layoutRect({y:r._autoFixY}).repaint(),e(!1,r._autoFixY-t)):(r._autoFixY=r.layoutRect().y,r._autoFixY<t&&(r.fixed(!0).layoutRect({y:0}).repaint(),e(!0,t-r._autoFixY))))}function Ie(t,e){var n,i,r=ze.zIndex||65535;if(t)De.push(e);else for(n=De.length;n--;)De[n]===e&&De.splice(n,1);if(De.length)for(n=0;n<De.length;n++)De[n].modal&&(r++,i=De[n]),De[n].getEl().style.zIndex=r,De[n].zIndex=r,r++;var o=Tt("#"+e.classPrefix+"modal-block",e.getContainerElm())[0];i?Tt(o).css("z-index",i.zIndex-1):o&&(o.parentNode.removeChild(o),Te=!1),ze.currentZIndex=r}var ze=Oe.extend({Mixins:[he,We],init:function(t){var i=this;i._super(t),(i._eventsRoot=i).classes.add("floatpanel"),t.autohide&&(Be(),function(){if(!He){var t=_.document.documentElement,e=t.clientWidth,n=t.clientHeight;He=function(){_.document.all&&e===t.clientWidth&&n===t.clientHeight||(e=t.clientWidth,n=t.clientHeight,ze.hideAll())},Tt(_.window).on("resize",He)}}(),Pe.push(i)),t.autofix&&(Ee||(Ee=function(){var t;for(t=Pe.length;t--;)Le(Pe[t])},Tt(_.window).on("scroll",Ee)),i.on("move",function(){Le(this)})),i.on("postrender show",function(t){if(t.control===i){var e,n=i.classPrefix;i.modal&&!Te&&((e=Tt("#"+n+"modal-block",i.getContainerElm()))[0]||(e=Tt('<div id="'+n+'modal-block" class="'+n+"reset "+n+'fade"></div>').appendTo(i.getContainerElm())),c.setTimeout(function(){e.addClass(n+"in"),Tt(i.getEl()).addClass(n+"in")}),Te=!0),Ie(!0,i)}}),i.on("show",function(){i.parents().each(function(t){if(t.state.get("fixed"))return i.fixed(!0),!1})}),t.popover&&(i._preBodyHtml='<div class="'+i.classPrefix+'arrow"></div>',i.classes.add("popover").add("bottom").add(i.isRtl()?"end":"start")),i.aria("label",t.ariaLabel),i.aria("labelledby",i._id),i.aria("describedby",i.describedBy||i._id+"-none")},fixed:function(t){var e=this;if(e.state.get("fixed")!==t){if(e.state.get("rendered")){var n=Ht.getViewPort();t?e.layoutRect().y-=n.y:e.layoutRect().y+=n.y}e.classes.toggle("fixed",t),e.state.set("fixed",t)}return e},show:function(){var t,e=this._super();for(t=Pe.length;t--&&Pe[t]!==this;);return-1===t&&Pe.push(this),e},hide:function(){return Fe(this),Ie(!1,this),this._super()},hideAll:function(){ze.hideAll()},close:function(){return this.fire("close").isDefaultPrevented()||(this.remove(),Ie(!1,this)),this},remove:function(){Fe(this),this._super()},postRender:function(){return this.settings.bodyRole&&this.getEl("body").setAttribute("role",this.settings.bodyRole),this._super()}});function Fe(t){var e;for(e=Pe.length;e--;)Pe[e]===t&&Pe.splice(e,1);for(e=De.length;e--;)De[e]===t&&De.splice(e,1)}ze.hideAll=function(){for(var t=Pe.length;t--;){var e=Pe[t];e&&e.settings.autohide&&(e.hide(),Pe.splice(t,1))}};var Ue=[],Ve="";function qe(t){var e,n=Tt("meta[name=viewport]")[0];!1!==h.overrideViewPort&&(n||((n=_.document.createElement("meta")).setAttribute("name","viewport"),_.document.getElementsByTagName("head")[0].appendChild(n)),(e=n.getAttribute("content"))&&void 0!==Ve&&(Ve=e),n.setAttribute("content",t?"width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0":Ve))}function Ye(t,e){(function(){for(var t=0;t<Ue.length;t++)if(Ue[t]._fullscreen)return!0;return!1})()&&!1===e&&Tt([_.document.documentElement,_.document.body]).removeClass(t+"fullscreen")}var $e=ze.extend({modal:!0,Defaults:{border:1,layout:"flex",containerCls:"panel",role:"dialog",callbacks:{submit:function(){this.fire("submit",{data:this.toJSON()})},close:function(){this.close()}}},init:function(t){var n=this;n._super(t),n.isRtl()&&n.classes.add("rtl"),n.classes.add("window"),n.bodyClasses.add("window-body"),n.state.set("fixed",!0),t.buttons&&(n.statusbar=new Oe({layout:"flex",border:"1 0 0 0",spacing:3,padding:10,align:"center",pack:n.isRtl()?"start":"end",defaults:{type:"button"},items:t.buttons}),n.statusbar.classes.add("foot"),n.statusbar.parent(n)),n.on("click",function(t){var e=n.classPrefix+"close";(Ht.hasClass(t.target,e)||Ht.hasClass(t.target.parentNode,e))&&n.close()}),n.on("cancel",function(){n.close()}),n.on("move",function(t){t.control===n&&ze.hideAll()}),n.aria("describedby",n.describedBy||n._id+"-none"),n.aria("label",t.title),n._fullscreen=!1},recalc:function(){var t,e,n,i,r=this,o=r.statusbar;r._fullscreen&&(r.layoutRect(Ht.getWindowSize()),r.layoutRect().contentH=r.layoutRect().innerH),r._super(),t=r.layoutRect(),r.settings.title&&!r._fullscreen&&(e=t.headerW)>t.w&&(n=t.x-Math.max(0,e/2),r.layoutRect({w:e,x:n}),i=!0),o&&(o.layoutRect({w:r.layoutRect().innerW}).recalc(),(e=o.layoutRect().minW+t.deltaW)>t.w&&(n=t.x-Math.max(0,e-t.w),r.layoutRect({w:e,x:n}),i=!0)),i&&r.recalc()},initLayoutRect:function(){var t,e=this,n=e._super(),i=0;if(e.settings.title&&!e._fullscreen){t=e.getEl("head");var r=Ht.getSize(t);n.headerW=r.width,n.headerH=r.height,i+=n.headerH}e.statusbar&&(i+=e.statusbar.layoutRect().h),n.deltaH+=i,n.minH+=i,n.h+=i;var o=Ht.getWindowSize();return n.x=e.settings.x||Math.max(0,o.w/2-n.w/2),n.y=e.settings.y||Math.max(0,o.h/2-n.h/2),n},renderHtml:function(){var t=this,e=t._layout,n=t._id,i=t.classPrefix,r=t.settings,o="",s="",a=r.html;return t.preRender(),e.preRender(t),r.title&&(o='<div id="'+n+'-head" class="'+i+'window-head"><div id="'+n+'-title" class="'+i+'title">'+t.encode(r.title)+'</div><div id="'+n+'-dragh" class="'+i+'dragh"></div><button type="button" class="'+i+'close" aria-hidden="true"><i class="mce-ico mce-i-remove"></i></button></div>'),r.url&&(a='<iframe src="'+r.url+'" tabindex="-1"></iframe>'),void 0===a&&(a=e.renderHtml(t)),t.statusbar&&(s=t.statusbar.renderHtml()),'<div id="'+n+'" class="'+t.classes+'" hidefocus="1"><div class="'+t.classPrefix+'reset" role="application">'+o+'<div id="'+n+'-body" class="'+t.bodyClasses+'">'+a+"</div>"+s+"</div></div>"},fullscreen:function(t){var n,e,i=this,r=_.document.documentElement,o=i.classPrefix;if(t!==i._fullscreen)if(Tt(_.window).on("resize",function(){var t;if(i._fullscreen)if(n)i._timer||(i._timer=c.setTimeout(function(){var t=Ht.getWindowSize();i.moveTo(0,0).resizeTo(t.w,t.h),i._timer=0},50));else{t=(new Date).getTime();var e=Ht.getWindowSize();i.moveTo(0,0).resizeTo(e.w,e.h),50<(new Date).getTime()-t&&(n=!0)}}),e=i.layoutRect(),i._fullscreen=t){i._initial={x:e.x,y:e.y,w:e.w,h:e.h},i.borderBox=Nt("0"),i.getEl("head").style.display="none",e.deltaH-=e.headerH+2,Tt([r,_.document.body]).addClass(o+"fullscreen"),i.classes.add("fullscreen");var s=Ht.getWindowSize();i.moveTo(0,0).resizeTo(s.w,s.h)}else i.borderBox=Nt(i.settings.border),i.getEl("head").style.display="",e.deltaH+=e.headerH,Tt([r,_.document.body]).removeClass(o+"fullscreen"),i.classes.remove("fullscreen"),i.moveTo(i._initial.x,i._initial.y).resizeTo(i._initial.w,i._initial.h);return i.reflow()},postRender:function(){var e,n=this;setTimeout(function(){n.classes.add("in"),n.fire("open")},0),n._super(),n.statusbar&&n.statusbar.postRender(),n.focus(),this.dragHelper=new we(n._id+"-dragh",{start:function(){e={x:n.layoutRect().x,y:n.layoutRect().y}},drag:function(t){n.moveTo(e.x+t.deltaX,e.y+t.deltaY)}}),n.on("submit",function(t){t.isDefaultPrevented()||n.close()}),Ue.push(n),qe(!0)},submit:function(){return this.fire("submit",{data:this.toJSON()})},remove:function(){var t,e=this;for(e.dragHelper.destroy(),e._super(),e.statusbar&&this.statusbar.remove(),Ye(e.classPrefix,!1),t=Ue.length;t--;)Ue[t]===e&&Ue.splice(t,1);qe(0<Ue.length)},getContentWindow:function(){var t=this.getEl().getElementsByTagName("iframe")[0];return t?t.contentWindow:null}});!function(){if(!h.desktop){var n={w:_.window.innerWidth,h:_.window.innerHeight};c.setInterval(function(){var t=_.window.innerWidth,e=_.window.innerHeight;n.w===t&&n.h===e||(n={w:t,h:e},Tt(_.window).trigger("resize"))},100)}Tt(_.window).on("resize",function(){var t,e,n=Ht.getWindowSize();for(t=0;t<Ue.length;t++)e=Ue[t].layoutRect(),Ue[t].moveTo(Ue[t].settings.x||Math.max(0,n.w/2-e.w/2),Ue[t].settings.y||Math.max(0,n.h/2-e.h/2))})}();var Xe,je,Je,Ge=$e.extend({init:function(t){t={border:1,padding:20,layout:"flex",pack:"center",align:"center",containerCls:"panel",autoScroll:!0,buttons:{type:"button",text:"Ok",action:"ok"},items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200}},this._super(t)},Statics:{OK:1,OK_CANCEL:2,YES_NO:3,YES_NO_CANCEL:4,msgBox:function(t){var e,i=t.callback||function(){};function n(t,e,n){return{type:"button",text:t,subtype:n?"primary":"",onClick:function(t){t.control.parents()[1].close(),i(e)}}}switch(t.buttons){case Ge.OK_CANCEL:e=[n("Ok",!0,!0),n("Cancel",!1)];break;case Ge.YES_NO:case Ge.YES_NO_CANCEL:e=[n("Yes",1,!0),n("No",0)],t.buttons===Ge.YES_NO_CANCEL&&e.push(n("Cancel",-1));break;default:e=[n("Ok",!0,!0)]}return new $e({padding:20,x:t.x,y:t.y,minWidth:300,minHeight:100,layout:"flex",pack:"center",align:"center",buttons:e,title:t.title,role:"alertdialog",items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200,text:t.text},onPostRender:function(){this.aria("describedby",this.items()[0]._id)},onClose:t.onClose,onCancel:function(){i(!1)}}).renderTo(_.document.body).reflow()},alert:function(t,e){return"string"==typeof t&&(t={text:t}),t.callback=e,Ge.msgBox(t)},confirm:function(t,e){return"string"==typeof t&&(t={text:t}),t.callback=e,t.buttons=Ge.OK_CANCEL,Ge.msgBox(t)}}}),Ke=function(t,e){return{renderUI:function(){return st(t,e)},getNotificationManagerImpl:function(){return ye(t)},getWindowManagerImpl:function(){return{open:function(n,t,e){var i;return n.title=n.title||" ",n.url=n.url||n.file,n.url&&(n.width=parseInt(n.width||320,10),n.height=parseInt(n.height||240,10)),n.body&&(n.items={defaults:n.defaults,type:n.bodyType||"form",items:n.body,data:n.data,callbacks:n.commands}),n.url||n.buttons||(n.buttons=[{text:"Ok",subtype:"primary",onclick:function(){i.find("form")[0].submit()}},{text:"Cancel",onclick:function(){i.close()}}]),(i=new $e(n)).on("close",function(){e(i)}),n.data&&i.on("postRender",function(){this.find("*").each(function(t){var e=t.name();e in n.data&&t.value(n.data[e])})}),i.features=n||{},i.params=t||{},i=i.renderTo(_.document.body).reflow()},alert:function(t,e,n){var i;return(i=Ge.alert(t,function(){e()})).on("close",function(){n(i)}),i},confirm:function(t,e,n){var i;return(i=Ge.confirm(t,function(t){e(t)})).on("close",function(){n(i)}),i},close:function(t){t.close()},getParams:function(t){return t.params},setParams:function(t,e){t.params=e}}}}},Ze="undefined"!=typeof _.window?_.window:Function("return this;")(),Qe=function(t,e){return function(t,e){for(var n=e!==undefined&&null!==e?e:Ze,i=0;i<t.length&&n!==undefined&&null!==n;++i)n=n[t[i]];return n}(t.split("."),e)},tn=function(t,e){var n=Qe(t,e);if(n===undefined||null===n)throw new Error(t+" not available on this browser");return n},en=tinymce.util.Tools.resolve("tinymce.util.Promise"),nn=function(n){return new en(function(t){var e=new(tn("FileReader"));e.onloadend=function(){t(e.result.split(",")[1])},e.readAsDataURL(n)})},rn=function(){return new en(function(e){var t;(t=_.document.createElement("input")).type="file",t.style.position="fixed",t.style.left=0,t.style.top=0,t.style.opacity=.001,_.document.body.appendChild(t),t.onchange=function(t){e(Array.prototype.slice.call(t.target.files))},t.click(),t.parentNode.removeChild(t)})},on=0,sn=function(t){return t+on+++(e=function(){return Math.round(4294967295*Math.random()).toString(36)},"s"+Date.now().toString(36)+e()+e()+e());var e},an=function(r,o){var s={};function t(t){var e,n,i;n=o[t?"startContainer":"endContainer"],i=o[t?"startOffset":"endOffset"],1===n.nodeType&&(e=r.create("span",{"data-mce-type":"bookmark"}),n.hasChildNodes()?(i=Math.min(i,n.childNodes.length-1),t?n.insertBefore(e,n.childNodes[i]):r.insertAfter(e,n.childNodes[i])):n.appendChild(e),n=e,i=0),s[t?"startContainer":"endContainer"]=n,s[t?"startOffset":"endOffset"]=i}return t(!0),o.collapsed||t(),s},ln=function(r,o){function t(t){var e,n,i;e=i=o[t?"startContainer":"endContainer"],n=o[t?"startOffset":"endOffset"],e&&(1===e.nodeType&&(n=function(t){for(var e=t.parentNode.firstChild,n=0;e;){if(e===t)return n;1===e.nodeType&&"bookmark"===e.getAttribute("data-mce-type")||n++,e=e.nextSibling}return-1}(e),e=e.parentNode,r.remove(i)),o[t?"startContainer":"endContainer"]=e,o[t?"startOffset":"endOffset"]=n)}t(!0),t();var e=r.createRng();return e.setStart(o.startContainer,o.startOffset),o.endContainer&&e.setEnd(o.endContainer,o.endOffset),e},un=tinymce.util.Tools.resolve("tinymce.dom.TreeWalker"),cn=tinymce.util.Tools.resolve("tinymce.dom.RangeUtils"),dn=function(t){return"A"===t.nodeName&&t.hasAttribute("href")},fn=function(t){var e,n,i,r,o,s,a,l;return r=t.selection,o=t.dom,s=r.getRng(),a=o,l=cn.getNode(s.startContainer,s.startOffset),e=a.getParent(l,dn)||l,n=cn.getNode(s.endContainer,s.endOffset),i=t.getBody(),R.grep(function(t,e,n){var i,r,o=[];for(i=new un(e,t),r=e;r&&(1===r.nodeType&&o.push(r),r!==n);r=i.next());return o}(i,e,n),dn)},hn=function(t){var e,n,i,r,o;n=fn(e=t),r=e.dom,o=e.selection,i=an(r,o.getRng()),R.each(n,function(t){e.dom.remove(t,!0)}),o.setRng(ln(r,i))},mn=function(t){t.selection.collapse(!1)},gn=function(t){t.focus(),hn(t),mn(t)},pn=function(t,e){var n,i,r,o,s,a=t.dom.getParent(t.selection.getStart(),"a[href]");a?(o=a,s=e,(r=t).focus(),r.dom.setAttrib(o,"href",s),mn(r)):(i=e,(n=t).execCommand("mceInsertLink",!1,{href:i}),mn(n))},vn=function(t,e,n){var i,r,o;t.plugins.table?t.plugins.table.insertTable(e,n):(r=e,o=n,(i=t).undoManager.transact(function(){var t,e;i.insertContent(function(t,e){var n,i,r;for(r='<table data-mce-id="mce" style="width: 100%">',r+="<tbody>",i=0;i<e;i++){for(r+="<tr>",n=0;n<t;n++)r+="<td><br></td>";r+="</tr>"}return r+="</tbody>",r+="</table>"}(r,o)),(t=i.dom.select("*[data-mce-id]")[0]).removeAttribute("data-mce-id"),e=i.dom.select("td,th",t),i.selection.setCursorLocation(e[0],0)}))},bn=function(t,e){t.execCommand("FormatBlock",!1,e)},yn=function(t,e,n){var i,r;r=(i=t.editorUpload.blobCache).create(sn("mceu"),n,e),i.add(r),t.insertContent(t.dom.createHTML("img",{src:r.blobUri()}))},xn=function(t,e){0===e.trim().length?gn(t):pn(t,e)},wn=gn,_n=function(n,t){n.addButton("quicklink",{icon:"link",tooltip:"Insert/Edit link",stateSelector:"a[href]",onclick:function(){t.showForm(n,"quicklink")}}),n.addButton("quickimage",{icon:"image",tooltip:"Insert image",onclick:function(){rn().then(function(t){var e=t[0];nn(e).then(function(t){yn(n,t,e)})})}}),n.addButton("quicktable",{icon:"table",tooltip:"Insert table",onclick:function(){t.hide(),vn(n,2,2)}}),function(e){for(var t=function(t){return function(){bn(e,t)}},n=1;n<6;n++){var i="h"+n;e.addButton(i,{text:i.toUpperCase(),tooltip:"Heading "+n,stateSelector:i,onclick:t(i),onPostRender:function(){this.getEl().firstChild.firstChild.style.fontWeight="bold"}})}}(n)},Rn=function(){var t=h.container;if(t&&"static"!==v.DOM.getStyle(t,"position",!0)){var e=v.DOM.getPos(t),n=e.x-t.scrollLeft,i=e.y-t.scrollTop;return mt.some({x:n,y:i})}return mt.none()},Cn=function(t){return/^www\.|\.(com|org|edu|gov|uk|net|ca|de|jp|fr|au|us|ru|ch|it|nl|se|no|es|mil)$/i.test(t.trim())},kn=function(t){return/^https?:\/\//.test(t.trim())},En=function(t,e){return!kn(e)&&Cn(e)?(n=t,i=e,new en(function(e){n.windowManager.confirm("The URL you entered seems to be an external link. Do you want to add the required http:// prefix?",function(t){e(!0===t?"http://"+i:i)})})):en.resolve(e);var n,i},Hn=function(r,e){var t,n,i,o={};return t="quicklink",n={items:[{type:"button",name:"unlink",icon:"unlink",onclick:function(){r.focus(),wn(r),e()},tooltip:"Remove link"},{type:"filepicker",name:"linkurl",placeholder:"Paste or type a link",filetype:"file",onchange:function(t){var e=t.meta;e&&e.attach&&(o={href:this.value(),attach:e.attach})}},{type:"button",icon:"checkmark",subtype:"primary",tooltip:"Ok",onclick:"submit"}],onshow:function(t){if(t.control===this){var e,n="";(e=r.dom.getParent(r.selection.getStart(),"a[href]"))&&(n=r.dom.getAttrib(e,"href")),this.fromJSON({linkurl:n}),i=this.find("#unlink"),e?i.show():i.hide(),this.find("#linkurl")[0].focus()}var i},onsubmit:function(t){En(r,t.data.linkurl).then(function(t){r.undoManager.transact(function(){t===o.href&&(o.attach(),o={}),xn(r,t)}),e()})}},(i=_e.create(R.extend({type:"form",layout:"flex",direction:"row",padding:5,name:t,spacing:3},n))).on("show",function(){i.find("textbox").eq(0).each(function(t){t.focus()})}),i},Tn=function(n,t,e){var o,i,s=[];if(e)return R.each(B(i=e)?i:W(i)?i.split(/[ ,]/):[],function(t){if("|"===t)o=null;else if(n.buttons[t]){o||(o={type:"buttongroup",items:[]},s.push(o));var e=n.buttons[t];A(e)&&(e=e()),e.type=e.type||"button",(e=_e.create(e)).on("postRender",(i=n,r=e,function(){var e,t,n=(t=function(t,e){return{selector:t,handler:e}},(e=r).settings.stateSelector?t(e.settings.stateSelector,function(t){e.active(t)}):e.settings.disabledStateSelector?t(e.settings.disabledStateSelector,function(t){e.disabled(t)}):null);null!==n&&i.selection.selectorChanged(n.selector,n.handler)})),o.items.push(e)}var i,r}),_e.create({type:"toolbar",layout:"flow",name:t,items:s})},Sn=function(){var l,c,o=function(t){return 0<t.items().length},u=function(t,e){var n,i,r=(n=t,i=e,R.map(i,function(t){return Tn(n,t.id,t.items)})).concat([Tn(t,"text",J(t)),Tn(t,"insert",G(t)),Hn(t,p)]);return _e.create({type:"floatpanel",role:"dialog",classes:"tinymce tinymce-inline arrow",ariaLabel:"Inline toolbar",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:!0,border:1,items:R.grep(r,o),oncancel:function(){t.focus()}})},d=function(t){t&&t.show()},f=function(t,e){t.moveTo(e.x,e.y)},h=function(n,i){i=i?i.substr(0,2):"",R.each({t:"down",b:"up",c:"center"},function(t,e){n.classes.toggle("arrow-"+t,e===i.substr(0,1))}),"cr"===i?(n.classes.toggle("arrow-left",!0),n.classes.toggle("arrow-right",!1)):"cl"===i?(n.classes.toggle("arrow-left",!1),n.classes.toggle("arrow-right",!0)):R.each({l:"left",r:"right"},function(t,e){n.classes.toggle("arrow-"+t,e===i.substr(1,1))})},m=function(t,e){var n=t.items().filter("#"+e);return 0<n.length&&(n[0].show(),t.reflow(),!0)},g=function(t,e,n,i){var r,o,s,a;if(a=K(n),r=b(n),o=v.DOM.getRect(t.getEl()),s="insert"===e?Y(i,r,o):$(i,r,o)){var l=Rn().getOr({x:0,y:0}),u={x:s.rect.x-l.x,y:s.rect.y-l.y,w:s.rect.w,h:s.rect.h};return f(t,X(a,c=i,r,u)),h(t,s.position),!0}return!1},p=function(){l&&l.hide()};return{show:function(t,e,n,i){var r,o,s,a;l||(S(t),(l=u(t,i)).renderTo().reflow().moveTo(n.x,n.y),t.nodeChanged()),o=e,s=t,a=n,d(r=l),r.items().hide(),m(r,o)?!1===g(r,o,s,a)&&p():p()},showForm:function(t,e){if(l){if(l.items().hide(),!m(l,e))return void p();var n,i,r,o=void 0;d(l),l.items().hide(),m(l,e),r=K(t),n=b(t),o=v.DOM.getRect(l.getEl()),(i=$(c,n,o))&&(o=i.rect,f(l,X(r,c,n,o)),h(l,i.position))}},reposition:function(t,e,n){l&&g(l,e,t,n)},inForm:function(){return l&&l.visible()&&0<l.items().filter("form:visible").length},hide:p,focus:function(){l&&l.find("toolbar:visible").eq(0).each(function(t){t.focus(!0)})},remove:function(){l&&(l.remove(),l=null)}}},Mn=St.extend({Defaults:{firstControlClass:"first",lastControlClass:"last"},init:function(t){this.settings=R.extend({},this.Defaults,t)},preRender:function(t){t.bodyClasses.add(this.settings.containerClass)},applyClasses:function(t){var e,n,i,r,o=this.settings;e=o.firstControlClass,n=o.lastControlClass,t.each(function(t){t.classes.remove(e).remove(n).add(o.controlClass),t.visible()&&(i||(i=t),r=t)}),i&&i.classes.add(e),r&&r.classes.add(n)},renderHtml:function(t){var e="";return this.applyClasses(t.items()),t.items().each(function(t){e+=t.renderHtml()}),e},recalc:function(){},postRender:function(){},isNative:function(){return!1}}),Nn=Mn.extend({Defaults:{containerClass:"abs-layout",controlClass:"abs-layout-item"},recalc:function(t){t.items().filter(":visible").each(function(t){var e=t.settings;t.layoutRect({x:e.x,y:e.y,w:e.w,h:e.h}),t.recalc&&t.recalc()})},renderHtml:function(t){return'<div id="'+t._id+'-absend" class="'+t.classPrefix+'abs-end"></div>'+this._super(t)}}),On=ge.extend({Defaults:{classes:"widget btn",role:"button"},init:function(t){var e,n=this;n._super(t),t=n.settings,e=n.settings.size,n.on("click mousedown",function(t){t.preventDefault()}),n.on("touchstart",function(t){n.fire("click",t),t.preventDefault()}),t.subtype&&n.classes.add(t.subtype),e&&n.classes.add("btn-"+e),t.icon&&n.icon(t.icon)},icon:function(t){return arguments.length?(this.state.set("icon",t),this):this.state.get("icon")},repaint:function(){var t,e=this.getEl().firstChild;e&&((t=e.style).width=t.height="100%"),this._super()},renderHtml:function(){var t,e,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a="",l=n.settings;return(t=l.image)?(o="none","string"!=typeof t&&(t=_.window.getSelection?t[0]:t[1]),t=" style=\"background-image: url('"+t+"')\""):t="",s&&(n.classes.add("btn-has-text"),a='<span class="'+r+'txt">'+n.encode(s)+"</span>"),o=o?r+"ico "+r+"i-"+o:"",e="boolean"==typeof l.active?' aria-pressed="'+l.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" tabindex="-1"'+e+'><button id="'+i+'-button" role="presentation" type="button" tabindex="-1">'+(o?'<i class="'+o+'"'+t+"></i>":"")+a+"</button></div>"},bindStates:function(){var o=this,n=o.$,i=o.classPrefix+"txt";function s(t){var e=n("span."+i,o.getEl());t?(e[0]||(n("button:first",o.getEl()).append('<span class="'+i+'"></span>'),e=n("span."+i,o.getEl())),e.html(o.encode(t))):e.remove(),o.classes.toggle("btn-has-text",!!t)}return o.state.on("change:text",function(t){s(t.value)}),o.state.on("change:icon",function(t){var e=t.value,n=o.classPrefix;e=(o.settings.icon=e)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];e?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=e):r&&i.removeChild(r),s(o.state.get("text"))}),o._super()}}),Wn=On.extend({init:function(t){t=R.extend({text:"Browse...",multiple:!1,accept:null},t),this._super(t),this.classes.add("browsebutton"),t.multiple&&this.classes.add("multiple")},postRender:function(){var n=this,e=Ht.create("input",{type:"file",id:n._id+"-browse",accept:n.settings.accept});n._super(),Tt(e).on("change",function(t){var e=t.target.files;n.value=function(){return e.length?n.settings.multiple?e:e[0]:null},t.preventDefault(),e.length&&n.fire("change",t)}),Tt(e).on("click",function(t){t.stopPropagation()}),Tt(n.getEl("button")).on("click touchstart",function(t){t.stopPropagation(),e.click(),t.preventDefault()}),n.getEl().appendChild(e)},remove:function(){Tt(this.getEl("button")).off(),Tt(this.getEl("input")).off(),this._super()}}),Pn=Me.extend({Defaults:{defaultType:"button",role:"group"},renderHtml:function(){var t=this,e=t._layout;return t.classes.add("btn-group"),t.preRender(),e.preRender(t),'<div id="'+t._id+'" class="'+t.classes+'"><div id="'+t._id+'-body">'+(t.settings.html||"")+e.renderHtml(t)+"</div></div>"}}),Dn=ge.extend({Defaults:{classes:"checkbox",role:"checkbox",checked:!1},init:function(t){var e=this;e._super(t),e.on("click mousedown",function(t){t.preventDefault()}),e.on("click",function(t){t.preventDefault(),e.disabled()||e.checked(!e.checked())}),e.checked(e.settings.checked)},checked:function(t){return arguments.length?(this.state.set("checked",t),this):this.state.get("checked")},value:function(t){return arguments.length?this.checked(t):this.checked()},renderHtml:function(){var t=this,e=t._id,n=t.classPrefix;return'<div id="'+e+'" class="'+t.classes+'" unselectable="on" aria-labelledby="'+e+'-al" tabindex="-1"><i class="'+n+"ico "+n+'i-checkbox"></i><span id="'+e+'-al" class="'+n+'label">'+t.encode(t.state.get("text"))+"</span></div>"},bindStates:function(){var o=this;function e(t){o.classes.toggle("checked",t),o.aria("checked",t)}return o.state.on("change:text",function(t){o.getEl("al").firstChild.data=o.translate(t.value)}),o.state.on("change:checked change:value",function(t){o.fire("change"),e(t.value)}),o.state.on("change:icon",function(t){var e=t.value,n=o.classPrefix;if(void 0===e)return o.settings.icon;e=(o.settings.icon=e)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];e?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=e):r&&i.removeChild(r)}),o.state.get("checked")&&e(!0),o._super()}}),An=tinymce.util.Tools.resolve("tinymce.util.VK"),Bn=ge.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.classes.add("combobox"),r.subinput=!0,r.ariaTarget="inp",i.menu=i.menu||i.values,i.menu&&(i.icon="caret"),r.on("click",function(t){var e=t.target,n=r.getEl();if(Tt.contains(n,e)||e===n)for(;e&&e!==n;)e.id&&-1!==e.id.indexOf("-open")&&(r.fire("action"),i.menu&&(r.showMenu(),t.aria&&r.menu.items()[0].focus())),e=e.parentNode}),r.on("keydown",function(t){var e;13===t.keyCode&&"INPUT"===t.target.nodeName&&(t.preventDefault(),r.parents().reverse().each(function(t){if(t.toJSON)return e=t,!1}),r.fire("submit",{data:e.toJSON()}))}),r.on("keyup",function(t){if("INPUT"===t.target.nodeName){var e=r.state.get("value"),n=t.target.value;n!==e&&(r.state.set("value",n),r.fire("autocomplete",t))}}),r.on("mouseover",function(t){var e=r.tooltip().moveTo(-65535);if(r.statusLevel()&&-1!==t.target.className.indexOf(r.classPrefix+"status")){var n=r.statusMessage()||"Ok",i=e.text(n).show().testMoveRel(t.target,["bc-tc","bc-tl","bc-tr"]);e.classes.toggle("tooltip-n","bc-tc"===i),e.classes.toggle("tooltip-nw","bc-tl"===i),e.classes.toggle("tooltip-ne","bc-tr"===i),e.moveRel(t.target,i)}})},statusLevel:function(t){return 0<arguments.length&&this.state.set("statusLevel",t),this.state.get("statusLevel")},statusMessage:function(t){return 0<arguments.length&&this.state.set("statusMessage",t),this.state.get("statusMessage")},showMenu:function(){var t,e=this,n=e.settings;e.menu||((t=n.menu||[]).length?t={type:"menu",items:t}:t.type=t.type||"menu",e.menu=_e.create(t).parent(e).renderTo(e.getContainerElm()),e.fire("createmenu"),e.menu.reflow(),e.menu.on("cancel",function(t){t.control===e.menu&&e.focus()}),e.menu.on("show hide",function(t){t.control.items().each(function(t){t.active(t.value()===e.value())})}).fire("show"),e.menu.on("select",function(t){e.value(t.control.value())}),e.on("focusin",function(t){"INPUT"===t.target.tagName.toUpperCase()&&e.menu.hide()}),e.aria("expanded",!0)),e.menu.show(),e.menu.layoutRect({w:e.layoutRect().w}),e.menu.moveRel(e.getEl(),e.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},focus:function(){this.getEl("inp").focus()},repaint:function(){var t,e,n=this,i=n.getEl(),r=n.getEl("open"),o=n.layoutRect(),s=0,a=i.firstChild;n.statusLevel()&&"none"!==n.statusLevel()&&(s=parseInt(Ht.getRuntimeStyle(a,"padding-right"),10)-parseInt(Ht.getRuntimeStyle(a,"padding-left"),10)),t=r?o.w-Ht.getSize(r).width-10:o.w-10;var l=_.document;return l.all&&(!l.documentMode||l.documentMode<=8)&&(e=n.layoutRect().h-2+"px"),Tt(a).css({width:t-s,lineHeight:e}),n._super(),n},postRender:function(){var e=this;return Tt(this.getEl("inp")).on("change",function(t){e.state.set("value",t.target.value),e.fire("change",t)}),e._super()},renderHtml:function(){var t,e,n,i=this,r=i._id,o=i.settings,s=i.classPrefix,a=i.state.get("value")||"",l="",u="";return"spellcheck"in o&&(u+=' spellcheck="'+o.spellcheck+'"'),o.maxLength&&(u+=' maxlength="'+o.maxLength+'"'),o.size&&(u+=' size="'+o.size+'"'),o.subtype&&(u+=' type="'+o.subtype+'"'),n='<i id="'+r+'-status" class="mce-status mce-ico" style="display: none"></i>',i.disabled()&&(u+=' disabled="disabled"'),(t=o.icon)&&"caret"!==t&&(t=s+"ico "+s+"i-"+o.icon),e=i.state.get("text"),(t||e)&&(l='<div id="'+r+'-open" class="'+s+"btn "+s+'open" tabIndex="-1" role="button"><button id="'+r+'-action" type="button" hidefocus="1" tabindex="-1">'+("caret"!==t?'<i class="'+t+'"></i>':'<i class="'+s+'caret"></i>')+(e?(t?" ":"")+e:"")+"</button></div>",i.classes.add("has-open")),'<div id="'+r+'" class="'+i.classes+'"><input id="'+r+'-inp" class="'+s+'textbox" value="'+i.encode(a,!1)+'" hidefocus="1"'+u+' placeholder="'+i.encode(o.placeholder)+'" />'+n+l+"</div>"},value:function(t){return arguments.length?(this.state.set("value",t),this):(this.state.get("rendered")&&this.state.set("value",this.getEl("inp").value),this.state.get("value"))},showAutoComplete:function(t,i){var r=this;if(0!==t.length){r.menu?r.menu.items().remove():r.menu=_e.create({type:"menu",classes:"combobox-menu",layout:"flow"}).parent(r).renderTo(),R.each(t,function(t){var e,n;r.menu.add({text:t.title,url:t.previewUrl,match:i,classes:"menu-item-ellipsis",onclick:(e=t.value,n=t.title,function(){r.fire("selectitem",{title:n,value:e})})})}),r.menu.renderNew(),r.hideMenu(),r.menu.on("cancel",function(t){t.control.parent()===r.menu&&(t.stopPropagation(),r.focus(),r.hideMenu())}),r.menu.on("select",function(){r.focus()});var e=r.layoutRect().w;r.menu.layoutRect({w:e,minW:0,maxW:e}),r.menu.repaint(),r.menu.reflow(),r.menu.show(),r.menu.moveRel(r.getEl(),r.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])}else r.hideMenu()},hideMenu:function(){this.menu&&this.menu.hide()},bindStates:function(){var r=this;r.state.on("change:value",function(t){r.getEl("inp").value!==t.value&&(r.getEl("inp").value=t.value)}),r.state.on("change:disabled",function(t){r.getEl("inp").disabled=t.value}),r.state.on("change:statusLevel",function(t){var e=r.getEl("status"),n=r.classPrefix,i=t.value;Ht.css(e,"display","none"===i?"none":""),Ht.toggleClass(e,n+"i-checkmark","ok"===i),Ht.toggleClass(e,n+"i-warning","warn"===i),Ht.toggleClass(e,n+"i-error","error"===i),r.classes.toggle("has-status","none"!==i),r.repaint()}),Ht.on(r.getEl("status"),"mouseleave",function(){r.tooltip().hide()}),r.on("cancel",function(t){r.menu&&r.menu.visible()&&(t.stopPropagation(),r.hideMenu())});var n=function(t,e){e&&0<e.items().length&&e.items().eq(t)[0].focus()};return r.on("keydown",function(t){var e=t.keyCode;"INPUT"===t.target.nodeName&&(e===An.DOWN?(t.preventDefault(),r.fire("autocomplete"),n(0,r.menu)):e===An.UP&&(t.preventDefault(),n(-1,r.menu)))}),r._super()},remove:function(){Tt(this.getEl("inp")).off(),this.menu&&this.menu.remove(),this._super()}}),Ln=Bn.extend({init:function(t){var e=this;t.spellcheck=!1,t.onaction&&(t.icon="none"),e._super(t),e.classes.add("colorbox"),e.on("change keyup postrender",function(){e.repaintColor(e.value())})},repaintColor:function(t){var e=this.getEl("open"),n=e?e.getElementsByTagName("i")[0]:null;if(n)try{n.style.background=t}catch(i){}},bindStates:function(){var e=this;return e.state.on("change:value",function(t){e.state.get("rendered")&&e.repaintColor(t.value)}),e._super()}}),In=On.extend({showPanel:function(){var e=this,t=e.settings;if(e.classes.add("opened"),e.panel)e.panel.show();else{var n=t.panel;n.type&&(n={layout:"grid",items:n}),n.role=n.role||"dialog",n.popover=!0,n.autohide=!0,n.ariaRoot=!0,e.panel=new ze(n).on("hide",function(){e.classes.remove("opened")}).on("cancel",function(t){t.stopPropagation(),e.focus(),e.hidePanel()}).parent(e).renderTo(e.getContainerElm()),e.panel.fire("show"),e.panel.reflow()}var i=e.panel.testMoveRel(e.getEl(),t.popoverAlign||(e.isRtl()?["bc-tc","bc-tl","bc-tr"]:["bc-tc","bc-tr","bc-tl","tc-bc","tc-br","tc-bl"]));e.panel.classes.toggle("start","l"===i.substr(-1)),e.panel.classes.toggle("end","r"===i.substr(-1));var r="t"===i.substr(0,1);e.panel.classes.toggle("bottom",!r),e.panel.classes.toggle("top",r),e.panel.moveRel(e.getEl(),i)},hidePanel:function(){this.panel&&this.panel.hide()},postRender:function(){var e=this;return e.aria("haspopup",!0),e.on("click",function(t){t.control===e&&(e.panel&&e.panel.visible()?e.hidePanel():(e.showPanel(),e.panel.focus(!!t.aria)))}),e._super()},remove:function(){return this.panel&&(this.panel.remove(),this.panel=null),this._super()}}),zn=v.DOM,Fn=In.extend({init:function(t){this._super(t),this.classes.add("splitbtn"),this.classes.add("colorbutton")},color:function(t){return t?(this._color=t,this.getEl("preview").style.backgroundColor=t,this):this._color},resetColor:function(){return this._color=null,this.getEl("preview").style.backgroundColor=null,this},renderHtml:function(){var t=this,e=t._id,n=t.classPrefix,i=t.state.get("text"),r=t.settings.icon?n+"ico "+n+"i-"+t.settings.icon:"",o=t.settings.image?" style=\"background-image: url('"+t.settings.image+"')\"":"",s="";return i&&(t.classes.add("btn-has-text"),s='<span class="'+n+'txt">'+t.encode(i)+"</span>"),'<div id="'+e+'" class="'+t.classes+'" role="button" tabindex="-1" aria-haspopup="true"><button role="presentation" hidefocus="1" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+o+"></i>":"")+'<span id="'+e+'-preview" class="'+n+'preview"></span>'+s+'</button><button type="button" class="'+n+'open" hidefocus="1" tabindex="-1"> <i class="'+n+'caret"></i></button></div>'},postRender:function(){var e=this,n=e.settings.onclick;return e.on("click",function(t){t.aria&&"down"===t.aria.key||t.control!==e||zn.getParent(t.target,"."+e.classPrefix+"open")||(t.stopImmediatePropagation(),n.call(e,t))}),delete e.settings.onclick,e._super()}}),Un=tinymce.util.Tools.resolve("tinymce.util.Color"),Vn=ge.extend({Defaults:{classes:"widget colorpicker"},init:function(t){this._super(t)},postRender:function(){var n,i,r,o,s,a=this,l=a.color();function u(t,e){var n,i,r=Ht.getPos(t);return n=e.pageX-r.x,i=e.pageY-r.y,{x:n=Math.max(0,Math.min(n/t.clientWidth,1)),y:i=Math.max(0,Math.min(i/t.clientHeight,1))}}function c(t,e){var n=(360-t.h)/360;Ht.css(r,{top:100*n+"%"}),e||Ht.css(s,{left:t.s+"%",top:100-t.v+"%"}),o.style.background=Un({s:100,v:100,h:t.h}).toHex(),a.color().parse({s:t.s,v:t.v,h:t.h})}function t(t){var e;e=u(o,t),n.s=100*e.x,n.v=100*(1-e.y),c(n),a.fire("change")}function e(t){var e;e=u(i,t),(n=l.toHsv()).h=360*(1-e.y),c(n,!0),a.fire("change")}i=a.getEl("h"),r=a.getEl("hp"),o=a.getEl("sv"),s=a.getEl("svp"),a._repaint=function(){c(n=l.toHsv())},a._super(),a._svdraghelper=new we(a._id+"-sv",{start:t,drag:t}),a._hdraghelper=new we(a._id+"-h",{start:e,drag:e}),a._repaint()},rgb:function(){return this.color().toRgb()},value:function(t){if(!arguments.length)return this.color().toHex();this.color().parse(t),this._rendered&&this._repaint()},color:function(){return this._color||(this._color=Un()),this._color},renderHtml:function(){var t,e=this._id,o=this.classPrefix,s="#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000";return t='<div id="'+e+'-h" class="'+o+'colorpicker-h" style="background: -ms-linear-gradient(top,'+s+");background: linear-gradient(to bottom,"+s+');">'+function(){var t,e,n,i,r="";for(n="filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=",t=0,e=(i=s.split(",")).length-1;t<e;t++)r+='<div class="'+o+'colorpicker-h-chunk" style="height:'+100/e+"%;"+n+i[t]+",endColorstr="+i[t+1]+");-ms-"+n+i[t]+",endColorstr="+i[t+1]+')"></div>';return r}()+'<div id="'+e+'-hp" class="'+o+'colorpicker-h-marker"></div></div>','<div id="'+e+'" class="'+this.classes+'"><div id="'+e+'-sv" class="'+o+'colorpicker-sv"><div class="'+o+'colorpicker-overlay1"><div class="'+o+'colorpicker-overlay2"><div id="'+e+'-svp" class="'+o+'colorpicker-selector1"><div class="'+o+'colorpicker-selector2"></div></div></div></div></div>'+t+"</div>"}}),qn=ge.extend({init:function(t){t=R.extend({height:100,text:"Drop an image here",multiple:!1,accept:null},t),this._super(t),this.classes.add("dropzone"),t.multiple&&this.classes.add("multiple")},renderHtml:function(){var t,e,n=this.settings;return t={id:this._id,hidefocus:"1"},e=Ht.create("div",t,"<span>"+this.translate(n.text)+"</span>"),n.height&&Ht.css(e,"height",n.height+"px"),n.width&&Ht.css(e,"width",n.width+"px"),e.className=this.classes,e.outerHTML},postRender:function(){var i=this,t=function(t){t.preventDefault(),i.classes.toggle("dragenter"),i.getEl().className=i.classes};i._super(),i.$el.on("dragover",function(t){t.preventDefault()}),i.$el.on("dragenter",t),i.$el.on("dragleave",t),i.$el.on("drop",function(t){if(t.preventDefault(),!i.state.get("disabled")){var e=function(t){var e=i.settings.accept;if("string"!=typeof e)return t;var n=new RegExp("("+e.split(/\s*,\s*/).join("|")+")$","i");return R.grep(t,function(t){return n.test(t.name)})}(t.dataTransfer.files);i.value=function(){return e.length?i.settings.multiple?e:e[0]:null},e.length&&i.fire("change",t)}})},remove:function(){this.$el.off(),this._super()}}),Yn=ge.extend({init:function(t){var n=this;t.delimiter||(t.delimiter="\xbb"),n._super(t),n.classes.add("path"),n.canFocus=!0,n.on("click",function(t){var e;(e=t.target.getAttribute("data-index"))&&n.fire("select",{value:n.row()[e],index:e})}),n.row(n.settings.row)},focus:function(){return this.getEl().firstChild.focus(),this},row:function(t){return arguments.length?(this.state.set("row",t),this):this.state.get("row")},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'">'+this._getDataPathHtml(this.state.get("row"))+"</div>"},bindStates:function(){var e=this;return e.state.on("change:row",function(t){e.innerHtml(e._getDataPathHtml(t.value))}),e._super()},_getDataPathHtml:function(t){var e,n,i=t||[],r="",o=this.classPrefix;for(e=0,n=i.length;e<n;e++)r+=(0<e?'<div class="'+o+'divider" aria-hidden="true"> '+this.settings.delimiter+" </div>":"")+'<div role="button" class="'+o+"path-item"+(e===n-1?" "+o+"last":"")+'" data-index="'+e+'" tabindex="-1" id="'+this._id+"-"+e+'" aria-level="'+(e+1)+'">'+i[e].name+"</div>";return r||(r='<div class="'+o+'path-item">\xa0</div>'),r}}),$n=Yn.extend({postRender:function(){var o=this,s=o.settings.editor;function a(t){if(1===t.nodeType){if("BR"===t.nodeName||t.getAttribute("data-mce-bogus"))return!0;if("bookmark"===t.getAttribute("data-mce-type"))return!0}return!1}return!1!==s.settings.elementpath&&(o.on("select",function(t){s.focus(),s.selection.select(this.row()[t.index].element),s.nodeChanged()}),s.on("nodeChange",function(t){for(var e=[],n=t.parents,i=n.length;i--;)if(1===n[i].nodeType&&!a(n[i])){var r=s.fire("ResolveName",{name:n[i].nodeName.toLowerCase(),target:n[i]});if(r.isDefaultPrevented()||e.push({name:r.name,element:n[i]}),r.isPropagationStopped())break}o.row(e)})),o._super()}}),Xn=Me.extend({Defaults:{layout:"flex",align:"center",defaults:{flex:1}},renderHtml:function(){var t=this,e=t._layout,n=t.classPrefix;return t.classes.add("formitem"),e.preRender(t),'<div id="'+t._id+'" class="'+t.classes+'" hidefocus="1" tabindex="-1">'+(t.settings.title?'<div id="'+t._id+'-title" class="'+n+'title">'+t.settings.title+"</div>":"")+'<div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+(t.settings.html||"")+e.renderHtml(t)+"</div></div>"}}),jn=Me.extend({Defaults:{containerCls:"form",layout:"flex",direction:"column",align:"stretch",flex:1,padding:15,labelGap:30,spacing:10,callbacks:{submit:function(){this.submit()}}},preRender:function(){var i=this,t=i.items();i.settings.formItemDefaults||(i.settings.formItemDefaults={layout:"flex",autoResize:"overflow",defaults:{flex:1}}),t.each(function(t){var e,n=t.settings.label;n&&((e=new Xn(R.extend({items:{type:"label",id:t._id+"-l",text:n,flex:0,forId:t._id,disabled:t.disabled()}},i.settings.formItemDefaults))).type="formitem",t.aria("labelledby",t._id+"-l"),"undefined"==typeof t.settings.flex&&(t.settings.flex=1),i.replace(t,e),e.add(t))})},submit:function(){return this.fire("submit",{data:this.toJSON()})},postRender:function(){this._super(),this.fromJSON(this.settings.data)},bindStates:function(){var n=this;function t(){var t,e,i=0,r=[];if(!1!==n.settings.labelGapCalc)for(("children"===n.settings.labelGapCalc?n.find("formitem"):n.items()).filter("formitem").each(function(t){var e=t.items()[0],n=e.getEl().clientWidth;i=i<n?n:i,r.push(e)}),e=n.settings.labelGap||0,t=r.length;t--;)r[t].settings.minWidth=i+e}n._super(),n.on("show",t),t()}}),Jn=jn.extend({Defaults:{containerCls:"fieldset",layout:"flex",direction:"column",align:"stretch",flex:1,padding:"25 15 5 15",labelGap:30,spacing:10,border:1},renderHtml:function(){var t=this,e=t._layout,n=t.classPrefix;return t.preRender(),e.preRender(t),'<fieldset id="'+t._id+'" class="'+t.classes+'" hidefocus="1" tabindex="-1">'+(t.settings.title?'<legend id="'+t._id+'-title" class="'+n+'fieldset-title">'+t.settings.title+"</legend>":"")+'<div id="'+t._id+'-body" class="'+t.bodyClasses+'">'+(t.settings.html||"")+e.renderHtml(t)+"</div></fieldset>"}}),Gn=0,Kn=function(t){if(null===t||t===undefined)throw new Error("Node cannot be null or undefined");return{dom:lt(t)}},Zn={fromHtml:function(t,e){var n=(e||_.document).createElement("div");if(n.innerHTML=t,!n.hasChildNodes()||1<n.childNodes.length)throw _.console.error("HTML does not have a single root node",t),new Error("HTML must have a single root node");return Kn(n.childNodes[0])},fromTag:function(t,e){var n=(e||_.document).createElement(t);return Kn(n)},fromText:function(t,e){var n=(e||_.document).createTextNode(t);return Kn(n)},fromDom:Kn,fromPoint:function(t,e,n){var i=t.dom();return mt.from(i.elementFromPoint(e,n)).map(Kn)}},Qn=(_.Node.ATTRIBUTE_NODE,_.Node.CDATA_SECTION_NODE,_.Node.COMMENT_NODE,_.Node.DOCUMENT_NODE),ti=(_.Node.DOCUMENT_TYPE_NODE,_.Node.DOCUMENT_FRAGMENT_NODE,_.Node.ELEMENT_NODE),ei=(_.Node.TEXT_NODE,_.Node.PROCESSING_INSTRUCTION_NODE,_.Node.ENTITY_REFERENCE_NODE,_.Node.ENTITY_NODE,_.Node.NOTATION_NODE,function(t,e){var n=function(t,e){for(var n=0;n<t.length;n++){var i=t[n];if(i.test(e))return i}return undefined}(t,e);if(!n)return{major:0,minor:0};var i=function(t){return Number(e.replace(n,"$"+t))};return ii(i(1),i(2))}),ni=function(){return ii(0,0)},ii=function(t,e){return{major:t,minor:e}},ri={nu:ii,detect:function(t,e){var n=String(e).toLowerCase();return 0===t.length?ni():ei(t,n)},unknown:ni},oi="Firefox",si=function(t,e){return function(){return e===t}},ai=function(t){var e=t.current;return{current:e,version:t.version,isEdge:si("Edge",e),isChrome:si("Chrome",e),isIE:si("IE",e),isOpera:si("Opera",e),isFirefox:si(oi,e),isSafari:si("Safari",e)}},li={unknown:function(){return ai({current:undefined,version:ri.unknown()})},nu:ai,edge:lt("Edge"),chrome:lt("Chrome"),ie:lt("IE"),opera:lt("Opera"),firefox:lt(oi),safari:lt("Safari")},ui="Windows",ci="Android",di="Solaris",fi="FreeBSD",hi=function(t,e){return function(){return e===t}},mi=function(t){var e=t.current;return{current:e,version:t.version,isWindows:hi(ui,e),isiOS:hi("iOS",e),isAndroid:hi(ci,e),isOSX:hi("OSX",e),isLinux:hi("Linux",e),isSolaris:hi(di,e),isFreeBSD:hi(fi,e)}},gi={unknown:function(){return mi({current:undefined,version:ri.unknown()})},nu:mi,windows:lt(ui),ios:lt("iOS"),android:lt(ci),linux:lt("Linux"),osx:lt("OSX"),solaris:lt(di),freebsd:lt(fi)},pi=function(t,e){var n=String(e).toLowerCase();return function(t,e){for(var n=0,i=t.length;n<i;n++){var r=t[n];if(e(r,n))return mt.some(r)}return mt.none()}(t,function(t){return t.search(n)})},vi=function(t,n){return pi(t,n).map(function(t){var e=ri.detect(t.versionRegexes,n);return{current:t.name,version:e}})},bi=function(t,n){return pi(t,n).map(function(t){var e=ri.detect(t.versionRegexes,n);return{current:t.name,version:e}})},yi=function(t,e){return-1!==t.indexOf(e)},xi=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,wi=function(e){return function(t){return yi(t,e)}},_i=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(t){return yi(t,"edge/")&&yi(t,"chrome")&&yi(t,"safari")&&yi(t,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,xi],search:function(t){return yi(t,"chrome")&&!yi(t,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(t){return yi(t,"msie")||yi(t,"trident")}},{name:"Opera",versionRegexes:[xi,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:wi("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:wi("firefox")},{name:"Safari",versionRegexes:[xi,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(t){return(yi(t,"safari")||yi(t,"mobile/"))&&yi(t,"applewebkit")}}],Ri=[{name:"Windows",search:wi("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(t){return yi(t,"iphone")||yi(t,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:wi("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:wi("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:wi("linux"),versionRegexes:[]},{name:"Solaris",search:wi("sunos"),versionRegexes:[]},{name:"FreeBSD",search:wi("freebsd"),versionRegexes:[]}],Ci={browsers:lt(_i),oses:lt(Ri)},ki=function(t){var e,n,i,r,o,s,a,l,u,c,d,f=Ci.browsers(),h=Ci.oses(),m=vi(f,t).fold(li.unknown,li.nu),g=bi(h,t).fold(gi.unknown,gi.nu);return{browser:m,os:g,deviceType:(n=m,i=t,r=(e=g).isiOS()&&!0===/ipad/i.test(i),o=e.isiOS()&&!r,s=e.isAndroid()&&3===e.version.major,a=e.isAndroid()&&4===e.version.major,l=r||s||a&&!0===/mobile/i.test(i),u=e.isiOS()||e.isAndroid(),c=u&&!l,d=n.isSafari()&&e.isiOS()&&!1===/safari/i.test(i),{isiPad:lt(r),isiPhone:lt(o),isTablet:lt(l),isPhone:lt(c),isTouch:lt(u),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:lt(d)})}},Ei=(Je=!(Xe=function(){var t=_.navigator.userAgent;return ki(t)}),function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];return Je||(Je=!0,je=Xe.apply(null,t)),je}),Hi=ti,Ti=Qn,Si=function(t){return t.nodeType!==Hi&&t.nodeType!==Ti||0===t.childElementCount},Mi=(Ei().browser.isIE(),function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e]}("element","offset"),R.trim),Ni=function(e){return function(t){if(t&&1===t.nodeType){if(t.contentEditable===e)return!0;if(t.getAttribute("data-mce-contenteditable")===e)return!0}return!1}},Oi=Ni("true"),Wi=Ni("false"),Pi=function(t,e,n,i,r){return{type:t,title:e,url:n,level:i,attach:r}},Di=function(t){return t.innerText||t.textContent},Ai=function(t){return t.id?t.id:(e="h",n=(new Date).getTime(),e+"_"+Math.floor(1e9*Math.random())+ ++Gn+String(n));var e,n},Bi=function(t){return(e=t)&&"A"===e.nodeName&&(e.id||e.name)&&Ii(t);var e},Li=function(t){return t&&/^(H[1-6])$/.test(t.nodeName)},Ii=function(t){return function(t){for(;t=t.parentNode;){var e=t.contentEditable;if(e&&"inherit"!==e)return Oi(t)}return!1}(t)&&!Wi(t)},zi=function(t){return Li(t)&&Ii(t)},Fi=function(t){var e,n=Ai(t);return Pi("header",Di(t),"#"+n,Li(e=t)?parseInt(e.nodeName.substr(1),10):0,function(){t.id=n})},Ui=function(t){var e=t.id||t.name,n=Di(t);return Pi("anchor",n||"#"+e,"#"+e,0,at)},Vi=function(t){var e,n,i,r,o,s;return e="h1,h2,h3,h4,h5,h6,a:not([href])",n=t,Rt((Ei().browser.isIE(),function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e]}("element","offset"),i=Zn.fromDom(n),r=e,s=(o=i)===undefined?_.document:o.dom(),Si(s)?[]:Rt(s.querySelectorAll(r),Zn.fromDom)),function(t){return t.dom()})},qi=function(t){return 0<Mi(t.title).length},Yi=function(t){var e,n=Vi(t);return kt((e=n,Rt(kt(e,zi),Fi)).concat(Rt(kt(n,Bi),Ui)),qi)},$i={},Xi=function(t){return{title:t.title,value:{title:{raw:t.title},url:t.url,attach:t.attach}}},ji=function(t,e){return{title:t,value:{title:t,url:e,attach:at}}},Ji=function(t,e,n){var i=e in t?t[e]:n;return!1===i?null:i},Gi=function(t,i,r,e){var n,o,s,a,l,u,c={title:"-"},d=function(t){var e=t.hasOwnProperty(r)?t[r]:[],n=kt(e,function(t){return e=t,!_t(i,function(t){return t.url===e});var e});return R.map(n,function(t){return{title:t,value:{title:t,url:t,attach:at}}})},f=function(e){var t,n=kt(i,function(t){return t.type===e});return t=n,R.map(t,Xi)};return!1===e.typeahead_urls?[]:"file"===r?(n=[Ki(t,d($i)),Ki(t,f("header")),Ki(t,(a=f("anchor"),l=Ji(e,"anchor_top","#top"),u=Ji(e,"anchor_bottom","#bottom"),null!==l&&a.unshift(ji("<top>",l)),null!==u&&a.push(ji("<bottom>",u)),a))],o=function(t,e){return 0===t.length||0===e.length?t.concat(e):t.concat(c,e)},s=[],Ct(n,function(t){s=o(s,t)}),s):Ki(t,d($i))},Ki=function(t,e){var n=t.toLowerCase(),i=R.grep(e,function(t){return-1!==t.title.toLowerCase().indexOf(n)});return 1===i.length&&i[0].title===t?[]:i},Zi=function(r,i,o,s){var e=function(t){var e=Yi(o),n=Gi(t,e,s,i);r.showAutoComplete(n,t)};r.on("autocomplete",function(){e(r.value())}),r.on("selectitem",function(t){var e=t.value;r.value(e.url);var n,i=(n=e.title).raw?n.raw:n;"image"===s?r.fire("change",{meta:{alt:i,attach:e.attach}}):r.fire("change",{meta:{text:i,attach:e.attach}}),r.focus()}),r.on("click",function(t){0===r.value().length&&"INPUT"===t.target.nodeName&&e("")}),r.on("PostRender",function(){r.getRoot().on("submit",function(t){var e,n,i;t.isDefaultPrevented()||(e=r.value(),i=$i[n=s],/^https?/.test(e)&&(i?wt(i,e).isNone()&&($i[n]=i.slice(0,5).concat(e)):$i[n]=[e]))})})},Qi=function(o,t,n){var i=t.filepicker_validator_handler;i&&o.state.on("change:value",function(t){var e;0!==(e=t.value).length?i({url:e,type:n},function(t){var e,n,i,r=(n=(e=t).status,i=e.message,"valid"===n?{status:"ok",message:i}:"unknown"===n?{status:"warn",message:i}:"invalid"===n?{status:"warn",message:i}:{status:"none",message:""});o.statusMessage(r.message),o.statusLevel(r.status)}):o.statusLevel("none")})},tr=Bn.extend({Statics:{clearHistory:function(){$i={}}},init:function(t){var e,n,i,r=this,o=window.tinymce?window.tinymce.activeEditor:M.activeEditor,s=o.settings,a=t.filetype;t.spellcheck=!1,(i=s.file_picker_types||s.file_browser_callback_types)&&(i=R.makeMap(i,/[, ]/)),i&&!i[a]||(!(n=s.file_picker_callback)||i&&!i[a]?!(n=s.file_browser_callback)||i&&!i[a]||(e=function(){n(r.getEl("inp").id,r.value(),a,window)}):e=function(){var t=r.fire("beforecall").meta;t=R.extend({filetype:a},t),n.call(o,function(t,e){r.value(t).fire("change",{meta:e})},r.value(),t)}),e&&(t.icon="browse",t.onaction=e),r._super(t),r.classes.add("filepicker"),Zi(r,s,o.getBody(),a),Qi(r,s,a)}}),er=Nn.extend({recalc:function(t){var e=t.layoutRect(),n=t.paddingBox;t.items().filter(":visible").each(function(t){t.layoutRect({x:n.left,y:n.top,w:e.innerW-n.right-n.left,h:e.innerH-n.top-n.bottom}),t.recalc&&t.recalc()})}}),nr=Nn.extend({recalc:function(t){var e,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,b,y,x,w,_,R,C,k,E,H,T,S,M,N,O,W,P,D,A,B,L=[],I=Math.max,z=Math.min;for(i=t.items().filter(":visible"),r=t.layoutRect(),o=t.paddingBox,s=t.settings,f=t.isRtl()?s.direction||"row-reversed":s.direction,a=s.align,l=t.isRtl()?s.pack||"end":s.pack,u=s.spacing||0,"row-reversed"!==f&&"column-reverse"!==f||(i=i.set(i.toArray().reverse()),f=f.split("-")[0]),"column"===f?(C="y",_="h",R="minH",k="maxH",H="innerH",E="top",T="deltaH",S="contentH",P="left",O="w",M="x",N="innerW",W="minW",D="right",A="deltaW",B="contentW"):(C="x",_="w",R="minW",k="maxW",H="innerW",E="left",T="deltaW",S="contentW",P="top",O="h",M="y",N="innerH",W="minH",D="bottom",A="deltaH",B="contentH"),d=r[H]-o[E]-o[E],w=c=0,e=0,n=i.length;e<n;e++)m=(h=i[e]).layoutRect(),d-=e<n-1?u:0,0<(g=h.settings.flex)&&(c+=g,m[k]&&L.push(h),m.flex=g),d-=m[R],w<(p=o[P]+m[W]+o[D])&&(w=p);if((y={})[R]=d<0?r[R]-d+r[T]:r[H]-d+r[T],y[W]=w+r[A],y[S]=r[H]-d,y[B]=w,y.minW=z(y.minW,r.maxW),y.minH=z(y.minH,r.maxH),y.minW=I(y.minW,r.startMinWidth),y.minH=I(y.minH,r.startMinHeight),!r.autoResize||y.minW===r.minW&&y.minH===r.minH){for(b=d/c,e=0,n=L.length;e<n;e++)(v=(m=(h=L[e]).layoutRect())[k])<(p=m[R]+m.flex*b)?(d-=m[k]-m[R],c-=m.flex,m.flex=0,m.maxFlexSize=v):m.maxFlexSize=0;for(b=d/c,x=o[E],y={},0===c&&("end"===l?x=d+o[E]:"center"===l?(x=Math.round(r[H]/2-(r[H]-d)/2)+o[E])<0&&(x=o[E]):"justify"===l&&(x=o[E],u=Math.floor(d/(i.length-1)))),y[M]=o[P],e=0,n=i.length;e<n;e++)p=(m=(h=i[e]).layoutRect()).maxFlexSize||m[R],"center"===a?y[M]=Math.round(r[N]/2-m[O]/2):"stretch"===a?(y[O]=I(m[W]||0,r[N]-o[P]-o[D]),y[M]=o[P]):"end"===a&&(y[M]=r[N]-m[O]-o.top),0<m.flex&&(p+=m.flex*b),y[_]=p,y[C]=x,h.layoutRect(y),h.recalc&&h.recalc(),x+=p+u}else if(y.w=y.minW,y.h=y.minH,t.layoutRect(y),this.recalc(t),null===t._lastRect){var F=t.parent();F&&(F._lastRect=null,F.recalc())}}}),ir=Mn.extend({Defaults:{containerClass:"flow-layout",controlClass:"flow-layout-item",endClass:"break"},recalc:function(t){t.items().filter(":visible").each(function(t){t.recalc&&t.recalc()})},isNative:function(){return!0}}),rr=function(t,e){return n=e,r=(i=t)===undefined?_.document:i.dom(),Si(r)?mt.none():mt.from(r.querySelector(n)).map(Zn.fromDom);var n,i,r},or=function(t,e){return function(){t.execCommand("mceToggleFormat",!1,e)}},sr=function(t,e,n){var i=function(t){n(t,e)};t.formatter?t.formatter.formatChanged(e,i):t.on("init",function(){t.formatter.formatChanged(e,i)})},ar=function(t,n){return function(e){sr(t,n,function(t){e.control.active(t)})}},lr=function(i){var e=["alignleft","aligncenter","alignright","alignjustify"],r="alignleft",t=[{text:"Left",icon:"alignleft",onclick:or(i,"alignleft")},{text:"Center",icon:"aligncenter",onclick:or(i,"aligncenter")},{text:"Right",icon:"alignright",onclick:or(i,"alignright")},{text:"Justify",icon:"alignjustify",onclick:or(i,"alignjustify")}];i.addMenuItem("align",{text:"Align",menu:t}),i.addButton("align",{type:"menubutton",icon:r,menu:t,onShowMenu:function(t){var n=t.control.menu;R.each(e,function(e,t){n.items().eq(t).each(function(t){return t.active(i.formatter.match(e))})})},onPostRender:function(t){var n=t.control;R.each(e,function(e,t){sr(i,e,function(t){n.icon(r),t&&n.icon(e)})})}}),R.each({alignleft:["Align left","JustifyLeft"],aligncenter:["Align center","JustifyCenter"],alignright:["Align right","JustifyRight"],alignjustify:["Justify","JustifyFull"],alignnone:["No alignment","JustifyNone"]},function(t,e){i.addButton(e,{active:!1,tooltip:t[0],cmd:t[1],onPostRender:ar(i,e)})})},ur=function(t){return t?t.split(",")[0]:""},cr=function(l,u){return function(){var a=this;a.state.set("value",null),l.on("init nodeChange",function(t){var e,n,i,r,o=l.queryCommandValue("FontName"),s=(e=u,r=(n=o)?n.toLowerCase():"",R.each(e,function(t){t.value.toLowerCase()===r&&(i=t.value)}),R.each(e,function(t){i||ur(t.value).toLowerCase()!==ur(r).toLowerCase()||(i=t.value)}),i);a.value(s||null),!s&&o&&a.text(ur(o))})}},dr=function(n){n.addButton("fontselect",function(){var t,e=(t=function(t){for(var e=(t=t.replace(/;$/,"").split(";")).length;e--;)t[e]=t[e].split("=");return t}(n.settings.font_formats||"Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats"),R.map(t,function(t){return{text:{raw:t[0]},value:t[1],textStyle:-1===t[1].indexOf("dings")?"font-family:"+t[1]:""}}));return{type:"listbox",text:"Font Family",tooltip:"Font Family",values:e,fixedWidth:!0,onPostRender:cr(n,e),onselect:function(t){t.control.settings.value&&n.execCommand("FontName",!1,t.control.settings.value)}}})},fr=function(t){dr(t)},hr=function(t,e){return/[0-9.]+px$/.test(t)?(n=72*parseInt(t,10)/96,i=e||0,r=Math.pow(10,i),Math.round(n*r)/r+"pt"):t;var n,i,r},mr=function(t,e,n){var i;return R.each(t,function(t){t.value===n?i=n:t.value===e&&(i=e)}),i},gr=function(n){n.addButton("fontsizeselect",function(){var t,s,a,e=(t=n.settings.fontsize_formats||"8pt 10pt 12pt 14pt 18pt 24pt 36pt",R.map(t.split(" "),function(t){var e=t,n=t,i=t.split("=");return 1<i.length&&(e=i[0],n=i[1]),{text:e,value:n}}));return{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:e,fixedWidth:!0,onPostRender:(s=n,a=e,function(){var o=this;s.on("init nodeChange",function(t){var e,n,i,r;if(e=s.queryCommandValue("FontSize"))for(i=3;!r&&0<=i;i--)n=hr(e,i),r=mr(a,n,e);o.value(r||null),r||o.text(n)})}),onclick:function(t){t.control.settings.value&&n.execCommand("FontSize",!1,t.control.settings.value)}}})},pr=function(t){gr(t)},vr=function(n,t){var i=t.length;return R.each(t,function(t){t.menu&&(t.hidden=0===vr(n,t.menu));var e=t.format;e&&(t.hidden=!n.formatter.canApply(e)),t.hidden&&i--}),i},br=function(n,t){var i=t.items().length;return t.items().each(function(t){t.menu&&t.visible(0<br(n,t.menu)),!t.menu&&t.settings.menu&&t.visible(0<vr(n,t.settings.menu));var e=t.settings.format;e&&t.visible(n.formatter.canApply(e)),t.visible()||i--}),i},yr=function(t){var i,r,o,e,s,n,a,l,u=(r=0,o=[],e=[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],s=function(t){var i=[];if(t)return R.each(t,function(t){var e={text:t.title,icon:t.icon};if(t.items)e.menu=s(t.items);else{var n=t.format||"custom"+r++;t.format||(t.name=n,o.push(t)),e.format=n,e.cmd=t.cmd}i.push(e)}),i},(i=t).on("init",function(){R.each(o,function(t){i.formatter.register(t.name,t)})}),{type:"menu",items:i.settings.style_formats_merge?i.settings.style_formats?s(e.concat(i.settings.style_formats)):s(e):s(i.settings.style_formats||e),onPostRender:function(t){i.fire("renderFormatsMenu",{control:t.control})},itemDefaults:{preview:!0,textStyle:function(){if(this.settings.format)return i.formatter.getCssText(this.settings.format)},onPostRender:function(){var n=this;n.parent().on("show",function(){var t,e;(t=n.settings.format)&&(n.disabled(!i.formatter.canApply(t)),n.active(i.formatter.match(t))),(e=n.settings.cmd)&&n.active(i.queryCommandState(e))})},onclick:function(){this.settings.format&&or(i,this.settings.format)(),this.settings.cmd&&i.execCommand(this.settings.cmd)}}});n=u,t.addMenuItem("formats",{text:"Formats",menu:n}),l=u,(a=t).addButton("styleselect",{type:"menubutton",text:"Formats",menu:l,onShowMenu:function(){a.settings.style_formats_autohide&&br(a,this.menu)}})},xr=function(n,t){return function(){var r,o,s,e=[];return R.each(t,function(t){e.push({text:t[0],value:t[1],textStyle:function(){return n.formatter.getCssText(t[1])}})}),{type:"listbox",text:t[0][0],values:e,fixedWidth:!0,onselect:function(t){if(t.control){var e=t.control.value();or(n,e)()}},onPostRender:(r=n,o=e,function(){var e=this;r.on("nodeChange",function(t){var n=r.formatter,i=null;R.each(t.parents,function(e){if(R.each(o,function(t){if(s?n.matchNode(e,s,{value:t.value})&&(i=t.value):n.matchNode(e,t.value)&&(i=t.value),i)return!1}),i)return!1}),e.value(i)})})}}},wr=function(t){var e,n,i=function(t){for(var e=(t=t.replace(/;$/,"").split(";")).length;e--;)t[e]=t[e].split("=");return t}(t.settings.block_formats||"Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre");t.addMenuItem("blockformats",{text:"Blocks",menu:(e=t,n=i,R.map(n,function(t){return{text:t[0],onclick:or(e,t[1]),textStyle:function(){return e.formatter.getCssText(t[1])}}}))}),t.addButton("formatselect",xr(t,i))},_r=function(e,t){var n,i;if("string"==typeof t)i=t.split(" ");else if(R.isArray(t))return function(t){for(var e=[],n=0,i=t.length;n<i;++n){if(!pt(t[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+t);xt.apply(e,t[n])}return e}(R.map(t,function(t){return _r(e,t)}));return n=R.grep(i,function(t){return"|"===t||t in e.menuItems}),R.map(n,function(t){return"|"===t?{text:"-"}:e.menuItems[t]})},Rr=function(t){return t&&"-"===t.text},Cr=function(n){var i=kt(n,function(t,e){return!Rr(t)||!Rr(n[e-1])});return kt(i,function(t,e){return!Rr(t)||0<e&&e<i.length-1})},kr=function(t){var e,n,i,r,o=t.settings.insert_button_items;return Cr(o?_r(t,o):(e=t,n="insert",i=[{text:"-"}],r=R.grep(e.menuItems,function(t){return t.context===n}),R.each(r,function(t){"before"===t.separator&&i.push({text:"|"}),t.prependToContext?i.unshift(t):i.push(t),"after"===t.separator&&i.push({text:"|"})}),i))},Er=function(t){var e;(e=t).addButton("insert",{type:"menubutton",icon:"insert",menu:[],oncreatemenu:function(){this.menu.add(kr(e)),this.menu.renderNew()}})},Hr=function(t){var n,i,r;n=t,R.each({bold:"Bold",italic:"Italic",underline:"Underline",strikethrough:"Strikethrough",subscript:"Subscript",superscript:"Superscript"},function(t,e){n.addButton(e,{active:!1,tooltip:t,onPostRender:ar(n,e),onclick:or(n,e)})}),i=t,R.each({outdent:["Decrease indent","Outdent"],indent:["Increase indent","Indent"],cut:["Cut","Cut"],copy:["Copy","Copy"],paste:["Paste","Paste"],help:["Help","mceHelp"],selectall:["Select all","SelectAll"],visualaid:["Visual aids","mceToggleVisualAid"],newdocument:["New document","mceNewDocument"],removeformat:["Clear formatting","RemoveFormat"],remove:["Remove","Delete"]},function(t,e){i.addButton(e,{tooltip:t[0],cmd:t[1]})}),r=t,R.each({blockquote:["Blockquote","mceBlockQuote"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"]},function(t,e){r.addButton(e,{active:!1,tooltip:t[0],cmd:t[1],onPostRender:ar(r,e)})})},Tr=function(t){var n;Hr(t),n=t,R.each({bold:["Bold","Bold","Meta+B"],italic:["Italic","Italic","Meta+I"],underline:["Underline","Underline","Meta+U"],strikethrough:["Strikethrough","Strikethrough"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"],removeformat:["Clear formatting","RemoveFormat"],newdocument:["New document","mceNewDocument"],cut:["Cut","Cut","Meta+X"],copy:["Copy","Copy","Meta+C"],paste:["Paste","Paste","Meta+V"],selectall:["Select all","SelectAll","Meta+A"]},function(t,e){n.addMenuItem(e,{text:t[0],icon:e,shortcut:t[2],cmd:t[1]})}),n.addMenuItem("codeformat",{text:"Code",icon:"code",onclick:or(n,"code")})},Sr=function(n,i){return function(){var t=this,e=function(){var t="redo"===i?"hasRedo":"hasUndo";return!!n.undoManager&&n.undoManager[t]()};t.disabled(!e()),n.on("Undo Redo AddUndo TypingUndo ClearUndos SwitchMode",function(){t.disabled(n.readonly||!e())})}},Mr=function(t){var e,n;(e=t).addMenuItem("undo",{text:"Undo",icon:"undo",shortcut:"Meta+Z",onPostRender:Sr(e,"undo"),cmd:"undo"}),e.addMenuItem("redo",{text:"Redo",icon:"redo",shortcut:"Meta+Y",onPostRender:Sr(e,"redo"),cmd:"redo"}),(n=t).addButton("undo",{tooltip:"Undo",onPostRender:Sr(n,"undo"),cmd:"undo"}),n.addButton("redo",{tooltip:"Redo",onPostRender:Sr(n,"redo"),cmd:"redo"})},Nr=function(t){var e,n;(e=t).addMenuItem("visualaid",{text:"Visual aids",selectable:!0,onPostRender:(n=e,function(){var e=this;n.on("VisualAid",function(t){e.active(t.hasVisual)}),e.active(n.hasVisual)}),cmd:"mceToggleVisualAid"})},Or={setup:function(t){var e;t.rtl&&(ae.rtl=!0),t.on("mousedown progressstate",function(){ze.hideAll()}),(e=t).settings.ui_container&&(h.container=rr(Zn.fromDom(_.document.body),e.settings.ui_container).fold(lt(null),function(t){return t.dom()})),ge.tooltips=!h.iOS,ae.translate=function(t){return M.translate(t)},wr(t),lr(t),Tr(t),Mr(t),pr(t),fr(t),yr(t),Nr(t),Er(t)}},Wr=Nn.extend({recalc:function(t){var e,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,b,y,x,w,_,R,C,k,E,H,T,S=[],M=[];e=t.settings,r=t.items().filter(":visible"),o=t.layoutRect(),i=e.columns||Math.ceil(Math.sqrt(r.length)),n=Math.ceil(r.length/i),b=e.spacingH||e.spacing||0,y=e.spacingV||e.spacing||0,x=e.alignH||e.align,w=e.alignV||e.align,p=t.paddingBox,T="reverseRows"in e?e.reverseRows:t.isRtl(),x&&"string"==typeof x&&(x=[x]),w&&"string"==typeof w&&(w=[w]);for(d=0;d<i;d++)S.push(0);for(f=0;f<n;f++)M.push(0);for(f=0;f<n;f++)for(d=0;d<i&&(c=r[f*i+d]);d++)C=(u=c.layoutRect()).minW,k=u.minH,S[d]=C>S[d]?C:S[d],M[f]=k>M[f]?k:M[f];for(E=o.innerW-p.left-p.right,d=_=0;d<i;d++)_+=S[d]+(0<d?b:0),E-=(0<d?b:0)+S[d];for(H=o.innerH-p.top-p.bottom,f=R=0;f<n;f++)R+=M[f]+(0<f?y:0),H-=(0<f?y:0)+M[f];if(_+=p.left+p.right,R+=p.top+p.bottom,(l={}).minW=_+(o.w-o.innerW),l.minH=R+(o.h-o.innerH),l.contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH,l.minW=Math.min(l.minW,o.maxW),l.minH=Math.min(l.minH,o.maxH),l.minW=Math.max(l.minW,o.startMinWidth),l.minH=Math.max(l.minH,o.startMinHeight),!o.autoResize||l.minW===o.minW&&l.minH===o.minH){var N;o.autoResize&&((l=t.layoutRect(l)).contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH),N="start"===e.packV?0:0<H?Math.floor(H/n):0;var O=0,W=e.flexWidths;if(W)for(d=0;d<W.length;d++)O+=W[d];else O=i;var P=E/O;for(d=0;d<i;d++)S[d]+=W?W[d]*P:P;for(m=p.top,f=0;f<n;f++){for(h=p.left,a=M[f]+N,d=0;d<i&&(c=r[T?f*i+i-1-d:f*i+d]);d++)g=c.settings,u=c.layoutRect(),s=Math.max(S[d],u.startMinWidth),u.x=h,u.y=m,"center"===(v=g.alignH||(x?x[d]||x[0]:null))?u.x=h+s/2-u.w/2:"right"===v?u.x=h+s-u.w:"stretch"===v&&(u.w=s),"center"===(v=g.alignV||(w?w[d]||w[0]:null))?u.y=m+a/2-u.h/2:"bottom"===v?u.y=m+a-u.h:"stretch"===v&&(u.h=a),c.layoutRect(u),h+=s+b,c.recalc&&c.recalc();m+=a+y}}else if(l.w=l.minW,l.h=l.minH,t.layoutRect(l),this.recalc(t),null===t._lastRect){var D=t.parent();D&&(D._lastRect=null,D.recalc())}}}),Pr=ge.extend({renderHtml:function(){var t=this;return t.classes.add("iframe"),t.canFocus=!1,'<iframe id="'+t._id+'" class="'+t.classes+'" tabindex="-1" src="'+(t.settings.url||"javascript:''")+'" frameborder="0"></iframe>'},src:function(t){this.getEl().src=t},html:function(t,e){var n=this,i=this.getEl().contentWindow.document.body;return i?(i.innerHTML=t,e&&e()):c.setTimeout(function(){n.html(t)}),this}}),Dr=ge.extend({init:function(t){this._super(t),this.classes.add("widget").add("infobox"),this.canFocus=!1},severity:function(t){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(t)},help:function(t){this.state.set("help",t)},renderHtml:function(){var t=this,e=t.classPrefix;return'<div id="'+t._id+'" class="'+t.classes+'"><div id="'+t._id+'-body">'+t.encode(t.state.get("text"))+'<button role="button" tabindex="-1"><i class="'+e+"ico "+e+'i-help"></i></button></div></div>'},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl("body").firstChild.data=e.encode(t.value),e.state.get("rendered")&&e.updateLayoutRect()}),e.state.on("change:help",function(t){e.classes.toggle("has-help",t.value),e.state.get("rendered")&&e.updateLayoutRect()}),e._super()}}),Ar=ge.extend({init:function(t){var e=this;e._super(t),e.classes.add("widget").add("label"),e.canFocus=!1,t.multiline&&e.classes.add("autoscroll"),t.strong&&e.classes.add("strong")},initLayoutRect:function(){var t=this,e=t._super();return t.settings.multiline&&(Ht.getSize(t.getEl()).width>e.maxW&&(e.minW=e.maxW,t.classes.add("multiline")),t.getEl().style.width=e.minW+"px",e.startMinH=e.h=e.minH=Math.min(e.maxH,Ht.getSize(t.getEl()).height)),e},repaint:function(){return this.settings.multiline||(this.getEl().style.lineHeight=this.layoutRect().h+"px"),this._super()},severity:function(t){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(t)},renderHtml:function(){var t,e,n=this,i=n.settings.forId,r=n.settings.html?n.settings.html:n.encode(n.state.get("text"));return!i&&(e=n.settings.forName)&&(t=n.getRoot().find("#"+e)[0])&&(i=t._id),i?'<label id="'+n._id+'" class="'+n.classes+'"'+(i?' for="'+i+'"':"")+">"+r+"</label>":'<span id="'+n._id+'" class="'+n.classes+'">'+r+"</span>"},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.innerHtml(e.encode(t.value)),e.state.get("rendered")&&e.updateLayoutRect()}),e._super()}}),Br=Me.extend({Defaults:{role:"toolbar",layout:"flow"},init:function(t){this._super(t),this.classes.add("toolbar")},postRender:function(){return this.items().each(function(t){t.classes.add("toolbar-item")}),this._super()}}),Lr=Br.extend({Defaults:{role:"menubar",containerCls:"menubar",ariaRoot:!0,defaults:{type:"menubutton"}}}),Ir=On.extend({init:function(t){var e=this;e._renderOpen=!0,e._super(t),t=e.settings,e.classes.add("menubtn"),t.fixedWidth&&e.classes.add("fixed-width"),e.aria("haspopup",!0),e.state.set("menu",t.menu||e.render())},showMenu:function(t){var e,n=this;if(n.menu&&n.menu.visible()&&!1!==t)return n.hideMenu();n.menu||(e=n.state.get("menu")||[],n.classes.add("opened"),e.length?e={type:"menu",animate:!0,items:e}:(e.type=e.type||"menu",e.animate=!0),e.renderTo?n.menu=e.parent(n).show().renderTo():n.menu=_e.create(e).parent(n).renderTo(),n.fire("createmenu"),n.menu.reflow(),n.menu.on("cancel",function(t){t.control.parent()===n.menu&&(t.stopPropagation(),n.focus(),n.hideMenu())}),n.menu.on("select",function(){n.focus()}),n.menu.on("show hide",function(t){"hide"===t.type&&t.control.parent()===n&&n.classes.remove("opened-under"),t.control===n.menu&&(n.activeMenu("show"===t.type),n.classes.toggle("opened","show"===t.type)),n.aria("expanded","show"===t.type)}).fire("show")),n.menu.show(),n.menu.layoutRect({w:n.layoutRect().w}),n.menu.repaint(),n.menu.moveRel(n.getEl(),n.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"]);var i=n.menu.layoutRect(),r=n.$el.offset().top+n.layoutRect().h;r>i.y&&r<i.y+i.h&&n.classes.add("opened-under"),n.fire("showmenu")},hideMenu:function(){this.menu&&(this.menu.items().each(function(t){t.hideMenu&&t.hideMenu()}),this.menu.hide())},activeMenu:function(t){this.classes.toggle("active",t)},renderHtml:function(){var t,e=this,n=e._id,i=e.classPrefix,r=e.settings.icon,o=e.state.get("text"),s="";return(t=e.settings.image)?(r="none","string"!=typeof t&&(t=_.window.getSelection?t[0]:t[1]),t=" style=\"background-image: url('"+t+"')\""):t="",o&&(e.classes.add("btn-has-text"),s='<span class="'+i+'txt">'+e.encode(o)+"</span>"),r=e.settings.icon?i+"ico "+i+"i-"+r:"",e.aria("role",e.parent()instanceof Lr?"menuitem":"button"),'<div id="'+n+'" class="'+e.classes+'" tabindex="-1" aria-labelledby="'+n+'"><button id="'+n+'-open" role="presentation" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+t+"></i>":"")+s+' <i class="'+i+'caret"></i></button></div>'},postRender:function(){var r=this;return r.on("click",function(t){t.control===r&&function(t,e){for(;t;){if(e===t)return!0;t=t.parentNode}return!1}(t.target,r.getEl())&&(r.focus(),r.showMenu(!t.aria),t.aria&&r.menu.items().filter(":visible")[0].focus())}),r.on("mouseenter",function(t){var e,n=t.control,i=r.parent();n&&i&&n instanceof Ir&&n.parent()===i&&(i.items().filter("MenuButton").each(function(t){t.hideMenu&&t!==n&&(t.menu&&t.menu.visible()&&(e=!0),t.hideMenu())}),e&&(n.focus(),n.showMenu()))}),r._super()},bindStates:function(){var t=this;return t.state.on("change:menu",function(){t.menu&&t.menu.remove(),t.menu=null}),t._super()},remove:function(){this._super(),this.menu&&this.menu.remove()}});function zr(i,r){var o,s,a=this,l=ae.classPrefix;a.show=function(t,e){function n(){o&&(Tt(i).append('<div class="'+l+"throbber"+(r?" "+l+"throbber-inline":"")+'"></div>'),e&&e())}return a.hide(),o=!0,t?s=c.setTimeout(n,t):n(),a},a.hide=function(){var t=i.lastChild;return c.clearTimeout(s),t&&-1!==t.className.indexOf("throbber")&&t.parentNode.removeChild(t),o=!1,a}}var Fr=ze.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(t){if(t.autohide=!0,t.constrainToViewport=!0,"function"==typeof t.items&&(t.itemsFactory=t.items,t.items=[]),t.itemDefaults)for(var e=t.items,n=e.length;n--;)e[n]=R.extend({},t.itemDefaults,e[n]);this._super(t),this.classes.add("menu"),t.animate&&11!==h.ie&&this.classes.add("animate")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){this.hideAll(),this.fire("select")},load:function(){var e,n=this;function i(){n.throbber&&(n.throbber.hide(),n.throbber=null)}n.settings.itemsFactory&&(n.throbber||(n.throbber=new zr(n.getEl("body"),!0),0===n.items().length?(n.throbber.show(),n.fire("loading")):n.throbber.show(100,function(){n.items().remove(),n.fire("loading")}),n.on("hide close",i)),n.requestTime=e=(new Date).getTime(),n.settings.itemsFactory(function(t){0!==t.length?n.requestTime===e&&(n.getEl().style.width="",n.getEl("body").style.width="",i(),n.items().remove(),n.getEl("body").innerHTML="",n.add(t),n.renderNew(),n.fire("loaded")):n.hide()}))},hideAll:function(){return this.find("menuitem").exec("hideMenu"),this._super()},preRender:function(){var n=this;return n.items().each(function(t){var e=t.settings;if(e.icon||e.image||e.selectable)return!(n._hasIcons=!0)}),n.settings.itemsFactory&&n.on("postrender",function(){n.settings.itemsFactory&&n.load()}),n.on("show hide",function(t){t.control===n&&("show"===t.type?c.setTimeout(function(){n.classes.add("in")},0):n.classes.remove("in"))}),n._super()}}),Ur=Ir.extend({init:function(i){var e,r,o,n,s=this;s._super(i),i=s.settings,s._values=e=i.values,e&&("undefined"!=typeof i.value&&function t(e){for(var n=0;n<e.length;n++){if(r=e[n].selected||i.value===e[n].value)return o=o||e[n].text,s.state.set("value",e[n].value),!0;if(e[n].menu&&t(e[n].menu))return!0}}(e),!r&&0<e.length&&(o=e[0].text,s.state.set("value",e[0].value)),s.state.set("menu",e)),s.state.set("text",i.text||o),s.classes.add("listbox"),s.on("select",function(t){var e=t.control;n&&(t.lastControl=n),i.multiple?e.active(!e.active()):s.value(t.control.value()),n=e})},value:function(n){return 0===arguments.length?this.state.get("value"):(void 0===n||(this.settings.values&&!function e(t){return _t(t,function(t){return t.menu?e(t.menu):t.value===n})}(this.settings.values)?null===n&&this.state.set("value",null):this.state.set("value",n)),this)},bindStates:function(){var i=this;return i.on("show",function(t){var e,n;e=t.control,n=i.value(),e instanceof Fr&&e.items().each(function(t){t.hasMenus()||t.active(t.value()===n)})}),i.state.on("change:value",function(e){var n=function t(e,n){var i;if(e)for(var r=0;r<e.length;r++){if(e[r].value===n)return e[r];if(e[r].menu&&(i=t(e[r].menu,n)))return i}}(i.state.get("menu"),e.value);n?i.text(n.text):i.text(i.settings.text)}),i._super()}}),Vr=ge.extend({Defaults:{border:0,role:"menuitem"},init:function(t){var e,n=this;n._super(t),t=n.settings,n.classes.add("menu-item"),t.menu&&n.classes.add("menu-item-expand"),t.preview&&n.classes.add("menu-item-preview"),"-"!==(e=n.state.get("text"))&&"|"!==e||(n.classes.add("menu-item-sep"),n.aria("role","separator"),n.state.set("text","-")),t.selectable&&(n.aria("role","menuitemcheckbox"),n.classes.add("menu-item-checkbox"),t.icon="selected"),t.preview||t.selectable||n.classes.add("menu-item-normal"),n.on("mousedown",function(t){t.preventDefault()}),t.menu&&!t.ariaHideMenu&&n.aria("haspopup",!0)},hasMenus:function(){return!!this.settings.menu},showMenu:function(){var e,n=this,t=n.settings,i=n.parent();if(i.items().each(function(t){t!==n&&t.hideMenu()}),t.menu){(e=n.menu)?e.show():((e=t.menu).length?e={type:"menu",items:e}:e.type=e.type||"menu",i.settings.itemDefaults&&(e.itemDefaults=i.settings.itemDefaults),(e=n.menu=_e.create(e).parent(n).renderTo()).reflow(),e.on("cancel",function(t){t.stopPropagation(),n.focus(),e.hide()}),e.on("show hide",function(t){t.control.items&&t.control.items().each(function(t){t.active(t.settings.selected)})}).fire("show"),e.on("hide",function(t){t.control===e&&n.classes.remove("selected")}),e.submenu=!0),e._parentMenu=i,e.classes.add("menu-sub");var r=e.testMoveRel(n.getEl(),n.isRtl()?["tl-tr","bl-br","tr-tl","br-bl"]:["tr-tl","br-bl","tl-tr","bl-br"]);e.moveRel(n.getEl(),r),r="menu-sub-"+(e.rel=r),e.classes.remove(e._lastRel).add(r),e._lastRel=r,n.classes.add("selected"),n.aria("expanded",!0)}},hideMenu:function(){var t=this;return t.menu&&(t.menu.items().each(function(t){t.hideMenu&&t.hideMenu()}),t.menu.hide(),t.aria("expanded",!1)),t},renderHtml:function(){var t,e=this,n=e._id,i=e.settings,r=e.classPrefix,o=e.state.get("text"),s=e.settings.icon,a="",l=i.shortcut,u=e.encode(i.url);function c(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function d(t){var e=i.match||"";return e?t.replace(new RegExp(c(e),"gi"),function(t){return"!mce~match["+t+"]mce~match!"}):t}function f(t){return t.replace(new RegExp(c("!mce~match["),"g"),"<b>").replace(new RegExp(c("]mce~match!"),"g"),"</b>")}return s&&e.parent().classes.add("menu-has-icons"),i.image&&(a=" style=\"background-image: url('"+i.image+"')\""),l&&(l=function(t){var e,n,i={};for(i=h.mac?{alt:"⌥",ctrl:"⌘",shift:"⇧",meta:"⌘"}:{meta:"Ctrl"},t=t.split("+"),e=0;e<t.length;e++)(n=i[t[e].toLowerCase()])&&(t[e]=n);return t.join("+")}(l)),s=r+"ico "+r+"i-"+(e.settings.icon||"none"),t="-"!==o?'<i class="'+s+'"'+a+"></i>\xa0":"",o=f(e.encode(d(o))),u=f(e.encode(d(u))),'<div id="'+n+'" class="'+e.classes+'" tabindex="-1">'+t+("-"!==o?'<span id="'+n+'-text" class="'+r+'text">'+o+"</span>":"")+(l?'<div id="'+n+'-shortcut" class="'+r+'menu-shortcut">'+l+"</div>":"")+(i.menu?'<div class="'+r+'caret"></div>':"")+(u?'<div class="'+r+'menu-item-link">'+u+"</div>":"")+"</div>"},postRender:function(){var e=this,n=e.settings,t=n.textStyle;if("function"==typeof t&&(t=t.call(this)),t){var i=e.getEl("text");i&&(i.setAttribute("style",t),e._textStyle=t)}return e.on("mouseenter click",function(t){t.control===e&&(n.menu||"click"!==t.type?(e.showMenu(),t.aria&&e.menu.focus(!0)):(e.fire("select"),c.requestAnimationFrame(function(){e.parent().hideAll()})))}),e._super(),e},hover:function(){return this.parent().items().each(function(t){t.classes.remove("selected")}),this.classes.toggle("selected",!0),this},active:function(t){return function(t,e){var n=t._textStyle;if(n){var i=t.getEl("text");i.setAttribute("style",n),e&&(i.style.color="",i.style.backgroundColor="")}}(this,t),void 0!==t&&this.aria("checked",t),this._super(t)},remove:function(){this._super(),this.menu&&this.menu.remove()}}),qr=Dn.extend({Defaults:{classes:"radio",role:"radio"}}),Yr=ge.extend({renderHtml:function(){var t=this,e=t.classPrefix;return t.classes.add("resizehandle"),"both"===t.settings.direction&&t.classes.add("resizehandle-both"),t.canFocus=!1,'<div id="'+t._id+'" class="'+t.classes+'"><i class="'+e+"ico "+e+'i-resize"></i></div>'},postRender:function(){var e=this;e._super(),e.resizeDragHelper=new we(this._id,{start:function(){e.fire("ResizeStart")},drag:function(t){"both"!==e.settings.direction&&(t.deltaX=0),e.fire("Resize",t)},stop:function(){e.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}});function $r(t){var e="";if(t)for(var n=0;n<t.length;n++)e+='<option value="'+t[n]+'">'+t[n]+"</option>";return e}var Xr=ge.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(t){var n=this;n._super(t),n.settings.size&&(n.size=n.settings.size),n.settings.options&&(n._options=n.settings.options),n.on("keydown",function(t){var e;13===t.keyCode&&(t.preventDefault(),n.parents().reverse().each(function(t){if(t.toJSON)return e=t,!1}),n.fire("submit",{data:e.toJSON()}))})},options:function(t){return arguments.length?(this.state.set("options",t),this):this.state.get("options")},renderHtml:function(){var t,e=this,n="";return t=$r(e._options),e.size&&(n=' size = "'+e.size+'"'),'<select id="'+e._id+'" class="'+e.classes+'"'+n+">"+t+"</select>"},bindStates:function(){var e=this;return e.state.on("change:options",function(t){e.getEl().innerHTML=$r(t.value)}),e._super()}});function jr(t,e,n){return t<e&&(t=e),n<t&&(t=n),t}function Jr(t,e,n){t.setAttribute("aria-"+e,n)}function Gr(t,e){var n,i,r,o,s;"v"===t.settings.orientation?(r="top",i="height",n="h"):(r="left",i="width",n="w"),s=t.getEl("handle"),o=((t.layoutRect()[n]||100)-Ht.getSize(s)[i])*((e-t._minValue)/(t._maxValue-t._minValue))+"px",s.style[r]=o,s.style.height=t.layoutRect().h+"px",Jr(s,"valuenow",e),Jr(s,"valuetext",""+t.settings.previewFilter(e)),Jr(s,"valuemin",t._minValue),Jr(s,"valuemax",t._maxValue)}var Kr=ge.extend({init:function(t){var e=this;t.previewFilter||(t.previewFilter=function(t){return Math.round(100*t)/100}),e._super(t),e.classes.add("slider"),"v"===t.orientation&&e.classes.add("vertical"),e._minValue=bt(t.minValue)?t.minValue:0,e._maxValue=bt(t.maxValue)?t.maxValue:100,e._initValue=e.state.get("value")},renderHtml:function(){var t=this._id,e=this.classPrefix;return'<div id="'+t+'" class="'+this.classes+'"><div id="'+t+'-handle" class="'+e+'slider-handle" role="slider" tabindex="-1"></div></div>'},reset:function(){this.value(this._initValue).repaint()},postRender:function(){var t,e,n,i,r,o,s,a,l,u,c,d,f,h,m=this;t=m._minValue,e=m._maxValue,"v"===m.settings.orientation?(n="screenY",i="top",r="height",o="h"):(n="screenX",i="left",r="width",o="w"),m._super(),function(o,s){function e(t){var e,n,i,r;e=jr(e=(((e=m.value())+(r=n=o))/((i=s)-r)+.05*t)*(i-n)-n,o,s),m.value(e),m.fire("dragstart",{value:e}),m.fire("drag",{value:e}),m.fire("dragend",{value:e})}m.on("keydown",function(t){switch(t.keyCode){case 37:case 38:e(-1);break;case 39:case 40:e(1)}})}(t,e),s=t,a=e,l=m.getEl("handle"),m._dragHelper=new we(m._id,{handle:m._id+"-handle",start:function(t){u=t[n],c=parseInt(m.getEl("handle").style[i],10),d=(m.layoutRect()[o]||100)-Ht.getSize(l)[r],m.fire("dragstart",{value:h})},drag:function(t){var e=t[n]-u;f=jr(c+e,0,d),l.style[i]=f+"px",h=s+f/d*(a-s),m.value(h),m.tooltip().text(""+m.settings.previewFilter(h)).show().moveRel(l,"bc tc"),m.fire("drag",{value:h})},stop:function(){m.tooltip().hide(),m.fire("dragend",{value:h})}})},repaint:function(){this._super(),Gr(this,this.value())},bindStates:function(){var e=this;return e.state.on("change:value",function(t){Gr(e,t.value)}),e._super()}}),Zr=ge.extend({renderHtml:function(){return this.classes.add("spacer"),this.canFocus=!1,'<div id="'+this._id+'" class="'+this.classes+'"></div>'}}),Qr=Ir.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var t,e,n=this.getEl(),i=this.layoutRect();return this._super(),t=n.firstChild,e=n.lastChild,Tt(t).css({width:i.w-Ht.getSize(e).width,height:i.h-2}),Tt(e).css({height:i.h-2}),this},activeMenu:function(t){Tt(this.getEl().lastChild).toggleClass(this.classPrefix+"active",t)},renderHtml:function(){var t,e,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a=n.settings,l="";return(t=a.image)?(o="none","string"!=typeof t&&(t=_.window.getSelection?t[0]:t[1]),t=" style=\"background-image: url('"+t+"')\""):t="",o=a.icon?r+"ico "+r+"i-"+o:"",s&&(n.classes.add("btn-has-text"),l='<span class="'+r+'txt">'+n.encode(s)+"</span>"),e="boolean"==typeof a.active?' aria-pressed="'+a.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" role="button"'+e+' tabindex="-1"><button type="button" hidefocus="1" tabindex="-1">'+(o?'<i class="'+o+'"'+t+"></i>":"")+l+'</button><button type="button" class="'+r+'open" hidefocus="1" tabindex="-1">'+(n._menuBtnText?(o?"\xa0":"")+n._menuBtnText:"")+' <i class="'+r+'caret"></i></button></div>'},postRender:function(){var n=this.settings.onclick;return this.on("click",function(t){var e=t.target;if(t.control===this)for(;e;){if(t.aria&&"down"!==t.aria.key||"BUTTON"===e.nodeName&&-1===e.className.indexOf("open"))return t.stopImmediatePropagation(),void(n&&n.call(this,t));e=e.parentNode}}),delete this.settings.onclick,this._super()}}),to=ir.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}}),eo=Oe.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(n){var t;this.activeTabId&&(t=this.getEl(this.activeTabId),Tt(t).removeClass(this.classPrefix+"active"),t.setAttribute("aria-selected","false")),this.activeTabId="t"+n,(t=this.getEl("t"+n)).setAttribute("aria-selected","true"),Tt(t).addClass(this.classPrefix+"active"),this.items()[n].show().fire("showtab"),this.reflow(),this.items().each(function(t,e){n!==e&&t.hide()})},renderHtml:function(){var i=this,t=i._layout,r="",o=i.classPrefix;return i.preRender(),t.preRender(i),i.items().each(function(t,e){var n=i._id+"-t"+e;t.aria("role","tabpanel"),t.aria("labelledby",n),r+='<div id="'+n+'" class="'+o+'tab" unselectable="on" role="tab" aria-controls="'+t._id+'" aria-selected="false" tabIndex="-1">'+i.encode(t.settings.title)+"</div>"}),'<div id="'+i._id+'" class="'+i.classes+'" hidefocus="1" tabindex="-1"><div id="'+i._id+'-head" class="'+o+'tabs" role="tablist">'+r+'</div><div id="'+i._id+'-body" class="'+i.bodyClasses+'">'+t.renderHtml(i)+"</div></div>"},postRender:function(){var i=this;i._super(),i.settings.activeTab=i.settings.activeTab||0,i.activateTab(i.settings.activeTab),this.on("click",function(t){var e=t.target.parentNode;if(e&&e.id===i._id+"-head")for(var n=e.childNodes.length;n--;)e.childNodes[n]===t.target&&i.activateTab(n)})},initLayoutRect:function(){var t,e,n,i=this;e=(e=Ht.getSize(i.getEl("head")).width)<0?0:e,n=0,i.items().each(function(t){e=Math.max(e,t.layoutRect().minW),n=Math.max(n,t.layoutRect().minH)}),i.items().each(function(t){t.settings.x=0,t.settings.y=0,t.settings.w=e,t.settings.h=n,t.layoutRect({x:0,y:0,w:e,h:n})});var r=Ht.getSize(i.getEl("head")).height;return i.settings.minWidth=e,i.settings.minHeight=n+r,(t=i._super()).deltaH+=r,t.innerH=t.h-t.deltaH,t}}),no=ge.extend({init:function(t){var n=this;n._super(t),n.classes.add("textbox"),t.multiline?n.classes.add("multiline"):(n.on("keydown",function(t){var e;13===t.keyCode&&(t.preventDefault(),n.parents().reverse().each(function(t){if(t.toJSON)return e=t,!1}),n.fire("submit",{data:e.toJSON()}))}),n.on("keyup",function(t){n.state.set("value",t.target.value)}))},repaint:function(){var t,e,n,i,r,o=this,s=0;t=o.getEl().style,e=o._layoutRect,r=o._lastRepaintRect||{};var a=_.document;return!o.settings.multiline&&a.all&&(!a.documentMode||a.documentMode<=8)&&(t.lineHeight=e.h-s+"px"),i=(n=o.borderBox).left+n.right+8,s=n.top+n.bottom+(o.settings.multiline?8:0),e.x!==r.x&&(t.left=e.x+"px",r.x=e.x),e.y!==r.y&&(t.top=e.y+"px",r.y=e.y),e.w!==r.w&&(t.width=e.w-i+"px",r.w=e.w),e.h!==r.h&&(t.height=e.h-s+"px",r.h=e.h),o._lastRepaintRect=r,o.fire("repaint",{},!1),o},renderHtml:function(){var e,t,n=this,i=n.settings;return e={id:n._id,hidefocus:"1"},R.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(t){e[t]=i[t]}),n.disabled()&&(e.disabled="disabled"),i.subtype&&(e.type=i.subtype),(t=Ht.create(i.multiline?"textarea":"input",e)).value=n.state.get("value"),t.className=n.classes.toString(),t.outerHTML},value:function(t){return arguments.length?(this.state.set("value",t),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var e=this;e.getEl().value=e.state.get("value"),e._super(),e.$el.on("change",function(t){e.state.set("value",t.target.value),e.fire("change",t)})},bindStates:function(){var e=this;return e.state.on("change:value",function(t){e.getEl().value!==t.value&&(e.getEl().value=t.value)}),e.state.on("change:disabled",function(t){e.getEl().disabled=t.value}),e._super()},remove:function(){this.$el.off(),this._super()}}),io=function(){return{Selector:Ft,Collection:qt,ReflowQueue:Zt,Control:ae,Factory:_e,KeyboardNavigation:Ce,Container:Me,DragHelper:we,Scrollable:Ne,Panel:Oe,Movable:he,Resizable:We,FloatPanel:ze,Window:$e,MessageBox:Ge,Tooltip:me,Widget:ge,Progress:pe,Notification:be,Layout:Mn,AbsoluteLayout:Nn,Button:On,ButtonGroup:Pn,Checkbox:Dn,ComboBox:Bn,ColorBox:Ln,PanelButton:In,ColorButton:Fn,ColorPicker:Vn,Path:Yn,ElementPath:$n,FormItem:Xn,Form:jn,FieldSet:Jn,FilePicker:tr,FitLayout:er,FlexLayout:nr,FlowLayout:ir,FormatControls:Or,GridLayout:Wr,Iframe:Pr,InfoBox:Dr,Label:Ar,Toolbar:Br,MenuBar:Lr,MenuButton:Ir,MenuItem:Vr,Throbber:zr,Menu:Fr,ListBox:Ur,Radio:qr,ResizeHandle:Yr,SelectBox:Xr,Slider:Kr,Spacer:Zr,SplitButton:Qr,StackLayout:to,TabPanel:eo,TextBox:no,DropZone:qn,BrowseButton:Wn}},ro=function(n){n.ui?R.each(io(),function(t,e){n.ui[e]=t}):n.ui=io()};R.each(io(),function(t,e){_e.add(e,t)}),ro(window.tinymce?window.tinymce:{}),r.add("inlite",function(t){var e=Sn();return Or.setup(t),_n(t,e),Ke(t,e)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/themes/mobile/theme.min.js b/lib/web/tiny_mce_4/themes/mobile/theme.min.js index 9c96eb5c90b65..1b566272f8fe1 100644 --- a/lib/web/tiny_mce_4/themes/mobile/theme.min.js +++ b/lib/web/tiny_mce_4/themes/mobile/theme.min.js @@ -1 +1 @@ -!function(v){"use strict";var I=function(){},p=function(t,r){return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return t(r.apply(null,n))}},M=function(n){return function(){return n}},h=function(n){return n};function l(r){for(var o=[],n=1;n<arguments.length;n++)o[n-1]=arguments[n];return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];var t=o.concat(n);return r.apply(null,t)}}var n,e,t,r,o,i,u,c,w=function(t){return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return!t.apply(null,n)}},a=function(n){return function(){throw new Error(n)}},s=function(n){return n()},f=M(!1),d=M(!0),m=function(e){return function(n){return function(n){if(null===n)return"null";var e=typeof n;return"object"===e&&(Array.prototype.isPrototypeOf(n)||n.constructor&&"Array"===n.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(n)||n.constructor&&"String"===n.constructor.name)?"string":e}(n)===e}},b=m("string"),g=m("object"),y=m("array"),x=m("boolean"),S=m("function"),O=m("number"),T=Object.prototype.hasOwnProperty,k=function(u){return function(){for(var n=new Array(arguments.length),e=0;e<n.length;e++)n[e]=arguments[e];if(0===n.length)throw new Error("Can't merge zero objects");for(var t={},r=0;r<n.length;r++){var o=n[r];for(var i in o)T.call(o,i)&&(t[i]=u(t[i],o[i]))}return t}},C=k(function(n,e){return g(n)&&g(e)?C(n,e):e}),E=k(function(n,e){return e}),D=f,A=d,B=function(){return R},R=(r={fold:function(n,e){return n()},is:D,isSome:D,isNone:A,getOr:t=function(n){return n},getOrThunk:e=function(n){return n()},getOrDie:function(n){throw new Error(n||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:t,orThunk:e,map:B,ap:B,each:function(){},bind:B,flatten:B,exists:D,forall:A,filter:B,equals:n=function(n){return n.isNone()},equals_:n,toArray:function(){return[]},toString:M("none()")},Object.freeze&&Object.freeze(r),r),F=function(t){var n=function(){return t},e=function(){return o},r=function(n){return n(t)},o={fold:function(n,e){return e(t)},is:function(n){return t===n},isSome:A,isNone:D,getOr:n,getOrThunk:n,getOrDie:n,getOrNull:n,getOrUndefined:n,or:e,orThunk:e,map:function(n){return F(n(t))},ap:function(n){return n.fold(B,function(n){return F(n(t))})},each:function(n){n(t)},bind:r,flatten:n,exists:r,forall:r,filter:function(n){return n(t)?o:R},equals:function(n){return n.is(t)},equals_:function(n,e){return n.fold(D,function(n){return e(t,n)})},toArray:function(){return[t]},toString:function(){return"some("+t+")"}};return o},V={some:F,none:B,from:function(n){return null===n||n===undefined?R:F(n)}},N=Object.keys,H=function(n,e){for(var t=N(n),r=0,o=t.length;r<o;r++){var i=t[r];e(n[i],i,n)}},z=function(n,r){return j(n,function(n,e,t){return{k:e,v:r(n,e,t)}})},j=function(r,o){var i={};return H(r,function(n,e){var t=o(n,e,r);i[t.k]=t.v}),i},L=function(n,t){var r=[];return H(n,function(n,e){r.push(t(n,e))}),r},P=M("touchstart"),$=M("touchmove"),W=M("touchend"),G=M("mousedown"),_=M("mousemove"),U=M("mouseup"),q=M("mouseover"),Y=M("keydown"),K=M("input"),X=M("change"),J=M("click"),Q=M("transitionend"),Z=M("selectstart"),nn=function(t){var r,o=!1;return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return o||(o=!0,r=t.apply(null,n)),r}},en=function(n,e){var t=function(n,e){for(var t=0;t<n.length;t++){var r=n[t];if(r.test(e))return r}return undefined}(n,e);if(!t)return{major:0,minor:0};var r=function(n){return Number(e.replace(t,"$"+n))};return rn(r(1),r(2))},tn=function(){return rn(0,0)},rn=function(n,e){return{major:n,minor:e}},on={nu:rn,detect:function(n,e){var t=String(e).toLowerCase();return 0===n.length?tn():en(n,t)},unknown:tn},un="Firefox",cn=function(n,e){return function(){return e===n}},an=function(n){var e=n.current;return{current:e,version:n.version,isEdge:cn("Edge",e),isChrome:cn("Chrome",e),isIE:cn("IE",e),isOpera:cn("Opera",e),isFirefox:cn(un,e),isSafari:cn("Safari",e)}},sn={unknown:function(){return an({current:undefined,version:on.unknown()})},nu:an,edge:M("Edge"),chrome:M("Chrome"),ie:M("IE"),opera:M("Opera"),firefox:M(un),safari:M("Safari")},fn="Windows",ln="Android",dn="Solaris",mn="FreeBSD",gn=function(n,e){return function(){return e===n}},vn=function(n){var e=n.current;return{current:e,version:n.version,isWindows:gn(fn,e),isiOS:gn("iOS",e),isAndroid:gn(ln,e),isOSX:gn("OSX",e),isLinux:gn("Linux",e),isSolaris:gn(dn,e),isFreeBSD:gn(mn,e)}},pn={unknown:function(){return vn({current:undefined,version:on.unknown()})},nu:vn,windows:M(fn),ios:M("iOS"),android:M(ln),linux:M("Linux"),osx:M("OSX"),solaris:M(dn),freebsd:M(mn)},hn=Array.prototype.slice,bn=(o=Array.prototype.indexOf)===undefined?function(n,e){return En(n,e)}:function(n,e){return o.call(n,e)},yn=function(n,e){return-1<bn(n,e)},xn=function(n,e){for(var t=n.length,r=new Array(t),o=0;o<t;o++){var i=n[o];r[o]=e(i,o,n)}return r},wn=function(n,e){for(var t=0,r=n.length;t<r;t++)e(n[t],t,n)},Sn=function(n,e){for(var t=[],r=0,o=n.length;r<o;r++){var i=n[r];e(i,r,n)&&t.push(i)}return t},On=function(n,e,t){return function(n,e){for(var t=n.length-1;0<=t;t--)e(n[t],t,n)}(n,function(n){t=e(t,n)}),t},Tn=function(n,e,t){return wn(n,function(n){t=e(t,n)}),t},kn=function(n,e){for(var t=0,r=n.length;t<r;t++){var o=n[t];if(e(o,t,n))return V.some(o)}return V.none()},Cn=function(n,e){for(var t=0,r=n.length;t<r;t++)if(e(n[t],t,n))return V.some(t);return V.none()},En=function(n,e){for(var t=0,r=n.length;t<r;++t)if(n[t]===e)return t;return-1},Dn=Array.prototype.push,In=function(n){for(var e=[],t=0,r=n.length;t<r;++t){if(!Array.prototype.isPrototypeOf(n[t]))throw new Error("Arr.flatten item "+t+" was not an array, input: "+n);Dn.apply(e,n[t])}return e},Mn=function(n,e){var t=xn(n,e);return In(t)},An=function(n,e){for(var t=0,r=n.length;t<r;++t)if(!0!==e(n[t],t,n))return!1;return!0},Bn=function(n){var e=hn.call(n,0);return e.reverse(),e},Rn=function(n){return[n]},Fn=(S(Array.from)&&Array.from,function(n,e){var t=String(e).toLowerCase();return kn(n,function(n){return n.search(t)})}),Vn=function(n,t){return Fn(n,t).map(function(n){var e=on.detect(n.versionRegexes,t);return{current:n.name,version:e}})},Nn=function(n,t){return Fn(n,t).map(function(n){var e=on.detect(n.versionRegexes,t);return{current:n.name,version:e}})},Hn=function(n,e){return-1!==n.indexOf(e)},zn=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,jn=function(e){return function(n){return Hn(n,e)}},Ln=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(n){return Hn(n,"edge/")&&Hn(n,"chrome")&&Hn(n,"safari")&&Hn(n,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,zn],search:function(n){return Hn(n,"chrome")&&!Hn(n,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(n){return Hn(n,"msie")||Hn(n,"trident")}},{name:"Opera",versionRegexes:[zn,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:jn("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:jn("firefox")},{name:"Safari",versionRegexes:[zn,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(n){return(Hn(n,"safari")||Hn(n,"mobile/"))&&Hn(n,"applewebkit")}}],Pn=[{name:"Windows",search:jn("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(n){return Hn(n,"iphone")||Hn(n,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:jn("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:jn("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:jn("linux"),versionRegexes:[]},{name:"Solaris",search:jn("sunos"),versionRegexes:[]},{name:"FreeBSD",search:jn("freebsd"),versionRegexes:[]}],$n={browsers:M(Ln),oses:M(Pn)},Wn=function(n){var e,t,r,o,i,u,c,a,s,f,l,d=$n.browsers(),m=$n.oses(),g=Vn(d,n).fold(sn.unknown,sn.nu),v=Nn(m,n).fold(pn.unknown,pn.nu);return{browser:g,os:v,deviceType:(t=g,r=n,o=(e=v).isiOS()&&!0===/ipad/i.test(r),i=e.isiOS()&&!o,u=e.isAndroid()&&3===e.version.major,c=e.isAndroid()&&4===e.version.major,a=o||u||c&&!0===/mobile/i.test(r),s=e.isiOS()||e.isAndroid(),f=s&&!a,l=t.isSafari()&&e.isiOS()&&!1===/safari/i.test(r),{isiPad:M(o),isiPhone:M(i),isTablet:M(a),isPhone:M(f),isTouch:M(s),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:M(l)})}},Gn={detect:nn(function(){var n=v.navigator.userAgent;return Wn(n)})},_n={tap:M("alloy.tap")},Un=M("alloy.focus"),qn=M("alloy.blur.post"),Yn=M("alloy.receive"),Kn=M("alloy.execute"),Xn=M("alloy.focus.item"),Jn=_n.tap,Qn=Gn.detect().deviceType.isTouch()?_n.tap:J,Zn=M("alloy.longpress"),ne=M("alloy.system.init"),ee=M("alloy.system.scroll"),te=M("alloy.system.attached"),re=M("alloy.system.detached"),oe=function(n,e){ae(n,n.element(),e,{})},ie=function(n,e,t){ae(n,n.element(),e,t)},ue=function(n){oe(n,Kn())},ce=function(n,e,t){ae(n,e,t,{})},ae=function(n,e,t,r){var o=C({target:e},r);n.getSystem().triggerEvent(t,e,z(o,M))},se=function(n){if(null===n||n===undefined)throw new Error("Node cannot be null or undefined");return{dom:M(n)}},fe={fromHtml:function(n,e){var t=(e||v.document).createElement("div");if(t.innerHTML=n,!t.hasChildNodes()||1<t.childNodes.length)throw v.console.error("HTML does not have a single root node",n),new Error("HTML must have a single root node");return se(t.childNodes[0])},fromTag:function(n,e){var t=(e||v.document).createElement(n);return se(t)},fromText:function(n,e){var t=(e||v.document).createTextNode(n);return se(t)},fromDom:se,fromPoint:function(n,e,t){var r=n.dom();return V.from(r.elementFromPoint(e,t)).map(se)}},le=(v.Node.ATTRIBUTE_NODE,v.Node.CDATA_SECTION_NODE,v.Node.COMMENT_NODE,v.Node.DOCUMENT_NODE),de=(v.Node.DOCUMENT_TYPE_NODE,v.Node.DOCUMENT_FRAGMENT_NODE,v.Node.ELEMENT_NODE),me=v.Node.TEXT_NODE,ge=(v.Node.PROCESSING_INSTRUCTION_NODE,v.Node.ENTITY_REFERENCE_NODE,v.Node.ENTITY_NODE,v.Node.NOTATION_NODE,function(n){return n.dom().nodeName.toLowerCase()}),ve=function(e){return function(n){return n.dom().nodeType===e}},pe=ve(de),he=ve(me),be=function(n){var e=he(n)?n.dom().parentNode:n.dom();return e!==undefined&&null!==e&&e.ownerDocument.body.contains(e)},ye=nn(function(){return xe(fe.fromDom(v.document))}),xe=function(n){var e=n.dom().body;if(null===e||e===undefined)throw new Error("Body is not available yet");return fe.fromDom(e)},we=function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return function(){for(var t=[],n=0;n<arguments.length;n++)t[n]=arguments[n];if(e.length!==t.length)throw new Error('Wrong number of arguments to struct. Expected "['+e.length+']", got '+t.length+" arguments");var r={};return wn(e,function(n,e){r[n]=M(t[e])}),r}},Se=function(n){return n.slice(0).sort()},Oe=function(n,e){throw new Error("All required keys ("+Se(n).join(", ")+") were not specified. Specified keys were: "+Se(e).join(", ")+".")},Te=function(n){throw new Error("Unsupported keys for object: "+Se(n).join(", "))},ke=function(e,n){if(!y(n))throw new Error("The "+e+" fields must be an array. Was: "+n+".");wn(n,function(n){if(!b(n))throw new Error("The value "+n+" in the "+e+" fields was not a string.")})},Ce=function(n){var t=Se(n);kn(t,function(n,e){return e<t.length-1&&n===t[e+1]}).each(function(n){throw new Error("The field: "+n+" occurs more than once in the combined fields: ["+t.join(", ")+"].")})},Ee=function(o,i){var u=o.concat(i);if(0===u.length)throw new Error("You must specify at least one required or optional field.");return ke("required",o),ke("optional",i),Ce(u),function(e){var t=N(e);An(o,function(n){return yn(t,n)})||Oe(o,t);var n=Sn(t,function(n){return!yn(u,n)});0<n.length&&Te(n);var r={};return wn(o,function(n){r[n]=M(e[n])}),wn(i,function(n){r[n]=M(Object.prototype.hasOwnProperty.call(e,n)?V.some(e[n]):V.none())}),r}},De="undefined"!=typeof v.window?v.window:Function("return this;")(),Ie=function(n,e){return function(n,e){for(var t=e!==undefined&&null!==e?e:De,r=0;r<n.length&&t!==undefined&&null!==t;++r)t=t[n[r]];return t}(n.split("."),e)},Me={getOrDie:function(n,e){var t=Ie(n,e);if(t===undefined||null===t)throw n+" not available on this browser";return t}},Ae=de,Be=le,Re=function(n,e){var t=n.dom();if(t.nodeType!==Ae)return!1;if(t.matches!==undefined)return t.matches(e);if(t.msMatchesSelector!==undefined)return t.msMatchesSelector(e);if(t.webkitMatchesSelector!==undefined)return t.webkitMatchesSelector(e);if(t.mozMatchesSelector!==undefined)return t.mozMatchesSelector(e);throw new Error("Browser lacks native selectors")},Fe=function(n){return n.nodeType!==Ae&&n.nodeType!==Be||0===n.childElementCount},Ve=function(n,e){var t=e===undefined?v.document:e.dom();return Fe(t)?[]:xn(t.querySelectorAll(n),fe.fromDom)},Ne=function(n,e){var t=e===undefined?v.document:e.dom();return Fe(t)?V.none():V.from(t.querySelector(n)).map(fe.fromDom)},He=function(n,e){return n.dom()===e.dom()},ze=(Gn.detect().browser.isIE(),function(n){return fe.fromDom(n.dom().ownerDocument)}),je=function(n){var e=n.dom().ownerDocument.defaultView;return fe.fromDom(e)},Le=function(n){var e=n.dom();return V.from(e.parentNode).map(fe.fromDom)},Pe=function(n){var e=n.dom();return xn(e.childNodes,fe.fromDom)},$e=function(n){return e=0,t=n.dom().childNodes,V.from(t[e]).map(fe.fromDom);var e,t},We=(we("element","offset"),function(e,t){$e(e).fold(function(){Ge(e,t)},function(n){e.dom().insertBefore(t.dom(),n.dom())})}),Ge=function(n,e){n.dom().appendChild(e.dom())},_e=function(e,n){wn(n,function(n){Ge(e,n)})},Ue=function(n){n.dom().textContent="",wn(Pe(n),function(n){qe(n)})},qe=function(n){var e=n.dom();null!==e.parentNode&&e.parentNode.removeChild(e)},Ye=function(n){oe(n,re());var e=n.components();wn(e,Ye)},Ke=function(n){var e=n.components();wn(e,Ke),oe(n,te())},Xe=function(n,e){Je(n,e,Ge)},Je=function(n,e,t){n.getSystem().addToWorld(e),t(n.element(),e.element()),be(n.element())&&Ke(e),n.syncComponents()},Qe=function(n){Ye(n),qe(n.element()),n.getSystem().removeFromWorld(n)},Ze=function(e){var n=Le(e.element()).bind(function(n){return e.getSystem().getByDom(n).fold(V.none,V.some)});Qe(e),n.each(function(n){n.syncComponents()})},nt=function(t){return{is:function(n){return t===n},isValue:d,isError:f,getOr:M(t),getOrThunk:M(t),getOrDie:M(t),or:function(n){return nt(t)},orThunk:function(n){return nt(t)},fold:function(n,e){return e(t)},map:function(n){return nt(n(t))},mapError:function(n){return nt(t)},each:function(n){n(t)},bind:function(n){return n(t)},exists:function(n){return n(t)},forall:function(n){return n(t)},toOption:function(){return V.some(t)}}},et=function(t){return{is:f,isValue:f,isError:d,getOr:h,getOrThunk:function(n){return n()},getOrDie:function(){return a(String(t))()},or:function(n){return n},orThunk:function(n){return n()},fold:function(n,e){return n(t)},map:function(n){return et(t)},mapError:function(n){return et(n(t))},each:I,bind:function(n){return et(t)},exists:f,forall:d,toOption:V.none}},tt={value:nt,error:et,fromOption:function(n,e){return n.fold(function(){return et(e)},nt)}},rt=function(u){if(!y(u))throw new Error("cases must be an array");if(0===u.length)throw new Error("there must be at least one case");var c=[],t={};return wn(u,function(n,r){var e=N(n);if(1!==e.length)throw new Error("one and only one name per case");var o=e[0],i=n[o];if(t[o]!==undefined)throw new Error("duplicate key detected:"+o);if("cata"===o)throw new Error("cannot have a case named cata (sorry)");if(!y(i))throw new Error("case arguments must be an array");c.push(o),t[o]=function(){var n=arguments.length;if(n!==i.length)throw new Error("Wrong number of arguments to case "+o+". Expected "+i.length+" ("+i+"), got "+n);for(var t=new Array(n),e=0;e<t.length;e++)t[e]=arguments[e];return{fold:function(){if(arguments.length!==u.length)throw new Error("Wrong number of arguments to fold. Expected "+u.length+", got "+arguments.length);return arguments[r].apply(null,t)},match:function(n){var e=N(n);if(c.length!==e.length)throw new Error("Wrong number of arguments to match. Expected: "+c.join(",")+"\nActual: "+e.join(","));if(!An(c,function(n){return yn(e,n)}))throw new Error("Not all branches were specified when using match. Specified: "+e.join(", ")+"\nRequired: "+c.join(", "));return n[o].apply(null,t)},log:function(n){v.console.log(n,{constructors:c,constructor:o,params:t})}}}}),t},ot=rt([{strict:[]},{defaultedThunk:["fallbackThunk"]},{asOption:[]},{asDefaultedOptionThunk:["fallbackThunk"]},{mergeWithThunk:["baseThunk"]}]),it=function(n){return ot.defaultedThunk(M(n))},ut=ot.strict,ct=ot.asOption,at=ot.defaultedThunk,st=ot.mergeWithThunk,ft=(rt([{bothErrors:["error1","error2"]},{firstError:["error1","value2"]},{secondError:["value1","error2"]},{bothValues:["value1","value2"]}]),function(n){var e=[],t=[];return wn(n,function(n){n.fold(function(n){e.push(n)},function(n){t.push(n)})}),{errors:e,values:t}}),lt=function(n){return p(tt.error,In)(n)},dt=function(n,e){var t,r,o=ft(n);return 0<o.errors.length?lt(o.errors):(t=o.values,r=e,tt.value(C.apply(undefined,[r].concat(t))))},mt=function(n){var e=ft(n);return 0<e.errors.length?lt(e.errors):tt.value(e.values)},gt=function(e){return function(n){return n.hasOwnProperty(e)?V.from(n[e]):V.none()}},vt=function(n,e){return gt(e)(n)},pt=function(n,e){var t={};return t[n]=e,t},ht=function(n,e){return t=n,r={},wn(e,function(n){t[n]!==undefined&&t.hasOwnProperty(n)&&(r[n]=t[n])}),r;var t,r},bt=function(n,e){return t=e,r={},H(n,function(n,e){yn(t,e)||(r[e]=n)}),r;var t,r},yt=function(n){return gt(n)},xt=function(n,e){return t=n,r=e,function(n){return gt(t)(n).getOr(r)};var t,r},wt=function(n,e){return vt(n,e)},St=function(n,e){return pt(n,e)},Ot=function(n){return e={},wn(n,function(n){e[n.key]=n.value}),e;var e},Tt=function(n,e){return dt(n,e)},kt=function(n,e){return r=e,(t=n).hasOwnProperty(r)&&t[r]!==undefined&&null!==t[r];var t,r},Ct=rt([{setOf:["validator","valueType"]},{arrOf:["valueType"]},{objOf:["fields"]},{itemOf:["validator"]},{choiceOf:["key","branches"]},{thunk:["description"]},{func:["args","outputSchema"]}]),Et=rt([{field:["name","presence","type"]},{state:["name"]}]),Dt=function(){return Me.getOrDie("JSON")},It=function(n,e,t){return Dt().stringify(n,e,t)},Mt=function(n){return g(n)&&100<N(n).length?" removed due to size":It(n,null,2)},At=function(n,e){return tt.error([{path:n,getErrorInfo:e}])},Bt=rt([{field:["key","okey","presence","prop"]},{state:["okey","instantiator"]}]),Rt=function(t,r,o){return vt(r,o).fold(function(){return n=o,e=r,At(t,function(){return'Could not find valid *strict* value for "'+n+'" in '+Mt(e)});var n,e},tt.value)},Ft=function(n,e,t){var r=vt(n,e).fold(function(){return t(n)},h);return tt.value(r)},Vt=function(o,c,n,a){return n.fold(function(i,e,n,t){var r=function(n){return t.extract(o.concat([i]),a,n).map(function(n){return pt(e,a(n))})},u=function(n){return n.fold(function(){var n=pt(e,a(V.none()));return tt.value(n)},function(n){return t.extract(o.concat([i]),a,n).map(function(n){return pt(e,a(V.some(n)))})})};return n.fold(function(){return Rt(o,c,i).bind(r)},function(n){return Ft(c,i,n).bind(r)},function(){return(n=c,e=i,tt.value(vt(n,e))).bind(u);var n,e},function(n){return(e=c,t=i,r=n,o=vt(e,t).map(function(n){return!0===n?r(e):n}),tt.value(o)).bind(u);var e,t,r,o},function(n){var e=n(c);return Ft(c,i,M({})).map(function(n){return C(e,n)}).bind(r)})},function(n,e){var t=e(c);return tt.value(pt(n,a(t)))})},Nt=function(r){return{extract:function(t,n,e){return r(e,n).fold(function(n){return e=n,At(t,function(){return e});var e},tt.value)},toString:function(){return"val"},toDsl:function(){return Ct.itemOf(r)}}},Ht=function(n){var a=zt(n),s=On(n,function(e,n){return n.fold(function(n){return C(e,St(n,!0))},M(e))},{});return{extract:function(n,e,t){var r,o,i,u=x(t)?[]:(o=N(r=t),Sn(o,function(n){return kt(r,n)})),c=Sn(u,function(n){return!kt(s,n)});return 0===c.length?a.extract(n,e,t):(i=c,At(n,function(){return"There are unsupported fields: ["+i.join(", ")+"] specified"}))},toString:a.toString,toDsl:a.toDsl}},zt=function(c){return{extract:function(n,e,t){return r=n,o=t,i=e,u=xn(c,function(n){return Vt(r,o,n,i)}),dt(u,{});var r,o,i,u},toString:function(){return"obj{\n"+xn(c,function(n){return n.fold(function(n,e,t,r){return n+" -> "+r.toString()},function(n,e){return"state("+n+")"})}).join("\n")+"}"},toDsl:function(){return Ct.objOf(xn(c,function(n){return n.fold(function(n,e,t,r){return Et.field(n,t,r)},function(n,e){return Et.state(n)})}))}}},jt=function(t,i){var e=function(n,e){return(o=Nt(t),{extract:function(t,r,n){var e=xn(n,function(n,e){return o.extract(t.concat(["["+e+"]"]),r,n)});return mt(e)},toString:function(){return"array("+o.toString()+")"},toDsl:function(){return Ct.arrOf(o)}}).extract(n,h,e);var o};return{extract:function(t,r,o){var n=N(o);return e(t,n).bind(function(n){var e=xn(n,function(n){return Bt.field(n,n,ut(),i)});return zt(e).extract(t,r,o)})},toString:function(){return"setOf("+i.toString()+")"},toDsl:function(){return Ct.setOf(t,i)}}},Lt=M(Nt(tt.value)),Pt=Bt.state,$t=Bt.field,Wt=function(t,e,r,o,i){return wt(o,i).fold(function(){return n=o,e=i,At(t,function(){return'The chosen schema: "'+e+'" did not exist in branches: '+Mt(n)});var n,e},function(n){return zt(n).extract(t.concat(["branch: "+i]),e,r)})},Gt=function(o,i){return{extract:function(e,t,r){return wt(r,o).fold(function(){return n=o,At(e,function(){return'Choice schema did not contain choice key: "'+n+'"'});var n},function(n){return Wt(e,t,r,i,n)})},toString:function(){return"chooseOn("+o+"). Possible values: "+N(i)},toDsl:function(){return Ct.choiceOf(o,i)}}},_t=Nt(tt.value),Ut=function(n,e,t,r){return e.extract([n],t,r).fold(function(n){return tt.error({input:r,errors:n})},tt.value)},qt=function(n,e,t){return Ut(n,e,M,t)},Yt=function(n){return n.fold(function(n){throw new Error(Jt(n))},h)},Kt=function(n,e,t){return Yt(Ut(n,e,h,t))},Xt=function(n,e,t){return Yt(qt(n,e,t))},Jt=function(n){return"Errors: \n"+(e=n.errors,t=10<e.length?e.slice(0,10).concat([{path:[],getErrorInfo:function(){return"... (only showing first ten failures)"}}]):e,xn(t,function(n){return"Failed path: ("+n.path.join(" > ")+")\n"+n.getErrorInfo()}))+"\n\nInput object: "+Mt(n.input);var e,t},Qt=function(n,e){return Gt(n,e)},Zt=M(_t),nr=(i=S,u="function",Nt(function(n){var e=typeof n;return i(n)?tt.value(n):tt.error("Expected type: "+u+" but got: "+e)})),er=function(n){return $t(n,n,ut(),Lt())},tr=function(n,e){return $t(n,n,ut(),e)},rr=function(n){return tr(n,nr)},or=function(n,e){return $t(n,n,ut(),zt(e))},ir=function(n){return $t(n,n,ct(),Lt())},ur=function(n,e){return $t(n,n,ct(),zt(e))},cr=function(n,e){return $t(n,n,ct(),Ht(e))},ar=function(n,e){return $t(n,n,it(e),Lt())},sr=function(n,e,t){return $t(n,n,it(e),t)},fr=function(n,e){return Pt(n,e)},lr=function(n){if(!kt(n,"can")&&!kt(n,"abort")&&!kt(n,"run"))throw new Error("EventHandler defined by: "+It(n,null,2)+" does not have can, abort, or run!");return Kt("Extracting event.handler",Ht([ar("can",M(!0)),ar("abort",M(!1)),ar("run",I)]),n)},dr=function(t){var e,r,o,i,n=(e=t,r=function(n){return n.can},function(){for(var t=[],n=0;n<arguments.length;n++)t[n]=arguments[n];return Tn(e,function(n,e){return n&&r(e).apply(undefined,t)},!0)}),u=(o=t,i=function(n){return n.abort},function(){for(var t=[],n=0;n<arguments.length;n++)t[n]=arguments[n];return Tn(o,function(n,e){return n||i(e).apply(undefined,t)},!1)});return lr({can:n,abort:u,run:function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];wn(t,function(n){n.run.apply(undefined,e)})}})},mr=function(n){return Ot(n)},gr=function(n,e){return{key:n,value:lr({abort:e})}},vr=function(n,e){return{key:n,value:lr({run:e})}},pr=function(n,e,t){return{key:n,value:lr({run:function(n){e.apply(undefined,[n].concat(t))}})}},hr=function(n){return function(r){return{key:n,value:lr({run:function(n,e){var t;t=e,He(n.element(),t.event().target())&&r(n,e)}})}}},br=function(n,e,t){var u,r,o=e.partUids()[t];return r=o,vr(u=n,function(n,i){n.getSystem().getByUid(r).each(function(n){var e,t,r,o;t=(e=n).element(),r=u,o=i,e.getSystem().triggerEvent(r,t,o.event())})})},yr=function(n){return vr(n,function(n,e){e.cut()})},xr=hr(te()),wr=hr(re()),Sr=hr(ne()),Or=(c=Kn(),function(n){return vr(c,n)}),Tr=function(n){return xn(n,function(n){return r=e="/*",o=(t=n).length-e.length,""!==r&&(t.length<r.length||t.substr(o,o+r.length)!==r)?n:n.substring(0,n.length-"/*".length);var e,t,r,o})},kr=function(n,e){var t=n.toString(),r=t.indexOf(")")+1,o=t.indexOf("("),i=t.substring(o+1,r-1).split(/,\s*/);return n.toFunctionAnnotation=function(){return{name:e,parameters:Tr(i)}},n},Cr=Ee(["tag"],["classes","attributes","styles","value","innerHtml","domChildren","defChildren"]),Er=function(n){return{tag:n.tag(),classes:n.classes().getOr([]),attributes:n.attributes().getOr({}),styles:n.styles().getOr({}),value:n.value().getOr("<none>"),innerHtml:n.innerHtml().getOr("<none>"),defChildren:n.defChildren().fold(function(){return"<none>"},function(n){return It(n,null,2)}),domChildren:n.domChildren().fold(function(){return"<none>"},function(n){return 0===n.length?"0 children, but still specified":String(n.length)})}},Dr=Ee([],["classes","attributes","styles","value","innerHtml","defChildren","domChildren"]),Ir=function(e,n,t){return n.fold(function(){return t.fold(function(){return{}},function(n){return St(e,n)})},function(n){return t.fold(function(){return St(e,n)},function(n){return St(e,n)})})},Mr=function(t,r,o){return Sr(function(n,e){o(n,t,r)})},Ar=function(n,e,t,r,o,i){var u,c,a=n,s=ur(e,[(u="config",c=n,$t(u,u,ct(),c))]);return Fr(a,s,e,t,r,o,i)},Br=function(o,i,u){var n,e,t,r,c,a;return n=function(t){for(var n=[],e=1;e<arguments.length;e++)n[e-1]=arguments[e];var r=[t].concat(n);return t.config({name:M(o)}).fold(function(){throw new Error("We could not find any behaviour configuration for: "+o+". Using API: "+u)},function(n){var e=Array.prototype.slice.call(r,1);return i.apply(undefined,[t,n.config,n.state].concat(e))})},e=u,t=i.toString(),r=t.indexOf(")")+1,c=t.indexOf("("),a=t.substring(c+1,r-1).split(/,\s*/),n.toFunctionAnnotation=function(){return{name:e,parameters:Tr(a.slice(0,1).concat(a.slice(3)))}},n},Rr=function(n){return{key:n,value:undefined}},Fr=function(t,n,r,o,e,i,u){var c=function(n){return kt(n,r)?n[r]():V.none()},a=z(e,function(n,e){return Br(r,n,e)}),s=z(i,function(n,e){return kr(n,e)}),f=C(s,a,{revoke:l(Rr,r),config:function(n){var e=Xt(r+"-config",t,n);return{key:r,value:{config:e,me:f,configAsRaw:nn(function(){return Kt(r+"-config",t,n)}),initialConfig:n,state:u}}},schema:function(){return n},exhibit:function(n,t){return c(n).bind(function(e){return wt(o,"exhibit").map(function(n){return n(t,e.config,e.state)})}).getOr(Dr({}))},name:function(){return r},handlers:function(n){return c(n).bind(function(e){return wt(o,"events").map(function(n){return n(e.config,e.state)})}).getOr({})}});return f},Vr=function(n,e){return Nr(n,e,{validate:S,label:"function"})},Nr=function(r,o,i){if(0===o.length)throw new Error("You must specify at least one required field.");return ke("required",o),Ce(o),function(e){var t=N(e);An(o,function(n){return yn(t,n)})||Oe(o,t),r(o,t);var n=Sn(o,function(n){return!i.validate(e[n],n)});return 0<n.length&&function(n,e){throw new Error("All values need to be of type: "+e+". Keys ("+Se(n).join(", ")+") were not.")}(n,i.label),e}},Hr=function(e,n){var t=Sn(n,function(n){return!yn(e,n)});0<t.length&&Te(t)},zr=I,jr=function(n){return Vr(Hr,n)},Lr={init:function(){return Pr({readState:function(){return"No State required"}})}},Pr=function(n){return Vr(zr,["readState"])(n),n},$r=function(n){return Ot(n)},Wr=Ht([er("fields"),er("name"),ar("active",{}),ar("apis",{}),ar("state",Lr),ar("extra",{})]),Gr=function(n){var e,t,r,o,i,u,c,a,s=Kt("Creating behaviour: "+n.name,Wr,n);return e=s.fields,t=s.name,r=s.active,o=s.apis,i=s.extra,u=s.state,c=Ht(e),a=ur(t,[cr("config",e)]),Fr(c,a,t,r,o,i,u)},_r=Ht([er("branchKey"),er("branches"),er("name"),ar("active",{}),ar("apis",{}),ar("state",Lr),ar("extra",{})]),Ur=M(undefined),qr=function(n,e,t){if(!(b(t)||x(t)||O(t)))throw v.console.error("Invalid call to Attr.set. Key ",e,":: Value ",t,":: Element ",n),new Error("Attribute value was not simple");n.setAttribute(e,t+"")},Yr=function(n,e,t){qr(n.dom(),e,t)},Kr=function(n,e){var t=n.dom();H(e,function(n,e){qr(t,e,n)})},Xr=function(n,e){var t=n.dom().getAttribute(e);return null===t?undefined:t},Jr=function(n,e){var t=n.dom();return!(!t||!t.hasAttribute)&&t.hasAttribute(e)},Qr=function(n,e){n.dom().removeAttribute(e)},Zr=function(n,e){var t=Xr(n,e);return t===undefined||""===t?[]:t.split(" ")},no=function(n){return n.dom().classList!==undefined},eo=function(n){return Zr(n,"class")},to=function(n,e){return o=e,i=Zr(t=n,r="class").concat([o]),Yr(t,r,i.join(" ")),!0;var t,r,o,i},ro=function(n,e){return o=e,0<(i=Sn(Zr(t=n,r="class"),function(n){return n!==o})).length?Yr(t,r,i.join(" ")):Qr(t,r),!1;var t,r,o,i},oo=function(n,e){no(n)?n.dom().classList.add(e):to(n,e)},io=function(n,e){var t;no(n)?n.dom().classList.remove(e):ro(n,e),0===(no(t=n)?t.dom().classList:eo(t)).length&&Qr(t,"class")},uo=function(n,e){return no(n)?n.dom().classList.toggle(e):(r=e,yn(eo(t=n),r)?ro(t,r):to(t,r));var t,r},co=function(n,e){return no(n)&&n.dom().classList.contains(e)},ao=function(n,e,t){io(n,t),oo(n,e)},so=Object.freeze({toAlpha:function(n,e,t){ao(n.element(),e.alpha(),e.omega())},toOmega:function(n,e,t){ao(n.element(),e.omega(),e.alpha())},isAlpha:function(n,e,t){return co(n.element(),e.alpha())},isOmega:function(n,e,t){return co(n.element(),e.omega())},clear:function(n,e,t){io(n.element(),e.alpha()),io(n.element(),e.omega())}}),fo=[er("alpha"),er("omega")],lo=Gr({fields:fo,name:"swapping",apis:so}),mo=function(n){var e=n,t=function(){return e};return{get:t,set:function(n){e=n},clone:function(){return mo(t())}}};function go(n,e,t,r,o){return n(t,r)?V.some(t):S(o)&&o(t)?V.none():e(t,r,o)}var vo=function(n,e,t){for(var r=n.dom(),o=S(t)?t:M(!1);r.parentNode;){r=r.parentNode;var i=fe.fromDom(r);if(e(i))return V.some(i);if(o(i))break}return V.none()},po=function(n,e,t){return go(function(n){return e(n)},vo,n,e,t)},ho=function(n,r){var o=function(n){for(var e=0;e<n.childNodes.length;e++){if(r(fe.fromDom(n.childNodes[e])))return V.some(fe.fromDom(n.childNodes[e]));var t=o(n.childNodes[e]);if(t.isSome())return t}return V.none()};return o(n.dom())},bo=function(n){n.dom().focus()},yo=function(n){n.dom().blur()},xo=function(n){var e=n!==undefined?n.dom():v.document;return V.from(e.activeElement).map(fe.fromDom)},wo=function(e){return xo(ze(e)).filter(function(n){return e.dom().contains(n.dom())})},So=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),Oo=tinymce.util.Tools.resolve("tinymce.ThemeManager"),To=function(n){var e=v.document.createElement("a");e.target="_blank",e.href=n.href,e.rel="noreferrer noopener";var t=v.document.createEvent("MouseEvents");t.initMouseEvent("click",!0,!0,v.window,0,0,0,0,0,!1,!1,!1,!1,0,null),v.document.body.appendChild(e),e.dispatchEvent(t),v.document.body.removeChild(e)},ko={formatChanged:M("formatChanged"),orientationChanged:M("orientationChanged"),dropupDismissed:M("dropupDismissed")},Co=function(n){return n.dom().innerHTML},Eo=function(n,e){var t,r,o=ze(n).dom(),i=fe.fromDom(o.createDocumentFragment()),u=(t=e,(r=(o||v.document).createElement("div")).innerHTML=t,Pe(fe.fromDom(r)));_e(i,u),Ue(n),Ge(n,i)},Do=function(n){return e=n,t=!1,fe.fromDom(e.dom().cloneNode(t));var e,t},Io=function(n){var e,t,r,o=Do(n);return e=o,t=fe.fromTag("div"),r=fe.fromDom(e.dom().cloneNode(!0)),Ge(t,r),Co(t)},Mo=function(n){return Io(n)},Ao=Object.freeze({events:function(c){return mr([vr(Yn(),function(o,i){var n,e,u=c.channels(),t=N(u),r=(n=t,(e=i).universal()?n:Sn(n,function(n){return yn(e.channels(),n)}));wn(r,function(n){var e=u[n](),t=e.schema(),r=Xt("channel["+n+"] data\nReceiver: "+Mo(o.element()),t,i.data());e.onReceive()(o,r)})})])}}),Bo=function(n){for(var e=[],t=function(n){e.push(n)},r=0;r<n.length;r++)n[r].each(t);return e},Ro=function(n,e){for(var t=0;t<n.length;t++){var r=e(n[t],t);if(r.isSome())return r}return V.none()},Fo="unknown",Vo=[],No=["alloy/data/Fields","alloy/debugging/Debugging"],Ho=function(){var n=new Error;if(n.stack!==undefined){var e=n.stack.split("\n");return kn(e,function(e){return 0<e.indexOf("alloy")&&!Cn(No,function(n){return-1<e.indexOf(n)}).isSome()}).getOr(Fo)}return Fo},zo={logEventCut:I,logEventStopped:I,logNoParent:I,logEventNoHandlers:I,logEventResponse:I,write:I},jo=function(n,e,t){var r,o="*"===Vo||yn(Vo,n)?(r=[],{logEventCut:function(n,e,t){r.push({outcome:"cut",target:e,purpose:t})},logEventStopped:function(n,e,t){r.push({outcome:"stopped",target:e,purpose:t})},logNoParent:function(n,e,t){r.push({outcome:"no-parent",target:e,purpose:t})},logEventNoHandlers:function(n,e){r.push({outcome:"no-handlers-left",target:e})},logEventResponse:function(n,e,t){r.push({outcome:"response",purpose:t,target:e})},write:function(){yn(["mousemove","mouseover","mouseout",ne()],n)||v.console.log(n,{event:n,target:e.dom(),sequence:xn(r,function(n){return yn(["cut","stopped","response"],n.outcome)?"{"+n.purpose+"} "+n.outcome+" at ("+Mo(n.target)+")":n.outcome})})}}):zo,i=t(o);return o.write(),i},Lo=M([er("menu"),er("selectedMenu")]),Po=M([er("item"),er("selectedItem")]),$o=(M(Ht(Po().concat(Lo()))),M(Ht(Po()))),Wo=or("initSize",[er("numColumns"),er("numRows")]),Go=function(n,e,t){var r;return Ho(),$t(e,e,t,(r=function(t){return tt.value(function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return t.apply(undefined,n)})},Nt(function(n){return r(n)})))},_o=function(n){return Go(0,n,it(I))},Uo=function(n){return Go(0,n,it(V.none))},qo=function(n){return Go(0,n,ut())},Yo=function(n){return Go(0,n,ut())},Ko=function(n,e){return fr(n,M(e))},Xo=function(n){return fr(n,h)},Jo=M(Wo),Qo=[tr("channels",jt(tt.value,Ht([qo("onReceive"),ar("schema",Zt())])))],Zo=Gr({fields:Qo,name:"receiving",active:Ao}),ni=function(n,e){var t=oi(n,e),r=e.aria();r.update()(n,r,t)},ei=function(n,e,t){uo(n.element(),e.toggleClass()),ni(n,e)},ti=function(n,e,t){oo(n.element(),e.toggleClass()),ni(n,e)},ri=function(n,e,t){io(n.element(),e.toggleClass()),ni(n,e)},oi=function(n,e){return co(n.element(),e.toggleClass())},ii=function(n,e,t){(e.selected()?ti:ri)(n,e,t)},ui=Object.freeze({onLoad:ii,toggle:ei,isOn:oi,on:ti,off:ri}),ci=Object.freeze({exhibit:function(n,e,t){return Dr({})},events:function(n,e){var t,r,o,i=(t=n,r=e,o=ei,Or(function(n){o(n,t,r)})),u=Mr(n,e,ii);return mr(In([n.toggleOnExecute()?[i]:[],[u]]))}}),ai=function(n,e,t){Yr(n.element(),"aria-expanded",t)},si=[ar("selected",!1),er("toggleClass"),ar("toggleOnExecute",!0),sr("aria",{mode:"none"},Qt("mode",{pressed:[ar("syncWithExpanded",!1),Ko("update",function(n,e,t){Yr(n.element(),"aria-pressed",t),e.syncWithExpanded()&&ai(n,e,t)})],checked:[Ko("update",function(n,e,t){Yr(n.element(),"aria-checked",t)})],expanded:[Ko("update",ai)],selected:[Ko("update",function(n,e,t){Yr(n.element(),"aria-selected",t)})],none:[Ko("update",I)]}))],fi=Gr({fields:si,name:"toggling",active:ci,apis:ui}),li=function(t,r){return Zo.config({channels:St(ko.formatChanged(),{onReceive:function(n,e){e.command===t&&r(n,e.state)}})})},di=function(n){return Zo.config({channels:St(ko.orientationChanged(),{onReceive:n})})},mi=function(n,e){return{key:n,value:{onReceive:e}}},gi="tinymce-mobile",vi={resolve:function(n){return gi+"-"+n},prefix:M(gi)},pi=function(n,e){e.ignore()||(bo(n.element()),e.onFocus()(n))},hi=Object.freeze({focus:pi,blur:function(n,e){e.ignore()||yo(n.element())},isFocused:function(n){return e=n.element(),t=ze(e).dom(),e.dom()===t.activeElement;var e,t}}),bi=Object.freeze({exhibit:function(n,e){return e.ignore()?Dr({}):Dr({attributes:{tabindex:"-1"}})},events:function(t){return mr([vr(Un(),function(n,e){pi(n,t),e.stop()})])}}),yi=[_o("onFocus"),ar("ignore",!1)],xi=Gr({fields:yi,name:"focusing",active:bi,apis:hi}),wi=function(n){return n.style!==undefined&&S(n.style.getPropertyValue)},Si=function(n,e,t){if(!b(t))throw v.console.error("Invalid call to CSS.set. Property ",e,":: Value ",t,":: Element ",n),new Error("CSS value must be a string: "+t);wi(n)&&n.style.setProperty(e,t)},Oi=function(n,e,t){var r=n.dom();Si(r,e,t)},Ti=function(n,e){var t=n.dom();H(e,function(n,e){Si(t,e,n)})},ki=function(n,e){var t=n.dom(),r=v.window.getComputedStyle(t).getPropertyValue(e),o=""!==r||be(n)?r:Ci(t,e);return null===o?undefined:o},Ci=function(n,e){return wi(n)?n.style.getPropertyValue(e):""},Ei=function(n,e){var t=n.dom(),r=Ci(t,e);return V.from(r).filter(function(n){return 0<n.length})},Di=function(n,e){var t,r,o=n.dom();r=e,wi(t=o)&&t.style.removeProperty(r),Jr(n,"style")&&""===Xr(n,"style").replace(/^\s+|\s+$/g,"")&&Qr(n,"style")},Ii=function(n){return n.dom().offsetWidth};function Mi(r,o){var n=function(n){var e=o(n);if(e<=0||null===e){var t=ki(n,r);return parseFloat(t)||0}return e},i=function(o,n){return Tn(n,function(n,e){var t=ki(o,e),r=t===undefined?0:parseInt(t,10);return isNaN(r)?n:n+r},0)};return{set:function(n,e){if(!O(e)&&!e.match(/^[0-9]+$/))throw new Error(r+".set accepts only positive integer values. Value was "+e);var t=n.dom();wi(t)&&(t.style[r]=e+"px")},get:n,getOuter:n,aggregate:i,max:function(n,e,t){var r=i(n,t);return r<e?e-r:0}}}var Ai,Bi,Ri=Mi("height",function(n){var e=n.dom();return be(n)?e.getBoundingClientRect().height:e.offsetHeight}),Fi=function(n){return Ri.get(n)},Vi=function(n,e,t){return Sn(function(n,e){for(var t=S(e)?e:M(!1),r=n.dom(),o=[];null!==r.parentNode&&r.parentNode!==undefined;){var i=r.parentNode,u=fe.fromDom(i);if(o.push(u),!0===t(u))break;r=i}return o}(n,t),e)},Ni=function(n,e){return Sn(Le(t=n).map(Pe).map(function(n){return Sn(n,function(n){return!He(t,n)})}).getOr([]),e);var t},Hi=function(n,e){return Ve(e,n)},zi=function(n){return Ne(n)},ji=function(n,e,t){return vo(n,function(n){return Re(n,e)},t)},Li=function(n,e){return Ne(e,n)},Pi=function(n,e,t){return go(Re,ji,n,e,t)},$i=function(n,e,t){var r=Bn(n.slice(0,e)),o=Bn(n.slice(e+1));return kn(r.concat(o),t)},Wi=function(n,e,t){var r=Bn(n.slice(0,e));return kn(r,t)},Gi=function(n,e,t){var r=n.slice(0,e),o=n.slice(e+1);return kn(o.concat(r),t)},_i=function(n,e,t){var r=n.slice(e+1);return kn(r,t)},Ui=function(t){return function(n){var e=n.raw();return yn(t,e.which)}},qi=function(n){return function(e){return An(n,function(n){return n(e)})}},Yi=function(n){return!0===n.raw().shiftKey},Ki=function(n){return!0===n.raw().ctrlKey},Xi=w(Yi),Ji=function(n,e){return{matches:n,classification:e}},Qi=function(n,e,t,r){var o=n+e;return r<o?t:o<t?r:o},Zi=function(n,e,t){return n<=e?e:t<=n?t:n},nu=function(e,t,n){var r=Hi(e.element(),"."+t.highlightClass());wn(r,function(n){io(n,t.highlightClass()),e.getSystem().getByDom(n).each(function(n){t.onDehighlight()(e,n)})})},eu=function(n,e,t,r){var o=tu(n,e,t,r);nu(n,e),oo(r.element(),e.highlightClass()),o||e.onHighlight()(n,r)},tu=function(n,e,t,r){return co(r.element(),e.highlightClass())},ru=function(n,e,t,r){var o=Hi(n.element(),"."+e.itemClass());return V.from(o[r]).fold(function(){return tt.error("No element found with index "+r)},n.getSystem().getByDom)},ou=function(e,n,t){return Li(e.element(),"."+n.itemClass()).bind(function(n){return e.getSystem().getByDom(n).toOption()})},iu=function(e,n,t){var r=Hi(e.element(),"."+n.itemClass());return(0<r.length?V.some(r[r.length-1]):V.none()).bind(function(n){return e.getSystem().getByDom(n).toOption()})},uu=function(t,e,n,r){var o=Hi(t.element(),"."+e.itemClass());return Cn(o,function(n){return co(n,e.highlightClass())}).bind(function(n){var e=Qi(n,r,0,o.length-1);return t.getSystem().getByDom(o[e]).toOption()})},cu=Object.freeze({dehighlightAll:nu,dehighlight:function(n,e,t,r){var o=tu(n,e,t,r);io(r.element(),e.highlightClass()),o&&e.onDehighlight()(n,r)},highlight:eu,highlightFirst:function(e,t,r){ou(e,t).each(function(n){eu(e,t,r,n)})},highlightLast:function(e,t,r){iu(e,t).each(function(n){eu(e,t,r,n)})},highlightAt:function(e,t,r,n){ru(e,t,r,n).fold(function(n){throw new Error(n)},function(n){eu(e,t,r,n)})},highlightBy:function(e,t,r,n){var o=Hi(e.element(),"."+t.itemClass()),i=Bo(xn(o,function(n){return e.getSystem().getByDom(n).toOption()}));kn(i,n).each(function(n){eu(e,t,r,n)})},isHighlighted:tu,getHighlighted:function(e,n,t){return Li(e.element(),"."+n.highlightClass()).bind(function(n){return e.getSystem().getByDom(n).toOption()})},getFirst:ou,getLast:iu,getPrevious:function(n,e,t){return uu(n,e,0,-1)},getNext:function(n,e,t){return uu(n,e,0,1)}}),au=[er("highlightClass"),er("itemClass"),_o("onHighlight"),_o("onDehighlight")],su=Gr({fields:au,name:"highlighting",apis:cu}),fu=function(){return{get:function(n){return wo(n.element())},set:function(n,e){n.getSystem().triggerFocus(e,n.element())}}},lu=function(n,e,c,t,r,i){var u=function(e,t,r,o){var n,i,u=c(e,t,r,o);return(n=u,i=t.event(),kn(n,function(n){return n.matches(i)}).map(function(n){return n.classification})).bind(function(n){return n(e,t,r,o)})},o={schema:function(){return n.concat([ar("focusManager",fu()),Ko("handler",o),Ko("state",e)])},processKey:u,toEvents:function(r,o){var n=t(r,o),e=mr(i.map(function(t){return vr(Un(),function(n,e){t(n,r,o,e),e.stop()})}).toArray().concat([vr(Y(),function(n,e){u(n,e,r,o).each(function(n){e.stop()})})]));return C(n,e)},toApis:r};return o},du=function(n){var e=[ir("onEscape"),ir("onEnter"),ar("selector",'[data-alloy-tabstop="true"]'),ar("firstTabstop",0),ar("useTabstopAt",M(!0)),ir("visibilitySelector")].concat([n]),u=function(n,e){var t=n.visibilitySelector().bind(function(n){return Pi(e,n)}).getOr(e);return 0<Fi(t)},c=function(e,n,t,r,o){return o(n,t,function(n){return u(e=r,t=n)&&e.useTabstopAt()(t);var e,t}).fold(function(){return r.cyclic()?V.some(!0):V.none()},function(n){return r.focusManager().set(e,n),V.some(!0)})},i=function(e,n,t,r){var o,i,u=Hi(e.element(),t.selector());return(o=e,i=t,i.focusManager().get(o).bind(function(n){return Pi(n,i.selector())})).bind(function(n){return Cn(u,l(He,n)).bind(function(n){return c(e,u,n,t,r)})})},t=M([Ji(qi([Yi,Ui([9])]),function(n,e,t,r){var o=t.cyclic()?$i:Wi;return i(n,0,t,o)}),Ji(Ui([9]),function(n,e,t,r){var o=t.cyclic()?Gi:_i;return i(n,0,t,o)}),Ji(Ui([27]),function(e,t,n,r){return n.onEscape().bind(function(n){return n(e,t)})}),Ji(qi([Xi,Ui([13])]),function(e,t,n,r){return n.onEnter().bind(function(n){return n(e,t)})})]),r=M({}),o=M({});return lu(e,Lr.init,t,r,o,V.some(function(e,t){var n,r,o,i;(n=e,r=t,o=Hi(n.element(),r.selector()),i=Sn(o,function(n){return u(r,n)}),V.from(i[r.firstTabstop()])).each(function(n){t.focusManager().set(e,n)})}))},mu=du(fr("cyclic",M(!1))),gu=du(fr("cyclic",M(!0))),vu=function(n){return"input"===ge(n)&&"radio"!==Xr(n,"type")||"textarea"===ge(n)},pu=function(n,e,t){return vu(t)&&Ui([32])(e.event())?V.none():(ce(n,t,Kn()),V.some(!0))},hu=[ar("execute",pu),ar("useSpace",!1),ar("useEnter",!0),ar("useControlEnter",!1),ar("useDown",!1)],bu=function(n,e,t){return t.execute()(n,e,n.element())},yu=M({}),xu=M({}),wu=lu(hu,Lr.init,function(n,e,t,r){var o=t.useSpace()&&!vu(n.element())?[32]:[],i=t.useEnter()?[13]:[],u=t.useDown()?[40]:[],c=o.concat(i).concat(u);return[Ji(Ui(c),bu)].concat(t.useControlEnter()?[Ji(qi([Ki,Ui([13])]),bu)]:[])},yu,xu,V.none()),Su=function(n){var t=mo(V.none());return Pr({readState:M({}),setGridSize:function(n,e){t.set(V.some({numRows:M(n),numColumns:M(e)}))},getNumRows:function(){return t.get().map(function(n){return n.numRows()})},getNumColumns:function(){return t.get().map(function(n){return n.numColumns()})}})},Ou=Object.freeze({flatgrid:Su,init:function(n){return n.state()(n)}}),Tu=function(e,t){return function(n){return"rtl"===ku(n)?t:e}},ku=function(n){return"rtl"===ki(n,"direction")?"rtl":"ltr"},Cu=function(i){return function(n,e,t,r){var o=i(n.element());return Mu(o,n,e,t,r)}},Eu=function(n,e){var t=Tu(n,e);return Cu(t)},Du=function(n,e){var t=Tu(e,n);return Cu(t)},Iu=function(o){return function(n,e,t,r){return Mu(o,n,e,t,r)}},Mu=function(e,t,n,r,o){return r.focusManager().get(t).bind(function(n){return e(t.element(),n,r,o)}).map(function(n){return r.focusManager().set(t,n),!0})},Au=Iu,Bu=Iu,Ru=Iu,Fu=function(n){var e,t=n.dom();return!((e=t).offsetWidth<=0&&e.offsetHeight<=0)},Vu=Ee(["index","candidates"],[]),Nu=function(n,e,t){return Hu(n,e,t)},Hu=function(n,e,t,r){var o,i=l(He,e),u=Hi(n,t),c=Sn(u,Fu);return Cn(o=c,i).map(function(n){return Vu({index:n,candidates:o})})},zu=function(n,e){return Cn(n,function(n){return He(e,n)})},ju=function(t,n,r,e){return e(Math.floor(n/r),n%r).bind(function(n){var e=n.row()*r+n.column();return 0<=e&&e<t.length?V.some(t[e]):V.none()})},Lu=function(o,n,i,u,c){return ju(o,n,u,function(n,e){var t=n===i-1?o.length-n*u:u,r=Qi(e,c,0,t-1);return V.some({row:M(n),column:M(r)})})},Pu=function(i,n,u,c,a){return ju(i,n,c,function(n,e){var t=Qi(n,a,0,u-1),r=t===u-1?i.length-t*c:c,o=Zi(e,0,r-1);return V.some({row:M(t),column:M(o)})})},$u=[er("selector"),ar("execute",pu),Uo("onEscape"),ar("captureTab",!1),Jo()],Wu=function(o){return function(n,e,t,r){return Nu(n,e,t.selector()).bind(function(n){return o(n.candidates(),n.index(),r.getNumRows().getOr(t.initSize().numRows()),r.getNumColumns().getOr(t.initSize().numColumns()))})}},Gu=function(n,e,t,r){return t.captureTab()?V.some(!0):V.none()},_u=Wu(function(n,e,t,r){return Lu(n,e,t,r,-1)}),Uu=Wu(function(n,e,t,r){return Lu(n,e,t,r,1)}),qu=Wu(function(n,e,t,r){return Pu(n,e,t,r,-1)}),Yu=Wu(function(n,e,t,r){return Pu(n,e,t,r,1)}),Ku=M([Ji(Ui([37]),Eu(_u,Uu)),Ji(Ui([39]),Du(_u,Uu)),Ji(Ui([38]),Au(qu)),Ji(Ui([40]),Bu(Yu)),Ji(qi([Yi,Ui([9])]),Gu),Ji(qi([Xi,Ui([9])]),Gu),Ji(Ui([27]),function(n,e,t,r){return t.onEscape()(n,e)}),Ji(Ui([32].concat([13])),function(e,t,r,n){return(o=e,i=r,i.focusManager().get(o).bind(function(n){return Pi(n,i.selector())})).bind(function(n){return r.execute()(e,t,n)});var o,i})]),Xu=M({}),Ju=lu($u,Su,Ku,Xu,{},V.some(function(e,t,n){Li(e.element(),t.selector()).each(function(n){t.focusManager().set(e,n)})})),Qu=function(n,e,t,o){return Nu(n,t,e).bind(function(n){var e=n.index(),t=n.candidates(),r=Qi(e,o,0,t.length-1);return V.from(t[r])})},Zu=[er("selector"),ar("getInitial",V.none),ar("execute",pu),ar("executeOnMove",!1),ar("allowVertical",!0)],nc=function(e,t,r){return(n=e,o=r,o.focusManager().get(n).bind(function(n){return Pi(n,o.selector())})).bind(function(n){return r.execute()(e,t,n)});var n,o},ec=function(n,e,t){return Qu(n,t.selector(),e,-1)},tc=function(n,e,t){return Qu(n,t.selector(),e,1)},rc=function(r){return function(n,e,t){return r(n,e,t).bind(function(){return t.executeOnMove()?nc(n,e,t):V.some(!0)})}},oc=M({}),ic=M({}),uc=lu(Zu,Lr.init,function(n,e,t,r){var o=[37].concat(t.allowVertical()?[38]:[]),i=[39].concat(t.allowVertical()?[40]:[]);return[Ji(Ui(o),rc(Eu(ec,tc))),Ji(Ui(i),rc(Du(ec,tc))),Ji(Ui([13]),nc),Ji(Ui([32]),nc)]},oc,ic,V.some(function(e,t){t.getInitial()(e).or(Li(e.element(),t.selector())).each(function(n){t.focusManager().set(e,n)})})),cc=Ee(["rowIndex","columnIndex","cell"],[]),ac=function(n,e,t){return V.from(n[e]).bind(function(n){return V.from(n[t]).map(function(n){return cc({rowIndex:e,columnIndex:t,cell:n})})})},sc=function(n,e,t,r){var o=n[e].length,i=Qi(t,r,0,o-1);return ac(n,e,i)},fc=function(n,e,t,r){var o=Qi(t,r,0,n.length-1),i=n[o].length,u=Zi(e,0,i-1);return ac(n,o,u)},lc=function(n,e,t,r){var o=n[e].length,i=Zi(t+r,0,o-1);return ac(n,e,i)},dc=function(n,e,t,r){var o=Zi(t+r,0,n.length-1),i=n[o].length,u=Zi(e,0,i-1);return ac(n,o,u)},mc=[or("selectors",[er("row"),er("cell")]),ar("cycles",!0),ar("previousSelector",V.none),ar("execute",pu)],gc=function(n,e){return function(t,r,i){var u=i.cycles()?n:e;return Pi(r,i.selectors().row()).bind(function(n){var e=Hi(n,i.selectors().cell());return zu(e,r).bind(function(r){var o=Hi(t,i.selectors().row());return zu(o,n).bind(function(n){var e,t=(e=i,xn(o,function(n){return Hi(n,e.selectors().cell())}));return u(t,n,r).map(function(n){return n.cell()})})})})}},vc=gc(function(n,e,t){return sc(n,e,t,-1)},function(n,e,t){return lc(n,e,t,-1)}),pc=gc(function(n,e,t){return sc(n,e,t,1)},function(n,e,t){return lc(n,e,t,1)}),hc=gc(function(n,e,t){return fc(n,t,e,-1)},function(n,e,t){return dc(n,t,e,-1)}),bc=gc(function(n,e,t){return fc(n,t,e,1)},function(n,e,t){return dc(n,t,e,1)}),yc=M([Ji(Ui([37]),Eu(vc,pc)),Ji(Ui([39]),Du(vc,pc)),Ji(Ui([38]),Au(hc)),Ji(Ui([40]),Bu(bc)),Ji(Ui([32].concat([13])),function(e,t,r){return wo(e.element()).bind(function(n){return r.execute()(e,t,n)})})]),xc=M({}),wc=M({}),Sc=lu(mc,Lr.init,yc,xc,wc,V.some(function(e,t){t.previousSelector()(e).orThunk(function(){var n=t.selectors();return Li(e.element(),n.cell())}).each(function(n){t.focusManager().set(e,n)})})),Oc=[er("selector"),ar("execute",pu),ar("moveOnTab",!1)],Tc=function(e,t,r){return r.focusManager().get(e).bind(function(n){return r.execute()(e,t,n)})},kc=function(n,e,t){return Qu(n,t.selector(),e,-1)},Cc=function(n,e,t){return Qu(n,t.selector(),e,1)},Ec=M([Ji(Ui([38]),Ru(kc)),Ji(Ui([40]),Ru(Cc)),Ji(qi([Yi,Ui([9])]),function(n,e,t){return t.moveOnTab()?Ru(kc)(n,e,t):V.none()}),Ji(qi([Xi,Ui([9])]),function(n,e,t){return t.moveOnTab()?Ru(Cc)(n,e,t):V.none()}),Ji(Ui([13]),Tc),Ji(Ui([32]),Tc)]),Dc=M({}),Ic=M({}),Mc=lu(Oc,Lr.init,Ec,Dc,Ic,V.some(function(e,t){Li(e.element(),t.selector()).each(function(n){t.focusManager().set(e,n)})})),Ac=[Uo("onSpace"),Uo("onEnter"),Uo("onShiftEnter"),Uo("onLeft"),Uo("onRight"),Uo("onTab"),Uo("onShiftTab"),Uo("onUp"),Uo("onDown"),Uo("onEscape"),ir("focusIn")],Bc=lu(Ac,Lr.init,function(n,e,t){return[Ji(Ui([32]),t.onSpace()),Ji(qi([Xi,Ui([13])]),t.onEnter()),Ji(qi([Yi,Ui([13])]),t.onShiftEnter()),Ji(qi([Yi,Ui([9])]),t.onShiftTab()),Ji(qi([Xi,Ui([9])]),t.onTab()),Ji(Ui([38]),t.onUp()),Ji(Ui([40]),t.onDown()),Ji(Ui([37]),t.onLeft()),Ji(Ui([39]),t.onRight()),Ji(Ui([32]),t.onSpace()),Ji(Ui([27]),t.onEscape())]},function(){return{}},function(){return{}},V.some(function(e,t){return t.focusIn().bind(function(n){return n(e,t)})})),Rc=mu.schema(),Fc=gu.schema(),Vc=uc.schema(),Nc=Ju.schema(),Hc=Sc.schema(),zc=wu.schema(),jc=Mc.schema(),Lc=Bc.schema(),Pc=(Bi=Kt("Creating behaviour: "+(Ai={branchKey:"mode",branches:Object.freeze({acyclic:Rc,cyclic:Fc,flow:Vc,flatgrid:Nc,matrix:Hc,execution:zc,menu:jc,special:Lc}),name:"keying",active:{events:function(n,e){return n.handler().toEvents(n,e)}},apis:{focusIn:function(n){n.getSystem().triggerFocus(n.element(),n.element())},setGridSize:function(n,e,t,r,o){kt(t,"setGridSize")?t.setGridSize(r,o):v.console.error("Layout does not support setGridSize")}},state:Ou}).name,_r,Ai),Ar(Qt(Bi.branchKey,Bi.branches),Bi.name,Bi.active,Bi.apis,Bi.extra,Bi.state)),$c=function(r,n){return e=r,t={},o=xn(n,function(n){return e=n.name(),t="Cannot configure "+n.name()+" for "+r,$t(e,e,ct(),Nt(function(n){return tt.error("The field: "+e+" is forbidden. "+t)}));var e,t}).concat([fr("dump",h)]),$t(e,e,it(t),zt(o));var e,t,o},Wc=function(n){return n.dump()},Gc="placeholder",_c=rt([{single:["required","valueThunk"]},{multiple:["required","valueThunks"]}]),Uc=function(n,e,t,r){return t.uiType===Gc?(i=t,u=r,(o=n).exists(function(n){return n!==i.owner})?_c.single(!0,M(i)):wt(u,i.name).fold(function(){throw new Error("Unknown placeholder component: "+i.name+"\nKnown: ["+N(u)+"]\nNamespace: "+o.getOr("none")+"\nSpec: "+It(i,null,2))},function(n){return n.replace()})):_c.single(!1,M(t));var o,i,u},qc=function(i,u,c,a){return Uc(i,0,c,a).fold(function(n,e){var t=e(u,c.config,c.validated),r=wt(t,"components").getOr([]),o=Mn(r,function(n){return qc(i,u,n,a)});return[C(t,{components:o})]},function(n,e){return e(u,c.config,c.validated)})},Yc=function(e,t,n,r){var o,i,u,c=z(r,function(n,e){return r=n,o=!1,{name:M(t=e),required:function(){return r.fold(function(n,e){return n},function(n,e){return n})},used:function(){return o},replace:function(){if(!0===o)throw new Error("Trying to use the same placeholder more than once: "+t);return o=!0,r}};var t,r,o}),a=(o=e,i=t,u=c,Mn(n,function(n){return qc(o,i,n,u)}));return H(c,function(n){if(!1===n.used()&&n.required())throw new Error("Placeholder: "+n.name()+" was not found in components list\nNamespace: "+e.getOr("none")+"\nComponents: "+It(t.components(),null,2))}),a},Kc=_c.single,Xc=_c.multiple,Jc=M(Gc),Qc=0,Zc=function(n){var e=(new Date).getTime();return n+"_"+Math.floor(1e9*Math.random())+ ++Qc+String(e)},na=rt([{required:["data"]},{external:["data"]},{optional:["data"]},{group:["data"]}]),ea=ar("factory",{sketch:h}),ta=ar("schema",[]),ra=er("name"),oa=$t("pname","pname",at(function(n){return"<alloy."+Zc(n.name)+">"}),Zt()),ia=ar("defaults",M({})),ua=ar("overrides",M({})),ca=zt([ea,ta,ra,oa,ia,ua]),aa=zt([ea,ta,ra,oa,ia,ua]),sa=zt([ea,ta,ra,er("unit"),oa,ia,ua]),fa=function(n){var e=function(n){return n.name()};return n.fold(e,e,e,e)},la=function(t,r){return function(n){var e=Xt("Converting part type",r,n);return t(e)}},da=la(na.required,ca),ma=la(na.optional,aa),ga=la(na.group,sa),va=M("entirety"),pa=function(n,e,t,r){var o=t;return C(e.defaults()(n,t,r),t,{uid:n.partUids()[e.name()]},e.overrides()(n,t,r),{"debug.sketcher":St("part-"+e.name(),o)})},ha=function(o,n){var i={};return wn(n,function(n){var e;(e=n,e.fold(V.some,V.none,V.some,V.some)).each(function(t){var r=ba(o,t.pname());i[t.name()]=function(n){var e=Kt("Part: "+t.name()+" in "+o,zt(t.schema()),n);return C(r,{config:n,validated:e})}})}),i},ba=function(n,e){return{uiType:Jc(),owner:n,name:e}},ya=function(n,e,t){return r=e,i={},o={},wn(t,function(n){n.fold(function(r){i[r.pname()]=Kc(!0,function(n,e,t){return r.factory().sketch(pa(n,r,e,t))})},function(n){var e=r.parts()[n.name()]();o[n.name()]=M(pa(r,n,e[va()]()))},function(r){i[r.pname()]=Kc(!1,function(n,e,t){return r.factory().sketch(pa(n,r,e,t))})},function(o){i[o.pname()]=Xc(!0,function(e,n,t){var r=e[o.name()]();return xn(r,function(n){return o.factory().sketch(C(o.defaults()(e,n),n,o.overrides()(e,n)))})})})}),{internals:M(i),externals:M(o)};var r,i,o},xa=function(n,e,t){return Yc(V.some(n),e,e.components(),t)},wa=function(n,e,t){var r=e.partUids()[t];return n.getSystem().getByUid(r).toOption()},Sa=function(n,e,t){return wa(n,e,t).getOrDie("Could not find part: "+t)},Oa=function(e,n){var t=xn(n,fa);return Ot(xn(t,function(n){return{key:n,value:e+"-"+n}}))},Ta=function(e){return $t("partUids","partUids",st(function(n){return Oa(n.uid,e)}),Zt())},ka=Zc("alloy-premade"),Ca=Zc("api"),Ea=function(n){return St(ka,n)},Da=function(o){return n=function(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];var r=n.config(Ca);return o.apply(undefined,[r].concat([n].concat(e)))},e=o.toString(),t=e.indexOf(")")+1,r=e.indexOf("("),i=e.substring(r+1,t-1).split(/,\s*/),n.toFunctionAnnotation=function(){return{name:"OVERRIDE",parameters:Tr(i.slice(1))}},n;var n,e,t,r,i},Ia=M(Ca),Ma=M("alloy-id-"),Aa=M("data-alloy-id"),Ba=Ma(),Ra=Aa(),Fa=function(n){var e=pe(n)?Xr(n,Ra):null;return V.from(e)},Va=function(n){return Zc(n)},Na=function(n,e,t,r,o){var i,u,c=(u=o,(0<(i=r).length?[or("parts",i)]:[]).concat([er("uid"),ar("dom",{}),ar("components",[]),Xo("originalSpec"),ar("debug.sketcher",{})]).concat(u));return Xt(n+" [SpecSchema]",Ht(c.concat(e)),t)},Ha=function(n,e,t,r,o){var i=za(o),u=Mn(t,function(n){return n.fold(V.none,V.some,V.none,V.none).map(function(n){return or(n.name(),n.schema().concat([Xo(va())]))}).toArray()}),c=Ta(t),a=Na(n,e,i,u,[c]),s=ya(0,a,t),f=xa(n,a,s.internals());return C(r(a,f,i,s.externals()),{"debug.sketcher":St(n,o)})},za=function(n){return C({uid:Va("uid")},n)},ja=Ht([er("name"),er("factory"),er("configFields"),ar("apis",{}),ar("extraApis",{})]),La=Ht([er("name"),er("factory"),er("configFields"),er("partFields"),ar("apis",{}),ar("extraApis",{})]),Pa=function(n){var c=Kt("Sketcher for "+n.name,ja,n),e=z(c.apis,Da),t=z(c.extraApis,function(n,e){return kr(n,e)});return C({name:M(c.name),partFields:M([]),configFields:M(c.configFields),sketch:function(n){return e=c.name,t=c.configFields,r=c.factory,i=za(o=n),u=Na(e,t,i,[],[]),C(r(u,i),{"debug.sketcher":St(e,o)});var e,t,r,o,i,u}},e,t)},$a=function(n){var e=Kt("Sketcher for "+n.name,La,n),t=ha(e.name,e.partFields),r=z(e.apis,Da),o=z(e.extraApis,function(n,e){return kr(n,e)});return C({name:M(e.name),partFields:M(e.partFields),configFields:M(e.configFields),sketch:function(n){return Ha(e.name,e.configFields,e.partFields,e.factory,n)},parts:M(t)},r,o)},Wa=Pa({name:"Button",factory:function(n){var e,t,r,o=(e=n.action(),t=function(n,e){e.stop(),ue(n)},r=Gn.detect().deviceType.isTouch()?[vr(Jn(),t)]:[vr(J(),t),vr(G(),function(n,e){e.cut()})],mr(In([e.map(function(t){return vr(Kn(),function(n,e){t(n),e.stop()})}).toArray(),r]))),i=wt(n.dom(),"attributes").bind(yt("type")),u=wt(n.dom(),"tag");return{uid:n.uid(),dom:n.dom(),components:n.components(),events:o,behaviours:C($r([xi.config({}),Pc.config({mode:"execution",useSpace:!0,useEnter:!0})]),Wc(n.buttonBehaviours())),domModification:{attributes:C(i.fold(function(){return u.is("button")?{type:"button"}:{}},function(n){return{}}),{role:n.role().getOr("button")})},eventOrder:n.eventOrder()}},configFields:[ar("uid",undefined),er("dom"),ar("components",[]),$c("buttonBehaviours",[xi,Pc]),ir("action"),ir("role"),ar("eventOrder",{})]}),Ga=Gr({fields:[],name:"unselecting",active:Object.freeze({events:function(n){return mr([gr(Z(),M(!0))])},exhibit:function(n,e){return Dr({styles:{"-webkit-user-select":"none","user-select":"none","-ms-user-select":"none","-moz-user-select":"-moz-none"},attributes:{unselectable:"on"}})}})}),_a=function(n){var e,t,r,o=fe.fromHtml(n),i=Pe(o),u=(t=(e=o).dom().attributes!==undefined?e.dom().attributes:[],Tn(t,function(n,e){return"class"===e.name?n:C(n,St(e.name,e.value))},{})),c=(r=o,Array.prototype.slice.call(r.dom().classList,0)),a=0===i.length?{}:{innerHtml:Co(o)};return C({tag:ge(o),classes:c,attributes:u},a)},Ua=function(n){var e,o,t=(e=n,o={prefix:vi.prefix()},e.replace(/\$\{([^{}]*)\}/g,function(n,e){var t,r=o[e];return"string"==(t=typeof r)||"number"===t?r.toString():n}));return _a(t)},qa=function(n){return{dom:Ua(n)}},Ya=function(n){return $r([fi.config({toggleClass:vi.resolve("toolbar-button-selected"),toggleOnExecute:!1,aria:{mode:"pressed"}}),li(n,function(n,e){(e?fi.on:fi.off)(n)})])},Ka=function(n,e,t){return Wa.sketch({dom:Ua('<span class="${prefix}-toolbar-button ${prefix}-icon-'+n+' ${prefix}-icon"></span>'),action:e,buttonBehaviours:C($r([Ga.config({})]),t)})},Xa={forToolbar:Ka,forToolbarCommand:function(n,e){return Ka(e,function(){n.execCommand(e)},{})},forToolbarStateAction:function(n,e,t,r){var o=Ya(t);return Ka(e,r,o)},forToolbarStateCommand:function(n,e){var t=Ya(e);return Ka(e,function(){n.execCommand(e)},t)}},Ja=function(t,r){return{left:M(t),top:M(r),translate:function(n,e){return Ja(t+n,r+e)}}},Qa=Ja,Za=function(n,e,t){return Math.max(e,Math.min(t,n))},ns=function(n,e,t,r,o,i,u){var c=t-e;if(r<n.left)return e-1;if(r>n.right)return t+1;var a,s,f,l,d=Math.min(n.right,Math.max(r,n.left))-n.left,m=Za(d/n.width*c+e,e-1,t+1),g=Math.round(m);return i&&e<=m&&m<=t?(a=m,s=e,f=t,l=o,u.fold(function(){var n=a-s,e=Math.round(n/l)*l;return Za(s+e,s-1,f+1)},function(n){var e=(a-n)%l,t=Math.round(e/l),r=Math.floor((a-n)/l),o=Math.floor((f-n)/l),i=n+Math.min(o,r+t)*l;return Math.max(n,i)})):g},es="slider.change.value",ts=Gn.detect().deviceType.isTouch(),rs=function(n){return function(n){var e=n.event().raw();if(ts){var t=e;return t.touches!==undefined&&1===t.touches.length?V.some(t.touches[0]).map(function(n){return Qa(n.clientX,n.clientY)}):V.none()}var r=e;return r.clientX!==undefined?V.some(r).map(function(n){return Qa(n.clientX,n.clientY)}):V.none()}(n).map(function(n){return n.left()})},os=function(n,e){ie(n,es,{value:e})},is=function(i,u,c,n){return rs(n).map(function(n){var e,t,r,o;return e=i,r=n,o=ns(c,(t=u).min(),t.max(),r,t.stepSize(),t.snapToGrid(),t.snapStart()),os(e,o),n})},us=function(n,e){var t,r,o,i,u=(t=e.value().get(),r=e.min(),o=e.max(),i=e.stepSize(),t<r?t:o<t?o:t===r?r-1:Math.max(r,t-i));os(n,u)},cs=function(n,e){var t,r,o,i,u=(t=e.value().get(),r=e.min(),o=e.max(),i=e.stepSize(),o<t?t:t<r?r:t===o?o+1:Math.min(o,t+i));os(n,u)},as=Gn.detect().deviceType.isTouch(),ss=function(n,r){return ma({name:n+"-edge",overrides:function(n){var e=mr([pr(P(),r,[n])]),t=mr([pr(G(),r,[n]),pr(_(),function(n,e){e.mouseIsDown().get()&&r(n,e)},[n])]);return{events:as?e:t}}})},fs=[ss("left",function(n,e){os(n,e.min()-1)}),ss("right",function(n,e){os(n,e.max()+1)}),da({name:"thumb",defaults:M({dom:{styles:{position:"absolute"}}}),overrides:function(n){return{events:mr([br(P(),n,"spectrum"),br($(),n,"spectrum"),br(W(),n,"spectrum")])}}}),da({schema:[fr("mouseIsDown",function(){return mo(!1)})],name:"spectrum",overrides:function(r){var t=function(n,e){var t=n.element().dom().getBoundingClientRect();is(n,r,t,e)},n=mr([vr(P(),t),vr($(),t)]),e=mr([vr(G(),t),vr(_(),function(n,e){r.mouseIsDown().get()&&t(n,e)})]);return{behaviours:$r(as?[]:[Pc.config({mode:"special",onLeft:function(n){return us(n,r),V.some(!0)},onRight:function(n){return cs(n,r),V.some(!0)}}),xi.config({})]),events:as?n:e}}})],ls=function(n,e,t){e.store().manager().onLoad(n,e,t)},ds=function(n,e,t){e.store().manager().onUnload(n,e,t)},ms=Object.freeze({onLoad:ls,onUnload:ds,setValue:function(n,e,t,r){e.store().manager().setValue(n,e,t,r)},getValue:function(n,e,t){return e.store().manager().getValue(n,e,t)}}),gs=Object.freeze({events:function(t,r){var n=t.resetOnDom()?[xr(function(n,e){ls(n,t,r)}),wr(function(n,e){ds(n,t,r)})]:[Mr(t,r,ls)];return mr(n)}}),vs=function(){var n=mo(null);return Pr({set:n.set,get:n.get,isNotSet:function(){return null===n.get()},clear:function(){n.set(null)},readState:function(){return{mode:"memory",value:n.get()}}})},ps=function(){var n=mo({});return Pr({readState:function(){return{mode:"dataset",dataset:n.get()}},set:n.set,get:n.get})},hs=Object.freeze({memory:vs,dataset:ps,manual:function(){return Pr({readState:function(){}})},init:function(n){return n.store().manager().state(n)}}),bs=function(n,e,t,r){e.store().getDataKey(),t.set({}),e.store().setData()(n,r),e.onSetValue()(n,r)},ys=[ir("initialValue"),er("getFallbackEntry"),er("getDataKey"),er("setData"),Ko("manager",{setValue:bs,getValue:function(n,e,t){var r=e.store().getDataKey()(n),o=t.get();return wt(o,r).fold(function(){return e.store().getFallbackEntry()(r)},function(n){return n})},onLoad:function(e,t,r){t.store().initialValue().each(function(n){bs(e,t,r,n)})},onUnload:function(n,e,t){t.set({})},state:ps})],xs=[er("getValue"),ar("setValue",I),ir("initialValue"),Ko("manager",{setValue:function(n,e,t,r){e.store().setValue()(n,r),e.onSetValue()(n,r)},getValue:function(n,e,t){return e.store().getValue()(n)},onLoad:function(e,t,n){t.store().initialValue().each(function(n){t.store().setValue()(e,n)})},onUnload:I,state:Lr.init})],ws=[ir("initialValue"),Ko("manager",{setValue:function(n,e,t,r){t.set(r),e.onSetValue()(n,r)},getValue:function(n,e,t){return t.get()},onLoad:function(n,e,t){e.store().initialValue().each(function(n){t.isNotSet()&&t.set(n)})},onUnload:function(n,e,t){t.clear()},state:vs})],Ss=[sr("store",{mode:"memory"},Qt("mode",{memory:ws,manual:xs,dataset:ys})),_o("onSetValue"),ar("resetOnDom",!1)],Os=Gr({fields:Ss,name:"representing",active:gs,apis:ms,extra:{setValueFrom:function(n,e){var t=Os.getValue(e);Os.setValue(n,t)}},state:hs}),Ts=Gn.detect().deviceType.isTouch(),ks=[er("min"),er("max"),ar("stepSize",1),ar("onChange",I),ar("onInit",I),ar("onDragStart",I),ar("onDragEnd",I),ar("snapToGrid",!1),ir("snapStart"),er("getInitialValue"),$c("sliderBehaviours",[Pc,Os]),fr("value",function(n){return mo(n.min)})].concat(Ts?[]:[fr("mouseIsDown",function(){return mo(!1)})]),Cs=Mi("width",function(n){return n.dom().offsetWidth}),Es=function(n,e){Cs.set(n,e)},Ds=function(n){return Cs.get(n)},Is=Gn.detect().deviceType.isTouch(),Ms=$a({name:"Slider",configFields:ks,partFields:fs,factory:function(a,n,e,t){var s=a.max()-a.min(),f=function(n){var e=n.element().dom().getBoundingClientRect();return(e.left+e.right)/2},o=function(n){return Sa(n,a,"thumb")},i=function(n){var e,t,r,o,i=Sa(n,a,"spectrum").element().dom().getBoundingClientRect(),u=n.element().dom().getBoundingClientRect(),c=(e=n,t=i,(o=(r=a).value().get())<r.min()?wa(e,r,"left-edge").fold(function(){return 0},function(n){return f(n)-t.left}):o>r.max()?wa(e,r,"right-edge").fold(function(){return t.width},function(n){return f(n)-t.left}):(r.value().get()-r.min())/s*t.width);return i.left-u.left+c},u=function(n){var e=i(n),t=o(n),r=Ds(t.element())/2;Oi(t.element(),"left",e-r+"px")},r=function(n,e){var t=a.value().get(),r=o(n);return t!==e||Ei(r.element(),"left").isNone()?(a.value().set(e),u(n),a.onChange()(n,r,e),V.some(!0)):V.none()},c=Is?[vr(P(),function(n,e){a.onDragStart()(n,o(n))}),vr(W(),function(n,e){a.onDragEnd()(n,o(n))})]:[vr(G(),function(n,e){e.stop(),a.onDragStart()(n,o(n)),a.mouseIsDown().set(!0)}),vr(U(),function(n,e){a.onDragEnd()(n,o(n)),a.mouseIsDown().set(!1)})];return{uid:a.uid(),dom:a.dom(),components:n,behaviours:C($r(In([Is?[]:[Pc.config({mode:"special",focusIn:function(n){return wa(n,a,"spectrum").map(Pc.focusIn).map(M(!0))}})],[Os.config({store:{mode:"manual",getValue:function(n){return a.value().get()}}})]])),Wc(a.sliderBehaviours())),events:mr([vr(es,function(n,e){r(n,e.event().value())}),xr(function(n,e){a.value().set(a.getInitialValue()());var t=o(n);u(n),a.onInit()(n,t,a.value().get())})].concat(c)),apis:{resetToMin:function(n){r(n,a.min())},resetToMax:function(n){r(n,a.max())},refresh:u},domModification:{styles:{position:"relative"}}}},apis:{resetToMin:function(n,e){n.resetToMin(e)},resetToMax:function(n,e){n.resetToMax(e)},refresh:function(n,e){n.refresh(e)}}}),As=function(e,t,r){return Xa.forToolbar(t,function(){var n=r();e.setContextToolbar([{label:t+" group",items:n}])},{})},Bs=function(n){return[(o=n,i=function(n){return n<0?"black":360<n?"white":"hsl("+n+", 100%, 50%)"},Ms.sketch({dom:Ua('<div class="${prefix}-slider ${prefix}-hue-slider-container"></div>'),components:[Ms.parts()["left-edge"](qa('<div class="${prefix}-hue-slider-black"></div>')),Ms.parts().spectrum({dom:Ua('<div class="${prefix}-slider-gradient-container"></div>'),components:[qa('<div class="${prefix}-slider-gradient"></div>')],behaviours:$r([fi.config({toggleClass:vi.resolve("thumb-active")})])}),Ms.parts()["right-edge"](qa('<div class="${prefix}-hue-slider-white"></div>')),Ms.parts().thumb({dom:Ua('<div class="${prefix}-slider-thumb"></div>'),behaviours:$r([fi.config({toggleClass:vi.resolve("thumb-active")})])})],onChange:function(n,e,t){var r=i(t);Oi(e.element(),"background-color",r),o.onChange(n,e,r)},onDragStart:function(n,e){fi.on(e)},onDragEnd:function(n,e){fi.off(e)},onInit:function(n,e,t){var r=i(t);Oi(e.element(),"background-color",r)},stepSize:10,min:0,max:360,getInitialValue:o.getInitialValue,sliderBehaviours:$r([di(Ms.refresh)])}))];var o,i},Rs=function(n,r){var e={onChange:function(n,e,t){r.undoManager.transact(function(){r.formatter.apply("forecolor",{value:t}),r.nodeChanged()})},getInitialValue:function(){return-1}};return As(n,"color",function(){return Bs(e)})},Fs=Ht([er("getInitialValue"),er("onChange"),er("category"),er("sizes")]),Vs=function(n){var o=Kt("SizeSlider",Fs,n);return Ms.sketch({dom:{tag:"div",classes:[vi.resolve("slider-"+o.category+"-size-container"),vi.resolve("slider"),vi.resolve("slider-size-container")]},onChange:function(n,e,t){var r;0<=(r=t)&&r<o.sizes.length&&o.onChange(t)},onDragStart:function(n,e){fi.on(e)},onDragEnd:function(n,e){fi.off(e)},min:0,max:o.sizes.length-1,stepSize:1,getInitialValue:o.getInitialValue,snapToGrid:!0,sliderBehaviours:$r([di(Ms.refresh)]),components:[Ms.parts().spectrum({dom:Ua('<div class="${prefix}-slider-size-container"></div>'),components:[qa('<div class="${prefix}-slider-size-line"></div>')]}),Ms.parts().thumb({dom:Ua('<div class="${prefix}-slider-thumb"></div>'),behaviours:$r([fi.config({toggleClass:vi.resolve("thumb-active")})])})]})},Ns=["9px","10px","11px","12px","14px","16px","18px","20px","24px","32px","36px"],Hs=function(n){var e,t,r=n.selection.getStart(),o=fe.fromDom(r),i=fe.fromDom(n.getBody()),u=(e=function(n){return He(i,n)},(pe(t=o)?V.some(t):Le(t)).map(function(n){return po(n,function(n){return Ei(n,"font-size").isSome()},e).bind(function(n){return Ei(n,"font-size")}).getOrThunk(function(){return ki(n,"font-size")})}).getOr(""));return kn(Ns,function(n){return u===n}).getOr("medium")},zs={candidates:M(Ns),get:function(n){var e,t=Hs(n);return(e=t,Cn(Ns,function(n){return n===e})).getOr(2)},apply:function(r,n){var e;(e=n,V.from(Ns[e])).each(function(n){var e,t;t=n,Hs(e=r)!==t&&e.execCommand("fontSize",!1,t)})}},js=zs.candidates(),Ls=function(n){return[qa('<span class="${prefix}-toolbar-button ${prefix}-icon-small-font ${prefix}-icon"></span>'),(e=n,Vs({onChange:e.onChange,sizes:js,category:"font",getInitialValue:e.getInitialValue})),qa('<span class="${prefix}-toolbar-button ${prefix}-icon-large-font ${prefix}-icon"></span>')];var e},Ps=function(n){var e=n.uid!==undefined&&kt(n,"uid")?n.uid:Va("memento");return{get:function(n){return n.getSystem().getByUid(e).getOrDie()},getOpt:function(n){return n.getSystem().getByUid(e).fold(V.none,V.some)},asSpec:function(){return C(n,{uid:e})}}},$s=window.Promise?window.Promise:function(){var i=function(n){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof n)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],f(n,r(o,this),r(c,this))},n=i.immediateFn||"function"==typeof window.setImmediate&&window.setImmediate||function(n){v.setTimeout(n,1)};function r(n,e){return function(){return n.apply(e,arguments)}}var t=Array.isArray||function(n){return"[object Array]"===Object.prototype.toString.call(n)};function u(r){var o=this;null!==this._state?n(function(){var n=o._state?r.onFulfilled:r.onRejected;if(null!==n){var e;try{e=n(o._value)}catch(t){return void r.reject(t)}r.resolve(e)}else(o._state?r.resolve:r.reject)(o._value)}):this._deferreds.push(r)}function o(n){try{if(n===this)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var e=n.then;if("function"==typeof e)return void f(r(e,n),r(o,this),r(c,this))}this._state=!0,this._value=n,a.call(this)}catch(t){c.call(this,t)}}function c(n){this._state=!1,this._value=n,a.call(this)}function a(){for(var n=0,e=this._deferreds;n<e.length;n++){var t=e[n];u.call(this,t)}this._deferreds=[]}function s(n,e,t,r){this.onFulfilled="function"==typeof n?n:null,this.onRejected="function"==typeof e?e:null,this.resolve=t,this.reject=r}function f(n,e,t){var r=!1;try{n(function(n){r||(r=!0,e(n))},function(n){r||(r=!0,t(n))})}catch(o){if(r)return;r=!0,t(o)}}return i.prototype["catch"]=function(n){return this.then(null,n)},i.prototype.then=function(t,r){var o=this;return new i(function(n,e){u.call(o,new s(t,r,n,e))})},i.all=function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];var a=Array.prototype.slice.call(1===n.length&&t(n[0])?n[0]:n);return new i(function(o,i){if(0===a.length)return o([]);var u=a.length;function c(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var t=n.then;if("function"==typeof t)return void t.call(n,function(n){c(e,n)},i)}a[e]=n,0==--u&&o(a)}catch(r){i(r)}}for(var n=0;n<a.length;n++)c(n,a[n])})},i.resolve=function(e){return e&&"object"==typeof e&&e.constructor===i?e:new i(function(n){n(e)})},i.reject=function(t){return new i(function(n,e){e(t)})},i.race=function(o){return new i(function(n,e){for(var t=0,r=o;t<r.length;t++)r[t].then(n,e)})},i}();function Ws(t){return new $s(function(n){var e=new(Me.getOrDie("FileReader"));e.onloadend=function(){n(e.result)},e.readAsDataURL(t)})}var Gs,_s,Us,qs,Ys,Ks,Xs,Js,Qs=function(n){return Ws(n).then(function(n){return n.split(",")[1]})},Zs=function(u){var e=Ps({dom:{tag:"input",attributes:{accept:"image/*",type:"file",title:""},styles:{visibility:"hidden",position:"absolute"}},events:mr([yr(J()),vr(X(),function(n,e){var t,r,o;(t=e,r=t.event(),o=r.raw().target.files||r.raw().dataTransfer.files,V.from(o[0])).each(function(n){var o,i;o=u,Qs(i=n).then(function(r){o.undoManager.transact(function(){var n=o.editorUpload.blobCache,e=n.create(Zc("mceu"),i,r);n.add(e);var t=o.dom.createHTML("img",{src:e.blobUri()});o.insertContent(t)})})})})])});return Wa.sketch({dom:Ua('<span class="${prefix}-toolbar-button ${prefix}-icon-image ${prefix}-icon"></span>'),components:[e.asSpec()],action:function(n){e.get(n).element().dom().click()}})},nf=function(n){return n.dom().textContent},ef=function(n){return 0<n.length},tf=function(n){return n===undefined||null===n?"":n},rf=function(e,t,n){return n.text.filter(ef).fold(function(){return Xr(n=e,"href")===nf(n)?V.some(t):V.none();var n},V.some)},of=function(n){var e=fe.fromDom(n.selection.getStart());return Pi(e,"a")},uf={getInfo:function(n){return of(n).fold(function(){return{url:"",text:n.selection.getContent({format:"text"}),title:"",target:"",link:V.none()}},function(n){return t=nf(e=n),r=Xr(e,"href"),o=Xr(e,"title"),i=Xr(e,"target"),{url:tf(r),text:t!==r?tf(t):"",title:tf(o),target:tf(i),link:V.some(e)};var e,t,r,o,i})},applyInfo:function(o,i){i.url.filter(ef).fold(function(){var e;e=o,i.link.bind(h).each(function(n){e.execCommand("unlink")})},function(e){var n,t,r=(n=i,(t={}).href=e,n.title.filter(ef).each(function(n){t.title=n}),n.target.filter(ef).each(function(n){t.target=n}),t);i.link.bind(h).fold(function(){var n=i.text.filter(ef).getOr(e);o.insertContent(o.dom.createHTML("a",r,o.dom.encode(n)))},function(t){var n=rf(t,e,i);Kr(t,r),n.each(function(n){var e;e=n,t.dom().textContent=e})})})},query:of},cf=Gn.detect(),af=function(n,e){var t=e.selection.getRng();n(),e.selection.setRng(t)},sf=function(n,e){(cf.os.isAndroid()?af:s)(e,n)},ff=function(n,e){var t,r;return{key:n,value:{config:{},me:(t=n,r=mr(e),Gr({fields:[er("enabled")],name:t,active:{events:M(r)}})),configAsRaw:M({}),initialConfig:{},state:Lr}}},lf=Object.freeze({getCurrent:function(n,e,t){return e.find()(n)}}),df=[er("find")],mf=Gr({fields:df,name:"composing",apis:lf}),gf=Pa({name:"Container",factory:function(n){return{uid:n.uid(),dom:C({tag:"div",attributes:{role:"presentation"}},n.dom()),components:n.components(),behaviours:Wc(n.containerBehaviours()),events:n.events(),domModification:n.domModification(),eventOrder:n.eventOrder()}},configFields:[ar("components",[]),$c("containerBehaviours",[]),ar("events",{}),ar("domModification",{}),ar("eventOrder",{})]}),vf=Pa({name:"DataField",factory:function(t){return{uid:t.uid(),dom:t.dom(),behaviours:C($r([Os.config({store:{mode:"memory",initialValue:t.getInitialValue()()}}),mf.config({find:V.some})]),Wc(t.dataBehaviours())),events:mr([xr(function(n,e){Os.setValue(n,t.getInitialValue()())})])}},configFields:[er("uid"),er("dom"),er("getInitialValue"),$c("dataBehaviours",[Os,mf])]}),pf=function(n){return n.dom().value},hf=function(n,e){if(e===undefined)throw new Error("Value.set was undefined");n.dom().value=e},bf=M([ir("data"),ar("inputAttributes",{}),ar("inputStyles",{}),ar("type","input"),ar("tag","input"),ar("inputClasses",[]),_o("onSetValue"),ar("styles",{}),ir("placeholder"),ar("eventOrder",{}),$c("inputBehaviours",[Os,xi]),ar("selectOnFocus",!0)]),yf=function(n){return C($r([Os.config({store:{mode:"manual",initialValue:n.data().getOr(undefined),getValue:function(n){return pf(n.element())},setValue:function(n,e){pf(n.element())!==e&&hf(n.element(),e)}},onSetValue:n.onSetValue()})]),(e=n,$r([xi.config({onFocus:!1===e.selectOnFocus()?I:function(n){var e=n.element(),t=pf(e);e.dom().setSelectionRange(0,t.length)}})])),Wc(n.inputBehaviours()));var e},xf=Pa({name:"Input",configFields:bf(),factory:function(n,e){return{uid:n.uid(),dom:(t=n,{tag:t.tag(),attributes:C(Ot([{key:"type",value:t.type()}].concat(t.placeholder().map(function(n){return{key:"placeholder",value:n}}).toArray())),t.inputAttributes()),styles:t.inputStyles(),classes:t.inputClasses()}),components:[],behaviours:yf(n),eventOrder:n.eventOrder()};var t}}),wf=Object.freeze({exhibit:function(n,e){return Dr({attributes:Ot([{key:e.tabAttr(),value:"true"}])})}}),Sf=[ar("tabAttr","data-alloy-tabstop")],Of=Gr({fields:Sf,name:"tabstopping",active:wf}),Tf=function(n,e){var t=Ps(xf.sketch({placeholder:e,onSetValue:function(n,e){oe(n,K())},inputBehaviours:$r([mf.config({find:V.some}),Of.config({}),Pc.config({mode:"execution"})]),selectOnFocus:!1})),r=Ps(Wa.sketch({dom:Ua('<button class="${prefix}-input-container-x ${prefix}-icon-cancel-circle ${prefix}-icon"></button>'),action:function(n){var e=t.get(n);Os.setValue(e,"")}}));return{name:n,spec:gf.sketch({dom:Ua('<div class="${prefix}-input-container"></div>'),components:[t.asSpec(),r.asSpec()],containerBehaviours:$r([fi.config({toggleClass:vi.resolve("input-container-empty")}),mf.config({find:function(n){return V.some(t.get(n))}}),ff("input-clearing",[vr(K(),function(n){var e=t.get(n);(0<Os.getValue(e).length?fi.off:fi.on)(n)})])])})}},kf=["input","button","textarea"],Cf=function(n,e,t){e.disabled()&&Bf(n,e)},Ef=function(n){return yn(kf,ge(n.element()))},Df=function(n){Yr(n.element(),"disabled","disabled")},If=function(n){Qr(n.element(),"disabled")},Mf=function(n){Yr(n.element(),"aria-disabled","true")},Af=function(n){Yr(n.element(),"aria-disabled","false")},Bf=function(e,n,t){n.disableClass().each(function(n){oo(e.element(),n)}),(Ef(e)?Df:Mf)(e)},Rf=function(n){return Ef(n)?Jr(n.element(),"disabled"):"true"===Xr(n.element(),"aria-disabled")},Ff=Object.freeze({enable:function(e,n,t){n.disableClass().each(function(n){io(e.element(),n)}),(Ef(e)?If:Af)(e)},disable:Bf,isDisabled:Rf,onLoad:Cf}),Vf=Object.freeze({exhibit:function(n,e,t){return Dr({classes:e.disabled()?e.disableClass().map(Rn).getOr([]):[]})},events:function(n,e){return mr([gr(Kn(),function(n,e){return Rf(n)}),Mr(n,e,Cf)])}}),Nf=[ar("disabled",!1),ir("disableClass")],Hf=Gr({fields:Nf,name:"disabling",active:Vf,apis:Ff}),zf=[$c("formBehaviours",[Os])],jf=function(n){return"<alloy.field."+n+">"},Lf=function(o,n,e){return C({"debug.sketcher":{Form:e},uid:o.uid(),dom:o.dom(),components:n,behaviours:C($r([Os.config({store:{mode:"manual",getValue:function(n){var e,t,r=(e=o,t=n.getSystem(),z(e.partUids(),function(n,e){return M(t.getByUid(n))}));return z(r,function(n,e){return n().bind(mf.getCurrent).map(Os.getValue)})},setValue:function(t,n){H(n,function(e,n){wa(t,o,n).each(function(n){mf.getCurrent(n).each(function(n){Os.setValue(n,e)})})})}}})]),Wc(o.formBehaviours())),apis:{getField:function(n,e){return wa(n,o,e).bind(mf.getCurrent)}}})},Pf=(Da(function(n,e,t){return n.getField(e,t)}),function(n){var i,e=(i=[],{field:function(n,e){return i.push(n),t="form",r=jf(n),o=e,{uiType:Jc(),owner:t,name:r,config:o,validated:{}};var t,r,o},record:function(){return i}}),t=n(e),r=e.record(),o=xn(r,function(n){return da({name:n,pname:jf(n)})});return Ha("form",zf,o,Lf,t)}),$f=function(){var e=mo(V.none()),t=function(){e.get().each(function(n){n.destroy()})};return{clear:function(){t(),e.set(V.none())},isSet:function(){return e.get().isSome()},set:function(n){t(),e.set(V.some(n))},run:function(n){e.get().each(n)}}},Wf=function(){var e=mo(V.none());return{clear:function(){e.set(V.none())},set:function(n){e.set(V.some(n))},isSet:function(){return e.get().isSome()},on:function(n){e.get().each(n)}}},Gf=function(n){return{xValue:n,points:[]}},_f=function(n,e){if(e===n.xValue)return n;var t=0<e-n.xValue?1:-1,r={direction:t,xValue:e};return{xValue:e,points:(0===n.points.length?[]:n.points[n.points.length-1].direction===t?n.points.slice(0,n.points.length-1):n.points).concat([r])}},Uf=function(n){if(0===n.points.length)return 0;var e=n.points[0].direction,t=n.points[n.points.length-1].direction;return-1===e&&-1===t?-1:1===e&&1===t?1:0},qf=function(n){var r="navigateEvent",e=zt([er("fields"),ar("maxFieldIndex",n.fields.length-1),er("onExecute"),er("getInitialValue"),fr("state",function(){return{dialogSwipeState:Wf(),currentScreen:mo(0)}})]),u=Kt("SerialisedDialog",e,n),o=function(e,n,t){return Wa.sketch({dom:Ua('<span class="${prefix}-icon-'+n+' ${prefix}-icon"></span>'),action:function(n){ie(n,r,{direction:e})},buttonBehaviours:$r([Hf.config({disableClass:vi.resolve("toolbar-navigation-disabled"),disabled:!t})])})},i=function(n,o){var i=Hi(n.element(),"."+vi.resolve("serialised-dialog-screen"));Li(n.element(),"."+vi.resolve("serialised-dialog-chain")).each(function(r){0<=u.state.currentScreen.get()+o&&u.state.currentScreen.get()+o<i.length&&(Ei(r,"left").each(function(n){var e=parseInt(n,10),t=Ds(i[0]);Oi(r,"left",e-o*t+"px")}),u.state.currentScreen.set(u.state.currentScreen.get()+o))})},c=function(r){var n=Hi(r.element(),"input");V.from(n[u.state.currentScreen.get()]).each(function(n){r.getSystem().getByDom(n).each(function(n){var e,t;e=r,t=n.element(),e.getSystem().triggerFocus(t,e.element())})});var e=s.get(r);su.highlightAt(e,u.state.currentScreen.get())},a=Ps(Pf(function(t){return{dom:Ua('<div class="${prefix}-serialised-dialog"></div>'),components:[gf.sketch({dom:Ua('<div class="${prefix}-serialised-dialog-chain" style="left: 0px; position: absolute;"></div>'),components:xn(u.fields,function(n,e){return e<=u.maxFieldIndex?gf.sketch({dom:Ua('<div class="${prefix}-serialised-dialog-screen"></div>'),components:In([[o(-1,"previous",0<e)],[t.field(n.name,n.spec)],[o(1,"next",e<u.maxFieldIndex)]])}):t.field(n.name,n.spec)})})],formBehaviours:$r([di(function(n,e){var t;t=e,Li(n.element(),"."+vi.resolve("serialised-dialog-chain")).each(function(n){Oi(n,"left",-u.state.currentScreen.get()*t.width+"px")})}),Pc.config({mode:"special",focusIn:function(n){c(n)},onTab:function(n){return i(n,1),V.some(!0)},onShiftTab:function(n){return i(n,-1),V.some(!0)}}),ff("form-events",[xr(function(e,n){u.state.currentScreen.set(0),u.state.dialogSwipeState.clear();var t=s.get(e);su.highlightFirst(t),u.getInitialValue(e).each(function(n){Os.setValue(e,n)})}),Or(u.onExecute),vr(Q(),function(n,e){"left"===e.event().raw().propertyName&&c(n)}),vr(r,function(n,e){var t=e.event().direction();i(n,t)})])])}})),s=Ps({dom:Ua('<div class="${prefix}-dot-container"></div>'),behaviours:$r([su.config({highlightClass:vi.resolve("dot-active"),itemClass:vi.resolve("dot-item")})]),components:Mn(u.fields,function(n,e){return e<=u.maxFieldIndex?[qa('<div class="${prefix}-dot-item ${prefix}-icon-full-dot ${prefix}-icon"></div>')]:[]})});return{dom:Ua('<div class="${prefix}-serializer-wrapper"></div>'),components:[a.asSpec(),s.asSpec()],behaviours:$r([Pc.config({mode:"special",focusIn:function(n){var e=a.get(n);Pc.focusIn(e)}}),ff("serializer-wrapper-events",[vr(P(),function(n,e){var t=e.event();u.state.dialogSwipeState.set(Gf(t.touches[0].clientX))}),vr($(),function(n,e){var t=e.event();u.state.dialogSwipeState.on(function(n){e.event().prevent(),u.state.dialogSwipeState.set(_f(n,t.raw().touches[0].clientX))})}),vr(W(),function(r){u.state.dialogSwipeState.on(function(n){var e=a.get(r),t=-1*Uf(n);i(e,t)})})])])}},Yf=nn(function(t,r){return[{label:"the link group",items:[qf({fields:[Tf("url","Type or paste URL"),Tf("text","Link text"),Tf("title","Link title"),Tf("target","Link target"),(n="link",{name:n,spec:vf.sketch({dom:{tag:"span",styles:{display:"none"}},getInitialValue:function(){return V.none()}})})],maxFieldIndex:["url","text","title","target"].length-1,getInitialValue:function(){return V.some(uf.getInfo(r))},onExecute:function(n){var e=Os.getValue(n);uf.applyInfo(r,e),t.restoreToolbar(),r.focus()}})]}];var n}),Kf=[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],Xf=mr([(Gs=Un(),_s=function(n,e){var t,r,o=e.event().originator(),i=e.event().target();return r=i,!(He(t=o,n.element())&&!He(t,r)&&(v.console.warn(Un()+" did not get interpreted by the desired target. \nOriginator: "+Mo(o)+"\nTarget: "+Mo(i)+"\nCheck the "+Un()+" event handlers"),1))},{key:Gs,value:lr({can:_s})})]),Jf=Object.freeze({events:Xf}),Qf=h,Zf=jr(["debugInfo","triggerFocus","triggerEvent","triggerEscape","addToWorld","removeFromWorld","addToGui","removeFromGui","build","getByUid","getByDom","broadcast","broadcastOn","isConnected"]),nl=function(e){var n=function(n){return function(){throw new Error("The component must be in a context to send: "+n+"\n"+Mo(e().element())+" is not in context.")}};return Zf({debugInfo:M("fake"),triggerEvent:n("triggerEvent"),triggerFocus:n("triggerFocus"),triggerEscape:n("triggerEscape"),build:n("build"),addToWorld:n("addToWorld"),removeFromWorld:n("removeFromWorld"),addToGui:n("addToGui"),removeFromGui:n("removeFromGui"),getByUid:n("getByUid"),getByDom:n("getByDom"),broadcast:n("broadcast"),broadcastOn:n("broadcastOn"),isConnected:M(!1)})},el=function(n,o){var i={};return H(n,function(n,r){H(n,function(n,e){var t=xt(e,[])(i);i[e]=t.concat([o(r,n)])})}),i},tl=function(n,e){return 1<n.length?tt.error('Multiple behaviours have tried to change DOM "'+e+'". The guilty behaviours are: '+It(xn(n,function(n){return n.name()}))+". At this stage, this is not supported. Future releases might provide strategies for resolving this."):0===n.length?tt.value({}):tt.value(n[0].modification().fold(function(){return{}},function(n){return St(e,n)}))},rl=function(u,c){return Tn(u,function(n,e){var t=e.modification().getOr({});return n.bind(function(i){var n=L(t,function(n,e){return i[e]!==undefined?(t=c,r=e,o=u,tt.error("Mulitple behaviours have tried to change the _"+r+'_ "'+t+'". The guilty behaviours are: '+It(Mn(o,function(n){return n.modification().getOr({})[r]!==undefined?[n.name()]:[]}),null,2)+". This is not currently supported.")):tt.value(St(e,n));var t,r,o});return Tt(n,i)})},tt.value({})).map(function(n){return St(c,n)})},ol={classes:function(n,e){var t=Mn(n,function(n){return n.modification().getOr([])});return tt.value(St(e,t))},attributes:rl,styles:rl,domChildren:tl,defChildren:tl,innerHtml:tl,value:tl},il=function(n,e){return t=l.apply(undefined,[n.handler].concat(e)),r=n.purpose(),{cHandler:t,purpose:M(r)};var t,r},ul=function(n){return n.cHandler},cl=function(n,e){return{name:M(n),handler:M(e)}},al=function(n,e,t){var r,o,i=C(t,(r=n,o={},wn(e,function(n){o[n.name()]=n.handlers(r)}),o));return el(i,cl)},sl=function(n){var e,i=S(e=n)?{can:M(!0),abort:M(!1),run:e}:e;return function(n,e){for(var t=[],r=2;r<arguments.length;r++)t[r-2]=arguments[r];var o=[n,e].concat(t);i.abort.apply(undefined,o)?e.stop():i.can.apply(undefined,o)&&i.run.apply(undefined,o)}},fl=function(n,e,t){var r,o,i=e[t];return i?function(u,c,n,a){var e=n.slice(0);try{var t=e.sort(function(n,e){var t=n[c](),r=e[c](),o=a.indexOf(t),i=a.indexOf(r);if(-1===o)throw new Error("The ordering for "+u+" does not have an entry for "+t+".\nOrder specified: "+It(a,null,2));if(-1===i)throw new Error("The ordering for "+u+" does not have an entry for "+r+".\nOrder specified: "+It(a,null,2));return o<i?-1:i<o?1:0});return tt.value(t)}catch(r){return tt.error([r])}}("Event: "+t,"name",n,i).map(function(n){var e=xn(n,function(n){return n.handler()});return dr(e)}):(r=t,o=n,tt.error(["The event ("+r+') has more than one behaviour that listens to it.\nWhen this occurs, you must specify an event ordering for the behaviours in your spec (e.g. [ "listing", "toggling" ]).\nThe behaviours that can trigger it are: '+It(xn(o,function(n){return n.name()}),null,2)]))},ll=function(n,i){var e=L(n,function(r,o){return(1===r.length?tt.value(r[0].handler()):fl(r,i,o)).map(function(n){var e=sl(n),t=1<r.length?Sn(i,function(e){return yn(r,function(n){return n.name()===e})}).join(" > "):r[0].name();return St(o,{handler:e,purpose:M(t)})})});return Tt(e,{})},dl=function(n){return qt("custom.definition",Ht([$t("dom","dom",ut(),Ht([er("tag"),ar("styles",{}),ar("classes",[]),ar("attributes",{}),ir("value"),ir("innerHtml")])),er("components"),er("uid"),ar("events",{}),ar("apis",M({})),$t("eventOrder","eventOrder",(e={"alloy.execute":["disabling","alloy.base.behaviour","toggling"],"alloy.focus":["alloy.base.behaviour","focusing","keying"],"alloy.system.init":["alloy.base.behaviour","disabling","toggling","representing"],input:["alloy.base.behaviour","representing","streaming","invalidating"],"alloy.system.detached":["alloy.base.behaviour","representing"]},ot.mergeWithThunk(M(e))),Zt()),ir("domModification"),Xo("originalSpec"),ar("debug.sketcher","unknown")]),n);var e},ml=function(n){var e,t={tag:n.dom().tag(),classes:n.dom().classes(),attributes:C((e=n,St(Aa(),e.uid())),n.dom().attributes()),styles:n.dom().styles(),domChildren:xn(n.components(),function(n){return n.element()})};return Cr(C(t,n.dom().innerHtml().map(function(n){return St("innerHtml",n)}).getOr({}),n.dom().value().map(function(n){return St("value",n)}).getOr({})))},gl=function(e,n){wn(n,function(n){oo(e,n)})},vl=function(e,n){wn(n,function(n){io(e,n)})},pl=function(e){if(e.domChildren().isSome()&&e.defChildren().isSome())throw new Error("Cannot specify children and child specs! Must be one or the other.\nDef: "+(n=Er(e),It(n,null,2)));return e.domChildren().fold(function(){var n=e.defChildren().getOr([]);return xn(n,bl)},function(n){return n});var n},hl=function(n){var e=fe.fromTag(n.tag());Kr(e,n.attributes().getOr({})),gl(e,n.classes().getOr([])),Ti(e,n.styles().getOr({})),Eo(e,n.innerHtml().getOr(""));var t=pl(n);return _e(e,t),n.value().each(function(n){hf(e,n)}),e},bl=function(n){var e=Cr(n);return hl(e)},yl=function(n,e){return t=n,o=xn(r=e,function(n){return ur(n.name(),[er("config"),ar("state",Lr)])}),i=qt("component.behaviours",zt(o),t.behaviours).fold(function(n){throw new Error(Jt(n)+"\nComplete spec:\n"+It(t,null,2))},function(n){return n}),{list:r,data:z(i,function(n){var e=n().map(function(n){return{config:n.config(),state:n.state().init(n.config())}});return function(){return e}})};var t,r,o,i},xl=function(n){var e,t,r=(e=wt(n,"behaviours").getOr({}),t=Sn(N(e),function(n){return e[n]!==undefined}),xn(t,function(n){return e[n].me}));return yl(n,r)},wl=jr(["getSystem","config","hasConfigured","spec","connect","disconnect","element","syncComponents","readState","components","events"]),Sl=function(n,e,t){var r,o,i,u,c=ml(n),a=function(e,n,t,r){var o=C({},n);wn(t,function(n){o[n.name()]=n.exhibit(e,r)});var i=el(o,function(n,e){return{name:function(){return n},modification:e}}),u=z(i,function(n,e){return Mn(n,function(e){return e.modification().fold(function(){return[]},function(n){return[e]})})}),c=L(u,function(e,t){return wt(ol,t).fold(function(){return tt.error("Unknown field type: "+t)},function(n){return n(e,t)})});return Tt(c,{}).map(Dr)}(t,{"alloy.base.modification":(r=n,r.domModification().fold(function(){return Dr({})},Dr))},e,c).getOrDie();return i=a,u=C({tag:(o=c).tag(),classes:i.classes().getOr([]).concat(o.classes().getOr([])),attributes:E(o.attributes().getOr({}),i.attributes().getOr({})),styles:E(o.styles().getOr({}),i.styles().getOr({}))},i.innerHtml().or(o.innerHtml()).map(function(n){return St("innerHtml",n)}).getOr({}),Ir("domChildren",i.domChildren(),o.domChildren()),Ir("defChildren",i.defChildren(),o.defChildren()),i.value().or(o.value()).map(function(n){return St("value",n)}).getOr({})),Cr(u)},Ol=function(n,e,t){var r,o,i,u,c,a,s={"alloy.base.behaviour":(r=n,r.events())};return(o=t,i=n.eventOrder(),u=e,c=s,a=al(o,u,c),ll(a,i)).getOrDie()},Tl=function(n){var e,t,r,o,i,u,c,a,s,f,l,d,m,g,v=Qf(n),p=(e=v,t=xt("components",[])(e),xn(t,El)),h=C(Jf,v,St("components",p));return tt.value((r=h,i=mo(nl(o=function(){return g})),u=Yt(dl(C(r,{behaviours:undefined}))),c=xl(r),a=c.list,s=c.data,f=Sl(u,a,s),l=hl(f),d=Ol(u,a,s),m=mo(u.components()),g=wl({getSystem:i.get,config:function(n){if(n===Ia())return u.apis();if(b(n))throw new Error("Invalid input: only API constant is allowed");var e=s;return(S(e[n.name()])?e[n.name()]:function(){throw new Error("Could not find "+n.name()+" in "+It(r,null,2))})()},hasConfigured:function(n){return S(s[n.name()])},spec:M(r),readState:function(n){return s[n]().map(function(n){return n.state.readState()}).getOr("not enabled")},connect:function(n){i.set(n)},disconnect:function(){i.set(nl(o))},element:M(l),syncComponents:function(){var n=Pe(l),e=Mn(n,function(n){return i.get().getByDom(n).fold(function(){return[]},function(n){return[n]})});m.set(e)},components:m.get,events:M(d)})))},kl=function(n){var e=fe.fromText(n);return Cl({element:e})},Cl=function(n){var t=Xt("external.component",Ht([er("element"),ir("uid")]),n),e=mo(nl());t.uid().each(function(n){var e;e=t.element(),Yr(e,Ra,n)});var r=wl({getSystem:e.get,config:V.none,hasConfigured:M(!1),connect:function(n){e.set(n)},disconnect:function(){e.set(nl(function(){return r}))},element:M(t.element()),spec:M(n),readState:M("No state"),syncComponents:I,components:M([]),events:M({})});return Ea(r)},El=function(e){return(n=e,wt(n,ka)).fold(function(){var n=C({uid:Va("")},e);return Tl(n).getOrDie()},function(n){return n});var n},Dl=Ea,Il="alloy.item-hover",Ml="alloy.item-focus",Al=function(n){(wo(n.element()).isNone()||xi.isFocused(n))&&(xi.isFocused(n)||xi.focus(n),ie(n,Il,{item:n}))},Bl=function(n){ie(n,Ml,{item:n})},Rl=M(Il),Fl=M(Ml),Vl=[er("data"),er("components"),er("dom"),ir("toggling"),ar("itemBehaviours",{}),ar("ignoreFocus",!1),ar("domModification",{}),Ko("builder",function(n){return{dom:C(n.dom(),{attributes:{role:n.toggling().isSome()?"menuitemcheckbox":"menuitem"}}),behaviours:C($r([n.toggling().fold(fi.revoke,function(n){return fi.config(C({aria:{mode:"checked"}},n))}),xi.config({ignore:n.ignoreFocus(),onFocus:function(n){Bl(n)}}),Pc.config({mode:"execution"}),Os.config({store:{mode:"memory",initialValue:n.data()}})]),n.itemBehaviours()),events:mr([(e=Qn(),r=ue,vr(e,function(e,t){var n=t.event();e.getSystem().getByDom(n.target()).each(function(n){r(e,n,t)})})),yr(G()),vr(q(),Al),vr(Xn(),xi.focus)]),components:n.components(),domModification:n.domModification(),eventOrder:n.eventOrder()};var e,r}),ar("eventOrder",{})],Nl=[er("dom"),er("components"),Ko("builder",function(n){return{dom:n.dom(),components:n.components(),events:mr([(e=Xn(),vr(e,function(n,e){e.stop()}))])};var e})],Hl=M([da({name:"widget",overrides:function(e){return{behaviours:$r([Os.config({store:{mode:"manual",getValue:function(n){return e.data()},setValue:function(){}}})])}}})]),zl=[er("uid"),er("data"),er("components"),er("dom"),ar("autofocus",!1),ar("domModification",{}),Ta(Hl()),Ko("builder",function(t){var n=ya(0,t,Hl()),e=xa("item-widget",t,n.internals()),r=function(n){return wa(n,t,"widget").map(function(n){return Pc.focusIn(n),n})},o=function(n,e){return vu(e.event().target())||t.autofocus()&&e.setSource(n.element()),V.none()};return C({dom:t.dom(),components:e,domModification:t.domModification(),events:mr([Or(function(n,e){r(n).each(function(n){e.stop()})}),vr(q(),Al),vr(Xn(),function(n,e){t.autofocus()?r(n):xi.focus(n)})]),behaviours:$r([Os.config({store:{mode:"memory",initialValue:t.data()}}),xi.config({onFocus:function(n){Bl(n)}}),Pc.config({mode:"special",focusIn:t.autofocus()?function(n){r(n)}:Ur(),onLeft:o,onRight:o,onEscape:function(n,e){return xi.isFocused(n)||t.autofocus()?(t.autofocus()&&e.setSource(n.element()),V.none()):(xi.focus(n),V.some(!0))}})])})})],jl=Qt("type",{widget:zl,item:Vl,separator:Nl}),Ll=M([ga({factory:{sketch:function(n){var e=Xt("menu.spec item",jl,n);return e.builder()(e)}},name:"items",unit:"item",defaults:function(n,e){var t=Va("");return C({uid:t},e)},overrides:function(n,e){return{type:e.type,ignoreFocus:n.fakeFocus(),domModification:{classes:[n.markers().item()]}}}})]),Pl=M([er("value"),er("items"),er("dom"),er("components"),ar("eventOrder",{}),$c("menuBehaviours",[su,Os,mf,Pc]),sr("movement",{mode:"menu",moveOnTab:!0},Qt("mode",{grid:[Jo(),Ko("config",function(n,e){return{mode:"flatgrid",selector:"."+n.markers().item(),initSize:{numColumns:e.initSize().numColumns(),numRows:e.initSize().numRows()},focusManager:n.focusManager()}})],menu:[ar("moveOnTab",!0),Ko("config",function(n,e){return{mode:"menu",selector:"."+n.markers().item(),moveOnTab:e.moveOnTab(),focusManager:n.focusManager()}})]})),tr("markers",$o()),ar("fakeFocus",!1),ar("focusManager",fu()),_o("onHighlight")]),$l=M("alloy.menu-focus"),Wl=$a({name:"Menu",configFields:Pl(),partFields:Ll(),factory:function(n,e,t,r){return C({dom:C(n.dom(),{attributes:{role:"menu"}}),uid:n.uid(),behaviours:C($r([su.config({highlightClass:n.markers().selectedItem(),itemClass:n.markers().item(),onHighlight:n.onHighlight()}),Os.config({store:{mode:"memory",initialValue:n.value()}}),mf.config({find:V.some}),Pc.config(n.movement().config()(n,n.movement()))]),Wc(n.menuBehaviours())),events:mr([vr(Fl(),function(e,t){var n=t.event();e.getSystem().getByDom(n.target()).each(function(n){su.highlight(e,n),t.stop(),ie(e,$l(),{menu:e,item:n})})}),vr(Rl(),function(n,e){var t=e.event().item();su.highlight(n,t)})]),components:e,eventOrder:n.eventOrder()})}}),Gl=function(n,e,t,r){var o=n.getSystem().build(r);Je(n,o,t)},_l=function(n,e){return n.components()},Ul=Gr({fields:[],name:"replacing",apis:Object.freeze({append:function(n,e,t,r){Gl(n,0,Ge,r)},prepend:function(n,e,t,r){Gl(n,0,We,r)},remove:function(n,e,t,r){var o=_l(n);kn(o,function(n){return He(r.element(),n.element())}).each(Ze)},set:function(e,n,t,r){var o,i,u,c,a,s;i=(o=e).components(),wn(i,Qe),Ue(o.element()),o.syncComponents(),u=function(){var n=xn(r,e.getSystem().build);wn(n,function(n){Xe(e,n)})},c=e.element(),a=ze(c),s=xo(a).bind(function(e){var n=function(n){return He(e,n)};return n(c)?V.some(c):ho(c,n)}),u(c),s.each(function(e){xo(a).filter(function(n){return He(n,e)}).fold(function(){bo(e)},I)})},contents:_l})}),ql=function(t,r,o,n){return wt(o,n).bind(function(n){return wt(t,n).bind(function(n){var e=ql(t,r,o,n);return V.some([n].concat(e))})}).getOr([])},Yl=function(n,e){var t={};H(n,function(n,e){wn(n,function(n){t[n]=e})});var r=e,o=j(e,function(n,e){return{k:n,v:e}}),i=z(o,function(n,e){return[e].concat(ql(t,r,o,e))});return z(t,function(n){return wt(i,n).getOr([n])})},Kl=function(){var i=mo({}),u=mo({}),c=mo({}),a=mo(V.none()),s=mo({}),n=function(n){return wt(u.get(),n)};return{setContents:function(n,e,t,r){a.set(V.some(n)),i.set(t),u.set(e),s.set(r);var o=Yl(r,t);c.set(o)},expand:function(t){return wt(i.get(),t).map(function(n){var e=wt(c.get(),t).getOr([]);return[n].concat(e)})},refresh:function(n){return wt(c.get(),n)},collapse:function(n){return wt(c.get(),n).bind(function(n){return 1<n.length?V.some(n.slice(1)):V.none()})},lookupMenu:n,otherMenus:function(n){var e,t,r=s.get();return e=N(r),t=n,Sn(e,function(n){return!yn(t,n)})},getPrimary:function(){return a.get().bind(n)},getMenus:function(){return u.get()},clear:function(){i.set({}),u.set({}),c.set({}),a.set(V.none())},isClear:function(){return a.get().isNone()}}},Xl=M("collapse-item"),Jl=Pa({name:"TieredMenu",configFields:[Yo("onExecute"),Yo("onEscape"),qo("onOpenMenu"),qo("onOpenSubmenu"),_o("onCollapseMenu"),ar("openImmediately",!0),or("data",[er("primary"),er("menus"),er("expansions")]),ar("fakeFocus",!1),_o("onHighlight"),_o("onHover"),or("markers",[er("backgroundMenu")].concat(Lo()).concat(Po())),er("dom"),ar("navigateOnHover",!0),ar("stayInDom",!1),$c("tmenuBehaviours",[Pc,su,mf,Ul]),ar("eventOrder",{})],apis:{collapseMenu:function(n,e){n.collapseMenu(e)}},factory:function(u,o){var i=function(r,n){return z(n,function(n,e){var t=Wl.sketch(C(n,{value:e,items:n.items,markers:ht(o.markers,["item","selectedItem"]),fakeFocus:u.fakeFocus(),onHighlight:u.onHighlight(),focusManager:u.fakeFocus()?{get:function(n){return su.getHighlighted(n).map(function(n){return n.element()})},set:function(e,n){e.getSystem().getByDom(n).fold(I,function(n){su.highlight(e,n)})}}:fu()}));return r.getSystem().build(t)})},c=Kl(),a=function(n){return Os.getValue(n).value},s=function(n){return z(u.data().menus(),function(n,e){return Mn(n.items,function(n){return"separator"===n.type?[]:[n.data.value]})})},f=function(e,n){su.highlight(e,n),su.getHighlighted(n).orThunk(function(){return su.getFirst(n)}).each(function(n){ce(e,n.element(),Xn())})},l=function(n,e){return Bo(xn(e,n.lookupMenu))},d=function(r,o,i){return V.from(i[0]).bind(o.lookupMenu).map(function(n){var e=l(o,i.slice(1));wn(e,function(n){oo(n.element(),u.markers().backgroundMenu())}),be(n.element())||Ul.append(r,Dl(n)),vl(n.element(),[u.markers().backgroundMenu()]),f(r,n);var t=l(o,o.otherMenus(i));return wn(t,function(n){vl(n.element(),[u.markers().backgroundMenu()]),u.stayInDom()||Ul.remove(r,n)}),n})},m=function(e,t){var n=a(t);return c.expand(n).bind(function(n){return V.from(n[0]).bind(c.lookupMenu).each(function(n){be(n.element())||Ul.append(e,Dl(n)),u.onOpenSubmenu()(e,t,n),su.highlightFirst(n)}),d(e,c,n)})},r=function(e,t){var n=a(t);return c.collapse(n).bind(function(n){return d(e,c,n).map(function(n){return u.onCollapseMenu()(e,t,n),n})})},n=function(t){return function(e,n){return Pi(n.getSource(),"."+u.markers().item()).bind(function(n){return e.getSystem().getByDom(n).toOption().bind(function(n){return t(e,n).map(function(){return!0})})})}},e=mr([vr($l(),function(n,e){var t=e.event().menu();su.highlight(n,t)}),Or(function(e,n){var t=n.event().target();e.getSystem().getByDom(t).each(function(n){0===a(n).indexOf("collapse-item")&&r(e,n),m(e,n).fold(function(){u.onExecute()(e,n)},function(){})})}),xr(function(e,n){var t,r,o;(t=e,r=i(t,u.data().menus()),o=s(),c.setContents(u.data().primary(),r,u.data().expansions(),o),c.getPrimary()).each(function(n){Ul.append(e,Dl(n)),u.openImmediately()&&(f(e,n),u.onOpenMenu()(e,n))})})].concat(u.navigateOnHover()?[vr(Rl(),function(n,e){var t,r,o=e.event().item();t=n,r=a(o),c.refresh(r).bind(function(n){return d(t,c,n)}),m(n,o),u.onHover()(n,o)})]:[]));return{uid:u.uid(),dom:u.dom(),behaviours:C($r([Pc.config({mode:"special",onRight:n(function(n,e){return vu(e.element())?V.none():m(n,e)}),onLeft:n(function(n,e){return vu(e.element())?V.none():r(n,e)}),onEscape:n(function(n,e){return r(n,e).orThunk(function(){return u.onEscape()(n,e).map(function(){return n})})}),focusIn:function(e,n){c.getPrimary().each(function(n){ce(e,n.element(),Xn())})}}),su.config({highlightClass:u.markers().selectedMenu(),itemClass:u.markers().menu()}),mf.config({find:function(n){return su.getHighlighted(n)}}),Ul.config({})]),Wc(u.tmenuBehaviours())),eventOrder:u.eventOrder(),apis:{collapseMenu:function(e){su.getHighlighted(e).each(function(n){su.getHighlighted(n).each(function(n){r(e,n)})})}},events:e}},extraApis:{tieredData:function(n,e,t){return{primary:n,menus:e,expansions:t}},singleData:function(n,e){return{primary:n,menus:St(n,e),expansions:{}}},collapseItem:function(n){return{value:Zc(Xl()),text:n}}}}),Ql=function(n,e,t,r){return wt(e.routes(),r.start()).map(s).bind(function(n){return wt(n,r.destination()).map(s)})},Zl=function(n,e,t,r){return Ql(0,e,0,r).bind(function(e){return e.transition().map(function(n){return{transition:M(n),route:M(e)}})})},nd=function(t,r,n){var e,o,i;(e=t,o=r,i=n,ed(e,o).bind(function(n){return Zl(e,o,i,n)})).each(function(n){var e=n.transition();io(t.element(),e.transitionClass()),Qr(t.element(),r.destinationAttr())})},ed=function(n,e,t){var r=n.element();return Jr(r,e.destinationAttr())?V.some({start:M(Xr(n.element(),e.stateAttr())),destination:M(Xr(n.element(),e.destinationAttr()))}):V.none()},td=function(n,e,t,r){nd(n,e,t),Jr(n.element(),e.stateAttr())&&Xr(n.element(),e.stateAttr())!==r&&e.onFinish()(n,r),Yr(n.element(),e.stateAttr(),r)},rd=Object.freeze({findRoute:Ql,disableTransition:nd,getCurrentRoute:ed,jumpTo:td,progressTo:function(t,r,o,i){var n,e;e=r,Jr((n=t).element(),e.destinationAttr())&&(Yr(n.element(),e.stateAttr(),Xr(n.element(),e.destinationAttr())),Qr(n.element(),e.destinationAttr()));var u,c,a=(u=r,c=i,{start:M(Xr(t.element(),u.stateAttr())),destination:M(c)});Zl(t,r,o,a).fold(function(){td(t,r,o,i)},function(n){nd(t,r,o);var e=n.transition();oo(t.element(),e.transitionClass()),Yr(t.element(),r.destinationAttr(),i)})},getState:function(n,e,t){var r=n.element();return Jr(r,e.stateAttr())?V.some(Xr(r,e.stateAttr())):V.none()}}),od=Object.freeze({events:function(o,i){return mr([vr(Q(),function(t,n){var r=n.event().raw();ed(t,o).each(function(e){Ql(0,o,0,e).each(function(n){n.transition().each(function(n){r.propertyName===n.property()&&(td(t,o,i,e.destination()),o.onTransition()(t,e))})})})}),xr(function(n,e){td(n,o,i,o.initialState())})])}}),id=[ar("destinationAttr","data-transitioning-destination"),ar("stateAttr","data-transitioning-state"),er("initialState"),_o("onTransition"),_o("onFinish"),tr("routes",jt(tt.value,jt(tt.value,Ht([cr("transition",[er("property"),er("transitionClass")])]))))],ud=Gr({fields:id,name:"transitioning",active:od,apis:rd,extra:{createRoutes:function(n){var r={};return H(n,function(n,e){var t=e.split("<->");r[t[0]]=St(t[1],n),r[t[1]]=St(t[0],n)}),r},createBistate:function(n,e,t){return Ot([{key:n,value:St(e,t)},{key:e,value:St(n,t)}])},createTristate:function(n,e,t,r){return Ot([{key:n,value:Ot([{key:e,value:r},{key:t,value:r}])},{key:e,value:Ot([{key:n,value:r},{key:t,value:r}])},{key:t,value:Ot([{key:n,value:r},{key:e,value:r}])}])}}}),cd=vi.resolve("scrollable"),ad={register:function(n){oo(n,cd)},deregister:function(n){io(n,cd)},scrollable:M(cd)},sd=function(n){return wt(n,"format").getOr(n.title)},fd=function(n,e,t,r,o){return{data:{value:n,text:e},type:"item",dom:{tag:"div",classes:o?[vi.resolve("styles-item-is-menu")]:[]},toggling:{toggleOnExecute:!1,toggleClass:vi.resolve("format-matches"),selected:t},itemBehaviours:$r(o?[]:[li(n,function(n,e){(e?fi.on:fi.off)(n)})]),components:[{dom:{tag:"div",attributes:{style:r},innerHtml:e}}]}},ld=function(n,e,t,r){return{value:n,dom:{tag:"div"},components:[Wa.sketch({dom:{tag:"div",classes:[vi.resolve("styles-collapser")]},components:r?[{dom:{tag:"span",classes:[vi.resolve("styles-collapse-icon")]}},kl(n)]:[kl(n)],action:function(n){if(r){var e=t().get(n);Jl.collapseMenu(e)}}}),{dom:{tag:"div",classes:[vi.resolve("styles-menu-items-container")]},components:[Wl.parts().items({})],behaviours:$r([ff("adhoc-scrollable-menu",[xr(function(n,e){Oi(n.element(),"overflow-y","auto"),Oi(n.element(),"-webkit-overflow-scrolling","touch"),ad.register(n.element())}),wr(function(n){Di(n.element(),"overflow-y"),Di(n.element(),"-webkit-overflow-scrolling"),ad.deregister(n.element())})])])}],items:e,menuBehaviours:$r([ud.config({initialState:"after",routes:ud.createTristate("before","current","after",{transition:{property:"transform",transitionClass:"transitioning"}})})])}},dd=function(r){var o,i,n,e,t,u=(o=r.formats,i=function(){return c},n=ld("Styles",[].concat(xn(o.items,function(n){return fd(sd(n),n.title,n.isSelected(),n.getPreview(),kt(o.expansions,sd(n)))})),i,!1),e=z(o.menus,function(n,e){var t=xn(n,function(n){return fd(sd(n),n.title,n.isSelected!==undefined&&n.isSelected(),n.getPreview!==undefined?n.getPreview():"",kt(o.expansions,sd(n)))});return ld(e,t,i,!0)}),t=C(e,St("styles",n)),{tmenu:Jl.tieredData("styles",t,o.expansions)}),c=Ps(Jl.sketch({dom:{tag:"div",classes:[vi.resolve("styles-menu")]},components:[],fakeFocus:!0,stayInDom:!0,onExecute:function(n,e){var t=Os.getValue(e);return r.handle(e,t.value),V.none()},onEscape:function(){return V.none()},onOpenMenu:function(n,e){var t=Ds(n.element());Es(e.element(),t),ud.jumpTo(e,"current")},onOpenSubmenu:function(n,e,t){var r=Ds(n.element()),o=ji(e.element(),'[role="menu"]').getOrDie("hacky"),i=n.getSystem().getByDom(o).getOrDie();Es(t.element(),r),ud.progressTo(i,"before"),ud.jumpTo(t,"after"),ud.progressTo(t,"current")},onCollapseMenu:function(n,e,t){var r=ji(e.element(),'[role="menu"]').getOrDie("hacky"),o=n.getSystem().getByDom(r).getOrDie();ud.progressTo(o,"after"),ud.progressTo(t,"current")},navigateOnHover:!1,openImmediately:!0,data:u.tmenu,markers:{backgroundMenu:vi.resolve("styles-background-menu"),menu:vi.resolve("styles-menu"),selectedMenu:vi.resolve("styles-selected-menu"),item:vi.resolve("styles-item"),selectedItem:vi.resolve("styles-selected-item")}}));return c.asSpec()},md=function(n){return kt(n,"items")?(t=C(bt(e=n,["items"]),{menu:!0}),r=gd(e.items),{item:t,menus:C(r.menus,St(e.title,r.items)),expansions:C(r.expansions,St(e.title,e.title))}):{item:n,menus:{},expansions:{}};var e,t,r},gd=function(n){return On(n,function(n,e){var t=md(e);return{menus:C(n.menus,t.menus),items:[t.item].concat(n.items),expansions:C(n.expansions,t.expansions)}},{menus:{},expansions:{},items:[]})},vd={expand:gd},pd=function(u,n){var c=function(n){return function(){return u.formatter.match(n)}},a=function(n){return function(){return u.formatter.getCssText(n)}},e=wt(n,"style_formats").getOr(Kf),s=function(n){return xn(n,function(n){if(kt(n,"items")){var e=s(n.items);return C(C(n,{isSelected:M(!1),getPreview:M("")}),{items:e})}return kt(n,"format")?C(i=n,{isSelected:c(i.format),getPreview:a(i.format)}):(r=Zc((t=n).title),o=C(t,{format:r,isSelected:c(r),getPreview:a(r)}),u.formatter.register(r,o),o);var t,r,o,i})};return s(e)},hd=function(t,n,r){var e,o,i,u=(e=t,i=(o=function(n){return Mn(n,function(n){return n.items!==undefined?0<o(n.items).length?[n]:[]:!kt(n,"format")||e.formatter.canApply(n.format)?[n]:[]})})(n),vd.expand(i));return dd({formats:u,handle:function(n,e){t.undoManager.transact(function(){fi.isOn(n)?t.formatter.remove(e):t.formatter.apply(e)}),r()}})},bd=["undo","bold","italic","link","image","bullist","styleselect"],yd=function(n){var e=n.replace(/\|/g," ").trim();return 0<e.length?e.split(/\s+/):[]},xd=function(n){return Mn(n,function(n){return y(n)?xd(n):yd(n)})},wd=function(n){var e=n.toolbar!==undefined?n.toolbar:bd;return y(e)?xd(e):yd(e)},Sd=function(r,o){var n=function(n){return function(){return Xa.forToolbarCommand(o,n)}},e=function(n){return function(){return Xa.forToolbarStateCommand(o,n)}},t=function(n,e,t){return function(){return Xa.forToolbarStateAction(o,n,e,t)}},i=n("undo"),u=n("redo"),c=e("bold"),a=e("italic"),s=e("underline"),f=n("removeformat"),l=t("unlink","link",function(){o.execCommand("unlink",null,!1)}),d=t("unordered-list","ul",function(){o.execCommand("InsertUnorderedList",null,!1)}),m=t("ordered-list","ol",function(){o.execCommand("InsertOrderedList",null,!1)}),g=pd(o,o.settings),v=function(){return hd(o,g,function(){o.fire("scrollIntoView")})},p=function(n,e){return{isSupported:function(){return n.forall(function(n){return kt(o.buttons,n)})},sketch:e}};return{undo:p(V.none(),i),redo:p(V.none(),u),bold:p(V.none(),c),italic:p(V.none(),a),underline:p(V.none(),s),removeformat:p(V.none(),f),link:p(V.none(),function(){return e=r,t=o,Xa.forToolbarStateAction(t,"link","link",function(){var n=Yf(e,t);e.setContextToolbar(n),sf(t,function(){e.focusToolbar()}),uf.query(t).each(function(n){t.selection.select(n.dom())})});var e,t}),unlink:p(V.none(),l),image:p(V.none(),function(){return Zs(o)}),bullist:p(V.some("bullist"),d),numlist:p(V.some("numlist"),m),fontsizeselect:p(V.none(),function(){return e=o,n={onChange:function(n){zs.apply(e,n)},getInitialValue:function(){return zs.get(e)}},As(r,"font-size",function(){return Ls(n)});var e,n}),forecolor:p(V.none(),function(){return Rs(r,o)}),styleselect:p(V.none(),function(){return Xa.forToolbar("style-formats",function(n){o.fire("toReading"),r.dropup().appear(v,fi.on,n)},$r([fi.config({toggleClass:vi.resolve("toolbar-button-selected"),toggleOnExecute:!1,aria:{mode:"pressed"}}),Zo.config({channels:Ot([mi(ko.orientationChanged(),fi.off),mi(ko.dropupDismissed(),fi.off)])})]))})}},Od=function(n,t){var e=wd(n),r={};return Mn(e,function(n){var e=!kt(r,n)&&kt(t,n)&&t[n].isSupported()?[t[n].sketch()]:[];return r[n]=!0,e})},Td=function(m,g){return function(n){if(m(n)){var e,t,r,o,i,u,c,a=fe.fromDom(n.target),s=function(){n.stopPropagation()},f=function(){n.preventDefault()},l=p(f,s),d=(e=a,t=n.clientX,r=n.clientY,o=s,i=f,u=l,c=n,{target:M(e),x:M(t),y:M(r),stop:o,prevent:i,kill:u,raw:M(c)});g(d)}}},kd=function(n,e,t,r,o){var i=Td(t,r);return n.dom().addEventListener(e,i,o),{unbind:l(Cd,n,e,i,o)}},Cd=function(n,e,t,r){n.dom().removeEventListener(e,t,r)},Ed=M(!0),Dd=function(n,e,t){return kd(n,e,Ed,t,!1)},Id=function(n,e,t){return kd(n,e,Ed,t,!0)},Md=function(n){var e=n.matchMedia("(orientation: portrait)").matches;return{isPortrait:M(e)}},Ad=Md,Bd=function(r,e){var n=fe.fromDom(r),o=null,t=Dd(n,"orientationchange",function(){clearInterval(o);var n=Md(r);e.onChange(n),i(function(){e.onReady(n)})}),i=function(n){clearInterval(o);var e=r.innerHeight,t=0;o=setInterval(function(){e!==r.innerHeight?(clearInterval(o),n(V.some(r.innerHeight))):20<t&&(clearInterval(o),n(V.none())),t++},50)};return{onAdjustment:i,destroy:function(){t.unbind()}}},Rd=function(n){var e=Gn.detect().os.isiOS(),t=Md(n).isPortrait();return e&&!t?n.screen.height:n.screen.width},Fd=function(n){var e=n.raw();return e.touches===undefined||1!==e.touches.length?V.none():V.some(e.touches[0])},Vd=function(t){var r,o,i,u=mo(V.none()),c=(r=function(n){u.set(V.none()),t.triggerEvent(Zn(),n)},o=400,i=null,{cancel:function(){null!==i&&(v.clearTimeout(i),i=null)},schedule:function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];i=v.setTimeout(function(){r.apply(null,n),i=null},o)}}),a=Ot([{key:P(),value:function(t){return Fd(t).each(function(n){c.cancel();var e={x:M(n.clientX),y:M(n.clientY),target:t.target};c.schedule(t),u.set(V.some(e))}),V.none()}},{key:$(),value:function(n){return c.cancel(),Fd(n).each(function(i){u.get().each(function(n){var e,t,r,o;e=i,t=n,r=Math.abs(e.clientX-t.x()),o=Math.abs(e.clientY-t.y()),(5<r||5<o)&&u.set(V.none())})}),V.none()}},{key:W(),value:function(e){return c.cancel(),u.get().filter(function(n){return He(n.target(),e.target())}).map(function(n){return t.triggerEvent(Jn(),e)})}}]);return{fireIfReady:function(e,n){return wt(a,n).bind(function(n){return n(e)})}}},Nd=function(t){var e=Vd({triggerEvent:function(n,e){t.onTapContent(e)}});return{fireTouchstart:function(n){e.fireIfReady(n,"touchstart")},onTouchend:function(){return Dd(t.body(),"touchend",function(n){e.fireIfReady(n,"touchend")})},onTouchmove:function(){return Dd(t.body(),"touchmove",function(n){e.fireIfReady(n,"touchmove")})}}},Hd=6<=Gn.detect().os.version.major,zd=function(r,e,t){var o=Nd(r),i=ze(e),u=function(n){return!He(n.start(),n.finish())||n.soffset()!==n.foffset()},n=function(){var n=r.doc().dom().hasFocus()&&r.getSelection().exists(u);t.getByDom(e).each(!0===(n||xo(i).filter(function(n){return"input"===ge(n)}).exists(function(n){return n.dom().selectionStart!==n.dom().selectionEnd}))?fi.on:fi.off)},c=[Dd(r.body(),"touchstart",function(n){r.onTouchContent(),o.fireTouchstart(n)}),o.onTouchmove(),o.onTouchend(),Dd(e,"touchstart",function(n){r.onTouchToolstrip()}),r.onToReading(function(){yo(r.body())}),r.onToEditing(I),r.onScrollToCursor(function(n){n.preventDefault(),r.getCursorBox().each(function(n){var e=r.win(),t=n.top()>e.innerHeight||n.bottom()>e.innerHeight?n.bottom()-e.innerHeight+50:0;0!==t&&e.scrollTo(e.pageXOffset,e.pageYOffset+t)})})].concat(!0===Hd?[]:[Dd(fe.fromDom(r.win()),"blur",function(){t.getByDom(e).each(fi.off)}),Dd(i,"select",n),Dd(r.doc(),"selectionchange",n)]);return{destroy:function(){wn(c,function(n){n.unbind()})}}},jd=function(n,e){var t=parseInt(Xr(n,e),10);return isNaN(t)?0:t},Ld=(Us=he,qs="text",Ys=function(n){return Us(n)?V.from(n.dom().nodeValue):V.none()},Ks=Gn.detect().browser,{get:function(n){if(!Us(n))throw new Error("Can only get "+qs+" value of a "+qs+" node");return Xs(n).getOr("")},getOption:Xs=Ks.isIE()&&10===Ks.version.major?function(n){try{return Ys(n)}catch(e){return V.none()}}:Ys,set:function(n,e){if(!Us(n))throw new Error("Can only set raw "+qs+" value of a "+qs+" node");n.dom().nodeValue=e}}),Pd=function(n){return Ld.getOption(n)},$d={create:we("start","soffset","finish","foffset")},Wd=rt([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),Gd={before:Wd.before,on:Wd.on,after:Wd.after,cata:function(n,e,t,r){return n.fold(e,t,r)},getStart:function(n){return n.fold(h,h,h)}},_d=rt([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),Ud={domRange:_d.domRange,relative:_d.relative,exact:_d.exact,exactFromRange:function(n){return _d.exact(n.start(),n.soffset(),n.finish(),n.foffset())},getWin:function(n){var e=n.match({domRange:function(n){return fe.fromDom(n.startContainer)},relative:function(n,e){return Gd.getStart(n)},exact:function(n,e,t,r){return n}});return je(e)},range:$d.create},qd=function(n,e,t){var r,o,i=n.document.createRange();return r=i,e.fold(function(n){r.setStartBefore(n.dom())},function(n,e){r.setStart(n.dom(),e)},function(n){r.setStartAfter(n.dom())}),o=i,t.fold(function(n){o.setEndBefore(n.dom())},function(n,e){o.setEnd(n.dom(),e)},function(n){o.setEndAfter(n.dom())}),i},Yd=function(n,e,t,r,o){var i=n.document.createRange();return i.setStart(e.dom(),t),i.setEnd(r.dom(),o),i},Kd=function(n){return{left:M(n.left),top:M(n.top),right:M(n.right),bottom:M(n.bottom),width:M(n.width),height:M(n.height)}},Xd=rt([{ltr:["start","soffset","finish","foffset"]},{rtl:["start","soffset","finish","foffset"]}]),Jd=function(n,e,t){return e(fe.fromDom(t.startContainer),t.startOffset,fe.fromDom(t.endContainer),t.endOffset)},Qd=function(n,e){var o,t,r,i=(o=n,e.match({domRange:function(n){return{ltr:M(n),rtl:V.none}},relative:function(n,e){return{ltr:nn(function(){return qd(o,n,e)}),rtl:nn(function(){return V.some(qd(o,e,n))})}},exact:function(n,e,t,r){return{ltr:nn(function(){return Yd(o,n,e,t,r)}),rtl:nn(function(){return V.some(Yd(o,t,r,n,e))})}}}));return(r=(t=i).ltr()).collapsed?t.rtl().filter(function(n){return!1===n.collapsed}).map(function(n){return Xd.rtl(fe.fromDom(n.endContainer),n.endOffset,fe.fromDom(n.startContainer),n.startOffset)}).getOrThunk(function(){return Jd(0,Xd.ltr,r)}):Jd(0,Xd.ltr,r)},Zd=(document.caretPositionFromPoint||document.caretRangeFromPoint,function(n,e){var t=ge(n);return"input"===t?Gd.after(n):yn(["br","img"],t)?0===e?Gd.before(n):Gd.after(n):Gd.on(n,e)}),nm=function(n,e,t,r){var o,i,u,c,a,s=(i=e,u=t,c=r,(a=ze(o=n).dom().createRange()).setStart(o.dom(),i),a.setEnd(u.dom(),c),a),f=He(n,t)&&e===r;return s.collapsed&&!f},em=function(n,e,t,r,o){var i,u,c=Yd(n,e,t,r,o);i=n,u=c,V.from(i.getSelection()).each(function(n){n.removeAllRanges(),n.addRange(u)})},tm=function(n,e,t,r,o){var i,u,c,a,l,s=(i=r,u=o,c=Zd(e,t),a=Zd(i,u),Ud.relative(c,a));Qd(l=n,s).match({ltr:function(n,e,t,r){em(l,n,e,t,r)},rtl:function(n,e,t,r){var o,i,u,c,a,s=l.getSelection();if(s.setBaseAndExtent)s.setBaseAndExtent(n.dom(),e,t.dom(),r);else if(s.extend)try{i=n,u=e,c=t,a=r,(o=s).collapse(i.dom(),u),o.extend(c.dom(),a)}catch(f){em(l,t,r,n,e)}else em(l,t,r,n,e)}})},rm=function(n){var e=fe.fromDom(n.anchorNode),t=fe.fromDom(n.focusNode);return nm(e,n.anchorOffset,t,n.focusOffset)?V.some($d.create(e,n.anchorOffset,t,n.focusOffset)):function(n){if(0<n.rangeCount){var e=n.getRangeAt(0),t=n.getRangeAt(n.rangeCount-1);return V.some($d.create(fe.fromDom(e.startContainer),e.startOffset,fe.fromDom(t.endContainer),t.endOffset))}return V.none()}(n)},om=function(n){return V.from(n.getSelection()).filter(function(n){return 0<n.rangeCount}).bind(rm)},im=function(n,e){var i,t,r,o,u=Qd(i=n,e).match({ltr:function(n,e,t,r){var o=i.document.createRange();return o.setStart(n.dom(),e),o.setEnd(t.dom(),r),o},rtl:function(n,e,t,r){var o=i.document.createRange();return o.setStart(t.dom(),r),o.setEnd(n.dom(),e),o}});return r=(t=u).getClientRects(),0<(o=0<r.length?r[0]:t.getBoundingClientRect()).width||0<o.height?V.some(o).map(Kd):V.none()},um=function(n){return{left:n.left,top:n.top,right:n.right,bottom:n.bottom,width:M(2),height:n.height}},cm=function(n){return{left:M(n.left),top:M(n.top),right:M(n.right),bottom:M(n.bottom),width:M(n.width),height:M(n.height)}},am=function(r){if(r.collapsed){var o=fe.fromDom(r.startContainer);return Le(o).bind(function(n){var e,t=Ud.exact(o,r.startOffset,n,"img"===ge(e=n)?1:Pd(e).fold(function(){return Pe(e).length},function(n){return n.length}));return im(r.startContainer.ownerDocument.defaultView,t).map(um).map(Rn)}).getOr([])}return xn(r.getClientRects(),cm)},sm=function(n){var e=n.getSelection();return e!==undefined&&0<e.rangeCount?am(e.getRangeAt(0)):[]},fm=function(n){n.focus();var e=fe.fromDom(n.document.body);(xo().exists(function(n){return yn(["input","textarea"],ge(n))})?function(n){setTimeout(function(){n()},0)}:s)(function(){xo().each(yo),bo(e)})},lm="data-"+vi.resolve("last-outer-height"),dm=function(n,e){Yr(n,lm,e)},mm=function(n){return{top:M(n.top()),bottom:M(n.top()+n.height())}},gm=function(n,e){var t=jd(e,lm),r=n.innerHeight;return r<t?V.some(t-r):V.none()},vm=function(n,u){var e=fe.fromDom(u.document.body),t=Dd(fe.fromDom(n),"resize",function(){gm(n,e).each(function(i){var n,e;(n=u,e=sm(n),0<e.length?V.some(e[0]).map(mm):V.none()).each(function(n){var e,t,r,o=(e=u,r=i,(t=n).top()>e.innerHeight||t.bottom()>e.innerHeight?Math.min(r,t.bottom()-e.innerHeight+50):0);0!==o&&u.scrollTo(u.pageXOffset,u.pageYOffset+o)})}),dm(e,n.innerHeight)});return dm(e,n.innerHeight),{toEditing:function(){fm(u)},destroy:function(){t.unbind()}}},pm=function(n){return V.some(fe.fromDom(n.dom().contentWindow.document.body))},hm=function(n){return V.some(fe.fromDom(n.dom().contentWindow.document))},bm=function(n){return V.from(n.dom().contentWindow)},ym=function(n){return bm(n).bind(om)},xm=function(n){return n.getFrame()},wm=function(n,t){return function(e){return e[n].getOrThunk(function(){var n=xm(e);return function(){return t(n)}})()}},Sm=function(n,e,t,r){return n[t].getOrThunk(function(){return function(n){return Dd(e,r,n)}})},Om=function(n){return{left:M(n.left),top:M(n.top),right:M(n.right),bottom:M(n.bottom),width:M(n.width),height:M(n.height)}},Tm={getBody:wm("getBody",pm),getDoc:wm("getDoc",hm),getWin:wm("getWin",bm),getSelection:wm("getSelection",ym),getFrame:xm,getActiveApi:function(c){var a=xm(c);return pm(a).bind(function(u){return hm(a).bind(function(i){return bm(a).map(function(o){var n=fe.fromDom(i.dom().documentElement),e=c.getCursorBox.getOrThunk(function(){return function(){return(n=o,om(n).map(function(n){return Ud.exact(n.start(),n.soffset(),n.finish(),n.foffset())})).bind(function(n){return im(o,n).orThunk(function(){return om(o).filter(function(n){return He(n.start(),n.finish())&&n.soffset()===n.foffset()}).bind(function(n){var e=n.start().dom().getBoundingClientRect();return 0<e.width||0<e.height?V.some(e).map(Om):V.none()})})});var n}}),t=c.setSelection.getOrThunk(function(){return function(n,e,t,r){tm(o,n,e,t,r)}}),r=c.clearSelection.getOrThunk(function(){return function(){o.getSelection().removeAllRanges()}});return{body:M(u),doc:M(i),win:M(o),html:M(n),getSelection:l(ym,a),setSelection:t,clearSelection:r,frame:M(a),onKeyup:Sm(c,i,"onKeyup","keyup"),onNodeChanged:Sm(c,i,"onNodeChanged","selectionchange"),onDomChanged:c.onDomChanged,onScrollToCursor:c.onScrollToCursor,onScrollToElement:c.onScrollToElement,onToReading:c.onToReading,onToEditing:c.onToEditing,onToolbarScrollStart:c.onToolbarScrollStart,onTouchContent:c.onTouchContent,onTapContent:c.onTapContent,onTouchToolstrip:c.onTouchToolstrip,getCursorBox:e}})})})}},km="data-ephox-mobile-fullscreen-style",Cm="position:absolute!important;",Em="top:0!important;left:0!important;margin:0!important;padding:0!important;width:100%!important;height:100%!important;overflow:visible!important;",Dm=Gn.detect().os.isAndroid(),Im=function(n,e){var t,r,o,i=function(r){return function(n){var e=Xr(n,"style"),t=e===undefined?"no-styles":e.trim();t!==r&&(Yr(n,km,t),Yr(n,"style",r))}},u=(t="*",Vi(n,function(n){return Re(n,t)},r)),c=Mn(u,function(n){var e;return e="*",Ni(n,function(n){return Re(n,e)})}),a=(o=ki(e,"background-color"))!==undefined&&""!==o?"background-color:"+o+"!important":"background-color:rgb(255,255,255)!important;";wn(c,i("display:none!important;")),wn(u,i(Cm+Em+a)),i((!0===Dm?"":Cm)+Em+a)(n)},Mm=function(){var n=Ve("["+km+"]");wn(n,function(n){var e=Xr(n,km);"no-styles"!==e?Yr(n,"style",e):Qr(n,"style"),Qr(n,km)})},Am=function(){var e=zi("head").getOrDie(),n=zi('meta[name="viewport"]').getOrThunk(function(){var n=fe.fromTag("meta");return Yr(n,"name","viewport"),Ge(e,n),n}),t=Xr(n,"content");return{maximize:function(){Yr(n,"content","width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0")},restore:function(){t!==undefined&&null!==t&&0<t.length?Yr(n,"content",t):Yr(n,"content","user-scalable=yes")}}},Bm=function(e,n){var t=Am(),r=$f(),o=$f();return{enter:function(){n.hide(),oo(e.container,vi.resolve("fullscreen-maximized")),oo(e.container,vi.resolve("android-maximized")),t.maximize(),oo(e.body,vi.resolve("android-scroll-reload")),r.set(vm(e.win,Tm.getWin(e.editor).getOrDie("no"))),Tm.getActiveApi(e.editor).each(function(n){Im(e.container,n.body()),o.set(zd(n,e.toolstrip,e.alloy))})},exit:function(){t.restore(),n.show(),io(e.container,vi.resolve("fullscreen-maximized")),io(e.container,vi.resolve("android-maximized")),Mm(),io(e.body,vi.resolve("android-scroll-reload")),o.clear(),r.clear()}}},Rm=function(t,r){var o=null;return{cancel:function(){null!==o&&(v.clearTimeout(o),o=null)},throttle:function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];null!==o&&v.clearTimeout(o),o=v.setTimeout(function(){t.apply(null,n),o=null},r)}}},Fm=function(n,e){var t,r,o,i=Ps(gf.sketch({dom:Ua('<div aria-hidden="true" class="${prefix}-mask-tap-icon"></div>'),containerBehaviours:$r([fi.config({toggleClass:vi.resolve("mask-tap-icon-selected"),toggleOnExecute:!1})])})),u=(t=n,r=200,o=null,{cancel:function(){null!==o&&(v.clearTimeout(o),o=null)},throttle:function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];null===o&&(o=v.setTimeout(function(){t.apply(null,n),o=null},r))}});return gf.sketch({dom:Ua('<div class="${prefix}-disabled-mask"></div>'),components:[gf.sketch({dom:Ua('<div class="${prefix}-content-container"></div>'),components:[Wa.sketch({dom:Ua('<div class="${prefix}-content-tap-section"></div>'),components:[i.asSpec()],action:function(n){u.throttle()},buttonBehaviours:$r([fi.config({toggleClass:vi.resolve("mask-tap-icon-selected")})])})]})]})},Vm=zt([or("editor",[er("getFrame"),ir("getBody"),ir("getDoc"),ir("getWin"),ir("getSelection"),ir("setSelection"),ir("clearSelection"),ir("cursorSaver"),ir("onKeyup"),ir("onNodeChanged"),ir("getCursorBox"),er("onDomChanged"),ar("onTouchContent",I),ar("onTapContent",I),ar("onTouchToolstrip",I),ar("onScrollToCursor",M({unbind:I})),ar("onScrollToElement",M({unbind:I})),ar("onToEditing",M({unbind:I})),ar("onToReading",M({unbind:I})),ar("onToolbarScrollStart",h)]),er("socket"),er("toolstrip"),er("dropup"),er("toolbar"),er("container"),er("alloy"),fr("win",function(n){return ze(n.socket).dom().defaultView}),fr("body",function(n){return fe.fromDom(n.socket.dom().ownerDocument.body)}),ar("translate",h),ar("setReadOnly",I),ar("readOnlyOnInit",M(!0))]),Nm=function(n){var e=Kt("Getting AndroidWebapp schema",Vm,n);Oi(e.toolstrip,"width","100%");var t=El(Fm(function(){e.setReadOnly(e.readOnlyOnInit()),o.enter()},e.translate));e.alloy.add(t);var r={show:function(){e.alloy.add(t)},hide:function(){e.alloy.remove(t)}};Ge(e.container,t.element());var o=Bm(e,r);return{setReadOnly:e.setReadOnly,refreshStructure:I,enter:o.enter,exit:o.exit,destroy:I}},Hm=M([ar("shell",!0),$c("toolbarBehaviours",[Ul])]),zm=M([ma({name:"groups",overrides:function(n){return{behaviours:$r([Ul.config({})])}}})]),jm=$a({name:"Toolbar",configFields:Hm(),partFields:zm(),factory:function(e,n,t,r){var o=function(n){return e.shell()?V.some(n):wa(n,e,"groups")},i=e.shell()?{behaviours:[Ul.config({})],components:[]}:{behaviours:[],components:n};return{uid:e.uid(),dom:e.dom(),components:i.components,behaviours:C($r(i.behaviours),Wc(e.toolbarBehaviours())),apis:{setGroups:function(n,e){o(n).fold(function(){throw v.console.error("Toolbar was defined to not be a shell, but no groups container was specified in components"),new Error("Toolbar was defined to not be a shell, but no groups container was specified in components")},function(n){Ul.set(n,e)})}},domModification:{attributes:{role:"group"}}}},apis:{setGroups:function(n,e,t){n.setGroups(e,t)}}}),Lm=M([er("items"),(Js=["itemClass"],or("markers",xn(Js,er))),$c("tgroupBehaviours",[Pc])]),Pm=M([ga({name:"items",unit:"item",overrides:function(n){return{domModification:{classes:[n.markers().itemClass()]}}}})]),$m=$a({name:"ToolbarGroup",configFields:Lm(),partFields:Pm(),factory:function(n,e,t,r){return C({dom:{attributes:{role:"toolbar"}}},{uid:n.uid(),dom:n.dom(),components:e,behaviours:C($r([Pc.config({mode:"flow",selector:"."+n.markers().itemClass()})]),Wc(n.tgroupBehaviours())),"debug.sketcher":t["debug.sketcher"]})}}),Wm="data-"+vi.resolve("horizontal-scroll"),Gm=function(n){return"true"===Xr(n,Wm)?0<(t=n).dom().scrollLeft||function(n){n.dom().scrollLeft=1;var e=0!==n.dom().scrollLeft;return n.dom().scrollLeft=0,e}(t):0<(e=n).dom().scrollTop||function(n){n.dom().scrollTop=1;var e=0!==n.dom().scrollTop;return n.dom().scrollTop=0,e}(e);var e,t},_m={exclusive:function(n,e){return Dd(n,"touchmove",function(n){Pi(n.target(),e).filter(Gm).fold(function(){n.raw().preventDefault()},I)})},markAsHorizontal:function(n){Yr(n,Wm,"true")}};function Um(){var e=function(n){var e=!0===n.scrollable?"${prefix}-toolbar-scrollable-group":"";return{dom:Ua('<div aria-label="'+n.label+'" class="${prefix}-toolbar-group '+e+'"></div>'),tgroupBehaviours:$r([ff("adhoc-scrollable-toolbar",!0===n.scrollable?[Sr(function(n,e){Oi(n.element(),"overflow-x","auto"),_m.markAsHorizontal(n.element()),ad.register(n.element())})]:[])]),components:[gf.sketch({components:[$m.parts().items({})]})],markers:{itemClass:vi.resolve("toolbar-group-item")},items:n.items}},t=El(jm.sketch({dom:Ua('<div class="${prefix}-toolbar"></div>'),components:[jm.parts().groups({})],toolbarBehaviours:$r([fi.config({toggleClass:vi.resolve("context-toolbar"),toggleOnExecute:!1,aria:{mode:"none"}}),Pc.config({mode:"cyclic"})]),shell:!0})),n=El(gf.sketch({dom:{classes:[vi.resolve("toolstrip")]},components:[Dl(t)],containerBehaviours:$r([fi.config({toggleClass:vi.resolve("android-selection-context-toolbar"),toggleOnExecute:!1})])})),r=function(){jm.setGroups(t,o.get()),fi.off(t)},o=mo([]);return{wrapper:M(n),toolbar:M(t),createGroups:function(n){return xn(n,p($m.sketch,e))},setGroups:function(n){o.set(n),r()},setContextToolbar:function(n){fi.on(t),jm.setGroups(t,n)},restoreToolbar:function(){fi.isOn(t)&&r()},refresh:function(){},focus:function(){Pc.focusIn(t)}}}var qm=function(n,e){Ul.append(n,Dl(e))},Ym=function(n,e){Ul.remove(n,e)},Km=function(n){return El(Wa.sketch({dom:Ua('<div class="${prefix}-mask-edit-icon ${prefix}-icon"></div>'),action:function(){n.run(function(n){n.setReadOnly(!1)})}}))},Xm=function(){return El(gf.sketch({dom:Ua('<div class="${prefix}-editor-socket"></div>'),components:[],containerBehaviours:$r([Ul.config({})])}))},Jm=function(n,e,t,r){(!0===t?lo.toAlpha:lo.toOmega)(r),(t?qm:Ym)(n,e)},Qm=function(e,n){return n.getAnimationRoot().fold(function(){return e.element()},function(n){return n(e)})},Zm=function(n){return n.dimension().property()},ng=function(n,e){return n.dimension().getDimension()(e)},eg=function(n,e){var t=Qm(n,e);vl(t,[e.shrinkingClass(),e.growingClass()])},tg=function(n,e){io(n.element(),e.openClass()),oo(n.element(),e.closedClass()),Oi(n.element(),Zm(e),"0px"),Ii(n.element())},rg=function(n,e){io(n.element(),e.closedClass()),oo(n.element(),e.openClass()),Di(n.element(),Zm(e))},og=function(n,e,t){t.setCollapsed(),Oi(n.element(),Zm(e),ng(e,n.element())),Ii(n.element());var r=Qm(n,e);oo(r,e.shrinkingClass()),tg(n,e),e.onStartShrink()(n)},ig=function(n,e,t){var r=function(n,e){rg(n,e);var t=ng(e,n.element());return tg(n,e),t}(n,e),o=Qm(n,e);oo(o,e.growingClass()),rg(n,e),Oi(n.element(),Zm(e),r),t.setExpanded(),e.onStartGrow()(n)},ug=function(n,e,t){var r=Qm(n,e);return!0===co(r,e.growingClass())},cg=function(n,e,t){var r=Qm(n,e);return!0===co(r,e.shrinkingClass())},ag=Object.freeze({grow:function(n,e,t){t.isExpanded()||ig(n,e,t)},shrink:function(n,e,t){t.isExpanded()&&og(n,e,t)},immediateShrink:function(n,e,t){var r,o;t.isExpanded()&&(r=n,o=e,t.setCollapsed(),Oi(r.element(),Zm(o),ng(o,r.element())),Ii(r.element()),eg(r,o),tg(r,o),o.onStartShrink()(r),o.onShrunk()(r))},hasGrown:function(n,e,t){return t.isExpanded()},hasShrunk:function(n,e,t){return t.isCollapsed()},isGrowing:ug,isShrinking:cg,isTransitioning:function(n,e,t){return!0===ug(n,e)||!0===cg(n,e)},toggleGrow:function(n,e,t){(t.isExpanded()?og:ig)(n,e,t)},disableTransitions:eg}),sg=Object.freeze({exhibit:function(n,e){var t=e.expanded();return Dr(t?{classes:[e.openClass()],styles:{}}:{classes:[e.closedClass()],styles:St(e.dimension().property(),"0px")})},events:function(t,r){return mr([vr(Q(),function(n,e){e.event().raw().propertyName===t.dimension().property()&&(eg(n,t),r.isExpanded()&&Di(n.element(),t.dimension().property()),(r.isExpanded()?t.onGrown():t.onShrunk())(n))})])}}),fg=[er("closedClass"),er("openClass"),er("shrinkingClass"),er("growingClass"),ir("getAnimationRoot"),_o("onShrunk"),_o("onStartShrink"),_o("onGrown"),_o("onStartGrow"),ar("expanded",!1),tr("dimension",Qt("property",{width:[Ko("property","width"),Ko("getDimension",function(n){return Ds(n)+"px"})],height:[Ko("property","height"),Ko("getDimension",function(n){return Fi(n)+"px"})]}))],lg=Gr({fields:fg,name:"sliding",active:sg,apis:ag,state:Object.freeze({init:function(n){var e=mo(n.expanded());return Pr({isExpanded:function(){return!0===e.get()},isCollapsed:function(){return!1===e.get()},setCollapsed:l(e.set,!1),setExpanded:l(e.set,!0),readState:function(){return"expanded: "+e.get()}})}})}),dg=function(e,t){var r=El(gf.sketch({dom:{tag:"div",classes:[vi.resolve("dropup")]},components:[],containerBehaviours:$r([Ul.config({}),lg.config({closedClass:vi.resolve("dropup-closed"),openClass:vi.resolve("dropup-open"),shrinkingClass:vi.resolve("dropup-shrinking"),growingClass:vi.resolve("dropup-growing"),dimension:{property:"height"},onShrunk:function(n){e(),t(),Ul.set(n,[])},onGrown:function(n){e(),t()}}),di(function(n,e){o(I)})])})),o=function(n){v.window.requestAnimationFrame(function(){n(),lg.shrink(r)})};return{appear:function(n,e,t){!0===lg.hasShrunk(r)&&!1===lg.isTransitioning(r)&&v.window.requestAnimationFrame(function(){e(t),Ul.set(r,[n()]),lg.grow(r)})},disappear:o,component:M(r),element:r.element}},mg=Gn.detect().browser.isFirefox(),gg=Ht([rr("triggerEvent"),rr("broadcastEvent"),ar("stopBackspace",!0)]),vg=function(e,n){var t,r,o,i,u=Kt("Getting GUI events settings",gg,n),c=Gn.detect().deviceType.isTouch()?["touchstart","touchmove","touchend","gesturestart"]:["mousedown","mouseup","mouseover","mousemove","mouseout","click"],a=Vd(u),s=xn(c.concat(["selectstart","input","contextmenu","change","transitionend","drag","dragstart","dragend","dragenter","dragleave","dragover","drop"]),function(n){return Dd(e,n,function(e){a.fireIfReady(e,n).each(function(n){n&&e.kill()}),u.triggerEvent(n,e)&&e.kill()})}),f=Dd(e,"keydown",function(n){var e;u.triggerEvent("keydown",n)?n.kill():!0!==u.stopBackspace||8!==(e=n).raw().which||yn(["input","textarea"],ge(e.target()))||n.prevent()}),l=(t=e,r=function(n){u.triggerEvent("focusin",n)&&n.kill()},mg?Id(t,"focus",r):Dd(t,"focusin",r)),d=(o=e,i=function(n){u.triggerEvent("focusout",n)&&n.kill(),v.setTimeout(function(){u.triggerEvent(qn(),n)},0)},mg?Id(o,"blur",i):Dd(o,"focusout",i)),m=je(e),g=Dd(m,"scroll",function(n){u.broadcastEvent(ee(),n)&&n.kill()});return{unbind:function(){wn(s,function(n){n.unbind()}),f.unbind(),l.unbind(),d.unbind(),g.unbind()}}},pg=function(n,e){var t=wt(n,"target").map(function(n){return n()}).getOr(e);return mo(t)},hg=rt([{stopped:[]},{resume:["element"]},{complete:[]}]),bg=function(n,r,e,t,o,i){var u,c,a,s,f=n(r,t),l=(u=e,c=o,a=mo(!1),s=mo(!1),{stop:function(){a.set(!0)},cut:function(){s.set(!0)},isStopped:a.get,isCut:s.get,event:M(u),setSource:c.set,getSource:c.get});return f.fold(function(){return i.logEventNoHandlers(r,t),hg.complete()},function(e){var t=e.descHandler();return ul(t)(l),l.isStopped()?(i.logEventStopped(r,e.element(),t.purpose()),hg.stopped()):l.isCut()?(i.logEventCut(r,e.element(),t.purpose()),hg.complete()):Le(e.element()).fold(function(){return i.logNoParent(r,e.element(),t.purpose()),hg.complete()},function(n){return i.logEventResponse(r,e.element(),t.purpose()),hg.resume(n)})})},yg=function(e,t,r,n,o,i){return bg(e,t,r,n,o,i).fold(function(){return!0},function(n){return yg(e,t,r,n,o,i)},function(){return!1})},xg=function(n,e,t){var r,o,i=(r=e,o=mo(!1),{stop:function(){o.set(!0)},cut:I,isStopped:o.get,isCut:M(!1),event:M(r),setSource:a("Cannot set source of a broadcasted event"),getSource:a("Cannot get source of a broadcasted event")});return wn(n,function(n){var e=n.descHandler();ul(e)(i)}),i.isStopped()},wg=function(n,e,t,r,o){var i=pg(t,r);return yg(n,e,t,r,i,o)},Sg=function(n,e,t){return po(n,function(n){return e(n).isSome()},t).bind(e)},Og=we("element","descHandler"),Tg=function(n,e){return{id:M(n),descHandler:M(e)}};function kg(){var i={};return{registerId:function(r,o,n){H(n,function(n,e){var t=i[e]!==undefined?i[e]:{};t[o]=il(n,r),i[e]=t})},unregisterId:function(t){H(i,function(n,e){n.hasOwnProperty(t)&&delete n[t]})},filterByType:function(n){return wt(i,n).map(function(n){return L(n,function(n,e){return Tg(e,n)})}).getOr([])},find:function(n,e,t){var o=yt(e)(i);return Sg(t,function(n){return t=o,Fa(r=n).fold(function(){return V.none()},function(n){var e=yt(n);return t.bind(e).map(function(n){return Og(r,n)})});var t,r},n)}}}function Cg(){var r=kg(),o={},i=function(r){var n=r.element();return Fa(n).fold(function(){return n="uid-",e=r.element(),t=Zc(Ba+n),Yr(e,Ra,t),t;var n,e,t},function(n){return n})},u=function(n){Fa(n.element()).each(function(n){o[n]=undefined,r.unregisterId(n)})};return{find:function(n,e,t){return r.find(n,e,t)},filter:function(n){return r.filterByType(n)},register:function(n){var e=i(n);kt(o,e)&&function(n,e){var t=o[e];if(t!==n)throw new Error('The tagId "'+e+'" is already used by: '+Mo(t.element())+"\nCannot use it for: "+Mo(n.element())+"\nThe conflicting element is"+(be(t.element())?" ":" not ")+"already in the DOM");u(n)}(n,e);var t=[n];r.registerId(t,e,n.events()),o[e]=n},unregister:u,getById:function(n){return yt(n)(o)}}}var Eg=function(t){var r=function(e){return Le(t.element()).fold(function(){return!0},function(n){return He(e,n)})},o=Cg(),s=function(n,e){return o.find(r,n,e)},n=vg(t.element(),{triggerEvent:function(u,c){return jo(u,c.target(),function(n){return e=s,t=u,o=n,i=(r=c).target(),wg(e,t,r,i,o);var e,t,r,o,i})},broadcastEvent:function(n,e){var t=o.filter(n);return xg(t,e)}}),i=Zf({debugInfo:M("real"),triggerEvent:function(e,t,r){jo(e,t,function(n){wg(s,e,r,t,n)})},triggerFocus:function(c,a){Fa(c).fold(function(){bo(c)},function(n){jo(Un(),c,function(n){var e,t,r,o,i,u;e=s,t=Un(),r={originator:M(a),kill:I,prevent:I,target:M(c)},i=n,u=pg(r,o=c),bg(e,t,r,o,u,i)})})},triggerEscape:function(n,e){i.triggerEvent("keydown",n.element(),e.event())},getByUid:function(n){return m(n)},getByDom:function(n){return g(n)},build:El,addToGui:function(n){c(n)},removeFromGui:function(n){a(n)},addToWorld:function(n){e(n)},removeFromWorld:function(n){u(n)},broadcast:function(n){l(n)},broadcastOn:function(n,e){d(n,e)},isConnected:M(!0)}),e=function(n){n.connect(i),he(n.element())||(o.register(n),wn(n.components(),e),i.triggerEvent(ne(),n.element(),{target:M(n.element())}))},u=function(n){he(n.element())||(wn(n.components(),u),o.unregister(n)),n.disconnect()},c=function(n){Xe(t,n)},a=function(n){Ze(n)},f=function(t){var n=o.filter(Yn());wn(n,function(n){var e=n.descHandler();ul(e)(t)})},l=function(n){f({universal:M(!0),data:M(n)})},d=function(n,e){f({universal:M(!1),channels:M(n),data:M(e)})},m=function(n){return o.getById(n).fold(function(){return tt.error(new Error('Could not find component with uid: "'+n+'" in system.'))},tt.value)},g=function(n){var e=Fa(n).getOr("not found");return m(e)};return e(t),{root:M(t),element:t.element,destroy:function(){n.unbind(),qe(t.element())},add:c,remove:a,getByUid:m,getByDom:g,addToWorld:e,removeFromWorld:u,broadcast:l,broadcastOn:d}},Dg=M(vi.resolve("readonly-mode")),Ig=M(vi.resolve("edit-mode"));function Mg(n){var e=El(gf.sketch({dom:{classes:[vi.resolve("outer-container")].concat(n.classes)},containerBehaviours:$r([lo.config({alpha:Dg(),omega:Ig()})])}));return Eg(e)}var Ag=function(n,e){var t=fe.fromTag("input");Ti(t,{opacity:"0",position:"absolute",top:"-1000px",left:"-1000px"}),Ge(n,t),bo(t),e(t),qe(t)},Bg=function(n){var e=n.getSelection();if(0<e.rangeCount){var t=e.getRangeAt(0),r=n.document.createRange();r.setStart(t.startContainer,t.startOffset),r.setEnd(t.endContainer,t.endOffset),e.removeAllRanges(),e.addRange(r)}},Rg=function(n,e){xo().each(function(n){He(n,e)||yo(n)}),n.focus(),bo(fe.fromDom(n.document.body)),Bg(n)},Fg={stubborn:function(n,e,t,r){var o=function(){Rg(e,r)},i=Dd(t,"keydown",function(n){yn(["input","textarea"],ge(n.target()))||o()});return{toReading:function(){Ag(n,yo)},toEditing:o,onToolbarTouch:function(){},destroy:function(){i.unbind()}}},timid:function(n,e,t,r){var o=function(){yo(r)};return{toReading:function(){o()},toEditing:function(){Rg(e,r)},onToolbarTouch:function(){o()},destroy:I}}},Vg=function(t,r,o,i,n){var u=function(){r.run(function(n){n.refreshSelection()})},e=function(n,e){var t=n-i.dom().scrollTop;r.run(function(n){n.scrollIntoView(t,t+e)})},c=function(){r.run(function(n){n.clearSelection()})},a=function(){t.getCursorBox().each(function(n){e(n.top(),n.height())}),r.run(function(n){n.syncHeight()})},s=Nd(t),f=Rm(a,300),l=[t.onKeyup(function(){c(),f.throttle()}),t.onNodeChanged(u),t.onDomChanged(f.throttle),t.onDomChanged(u),t.onScrollToCursor(function(n){n.preventDefault(),f.throttle()}),t.onScrollToElement(function(n){n.element(),e(r,i)}),t.onToEditing(function(){r.run(function(n){n.toEditing()})}),t.onToReading(function(){r.run(function(n){n.toReading()})}),Dd(t.doc(),"touchend",function(n){He(t.html(),n.target())||He(t.body(),n.target())}),Dd(o,"transitionend",function(n){var e;"height"===n.raw().propertyName&&(e=Fi(o),r.run(function(n){n.setViewportOffset(e)}),u(),a())}),Id(o,"touchstart",function(n){var e;r.run(function(n){n.highlightSelection()}),e=n,r.run(function(n){n.onToolbarTouch(e)}),t.onTouchToolstrip()}),Dd(t.body(),"touchstart",function(n){c(),t.onTouchContent(),s.fireTouchstart(n)}),s.onTouchmove(),s.onTouchend(),Dd(t.body(),"click",function(n){n.kill()}),Dd(o,"touchmove",function(){t.onToolbarScrollStart()})];return{destroy:function(){wn(l,function(n){n.unbind()})}}},Ng=function(n){var t=V.none(),e=[],r=function(n){o()?u(n):e.push(n)},o=function(){return t.isSome()},i=function(n){wn(n,u)},u=function(e){t.each(function(n){v.setTimeout(function(){e(n)},0)})};return n(function(n){t=V.some(n),i(e),e=[]}),{get:r,map:function(t){return Ng(function(e){r(function(n){e(t(n))})})},isReady:o}},Hg={nu:Ng,pure:function(e){return Ng(function(n){n(e)})}},zg=function(e){var n=function(n){var r;e((r=n,function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];var t=this;v.setTimeout(function(){r.apply(t,n)},0)}))},t=function(){return Hg.nu(n)};return{map:function(r){return zg(function(t){n(function(n){var e=r(n);t(e)})})},bind:function(t){return zg(function(e){n(function(n){t(n).get(e)})})},anonBind:function(t){return zg(function(e){n(function(n){t.get(e)})})},toLazy:t,toCached:function(){var e=null;return zg(function(n){null===e&&(e=t()),e.get(n)})},get:n}},jg={nu:zg,pure:function(e){return zg(function(n){n(e)})}},Lg=function(n,e,t){return Math.abs(n-e)<=t?V.none():n<e?V.some(n+t):V.some(n-t)},Pg=function(){var s=null;return{animate:function(r,o,n,i,e,t){var u=!1,c=function(n){u=!0,e(n)};clearInterval(s);var a=function(n){clearInterval(s),c(n)};s=setInterval(function(){var t=r();Lg(t,o,n).fold(function(){clearInterval(s),c(o)},function(n){if(i(n,a),!u){var e=r();(e!==n||Math.abs(e-o)>Math.abs(t-o))&&(clearInterval(s),c(o))}})},t)}}},$g=function(e,t){return Ro([{width:320,height:480,keyboard:{portrait:300,landscape:240}},{width:320,height:568,keyboard:{portrait:300,landscape:240}},{width:375,height:667,keyboard:{portrait:305,landscape:240}},{width:414,height:736,keyboard:{portrait:320,landscape:240}},{width:768,height:1024,keyboard:{portrait:320,landscape:400}},{width:1024,height:1366,keyboard:{portrait:380,landscape:460}}],function(n){return e<=n.width&&t<=n.height?V.some(n.keyboard):V.none()}).getOr({portrait:t/5,landscape:e/4})},Wg=function(n){var e,t=Ad(n).isPortrait(),r=$g((e=n).screen.width,e.screen.height),o=t?r.portrait:r.landscape;return(t?n.screen.height:n.screen.width)-n.innerHeight>o?0:o},Gg=function(n,e){var t=ze(n).dom().defaultView;return Fi(n)+Fi(e)-Wg(t)},_g=Gg,Ug=function(n,e,t){var r=Gg(e,t),o=Fi(e)+Fi(t)-r;Oi(n,"padding-bottom",o+"px")},qg=rt([{fixed:["element","property","offsetY"]},{scroller:["element","offsetY"]}]),Yg="data-"+vi.resolve("position-y-fixed"),Kg="data-"+vi.resolve("y-property"),Xg="data-"+vi.resolve("scrolling"),Jg="data-"+vi.resolve("last-window-height"),Qg=function(n){return jd(n,Yg)},Zg=function(n,e){var t=Xr(n,Kg);return qg.fixed(n,t,e)},nv=function(n,e){return qg.scroller(n,e)},ev=function(n){var e=Qg(n);return("true"===Xr(n,Xg)?nv:Zg)(n,e)},tv=function(n,e,t){var r=ze(n).dom().defaultView.innerHeight;return Yr(n,Jg,r+"px"),r-e-t},rv=function(n){var e=Hi(n,"["+Yg+"]");return xn(e,ev)},ov=function(r,o,i,u){var n,e,t,c,a,s,f,l,d=ze(r).dom().defaultView,m=(l=Xr(f=i,"style"),Ti(f,{position:"absolute",top:"0px"}),Yr(f,Yg,"0px"),Yr(f,Kg,"top"),{restore:function(){Yr(f,"style",l||""),Qr(f,Yg),Qr(f,Kg)}}),g=Fi(i),v=Fi(u),p=(c=tv(r,t=g,v),s=Xr(a=r,"style"),ad.register(a),Ti(a,{position:"absolute",height:c+"px",width:"100%",top:t+"px"}),Yr(a,Yg,t+"px"),Yr(a,Xg,"true"),Yr(a,Kg,"top"),{restore:function(){ad.deregister(a),Yr(a,"style",s||""),Qr(a,Yg),Qr(a,Xg),Qr(a,Kg)}}),h=(e=Xr(n=u,"style"),Ti(n,{position:"absolute",bottom:"0px"}),Yr(n,Yg,"0px"),Yr(n,Kg,"bottom"),{restore:function(){Yr(n,"style",e||""),Qr(n,Yg),Qr(n,Kg)}}),b=!0,y=function(){var n=d.innerHeight;return jd(r,Jg)<n},x=function(){if(b){var n=Fi(i),e=Fi(u),t=tv(r,n,e);Yr(r,Yg,n+"px"),Oi(r,"height",t+"px"),Ug(o,r,u)}};return Ug(o,r,u),{setViewportOffset:function(n){Yr(r,Yg,n+"px"),x()},isExpanding:y,isShrinking:w(y),refresh:x,restore:function(){b=!1,m.restore(),p.restore(),h.restore()}}},iv=Qg,uv=Pg(),cv="data-"+vi.resolve("last-scroll-top"),av=function(n){var e=Ei(n,"top").getOr("0");return parseInt(e,10)},sv=function(n){return parseInt(n.dom().scrollTop,10)},fv=function(n,e){var t=e+iv(n)+"px";Oi(n,"top",t)},lv=function(t,r,o){return jg.nu(function(n){var e=l(sv,t);uv.animate(e,r,15,function(n){t.dom().scrollTop=n,Oi(t,"top",av(t)+15+"px")},function(){t.dom().scrollTop=r,Oi(t,"top",o+"px"),n(r)},10)})},dv=function(o,i){return jg.nu(function(n){var e=l(sv,o);Yr(o,cv,e());var t=Math.abs(i-e()),r=Math.ceil(t/10);uv.animate(e,i,r,function(n,e){jd(o,cv)!==o.dom().scrollTop?e(o.dom().scrollTop):(o.dom().scrollTop=n,Yr(o,cv,n))},function(){o.dom().scrollTop=i,Yr(o,cv,i),n(i)},10)})},mv=function(i,u){return jg.nu(function(n){var e=l(av,i),t=function(n){Oi(i,"top",n+"px")},r=Math.abs(u-e()),o=Math.ceil(r/10);uv.animate(e,u,o,t,function(){t(u),n(u)},10)})},gv=function(e,t,r){var o=ze(e).dom().defaultView;return jg.nu(function(n){fv(e,r),fv(t,r),o.scrollTo(0,r),n(r)})},vv=function(n,e,t,r,o){var i=_g(e,t),u=l(Bg,n);i<r||i<o?dv(e,e.dom().scrollTop-i+o).get(u):r<0&&dv(e,e.dom().scrollTop+r).get(u)},pv=function(u,n){return n(function(r){var o=[],i=0;0===u.length?r([]):wn(u,function(n,e){var t;n.get((t=e,function(n){o[t]=n,++i>=u.length&&r(o)}))})})},hv=function(n,a){return n.fold(function(n,e,t){return Oi(n,e,a+(r=t)+"px"),jg.pure(r);var r},function(n,e){return o=a+(r=e),i=Ei(t=n,"top").getOr(r),u=o-parseInt(i,10),c=t.dom().scrollTop+u,lv(t,c,o);var t,r,o,i,u,c})},bv=function(n,e){var t=rv(n),r=xn(t,function(n){return hv(n,e)});return pv(r,jg.nu)},yv=function(e,t,n,r,o,i){var u,c,a=(u=function(n){return gv(e,t,n)},c=mo(Hg.pure({})),{start:function(e){var n=Hg.nu(function(n){return u(e).get(n)});c.set(n)},idle:function(n){c.get().get(function(){n()})}}),s=Rm(function(){a.idle(function(){bv(n,r.pageYOffset).get(function(){var n;(n=sm(i),V.from(n[0]).bind(function(n){var e=n.top()-t.dom().scrollTop;return e>r.innerHeight+5||e<-5?V.some({top:M(e),bottom:M(e+n.height())}):V.none()})).each(function(n){t.dom().scrollTop=t.dom().scrollTop+n.top()}),a.start(0),o.refresh()})})},1e3),f=Dd(fe.fromDom(r),"scroll",function(){r.pageYOffset<0||s.throttle()});return bv(n,r.pageYOffset).get(h),{unbind:f.unbind}},xv=function(n){var t=n.cWin(),e=n.ceBody(),r=n.socket(),o=n.toolstrip(),i=n.toolbar(),u=n.contentElement(),c=n.keyboardType(),a=n.outerWindow(),s=n.dropup(),f=ov(r,e,o,s),l=c(n.outerBody(),t,ye(),u,o,i),d=Bd(a,{onChange:I,onReady:f.refresh});d.onAdjustment(function(){f.refresh()});var m=Dd(fe.fromDom(a),"resize",function(){f.isExpanding()&&f.refresh()}),g=yv(o,r,n.outerBody(),a,f,t),v=function(t,e){var n=t.document,r=fe.fromTag("div");oo(r,vi.resolve("unfocused-selections")),Ge(fe.fromDom(n.documentElement),r);var o=Dd(r,"touchstart",function(n){n.prevent(),Rg(t,e),u()}),i=function(n){var e=fe.fromTag("span");return gl(e,[vi.resolve("layer-editor"),vi.resolve("unfocused-selection")]),Ti(e,{left:n.left()+"px",top:n.top()+"px",width:n.width()+"px",height:n.height()+"px"}),e},u=function(){Ue(r)};return{update:function(){u();var n=sm(t),e=xn(n,i);_e(r,e)},isActive:function(){return 0<Pe(r).length},destroy:function(){o.unbind(),qe(r)},clear:u}}(t,u),p=function(){v.clear()};return{toEditing:function(){l.toEditing(),p()},toReading:function(){l.toReading()},onToolbarTouch:function(n){l.onToolbarTouch(n)},refreshSelection:function(){v.isActive()&&v.update()},clearSelection:p,highlightSelection:function(){v.update()},scrollIntoView:function(n,e){vv(t,r,s,n,e)},updateToolbarPadding:I,setViewportOffset:function(n){f.setViewportOffset(n),mv(r,n).get(h)},syncHeight:function(){Oi(u,"height",u.dom().contentWindow.document.body.scrollHeight+"px")},refreshStructure:f.refresh,destroy:function(){f.restore(),d.destroy(),g.unbind(),m.unbind(),l.destroy(),v.destroy(),Ag(ye(),yo)}}},wv=function(r,n){var o=Am(),i=Wf(),u=Wf(),c=$f(),a=$f();return{enter:function(){n.hide();var t=fe.fromDom(v.document);Tm.getActiveApi(r.editor).each(function(n){i.set({socketHeight:Ei(r.socket,"height"),iframeHeight:Ei(n.frame(),"height"),outerScroll:v.document.body.scrollTop}),u.set({exclusives:_m.exclusive(t,"."+ad.scrollable())}),oo(r.container,vi.resolve("fullscreen-maximized")),Im(r.container,n.body()),o.maximize(),Oi(r.socket,"overflow","scroll"),Oi(r.socket,"-webkit-overflow-scrolling","touch"),bo(n.body());var e=Ee(["cWin","ceBody","socket","toolstrip","toolbar","dropup","contentElement","cursor","keyboardType","isScrolling","outerWindow","outerBody"],[]);c.set(xv(e({cWin:n.win(),ceBody:n.body(),socket:r.socket,toolstrip:r.toolstrip,toolbar:r.toolbar,dropup:r.dropup.element(),contentElement:n.frame(),cursor:I,outerBody:r.body,outerWindow:r.win,keyboardType:Fg.stubborn,isScrolling:function(){return u.get().exists(function(n){return n.socket.isScrolling()})}}))),c.run(function(n){n.syncHeight()}),a.set(Vg(n,c,r.toolstrip,r.socket,r.dropup))})},refreshStructure:function(){c.run(function(n){n.refreshStructure()})},exit:function(){o.restore(),a.clear(),c.clear(),n.show(),i.on(function(n){n.socketHeight.each(function(n){Oi(r.socket,"height",n)}),n.iframeHeight.each(function(n){Oi(r.editor.getFrame(),"height",n)}),v.document.body.scrollTop=n.scrollTop}),i.clear(),u.on(function(n){n.exclusives.unbind()}),u.clear(),io(r.container,vi.resolve("fullscreen-maximized")),Mm(),ad.deregister(r.toolbar),Di(r.socket,"overflow"),Di(r.socket,"-webkit-overflow-scrolling"),yo(r.editor.getFrame()),Tm.getActiveApi(r.editor).each(function(n){n.clearSelection()})}}},Sv=function(n){var e=Kt("Getting IosWebapp schema",Vm,n);Oi(e.toolstrip,"width","100%"),Oi(e.container,"position","relative");var t=El(Fm(function(){e.setReadOnly(e.readOnlyOnInit()),r.enter()},e.translate));e.alloy.add(t);var r=wv(e,{show:function(){e.alloy.add(t)},hide:function(){e.alloy.remove(t)}});return{setReadOnly:e.setReadOnly,refreshStructure:r.refreshStructure,enter:r.enter,exit:r.exit,destroy:I}},Ov=tinymce.util.Tools.resolve("tinymce.EditorManager"),Tv=function(n){var e=wt(n.settings,"skin_url").fold(function(){return Ov.baseURL+"/skins/lightgray"},function(n){return n});return{content:e+"/content.mobile.min.css",ui:e+"/skin.mobile.min.css"}},kv=function(n,e,t){n.system().broadcastOn([ko.formatChanged()],{command:e,state:t})},Cv=function(r,n){var e=N(n.formatter.get());wn(e,function(e){n.formatter.formatChanged(e,function(n){kv(r,e,n)})}),wn(["ul","ol"],function(t){n.selection.selectorChanged(t,function(n,e){kv(r,t,n)})})},Ev=(M(["x-small","small","medium","large","x-large"]),function(n){var e=function(){n._skinLoaded=!0,n.fire("SkinLoaded")};return function(){n.initialized?e():n.on("init",e)}}),Dv=M("toReading"),Iv=M("toEditing");Oo.add("mobile",function(D){return{getNotificationManagerImpl:function(){return{open:M({progressBar:{value:I},close:I}),close:I,reposition:I,getArgs:h}},renderUI:function(n){var e=Tv(D);0==(!1===D.settings.skin)?(D.contentCSS.push(e.content),So.DOM.styleSheetLoader.load(e.ui,Ev(D))):Ev(D)();var t,r,o,i,u,c,a,s,f,l,d,m,g,v,p,h,b,y=function(){D.fire("scrollIntoView")},x=fe.fromTag("div"),w=Gn.detect().os.isAndroid()?(s=y,f=Mg({classes:[vi.resolve("android-container")]}),l=Um(),d=$f(),m=Km(d),g=Xm(),v=dg(I,s),f.add(l.wrapper()),f.add(g),f.add(v.component()),{system:M(f),element:f.element,init:function(n){d.set(Nm(n))},exit:function(){d.run(function(n){n.exit(),Ul.remove(g,m)})},setToolbarGroups:function(n){var e=l.createGroups(n);l.setGroups(e)},setContextToolbar:function(n){var e=l.createGroups(n);l.setContextToolbar(e)},focusToolbar:function(){l.focus()},restoreToolbar:function(){l.restoreToolbar()},updateMode:function(n){Jm(g,m,n,f.root())},socket:M(g),dropup:M(v)}):(t=y,r=Mg({classes:[vi.resolve("ios-container")]}),o=Um(),i=$f(),u=Km(i),c=Xm(),a=dg(function(){i.run(function(n){n.refreshStructure()})},t),r.add(o.wrapper()),r.add(c),r.add(a.component()),{system:M(r),element:r.element,init:function(n){i.set(Sv(n))},exit:function(){i.run(function(n){Ul.remove(c,u),n.exit()})},setToolbarGroups:function(n){var e=o.createGroups(n);o.setGroups(e)},setContextToolbar:function(n){var e=o.createGroups(n);o.setContextToolbar(e)},focusToolbar:function(){o.focus()},restoreToolbar:function(){o.restoreToolbar()},updateMode:function(n){Jm(c,u,n,r.root())},socket:M(c),dropup:M(a)}),S=fe.fromDom(n.targetNode);we("element","offset"),h=x,(b=(p=S).dom(),V.from(b.nextSibling).map(fe.fromDom)).fold(function(){Le(p).each(function(n){Ge(n,h)})},function(n){var e,t;t=h,Le(e=n).each(function(n){n.dom().insertBefore(t.dom(),e.dom())})}),function(n,e){Ge(n,e.element());var t=Pe(e.element());wn(t,function(n){e.getByDom(n).each(Ke)})}(x,w.system());var O=n.targetNode.ownerDocument.defaultView,T=Bd(O,{onChange:function(){w.system().broadcastOn([ko.orientationChanged()],{width:Rd(O)})},onReady:I}),k=function(n,e,t,r){!1===r&&D.selection.collapse();var o=C(n,e,t);w.setToolbarGroups(!0===r?o.readOnly:o.main),D.setMode(!0===r?"readonly":"design"),D.fire(!0===r?Dv():Iv()),w.updateMode(r)},C=function(n,e,t){var r=n.get();return{readOnly:r.backToMask.concat(e.get()),main:r.backToMask.concat(t.get())}},E=function(n,e){return D.on(n,e),{unbind:function(){D.off(n)}}};return D.on("init",function(){w.init({editor:{getFrame:function(){return fe.fromDom(D.contentAreaContainer.querySelector("iframe"))},onDomChanged:function(){return{unbind:I}},onToReading:function(n){return E(Dv(),n)},onToEditing:function(n){return E(Iv(),n)},onScrollToCursor:function(e){return D.on("scrollIntoView",function(n){e(n)}),{unbind:function(){D.off("scrollIntoView"),T.destroy()}}},onTouchToolstrip:function(){t()},onTouchContent:function(){var n,e=fe.fromDom(D.editorContainer.querySelector("."+vi.resolve("toolbar")));(n=e,wo(n).bind(function(n){return w.system().getByDom(n).toOption()})).each(ue),w.restoreToolbar(),t()},onTapContent:function(n){var e=n.target();"img"===ge(e)?(D.selection.select(e.dom()),n.kill()):"a"===ge(e)&&w.system().getByDom(fe.fromDom(D.editorContainer)).each(function(n){lo.isAlpha(n)&&To(e.dom())})}},container:fe.fromDom(D.editorContainer),socket:fe.fromDom(D.contentAreaContainer),toolstrip:fe.fromDom(D.editorContainer.querySelector("."+vi.resolve("toolstrip"))),toolbar:fe.fromDom(D.editorContainer.querySelector("."+vi.resolve("toolbar"))),dropup:w.dropup(),alloy:w.system(),translate:I,setReadOnly:function(n){k(a,c,u,n)},readOnlyOnInit:function(){return!1}});var t=function(){w.dropup().disappear(function(){w.system().broadcastOn([ko.dropupDismissed()],{})})},n={label:"The first group",scrollable:!1,items:[Xa.forToolbar("back",function(){D.selection.collapse(),w.exit()},{})]},e={label:"Back to read only",scrollable:!1,items:[Xa.forToolbar("readonly-back",function(){k(a,c,u,!0)},{})]},r=Sd(w,D),o=Od(D.settings,r),i={label:"The extra group",scrollable:!1,items:[]},u=mo([{label:"the action group",scrollable:!0,items:o},i]),c=mo([{label:"The read only mode group",scrollable:!0,items:[]},i]),a=mo({backToMask:[n],backToReadOnly:[e]});Cv(w,D)}),D.on("remove",function(){w.exit()}),D.on("detach",function(){var e,n;e=w.system(),n=Pe(e.element()),wn(n,function(n){e.getByDom(n).each(Ye)}),qe(e.element()),w.system().destroy(),qe(x)}),{iframeContainer:w.socket().element().dom(),editorContainer:w.element().dom()}}}})}(window); \ No newline at end of file +!function(p){"use strict";var I=function(){},v=function(t,r){return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return t(r.apply(null,n))}},A=function(n){return function(){return n}},h=function(n){return n};function l(r){for(var o=[],n=1;n<arguments.length;n++)o[n-1]=arguments[n];return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];var t=o.concat(n);return r.apply(null,t)}}var n,e,t,r,o,i,u,x=function(t){return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return!t.apply(null,n)}},c=function(n){return function(){throw new Error(n)}},a=function(n){return n()},s=A(!1),f=A(!0),d=function(e){return function(n){return function(n){if(null===n)return"null";var e=typeof n;return"object"===e&&(Array.prototype.isPrototypeOf(n)||n.constructor&&"Array"===n.constructor.name)?"array":"object"===e&&(String.prototype.isPrototypeOf(n)||n.constructor&&"String"===n.constructor.name)?"string":e}(n)===e}},y=d("string"),m=d("object"),g=d("array"),b=d("boolean"),w=d("function"),T=d("number"),S=Object.prototype.hasOwnProperty,O=function(u){return function(){for(var n=new Array(arguments.length),e=0;e<n.length;e++)n[e]=arguments[e];if(0===n.length)throw new Error("Can't merge zero objects");for(var t={},r=0;r<n.length;r++){var o=n[r];for(var i in o)S.call(o,i)&&(t[i]=u(t[i],o[i]))}return t}},k=O(function(n,e){return m(n)&&m(e)?k(n,e):e}),C=O(function(n,e){return e}),E=function(){return D},D=(n=function(n){return n.isNone()},r={fold:function(n,e){return n()},is:s,isSome:s,isNone:f,getOr:t=function(n){return n},getOrThunk:e=function(n){return n()},getOrDie:function(n){throw new Error(n||"error: getOrDie called on none.")},getOrNull:A(null),getOrUndefined:A(undefined),or:t,orThunk:e,map:E,each:I,bind:E,exists:s,forall:f,filter:E,equals:n,equals_:n,toArray:function(){return[]},toString:A("none()")},Object.freeze&&Object.freeze(r),r),M=function(t){var n=A(t),e=function(){return o},r=function(n){return n(t)},o={fold:function(n,e){return e(t)},is:function(n){return t===n},isSome:f,isNone:s,getOr:n,getOrThunk:n,getOrDie:n,getOrNull:n,getOrUndefined:n,or:e,orThunk:e,map:function(n){return M(n(t))},each:function(n){n(t)},bind:r,exists:r,forall:r,filter:function(n){return n(t)?o:D},toArray:function(){return[t]},toString:function(){return"some("+t+")"},equals:function(n){return n.is(t)},equals_:function(n,e){return n.fold(s,function(n){return e(t,n)})}};return o},F={some:M,none:E,from:function(n){return null===n||n===undefined?D:M(n)}},R=Object.keys,B=function(n,e){for(var t=R(n),r=0,o=t.length;r<o;r++){var i=t[r];e(n[i],i)}},V=function(n,t){return N(n,function(n,e){return{k:e,v:t(n,e)}})},N=function(n,r){var o={};return B(n,function(n,e){var t=r(n,e);o[t.k]=t.v}),o},_=function(n,t){var r=[];return B(n,function(n,e){r.push(t(n,e))}),r},j=A("touchstart"),H=A("touchmove"),z=A("touchend"),L=A("mousedown"),P=A("mousemove"),$=A("mouseup"),W=A("mouseover"),U=A("keydown"),G=A("input"),q=A("change"),Y=A("click"),K=A("transitionend"),X=A("selectstart"),J=function(t){var r,o=!1;return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return o||(o=!0,r=t.apply(null,n)),r}},Q=function(n,e){var t=function(n,e){for(var t=0;t<n.length;t++){var r=n[t];if(r.test(e))return r}return undefined}(n,e);if(!t)return{major:0,minor:0};var r=function(n){return Number(e.replace(t,"$"+n))};return nn(r(1),r(2))},Z=function(){return nn(0,0)},nn=function(n,e){return{major:n,minor:e}},en={nu:nn,detect:function(n,e){var t=String(e).toLowerCase();return 0===n.length?Z():Q(n,t)},unknown:Z},tn="Firefox",rn=function(n,e){return function(){return e===n}},on=function(n){var e=n.current;return{current:e,version:n.version,isEdge:rn("Edge",e),isChrome:rn("Chrome",e),isIE:rn("IE",e),isOpera:rn("Opera",e),isFirefox:rn(tn,e),isSafari:rn("Safari",e)}},un={unknown:function(){return on({current:undefined,version:en.unknown()})},nu:on,edge:A("Edge"),chrome:A("Chrome"),ie:A("IE"),opera:A("Opera"),firefox:A(tn),safari:A("Safari")},cn="Windows",an="Android",sn="Solaris",fn="FreeBSD",ln=function(n,e){return function(){return e===n}},dn=function(n){var e=n.current;return{current:e,version:n.version,isWindows:ln(cn,e),isiOS:ln("iOS",e),isAndroid:ln(an,e),isOSX:ln("OSX",e),isLinux:ln("Linux",e),isSolaris:ln(sn,e),isFreeBSD:ln(fn,e)}},mn={unknown:function(){return dn({current:undefined,version:en.unknown()})},nu:dn,windows:A(cn),ios:A("iOS"),android:A(an),linux:A("Linux"),osx:A("OSX"),solaris:A(sn),freebsd:A(fn)},gn=Array.prototype.slice,pn=Array.prototype.indexOf,vn=Array.prototype.push,hn=function(n,e){return t=n,r=e,-1<pn.call(t,r);var t,r},yn=function(n,e){for(var t=n.length,r=new Array(t),o=0;o<t;o++){var i=n[o];r[o]=e(i,o)}return r},bn=function(n,e){for(var t=0,r=n.length;t<r;t++)e(n[t],t)},wn=function(n,e){for(var t=[],r=0,o=n.length;r<o;r++){var i=n[r];e(i,r)&&t.push(i)}return t},xn=function(n,e,t){return function(n,e){for(var t=n.length-1;0<=t;t--)e(n[t],t)}(n,function(n){t=e(t,n)}),t},Tn=function(n,e,t){return bn(n,function(n){t=e(t,n)}),t},Sn=function(n,e){for(var t=0,r=n.length;t<r;t++){var o=n[t];if(e(o,t))return F.some(o)}return F.none()},On=function(n,e){for(var t=0,r=n.length;t<r;t++)if(e(n[t],t))return F.some(t);return F.none()},kn=function(n){for(var e=[],t=0,r=n.length;t<r;++t){if(!g(n[t]))throw new Error("Arr.flatten item "+t+" was not an array, input: "+n);vn.apply(e,n[t])}return e},Cn=function(n,e){var t=yn(n,e);return kn(t)},En=function(n,e){for(var t=0,r=n.length;t<r;++t)if(!0!==e(n[t],t))return!1;return!0},Dn=function(n){var e=gn.call(n,0);return e.reverse(),e},In=function(n){return[n]},An=(w(Array.from)&&Array.from,function(n,e){var t=String(e).toLowerCase();return Sn(n,function(n){return n.search(t)})}),Mn=function(n,t){return An(n,t).map(function(n){var e=en.detect(n.versionRegexes,t);return{current:n.name,version:e}})},Fn=function(n,t){return An(n,t).map(function(n){var e=en.detect(n.versionRegexes,t);return{current:n.name,version:e}})},Rn=function(n,e){return-1!==n.indexOf(e)},Bn=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,Vn=function(e){return function(n){return Rn(n,e)}},Nn=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(n){return Rn(n,"edge/")&&Rn(n,"chrome")&&Rn(n,"safari")&&Rn(n,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,Bn],search:function(n){return Rn(n,"chrome")&&!Rn(n,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(n){return Rn(n,"msie")||Rn(n,"trident")}},{name:"Opera",versionRegexes:[Bn,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:Vn("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:Vn("firefox")},{name:"Safari",versionRegexes:[Bn,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(n){return(Rn(n,"safari")||Rn(n,"mobile/"))&&Rn(n,"applewebkit")}}],_n=[{name:"Windows",search:Vn("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(n){return Rn(n,"iphone")||Rn(n,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:Vn("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:Vn("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:Vn("linux"),versionRegexes:[]},{name:"Solaris",search:Vn("sunos"),versionRegexes:[]},{name:"FreeBSD",search:Vn("freebsd"),versionRegexes:[]}],jn={browsers:A(Nn),oses:A(_n)},Hn=function(n){var e,t,r,o,i,u,c,a,s,f,l,d=jn.browsers(),m=jn.oses(),g=Mn(d,n).fold(un.unknown,un.nu),p=Fn(m,n).fold(mn.unknown,mn.nu);return{browser:g,os:p,deviceType:(t=g,r=n,o=(e=p).isiOS()&&!0===/ipad/i.test(r),i=e.isiOS()&&!o,u=e.isAndroid()&&3===e.version.major,c=e.isAndroid()&&4===e.version.major,a=o||u||c&&!0===/mobile/i.test(r),s=e.isiOS()||e.isAndroid(),f=s&&!a,l=t.isSafari()&&e.isiOS()&&!1===/safari/i.test(r),{isiPad:A(o),isiPhone:A(i),isTablet:A(a),isPhone:A(f),isTouch:A(s),isAndroid:e.isAndroid,isiOS:e.isiOS,isWebView:A(l)})}},zn={detect:J(function(){var n=p.navigator.userAgent;return Hn(n)})},Ln={tap:A("alloy.tap")},Pn=A("alloy.focus"),$n=A("alloy.blur.post"),Wn=A("alloy.receive"),Un=A("alloy.execute"),Gn=A("alloy.focus.item"),qn=Ln.tap,Yn=zn.detect().deviceType.isTouch()?Ln.tap:Y,Kn=A("alloy.longpress"),Xn=A("alloy.system.init"),Jn=A("alloy.system.scroll"),Qn=A("alloy.system.attached"),Zn=A("alloy.system.detached"),ne=function(n,e){oe(n,n.element(),e,{})},ee=function(n,e,t){oe(n,n.element(),e,t)},te=function(n){ne(n,Un())},re=function(n,e,t){oe(n,e,t,{})},oe=function(n,e,t,r){var o=k({target:e},r);n.getSystem().triggerEvent(t,e,V(o,A))},ie=function(n){if(null===n||n===undefined)throw new Error("Node cannot be null or undefined");return{dom:A(n)}},ue={fromHtml:function(n,e){var t=(e||p.document).createElement("div");if(t.innerHTML=n,!t.hasChildNodes()||1<t.childNodes.length)throw p.console.error("HTML does not have a single root node",n),new Error("HTML must have a single root node");return ie(t.childNodes[0])},fromTag:function(n,e){var t=(e||p.document).createElement(n);return ie(t)},fromText:function(n,e){var t=(e||p.document).createTextNode(n);return ie(t)},fromDom:ie,fromPoint:function(n,e,t){var r=n.dom();return F.from(r.elementFromPoint(e,t)).map(ie)}},ce=(p.Node.ATTRIBUTE_NODE,p.Node.CDATA_SECTION_NODE,p.Node.COMMENT_NODE,p.Node.DOCUMENT_NODE),ae=(p.Node.DOCUMENT_TYPE_NODE,p.Node.DOCUMENT_FRAGMENT_NODE,p.Node.ELEMENT_NODE),se=p.Node.TEXT_NODE,fe=(p.Node.PROCESSING_INSTRUCTION_NODE,p.Node.ENTITY_REFERENCE_NODE,p.Node.ENTITY_NODE,p.Node.NOTATION_NODE,"undefined"!=typeof p.window?p.window:Function("return this;")()),le=function(n,e){return function(n,e){for(var t=e!==undefined&&null!==e?e:fe,r=0;r<n.length&&t!==undefined&&null!==t;++r)t=t[n[r]];return t}(n.split("."),e)},de=function(n,e){var t=le(n,e);if(t===undefined||null===t)throw new Error(n+" not available on this browser");return t},me=function(n){return n.dom().nodeName.toLowerCase()},ge=function(e){return function(n){return n.dom().nodeType===e}},pe=ge(ae),ve=ge(se),he=function(n){var e=ve(n)?n.dom().parentNode:n.dom();return e!==undefined&&null!==e&&e.ownerDocument.body.contains(e)},ye=J(function(){return be(ue.fromDom(p.document))}),be=function(n){var e=n.dom().body;if(null===e||e===undefined)throw new Error("Body is not available yet");return ue.fromDom(e)},we=function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];return function(){for(var t=[],n=0;n<arguments.length;n++)t[n]=arguments[n];if(e.length!==t.length)throw new Error('Wrong number of arguments to struct. Expected "['+e.length+']", got '+t.length+" arguments");var r={};return bn(e,function(n,e){r[n]=A(t[e])}),r}},xe=function(n){return n.slice(0).sort()},Te=function(n,e){throw new Error("All required keys ("+xe(n).join(", ")+") were not specified. Specified keys were: "+xe(e).join(", ")+".")},Se=function(n){throw new Error("Unsupported keys for object: "+xe(n).join(", "))},Oe=function(e,n){if(!g(n))throw new Error("The "+e+" fields must be an array. Was: "+n+".");bn(n,function(n){if(!y(n))throw new Error("The value "+n+" in the "+e+" fields was not a string.")})},ke=function(n){var t=xe(n);Sn(t,function(n,e){return e<t.length-1&&n===t[e+1]}).each(function(n){throw new Error("The field: "+n+" occurs more than once in the combined fields: ["+t.join(", ")+"].")})},Ce=function(o,i){var u=o.concat(i);if(0===u.length)throw new Error("You must specify at least one required or optional field.");return Oe("required",o),Oe("optional",i),ke(u),function(e){var t=R(e);En(o,function(n){return hn(t,n)})||Te(o,t);var n=wn(t,function(n){return!hn(u,n)});0<n.length&&Se(n);var r={};return bn(o,function(n){r[n]=A(e[n])}),bn(i,function(n){r[n]=A(Object.prototype.hasOwnProperty.call(e,n)?F.some(e[n]):F.none())}),r}},Ee=ae,De=ce,Ie=function(n,e){var t=n.dom();if(t.nodeType!==Ee)return!1;var r=t;if(r.matches!==undefined)return r.matches(e);if(r.msMatchesSelector!==undefined)return r.msMatchesSelector(e);if(r.webkitMatchesSelector!==undefined)return r.webkitMatchesSelector(e);if(r.mozMatchesSelector!==undefined)return r.mozMatchesSelector(e);throw new Error("Browser lacks native selectors")},Ae=function(n){return n.nodeType!==Ee&&n.nodeType!==De||0===n.childElementCount},Me=function(n,e){var t=e===undefined?p.document:e.dom();return Ae(t)?[]:yn(t.querySelectorAll(n),ue.fromDom)},Fe=function(n,e){var t=e===undefined?p.document:e.dom();return Ae(t)?F.none():F.from(t.querySelector(n)).map(ue.fromDom)},Re=function(n,e){return n.dom()===e.dom()},Be=(zn.detect().browser.isIE(),function(n){return ue.fromDom(n.dom().ownerDocument)}),Ve=function(n){return ue.fromDom(n.dom().ownerDocument.defaultView)},Ne=function(n){return F.from(n.dom().parentNode).map(ue.fromDom)},_e=function(n){return yn(n.dom().childNodes,ue.fromDom)},je=function(n){return e=0,t=n.dom().childNodes,F.from(t[e]).map(ue.fromDom);var e,t},He=(we("element","offset"),function(e,t){je(e).fold(function(){ze(e,t)},function(n){e.dom().insertBefore(t.dom(),n.dom())})}),ze=function(n,e){n.dom().appendChild(e.dom())},Le=function(e,n){bn(n,function(n){ze(e,n)})},Pe=function(n){n.dom().textContent="",bn(_e(n),function(n){$e(n)})},$e=function(n){var e=n.dom();null!==e.parentNode&&e.parentNode.removeChild(e)},We=function(n){ne(n,Zn());var e=n.components();bn(e,We)},Ue=function(n){var e=n.components();bn(e,Ue),ne(n,Qn())},Ge=function(n,e){qe(n,e,ze)},qe=function(n,e,t){n.getSystem().addToWorld(e),t(n.element(),e.element()),he(n.element())&&Ue(e),n.syncComponents()},Ye=function(n){We(n),$e(n.element()),n.getSystem().removeFromWorld(n)},Ke=function(e){var n=Ne(e.element()).bind(function(n){return e.getSystem().getByDom(n).fold(F.none,F.some)});Ye(e),n.each(function(n){n.syncComponents()})},Xe=function(t){return{is:function(n){return t===n},isValue:f,isError:s,getOr:A(t),getOrThunk:A(t),getOrDie:A(t),or:function(n){return Xe(t)},orThunk:function(n){return Xe(t)},fold:function(n,e){return e(t)},map:function(n){return Xe(n(t))},mapError:function(n){return Xe(t)},each:function(n){n(t)},bind:function(n){return n(t)},exists:function(n){return n(t)},forall:function(n){return n(t)},toOption:function(){return F.some(t)}}},Je=function(t){return{is:s,isValue:s,isError:f,getOr:h,getOrThunk:function(n){return n()},getOrDie:function(){return c(String(t))()},or:function(n){return n},orThunk:function(n){return n()},fold:function(n,e){return n(t)},map:function(n){return Je(t)},mapError:function(n){return Je(n(t))},each:I,bind:function(n){return Je(t)},exists:s,forall:f,toOption:F.none}},Qe={value:Xe,error:Je,fromOption:function(n,e){return n.fold(function(){return Je(e)},Xe)}},Ze=function(u){if(!g(u))throw new Error("cases must be an array");if(0===u.length)throw new Error("there must be at least one case");var c=[],t={};return bn(u,function(n,r){var e=R(n);if(1!==e.length)throw new Error("one and only one name per case");var o=e[0],i=n[o];if(t[o]!==undefined)throw new Error("duplicate key detected:"+o);if("cata"===o)throw new Error("cannot have a case named cata (sorry)");if(!g(i))throw new Error("case arguments must be an array");c.push(o),t[o]=function(){var n=arguments.length;if(n!==i.length)throw new Error("Wrong number of arguments to case "+o+". Expected "+i.length+" ("+i+"), got "+n);for(var t=new Array(n),e=0;e<t.length;e++)t[e]=arguments[e];return{fold:function(){if(arguments.length!==u.length)throw new Error("Wrong number of arguments to fold. Expected "+u.length+", got "+arguments.length);return arguments[r].apply(null,t)},match:function(n){var e=R(n);if(c.length!==e.length)throw new Error("Wrong number of arguments to match. Expected: "+c.join(",")+"\nActual: "+e.join(","));if(!En(c,function(n){return hn(e,n)}))throw new Error("Not all branches were specified when using match. Specified: "+e.join(", ")+"\nRequired: "+c.join(", "));return n[o].apply(null,t)},log:function(n){p.console.log(n,{constructors:c,constructor:o,params:t})}}}}),t},nt=Ze([{strict:[]},{defaultedThunk:["fallbackThunk"]},{asOption:[]},{asDefaultedOptionThunk:["fallbackThunk"]},{mergeWithThunk:["baseThunk"]}]),et=function(n){return nt.defaultedThunk(A(n))},tt=nt.strict,rt=nt.asOption,ot=nt.defaultedThunk,it=nt.mergeWithThunk,ut=(Ze([{bothErrors:["error1","error2"]},{firstError:["error1","value2"]},{secondError:["value1","error2"]},{bothValues:["value1","value2"]}]),function(n){var e=[],t=[];return bn(n,function(n){n.fold(function(n){e.push(n)},function(n){t.push(n)})}),{errors:e,values:t}}),ct=function(n){return v(Qe.error,kn)(n)},at=function(n,e){var t,r,o=ut(n);return 0<o.errors.length?ct(o.errors):(t=o.values,r=e,Qe.value(k.apply(undefined,[r].concat(t))))},st=function(n){var e=ut(n);return 0<e.errors.length?ct(e.errors):Qe.value(e.values)},ft=function(e){return function(n){return n.hasOwnProperty(e)?F.from(n[e]):F.none()}},lt=function(n,e){return ft(e)(n)},dt=function(n,e){var t={};return t[n]=e,t},mt=function(n,e){return t=n,r={},bn(e,function(n){t[n]!==undefined&&t.hasOwnProperty(n)&&(r[n]=t[n])}),r;var t,r},gt=function(n,e){return t=e,r={},B(n,function(n,e){hn(t,e)||(r[e]=n)}),r;var t,r},pt=function(n){return ft(n)},vt=function(n,e){return t=n,r=e,function(n){return ft(t)(n).getOr(r)};var t,r},ht=function(n,e){return lt(n,e)},yt=function(n,e){return dt(n,e)},bt=function(n){return e={},bn(n,function(n){e[n.key]=n.value}),e;var e},wt=function(n,e){return at(n,e)},xt=function(n,e){return r=e,(t=n).hasOwnProperty(r)&&t[r]!==undefined&&null!==t[r];var t,r},Tt=Ze([{setOf:["validator","valueType"]},{arrOf:["valueType"]},{objOf:["fields"]},{itemOf:["validator"]},{choiceOf:["key","branches"]},{thunk:["description"]},{func:["args","outputSchema"]}]),St=Ze([{field:["name","presence","type"]},{state:["name"]}]),Ot=function(){return de("JSON")},kt=function(n,e,t){return Ot().stringify(n,e,t)},Ct=function(n){return m(n)&&100<R(n).length?" removed due to size":kt(n,null,2)},Et=function(n,e){return Qe.error([{path:n,getErrorInfo:e}])},Dt=Ze([{field:["key","okey","presence","prop"]},{state:["okey","instantiator"]}]),It=function(t,r,o){return lt(r,o).fold(function(){return n=o,e=r,Et(t,function(){return'Could not find valid *strict* value for "'+n+'" in '+Ct(e)});var n,e},Qe.value)},At=function(n,e,t){var r=lt(n,e).fold(function(){return t(n)},h);return Qe.value(r)},Mt=function(o,c,n,a){return n.fold(function(i,e,n,t){var r=function(n){return t.extract(o.concat([i]),a,n).map(function(n){return dt(e,a(n))})},u=function(n){return n.fold(function(){var n=dt(e,a(F.none()));return Qe.value(n)},function(n){return t.extract(o.concat([i]),a,n).map(function(n){return dt(e,a(F.some(n)))})})};return n.fold(function(){return It(o,c,i).bind(r)},function(n){return At(c,i,n).bind(r)},function(){return(n=c,e=i,Qe.value(lt(n,e))).bind(u);var n,e},function(n){return(e=c,t=i,r=n,o=lt(e,t).map(function(n){return!0===n?r(e):n}),Qe.value(o)).bind(u);var e,t,r,o},function(n){var e=n(c);return At(c,i,A({})).map(function(n){return k(e,n)}).bind(r)})},function(n,e){var t=e(c);return Qe.value(dt(n,a(t)))})},Ft=function(r){return{extract:function(t,n,e){return r(e,n).fold(function(n){return e=n,Et(t,function(){return e});var e},Qe.value)},toString:function(){return"val"},toDsl:function(){return Tt.itemOf(r)}}},Rt=function(n){var a=Bt(n),s=xn(n,function(e,n){return n.fold(function(n){return k(e,yt(n,!0))},A(e))},{});return{extract:function(n,e,t){var r,o,i,u=b(t)?[]:(o=R(r=t),wn(o,function(n){return xt(r,n)})),c=wn(u,function(n){return!xt(s,n)});return 0===c.length?a.extract(n,e,t):(i=c,Et(n,function(){return"There are unsupported fields: ["+i.join(", ")+"] specified"}))},toString:a.toString,toDsl:a.toDsl}},Bt=function(c){return{extract:function(n,e,t){return r=n,o=t,i=e,u=yn(c,function(n){return Mt(r,o,n,i)}),at(u,{});var r,o,i,u},toString:function(){return"obj{\n"+yn(c,function(n){return n.fold(function(n,e,t,r){return n+" -> "+r.toString()},function(n,e){return"state("+n+")"})}).join("\n")+"}"},toDsl:function(){return Tt.objOf(yn(c,function(n){return n.fold(function(n,e,t,r){return St.field(n,t,r)},function(n,e){return St.state(n)})}))}}},Vt=function(t,i){var e=function(n,e){return(o=Ft(t),{extract:function(t,r,n){var e=yn(n,function(n,e){return o.extract(t.concat(["["+e+"]"]),r,n)});return st(e)},toString:function(){return"array("+o.toString()+")"},toDsl:function(){return Tt.arrOf(o)}}).extract(n,h,e);var o};return{extract:function(t,r,o){var n=R(o);return e(t,n).bind(function(n){var e=yn(n,function(n){return Dt.field(n,n,tt(),i)});return Bt(e).extract(t,r,o)})},toString:function(){return"setOf("+i.toString()+")"},toDsl:function(){return Tt.setOf(t,i)}}},Nt=A(Ft(Qe.value)),_t=Dt.state,jt=Dt.field,Ht=function(t,e,r,o,i){return ht(o,i).fold(function(){return n=o,e=i,Et(t,function(){return'The chosen schema: "'+e+'" did not exist in branches: '+Ct(n)});var n,e},function(n){return Bt(n).extract(t.concat(["branch: "+i]),e,r)})},zt=function(o,i){return{extract:function(e,t,r){return ht(r,o).fold(function(){return n=o,Et(e,function(){return'Choice schema did not contain choice key: "'+n+'"'});var n},function(n){return Ht(e,t,r,i,n)})},toString:function(){return"chooseOn("+o+"). Possible values: "+R(i)},toDsl:function(){return Tt.choiceOf(o,i)}}},Lt=Ft(Qe.value),Pt=function(n,e,t,r){return e.extract([n],t,r).fold(function(n){return Qe.error({input:r,errors:n})},Qe.value)},$t=function(n,e,t){return Pt(n,e,A,t)},Wt=function(n){return n.fold(function(n){throw new Error(qt(n))},h)},Ut=function(n,e,t){return Wt(Pt(n,e,h,t))},Gt=function(n,e,t){return Wt($t(n,e,t))},qt=function(n){return"Errors: \n"+(e=n.errors,t=10<e.length?e.slice(0,10).concat([{path:[],getErrorInfo:function(){return"... (only showing first ten failures)"}}]):e,yn(t,function(n){return"Failed path: ("+n.path.join(" > ")+")\n"+n.getErrorInfo()}))+"\n\nInput object: "+Ct(n.input);var e,t},Yt=function(n,e){return zt(n,e)},Kt=A(Lt),Xt=(o=w,i="function",Ft(function(n){var e=typeof n;return o(n)?Qe.value(n):Qe.error("Expected type: "+i+" but got: "+e)})),Jt=function(n){return jt(n,n,tt(),Nt())},Qt=function(n,e){return jt(n,n,tt(),e)},Zt=function(n){return Qt(n,Xt)},nr=function(n,e){return jt(n,n,tt(),Bt(e))},er=function(n){return jt(n,n,rt(),Nt())},tr=function(n,e){return jt(n,n,rt(),Bt(e))},rr=function(n,e){return jt(n,n,rt(),Rt(e))},or=function(n,e){return jt(n,n,et(e),Nt())},ir=function(n,e,t){return jt(n,n,et(e),t)},ur=function(n,e){return _t(n,e)},cr=function(n){if(!xt(n,"can")&&!xt(n,"abort")&&!xt(n,"run"))throw new Error("EventHandler defined by: "+kt(n,null,2)+" does not have can, abort, or run!");return Ut("Extracting event.handler",Rt([or("can",A(!0)),or("abort",A(!1)),or("run",I)]),n)},ar=function(t){var e,r,o,i,n=(e=t,r=function(n){return n.can},function(){for(var t=[],n=0;n<arguments.length;n++)t[n]=arguments[n];return Tn(e,function(n,e){return n&&r(e).apply(undefined,t)},!0)}),u=(o=t,i=function(n){return n.abort},function(){for(var t=[],n=0;n<arguments.length;n++)t[n]=arguments[n];return Tn(o,function(n,e){return n||i(e).apply(undefined,t)},!1)});return cr({can:n,abort:u,run:function(){for(var e=[],n=0;n<arguments.length;n++)e[n]=arguments[n];bn(t,function(n){n.run.apply(undefined,e)})}})},sr=function(n){return bt(n)},fr=function(n,e){return{key:n,value:cr({abort:e})}},lr=function(n,e){return{key:n,value:cr({run:e})}},dr=function(n,e,t){return{key:n,value:cr({run:function(n){e.apply(undefined,[n].concat(t))}})}},mr=function(n){return function(r){return{key:n,value:cr({run:function(n,e){var t;t=e,Re(n.element(),t.event().target())&&r(n,e)}})}}},gr=function(n,e,t){var u,r,o=e.partUids()[t];return r=o,lr(u=n,function(n,i){n.getSystem().getByUid(r).each(function(n){var e,t,r,o;t=(e=n).element(),r=u,o=i,e.getSystem().triggerEvent(r,t,o.event())})})},pr=function(n){return lr(n,function(n,e){e.cut()})},vr=mr(Qn()),hr=mr(Zn()),yr=mr(Xn()),br=(u=Un(),function(n){return lr(u,n)}),wr=function(n){return yn(n,function(n){return r=e="/*",o=(t=n).length-e.length,""!==r&&(t.length<r.length||t.substr(o,o+r.length)!==r)?n:n.substring(0,n.length-"/*".length);var e,t,r,o})},xr=function(n,e){var t=n.toString(),r=t.indexOf(")")+1,o=t.indexOf("("),i=t.substring(o+1,r-1).split(/,\s*/);return n.toFunctionAnnotation=function(){return{name:e,parameters:wr(i)}},n},Tr=Ce(["tag"],["classes","attributes","styles","value","innerHtml","domChildren","defChildren"]),Sr=function(n){return{tag:n.tag(),classes:n.classes().getOr([]),attributes:n.attributes().getOr({}),styles:n.styles().getOr({}),value:n.value().getOr("<none>"),innerHtml:n.innerHtml().getOr("<none>"),defChildren:n.defChildren().fold(function(){return"<none>"},function(n){return kt(n,null,2)}),domChildren:n.domChildren().fold(function(){return"<none>"},function(n){return 0===n.length?"0 children, but still specified":String(n.length)})}},Or=Ce([],["classes","attributes","styles","value","innerHtml","defChildren","domChildren"]),kr=function(e,n,t){return n.fold(function(){return t.fold(function(){return{}},function(n){return yt(e,n)})},function(n){return t.fold(function(){return yt(e,n)},function(n){return yt(e,n)})})},Cr=function(t,r,o){return yr(function(n,e){o(n,t,r)})},Er=function(n,e,t,r,o,i){var u,c,a=n,s=tr(e,[(u="config",c=n,jt(u,u,rt(),c))]);return Ar(a,s,e,t,r,o,i)},Dr=function(o,i,u){var n,e,t,r,c,a;return n=function(t){for(var n=[],e=1;e<arguments.length;e++)n[e-1]=arguments[e];var r=[t].concat(n);return t.config({name:A(o)}).fold(function(){throw new Error("We could not find any behaviour configuration for: "+o+". Using API: "+u)},function(n){var e=Array.prototype.slice.call(r,1);return i.apply(undefined,[t,n.config,n.state].concat(e))})},e=u,t=i.toString(),r=t.indexOf(")")+1,c=t.indexOf("("),a=t.substring(c+1,r-1).split(/,\s*/),n.toFunctionAnnotation=function(){return{name:e,parameters:wr(a.slice(0,1).concat(a.slice(3)))}},n},Ir=function(n){return{key:n,value:undefined}},Ar=function(t,n,r,o,e,i,u){var c=function(n){return xt(n,r)?n[r]():F.none()},a=V(e,function(n,e){return Dr(r,n,e)}),s=V(i,function(n,e){return xr(n,e)}),f=k(s,a,{revoke:l(Ir,r),config:function(n){var e=Gt(r+"-config",t,n);return{key:r,value:{config:e,me:f,configAsRaw:J(function(){return Ut(r+"-config",t,n)}),initialConfig:n,state:u}}},schema:function(){return n},exhibit:function(n,t){return c(n).bind(function(e){return ht(o,"exhibit").map(function(n){return n(t,e.config,e.state)})}).getOr(Or({}))},name:function(){return r},handlers:function(n){return c(n).bind(function(e){return ht(o,"events").map(function(n){return n(e.config,e.state)})}).getOr({})}});return f},Mr=function(n,e){return Fr(n,e,{validate:w,label:"function"})},Fr=function(r,o,i){if(0===o.length)throw new Error("You must specify at least one required field.");return Oe("required",o),ke(o),function(e){var t=R(e);En(o,function(n){return hn(t,n)})||Te(o,t),r(o,t);var n=wn(o,function(n){return!i.validate(e[n],n)});return 0<n.length&&function(n,e){throw new Error("All values need to be of type: "+e+". Keys ("+xe(n).join(", ")+") were not.")}(n,i.label),e}},Rr=function(e,n){var t=wn(n,function(n){return!hn(e,n)});0<t.length&&Se(t)},Br=I,Vr=function(n){return Mr(Rr,n)},Nr={init:function(){return _r({readState:function(){return"No State required"}})}},_r=function(n){return Mr(Br,["readState"])(n),n},jr=function(n){return bt(n)},Hr=Rt([Jt("fields"),Jt("name"),or("active",{}),or("apis",{}),or("state",Nr),or("extra",{})]),zr=function(n){var e,t,r,o,i,u,c,a,s=Ut("Creating behaviour: "+n.name,Hr,n);return e=s.fields,t=s.name,r=s.active,o=s.apis,i=s.extra,u=s.state,c=Rt(e),a=tr(t,[rr("config",e)]),Ar(c,a,t,r,o,i,u)},Lr=Rt([Jt("branchKey"),Jt("branches"),Jt("name"),or("active",{}),or("apis",{}),or("state",Nr),or("extra",{})]),Pr=A(undefined),$r=function(n,e,t){if(!(y(t)||b(t)||T(t)))throw p.console.error("Invalid call to Attr.set. Key ",e,":: Value ",t,":: Element ",n),new Error("Attribute value was not simple");n.setAttribute(e,t+"")},Wr=function(n,e,t){$r(n.dom(),e,t)},Ur=function(n,e){var t=n.dom();B(e,function(n,e){$r(t,e,n)})},Gr=function(n,e){var t=n.dom().getAttribute(e);return null===t?undefined:t},qr=function(n,e){var t=n.dom();return!(!t||!t.hasAttribute)&&t.hasAttribute(e)},Yr=function(n,e){n.dom().removeAttribute(e)},Kr=function(n,e){var t=Gr(n,e);return t===undefined||""===t?[]:t.split(" ")},Xr=function(n){return n.dom().classList!==undefined},Jr=function(n){return Kr(n,"class")},Qr=function(n,e){return o=e,i=Kr(t=n,r="class").concat([o]),Wr(t,r,i.join(" ")),!0;var t,r,o,i},Zr=function(n,e){return o=e,0<(i=wn(Kr(t=n,r="class"),function(n){return n!==o})).length?Wr(t,r,i.join(" ")):Yr(t,r),!1;var t,r,o,i},no=function(n,e){Xr(n)?n.dom().classList.add(e):Qr(n,e)},eo=function(n,e){var t;Xr(n)?n.dom().classList.remove(e):Zr(n,e),0===(Xr(t=n)?t.dom().classList:Jr(t)).length&&Yr(t,"class")},to=function(n,e){return Xr(n)?n.dom().classList.toggle(e):(r=e,hn(Jr(t=n),r)?Zr(t,r):Qr(t,r));var t,r},ro=function(n,e){return Xr(n)&&n.dom().classList.contains(e)},oo=function(n,e,t){eo(n,t),no(n,e)},io=Object.freeze({toAlpha:function(n,e,t){oo(n.element(),e.alpha(),e.omega())},toOmega:function(n,e,t){oo(n.element(),e.omega(),e.alpha())},isAlpha:function(n,e,t){return ro(n.element(),e.alpha())},isOmega:function(n,e,t){return ro(n.element(),e.omega())},clear:function(n,e,t){eo(n.element(),e.alpha()),eo(n.element(),e.omega())}}),uo=[Jt("alpha"),Jt("omega")],co=zr({fields:uo,name:"swapping",apis:io}),ao=function(n){var e=n,t=function(){return e};return{get:t,set:function(n){e=n},clone:function(){return ao(t())}}};function so(n,e,t,r,o){return n(t,r)?F.some(t):w(o)&&o(t)?F.none():e(t,r,o)}var fo=function(n,e,t){for(var r=n.dom(),o=w(t)?t:A(!1);r.parentNode;){r=r.parentNode;var i=ue.fromDom(r);if(e(i))return F.some(i);if(o(i))break}return F.none()},lo=function(n,e,t){return so(function(n,e){return e(n)},fo,n,e,t)},mo=function(n){n.dom().focus()},go=function(n){n.dom().blur()},po=function(n){var e=n!==undefined?n.dom():p.document;return F.from(e.activeElement).map(ue.fromDom)},vo=function(e){return po(Be(e)).filter(function(n){return e.dom().contains(n.dom())})},ho=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),yo=tinymce.util.Tools.resolve("tinymce.ThemeManager"),bo=function(n){var e=p.document.createElement("a");e.target="_blank",e.href=n.href,e.rel="noreferrer noopener";var t=p.document.createEvent("MouseEvents");t.initMouseEvent("click",!0,!0,p.window,0,0,0,0,0,!1,!1,!1,!1,0,null),p.document.body.appendChild(e),e.dispatchEvent(t),p.document.body.removeChild(e)},wo={formatChanged:A("formatChanged"),orientationChanged:A("orientationChanged"),dropupDismissed:A("dropupDismissed")},xo=function(n){return n.dom().innerHTML},To=function(n,e){var t,r,o=Be(n).dom(),i=ue.fromDom(o.createDocumentFragment()),u=(t=e,(r=(o||p.document).createElement("div")).innerHTML=t,_e(ue.fromDom(r)));Le(i,u),Pe(n),ze(n,i)},So=function(n){return e=n,t=!1,ue.fromDom(e.dom().cloneNode(t));var e,t},Oo=function(n){var e,t,r,o=So(n);return e=o,t=ue.fromTag("div"),r=ue.fromDom(e.dom().cloneNode(!0)),ze(t,r),xo(t)},ko=function(n){return Oo(n)},Co=Object.freeze({events:function(c){return sr([lr(Wn(),function(o,i){var n,e,u=c.channels(),t=R(u),r=(n=t,(e=i).universal()?n:wn(n,function(n){return hn(e.channels(),n)}));bn(r,function(n){var e=u[n](),t=e.schema(),r=Gt("channel["+n+"] data\nReceiver: "+ko(o.element()),t,i.data());e.onReceive()(o,r)})})])}}),Eo=function(n){for(var e=[],t=function(n){e.push(n)},r=0;r<n.length;r++)n[r].each(t);return e},Do="unknown",Io=[],Ao=["alloy/data/Fields","alloy/debugging/Debugging"],Mo={logEventCut:I,logEventStopped:I,logNoParent:I,logEventNoHandlers:I,logEventResponse:I,write:I},Fo=function(n,e,t){var r,o="*"===Io||hn(Io,n)?(r=[],{logEventCut:function(n,e,t){r.push({outcome:"cut",target:e,purpose:t})},logEventStopped:function(n,e,t){r.push({outcome:"stopped",target:e,purpose:t})},logNoParent:function(n,e,t){r.push({outcome:"no-parent",target:e,purpose:t})},logEventNoHandlers:function(n,e){r.push({outcome:"no-handlers-left",target:e})},logEventResponse:function(n,e,t){r.push({outcome:"response",purpose:t,target:e})},write:function(){hn(["mousemove","mouseover","mouseout",Xn()],n)||p.console.log(n,{event:n,target:e.dom(),sequence:yn(r,function(n){return hn(["cut","stopped","response"],n.outcome)?"{"+n.purpose+"} "+n.outcome+" at ("+ko(n.target)+")":n.outcome})})}}):Mo,i=t(o);return o.write(),i},Ro=A([Jt("menu"),Jt("selectedMenu")]),Bo=A([Jt("item"),Jt("selectedItem")]),Vo=(A(Rt(Bo().concat(Ro()))),A(Rt(Bo()))),No=nr("initSize",[Jt("numColumns"),Jt("numRows")]),_o=function(n,e,t){var r;return function(){var n=new Error;if(n.stack!==undefined){var e=n.stack.split("\n");Sn(e,function(e){return 0<e.indexOf("alloy")&&!function(n,e){for(var t=0,r=n.length;t<r;t++)if(e(n[t],t))return!0;return!1}(Ao,function(n){return-1<e.indexOf(n)})}).getOr(Do)}}(),jt(e,e,t,(r=function(t){return Qe.value(function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];return t.apply(undefined,n)})},Ft(function(n){return r(n)})))},jo=function(n){return _o(0,n,et(I))},Ho=function(n){return _o(0,n,et(F.none))},zo=function(n){return _o(0,n,tt())},Lo=function(n){return _o(0,n,tt())},Po=function(n,e){return ur(n,A(e))},$o=function(n){return ur(n,h)},Wo=A(No),Uo=[Qt("channels",Vt(Qe.value,Rt([zo("onReceive"),or("schema",Kt())])))],Go=zr({fields:Uo,name:"receiving",active:Co}),qo=function(n,e){var t=Jo(n,e),r=e.aria();r.update()(n,r,t)},Yo=function(n,e,t){to(n.element(),e.toggleClass()),qo(n,e)},Ko=function(n,e,t){no(n.element(),e.toggleClass()),qo(n,e)},Xo=function(n,e,t){eo(n.element(),e.toggleClass()),qo(n,e)},Jo=function(n,e){return ro(n.element(),e.toggleClass())},Qo=function(n,e,t){(e.selected()?Ko:Xo)(n,e,t)},Zo=Object.freeze({onLoad:Qo,toggle:Yo,isOn:Jo,on:Ko,off:Xo}),ni=Object.freeze({exhibit:function(n,e,t){return Or({})},events:function(n,e){var t,r,o,i=(t=n,r=e,o=Yo,br(function(n){o(n,t,r)})),u=Cr(n,e,Qo);return sr(kn([n.toggleOnExecute()?[i]:[],[u]]))}}),ei=function(n,e,t){Wr(n.element(),"aria-expanded",t)},ti=[or("selected",!1),Jt("toggleClass"),or("toggleOnExecute",!0),ir("aria",{mode:"none"},Yt("mode",{pressed:[or("syncWithExpanded",!1),Po("update",function(n,e,t){Wr(n.element(),"aria-pressed",t),e.syncWithExpanded()&&ei(n,e,t)})],checked:[Po("update",function(n,e,t){Wr(n.element(),"aria-checked",t)})],expanded:[Po("update",ei)],selected:[Po("update",function(n,e,t){Wr(n.element(),"aria-selected",t)})],none:[Po("update",I)]}))],ri=zr({fields:ti,name:"toggling",active:ni,apis:Zo}),oi=function(t,r){return Go.config({channels:yt(wo.formatChanged(),{onReceive:function(n,e){e.command===t&&r(n,e.state)}})})},ii=function(n){return Go.config({channels:yt(wo.orientationChanged(),{onReceive:n})})},ui=function(n,e){return{key:n,value:{onReceive:e}}},ci="tinymce-mobile",ai={resolve:function(n){return ci+"-"+n},prefix:A(ci)},si=function(n,e){e.ignore()||(mo(n.element()),e.onFocus()(n))},fi=Object.freeze({focus:si,blur:function(n,e){e.ignore()||go(n.element())},isFocused:function(n){return e=n.element(),t=Be(e).dom(),e.dom()===t.activeElement;var e,t}}),li=Object.freeze({exhibit:function(n,e){return e.ignore()?Or({}):Or({attributes:{tabindex:"-1"}})},events:function(t){return sr([lr(Pn(),function(n,e){si(n,t),e.stop()})])}}),di=[jo("onFocus"),or("ignore",!1)],mi=zr({fields:di,name:"focusing",active:li,apis:fi}),gi=function(n){return n.style!==undefined&&w(n.style.getPropertyValue)},pi=function(n,e,t){if(!y(t))throw p.console.error("Invalid call to CSS.set. Property ",e,":: Value ",t,":: Element ",n),new Error("CSS value must be a string: "+t);gi(n)&&n.style.setProperty(e,t)},vi=function(n,e,t){var r=n.dom();pi(r,e,t)},hi=function(n,e){var t=n.dom();B(e,function(n,e){pi(t,e,n)})},yi=function(n,e){var t=n.dom(),r=p.window.getComputedStyle(t).getPropertyValue(e),o=""!==r||he(n)?r:bi(t,e);return null===o?undefined:o},bi=function(n,e){return gi(n)?n.style.getPropertyValue(e):""},wi=function(n,e){var t=n.dom(),r=bi(t,e);return F.from(r).filter(function(n){return 0<n.length})},xi=function(n,e){var t,r,o=n.dom();r=e,gi(t=o)&&t.style.removeProperty(r),qr(n,"style")&&""===Gr(n,"style").replace(/^\s+|\s+$/g,"")&&Yr(n,"style")},Ti=function(n){return n.dom().offsetWidth};function Si(r,o){var n=function(n){var e=o(n);if(e<=0||null===e){var t=yi(n,r);return parseFloat(t)||0}return e},i=function(o,n){return Tn(n,function(n,e){var t=yi(o,e),r=t===undefined?0:parseInt(t,10);return isNaN(r)?n:n+r},0)};return{set:function(n,e){if(!T(e)&&!e.match(/^[0-9]+$/))throw new Error(r+".set accepts only positive integer values. Value was "+e);var t=n.dom();gi(t)&&(t.style[r]=e+"px")},get:n,getOuter:n,aggregate:i,max:function(n,e,t){var r=i(n,t);return r<e?e-r:0}}}var Oi,ki,Ci=Si("height",function(n){var e=n.dom();return he(n)?e.getBoundingClientRect().height:e.offsetHeight}),Ei=function(n){return Ci.get(n)},Di=function(n,e,t){return wn(function(n,e){for(var t=w(e)?e:s,r=n.dom(),o=[];null!==r.parentNode&&r.parentNode!==undefined;){var i=r.parentNode,u=ue.fromDom(i);if(o.push(u),!0===t(u))break;r=i}return o}(n,t),e)},Ii=function(n,e){return wn(Ne(t=n).map(_e).map(function(n){return wn(n,function(n){return!Re(t,n)})}).getOr([]),e);var t},Ai=function(n,e){return Me(e,n)},Mi=function(n){return Fe(n)},Fi=function(n,e,t){return fo(n,function(n){return Ie(n,e)},t)},Ri=function(n,e){return Fe(e,n)},Bi=function(n,e,t){return so(Ie,Fi,n,e,t)},Vi=function(n,e,t){var r=Dn(n.slice(0,e)),o=Dn(n.slice(e+1));return Sn(r.concat(o),t)},Ni=function(n,e,t){var r=Dn(n.slice(0,e));return Sn(r,t)},_i=function(n,e,t){var r=n.slice(0,e),o=n.slice(e+1);return Sn(o.concat(r),t)},ji=function(n,e,t){var r=n.slice(e+1);return Sn(r,t)},Hi=function(t){return function(n){var e=n.raw();return hn(t,e.which)}},zi=function(n){return function(e){return En(n,function(n){return n(e)})}},Li=function(n){return!0===n.raw().shiftKey},Pi=function(n){return!0===n.raw().ctrlKey},$i=x(Li),Wi=function(n,e){return{matches:n,classification:e}},Ui=function(n,e,t,r){var o=n+e;return r<o?t:o<t?r:o},Gi=function(n,e,t){return n<=e?e:t<=n?t:n},qi=function(e,t,n){var r=Ai(e.element(),"."+t.highlightClass());bn(r,function(n){eo(n,t.highlightClass()),e.getSystem().getByDom(n).each(function(n){t.onDehighlight()(e,n)})})},Yi=function(n,e,t,r){var o=Ki(n,e,t,r);qi(n,e),no(r.element(),e.highlightClass()),o||e.onHighlight()(n,r)},Ki=function(n,e,t,r){return ro(r.element(),e.highlightClass())},Xi=function(n,e,t,r){var o=Ai(n.element(),"."+e.itemClass());return F.from(o[r]).fold(function(){return Qe.error("No element found with index "+r)},n.getSystem().getByDom)},Ji=function(e,n,t){return Ri(e.element(),"."+n.itemClass()).bind(function(n){return e.getSystem().getByDom(n).toOption()})},Qi=function(e,n,t){var r=Ai(e.element(),"."+n.itemClass());return(0<r.length?F.some(r[r.length-1]):F.none()).bind(function(n){return e.getSystem().getByDom(n).toOption()})},Zi=function(t,e,n,r){var o=Ai(t.element(),"."+e.itemClass());return On(o,function(n){return ro(n,e.highlightClass())}).bind(function(n){var e=Ui(n,r,0,o.length-1);return t.getSystem().getByDom(o[e]).toOption()})},nu=Object.freeze({dehighlightAll:qi,dehighlight:function(n,e,t,r){var o=Ki(n,e,t,r);eo(r.element(),e.highlightClass()),o&&e.onDehighlight()(n,r)},highlight:Yi,highlightFirst:function(e,t,r){Ji(e,t).each(function(n){Yi(e,t,r,n)})},highlightLast:function(e,t,r){Qi(e,t).each(function(n){Yi(e,t,r,n)})},highlightAt:function(e,t,r,n){Xi(e,t,r,n).fold(function(n){throw new Error(n)},function(n){Yi(e,t,r,n)})},highlightBy:function(e,t,r,n){var o=Ai(e.element(),"."+t.itemClass()),i=Eo(yn(o,function(n){return e.getSystem().getByDom(n).toOption()}));Sn(i,n).each(function(n){Yi(e,t,r,n)})},isHighlighted:Ki,getHighlighted:function(e,n,t){return Ri(e.element(),"."+n.highlightClass()).bind(function(n){return e.getSystem().getByDom(n).toOption()})},getFirst:Ji,getLast:Qi,getPrevious:function(n,e,t){return Zi(n,e,0,-1)},getNext:function(n,e,t){return Zi(n,e,0,1)}}),eu=[Jt("highlightClass"),Jt("itemClass"),jo("onHighlight"),jo("onDehighlight")],tu=zr({fields:eu,name:"highlighting",apis:nu}),ru=function(){return{get:function(n){return vo(n.element())},set:function(n,e){n.getSystem().triggerFocus(e,n.element())}}},ou=function(n,e,c,t,r,i){var u=function(e,t,r,o){var n,i,u=c(e,t,r,o);return(n=u,i=t.event(),Sn(n,function(n){return n.matches(i)}).map(function(n){return n.classification})).bind(function(n){return n(e,t,r,o)})},o={schema:function(){return n.concat([or("focusManager",ru()),Po("handler",o),Po("state",e)])},processKey:u,toEvents:function(r,o){var n=t(r,o),e=sr(i.map(function(t){return lr(Pn(),function(n,e){t(n,r,o,e),e.stop()})}).toArray().concat([lr(U(),function(n,e){u(n,e,r,o).each(function(n){e.stop()})})]));return k(n,e)},toApis:r};return o},iu=function(n){var e=[er("onEscape"),er("onEnter"),or("selector",'[data-alloy-tabstop="true"]'),or("firstTabstop",0),or("useTabstopAt",A(!0)),er("visibilitySelector")].concat([n]),u=function(n,e){var t=n.visibilitySelector().bind(function(n){return Bi(e,n)}).getOr(e);return 0<Ei(t)},c=function(e,n,t,r,o){return o(n,t,function(n){return u(e=r,t=n)&&e.useTabstopAt()(t);var e,t}).fold(function(){return r.cyclic()?F.some(!0):F.none()},function(n){return r.focusManager().set(e,n),F.some(!0)})},i=function(e,n,t,r){var o,i,u=Ai(e.element(),t.selector());return(o=e,i=t,i.focusManager().get(o).bind(function(n){return Bi(n,i.selector())})).bind(function(n){return On(u,l(Re,n)).bind(function(n){return c(e,u,n,t,r)})})},t=A([Wi(zi([Li,Hi([9])]),function(n,e,t,r){var o=t.cyclic()?Vi:Ni;return i(n,0,t,o)}),Wi(Hi([9]),function(n,e,t,r){var o=t.cyclic()?_i:ji;return i(n,0,t,o)}),Wi(Hi([27]),function(e,t,n,r){return n.onEscape().bind(function(n){return n(e,t)})}),Wi(zi([$i,Hi([13])]),function(e,t,n,r){return n.onEnter().bind(function(n){return n(e,t)})})]),r=A({}),o=A({});return ou(e,Nr.init,t,r,o,F.some(function(e,t){var n,r,o,i;(n=e,r=t,o=Ai(n.element(),r.selector()),i=wn(o,function(n){return u(r,n)}),F.from(i[r.firstTabstop()])).each(function(n){t.focusManager().set(e,n)})}))},uu=iu(ur("cyclic",A(!1))),cu=iu(ur("cyclic",A(!0))),au=function(n){return"input"===me(n)&&"radio"!==Gr(n,"type")||"textarea"===me(n)},su=function(n,e,t){return au(t)&&Hi([32])(e.event())?F.none():(re(n,t,Un()),F.some(!0))},fu=[or("execute",su),or("useSpace",!1),or("useEnter",!0),or("useControlEnter",!1),or("useDown",!1)],lu=function(n,e,t){return t.execute()(n,e,n.element())},du=A({}),mu=A({}),gu=ou(fu,Nr.init,function(n,e,t,r){var o=t.useSpace()&&!au(n.element())?[32]:[],i=t.useEnter()?[13]:[],u=t.useDown()?[40]:[],c=o.concat(i).concat(u);return[Wi(Hi(c),lu)].concat(t.useControlEnter()?[Wi(zi([Pi,Hi([13])]),lu)]:[])},du,mu,F.none()),pu=function(n){var t=ao(F.none());return _r({readState:A({}),setGridSize:function(n,e){t.set(F.some({numRows:A(n),numColumns:A(e)}))},getNumRows:function(){return t.get().map(function(n){return n.numRows()})},getNumColumns:function(){return t.get().map(function(n){return n.numColumns()})}})},vu=Object.freeze({flatgrid:pu,init:function(n){return n.state()(n)}}),hu=function(e,t){return function(n){return"rtl"===yu(n)?t:e}},yu=function(n){return"rtl"===yi(n,"direction")?"rtl":"ltr"},bu=function(i){return function(n,e,t,r){var o=i(n.element());return Su(o,n,e,t,r)}},wu=function(n,e){var t=hu(n,e);return bu(t)},xu=function(n,e){var t=hu(e,n);return bu(t)},Tu=function(o){return function(n,e,t,r){return Su(o,n,e,t,r)}},Su=function(e,t,n,r,o){return r.focusManager().get(t).bind(function(n){return e(t.element(),n,r,o)}).map(function(n){return r.focusManager().set(t,n),!0})},Ou=Tu,ku=Tu,Cu=Tu,Eu=function(n){var e,t=n.dom();return!((e=t).offsetWidth<=0&&e.offsetHeight<=0)},Du=Ce(["index","candidates"],[]),Iu=function(n,e,t){return Au(n,e,t)},Au=function(n,e,t,r){var o,i=l(Re,e),u=Ai(n,t),c=wn(u,Eu);return On(o=c,i).map(function(n){return Du({index:n,candidates:o})})},Mu=function(n,e){return On(n,function(n){return Re(e,n)})},Fu=function(t,n,r,e){return e(Math.floor(n/r),n%r).bind(function(n){var e=n.row()*r+n.column();return 0<=e&&e<t.length?F.some(t[e]):F.none()})},Ru=function(o,n,i,u,c){return Fu(o,n,u,function(n,e){var t=n===i-1?o.length-n*u:u,r=Ui(e,c,0,t-1);return F.some({row:A(n),column:A(r)})})},Bu=function(i,n,u,c,a){return Fu(i,n,c,function(n,e){var t=Ui(n,a,0,u-1),r=t===u-1?i.length-t*c:c,o=Gi(e,0,r-1);return F.some({row:A(t),column:A(o)})})},Vu=[Jt("selector"),or("execute",su),Ho("onEscape"),or("captureTab",!1),Wo()],Nu=function(o){return function(n,e,t,r){return Iu(n,e,t.selector()).bind(function(n){return o(n.candidates(),n.index(),r.getNumRows().getOr(t.initSize().numRows()),r.getNumColumns().getOr(t.initSize().numColumns()))})}},_u=function(n,e,t,r){return t.captureTab()?F.some(!0):F.none()},ju=Nu(function(n,e,t,r){return Ru(n,e,t,r,-1)}),Hu=Nu(function(n,e,t,r){return Ru(n,e,t,r,1)}),zu=Nu(function(n,e,t,r){return Bu(n,e,t,r,-1)}),Lu=Nu(function(n,e,t,r){return Bu(n,e,t,r,1)}),Pu=A([Wi(Hi([37]),wu(ju,Hu)),Wi(Hi([39]),xu(ju,Hu)),Wi(Hi([38]),Ou(zu)),Wi(Hi([40]),ku(Lu)),Wi(zi([Li,Hi([9])]),_u),Wi(zi([$i,Hi([9])]),_u),Wi(Hi([27]),function(n,e,t,r){return t.onEscape()(n,e)}),Wi(Hi([32].concat([13])),function(e,t,r,n){return(o=e,i=r,i.focusManager().get(o).bind(function(n){return Bi(n,i.selector())})).bind(function(n){return r.execute()(e,t,n)});var o,i})]),$u=A({}),Wu=ou(Vu,pu,Pu,$u,{},F.some(function(e,t,n){Ri(e.element(),t.selector()).each(function(n){t.focusManager().set(e,n)})})),Uu=function(n,e,t,o){return Iu(n,t,e).bind(function(n){var e=n.index(),t=n.candidates(),r=Ui(e,o,0,t.length-1);return F.from(t[r])})},Gu=[Jt("selector"),or("getInitial",F.none),or("execute",su),or("executeOnMove",!1),or("allowVertical",!0)],qu=function(e,t,r){return(n=e,o=r,o.focusManager().get(n).bind(function(n){return Bi(n,o.selector())})).bind(function(n){return r.execute()(e,t,n)});var n,o},Yu=function(n,e,t){return Uu(n,t.selector(),e,-1)},Ku=function(n,e,t){return Uu(n,t.selector(),e,1)},Xu=function(r){return function(n,e,t){return r(n,e,t).bind(function(){return t.executeOnMove()?qu(n,e,t):F.some(!0)})}},Ju=A({}),Qu=A({}),Zu=ou(Gu,Nr.init,function(n,e,t,r){var o=[37].concat(t.allowVertical()?[38]:[]),i=[39].concat(t.allowVertical()?[40]:[]);return[Wi(Hi(o),Xu(wu(Yu,Ku))),Wi(Hi(i),Xu(xu(Yu,Ku))),Wi(Hi([13]),qu),Wi(Hi([32]),qu)]},Ju,Qu,F.some(function(e,t){t.getInitial()(e).or(Ri(e.element(),t.selector())).each(function(n){t.focusManager().set(e,n)})})),nc=Ce(["rowIndex","columnIndex","cell"],[]),ec=function(n,e,t){return F.from(n[e]).bind(function(n){return F.from(n[t]).map(function(n){return nc({rowIndex:e,columnIndex:t,cell:n})})})},tc=function(n,e,t,r){var o=n[e].length,i=Ui(t,r,0,o-1);return ec(n,e,i)},rc=function(n,e,t,r){var o=Ui(t,r,0,n.length-1),i=n[o].length,u=Gi(e,0,i-1);return ec(n,o,u)},oc=function(n,e,t,r){var o=n[e].length,i=Gi(t+r,0,o-1);return ec(n,e,i)},ic=function(n,e,t,r){var o=Gi(t+r,0,n.length-1),i=n[o].length,u=Gi(e,0,i-1);return ec(n,o,u)},uc=[nr("selectors",[Jt("row"),Jt("cell")]),or("cycles",!0),or("previousSelector",F.none),or("execute",su)],cc=function(n,e){return function(t,r,i){var u=i.cycles()?n:e;return Bi(r,i.selectors().row()).bind(function(n){var e=Ai(n,i.selectors().cell());return Mu(e,r).bind(function(r){var o=Ai(t,i.selectors().row());return Mu(o,n).bind(function(n){var e,t=(e=i,yn(o,function(n){return Ai(n,e.selectors().cell())}));return u(t,n,r).map(function(n){return n.cell()})})})})}},ac=cc(function(n,e,t){return tc(n,e,t,-1)},function(n,e,t){return oc(n,e,t,-1)}),sc=cc(function(n,e,t){return tc(n,e,t,1)},function(n,e,t){return oc(n,e,t,1)}),fc=cc(function(n,e,t){return rc(n,t,e,-1)},function(n,e,t){return ic(n,t,e,-1)}),lc=cc(function(n,e,t){return rc(n,t,e,1)},function(n,e,t){return ic(n,t,e,1)}),dc=A([Wi(Hi([37]),wu(ac,sc)),Wi(Hi([39]),xu(ac,sc)),Wi(Hi([38]),Ou(fc)),Wi(Hi([40]),ku(lc)),Wi(Hi([32].concat([13])),function(e,t,r){return vo(e.element()).bind(function(n){return r.execute()(e,t,n)})})]),mc=A({}),gc=A({}),pc=ou(uc,Nr.init,dc,mc,gc,F.some(function(e,t){t.previousSelector()(e).orThunk(function(){var n=t.selectors();return Ri(e.element(),n.cell())}).each(function(n){t.focusManager().set(e,n)})})),vc=[Jt("selector"),or("execute",su),or("moveOnTab",!1)],hc=function(e,t,r){return r.focusManager().get(e).bind(function(n){return r.execute()(e,t,n)})},yc=function(n,e,t){return Uu(n,t.selector(),e,-1)},bc=function(n,e,t){return Uu(n,t.selector(),e,1)},wc=A([Wi(Hi([38]),Cu(yc)),Wi(Hi([40]),Cu(bc)),Wi(zi([Li,Hi([9])]),function(n,e,t){return t.moveOnTab()?Cu(yc)(n,e,t):F.none()}),Wi(zi([$i,Hi([9])]),function(n,e,t){return t.moveOnTab()?Cu(bc)(n,e,t):F.none()}),Wi(Hi([13]),hc),Wi(Hi([32]),hc)]),xc=A({}),Tc=A({}),Sc=ou(vc,Nr.init,wc,xc,Tc,F.some(function(e,t){Ri(e.element(),t.selector()).each(function(n){t.focusManager().set(e,n)})})),Oc=[Ho("onSpace"),Ho("onEnter"),Ho("onShiftEnter"),Ho("onLeft"),Ho("onRight"),Ho("onTab"),Ho("onShiftTab"),Ho("onUp"),Ho("onDown"),Ho("onEscape"),er("focusIn")],kc=ou(Oc,Nr.init,function(n,e,t){return[Wi(Hi([32]),t.onSpace()),Wi(zi([$i,Hi([13])]),t.onEnter()),Wi(zi([Li,Hi([13])]),t.onShiftEnter()),Wi(zi([Li,Hi([9])]),t.onShiftTab()),Wi(zi([$i,Hi([9])]),t.onTab()),Wi(Hi([38]),t.onUp()),Wi(Hi([40]),t.onDown()),Wi(Hi([37]),t.onLeft()),Wi(Hi([39]),t.onRight()),Wi(Hi([32]),t.onSpace()),Wi(Hi([27]),t.onEscape())]},function(){return{}},function(){return{}},F.some(function(e,t){return t.focusIn().bind(function(n){return n(e,t)})})),Cc=uu.schema(),Ec=cu.schema(),Dc=Zu.schema(),Ic=Wu.schema(),Ac=pc.schema(),Mc=gu.schema(),Fc=Sc.schema(),Rc=kc.schema(),Bc=(ki=Ut("Creating behaviour: "+(Oi={branchKey:"mode",branches:Object.freeze({acyclic:Cc,cyclic:Ec,flow:Dc,flatgrid:Ic,matrix:Ac,execution:Mc,menu:Fc,special:Rc}),name:"keying",active:{events:function(n,e){return n.handler().toEvents(n,e)}},apis:{focusIn:function(n){n.getSystem().triggerFocus(n.element(),n.element())},setGridSize:function(n,e,t,r,o){xt(t,"setGridSize")?t.setGridSize(r,o):p.console.error("Layout does not support setGridSize")}},state:vu}).name,Lr,Oi),Er(Yt(ki.branchKey,ki.branches),ki.name,ki.active,ki.apis,ki.extra,ki.state)),Vc=function(r,n){return e=r,t={},o=yn(n,function(n){return e=n.name(),t="Cannot configure "+n.name()+" for "+r,jt(e,e,rt(),Ft(function(n){return Qe.error("The field: "+e+" is forbidden. "+t)}));var e,t}).concat([ur("dump",h)]),jt(e,e,et(t),Bt(o));var e,t,o},Nc=function(n){return n.dump()},_c="placeholder",jc=Ze([{single:["required","valueThunk"]},{multiple:["required","valueThunks"]}]),Hc=function(n,e,t,r){return t.uiType===_c?(i=t,u=r,(o=n).exists(function(n){return n!==i.owner})?jc.single(!0,A(i)):ht(u,i.name).fold(function(){throw new Error("Unknown placeholder component: "+i.name+"\nKnown: ["+R(u)+"]\nNamespace: "+o.getOr("none")+"\nSpec: "+kt(i,null,2))},function(n){return n.replace()})):jc.single(!1,A(t));var o,i,u},zc=function(i,u,c,a){return Hc(i,0,c,a).fold(function(n,e){var t=e(u,c.config,c.validated),r=ht(t,"components").getOr([]),o=Cn(r,function(n){return zc(i,u,n,a)});return[k(t,{components:o})]},function(n,e){return e(u,c.config,c.validated)})},Lc=function(e,t,n,r){var o,i,u,c=V(r,function(n,e){return r=n,o=!1,{name:A(t=e),required:function(){return r.fold(function(n,e){return n},function(n,e){return n})},used:function(){return o},replace:function(){if(!0===o)throw new Error("Trying to use the same placeholder more than once: "+t);return o=!0,r}};var t,r,o}),a=(o=e,i=t,u=c,Cn(n,function(n){return zc(o,i,n,u)}));return B(c,function(n){if(!1===n.used()&&n.required())throw new Error("Placeholder: "+n.name()+" was not found in components list\nNamespace: "+e.getOr("none")+"\nComponents: "+kt(t.components(),null,2))}),a},Pc=jc.single,$c=jc.multiple,Wc=A(_c),Uc=0,Gc=function(n){var e=(new Date).getTime();return n+"_"+Math.floor(1e9*Math.random())+ ++Uc+String(e)},qc=Ze([{required:["data"]},{external:["data"]},{optional:["data"]},{group:["data"]}]),Yc=or("factory",{sketch:h}),Kc=or("schema",[]),Xc=Jt("name"),Jc=jt("pname","pname",ot(function(n){return"<alloy."+Gc(n.name)+">"}),Kt()),Qc=or("defaults",A({})),Zc=or("overrides",A({})),na=Bt([Yc,Kc,Xc,Jc,Qc,Zc]),ea=Bt([Yc,Kc,Xc,Jc,Qc,Zc]),ta=Bt([Yc,Kc,Xc,Jt("unit"),Jc,Qc,Zc]),ra=function(n){var e=function(n){return n.name()};return n.fold(e,e,e,e)},oa=function(t,r){return function(n){var e=Gt("Converting part type",r,n);return t(e)}},ia=oa(qc.required,na),ua=oa(qc.optional,ea),ca=oa(qc.group,ta),aa=A("entirety"),sa=function(n,e,t,r){var o=t;return k(e.defaults()(n,t,r),t,{uid:n.partUids()[e.name()]},e.overrides()(n,t,r),{"debug.sketcher":yt("part-"+e.name(),o)})},fa=function(o,n){var i={};return bn(n,function(n){var e;(e=n,e.fold(F.some,F.none,F.some,F.some)).each(function(t){var r=la(o,t.pname());i[t.name()]=function(n){var e=Ut("Part: "+t.name()+" in "+o,Bt(t.schema()),n);return k(r,{config:n,validated:e})}})}),i},la=function(n,e){return{uiType:Wc(),owner:n,name:e}},da=function(n,e,t){return r=e,i={},o={},bn(t,function(n){n.fold(function(r){i[r.pname()]=Pc(!0,function(n,e,t){return r.factory().sketch(sa(n,r,e,t))})},function(n){var e=r.parts()[n.name()]();o[n.name()]=A(sa(r,n,e[aa()]()))},function(r){i[r.pname()]=Pc(!1,function(n,e,t){return r.factory().sketch(sa(n,r,e,t))})},function(o){i[o.pname()]=$c(!0,function(e,n,t){var r=e[o.name()]();return yn(r,function(n){return o.factory().sketch(k(o.defaults()(e,n),n,o.overrides()(e,n)))})})})}),{internals:A(i),externals:A(o)};var r,i,o},ma=function(n,e,t){return Lc(F.some(n),e,e.components(),t)},ga=function(n,e,t){var r=e.partUids()[t];return n.getSystem().getByUid(r).toOption()},pa=function(n,e,t){return ga(n,e,t).getOrDie("Could not find part: "+t)},va=function(e,n){var t=yn(n,ra);return bt(yn(t,function(n){return{key:n,value:e+"-"+n}}))},ha=function(e){return jt("partUids","partUids",it(function(n){return va(n.uid,e)}),Kt())},ya=Gc("alloy-premade"),ba=Gc("api"),wa=function(n){return yt(ya,n)},xa=function(o){return n=function(n){for(var e=[],t=1;t<arguments.length;t++)e[t-1]=arguments[t];var r=n.config(ba);return o.apply(undefined,[r].concat([n].concat(e)))},e=o.toString(),t=e.indexOf(")")+1,r=e.indexOf("("),i=e.substring(r+1,t-1).split(/,\s*/),n.toFunctionAnnotation=function(){return{name:"OVERRIDE",parameters:wr(i.slice(1))}},n;var n,e,t,r,i},Ta=A(ba),Sa=A("alloy-id-"),Oa=A("data-alloy-id"),ka=Sa(),Ca=Oa(),Ea=function(n){var e=pe(n)?Gr(n,Ca):null;return F.from(e)},Da=function(n){return Gc(n)},Ia=function(n,e,t,r,o){var i,u,c=(u=o,(0<(i=r).length?[nr("parts",i)]:[]).concat([Jt("uid"),or("dom",{}),or("components",[]),$o("originalSpec"),or("debug.sketcher",{})]).concat(u));return Gt(n+" [SpecSchema]",Rt(c.concat(e)),t)},Aa=function(n,e,t,r,o){var i=Ma(o),u=Cn(t,function(n){return n.fold(F.none,F.some,F.none,F.none).map(function(n){return nr(n.name(),n.schema().concat([$o(aa())]))}).toArray()}),c=ha(t),a=Ia(n,e,i,u,[c]),s=da(0,a,t),f=ma(n,a,s.internals());return k(r(a,f,i,s.externals()),{"debug.sketcher":yt(n,o)})},Ma=function(n){return k({uid:Da("uid")},n)},Fa=Rt([Jt("name"),Jt("factory"),Jt("configFields"),or("apis",{}),or("extraApis",{})]),Ra=Rt([Jt("name"),Jt("factory"),Jt("configFields"),Jt("partFields"),or("apis",{}),or("extraApis",{})]),Ba=function(n){var c=Ut("Sketcher for "+n.name,Fa,n),e=V(c.apis,xa),t=V(c.extraApis,function(n,e){return xr(n,e)});return k({name:A(c.name),partFields:A([]),configFields:A(c.configFields),sketch:function(n){return e=c.name,t=c.configFields,r=c.factory,i=Ma(o=n),u=Ia(e,t,i,[],[]),k(r(u,i),{"debug.sketcher":yt(e,o)});var e,t,r,o,i,u}},e,t)},Va=function(n){var e=Ut("Sketcher for "+n.name,Ra,n),t=fa(e.name,e.partFields),r=V(e.apis,xa),o=V(e.extraApis,function(n,e){return xr(n,e)});return k({name:A(e.name),partFields:A(e.partFields),configFields:A(e.configFields),sketch:function(n){return Aa(e.name,e.configFields,e.partFields,e.factory,n)},parts:A(t)},r,o)},Na=Ba({name:"Button",factory:function(n){var e,t,r,o=(e=n.action(),t=function(n,e){e.stop(),te(n)},r=zn.detect().deviceType.isTouch()?[lr(qn(),t)]:[lr(Y(),t),lr(L(),function(n,e){e.cut()})],sr(kn([e.map(function(t){return lr(Un(),function(n,e){t(n),e.stop()})}).toArray(),r]))),i=ht(n.dom(),"attributes").bind(pt("type")),u=ht(n.dom(),"tag");return{uid:n.uid(),dom:n.dom(),components:n.components(),events:o,behaviours:k(jr([mi.config({}),Bc.config({mode:"execution",useSpace:!0,useEnter:!0})]),Nc(n.buttonBehaviours())),domModification:{attributes:k(i.fold(function(){return u.is("button")?{type:"button"}:{}},function(n){return{}}),{role:n.role().getOr("button")})},eventOrder:n.eventOrder()}},configFields:[or("uid",undefined),Jt("dom"),or("components",[]),Vc("buttonBehaviours",[mi,Bc]),er("action"),er("role"),or("eventOrder",{})]}),_a=zr({fields:[],name:"unselecting",active:Object.freeze({events:function(n){return sr([fr(X(),A(!0))])},exhibit:function(n,e){return Or({styles:{"-webkit-user-select":"none","user-select":"none","-ms-user-select":"none","-moz-user-select":"-moz-none"},attributes:{unselectable:"on"}})}})}),ja=function(n){var e,t,r,o=ue.fromHtml(n),i=_e(o),u=(t=(e=o).dom().attributes!==undefined?e.dom().attributes:[],Tn(t,function(n,e){return"class"===e.name?n:k(n,yt(e.name,e.value))},{})),c=(r=o,Array.prototype.slice.call(r.dom().classList,0)),a=0===i.length?{}:{innerHtml:xo(o)};return k({tag:me(o),classes:c,attributes:u},a)},Ha=function(n){var e,o,t=(e=n,o={prefix:ai.prefix()},e.replace(/\$\{([^{}]*)\}/g,function(n,e){var t,r=o[e];return"string"==(t=typeof r)||"number"===t?r.toString():n}));return ja(t)},za=function(n){return{dom:Ha(n)}},La=function(n){return jr([ri.config({toggleClass:ai.resolve("toolbar-button-selected"),toggleOnExecute:!1,aria:{mode:"pressed"}}),oi(n,function(n,e){(e?ri.on:ri.off)(n)})])},Pa=function(n,e,t){return Na.sketch({dom:Ha('<span class="${prefix}-toolbar-button ${prefix}-icon-'+n+' ${prefix}-icon"></span>'),action:e,buttonBehaviours:k(jr([_a.config({})]),t)})},$a={forToolbar:Pa,forToolbarCommand:function(n,e){return Pa(e,function(){n.execCommand(e)},{})},forToolbarStateAction:function(n,e,t,r){var o=La(t);return Pa(e,r,o)},forToolbarStateCommand:function(n,e){var t=La(e);return Pa(e,function(){n.execCommand(e)},t)}},Wa=function(t,r){return{left:A(t),top:A(r),translate:function(n,e){return Wa(t+n,r+e)}}},Ua=Wa,Ga=function(n,e,t){return Math.max(e,Math.min(t,n))},qa=function(n,e,t,r,o,i,u){var c=t-e;if(r<n.left)return e-1;if(r>n.right)return t+1;var a,s,f,l,d=Math.min(n.right,Math.max(r,n.left))-n.left,m=Ga(d/n.width*c+e,e-1,t+1),g=Math.round(m);return i&&e<=m&&m<=t?(a=m,s=e,f=t,l=o,u.fold(function(){var n=a-s,e=Math.round(n/l)*l;return Ga(s+e,s-1,f+1)},function(n){var e=(a-n)%l,t=Math.round(e/l),r=Math.floor((a-n)/l),o=Math.floor((f-n)/l),i=n+Math.min(o,r+t)*l;return Math.max(n,i)})):g},Ya="slider.change.value",Ka=zn.detect().deviceType.isTouch(),Xa=function(n){return function(n){var e=n.event().raw();if(Ka){var t=e;return t.touches!==undefined&&1===t.touches.length?F.some(t.touches[0]).map(function(n){return Ua(n.clientX,n.clientY)}):F.none()}var r=e;return r.clientX!==undefined?F.some(r).map(function(n){return Ua(n.clientX,n.clientY)}):F.none()}(n).map(function(n){return n.left()})},Ja=function(n,e){ee(n,Ya,{value:e})},Qa=function(i,u,c,n){return Xa(n).map(function(n){var e,t,r,o;return e=i,r=n,o=qa(c,(t=u).min(),t.max(),r,t.stepSize(),t.snapToGrid(),t.snapStart()),Ja(e,o),n})},Za=function(n,e){var t,r,o,i,u=(t=e.value().get(),r=e.min(),o=e.max(),i=e.stepSize(),t<r?t:o<t?o:t===r?r-1:Math.max(r,t-i));Ja(n,u)},ns=function(n,e){var t,r,o,i,u=(t=e.value().get(),r=e.min(),o=e.max(),i=e.stepSize(),o<t?t:t<r?r:t===o?o+1:Math.min(o,t+i));Ja(n,u)},es=zn.detect().deviceType.isTouch(),ts=function(n,r){return ua({name:n+"-edge",overrides:function(n){var e=sr([dr(j(),r,[n])]),t=sr([dr(L(),r,[n]),dr(P(),function(n,e){e.mouseIsDown().get()&&r(n,e)},[n])]);return{events:es?e:t}}})},rs=[ts("left",function(n,e){Ja(n,e.min()-1)}),ts("right",function(n,e){Ja(n,e.max()+1)}),ia({name:"thumb",defaults:A({dom:{styles:{position:"absolute"}}}),overrides:function(n){return{events:sr([gr(j(),n,"spectrum"),gr(H(),n,"spectrum"),gr(z(),n,"spectrum")])}}}),ia({schema:[ur("mouseIsDown",function(){return ao(!1)})],name:"spectrum",overrides:function(r){var t=function(n,e){var t=n.element().dom().getBoundingClientRect();Qa(n,r,t,e)},n=sr([lr(j(),t),lr(H(),t)]),e=sr([lr(L(),t),lr(P(),function(n,e){r.mouseIsDown().get()&&t(n,e)})]);return{behaviours:jr(es?[]:[Bc.config({mode:"special",onLeft:function(n){return Za(n,r),F.some(!0)},onRight:function(n){return ns(n,r),F.some(!0)}}),mi.config({})]),events:es?n:e}}})],os=function(n,e,t){e.store().manager().onLoad(n,e,t)},is=function(n,e,t){e.store().manager().onUnload(n,e,t)},us=Object.freeze({onLoad:os,onUnload:is,setValue:function(n,e,t,r){e.store().manager().setValue(n,e,t,r)},getValue:function(n,e,t){return e.store().manager().getValue(n,e,t)}}),cs=Object.freeze({events:function(t,r){var n=t.resetOnDom()?[vr(function(n,e){os(n,t,r)}),hr(function(n,e){is(n,t,r)})]:[Cr(t,r,os)];return sr(n)}}),as=function(){var n=ao(null);return _r({set:n.set,get:n.get,isNotSet:function(){return null===n.get()},clear:function(){n.set(null)},readState:function(){return{mode:"memory",value:n.get()}}})},ss=function(){var n=ao({});return _r({readState:function(){return{mode:"dataset",dataset:n.get()}},set:n.set,get:n.get})},fs=Object.freeze({memory:as,dataset:ss,manual:function(){return _r({readState:function(){}})},init:function(n){return n.store().manager().state(n)}}),ls=function(n,e,t,r){e.store().getDataKey(),t.set({}),e.store().setData()(n,r),e.onSetValue()(n,r)},ds=[er("initialValue"),Jt("getFallbackEntry"),Jt("getDataKey"),Jt("setData"),Po("manager",{setValue:ls,getValue:function(n,e,t){var r=e.store().getDataKey()(n),o=t.get();return ht(o,r).fold(function(){return e.store().getFallbackEntry()(r)},function(n){return n})},onLoad:function(e,t,r){t.store().initialValue().each(function(n){ls(e,t,r,n)})},onUnload:function(n,e,t){t.set({})},state:ss})],ms=[Jt("getValue"),or("setValue",I),er("initialValue"),Po("manager",{setValue:function(n,e,t,r){e.store().setValue()(n,r),e.onSetValue()(n,r)},getValue:function(n,e,t){return e.store().getValue()(n)},onLoad:function(e,t,n){t.store().initialValue().each(function(n){t.store().setValue()(e,n)})},onUnload:I,state:Nr.init})],gs=[er("initialValue"),Po("manager",{setValue:function(n,e,t,r){t.set(r),e.onSetValue()(n,r)},getValue:function(n,e,t){return t.get()},onLoad:function(n,e,t){e.store().initialValue().each(function(n){t.isNotSet()&&t.set(n)})},onUnload:function(n,e,t){t.clear()},state:as})],ps=[ir("store",{mode:"memory"},Yt("mode",{memory:gs,manual:ms,dataset:ds})),jo("onSetValue"),or("resetOnDom",!1)],vs=zr({fields:ps,name:"representing",active:cs,apis:us,extra:{setValueFrom:function(n,e){var t=vs.getValue(e);vs.setValue(n,t)}},state:fs}),hs=zn.detect().deviceType.isTouch(),ys=[Jt("min"),Jt("max"),or("stepSize",1),or("onChange",I),or("onInit",I),or("onDragStart",I),or("onDragEnd",I),or("snapToGrid",!1),er("snapStart"),Jt("getInitialValue"),Vc("sliderBehaviours",[Bc,vs]),ur("value",function(n){return ao(n.min)})].concat(hs?[]:[ur("mouseIsDown",function(){return ao(!1)})]),bs=Si("width",function(n){return n.dom().offsetWidth}),ws=function(n,e){bs.set(n,e)},xs=function(n){return bs.get(n)},Ts=zn.detect().deviceType.isTouch(),Ss=Va({name:"Slider",configFields:ys,partFields:rs,factory:function(a,n,e,t){var s=a.max()-a.min(),f=function(n){var e=n.element().dom().getBoundingClientRect();return(e.left+e.right)/2},o=function(n){return pa(n,a,"thumb")},i=function(n){var e,t,r,o,i=pa(n,a,"spectrum").element().dom().getBoundingClientRect(),u=n.element().dom().getBoundingClientRect(),c=(e=n,t=i,(o=(r=a).value().get())<r.min()?ga(e,r,"left-edge").fold(function(){return 0},function(n){return f(n)-t.left}):o>r.max()?ga(e,r,"right-edge").fold(function(){return t.width},function(n){return f(n)-t.left}):(r.value().get()-r.min())/s*t.width);return i.left-u.left+c},u=function(n){var e=i(n),t=o(n),r=xs(t.element())/2;vi(t.element(),"left",e-r+"px")},r=function(n,e){var t=a.value().get(),r=o(n);return t!==e||wi(r.element(),"left").isNone()?(a.value().set(e),u(n),a.onChange()(n,r,e),F.some(!0)):F.none()},c=Ts?[lr(j(),function(n,e){a.onDragStart()(n,o(n))}),lr(z(),function(n,e){a.onDragEnd()(n,o(n))})]:[lr(L(),function(n,e){e.stop(),a.onDragStart()(n,o(n)),a.mouseIsDown().set(!0)}),lr($(),function(n,e){a.onDragEnd()(n,o(n)),a.mouseIsDown().set(!1)})];return{uid:a.uid(),dom:a.dom(),components:n,behaviours:k(jr(kn([Ts?[]:[Bc.config({mode:"special",focusIn:function(n){return ga(n,a,"spectrum").map(Bc.focusIn).map(A(!0))}})],[vs.config({store:{mode:"manual",getValue:function(n){return a.value().get()}}})]])),Nc(a.sliderBehaviours())),events:sr([lr(Ya,function(n,e){r(n,e.event().value())}),vr(function(n,e){a.value().set(a.getInitialValue()());var t=o(n);u(n),a.onInit()(n,t,a.value().get())})].concat(c)),apis:{resetToMin:function(n){r(n,a.min())},resetToMax:function(n){r(n,a.max())},refresh:u},domModification:{styles:{position:"relative"}}}},apis:{resetToMin:function(n,e){n.resetToMin(e)},resetToMax:function(n,e){n.resetToMax(e)},refresh:function(n,e){n.refresh(e)}}}),Os=function(e,t,r){return $a.forToolbar(t,function(){var n=r();e.setContextToolbar([{label:t+" group",items:n}])},{})},ks=function(n){return[(o=n,i=function(n){return n<0?"black":360<n?"white":"hsl("+n+", 100%, 50%)"},Ss.sketch({dom:Ha('<div class="${prefix}-slider ${prefix}-hue-slider-container"></div>'),components:[Ss.parts()["left-edge"](za('<div class="${prefix}-hue-slider-black"></div>')),Ss.parts().spectrum({dom:Ha('<div class="${prefix}-slider-gradient-container"></div>'),components:[za('<div class="${prefix}-slider-gradient"></div>')],behaviours:jr([ri.config({toggleClass:ai.resolve("thumb-active")})])}),Ss.parts()["right-edge"](za('<div class="${prefix}-hue-slider-white"></div>')),Ss.parts().thumb({dom:Ha('<div class="${prefix}-slider-thumb"></div>'),behaviours:jr([ri.config({toggleClass:ai.resolve("thumb-active")})])})],onChange:function(n,e,t){var r=i(t);vi(e.element(),"background-color",r),o.onChange(n,e,r)},onDragStart:function(n,e){ri.on(e)},onDragEnd:function(n,e){ri.off(e)},onInit:function(n,e,t){var r=i(t);vi(e.element(),"background-color",r)},stepSize:10,min:0,max:360,getInitialValue:o.getInitialValue,sliderBehaviours:jr([ii(Ss.refresh)])}))];var o,i},Cs=function(n,r){var e={onChange:function(n,e,t){r.undoManager.transact(function(){r.formatter.apply("forecolor",{value:t}),r.nodeChanged()})},getInitialValue:function(){return-1}};return Os(n,"color",function(){return ks(e)})},Es=Rt([Jt("getInitialValue"),Jt("onChange"),Jt("category"),Jt("sizes")]),Ds=function(n){var o=Ut("SizeSlider",Es,n);return Ss.sketch({dom:{tag:"div",classes:[ai.resolve("slider-"+o.category+"-size-container"),ai.resolve("slider"),ai.resolve("slider-size-container")]},onChange:function(n,e,t){var r;0<=(r=t)&&r<o.sizes.length&&o.onChange(t)},onDragStart:function(n,e){ri.on(e)},onDragEnd:function(n,e){ri.off(e)},min:0,max:o.sizes.length-1,stepSize:1,getInitialValue:o.getInitialValue,snapToGrid:!0,sliderBehaviours:jr([ii(Ss.refresh)]),components:[Ss.parts().spectrum({dom:Ha('<div class="${prefix}-slider-size-container"></div>'),components:[za('<div class="${prefix}-slider-size-line"></div>')]}),Ss.parts().thumb({dom:Ha('<div class="${prefix}-slider-thumb"></div>'),behaviours:jr([ri.config({toggleClass:ai.resolve("thumb-active")})])})]})},Is=["9px","10px","11px","12px","14px","16px","18px","20px","24px","32px","36px"],As=function(n){var e,t,r=n.selection.getStart(),o=ue.fromDom(r),i=ue.fromDom(n.getBody()),u=(e=function(n){return Re(i,n)},(pe(t=o)?F.some(t):Ne(t).filter(pe)).map(function(n){return lo(n,function(n){return wi(n,"font-size").isSome()},e).bind(function(n){return wi(n,"font-size")}).getOrThunk(function(){return yi(n,"font-size")})}).getOr(""));return Sn(Is,function(n){return u===n}).getOr("medium")},Ms={candidates:A(Is),get:function(n){var e,t=As(n);return(e=t,On(Is,function(n){return n===e})).getOr(2)},apply:function(r,n){var e;(e=n,F.from(Is[e])).each(function(n){var e,t;t=n,As(e=r)!==t&&e.execCommand("fontSize",!1,t)})}},Fs=Ms.candidates(),Rs=function(n){return[za('<span class="${prefix}-toolbar-button ${prefix}-icon-small-font ${prefix}-icon"></span>'),(e=n,Ds({onChange:e.onChange,sizes:Fs,category:"font",getInitialValue:e.getInitialValue})),za('<span class="${prefix}-toolbar-button ${prefix}-icon-large-font ${prefix}-icon"></span>')];var e},Bs=function(n){var e=n.uid!==undefined&&xt(n,"uid")?n.uid:Da("memento");return{get:function(n){return n.getSystem().getByUid(e).getOrDie()},getOpt:function(n){return n.getSystem().getByUid(e).fold(F.none,F.some)},asSpec:function(){return k(n,{uid:e})}}},Vs=window.Promise?window.Promise:function(){var i=function(n){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof n)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],f(n,r(o,this),r(c,this))},n=i.immediateFn||"function"==typeof window.setImmediate&&window.setImmediate||function(n){p.setTimeout(n,1)};function r(n,e){return function(){return n.apply(e,arguments)}}var t=Array.isArray||function(n){return"[object Array]"===Object.prototype.toString.call(n)};function u(r){var o=this;null!==this._state?n(function(){var n=o._state?r.onFulfilled:r.onRejected;if(null!==n){var e;try{e=n(o._value)}catch(t){return void r.reject(t)}r.resolve(e)}else(o._state?r.resolve:r.reject)(o._value)}):this._deferreds.push(r)}function o(n){try{if(n===this)throw new TypeError("A promise cannot be resolved with itself.");if(n&&("object"==typeof n||"function"==typeof n)){var e=n.then;if("function"==typeof e)return void f(r(e,n),r(o,this),r(c,this))}this._state=!0,this._value=n,a.call(this)}catch(t){c.call(this,t)}}function c(n){this._state=!1,this._value=n,a.call(this)}function a(){for(var n=0,e=this._deferreds;n<e.length;n++){var t=e[n];u.call(this,t)}this._deferreds=[]}function s(n,e,t,r){this.onFulfilled="function"==typeof n?n:null,this.onRejected="function"==typeof e?e:null,this.resolve=t,this.reject=r}function f(n,e,t){var r=!1;try{n(function(n){r||(r=!0,e(n))},function(n){r||(r=!0,t(n))})}catch(o){if(r)return;r=!0,t(o)}}return i.prototype["catch"]=function(n){return this.then(null,n)},i.prototype.then=function(t,r){var o=this;return new i(function(n,e){u.call(o,new s(t,r,n,e))})},i.all=function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];var a=Array.prototype.slice.call(1===n.length&&t(n[0])?n[0]:n);return new i(function(o,i){if(0===a.length)return o([]);var u=a.length;function c(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var t=n.then;if("function"==typeof t)return void t.call(n,function(n){c(e,n)},i)}a[e]=n,0==--u&&o(a)}catch(r){i(r)}}for(var n=0;n<a.length;n++)c(n,a[n])})},i.resolve=function(e){return e&&"object"==typeof e&&e.constructor===i?e:new i(function(n){n(e)})},i.reject=function(t){return new i(function(n,e){e(t)})},i.race=function(o){return new i(function(n,e){for(var t=0,r=o;t<r.length;t++)r[t].then(n,e)})},i}();function Ns(n){return(t=n,new Vs(function(n){var e=new p.FileReader;e.onloadend=function(){n(e.result)},e.readAsDataURL(t)})).then(function(n){return n.split(",")[1]});var t}var _s,js,Hs,zs,Ls,Ps,$s=function(o,i){var n;(n=i,Ns(n)).then(function(r){o.undoManager.transact(function(){var n=o.editorUpload.blobCache,e=n.create(Gc("mceu"),i,r);n.add(e);var t=o.dom.createHTML("img",{src:e.blobUri()});o.insertContent(t)})})},Ws=function(i){var e=Bs({dom:{tag:"input",attributes:{accept:"image/*",type:"file",title:""},styles:{visibility:"hidden",position:"absolute"}},events:sr([pr(Y()),lr(q(),function(n,e){var t,r,o;(t=e,r=t.event(),o=r.raw().target.files||r.raw().dataTransfer.files,F.from(o[0])).each(function(n){$s(i,n)})})])});return Na.sketch({dom:Ha('<span class="${prefix}-toolbar-button ${prefix}-icon-image ${prefix}-icon"></span>'),components:[e.asSpec()],action:function(n){e.get(n).element().dom().click()}})},Us=function(n){return n.dom().textContent},Gs=function(n){return 0<n.length},qs=function(n){return n===undefined||null===n?"":n},Ys=function(e,t,n){return n.text.filter(Gs).fold(function(){return Gr(n=e,"href")===Us(n)?F.some(t):F.none();var n},F.some)},Ks=function(n){var e=ue.fromDom(n.selection.getStart());return Bi(e,"a")},Xs={getInfo:function(n){return Ks(n).fold(function(){return{url:"",text:n.selection.getContent({format:"text"}),title:"",target:"",link:F.none()}},function(n){return t=Us(e=n),r=Gr(e,"href"),o=Gr(e,"title"),i=Gr(e,"target"),{url:qs(r),text:t!==r?qs(t):"",title:qs(o),target:qs(i),link:F.some(e)};var e,t,r,o,i})},applyInfo:function(o,i){i.url.filter(Gs).fold(function(){var e;e=o,i.link.bind(h).each(function(n){e.execCommand("unlink")})},function(e){var n,t,r=(n=i,(t={}).href=e,n.title.filter(Gs).each(function(n){t.title=n}),n.target.filter(Gs).each(function(n){t.target=n}),t);i.link.bind(h).fold(function(){var n=i.text.filter(Gs).getOr(e);o.insertContent(o.dom.createHTML("a",r,o.dom.encode(n)))},function(t){var n=Ys(t,e,i);Ur(t,r),n.each(function(n){var e;e=n,t.dom().textContent=e})})})},query:Ks},Js=zn.detect(),Qs=function(n,e){var t=e.selection.getRng();n(),e.selection.setRng(t)},Zs=function(n,e){(Js.os.isAndroid()?Qs:a)(e,n)},nf=function(n,e){var t,r;return{key:n,value:{config:{},me:(t=n,r=sr(e),zr({fields:[Jt("enabled")],name:t,active:{events:A(r)}})),configAsRaw:A({}),initialConfig:{},state:Nr}}},ef=Object.freeze({getCurrent:function(n,e,t){return e.find()(n)}}),tf=[Jt("find")],rf=zr({fields:tf,name:"composing",apis:ef}),of=Ba({name:"Container",factory:function(n){return{uid:n.uid(),dom:k({tag:"div",attributes:{role:"presentation"}},n.dom()),components:n.components(),behaviours:Nc(n.containerBehaviours()),events:n.events(),domModification:n.domModification(),eventOrder:n.eventOrder()}},configFields:[or("components",[]),Vc("containerBehaviours",[]),or("events",{}),or("domModification",{}),or("eventOrder",{})]}),uf=Ba({name:"DataField",factory:function(t){return{uid:t.uid(),dom:t.dom(),behaviours:k(jr([vs.config({store:{mode:"memory",initialValue:t.getInitialValue()()}}),rf.config({find:F.some})]),Nc(t.dataBehaviours())),events:sr([vr(function(n,e){vs.setValue(n,t.getInitialValue()())})])}},configFields:[Jt("uid"),Jt("dom"),Jt("getInitialValue"),Vc("dataBehaviours",[vs,rf])]}),cf=function(n){return n.dom().value},af=function(n,e){if(e===undefined)throw new Error("Value.set was undefined");n.dom().value=e},sf=A([er("data"),or("inputAttributes",{}),or("inputStyles",{}),or("type","input"),or("tag","input"),or("inputClasses",[]),jo("onSetValue"),or("styles",{}),er("placeholder"),or("eventOrder",{}),Vc("inputBehaviours",[vs,mi]),or("selectOnFocus",!0)]),ff=function(n){return k(jr([vs.config({store:{mode:"manual",initialValue:n.data().getOr(undefined),getValue:function(n){return cf(n.element())},setValue:function(n,e){cf(n.element())!==e&&af(n.element(),e)}},onSetValue:n.onSetValue()})]),(e=n,jr([mi.config({onFocus:!1===e.selectOnFocus()?I:function(n){var e=n.element(),t=cf(e);e.dom().setSelectionRange(0,t.length)}})])),Nc(n.inputBehaviours()));var e},lf=Ba({name:"Input",configFields:sf(),factory:function(n,e){return{uid:n.uid(),dom:(t=n,{tag:t.tag(),attributes:k(bt([{key:"type",value:t.type()}].concat(t.placeholder().map(function(n){return{key:"placeholder",value:n}}).toArray())),t.inputAttributes()),styles:t.inputStyles(),classes:t.inputClasses()}),components:[],behaviours:ff(n),eventOrder:n.eventOrder()};var t}}),df=Object.freeze({exhibit:function(n,e){return Or({attributes:bt([{key:e.tabAttr(),value:"true"}])})}}),mf=[or("tabAttr","data-alloy-tabstop")],gf=zr({fields:mf,name:"tabstopping",active:df}),pf=function(n,e){var t=Bs(lf.sketch({placeholder:e,onSetValue:function(n,e){ne(n,G())},inputBehaviours:jr([rf.config({find:F.some}),gf.config({}),Bc.config({mode:"execution"})]),selectOnFocus:!1})),r=Bs(Na.sketch({dom:Ha('<button class="${prefix}-input-container-x ${prefix}-icon-cancel-circle ${prefix}-icon"></button>'),action:function(n){var e=t.get(n);vs.setValue(e,"")}}));return{name:n,spec:of.sketch({dom:Ha('<div class="${prefix}-input-container"></div>'),components:[t.asSpec(),r.asSpec()],containerBehaviours:jr([ri.config({toggleClass:ai.resolve("input-container-empty")}),rf.config({find:function(n){return F.some(t.get(n))}}),nf("input-clearing",[lr(G(),function(n){var e=t.get(n);(0<vs.getValue(e).length?ri.off:ri.on)(n)})])])})}},vf=["input","button","textarea"],hf=function(n,e,t){e.disabled()&&Sf(n,e)},yf=function(n){return hn(vf,me(n.element()))},bf=function(n){Wr(n.element(),"disabled","disabled")},wf=function(n){Yr(n.element(),"disabled")},xf=function(n){Wr(n.element(),"aria-disabled","true")},Tf=function(n){Wr(n.element(),"aria-disabled","false")},Sf=function(e,n,t){n.disableClass().each(function(n){no(e.element(),n)}),(yf(e)?bf:xf)(e)},Of=function(n){return yf(n)?qr(n.element(),"disabled"):"true"===Gr(n.element(),"aria-disabled")},kf=Object.freeze({enable:function(e,n,t){n.disableClass().each(function(n){eo(e.element(),n)}),(yf(e)?wf:Tf)(e)},disable:Sf,isDisabled:Of,onLoad:hf}),Cf=Object.freeze({exhibit:function(n,e,t){return Or({classes:e.disabled()?e.disableClass().map(In).getOr([]):[]})},events:function(n,e){return sr([fr(Un(),function(n,e){return Of(n)}),Cr(n,e,hf)])}}),Ef=[or("disabled",!1),er("disableClass")],Df=zr({fields:Ef,name:"disabling",active:Cf,apis:kf}),If=[Vc("formBehaviours",[vs])],Af=function(n){return"<alloy.field."+n+">"},Mf=function(o,n,e){return k({"debug.sketcher":{Form:e},uid:o.uid(),dom:o.dom(),components:n,behaviours:k(jr([vs.config({store:{mode:"manual",getValue:function(n){var e,t,r=(e=o,t=n.getSystem(),V(e.partUids(),function(n,e){return A(t.getByUid(n))}));return V(r,function(n,e){return n().bind(rf.getCurrent).map(vs.getValue)})},setValue:function(t,n){B(n,function(e,n){ga(t,o,n).each(function(n){rf.getCurrent(n).each(function(n){vs.setValue(n,e)})})})}}})]),Nc(o.formBehaviours())),apis:{getField:function(n,e){return ga(n,o,e).bind(rf.getCurrent)}}})},Ff=(xa(function(n,e,t){return n.getField(e,t)}),function(n){var i,e=(i=[],{field:function(n,e){return i.push(n),t="form",r=Af(n),o=e,{uiType:Wc(),owner:t,name:r,config:o,validated:{}};var t,r,o},record:function(){return i}}),t=n(e),r=e.record(),o=yn(r,function(n){return ia({name:n,pname:Af(n)})});return Aa("form",If,o,Mf,t)}),Rf=function(){var e=ao(F.none()),t=function(){e.get().each(function(n){n.destroy()})};return{clear:function(){t(),e.set(F.none())},isSet:function(){return e.get().isSome()},set:function(n){t(),e.set(F.some(n))},run:function(n){e.get().each(n)}}},Bf=function(){var e=ao(F.none());return{clear:function(){e.set(F.none())},set:function(n){e.set(F.some(n))},isSet:function(){return e.get().isSome()},on:function(n){e.get().each(n)}}},Vf=function(n){return{xValue:n,points:[]}},Nf=function(n,e){if(e===n.xValue)return n;var t=0<e-n.xValue?1:-1,r={direction:t,xValue:e};return{xValue:e,points:(0===n.points.length?[]:n.points[n.points.length-1].direction===t?n.points.slice(0,n.points.length-1):n.points).concat([r])}},_f=function(n){if(0===n.points.length)return 0;var e=n.points[0].direction,t=n.points[n.points.length-1].direction;return-1===e&&-1===t?-1:1===e&&1===t?1:0},jf=function(n){var r="navigateEvent",e=Bt([Jt("fields"),or("maxFieldIndex",n.fields.length-1),Jt("onExecute"),Jt("getInitialValue"),ur("state",function(){return{dialogSwipeState:Bf(),currentScreen:ao(0)}})]),u=Ut("SerialisedDialog",e,n),o=function(e,n,t){return Na.sketch({dom:Ha('<span class="${prefix}-icon-'+n+' ${prefix}-icon"></span>'),action:function(n){ee(n,r,{direction:e})},buttonBehaviours:jr([Df.config({disableClass:ai.resolve("toolbar-navigation-disabled"),disabled:!t})])})},i=function(n,o){var i=Ai(n.element(),"."+ai.resolve("serialised-dialog-screen"));Ri(n.element(),"."+ai.resolve("serialised-dialog-chain")).each(function(r){0<=u.state.currentScreen.get()+o&&u.state.currentScreen.get()+o<i.length&&(wi(r,"left").each(function(n){var e=parseInt(n,10),t=xs(i[0]);vi(r,"left",e-o*t+"px")}),u.state.currentScreen.set(u.state.currentScreen.get()+o))})},c=function(r){var n=Ai(r.element(),"input");F.from(n[u.state.currentScreen.get()]).each(function(n){r.getSystem().getByDom(n).each(function(n){var e,t;e=r,t=n.element(),e.getSystem().triggerFocus(t,e.element())})});var e=s.get(r);tu.highlightAt(e,u.state.currentScreen.get())},a=Bs(Ff(function(t){return{dom:Ha('<div class="${prefix}-serialised-dialog"></div>'),components:[of.sketch({dom:Ha('<div class="${prefix}-serialised-dialog-chain" style="left: 0px; position: absolute;"></div>'),components:yn(u.fields,function(n,e){return e<=u.maxFieldIndex?of.sketch({dom:Ha('<div class="${prefix}-serialised-dialog-screen"></div>'),components:kn([[o(-1,"previous",0<e)],[t.field(n.name,n.spec)],[o(1,"next",e<u.maxFieldIndex)]])}):t.field(n.name,n.spec)})})],formBehaviours:jr([ii(function(n,e){var t;t=e,Ri(n.element(),"."+ai.resolve("serialised-dialog-chain")).each(function(n){vi(n,"left",-u.state.currentScreen.get()*t.width+"px")})}),Bc.config({mode:"special",focusIn:function(n){c(n)},onTab:function(n){return i(n,1),F.some(!0)},onShiftTab:function(n){return i(n,-1),F.some(!0)}}),nf("form-events",[vr(function(e,n){u.state.currentScreen.set(0),u.state.dialogSwipeState.clear();var t=s.get(e);tu.highlightFirst(t),u.getInitialValue(e).each(function(n){vs.setValue(e,n)})}),br(u.onExecute),lr(K(),function(n,e){"left"===e.event().raw().propertyName&&c(n)}),lr(r,function(n,e){var t=e.event().direction();i(n,t)})])])}})),s=Bs({dom:Ha('<div class="${prefix}-dot-container"></div>'),behaviours:jr([tu.config({highlightClass:ai.resolve("dot-active"),itemClass:ai.resolve("dot-item")})]),components:Cn(u.fields,function(n,e){return e<=u.maxFieldIndex?[za('<div class="${prefix}-dot-item ${prefix}-icon-full-dot ${prefix}-icon"></div>')]:[]})});return{dom:Ha('<div class="${prefix}-serializer-wrapper"></div>'),components:[a.asSpec(),s.asSpec()],behaviours:jr([Bc.config({mode:"special",focusIn:function(n){var e=a.get(n);Bc.focusIn(e)}}),nf("serializer-wrapper-events",[lr(j(),function(n,e){var t=e.event();u.state.dialogSwipeState.set(Vf(t.touches[0].clientX))}),lr(H(),function(n,e){var t=e.event();u.state.dialogSwipeState.on(function(n){e.event().prevent(),u.state.dialogSwipeState.set(Nf(n,t.raw().touches[0].clientX))})}),lr(z(),function(r){u.state.dialogSwipeState.on(function(n){var e=a.get(r),t=-1*_f(n);i(e,t)})})])])}},Hf=J(function(t,r){return[{label:"the link group",items:[jf({fields:[pf("url","Type or paste URL"),pf("text","Link text"),pf("title","Link title"),pf("target","Link target"),(n="link",{name:n,spec:uf.sketch({dom:{tag:"span",styles:{display:"none"}},getInitialValue:function(){return F.none()}})})],maxFieldIndex:["url","text","title","target"].length-1,getInitialValue:function(){return F.some(Xs.getInfo(r))},onExecute:function(n){var e=vs.getValue(n);Xs.applyInfo(r,e),t.restoreToolbar(),r.focus()}})]}];var n}),zf=[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],Lf=sr([(_s=Pn(),js=function(n,e){var t,r,o=e.event().originator(),i=e.event().target();return r=i,!(Re(t=o,n.element())&&!Re(t,r)&&(p.console.warn(Pn()+" did not get interpreted by the desired target. \nOriginator: "+ko(o)+"\nTarget: "+ko(i)+"\nCheck the "+Pn()+" event handlers"),1))},{key:_s,value:cr({can:js})})]),Pf=Object.freeze({events:Lf}),$f=h,Wf=Vr(["debugInfo","triggerFocus","triggerEvent","triggerEscape","addToWorld","removeFromWorld","addToGui","removeFromGui","build","getByUid","getByDom","broadcast","broadcastOn","isConnected"]),Uf=function(e){var n=function(n){return function(){throw new Error("The component must be in a context to send: "+n+"\n"+ko(e().element())+" is not in context.")}};return Wf({debugInfo:A("fake"),triggerEvent:n("triggerEvent"),triggerFocus:n("triggerFocus"),triggerEscape:n("triggerEscape"),build:n("build"),addToWorld:n("addToWorld"),removeFromWorld:n("removeFromWorld"),addToGui:n("addToGui"),removeFromGui:n("removeFromGui"),getByUid:n("getByUid"),getByDom:n("getByDom"),broadcast:n("broadcast"),broadcastOn:n("broadcastOn"),isConnected:A(!1)})},Gf=function(n,o){var i={};return B(n,function(n,r){B(n,function(n,e){var t=vt(e,[])(i);i[e]=t.concat([o(r,n)])})}),i},qf=function(n,e){return 1<n.length?Qe.error('Multiple behaviours have tried to change DOM "'+e+'". The guilty behaviours are: '+kt(yn(n,function(n){return n.name()}))+". At this stage, this is not supported. Future releases might provide strategies for resolving this."):0===n.length?Qe.value({}):Qe.value(n[0].modification().fold(function(){return{}},function(n){return yt(e,n)}))},Yf=function(u,c){return Tn(u,function(n,e){var t=e.modification().getOr({});return n.bind(function(i){var n=_(t,function(n,e){return i[e]!==undefined?(t=c,r=e,o=u,Qe.error("Mulitple behaviours have tried to change the _"+r+'_ "'+t+'". The guilty behaviours are: '+kt(Cn(o,function(n){return n.modification().getOr({})[r]!==undefined?[n.name()]:[]}),null,2)+". This is not currently supported.")):Qe.value(yt(e,n));var t,r,o});return wt(n,i)})},Qe.value({})).map(function(n){return yt(c,n)})},Kf={classes:function(n,e){var t=Cn(n,function(n){return n.modification().getOr([])});return Qe.value(yt(e,t))},attributes:Yf,styles:Yf,domChildren:qf,defChildren:qf,innerHtml:qf,value:qf},Xf=function(n,e){return t=l.apply(undefined,[n.handler].concat(e)),r=n.purpose(),{cHandler:t,purpose:A(r)};var t,r},Jf=function(n){return n.cHandler},Qf=function(n,e){return{name:A(n),handler:A(e)}},Zf=function(n,e,t){var r,o,i=k(t,(r=n,o={},bn(e,function(n){o[n.name()]=n.handlers(r)}),o));return Gf(i,Qf)},nl=function(n){var e,i=w(e=n)?{can:A(!0),abort:A(!1),run:e}:e;return function(n,e){for(var t=[],r=2;r<arguments.length;r++)t[r-2]=arguments[r];var o=[n,e].concat(t);i.abort.apply(undefined,o)?e.stop():i.can.apply(undefined,o)&&i.run.apply(undefined,o)}},el=function(n,e,t){var r,o,i=e[t];return i?function(u,c,n,a){var e=n.slice(0);try{var t=e.sort(function(n,e){var t=n[c](),r=e[c](),o=a.indexOf(t),i=a.indexOf(r);if(-1===o)throw new Error("The ordering for "+u+" does not have an entry for "+t+".\nOrder specified: "+kt(a,null,2));if(-1===i)throw new Error("The ordering for "+u+" does not have an entry for "+r+".\nOrder specified: "+kt(a,null,2));return o<i?-1:i<o?1:0});return Qe.value(t)}catch(r){return Qe.error([r])}}("Event: "+t,"name",n,i).map(function(n){var e=yn(n,function(n){return n.handler()});return ar(e)}):(r=t,o=n,Qe.error(["The event ("+r+') has more than one behaviour that listens to it.\nWhen this occurs, you must specify an event ordering for the behaviours in your spec (e.g. [ "listing", "toggling" ]).\nThe behaviours that can trigger it are: '+kt(yn(o,function(n){return n.name()}),null,2)]))},tl=function(n,i){var e=_(n,function(r,o){return(1===r.length?Qe.value(r[0].handler()):el(r,i,o)).map(function(n){var e=nl(n),t=1<r.length?wn(i,function(e){return hn(r,function(n){return n.name()===e})}).join(" > "):r[0].name();return yt(o,{handler:e,purpose:A(t)})})});return wt(e,{})},rl=function(n){return $t("custom.definition",Rt([jt("dom","dom",tt(),Rt([Jt("tag"),or("styles",{}),or("classes",[]),or("attributes",{}),er("value"),er("innerHtml")])),Jt("components"),Jt("uid"),or("events",{}),or("apis",A({})),jt("eventOrder","eventOrder",(e={"alloy.execute":["disabling","alloy.base.behaviour","toggling"],"alloy.focus":["alloy.base.behaviour","focusing","keying"],"alloy.system.init":["alloy.base.behaviour","disabling","toggling","representing"],input:["alloy.base.behaviour","representing","streaming","invalidating"],"alloy.system.detached":["alloy.base.behaviour","representing"]},nt.mergeWithThunk(A(e))),Kt()),er("domModification"),$o("originalSpec"),or("debug.sketcher","unknown")]),n);var e},ol=function(n){var e,t={tag:n.dom().tag(),classes:n.dom().classes(),attributes:k((e=n,yt(Oa(),e.uid())),n.dom().attributes()),styles:n.dom().styles(),domChildren:yn(n.components(),function(n){return n.element()})};return Tr(k(t,n.dom().innerHtml().map(function(n){return yt("innerHtml",n)}).getOr({}),n.dom().value().map(function(n){return yt("value",n)}).getOr({})))},il=function(e,n){bn(n,function(n){no(e,n)})},ul=function(e,n){bn(n,function(n){eo(e,n)})},cl=function(e){if(e.domChildren().isSome()&&e.defChildren().isSome())throw new Error("Cannot specify children and child specs! Must be one or the other.\nDef: "+(n=Sr(e),kt(n,null,2)));return e.domChildren().fold(function(){var n=e.defChildren().getOr([]);return yn(n,sl)},function(n){return n});var n},al=function(n){var e=ue.fromTag(n.tag());Ur(e,n.attributes().getOr({})),il(e,n.classes().getOr([])),hi(e,n.styles().getOr({})),To(e,n.innerHtml().getOr(""));var t=cl(n);return Le(e,t),n.value().each(function(n){af(e,n)}),e},sl=function(n){var e=Tr(n);return al(e)},fl=function(n,e){return t=n,o=yn(r=e,function(n){return tr(n.name(),[Jt("config"),or("state",Nr)])}),i=$t("component.behaviours",Bt(o),t.behaviours).fold(function(n){throw new Error(qt(n)+"\nComplete spec:\n"+kt(t,null,2))},function(n){return n}),{list:r,data:V(i,function(n){var e=n().map(function(n){return{config:n.config(),state:n.state().init(n.config())}});return function(){return e}})};var t,r,o,i},ll=function(n){var e,t,r=(e=ht(n,"behaviours").getOr({}),t=wn(R(e),function(n){return e[n]!==undefined}),yn(t,function(n){return e[n].me}));return fl(n,r)},dl=Vr(["getSystem","config","hasConfigured","spec","connect","disconnect","element","syncComponents","readState","components","events"]),ml=function(n,e,t){var r,o,i,u,c=ol(n),a=function(e,n,t,r){var o=k({},n);bn(t,function(n){o[n.name()]=n.exhibit(e,r)});var i=Gf(o,function(n,e){return{name:function(){return n},modification:e}}),u=V(i,function(n,e){return Cn(n,function(e){return e.modification().fold(function(){return[]},function(n){return[e]})})}),c=_(u,function(e,t){return ht(Kf,t).fold(function(){return Qe.error("Unknown field type: "+t)},function(n){return n(e,t)})});return wt(c,{}).map(Or)}(t,{"alloy.base.modification":(r=n,r.domModification().fold(function(){return Or({})},Or))},e,c).getOrDie();return i=a,u=k({tag:(o=c).tag(),classes:i.classes().getOr([]).concat(o.classes().getOr([])),attributes:C(o.attributes().getOr({}),i.attributes().getOr({})),styles:C(o.styles().getOr({}),i.styles().getOr({}))},i.innerHtml().or(o.innerHtml()).map(function(n){return yt("innerHtml",n)}).getOr({}),kr("domChildren",i.domChildren(),o.domChildren()),kr("defChildren",i.defChildren(),o.defChildren()),i.value().or(o.value()).map(function(n){return yt("value",n)}).getOr({})),Tr(u)},gl=function(n,e,t){var r,o,i,u,c,a,s={"alloy.base.behaviour":(r=n,r.events())};return(o=t,i=n.eventOrder(),u=e,c=s,a=Zf(o,u,c),tl(a,i)).getOrDie()},pl=function(n){var e,t,r,o,i,u,c,a,s,f,l,d,m,g,p=$f(n),v=(e=p,t=vt("components",[])(e),yn(t,yl)),h=k(Pf,p,yt("components",v));return Qe.value((r=h,i=ao(Uf(o=function(){return g})),u=Wt(rl(k(r,{behaviours:undefined}))),c=ll(r),a=c.list,s=c.data,f=ml(u,a,s),l=al(f),d=gl(u,a,s),m=ao(u.components()),g=dl({getSystem:i.get,config:function(n){if(n===Ta())return u.apis();if(y(n))throw new Error("Invalid input: only API constant is allowed");var e=s;return(w(e[n.name()])?e[n.name()]:function(){throw new Error("Could not find "+n.name()+" in "+kt(r,null,2))})()},hasConfigured:function(n){return w(s[n.name()])},spec:A(r),readState:function(n){return s[n]().map(function(n){return n.state.readState()}).getOr("not enabled")},connect:function(n){i.set(n)},disconnect:function(){i.set(Uf(o))},element:A(l),syncComponents:function(){var n=_e(l),e=Cn(n,function(n){return i.get().getByDom(n).fold(function(){return[]},function(n){return[n]})});m.set(e)},components:m.get,events:A(d)})))},vl=function(n){var e=ue.fromText(n);return hl({element:e})},hl=function(n){var t=Gt("external.component",Rt([Jt("element"),er("uid")]),n),e=ao(Uf());t.uid().each(function(n){var e;e=t.element(),Wr(e,Ca,n)});var r=dl({getSystem:e.get,config:F.none,hasConfigured:A(!1),connect:function(n){e.set(n)},disconnect:function(){e.set(Uf(function(){return r}))},element:A(t.element()),spec:A(n),readState:A("No state"),syncComponents:I,components:A([]),events:A({})});return wa(r)},yl=function(e){return(n=e,ht(n,ya)).fold(function(){var n=k({uid:Da("")},e);return pl(n).getOrDie()},function(n){return n});var n},bl=wa,wl="alloy.item-hover",xl="alloy.item-focus",Tl=function(n){(vo(n.element()).isNone()||mi.isFocused(n))&&(mi.isFocused(n)||mi.focus(n),ee(n,wl,{item:n}))},Sl=function(n){ee(n,xl,{item:n})},Ol=A(wl),kl=A(xl),Cl=[Jt("data"),Jt("components"),Jt("dom"),er("toggling"),or("itemBehaviours",{}),or("ignoreFocus",!1),or("domModification",{}),Po("builder",function(n){return{dom:k(n.dom(),{attributes:{role:n.toggling().isSome()?"menuitemcheckbox":"menuitem"}}),behaviours:k(jr([n.toggling().fold(ri.revoke,function(n){return ri.config(k({aria:{mode:"checked"}},n))}),mi.config({ignore:n.ignoreFocus(),onFocus:function(n){Sl(n)}}),Bc.config({mode:"execution"}),vs.config({store:{mode:"memory",initialValue:n.data()}})]),n.itemBehaviours()),events:sr([(e=Yn(),r=te,lr(e,function(e,t){var n=t.event();e.getSystem().getByDom(n.target()).each(function(n){r(e,n,t)})})),pr(L()),lr(W(),Tl),lr(Gn(),mi.focus)]),components:n.components(),domModification:n.domModification(),eventOrder:n.eventOrder()};var e,r}),or("eventOrder",{})],El=[Jt("dom"),Jt("components"),Po("builder",function(n){return{dom:n.dom(),components:n.components(),events:sr([(e=Gn(),lr(e,function(n,e){e.stop()}))])};var e})],Dl=A([ia({name:"widget",overrides:function(e){return{behaviours:jr([vs.config({store:{mode:"manual",getValue:function(n){return e.data()},setValue:function(){}}})])}}})]),Il=[Jt("uid"),Jt("data"),Jt("components"),Jt("dom"),or("autofocus",!1),or("domModification",{}),ha(Dl()),Po("builder",function(t){var n=da(0,t,Dl()),e=ma("item-widget",t,n.internals()),r=function(n){return ga(n,t,"widget").map(function(n){return Bc.focusIn(n),n})},o=function(n,e){return au(e.event().target())||t.autofocus()&&e.setSource(n.element()),F.none()};return k({dom:t.dom(),components:e,domModification:t.domModification(),events:sr([br(function(n,e){r(n).each(function(n){e.stop()})}),lr(W(),Tl),lr(Gn(),function(n,e){t.autofocus()?r(n):mi.focus(n)})]),behaviours:jr([vs.config({store:{mode:"memory",initialValue:t.data()}}),mi.config({onFocus:function(n){Sl(n)}}),Bc.config({mode:"special",focusIn:t.autofocus()?function(n){r(n)}:Pr(),onLeft:o,onRight:o,onEscape:function(n,e){return mi.isFocused(n)||t.autofocus()?(t.autofocus()&&e.setSource(n.element()),F.none()):(mi.focus(n),F.some(!0))}})])})})],Al=Yt("type",{widget:Il,item:Cl,separator:El}),Ml=A([ca({factory:{sketch:function(n){var e=Gt("menu.spec item",Al,n);return e.builder()(e)}},name:"items",unit:"item",defaults:function(n,e){var t=Da("");return k({uid:t},e)},overrides:function(n,e){return{type:e.type,ignoreFocus:n.fakeFocus(),domModification:{classes:[n.markers().item()]}}}})]),Fl=A([Jt("value"),Jt("items"),Jt("dom"),Jt("components"),or("eventOrder",{}),Vc("menuBehaviours",[tu,vs,rf,Bc]),ir("movement",{mode:"menu",moveOnTab:!0},Yt("mode",{grid:[Wo(),Po("config",function(n,e){return{mode:"flatgrid",selector:"."+n.markers().item(),initSize:{numColumns:e.initSize().numColumns(),numRows:e.initSize().numRows()},focusManager:n.focusManager()}})],menu:[or("moveOnTab",!0),Po("config",function(n,e){return{mode:"menu",selector:"."+n.markers().item(),moveOnTab:e.moveOnTab(),focusManager:n.focusManager()}})]})),Qt("markers",Vo()),or("fakeFocus",!1),or("focusManager",ru()),jo("onHighlight")]),Rl=A("alloy.menu-focus"),Bl=Va({name:"Menu",configFields:Fl(),partFields:Ml(),factory:function(n,e,t,r){return k({dom:k(n.dom(),{attributes:{role:"menu"}}),uid:n.uid(),behaviours:k(jr([tu.config({highlightClass:n.markers().selectedItem(),itemClass:n.markers().item(),onHighlight:n.onHighlight()}),vs.config({store:{mode:"memory",initialValue:n.value()}}),rf.config({find:F.some}),Bc.config(n.movement().config()(n,n.movement()))]),Nc(n.menuBehaviours())),events:sr([lr(kl(),function(e,t){var n=t.event();e.getSystem().getByDom(n.target()).each(function(n){tu.highlight(e,n),t.stop(),ee(e,Rl(),{menu:e,item:n})})}),lr(Ol(),function(n,e){var t=e.event().item();tu.highlight(n,t)})]),components:e,eventOrder:n.eventOrder()})}}),Vl=function(n,t){var r=Be(t),e=po(r).bind(function(e){var o,i,n=function(n){return Re(e,n)};return n(t)?F.some(t):(o=n,(i=function(n){for(var e=0;e<n.childNodes.length;e++){var t=ue.fromDom(n.childNodes[e]);if(o(t))return F.some(t);var r=i(n.childNodes[e]);if(r.isSome())return r}return F.none()})(t.dom()))}),o=n(t);return e.each(function(e){po(r).filter(function(n){return Re(n,e)}).fold(function(){mo(e)},I)}),o},Nl=function(n,e,t,r){var o=n.getSystem().build(r);qe(n,o,t)},_l=function(n,e){return n.components()},jl=zr({fields:[],name:"replacing",apis:Object.freeze({append:function(n,e,t,r){Nl(n,0,ze,r)},prepend:function(n,e,t,r){Nl(n,0,He,r)},remove:function(n,e,t,r){var o=_l(n);Sn(o,function(n){return Re(r.element(),n.element())}).each(Ke)},set:function(e,n,t,r){var o,i;i=(o=e).components(),bn(i,Ye),Pe(o.element()),o.syncComponents(),Vl(function(){var n=yn(r,e.getSystem().build);bn(n,function(n){Ge(e,n)})},e.element())},contents:_l})}),Hl=function(t,r,o,n){return ht(o,n).bind(function(n){return ht(t,n).bind(function(n){var e=Hl(t,r,o,n);return F.some([n].concat(e))})}).getOr([])},zl=function(n,e){var t={};B(n,function(n,e){bn(n,function(n){t[n]=e})});var r=e,o=N(e,function(n,e){return{k:n,v:e}}),i=V(o,function(n,e){return[e].concat(Hl(t,r,o,e))});return V(t,function(n){return ht(i,n).getOr([n])})},Ll=function(){var i=ao({}),u=ao({}),c=ao({}),a=ao(F.none()),s=ao({}),n=function(n){return ht(u.get(),n)};return{setContents:function(n,e,t,r){a.set(F.some(n)),i.set(t),u.set(e),s.set(r);var o=zl(r,t);c.set(o)},expand:function(t){return ht(i.get(),t).map(function(n){var e=ht(c.get(),t).getOr([]);return[n].concat(e)})},refresh:function(n){return ht(c.get(),n)},collapse:function(n){return ht(c.get(),n).bind(function(n){return 1<n.length?F.some(n.slice(1)):F.none()})},lookupMenu:n,otherMenus:function(n){var e,t,r=s.get();return e=R(r),t=n,wn(e,function(n){return!hn(t,n)})},getPrimary:function(){return a.get().bind(n)},getMenus:function(){return u.get()},clear:function(){i.set({}),u.set({}),c.set({}),a.set(F.none())},isClear:function(){return a.get().isNone()}}},Pl=A("collapse-item"),$l=Ba({name:"TieredMenu",configFields:[Lo("onExecute"),Lo("onEscape"),zo("onOpenMenu"),zo("onOpenSubmenu"),jo("onCollapseMenu"),or("openImmediately",!0),nr("data",[Jt("primary"),Jt("menus"),Jt("expansions")]),or("fakeFocus",!1),jo("onHighlight"),jo("onHover"),nr("markers",[Jt("backgroundMenu")].concat(Ro()).concat(Bo())),Jt("dom"),or("navigateOnHover",!0),or("stayInDom",!1),Vc("tmenuBehaviours",[Bc,tu,rf,jl]),or("eventOrder",{})],apis:{collapseMenu:function(n,e){n.collapseMenu(e)}},factory:function(u,o){var i=function(r,n){return V(n,function(n,e){var t=Bl.sketch(k(n,{value:e,items:n.items,markers:mt(o.markers,["item","selectedItem"]),fakeFocus:u.fakeFocus(),onHighlight:u.onHighlight(),focusManager:u.fakeFocus()?{get:function(n){return tu.getHighlighted(n).map(function(n){return n.element()})},set:function(e,n){e.getSystem().getByDom(n).fold(I,function(n){tu.highlight(e,n)})}}:ru()}));return r.getSystem().build(t)})},c=Ll(),a=function(n){return vs.getValue(n).value},s=function(n){return V(u.data().menus(),function(n,e){return Cn(n.items,function(n){return"separator"===n.type?[]:[n.data.value]})})},f=function(e,n){tu.highlight(e,n),tu.getHighlighted(n).orThunk(function(){return tu.getFirst(n)}).each(function(n){re(e,n.element(),Gn())})},l=function(n,e){return Eo(yn(e,n.lookupMenu))},d=function(r,o,i){return F.from(i[0]).bind(o.lookupMenu).map(function(n){var e=l(o,i.slice(1));bn(e,function(n){no(n.element(),u.markers().backgroundMenu())}),he(n.element())||jl.append(r,bl(n)),ul(n.element(),[u.markers().backgroundMenu()]),f(r,n);var t=l(o,o.otherMenus(i));return bn(t,function(n){ul(n.element(),[u.markers().backgroundMenu()]),u.stayInDom()||jl.remove(r,n)}),n})},m=function(e,t){var n=a(t);return c.expand(n).bind(function(n){return F.from(n[0]).bind(c.lookupMenu).each(function(n){he(n.element())||jl.append(e,bl(n)),u.onOpenSubmenu()(e,t,n),tu.highlightFirst(n)}),d(e,c,n)})},r=function(e,t){var n=a(t);return c.collapse(n).bind(function(n){return d(e,c,n).map(function(n){return u.onCollapseMenu()(e,t,n),n})})},n=function(t){return function(e,n){return Bi(n.getSource(),"."+u.markers().item()).bind(function(n){return e.getSystem().getByDom(n).toOption().bind(function(n){return t(e,n).map(function(){return!0})})})}},e=sr([lr(Rl(),function(n,e){var t=e.event().menu();tu.highlight(n,t)}),br(function(e,n){var t=n.event().target();e.getSystem().getByDom(t).each(function(n){0===a(n).indexOf("collapse-item")&&r(e,n),m(e,n).fold(function(){u.onExecute()(e,n)},function(){})})}),vr(function(e,n){var t,r,o;(t=e,r=i(t,u.data().menus()),o=s(),c.setContents(u.data().primary(),r,u.data().expansions(),o),c.getPrimary()).each(function(n){jl.append(e,bl(n)),u.openImmediately()&&(f(e,n),u.onOpenMenu()(e,n))})})].concat(u.navigateOnHover()?[lr(Ol(),function(n,e){var t,r,o=e.event().item();t=n,r=a(o),c.refresh(r).bind(function(n){return d(t,c,n)}),m(n,o),u.onHover()(n,o)})]:[]));return{uid:u.uid(),dom:u.dom(),behaviours:k(jr([Bc.config({mode:"special",onRight:n(function(n,e){return au(e.element())?F.none():m(n,e)}),onLeft:n(function(n,e){return au(e.element())?F.none():r(n,e)}),onEscape:n(function(n,e){return r(n,e).orThunk(function(){return u.onEscape()(n,e).map(function(){return n})})}),focusIn:function(e,n){c.getPrimary().each(function(n){re(e,n.element(),Gn())})}}),tu.config({highlightClass:u.markers().selectedMenu(),itemClass:u.markers().menu()}),rf.config({find:function(n){return tu.getHighlighted(n)}}),jl.config({})]),Nc(u.tmenuBehaviours())),eventOrder:u.eventOrder(),apis:{collapseMenu:function(e){tu.getHighlighted(e).each(function(n){tu.getHighlighted(n).each(function(n){r(e,n)})})}},events:e}},extraApis:{tieredData:function(n,e,t){return{primary:n,menus:e,expansions:t}},singleData:function(n,e){return{primary:n,menus:yt(n,e),expansions:{}}},collapseItem:function(n){return{value:Gc(Pl()),text:n}}}}),Wl=function(n,e,t,r){return ht(e.routes(),r.start()).map(a).bind(function(n){return ht(n,r.destination()).map(a)})},Ul=function(n,e,t,r){return Wl(0,e,0,r).bind(function(e){return e.transition().map(function(n){return{transition:A(n),route:A(e)}})})},Gl=function(t,r,n){var e,o,i;(e=t,o=r,i=n,ql(e,o).bind(function(n){return Ul(e,o,i,n)})).each(function(n){var e=n.transition();eo(t.element(),e.transitionClass()),Yr(t.element(),r.destinationAttr())})},ql=function(n,e,t){var r=n.element();return qr(r,e.destinationAttr())?F.some({start:A(Gr(n.element(),e.stateAttr())),destination:A(Gr(n.element(),e.destinationAttr()))}):F.none()},Yl=function(n,e,t,r){Gl(n,e,t),qr(n.element(),e.stateAttr())&&Gr(n.element(),e.stateAttr())!==r&&e.onFinish()(n,r),Wr(n.element(),e.stateAttr(),r)},Kl=Object.freeze({findRoute:Wl,disableTransition:Gl,getCurrentRoute:ql,jumpTo:Yl,progressTo:function(t,r,o,i){var n,e;e=r,qr((n=t).element(),e.destinationAttr())&&(Wr(n.element(),e.stateAttr(),Gr(n.element(),e.destinationAttr())),Yr(n.element(),e.destinationAttr()));var u,c,a=(u=r,c=i,{start:A(Gr(t.element(),u.stateAttr())),destination:A(c)});Ul(t,r,o,a).fold(function(){Yl(t,r,o,i)},function(n){Gl(t,r,o);var e=n.transition();no(t.element(),e.transitionClass()),Wr(t.element(),r.destinationAttr(),i)})},getState:function(n,e,t){var r=n.element();return qr(r,e.stateAttr())?F.some(Gr(r,e.stateAttr())):F.none()}}),Xl=Object.freeze({events:function(o,i){return sr([lr(K(),function(t,n){var r=n.event().raw();ql(t,o).each(function(e){Wl(0,o,0,e).each(function(n){n.transition().each(function(n){r.propertyName===n.property()&&(Yl(t,o,i,e.destination()),o.onTransition()(t,e))})})})}),vr(function(n,e){Yl(n,o,i,o.initialState())})])}}),Jl=[or("destinationAttr","data-transitioning-destination"),or("stateAttr","data-transitioning-state"),Jt("initialState"),jo("onTransition"),jo("onFinish"),Qt("routes",Vt(Qe.value,Vt(Qe.value,Rt([rr("transition",[Jt("property"),Jt("transitionClass")])]))))],Ql=zr({fields:Jl,name:"transitioning",active:Xl,apis:Kl,extra:{createRoutes:function(n){var r={};return B(n,function(n,e){var t=e.split("<->");r[t[0]]=yt(t[1],n),r[t[1]]=yt(t[0],n)}),r},createBistate:function(n,e,t){return bt([{key:n,value:yt(e,t)},{key:e,value:yt(n,t)}])},createTristate:function(n,e,t,r){return bt([{key:n,value:bt([{key:e,value:r},{key:t,value:r}])},{key:e,value:bt([{key:n,value:r},{key:t,value:r}])},{key:t,value:bt([{key:n,value:r},{key:e,value:r}])}])}}}),Zl=ai.resolve("scrollable"),nd={register:function(n){no(n,Zl)},deregister:function(n){eo(n,Zl)},scrollable:A(Zl)},ed=function(n){return ht(n,"format").getOr(n.title)},td=function(n,e,t,r,o){return{data:{value:n,text:e},type:"item",dom:{tag:"div",classes:o?[ai.resolve("styles-item-is-menu")]:[]},toggling:{toggleOnExecute:!1,toggleClass:ai.resolve("format-matches"),selected:t},itemBehaviours:jr(o?[]:[oi(n,function(n,e){(e?ri.on:ri.off)(n)})]),components:[{dom:{tag:"div",attributes:{style:r},innerHtml:e}}]}},rd=function(n,e,t,r){return{value:n,dom:{tag:"div"},components:[Na.sketch({dom:{tag:"div",classes:[ai.resolve("styles-collapser")]},components:r?[{dom:{tag:"span",classes:[ai.resolve("styles-collapse-icon")]}},vl(n)]:[vl(n)],action:function(n){if(r){var e=t().get(n);$l.collapseMenu(e)}}}),{dom:{tag:"div",classes:[ai.resolve("styles-menu-items-container")]},components:[Bl.parts().items({})],behaviours:jr([nf("adhoc-scrollable-menu",[vr(function(n,e){vi(n.element(),"overflow-y","auto"),vi(n.element(),"-webkit-overflow-scrolling","touch"),nd.register(n.element())}),hr(function(n){xi(n.element(),"overflow-y"),xi(n.element(),"-webkit-overflow-scrolling"),nd.deregister(n.element())})])])}],items:e,menuBehaviours:jr([Ql.config({initialState:"after",routes:Ql.createTristate("before","current","after",{transition:{property:"transform",transitionClass:"transitioning"}})})])}},od=function(r){var o,i,n,e,t,u=(o=r.formats,i=function(){return c},n=rd("Styles",[].concat(yn(o.items,function(n){return td(ed(n),n.title,n.isSelected(),n.getPreview(),xt(o.expansions,ed(n)))})),i,!1),e=V(o.menus,function(n,e){var t=yn(n,function(n){return td(ed(n),n.title,n.isSelected!==undefined&&n.isSelected(),n.getPreview!==undefined?n.getPreview():"",xt(o.expansions,ed(n)))});return rd(e,t,i,!0)}),t=k(e,yt("styles",n)),{tmenu:$l.tieredData("styles",t,o.expansions)}),c=Bs($l.sketch({dom:{tag:"div",classes:[ai.resolve("styles-menu")]},components:[],fakeFocus:!0,stayInDom:!0,onExecute:function(n,e){var t=vs.getValue(e);return r.handle(e,t.value),F.none()},onEscape:function(){return F.none()},onOpenMenu:function(n,e){var t=xs(n.element());ws(e.element(),t),Ql.jumpTo(e,"current")},onOpenSubmenu:function(n,e,t){var r=xs(n.element()),o=Fi(e.element(),'[role="menu"]').getOrDie("hacky"),i=n.getSystem().getByDom(o).getOrDie();ws(t.element(),r),Ql.progressTo(i,"before"),Ql.jumpTo(t,"after"),Ql.progressTo(t,"current")},onCollapseMenu:function(n,e,t){var r=Fi(e.element(),'[role="menu"]').getOrDie("hacky"),o=n.getSystem().getByDom(r).getOrDie();Ql.progressTo(o,"after"),Ql.progressTo(t,"current")},navigateOnHover:!1,openImmediately:!0,data:u.tmenu,markers:{backgroundMenu:ai.resolve("styles-background-menu"),menu:ai.resolve("styles-menu"),selectedMenu:ai.resolve("styles-selected-menu"),item:ai.resolve("styles-item"),selectedItem:ai.resolve("styles-selected-item")}}));return c.asSpec()},id=function(n){return xt(n,"items")?(t=k(gt(e=n,["items"]),{menu:!0}),r=ud(e.items),{item:t,menus:k(r.menus,yt(e.title,r.items)),expansions:k(r.expansions,yt(e.title,e.title))}):{item:n,menus:{},expansions:{}};var e,t,r},ud=function(n){return xn(n,function(n,e){var t=id(e);return{menus:k(n.menus,t.menus),items:[t.item].concat(n.items),expansions:k(n.expansions,t.expansions)}},{menus:{},expansions:{},items:[]})},cd={expand:ud},ad=function(u,n){var c=function(n){return function(){return u.formatter.match(n)}},a=function(n){return function(){return u.formatter.getCssText(n)}},e=ht(n,"style_formats").getOr(zf),s=function(n){return yn(n,function(n){if(xt(n,"items")){var e=s(n.items);return k(k(n,{isSelected:A(!1),getPreview:A("")}),{items:e})}return xt(n,"format")?k(i=n,{isSelected:c(i.format),getPreview:a(i.format)}):(r=Gc((t=n).title),o=k(t,{format:r,isSelected:c(r),getPreview:a(r)}),u.formatter.register(r,o),o);var t,r,o,i})};return s(e)},sd=function(t,n,r){var e,o,i,u=(e=t,i=(o=function(n){return Cn(n,function(n){return n.items!==undefined?0<o(n.items).length?[n]:[]:!xt(n,"format")||e.formatter.canApply(n.format)?[n]:[]})})(n),cd.expand(i));return od({formats:u,handle:function(n,e){t.undoManager.transact(function(){ri.isOn(n)?t.formatter.remove(e):t.formatter.apply(e)}),r()}})},fd=["undo","bold","italic","link","image","bullist","styleselect"],ld=function(n){var e=n.replace(/\|/g," ").trim();return 0<e.length?e.split(/\s+/):[]},dd=function(n){return Cn(n,function(n){return g(n)?dd(n):ld(n)})},md=function(n){var e=n.toolbar!==undefined?n.toolbar:fd;return g(e)?dd(e):ld(e)},gd=function(r,o){var n=function(n){return function(){return $a.forToolbarCommand(o,n)}},e=function(n){return function(){return $a.forToolbarStateCommand(o,n)}},t=function(n,e,t){return function(){return $a.forToolbarStateAction(o,n,e,t)}},i=n("undo"),u=n("redo"),c=e("bold"),a=e("italic"),s=e("underline"),f=n("removeformat"),l=t("unlink","link",function(){o.execCommand("unlink",null,!1)}),d=t("unordered-list","ul",function(){o.execCommand("InsertUnorderedList",null,!1)}),m=t("ordered-list","ol",function(){o.execCommand("InsertOrderedList",null,!1)}),g=ad(o,o.settings),p=function(){return sd(o,g,function(){o.fire("scrollIntoView")})},v=function(n,e){return{isSupported:function(){return n.forall(function(n){return xt(o.buttons,n)})},sketch:e}};return{undo:v(F.none(),i),redo:v(F.none(),u),bold:v(F.none(),c),italic:v(F.none(),a),underline:v(F.none(),s),removeformat:v(F.none(),f),link:v(F.none(),function(){return e=r,t=o,$a.forToolbarStateAction(t,"link","link",function(){var n=Hf(e,t);e.setContextToolbar(n),Zs(t,function(){e.focusToolbar()}),Xs.query(t).each(function(n){t.selection.select(n.dom())})});var e,t}),unlink:v(F.none(),l),image:v(F.none(),function(){return Ws(o)}),bullist:v(F.some("bullist"),d),numlist:v(F.some("numlist"),m),fontsizeselect:v(F.none(),function(){return e=o,n={onChange:function(n){Ms.apply(e,n)},getInitialValue:function(){return Ms.get(e)}},Os(r,"font-size",function(){return Rs(n)});var e,n}),forecolor:v(F.none(),function(){return Cs(r,o)}),styleselect:v(F.none(),function(){return $a.forToolbar("style-formats",function(n){o.fire("toReading"),r.dropup().appear(p,ri.on,n)},jr([ri.config({toggleClass:ai.resolve("toolbar-button-selected"),toggleOnExecute:!1,aria:{mode:"pressed"}}),Go.config({channels:bt([ui(wo.orientationChanged(),ri.off),ui(wo.dropupDismissed(),ri.off)])})]))})}},pd=function(n,t){var e=md(n),r={};return Cn(e,function(n){var e=!xt(r,n)&&xt(t,n)&&t[n].isSupported()?[t[n].sketch()]:[];return r[n]=!0,e})},vd=function(m,g){return function(n){if(m(n)){var e,t,r,o,i,u,c,a=ue.fromDom(n.target),s=function(){n.stopPropagation()},f=function(){n.preventDefault()},l=v(f,s),d=(e=a,t=n.clientX,r=n.clientY,o=s,i=f,u=l,c=n,{target:A(e),x:A(t),y:A(r),stop:o,prevent:i,kill:u,raw:A(c)});g(d)}}},hd=function(n,e,t,r,o){var i=vd(t,r);return n.dom().addEventListener(e,i,o),{unbind:l(yd,n,e,i,o)}},yd=function(n,e,t,r){n.dom().removeEventListener(e,t,r)},bd=A(!0),wd=function(n,e,t){return hd(n,e,bd,t,!1)},xd=function(n,e,t){return hd(n,e,bd,t,!0)},Td=function(n){var e=n.matchMedia("(orientation: portrait)").matches;return{isPortrait:A(e)}},Sd=Td,Od=function(r,e){var n=ue.fromDom(r),o=null,t=wd(n,"orientationchange",function(){clearInterval(o);var n=Td(r);e.onChange(n),i(function(){e.onReady(n)})}),i=function(n){clearInterval(o);var e=r.innerHeight,t=0;o=setInterval(function(){e!==r.innerHeight?(clearInterval(o),n(F.some(r.innerHeight))):20<t&&(clearInterval(o),n(F.none())),t++},50)};return{onAdjustment:i,destroy:function(){t.unbind()}}},kd=function(n){var e=zn.detect().os.isiOS(),t=Td(n).isPortrait();return e&&!t?n.screen.height:n.screen.width},Cd=function(n){var e=n.raw();return e.touches===undefined||1!==e.touches.length?F.none():F.some(e.touches[0])},Ed=function(t){var r,o,i,u=ao(F.none()),c=(r=function(n){u.set(F.none()),t.triggerEvent(Kn(),n)},o=400,i=null,{cancel:function(){null!==i&&(p.clearTimeout(i),i=null)},schedule:function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];i=p.setTimeout(function(){r.apply(null,n),i=null},o)}}),a=bt([{key:j(),value:function(t){return Cd(t).each(function(n){c.cancel();var e={x:A(n.clientX),y:A(n.clientY),target:t.target};c.schedule(t),u.set(F.some(e))}),F.none()}},{key:H(),value:function(n){return c.cancel(),Cd(n).each(function(i){u.get().each(function(n){var e,t,r,o;e=i,t=n,r=Math.abs(e.clientX-t.x()),o=Math.abs(e.clientY-t.y()),(5<r||5<o)&&u.set(F.none())})}),F.none()}},{key:z(),value:function(e){return c.cancel(),u.get().filter(function(n){return Re(n.target(),e.target())}).map(function(n){return t.triggerEvent(qn(),e)})}}]);return{fireIfReady:function(e,n){return ht(a,n).bind(function(n){return n(e)})}}},Dd=function(t){var e=Ed({triggerEvent:function(n,e){t.onTapContent(e)}});return{fireTouchstart:function(n){e.fireIfReady(n,"touchstart")},onTouchend:function(){return wd(t.body(),"touchend",function(n){e.fireIfReady(n,"touchend")})},onTouchmove:function(){return wd(t.body(),"touchmove",function(n){e.fireIfReady(n,"touchmove")})}}},Id=6<=zn.detect().os.version.major,Ad=function(r,e,t){var o=Dd(r),i=Be(e),u=function(n){return!Re(n.start(),n.finish())||n.soffset()!==n.foffset()},n=function(){var n=r.doc().dom().hasFocus()&&r.getSelection().exists(u);t.getByDom(e).each(!0===(n||po(i).filter(function(n){return"input"===me(n)}).exists(function(n){return n.dom().selectionStart!==n.dom().selectionEnd}))?ri.on:ri.off)},c=[wd(r.body(),"touchstart",function(n){r.onTouchContent(),o.fireTouchstart(n)}),o.onTouchmove(),o.onTouchend(),wd(e,"touchstart",function(n){r.onTouchToolstrip()}),r.onToReading(function(){go(r.body())}),r.onToEditing(I),r.onScrollToCursor(function(n){n.preventDefault(),r.getCursorBox().each(function(n){var e=r.win(),t=n.top()>e.innerHeight||n.bottom()>e.innerHeight?n.bottom()-e.innerHeight+50:0;0!==t&&e.scrollTo(e.pageXOffset,e.pageYOffset+t)})})].concat(!0===Id?[]:[wd(ue.fromDom(r.win()),"blur",function(){t.getByDom(e).each(ri.off)}),wd(i,"select",n),wd(r.doc(),"selectionchange",n)]);return{destroy:function(){bn(c,function(n){n.unbind()})}}},Md=function(n,e){var t=parseInt(Gr(n,e),10);return isNaN(t)?0:t},Fd=(Hs=ve,zs="text",{get:function(n){if(!Hs(n))throw new Error("Can only get "+zs+" value of a "+zs+" node");return Ls(n).getOr("")},getOption:Ls=function(n){return Hs(n)?F.from(n.dom().nodeValue):F.none()},set:function(n,e){if(!Hs(n))throw new Error("Can only set raw "+zs+" value of a "+zs+" node");n.dom().nodeValue=e}}),Rd=function(n){return"img"===me(n)?1:(e=n,Fd.getOption(e)).fold(function(){return _e(n).length},function(n){return n.length});var e},Bd={create:we("start","soffset","finish","foffset")},Vd=Ze([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),Nd={before:Vd.before,on:Vd.on,after:Vd.after,cata:function(n,e,t,r){return n.fold(e,t,r)},getStart:function(n){return n.fold(h,h,h)}},_d=Ze([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),jd={domRange:_d.domRange,relative:_d.relative,exact:_d.exact,exactFromRange:function(n){return _d.exact(n.start(),n.soffset(),n.finish(),n.foffset())},getWin:function(n){var e=n.match({domRange:function(n){return ue.fromDom(n.startContainer)},relative:function(n,e){return Nd.getStart(n)},exact:function(n,e,t,r){return n}});return Ve(e)},range:Bd.create},Hd=function(n,e,t){var r,o,i=n.document.createRange();return r=i,e.fold(function(n){r.setStartBefore(n.dom())},function(n,e){r.setStart(n.dom(),e)},function(n){r.setStartAfter(n.dom())}),o=i,t.fold(function(n){o.setEndBefore(n.dom())},function(n,e){o.setEnd(n.dom(),e)},function(n){o.setEndAfter(n.dom())}),i},zd=function(n,e,t,r,o){var i=n.document.createRange();return i.setStart(e.dom(),t),i.setEnd(r.dom(),o),i},Ld=function(n){return{left:A(n.left),top:A(n.top),right:A(n.right),bottom:A(n.bottom),width:A(n.width),height:A(n.height)}},Pd=Ze([{ltr:["start","soffset","finish","foffset"]},{rtl:["start","soffset","finish","foffset"]}]),$d=function(n,e,t){return e(ue.fromDom(t.startContainer),t.startOffset,ue.fromDom(t.endContainer),t.endOffset)},Wd=function(n,e){var o,t,r,i=(o=n,e.match({domRange:function(n){return{ltr:A(n),rtl:F.none}},relative:function(n,e){return{ltr:J(function(){return Hd(o,n,e)}),rtl:J(function(){return F.some(Hd(o,e,n))})}},exact:function(n,e,t,r){return{ltr:J(function(){return zd(o,n,e,t,r)}),rtl:J(function(){return F.some(zd(o,t,r,n,e))})}}}));return(r=(t=i).ltr()).collapsed?t.rtl().filter(function(n){return!1===n.collapsed}).map(function(n){return Pd.rtl(ue.fromDom(n.endContainer),n.endOffset,ue.fromDom(n.startContainer),n.startOffset)}).getOrThunk(function(){return $d(0,Pd.ltr,r)}):$d(0,Pd.ltr,r)},Ud=function(n,e){var t=me(n);return"input"===t?Nd.after(n):hn(["br","img"],t)?0===e?Nd.before(n):Nd.after(n):Nd.on(n,e)},Gd=function(n,e,t,r){var o,i,u,c,a,s=(i=e,u=t,c=r,(a=Be(o=n).dom().createRange()).setStart(o.dom(),i),a.setEnd(u.dom(),c),a),f=Re(n,t)&&e===r;return s.collapsed&&!f},qd=function(n,e,t,r,o){var i,u,c=zd(n,e,t,r,o);i=n,u=c,F.from(i.getSelection()).each(function(n){n.removeAllRanges(),n.addRange(u)})},Yd=function(n,e,t,r,o){var i,u,c,a,l,s=(i=r,u=o,c=Ud(e,t),a=Ud(i,u),jd.relative(c,a));Wd(l=n,s).match({ltr:function(n,e,t,r){qd(l,n,e,t,r)},rtl:function(n,e,t,r){var o,i,u,c,a,s=l.getSelection();if(s.setBaseAndExtent)s.setBaseAndExtent(n.dom(),e,t.dom(),r);else if(s.extend)try{i=n,u=e,c=t,a=r,(o=s).collapse(i.dom(),u),o.extend(c.dom(),a)}catch(f){qd(l,t,r,n,e)}else qd(l,t,r,n,e)}})},Kd=function(n){var e=ue.fromDom(n.anchorNode),t=ue.fromDom(n.focusNode);return Gd(e,n.anchorOffset,t,n.focusOffset)?F.some(Bd.create(e,n.anchorOffset,t,n.focusOffset)):function(n){if(0<n.rangeCount){var e=n.getRangeAt(0),t=n.getRangeAt(n.rangeCount-1);return F.some(Bd.create(ue.fromDom(e.startContainer),e.startOffset,ue.fromDom(t.endContainer),t.endOffset))}return F.none()}(n)},Xd=function(n){return F.from(n.getSelection()).filter(function(n){return 0<n.rangeCount}).bind(Kd)},Jd=function(n,e){var i,t,r,o,u=Wd(i=n,e).match({ltr:function(n,e,t,r){var o=i.document.createRange();return o.setStart(n.dom(),e),o.setEnd(t.dom(),r),o},rtl:function(n,e,t,r){var o=i.document.createRange();return o.setStart(t.dom(),r),o.setEnd(n.dom(),e),o}});return r=(t=u).getClientRects(),0<(o=0<r.length?r[0]:t.getBoundingClientRect()).width||0<o.height?F.some(o).map(Ld):F.none()},Qd=function(n){return{left:n.left,top:n.top,right:n.right,bottom:n.bottom,width:A(2),height:n.height}},Zd=function(n){return{left:A(n.left),top:A(n.top),right:A(n.right),bottom:A(n.bottom),width:A(n.width),height:A(n.height)}},nm=function(n){var e=n.getSelection();return e!==undefined&&0<e.rangeCount?function(t){if(t.collapsed){var r=ue.fromDom(t.startContainer);return Ne(r).bind(function(n){var e=jd.exact(r,t.startOffset,n,Rd(n));return Jd(t.startContainer.ownerDocument.defaultView,e).map(Qd).map(In)}).getOr([])}return yn(t.getClientRects(),Zd)}(e.getRangeAt(0)):[]},em=function(n){n.focus();var e=ue.fromDom(n.document.body);(po().exists(function(n){return hn(["input","textarea"],me(n))})?function(n){setTimeout(function(){n()},0)}:a)(function(){po().each(go),mo(e)})},tm="data-"+ai.resolve("last-outer-height"),rm=function(n,e){Wr(n,tm,e)},om=function(n){return{top:A(n.top()),bottom:A(n.top()+n.height())}},im=function(n,e){var t=Md(e,tm),r=n.innerHeight;return r<t?F.some(t-r):F.none()},um=function(n,u){var e=ue.fromDom(u.document.body),t=wd(ue.fromDom(n),"resize",function(){im(n,e).each(function(i){var n,e;(n=u,e=nm(n),0<e.length?F.some(e[0]).map(om):F.none()).each(function(n){var e,t,r,o=(e=u,r=i,(t=n).top()>e.innerHeight||t.bottom()>e.innerHeight?Math.min(r,t.bottom()-e.innerHeight+50):0);0!==o&&u.scrollTo(u.pageXOffset,u.pageYOffset+o)})}),rm(e,n.innerHeight)});return rm(e,n.innerHeight),{toEditing:function(){em(u)},destroy:function(){t.unbind()}}},cm=function(n){return F.some(ue.fromDom(n.dom().contentWindow.document.body))},am=function(n){return F.some(ue.fromDom(n.dom().contentWindow.document))},sm=function(n){return F.from(n.dom().contentWindow)},fm=function(n){return sm(n).bind(Xd)},lm=function(n){return n.getFrame()},dm=function(n,t){return function(e){return e[n].getOrThunk(function(){var n=lm(e);return function(){return t(n)}})()}},mm=function(n,e,t,r){return n[t].getOrThunk(function(){return function(n){return wd(e,r,n)}})},gm=function(n){return{left:A(n.left),top:A(n.top),right:A(n.right),bottom:A(n.bottom),width:A(n.width),height:A(n.height)}},pm={getBody:dm("getBody",cm),getDoc:dm("getDoc",am),getWin:dm("getWin",sm),getSelection:dm("getSelection",fm),getFrame:lm,getActiveApi:function(c){var a=lm(c);return cm(a).bind(function(u){return am(a).bind(function(i){return sm(a).map(function(o){var n=ue.fromDom(i.dom().documentElement),e=c.getCursorBox.getOrThunk(function(){return function(){return(n=o,Xd(n).map(function(n){return jd.exact(n.start(),n.soffset(),n.finish(),n.foffset())})).bind(function(n){return Jd(o,n).orThunk(function(){return Xd(o).filter(function(n){return Re(n.start(),n.finish())&&n.soffset()===n.foffset()}).bind(function(n){var e=n.start().dom().getBoundingClientRect();return 0<e.width||0<e.height?F.some(e).map(gm):F.none()})})});var n}}),t=c.setSelection.getOrThunk(function(){return function(n,e,t,r){Yd(o,n,e,t,r)}}),r=c.clearSelection.getOrThunk(function(){return function(){o.getSelection().removeAllRanges()}});return{body:A(u),doc:A(i),win:A(o),html:A(n),getSelection:l(fm,a),setSelection:t,clearSelection:r,frame:A(a),onKeyup:mm(c,i,"onKeyup","keyup"),onNodeChanged:mm(c,i,"onNodeChanged","selectionchange"),onDomChanged:c.onDomChanged,onScrollToCursor:c.onScrollToCursor,onScrollToElement:c.onScrollToElement,onToReading:c.onToReading,onToEditing:c.onToEditing,onToolbarScrollStart:c.onToolbarScrollStart,onTouchContent:c.onTouchContent,onTapContent:c.onTapContent,onTouchToolstrip:c.onTouchToolstrip,getCursorBox:e}})})})}},vm="data-ephox-mobile-fullscreen-style",hm="position:absolute!important;",ym="top:0!important;left:0!important;margin:0!important;padding:0!important;width:100%!important;height:100%!important;overflow:visible!important;",bm=zn.detect().os.isAndroid(),wm=function(n,e){var t,r,o,i=function(r){return function(n){var e=Gr(n,"style"),t=e===undefined?"no-styles":e.trim();t!==r&&(Wr(n,vm,t),Wr(n,"style",r))}},u=(t="*",Di(n,function(n){return Ie(n,t)},r)),c=Cn(u,function(n){var e;return e="*",Ii(n,function(n){return Ie(n,e)})}),a=(o=yi(e,"background-color"))!==undefined&&""!==o?"background-color:"+o+"!important":"background-color:rgb(255,255,255)!important;";bn(c,i("display:none!important;")),bn(u,i(hm+ym+a)),i((!0===bm?"":hm)+ym+a)(n)},xm=function(){var n=Me("["+vm+"]");bn(n,function(n){var e=Gr(n,vm);"no-styles"!==e?Wr(n,"style",e):Yr(n,"style"),Yr(n,vm)})},Tm=function(){var e=Mi("head").getOrDie(),n=Mi('meta[name="viewport"]').getOrThunk(function(){var n=ue.fromTag("meta");return Wr(n,"name","viewport"),ze(e,n),n}),t=Gr(n,"content");return{maximize:function(){Wr(n,"content","width=device-width, initial-scale=1.0, user-scalable=no, maximum-scale=1.0")},restore:function(){t!==undefined&&null!==t&&0<t.length?Wr(n,"content",t):Wr(n,"content","user-scalable=yes")}}},Sm=function(e,n){var t=Tm(),r=Rf(),o=Rf();return{enter:function(){n.hide(),no(e.container,ai.resolve("fullscreen-maximized")),no(e.container,ai.resolve("android-maximized")),t.maximize(),no(e.body,ai.resolve("android-scroll-reload")),r.set(um(e.win,pm.getWin(e.editor).getOrDie("no"))),pm.getActiveApi(e.editor).each(function(n){wm(e.container,n.body()),o.set(Ad(n,e.toolstrip,e.alloy))})},exit:function(){t.restore(),n.show(),eo(e.container,ai.resolve("fullscreen-maximized")),eo(e.container,ai.resolve("android-maximized")),xm(),eo(e.body,ai.resolve("android-scroll-reload")),o.clear(),r.clear()}}},Om=function(t,r){var o=null;return{cancel:function(){null!==o&&(p.clearTimeout(o),o=null)},throttle:function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];null!==o&&p.clearTimeout(o),o=p.setTimeout(function(){t.apply(null,n),o=null},r)}}},km=function(n,e){var t,r,o,i=Bs(of.sketch({dom:Ha('<div aria-hidden="true" class="${prefix}-mask-tap-icon"></div>'),containerBehaviours:jr([ri.config({toggleClass:ai.resolve("mask-tap-icon-selected"),toggleOnExecute:!1})])})),u=(t=n,r=200,o=null,{cancel:function(){null!==o&&(p.clearTimeout(o),o=null)},throttle:function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];null===o&&(o=p.setTimeout(function(){t.apply(null,n),o=null},r))}});return of.sketch({dom:Ha('<div class="${prefix}-disabled-mask"></div>'),components:[of.sketch({dom:Ha('<div class="${prefix}-content-container"></div>'),components:[Na.sketch({dom:Ha('<div class="${prefix}-content-tap-section"></div>'),components:[i.asSpec()],action:function(n){u.throttle()},buttonBehaviours:jr([ri.config({toggleClass:ai.resolve("mask-tap-icon-selected")})])})]})]})},Cm=Bt([nr("editor",[Jt("getFrame"),er("getBody"),er("getDoc"),er("getWin"),er("getSelection"),er("setSelection"),er("clearSelection"),er("cursorSaver"),er("onKeyup"),er("onNodeChanged"),er("getCursorBox"),Jt("onDomChanged"),or("onTouchContent",I),or("onTapContent",I),or("onTouchToolstrip",I),or("onScrollToCursor",A({unbind:I})),or("onScrollToElement",A({unbind:I})),or("onToEditing",A({unbind:I})),or("onToReading",A({unbind:I})),or("onToolbarScrollStart",h)]),Jt("socket"),Jt("toolstrip"),Jt("dropup"),Jt("toolbar"),Jt("container"),Jt("alloy"),ur("win",function(n){return Be(n.socket).dom().defaultView}),ur("body",function(n){return ue.fromDom(n.socket.dom().ownerDocument.body)}),or("translate",h),or("setReadOnly",I),or("readOnlyOnInit",A(!0))]),Em=function(n){var e=Ut("Getting AndroidWebapp schema",Cm,n);vi(e.toolstrip,"width","100%");var t=yl(km(function(){e.setReadOnly(e.readOnlyOnInit()),o.enter()},e.translate));e.alloy.add(t);var r={show:function(){e.alloy.add(t)},hide:function(){e.alloy.remove(t)}};ze(e.container,t.element());var o=Sm(e,r);return{setReadOnly:e.setReadOnly,refreshStructure:I,enter:o.enter,exit:o.exit,destroy:I}},Dm=A([or("shell",!0),Vc("toolbarBehaviours",[jl])]),Im=A([ua({name:"groups",overrides:function(n){return{behaviours:jr([jl.config({})])}}})]),Am=Va({name:"Toolbar",configFields:Dm(),partFields:Im(),factory:function(e,n,t,r){var o=function(n){return e.shell()?F.some(n):ga(n,e,"groups")},i=e.shell()?{behaviours:[jl.config({})],components:[]}:{behaviours:[],components:n};return{uid:e.uid(),dom:e.dom(),components:i.components,behaviours:k(jr(i.behaviours),Nc(e.toolbarBehaviours())),apis:{setGroups:function(n,e){o(n).fold(function(){throw p.console.error("Toolbar was defined to not be a shell, but no groups container was specified in components"),new Error("Toolbar was defined to not be a shell, but no groups container was specified in components")},function(n){jl.set(n,e)})}},domModification:{attributes:{role:"group"}}}},apis:{setGroups:function(n,e,t){n.setGroups(e,t)}}}),Mm=A([Jt("items"),(Ps=["itemClass"],nr("markers",yn(Ps,Jt))),Vc("tgroupBehaviours",[Bc])]),Fm=A([ca({name:"items",unit:"item",overrides:function(n){return{domModification:{classes:[n.markers().itemClass()]}}}})]),Rm=Va({name:"ToolbarGroup",configFields:Mm(),partFields:Fm(),factory:function(n,e,t,r){return k({dom:{attributes:{role:"toolbar"}}},{uid:n.uid(),dom:n.dom(),components:e,behaviours:k(jr([Bc.config({mode:"flow",selector:"."+n.markers().itemClass()})]),Nc(n.tgroupBehaviours())),"debug.sketcher":t["debug.sketcher"]})}}),Bm="data-"+ai.resolve("horizontal-scroll"),Vm=function(n){return"true"===Gr(n,Bm)?0<(t=n).dom().scrollLeft||function(n){n.dom().scrollLeft=1;var e=0!==n.dom().scrollLeft;return n.dom().scrollLeft=0,e}(t):0<(e=n).dom().scrollTop||function(n){n.dom().scrollTop=1;var e=0!==n.dom().scrollTop;return n.dom().scrollTop=0,e}(e);var e,t},Nm={exclusive:function(n,e){return wd(n,"touchmove",function(n){Bi(n.target(),e).filter(Vm).fold(function(){n.raw().preventDefault()},I)})},markAsHorizontal:function(n){Wr(n,Bm,"true")}};function _m(){var e=function(n){var e=!0===n.scrollable?"${prefix}-toolbar-scrollable-group":"";return{dom:Ha('<div aria-label="'+n.label+'" class="${prefix}-toolbar-group '+e+'"></div>'),tgroupBehaviours:jr([nf("adhoc-scrollable-toolbar",!0===n.scrollable?[yr(function(n,e){vi(n.element(),"overflow-x","auto"),Nm.markAsHorizontal(n.element()),nd.register(n.element())})]:[])]),components:[of.sketch({components:[Rm.parts().items({})]})],markers:{itemClass:ai.resolve("toolbar-group-item")},items:n.items}},t=yl(Am.sketch({dom:Ha('<div class="${prefix}-toolbar"></div>'),components:[Am.parts().groups({})],toolbarBehaviours:jr([ri.config({toggleClass:ai.resolve("context-toolbar"),toggleOnExecute:!1,aria:{mode:"none"}}),Bc.config({mode:"cyclic"})]),shell:!0})),n=yl(of.sketch({dom:{classes:[ai.resolve("toolstrip")]},components:[bl(t)],containerBehaviours:jr([ri.config({toggleClass:ai.resolve("android-selection-context-toolbar"),toggleOnExecute:!1})])})),r=function(){Am.setGroups(t,o.get()),ri.off(t)},o=ao([]);return{wrapper:A(n),toolbar:A(t),createGroups:function(n){return yn(n,v(Rm.sketch,e))},setGroups:function(n){o.set(n),r()},setContextToolbar:function(n){ri.on(t),Am.setGroups(t,n)},restoreToolbar:function(){ri.isOn(t)&&r()},refresh:function(){},focus:function(){Bc.focusIn(t)}}}var jm=function(n,e){jl.append(n,bl(e))},Hm=function(n,e){jl.remove(n,e)},zm=function(n){return yl(Na.sketch({dom:Ha('<div class="${prefix}-mask-edit-icon ${prefix}-icon"></div>'),action:function(){n.run(function(n){n.setReadOnly(!1)})}}))},Lm=function(){return yl(of.sketch({dom:Ha('<div class="${prefix}-editor-socket"></div>'),components:[],containerBehaviours:jr([jl.config({})])}))},Pm=function(n,e,t,r){(!0===t?co.toAlpha:co.toOmega)(r),(t?jm:Hm)(n,e)},$m=function(e,n){return n.getAnimationRoot().fold(function(){return e.element()},function(n){return n(e)})},Wm=function(n){return n.dimension().property()},Um=function(n,e){return n.dimension().getDimension()(e)},Gm=function(n,e){var t=$m(n,e);ul(t,[e.shrinkingClass(),e.growingClass()])},qm=function(n,e){eo(n.element(),e.openClass()),no(n.element(),e.closedClass()),vi(n.element(),Wm(e),"0px"),Ti(n.element())},Ym=function(n,e){eo(n.element(),e.closedClass()),no(n.element(),e.openClass()),xi(n.element(),Wm(e))},Km=function(n,e,t){t.setCollapsed(),vi(n.element(),Wm(e),Um(e,n.element())),Ti(n.element());var r=$m(n,e);no(r,e.shrinkingClass()),qm(n,e),e.onStartShrink()(n)},Xm=function(n,e,t){var r=function(n,e){Ym(n,e);var t=Um(e,n.element());return qm(n,e),t}(n,e),o=$m(n,e);no(o,e.growingClass()),Ym(n,e),vi(n.element(),Wm(e),r),t.setExpanded(),e.onStartGrow()(n)},Jm=function(n,e,t){var r=$m(n,e);return!0===ro(r,e.growingClass())},Qm=function(n,e,t){var r=$m(n,e);return!0===ro(r,e.shrinkingClass())},Zm=Object.freeze({grow:function(n,e,t){t.isExpanded()||Xm(n,e,t)},shrink:function(n,e,t){t.isExpanded()&&Km(n,e,t)},immediateShrink:function(n,e,t){var r,o;t.isExpanded()&&(r=n,o=e,t.setCollapsed(),vi(r.element(),Wm(o),Um(o,r.element())),Ti(r.element()),Gm(r,o),qm(r,o),o.onStartShrink()(r),o.onShrunk()(r))},hasGrown:function(n,e,t){return t.isExpanded()},hasShrunk:function(n,e,t){return t.isCollapsed()},isGrowing:Jm,isShrinking:Qm,isTransitioning:function(n,e,t){return!0===Jm(n,e)||!0===Qm(n,e)},toggleGrow:function(n,e,t){(t.isExpanded()?Km:Xm)(n,e,t)},disableTransitions:Gm}),ng=Object.freeze({exhibit:function(n,e){var t=e.expanded();return Or(t?{classes:[e.openClass()],styles:{}}:{classes:[e.closedClass()],styles:yt(e.dimension().property(),"0px")})},events:function(t,r){return sr([lr(K(),function(n,e){e.event().raw().propertyName===t.dimension().property()&&(Gm(n,t),r.isExpanded()&&xi(n.element(),t.dimension().property()),(r.isExpanded()?t.onGrown():t.onShrunk())(n))})])}}),eg=[Jt("closedClass"),Jt("openClass"),Jt("shrinkingClass"),Jt("growingClass"),er("getAnimationRoot"),jo("onShrunk"),jo("onStartShrink"),jo("onGrown"),jo("onStartGrow"),or("expanded",!1),Qt("dimension",Yt("property",{width:[Po("property","width"),Po("getDimension",function(n){return xs(n)+"px"})],height:[Po("property","height"),Po("getDimension",function(n){return Ei(n)+"px"})]}))],tg=zr({fields:eg,name:"sliding",active:ng,apis:Zm,state:Object.freeze({init:function(n){var e=ao(n.expanded());return _r({isExpanded:function(){return!0===e.get()},isCollapsed:function(){return!1===e.get()},setCollapsed:l(e.set,!1),setExpanded:l(e.set,!0),readState:function(){return"expanded: "+e.get()}})}})}),rg=function(e,t){var r=yl(of.sketch({dom:{tag:"div",classes:[ai.resolve("dropup")]},components:[],containerBehaviours:jr([jl.config({}),tg.config({closedClass:ai.resolve("dropup-closed"),openClass:ai.resolve("dropup-open"),shrinkingClass:ai.resolve("dropup-shrinking"),growingClass:ai.resolve("dropup-growing"),dimension:{property:"height"},onShrunk:function(n){e(),t(),jl.set(n,[])},onGrown:function(n){e(),t()}}),ii(function(n,e){o(I)})])})),o=function(n){p.window.requestAnimationFrame(function(){n(),tg.shrink(r)})};return{appear:function(n,e,t){!0===tg.hasShrunk(r)&&!1===tg.isTransitioning(r)&&p.window.requestAnimationFrame(function(){e(t),jl.set(r,[n()]),tg.grow(r)})},disappear:o,component:A(r),element:r.element}},og=zn.detect().browser.isFirefox(),ig=Rt([Zt("triggerEvent"),Zt("broadcastEvent"),or("stopBackspace",!0)]),ug=function(e,n){var t,r,o,i,u=Ut("Getting GUI events settings",ig,n),c=zn.detect().deviceType.isTouch()?["touchstart","touchmove","touchend","gesturestart"]:["mousedown","mouseup","mouseover","mousemove","mouseout","click"],a=Ed(u),s=yn(c.concat(["selectstart","input","contextmenu","change","transitionend","drag","dragstart","dragend","dragenter","dragleave","dragover","drop"]),function(n){return wd(e,n,function(e){a.fireIfReady(e,n).each(function(n){n&&e.kill()}),u.triggerEvent(n,e)&&e.kill()})}),f=wd(e,"keydown",function(n){var e;u.triggerEvent("keydown",n)?n.kill():!0!==u.stopBackspace||8!==(e=n).raw().which||hn(["input","textarea"],me(e.target()))||n.prevent()}),l=(t=e,r=function(n){u.triggerEvent("focusin",n)&&n.kill()},og?xd(t,"focus",r):wd(t,"focusin",r)),d=(o=e,i=function(n){u.triggerEvent("focusout",n)&&n.kill(),p.setTimeout(function(){u.triggerEvent($n(),n)},0)},og?xd(o,"blur",i):wd(o,"focusout",i)),m=Ve(e),g=wd(m,"scroll",function(n){u.broadcastEvent(Jn(),n)&&n.kill()});return{unbind:function(){bn(s,function(n){n.unbind()}),f.unbind(),l.unbind(),d.unbind(),g.unbind()}}},cg=function(n,e){var t=ht(n,"target").map(function(n){return n()}).getOr(e);return ao(t)},ag=Ze([{stopped:[]},{resume:["element"]},{complete:[]}]),sg=function(n,r,e,t,o,i){var u,c,a,s,f=n(r,t),l=(u=e,c=o,a=ao(!1),s=ao(!1),{stop:function(){a.set(!0)},cut:function(){s.set(!0)},isStopped:a.get,isCut:s.get,event:A(u),setSource:c.set,getSource:c.get});return f.fold(function(){return i.logEventNoHandlers(r,t),ag.complete()},function(e){var t=e.descHandler();return Jf(t)(l),l.isStopped()?(i.logEventStopped(r,e.element(),t.purpose()),ag.stopped()):l.isCut()?(i.logEventCut(r,e.element(),t.purpose()),ag.complete()):Ne(e.element()).fold(function(){return i.logNoParent(r,e.element(),t.purpose()),ag.complete()},function(n){return i.logEventResponse(r,e.element(),t.purpose()),ag.resume(n)})})},fg=function(e,t,r,n,o,i){return sg(e,t,r,n,o,i).fold(function(){return!0},function(n){return fg(e,t,r,n,o,i)},function(){return!1})},lg=function(n,e,t){var r,o,i=(r=e,o=ao(!1),{stop:function(){o.set(!0)},cut:I,isStopped:o.get,isCut:A(!1),event:A(r),setSource:c("Cannot set source of a broadcasted event"),getSource:c("Cannot get source of a broadcasted event")});return bn(n,function(n){var e=n.descHandler();Jf(e)(i)}),i.isStopped()},dg=function(n,e,t,r,o){var i=cg(t,r);return fg(n,e,t,r,i,o)},mg=function(n,e,t){return lo(n,function(n){return e(n).isSome()},t).bind(e)},gg=we("element","descHandler"),pg=function(n,e){return{id:A(n),descHandler:A(e)}};function vg(){var i={};return{registerId:function(r,o,n){B(n,function(n,e){var t=i[e]!==undefined?i[e]:{};t[o]=Xf(n,r),i[e]=t})},unregisterId:function(t){B(i,function(n,e){n.hasOwnProperty(t)&&delete n[t]})},filterByType:function(n){return ht(i,n).map(function(n){return _(n,function(n,e){return pg(e,n)})}).getOr([])},find:function(n,e,t){var o=pt(e)(i);return mg(t,function(n){return t=o,Ea(r=n).fold(function(){return F.none()},function(n){var e=pt(n);return t.bind(e).map(function(n){return gg(r,n)})});var t,r},n)}}}function hg(){var r=vg(),o={},i=function(r){var n=r.element();return Ea(n).fold(function(){return n="uid-",e=r.element(),t=Gc(ka+n),Wr(e,Ca,t),t;var n,e,t},function(n){return n})},u=function(n){Ea(n.element()).each(function(n){o[n]=undefined,r.unregisterId(n)})};return{find:function(n,e,t){return r.find(n,e,t)},filter:function(n){return r.filterByType(n)},register:function(n){var e=i(n);xt(o,e)&&function(n,e){var t=o[e];if(t!==n)throw new Error('The tagId "'+e+'" is already used by: '+ko(t.element())+"\nCannot use it for: "+ko(n.element())+"\nThe conflicting element is"+(he(t.element())?" ":" not ")+"already in the DOM");u(n)}(n,e);var t=[n];r.registerId(t,e,n.events()),o[e]=n},unregister:u,getById:function(n){return pt(n)(o)}}}var yg=function(t){var r=function(e){return Ne(t.element()).fold(function(){return!0},function(n){return Re(e,n)})},o=hg(),s=function(n,e){return o.find(r,n,e)},n=ug(t.element(),{triggerEvent:function(u,c){return Fo(u,c.target(),function(n){return e=s,t=u,o=n,i=(r=c).target(),dg(e,t,r,i,o);var e,t,r,o,i})},broadcastEvent:function(n,e){var t=o.filter(n);return lg(t,e)}}),i=Wf({debugInfo:A("real"),triggerEvent:function(e,t,r){Fo(e,t,function(n){dg(s,e,r,t,n)})},triggerFocus:function(c,a){Ea(c).fold(function(){mo(c)},function(n){Fo(Pn(),c,function(n){var e,t,r,o,i,u;e=s,t=Pn(),r={originator:A(a),kill:I,prevent:I,target:A(c)},i=n,u=cg(r,o=c),sg(e,t,r,o,u,i)})})},triggerEscape:function(n,e){i.triggerEvent("keydown",n.element(),e.event())},getByUid:function(n){return m(n)},getByDom:function(n){return g(n)},build:yl,addToGui:function(n){c(n)},removeFromGui:function(n){a(n)},addToWorld:function(n){e(n)},removeFromWorld:function(n){u(n)},broadcast:function(n){l(n)},broadcastOn:function(n,e){d(n,e)},isConnected:A(!0)}),e=function(n){n.connect(i),ve(n.element())||(o.register(n),bn(n.components(),e),i.triggerEvent(Xn(),n.element(),{target:A(n.element())}))},u=function(n){ve(n.element())||(bn(n.components(),u),o.unregister(n)),n.disconnect()},c=function(n){Ge(t,n)},a=function(n){Ke(n)},f=function(t){var n=o.filter(Wn());bn(n,function(n){var e=n.descHandler();Jf(e)(t)})},l=function(n){f({universal:A(!0),data:A(n)})},d=function(n,e){f({universal:A(!1),channels:A(n),data:A(e)})},m=function(n){return o.getById(n).fold(function(){return Qe.error(new Error('Could not find component with uid: "'+n+'" in system.'))},Qe.value)},g=function(n){var e=Ea(n).getOr("not found");return m(e)};return e(t),{root:A(t),element:t.element,destroy:function(){n.unbind(),$e(t.element())},add:c,remove:a,getByUid:m,getByDom:g,addToWorld:e,removeFromWorld:u,broadcast:l,broadcastOn:d}},bg=A(ai.resolve("readonly-mode")),wg=A(ai.resolve("edit-mode"));function xg(n){var e=yl(of.sketch({dom:{classes:[ai.resolve("outer-container")].concat(n.classes)},containerBehaviours:jr([co.config({alpha:bg(),omega:wg()})])}));return yg(e)}var Tg,Sg,Og,kg,Cg=function(n,e){var t=ue.fromTag("input");hi(t,{opacity:"0",position:"absolute",top:"-1000px",left:"-1000px"}),ze(n,t),mo(t),e(t),$e(t)},Eg=function(n){var e=n.getSelection();if(0<e.rangeCount){var t=e.getRangeAt(0),r=n.document.createRange();r.setStart(t.startContainer,t.startOffset),r.setEnd(t.endContainer,t.endOffset),e.removeAllRanges(),e.addRange(r)}},Dg=function(n,e){po().each(function(n){Re(n,e)||go(n)}),n.focus(),mo(ue.fromDom(n.document.body)),Eg(n)},Ig={stubborn:function(n,e,t,r){var o=function(){Dg(e,r)},i=wd(t,"keydown",function(n){hn(["input","textarea"],me(n.target()))||o()});return{toReading:function(){Cg(n,go)},toEditing:o,onToolbarTouch:function(){},destroy:function(){i.unbind()}}},timid:function(n,e,t,r){var o=function(){go(r)};return{toReading:function(){o()},toEditing:function(){Dg(e,r)},onToolbarTouch:function(){o()},destroy:I}}},Ag=function(t,r,o,i,n){var u=function(){r.run(function(n){n.refreshSelection()})},e=function(n,e){var t=n-i.dom().scrollTop;r.run(function(n){n.scrollIntoView(t,t+e)})},c=function(){r.run(function(n){n.clearSelection()})},a=function(){t.getCursorBox().each(function(n){e(n.top(),n.height())}),r.run(function(n){n.syncHeight()})},s=Dd(t),f=Om(a,300),l=[t.onKeyup(function(){c(),f.throttle()}),t.onNodeChanged(u),t.onDomChanged(f.throttle),t.onDomChanged(u),t.onScrollToCursor(function(n){n.preventDefault(),f.throttle()}),t.onScrollToElement(function(n){n.element(),e(r,i)}),t.onToEditing(function(){r.run(function(n){n.toEditing()})}),t.onToReading(function(){r.run(function(n){n.toReading()})}),wd(t.doc(),"touchend",function(n){Re(t.html(),n.target())||Re(t.body(),n.target())}),wd(o,"transitionend",function(n){var e;"height"===n.raw().propertyName&&(e=Ei(o),r.run(function(n){n.setViewportOffset(e)}),u(),a())}),xd(o,"touchstart",function(n){var e;r.run(function(n){n.highlightSelection()}),e=n,r.run(function(n){n.onToolbarTouch(e)}),t.onTouchToolstrip()}),wd(t.body(),"touchstart",function(n){c(),t.onTouchContent(),s.fireTouchstart(n)}),s.onTouchmove(),s.onTouchend(),wd(t.body(),"click",function(n){n.kill()}),wd(o,"touchmove",function(){t.onToolbarScrollStart()})];return{destroy:function(){bn(l,function(n){n.unbind()})}}},Mg={},Fg={exports:Mg};Tg=undefined,Sg=Mg,Og=Fg,kg=undefined,function(n){"object"==typeof Sg&&void 0!==Og?Og.exports=n():"function"==typeof Tg&&Tg.amd?Tg([],n):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).EphoxContactWrapper=n()}(function(){return function i(u,c,a){function s(e,n){if(!c[e]){if(!u[e]){var t="function"==typeof kg&&kg;if(!n&&t)return t(e,!0);if(f)return f(e,!0);var r=new Error("Cannot find module '"+e+"'");throw r.code="MODULE_NOT_FOUND",r}var o=c[e]={exports:{}};u[e][0].call(o.exports,function(n){return s(u[e][1][n]||n)},o,o.exports,i,u,c,a)}return c[e].exports}for(var f="function"==typeof kg&&kg,n=0;n<a.length;n++)s(a[n]);return s}({1:[function(n,e,t){var r,o,i=e.exports={};function u(){throw new Error("setTimeout has not been defined")}function c(){throw new Error("clearTimeout has not been defined")}function a(n){if(r===setTimeout)return setTimeout(n,0);if((r===u||!r)&&setTimeout)return r=setTimeout,setTimeout(n,0);try{return r(n,0)}catch(e){try{return r.call(null,n,0)}catch(e){return r.call(this,n,0)}}}!function(){try{r="function"==typeof setTimeout?setTimeout:u}catch(n){r=u}try{o="function"==typeof clearTimeout?clearTimeout:c}catch(n){o=c}}();var s,f=[],l=!1,d=-1;function m(){l&&s&&(l=!1,s.length?f=s.concat(f):d=-1,f.length&&g())}function g(){if(!l){var n=a(m);l=!0;for(var e=f.length;e;){for(s=f,f=[];++d<e;)s&&s[d].run();d=-1,e=f.length}s=null,l=!1,function(n){if(o===clearTimeout)return clearTimeout(n);if((o===c||!o)&&clearTimeout)return o=clearTimeout,clearTimeout(n);try{o(n)}catch(e){try{return o.call(null,n)}catch(e){return o.call(this,n)}}}(n)}}function p(n,e){this.fun=n,this.array=e}function v(){}i.nextTick=function(n){var e=new Array(arguments.length-1);if(1<arguments.length)for(var t=1;t<arguments.length;t++)e[t-1]=arguments[t];f.push(new p(n,e)),1!==f.length||l||a(g)},p.prototype.run=function(){this.fun.apply(null,this.array)},i.title="browser",i.browser=!0,i.env={},i.argv=[],i.version="",i.versions={},i.on=v,i.addListener=v,i.once=v,i.off=v,i.removeListener=v,i.removeAllListeners=v,i.emit=v,i.prependListener=v,i.prependOnceListener=v,i.listeners=function(n){return[]},i.binding=function(n){throw new Error("process.binding is not supported")},i.cwd=function(){return"/"},i.chdir=function(n){throw new Error("process.chdir is not supported")},i.umask=function(){return 0}},{}],2:[function(n,l,e){(function(t){!function(n){var e=setTimeout;function r(){}function u(n){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof n)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],f(n,this)}function o(r,o){for(;3===r._state;)r=r._value;0!==r._state?(r._handled=!0,u._immediateFn(function(){var n=1===r._state?o.onFulfilled:o.onRejected;if(null!==n){var e;try{e=n(r._value)}catch(t){return void c(o.promise,t)}i(o.promise,e)}else(1===r._state?i:c)(o.promise,r._value)})):r._deferreds.push(o)}function i(n,e){try{if(e===n)throw new TypeError("A promise cannot be resolved with itself.");if(e&&("object"==typeof e||"function"==typeof e)){var t=e.then;if(e instanceof u)return n._state=3,n._value=e,void a(n);if("function"==typeof t)return void f((r=t,o=e,function(){r.apply(o,arguments)}),n)}n._state=1,n._value=e,a(n)}catch(i){c(n,i)}var r,o}function c(n,e){n._state=2,n._value=e,a(n)}function a(n){2===n._state&&0===n._deferreds.length&&u._immediateFn(function(){n._handled||u._unhandledRejectionFn(n._value)});for(var e=0,t=n._deferreds.length;e<t;e++)o(n,n._deferreds[e]);n._deferreds=null}function s(n,e,t){this.onFulfilled="function"==typeof n?n:null,this.onRejected="function"==typeof e?e:null,this.promise=t}function f(n,e){var t=!1;try{n(function(n){t||(t=!0,i(e,n))},function(n){t||(t=!0,c(e,n))})}catch(r){if(t)return;t=!0,c(e,r)}}u.prototype["catch"]=function(n){return this.then(null,n)},u.prototype.then=function(n,e){var t=new this.constructor(r);return o(this,new s(n,e,t)),t},u.all=function(n){var a=Array.prototype.slice.call(n);return new u(function(o,i){if(0===a.length)return o([]);var u=a.length;function c(e,n){try{if(n&&("object"==typeof n||"function"==typeof n)){var t=n.then;if("function"==typeof t)return void t.call(n,function(n){c(e,n)},i)}a[e]=n,0==--u&&o(a)}catch(r){i(r)}}for(var n=0;n<a.length;n++)c(n,a[n])})},u.resolve=function(e){return e&&"object"==typeof e&&e.constructor===u?e:new u(function(n){n(e)})},u.reject=function(t){return new u(function(n,e){e(t)})},u.race=function(o){return new u(function(n,e){for(var t=0,r=o.length;t<r;t++)o[t].then(n,e)})},u._immediateFn="function"==typeof t?function(n){t(n)}:function(n){e(n,0)},u._unhandledRejectionFn=function(n){"undefined"!=typeof console&&console&&console.warn("Possible Unhandled Promise Rejection:",n)},u._setImmediateFn=function(n){u._immediateFn=n},u._setUnhandledRejectionFn=function(n){u._unhandledRejectionFn=n},void 0!==l&&l.exports?l.exports=u:n.Promise||(n.Promise=u)}(this)}).call(this,n("timers").setImmediate)},{timers:3}],3:[function(a,n,s){(function(n,e){var r=a("process/browser.js").nextTick,t=Function.prototype.apply,o=Array.prototype.slice,i={},u=0;function c(n,e){this._id=n,this._clearFn=e}s.setTimeout=function(){return new c(t.call(setTimeout,window,arguments),clearTimeout)},s.setInterval=function(){return new c(t.call(setInterval,window,arguments),clearInterval)},s.clearTimeout=s.clearInterval=function(n){n.close()},c.prototype.unref=c.prototype.ref=function(){},c.prototype.close=function(){this._clearFn.call(window,this._id)},s.enroll=function(n,e){clearTimeout(n._idleTimeoutId),n._idleTimeout=e},s.unenroll=function(n){clearTimeout(n._idleTimeoutId),n._idleTimeout=-1},s._unrefActive=s.active=function(n){clearTimeout(n._idleTimeoutId);var e=n._idleTimeout;0<=e&&(n._idleTimeoutId=setTimeout(function(){n._onTimeout&&n._onTimeout()},e))},s.setImmediate="function"==typeof n?n:function(n){var e=u++,t=!(arguments.length<2)&&o.call(arguments,1);return i[e]=!0,r(function(){i[e]&&(t?n.apply(null,t):n.call(null),s.clearImmediate(e))}),e},s.clearImmediate="function"==typeof e?e:function(n){delete i[n]}}).call(this,a("timers").setImmediate,a("timers").clearImmediate)},{"process/browser.js":1,timers:3}],4:[function(n,e,t){var r=n("promise-polyfill"),o="undefined"!=typeof window?window:Function("return this;")();e.exports={boltExport:o.Promise||r}},{"promise-polyfill":2}]},{},[4])(4)});var Rg=Fg.exports.boltExport,Bg=function(n){var t=F.none(),e=[],r=function(n){o()?u(n):e.push(n)},o=function(){return t.isSome()},i=function(n){bn(n,u)},u=function(e){t.each(function(n){p.setTimeout(function(){e(n)},0)})};return n(function(n){t=F.some(n),i(e),e=[]}),{get:r,map:function(t){return Bg(function(e){r(function(n){e(t(n))})})},isReady:o}},Vg={nu:Bg,pure:function(e){return Bg(function(n){n(e)})}},Ng=function(n){p.setTimeout(function(){throw n},0)},_g=function(t){var n=function(n){t().then(n,Ng)};return{map:function(n){return _g(function(){return t().then(n)})},bind:function(e){return _g(function(){return t().then(function(n){return e(n).toPromise()})})},anonBind:function(n){return _g(function(){return t().then(function(){return n.toPromise()})})},toLazy:function(){return Vg.nu(n)},toCached:function(){var n=null;return _g(function(){return null===n&&(n=t()),n})},toPromise:t,get:n}},jg=function(n){return _g(function(){return new Rg(n)})},Hg=function(n){return _g(function(){return Rg.resolve(n)})},zg=function(n,e,t){return Math.abs(n-e)<=t?F.none():n<e?F.some(n+t):F.some(n-t)},Lg=function(){var s=null;return{animate:function(r,o,n,i,e,t){var u=!1,c=function(n){u=!0,e(n)};clearInterval(s);var a=function(n){clearInterval(s),c(n)};s=setInterval(function(){var t=r();zg(t,o,n).fold(function(){clearInterval(s),c(o)},function(n){if(i(n,a),!u){var e=r();(e!==n||Math.abs(e-o)>Math.abs(t-o))&&(clearInterval(s),c(o))}})},t)}}},Pg=function(r,o){return function(n,e){for(var t=0;t<n.length;t++){var r=e(n[t],t);if(r.isSome())return r}return F.none()}([{width:320,height:480,keyboard:{portrait:300,landscape:240}},{width:320,height:568,keyboard:{portrait:300,landscape:240}},{width:375,height:667,keyboard:{portrait:305,landscape:240}},{width:414,height:736,keyboard:{portrait:320,landscape:240}},{width:768,height:1024,keyboard:{portrait:320,landscape:400}},{width:1024,height:1366,keyboard:{portrait:380,landscape:460}}],function(n){return e=r<=n.width&&o<=n.height,t=n.keyboard,e?F.some(t):F.none();var e,t}).getOr({portrait:o/5,landscape:r/4})},$g=function(n){var e,t=Sd(n).isPortrait(),r=Pg((e=n).screen.width,e.screen.height),o=t?r.portrait:r.landscape;return(t?n.screen.height:n.screen.width)-n.innerHeight>o?0:o},Wg=function(n,e){var t=Be(n).dom().defaultView;return Ei(n)+Ei(e)-$g(t)},Ug=Wg,Gg=function(n,e,t){var r=Wg(e,t),o=Ei(e)+Ei(t)-r;vi(n,"padding-bottom",o+"px")},qg=Ze([{fixed:["element","property","offsetY"]},{scroller:["element","offsetY"]}]),Yg="data-"+ai.resolve("position-y-fixed"),Kg="data-"+ai.resolve("y-property"),Xg="data-"+ai.resolve("scrolling"),Jg="data-"+ai.resolve("last-window-height"),Qg=function(n){return Md(n,Yg)},Zg=function(n,e){var t=Gr(n,Kg);return qg.fixed(n,t,e)},np=function(n,e){return qg.scroller(n,e)},ep=function(n){var e=Qg(n);return("true"===Gr(n,Xg)?np:Zg)(n,e)},tp=function(n,e,t){var r=Be(n).dom().defaultView.innerHeight;return Wr(n,Jg,r+"px"),r-e-t},rp=function(n){var e=Ai(n,"["+Yg+"]");return yn(e,ep)},op=function(r,o,i,u){var n,e,t,c,a,s,f,l,d=Be(r).dom().defaultView,m=(l=Gr(f=i,"style"),hi(f,{position:"absolute",top:"0px"}),Wr(f,Yg,"0px"),Wr(f,Kg,"top"),{restore:function(){Wr(f,"style",l||""),Yr(f,Yg),Yr(f,Kg)}}),g=Ei(i),p=Ei(u),v=(c=tp(r,t=g,p),s=Gr(a=r,"style"),nd.register(a),hi(a,{position:"absolute",height:c+"px",width:"100%",top:t+"px"}),Wr(a,Yg,t+"px"),Wr(a,Xg,"true"),Wr(a,Kg,"top"),{restore:function(){nd.deregister(a),Wr(a,"style",s||""),Yr(a,Yg),Yr(a,Xg),Yr(a,Kg)}}),h=(e=Gr(n=u,"style"),hi(n,{position:"absolute",bottom:"0px"}),Wr(n,Yg,"0px"),Wr(n,Kg,"bottom"),{restore:function(){Wr(n,"style",e||""),Yr(n,Yg),Yr(n,Kg)}}),y=!0,b=function(){var n=d.innerHeight;return Md(r,Jg)<n},w=function(){if(y){var n=Ei(i),e=Ei(u),t=tp(r,n,e);Wr(r,Yg,n+"px"),vi(r,"height",t+"px"),Gg(o,r,u)}};return Gg(o,r,u),{setViewportOffset:function(n){Wr(r,Yg,n+"px"),w()},isExpanding:b,isShrinking:x(b),refresh:w,restore:function(){y=!1,m.restore(),v.restore(),h.restore()}}},ip=Qg,up=Lg(),cp="data-"+ai.resolve("last-scroll-top"),ap=function(n){var e=wi(n,"top").getOr("0");return parseInt(e,10)},sp=function(n){return parseInt(n.dom().scrollTop,10)},fp=function(n,e){var t=e+ip(n)+"px";vi(n,"top",t)},lp=function(t,r,o){return jg(function(n){var e=l(sp,t);up.animate(e,r,15,function(n){t.dom().scrollTop=n,vi(t,"top",ap(t)+15+"px")},function(){t.dom().scrollTop=r,vi(t,"top",o+"px"),n(r)},10)})},dp=function(o,i){return jg(function(n){var e=l(sp,o);Wr(o,cp,e());var t=Math.abs(i-e()),r=Math.ceil(t/10);up.animate(e,i,r,function(n,e){Md(o,cp)!==o.dom().scrollTop?e(o.dom().scrollTop):(o.dom().scrollTop=n,Wr(o,cp,n))},function(){o.dom().scrollTop=i,Wr(o,cp,i),n(i)},10)})},mp=function(i,u){return jg(function(n){var e=l(ap,i),t=function(n){vi(i,"top",n+"px")},r=Math.abs(u-e()),o=Math.ceil(r/10);up.animate(e,u,o,t,function(){t(u),n(u)},10)})},gp=function(e,t,r){var o=Be(e).dom().defaultView;return jg(function(n){fp(e,r),fp(t,r),o.scrollTo(0,r),n(r)})},pp=function(n,e,t,r,o){var i=Ug(e,t),u=l(Eg,n);i<r||i<o?dp(e,e.dom().scrollTop-i+o).get(u):r<0&&dp(e,e.dom().scrollTop+r).get(u)},vp=function(u,n){return n(function(r){var o=[],i=0;0===u.length?r([]):bn(u,function(n,e){var t;n.get((t=e,function(n){o[t]=n,++i>=u.length&&r(o)}))})})},hp=function(n,a){return n.fold(function(n,e,t){return vi(n,e,a+(r=t)+"px"),Hg(r);var r},function(n,e){return o=a+(r=e),i=wi(t=n,"top").getOr(r),u=o-parseInt(i,10),c=t.dom().scrollTop+u,lp(t,c,o);var t,r,o,i,u,c})},yp=function(n,e){var t=rp(n),r=yn(t,function(n){return hp(n,e)});return vp(r,jg)},bp=function(e,t,n,r,o,i){var u,c,a=(u=function(n){return gp(e,t,n)},c=ao(Vg.pure({})),{start:function(e){var n=Vg.nu(function(n){return u(e).get(n)});c.set(n)},idle:function(n){c.get().get(function(){n()})}}),s=Om(function(){a.idle(function(){yp(n,r.pageYOffset).get(function(){var n;(n=nm(i),F.from(n[0]).bind(function(n){var e=n.top()-t.dom().scrollTop;return e>r.innerHeight+5||e<-5?F.some({top:A(e),bottom:A(e+n.height())}):F.none()})).each(function(n){t.dom().scrollTop=t.dom().scrollTop+n.top()}),a.start(0),o.refresh()})})},1e3),f=wd(ue.fromDom(r),"scroll",function(){r.pageYOffset<0||s.throttle()});return yp(n,r.pageYOffset).get(h),{unbind:f.unbind}},wp=function(n){var t=n.cWin(),e=n.ceBody(),r=n.socket(),o=n.toolstrip(),i=n.toolbar(),u=n.contentElement(),c=n.keyboardType(),a=n.outerWindow(),s=n.dropup(),f=op(r,e,o,s),l=c(n.outerBody(),t,ye(),u,o,i),d=Od(a,{onChange:I,onReady:f.refresh});d.onAdjustment(function(){f.refresh()});var m=wd(ue.fromDom(a),"resize",function(){f.isExpanding()&&f.refresh()}),g=bp(o,r,n.outerBody(),a,f,t),p=function(t,e){var n=t.document,r=ue.fromTag("div");no(r,ai.resolve("unfocused-selections")),ze(ue.fromDom(n.documentElement),r);var o=wd(r,"touchstart",function(n){n.prevent(),Dg(t,e),u()}),i=function(n){var e=ue.fromTag("span");return il(e,[ai.resolve("layer-editor"),ai.resolve("unfocused-selection")]),hi(e,{left:n.left()+"px",top:n.top()+"px",width:n.width()+"px",height:n.height()+"px"}),e},u=function(){Pe(r)};return{update:function(){u();var n=nm(t),e=yn(n,i);Le(r,e)},isActive:function(){return 0<_e(r).length},destroy:function(){o.unbind(),$e(r)},clear:u}}(t,u),v=function(){p.clear()};return{toEditing:function(){l.toEditing(),v()},toReading:function(){l.toReading()},onToolbarTouch:function(n){l.onToolbarTouch(n)},refreshSelection:function(){p.isActive()&&p.update()},clearSelection:v,highlightSelection:function(){p.update()},scrollIntoView:function(n,e){pp(t,r,s,n,e)},updateToolbarPadding:I,setViewportOffset:function(n){f.setViewportOffset(n),mp(r,n).get(h)},syncHeight:function(){vi(u,"height",u.dom().contentWindow.document.body.scrollHeight+"px")},refreshStructure:f.refresh,destroy:function(){f.restore(),d.destroy(),g.unbind(),m.unbind(),l.destroy(),p.destroy(),Cg(ye(),go)}}},xp=function(r,n){var o=Tm(),i=Bf(),u=Bf(),c=Rf(),a=Rf();return{enter:function(){n.hide();var t=ue.fromDom(p.document);pm.getActiveApi(r.editor).each(function(n){i.set({socketHeight:wi(r.socket,"height"),iframeHeight:wi(n.frame(),"height"),outerScroll:p.document.body.scrollTop}),u.set({exclusives:Nm.exclusive(t,"."+nd.scrollable())}),no(r.container,ai.resolve("fullscreen-maximized")),wm(r.container,n.body()),o.maximize(),vi(r.socket,"overflow","scroll"),vi(r.socket,"-webkit-overflow-scrolling","touch"),mo(n.body());var e=Ce(["cWin","ceBody","socket","toolstrip","toolbar","dropup","contentElement","cursor","keyboardType","isScrolling","outerWindow","outerBody"],[]);c.set(wp(e({cWin:n.win(),ceBody:n.body(),socket:r.socket,toolstrip:r.toolstrip,toolbar:r.toolbar,dropup:r.dropup.element(),contentElement:n.frame(),cursor:I,outerBody:r.body,outerWindow:r.win,keyboardType:Ig.stubborn,isScrolling:function(){return u.get().exists(function(n){return n.socket.isScrolling()})}}))),c.run(function(n){n.syncHeight()}),a.set(Ag(n,c,r.toolstrip,r.socket,r.dropup))})},refreshStructure:function(){c.run(function(n){n.refreshStructure()})},exit:function(){o.restore(),a.clear(),c.clear(),n.show(),i.on(function(n){n.socketHeight.each(function(n){vi(r.socket,"height",n)}),n.iframeHeight.each(function(n){vi(r.editor.getFrame(),"height",n)}),p.document.body.scrollTop=n.scrollTop}),i.clear(),u.on(function(n){n.exclusives.unbind()}),u.clear(),eo(r.container,ai.resolve("fullscreen-maximized")),xm(),nd.deregister(r.toolbar),xi(r.socket,"overflow"),xi(r.socket,"-webkit-overflow-scrolling"),go(r.editor.getFrame()),pm.getActiveApi(r.editor).each(function(n){n.clearSelection()})}}},Tp=function(n){var e=Ut("Getting IosWebapp schema",Cm,n);vi(e.toolstrip,"width","100%"),vi(e.container,"position","relative");var t=yl(km(function(){e.setReadOnly(e.readOnlyOnInit()),r.enter()},e.translate));e.alloy.add(t);var r=xp(e,{show:function(){e.alloy.add(t)},hide:function(){e.alloy.remove(t)}});return{setReadOnly:e.setReadOnly,refreshStructure:r.refreshStructure,enter:r.enter,exit:r.exit,destroy:I}},Sp=tinymce.util.Tools.resolve("tinymce.EditorManager"),Op=function(n){var e=ht(n.settings,"skin_url").fold(function(){return Sp.baseURL+"/skins/lightgray"},function(n){return n});return{content:e+"/content.mobile.min.css",ui:e+"/skin.mobile.min.css"}},kp=function(n,e,t){n.system().broadcastOn([wo.formatChanged()],{command:e,state:t})},Cp=function(r,n){var e=R(n.formatter.get());bn(e,function(e){n.formatter.formatChanged(e,function(n){kp(r,e,n)})}),bn(["ul","ol"],function(t){n.selection.selectorChanged(t,function(n,e){kp(r,t,n)})})},Ep=(A(["x-small","small","medium","large","x-large"]),function(n){var e=function(){n._skinLoaded=!0,n.fire("SkinLoaded")};return function(){n.initialized?e():n.on("init",e)}}),Dp=A("toReading"),Ip=A("toEditing");yo.add("mobile",function(D){return{getNotificationManagerImpl:function(){return{open:A({progressBar:{value:I},close:I}),close:I,reposition:I,getArgs:h}},renderUI:function(n){var e=Op(D);0==(!1===D.settings.skin)?(D.contentCSS.push(e.content),ho.DOM.styleSheetLoader.load(e.ui,Ep(D))):Ep(D)();var t,r,o,i,u,c,a,s,f,l,d,m,g,p,v,h,y,b=function(){D.fire("scrollIntoView")},w=ue.fromTag("div"),x=zn.detect().os.isAndroid()?(s=b,f=xg({classes:[ai.resolve("android-container")]}),l=_m(),d=Rf(),m=zm(d),g=Lm(),p=rg(I,s),f.add(l.wrapper()),f.add(g),f.add(p.component()),{system:A(f),element:f.element,init:function(n){d.set(Em(n))},exit:function(){d.run(function(n){n.exit(),jl.remove(g,m)})},setToolbarGroups:function(n){var e=l.createGroups(n);l.setGroups(e)},setContextToolbar:function(n){var e=l.createGroups(n);l.setContextToolbar(e)},focusToolbar:function(){l.focus()},restoreToolbar:function(){l.restoreToolbar()},updateMode:function(n){Pm(g,m,n,f.root())},socket:A(g),dropup:A(p)}):(t=b,r=xg({classes:[ai.resolve("ios-container")]}),o=_m(),i=Rf(),u=zm(i),c=Lm(),a=rg(function(){i.run(function(n){n.refreshStructure()})},t),r.add(o.wrapper()),r.add(c),r.add(a.component()),{system:A(r),element:r.element,init:function(n){i.set(Tp(n))},exit:function(){i.run(function(n){jl.remove(c,u),n.exit()})},setToolbarGroups:function(n){var e=o.createGroups(n);o.setGroups(e)},setContextToolbar:function(n){var e=o.createGroups(n);o.setContextToolbar(e)},focusToolbar:function(){o.focus()},restoreToolbar:function(){o.restoreToolbar()},updateMode:function(n){Pm(c,u,n,r.root())},socket:A(c),dropup:A(a)}),T=ue.fromDom(n.targetNode);we("element","offset"),h=w,(y=v=T,F.from(y.dom().nextSibling).map(ue.fromDom)).fold(function(){Ne(v).each(function(n){ze(n,h)})},function(n){var e,t;t=h,Ne(e=n).each(function(n){n.dom().insertBefore(t.dom(),e.dom())})}),function(n,e){ze(n,e.element());var t=_e(e.element());bn(t,function(n){e.getByDom(n).each(Ue)})}(w,x.system());var S=n.targetNode.ownerDocument.defaultView,O=Od(S,{onChange:function(){x.system().broadcastOn([wo.orientationChanged()],{width:kd(S)})},onReady:I}),k=function(n,e,t,r){!1===r&&D.selection.collapse();var o=C(n,e,t);x.setToolbarGroups(!0===r?o.readOnly:o.main),D.setMode(!0===r?"readonly":"design"),D.fire(!0===r?Dp():Ip()),x.updateMode(r)},C=function(n,e,t){var r=n.get();return{readOnly:r.backToMask.concat(e.get()),main:r.backToMask.concat(t.get())}},E=function(n,e){return D.on(n,e),{unbind:function(){D.off(n)}}};return D.on("init",function(){x.init({editor:{getFrame:function(){return ue.fromDom(D.contentAreaContainer.querySelector("iframe"))},onDomChanged:function(){return{unbind:I}},onToReading:function(n){return E(Dp(),n)},onToEditing:function(n){return E(Ip(),n)},onScrollToCursor:function(e){return D.on("scrollIntoView",function(n){e(n)}),{unbind:function(){D.off("scrollIntoView"),O.destroy()}}},onTouchToolstrip:function(){t()},onTouchContent:function(){var n,e=ue.fromDom(D.editorContainer.querySelector("."+ai.resolve("toolbar")));(n=e,vo(n).bind(function(n){return x.system().getByDom(n).toOption()})).each(te),x.restoreToolbar(),t()},onTapContent:function(n){var e=n.target();"img"===me(e)?(D.selection.select(e.dom()),n.kill()):"a"===me(e)&&x.system().getByDom(ue.fromDom(D.editorContainer)).each(function(n){co.isAlpha(n)&&bo(e.dom())})}},container:ue.fromDom(D.editorContainer),socket:ue.fromDom(D.contentAreaContainer),toolstrip:ue.fromDom(D.editorContainer.querySelector("."+ai.resolve("toolstrip"))),toolbar:ue.fromDom(D.editorContainer.querySelector("."+ai.resolve("toolbar"))),dropup:x.dropup(),alloy:x.system(),translate:I,setReadOnly:function(n){k(a,c,u,n)},readOnlyOnInit:function(){return!1}});var t=function(){x.dropup().disappear(function(){x.system().broadcastOn([wo.dropupDismissed()],{})})},n={label:"The first group",scrollable:!1,items:[$a.forToolbar("back",function(){D.selection.collapse(),x.exit()},{})]},e={label:"Back to read only",scrollable:!1,items:[$a.forToolbar("readonly-back",function(){k(a,c,u,!0)},{})]},r=gd(x,D),o=pd(D.settings,r),i={label:"The extra group",scrollable:!1,items:[]},u=ao([{label:"the action group",scrollable:!0,items:o},i]),c=ao([{label:"The read only mode group",scrollable:!0,items:[]},i]),a=ao({backToMask:[n],backToReadOnly:[e]});Cp(x,D)}),D.on("remove",function(){x.exit()}),D.on("detach",function(){var e,n;e=x.system(),n=_e(e.element()),bn(n,function(n){e.getByDom(n).each(We)}),$e(e.element()),x.system().destroy(),$e(w)}),{iframeContainer:x.socket().element().dom(),editorContainer:x.element().dom()}}}})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/themes/modern/theme.min.js b/lib/web/tiny_mce_4/themes/modern/theme.min.js index 7fcbe50b5fa15..0f406aa671c82 100644 --- a/lib/web/tiny_mce_4/themes/modern/theme.min.js +++ b/lib/web/tiny_mce_4/themes/modern/theme.min.js @@ -1 +1 @@ -!function(_){"use strict";var e,t,n,i,r,o=tinymce.util.Tools.resolve("tinymce.ThemeManager"),h=tinymce.util.Tools.resolve("tinymce.EditorManager"),w=tinymce.util.Tools.resolve("tinymce.util.Tools"),d=function(e){return!1!==c(e)},c=function(e){return e.getParam("menubar")},f=function(e){return e.getParam("toolbar_items_size")},m=function(e){return e.getParam("menu")},g=function(e){return!1===e.settings.skin},p=function(e){var t=e.getParam("resize","vertical");return!1===t?"none":"both"===t?"both":"vertical"},v=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),b=tinymce.util.Tools.resolve("tinymce.ui.Factory"),y=tinymce.util.Tools.resolve("tinymce.util.I18n"),s=function(e){return e.fire("SkinLoaded")},x=function(e){return e.fire("ResizeEditor")},R=function(e){return e.fire("BeforeRenderUI")},a=function(t,n){return function(){var e=t.find(n)[0];e&&e.focus(!0)}},C=function(e,t){e.shortcuts.add("Alt+F9","",a(t,"menubar")),e.shortcuts.add("Alt+F10,F10","",a(t,"toolbar")),e.shortcuts.add("Alt+F11","",a(t,"elementpath")),t.on("cancel",function(){e.focus()})},E=tinymce.util.Tools.resolve("tinymce.geom.Rect"),u=tinymce.util.Tools.resolve("tinymce.util.Delay"),k=function(){},H=function(e){return function(){return e}},l=H(!1),S=H(!0),T=l,M=S,N=function(){return P},P=(i={fold:function(e,t){return e()},is:T,isSome:T,isNone:M,getOr:n=function(e){return e},getOrThunk:t=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:n,orThunk:t,map:N,ap:N,each:function(){},bind:N,flatten:N,exists:T,forall:M,filter:N,equals:e=function(e){return e.isNone()},equals_:e,toArray:function(){return[]},toString:H("none()")},Object.freeze&&Object.freeze(i),i),W=function(n){var e=function(){return n},t=function(){return r},i=function(e){return e(n)},r={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:M,isNone:T,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return W(e(n))},ap:function(e){return e.fold(N,function(e){return W(e(n))})},each:function(e){e(n)},bind:i,flatten:e,exists:i,forall:i,filter:function(e){return e(n)?r:P},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(T,function(e){return t(n,e)})},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return r},D={some:W,none:N,from:function(e){return null===e||e===undefined?P:W(e)}},O=function(e){return e?e.getRoot().uiContainer:null},A={getUiContainerDelta:function(e){var t=O(e);if(t&&"static"!==v.DOM.getStyle(t,"position",!0)){var n=v.DOM.getPos(t),i=t.scrollLeft-n.x,r=t.scrollTop-n.y;return D.some({x:i,y:r})}return D.none()},setUiContainer:function(e,t){var n=v.DOM.select(e.settings.ui_container)[0];t.getRoot().uiContainer=n},getUiContainer:O,inheritUiContainer:function(e,t){return t.uiContainer=O(e)}},B=function(i,e,r){var o,s=[];if(e)return w.each(e.split(/[ ,]/),function(t){var e,n=function(){var e=i.selection;t.settings.stateSelector&&e.selectorChanged(t.settings.stateSelector,function(e){t.active(e)},!0),t.settings.disabledStateSelector&&e.selectorChanged(t.settings.disabledStateSelector,function(e){t.disabled(e)})};"|"===t?o=null:(o||(o={type:"buttongroup",items:[]},s.push(o)),i.buttons[t]&&(e=t,"function"==typeof(t=i.buttons[e])&&(t=t()),t.type=t.type||"button",t.size=r,t=b.create(t),o.items.push(t),i.initialized?n():i.on("init",n)))}),{type:"toolbar",layout:"flow",items:s}},L=B,z=function(n,i){var e,t,r=[];if(w.each(!1===(t=(e=n).getParam("toolbar"))?[]:w.isArray(t)?w.grep(t,function(e){return 0<e.length}):function(e,t){for(var n=[],i=1;i<10;i++){var r=e["toolbar"+i];if(!r)break;n.push(r)}var o=e.toolbar?[e.toolbar]:[t];return 0<n.length?n:o}(e.settings,"undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image"),function(e){var t;(t=e)&&r.push(B(n,t,i))}),r.length)return{type:"panel",layout:"stack",classes:"toolbar-grp",ariaRoot:!0,ariaRemember:!0,items:r}},I=v.DOM,F=function(e){return{left:e.x,top:e.y,width:e.w,height:e.h,right:e.x+e.w,bottom:e.y+e.h}},U=function(e,t){e.moveTo(t.left,t.top)},V=function(e,t,n,i,r,o){return o=F({x:t,y:n,w:o.w,h:o.h}),e&&(o=e({elementRect:F(i),contentAreaRect:F(r),panelRect:o})),o},Y=function(x){var i,o=function(){return x.contextToolbars||[]},n=function(e,t){var n,i,r,o,s,a,l,u=x.getParam("inline_toolbar_position_handler");if(!x.removed){if(!e||!e.toolbar.panel)return c=x,void w.each(c.contextToolbars,function(e){e.panel&&e.panel.hide()});var c,d,f,h,m;l=["bc-tc","tc-bc","tl-bl","bl-tl","tr-br","br-tr"],s=e.toolbar.panel,t&&s.show(),d=e.element,f=I.getPos(x.getContentAreaContainer()),h=x.dom.getRect(d),"BODY"===(m=x.dom.getRoot()).nodeName&&(h.x-=m.ownerDocument.documentElement.scrollLeft||m.scrollLeft,h.y-=m.ownerDocument.documentElement.scrollTop||m.scrollTop),h.x+=f.x,h.y+=f.y,r=h,i=I.getRect(s.getEl()),o=I.getRect(x.getContentAreaContainer()||x.getBody());var g,p,v,b=A.getUiContainerDelta(s).getOr({x:0,y:0});if(r.x+=b.x,r.y+=b.y,i.x+=b.x,i.y+=b.y,o.x+=b.x,o.y+=b.y,"inline"!==I.getStyle(e.element,"display",!0)){var y=e.element.getBoundingClientRect();r.w=y.width,r.h=y.height}x.inline||(o.w=x.getDoc().documentElement.offsetWidth),x.selection.controlSelection.isResizable(e.element)&&r.w<25&&(r=E.inflate(r,0,8)),n=E.findBestRelativePosition(i,r,o,l),r=E.clamp(r,o),n?(a=E.relativePosition(i,r,n),U(s,V(u,a.x,a.y,r,o,i))):(o.h+=i.h,(r=E.intersect(o,r))?(n=E.findBestRelativePosition(i,r,o,["bc-tc","bl-tl","br-tr"]))?(a=E.relativePosition(i,r,n),U(s,V(u,a.x,a.y,r,o,i))):U(s,V(u,r.x,r.y,r,o,i)):s.hide()),g=s,v=function(e,t){return e===t},p=(p=n)?p.substr(0,2):"",w.each({t:"down",b:"up"},function(e,t){g.classes.toggle("arrow-"+e,v(t,p.substr(0,1)))}),w.each({l:"left",r:"right"},function(e,t){g.classes.toggle("arrow-"+e,v(t,p.substr(1,1)))})}},r=function(e){return function(){u.requestAnimationFrame(function(){x.selection&&n(a(x.selection.getNode()),e)})}},t=function(e){var t;if(e.toolbar.panel)return e.toolbar.panel.show(),void n(e);t=b.create({type:"floatpanel",role:"dialog",classes:"tinymce tinymce-inline arrow",ariaLabel:"Inline toolbar",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:!0,border:1,items:L(x,e.toolbar.items),oncancel:function(){x.focus()}}),A.setUiContainer(x,t),function(e){if(!i){var t=r(!0),n=A.getUiContainer(e);i=x.selection.getScrollContainer()||x.getWin(),I.bind(i,"scroll",t),I.bind(n,"scroll",t),x.on("remove",function(){I.unbind(i,"scroll",t),I.unbind(n,"scroll",t)})}}(t),(e.toolbar.panel=t).renderTo().reflow(),n(e)},s=function(){w.each(o(),function(e){e.panel&&e.panel.hide()})},a=function(e){var t,n,i,r=o();for(t=(i=x.$(e).parents().add(e)).length-1;0<=t;t--)for(n=r.length-1;0<=n;n--)if(r[n].predicate(i[t]))return{toolbar:r[n],element:i[t]};return null};x.on("click keyup setContent ObjectResized",function(e){("setcontent"!==e.type||e.selection)&&u.setEditorTimeout(x,function(){var e;(e=a(x.selection.getNode()))?(s(),t(e)):s()})}),x.on("blur hide contextmenu",s),x.on("ObjectResizeStart",function(){var e=a(x.selection.getNode());e&&e.toolbar.panel&&e.toolbar.panel.hide()}),x.on("ResizeEditor ResizeWindow",r(!0)),x.on("nodeChange",r(!1)),x.on("remove",function(){w.each(o(),function(e){e.panel&&e.panel.remove()}),x.contextToolbars={}}),x.shortcuts.add("ctrl+F9","",function(){var e=a(x.selection.getNode());e&&e.toolbar.panel&&e.toolbar.panel.items()[0].focus()})},$=function(t){return function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===t}},q=$("function"),X=$("number"),j=(Array.prototype.slice,(r=Array.prototype.indexOf)===undefined?function(e,t){return ee(e,t)}:function(e,t){return r.call(e,t)}),J=function(e,t){return Q(e,t).isSome()},G=function(e,t){for(var n=e.length,i=new Array(n),r=0;r<n;r++){var o=e[r];i[r]=t(o,r,e)}return i},K=function(e,t){for(var n=0,i=e.length;n<i;n++)t(e[n],n,e)},Z=function(e,t){for(var n=[],i=0,r=e.length;i<r;i++){var o=e[i];t(o,i,e)&&n.push(o)}return n},Q=function(e,t){for(var n=0,i=e.length;n<i;n++)if(t(e[n],n,e))return D.some(n);return D.none()},ee=function(e,t){for(var n=0,i=e.length;n<i;++n)if(e[n]===t)return n;return-1},te=Array.prototype.push,ne=(q(Array.from)&&Array.from,{file:{title:"File",items:"newdocument restoredraft | preview | print"},edit:{title:"Edit",items:"undo redo | cut copy paste pastetext | selectall"},view:{title:"View",items:"code | visualaid visualchars visualblocks | spellchecker | preview fullscreen"},insert:{title:"Insert",items:"image link media template codesample inserttable | charmap hr | pagebreak nonbreaking anchor toc | insertdatetime"},format:{title:"Format",items:"bold italic underline strikethrough superscript subscript codeformat | blockformats align | removeformat"},tools:{title:"Tools",items:"spellchecker spellcheckerlanguage | a11ycheck code"},table:{title:"Table"},help:{title:"Help"}}),ie=function(e,t){return"|"===e?{name:"|",item:{text:"|"}}:t?{name:e,item:t}:null},re=function(e){return e&&"|"===e.item.text},oe=function(n,e,t,i){var r,o,s,a,l,u,c;return e?(o=e[i],a=!0):o=ne[i],o&&(r={text:o.title},s=[],w.each((o.items||"").split(/[ ,]/),function(e){var t=ie(e,n[e]);t&&s.push(t)}),a||w.each(n,function(e,t){var n;e.context!==i||(n=t,Q(s,function(e){return e.name===n}).isSome())||("before"===e.separator&&s.push({name:"|",item:{text:"|"}}),e.prependToContext?s.unshift(ie(t,e)):s.push(ie(t,e)),"after"===e.separator&&s.push({name:"|",item:{text:"|"}}))}),r.menu=G((l=t,u=Z(s,function(e){return!1===l.hasOwnProperty(e.name)}),c=Z(u,function(e,t,n){return!re(e)||!re(n[t-1])}),Z(c,function(e,t,n){return!re(e)||0<t&&t<n.length-1})),function(e){return e.item}),!r.menu.length)?null:r},se=function(e){for(var t,n=[],i=function(e){var t,n=[],i=m(e);if(i)for(t in i)n.push(t);else for(t in ne)n.push(t);return n}(e),r=w.makeMap((t=e,t.getParam("removed_menuitems","")).split(/[ ,]/)),o=c(e),s="string"==typeof o?o.split(/[ ,]/):i,a=0;a<s.length;a++){var l=s[a],u=oe(e.menuItems,m(e),r,l);u&&n.push(u)}return n},ae=v.DOM,le=function(e){return{width:e.clientWidth,height:e.clientHeight}},ue=function(e,t,n){var i,r,o,s;i=e.getContainer(),r=e.getContentAreaContainer().firstChild,o=le(i),s=le(r),null!==t&&(t=Math.max(e.getParam("min_width",100,"number"),t),t=Math.min(e.getParam("max_width",65535,"number"),t),ae.setStyle(i,"width",t+(o.width-s.width)),ae.setStyle(r,"width",t)),n=Math.max(e.getParam("min_height",100,"number"),n),n=Math.min(e.getParam("max_height",65535,"number"),n),ae.setStyle(r,"height",n),x(e)},ce=ue,de=function(e,t,n){var i=e.getContentAreaContainer();ue(e,i.clientWidth+t,i.clientHeight+n)},fe=tinymce.util.Tools.resolve("tinymce.Env"),he=function(e,t,n){var i,r=e.settings[n];r&&r((i=t.getEl("body"),{element:function(){return i}}))},me=function(c,d,f){return function(e){var t,n,i,r,o,s=e.control,a=s.parents().filter("panel")[0],l=a.find("#"+d)[0],u=(t=f,n=d,w.grep(t,function(e){return e.name===n})[0]);i=d,r=a,o=f,w.each(o,function(e){var t=r.items().filter("#"+e.name)[0];t&&t.visible()&&e.name!==i&&(he(e,t,"onhide"),t.visible(!1))}),s.parent().items().each(function(e){e.active(!1)}),l&&l.visible()?(he(u,l,"onhide"),l.hide(),s.active(!1)):(l?l.show():(l=b.create({type:"container",name:d,layout:"stack",classes:"sidebar-panel",html:""}),a.prepend(l),he(u,l,"onrender")),he(u,l,"onshow"),s.active(!0)),x(c)}},ge=function(e){return!(fe.ie&&!(11<=fe.ie)||!e.sidebars)&&0<e.sidebars.length},pe=function(n){return{type:"panel",name:"sidebar",layout:"stack",classes:"sidebar",items:[{type:"toolbar",layout:"stack",classes:"sidebar-toolbar",items:w.map(n.sidebars,function(e){var t=e.settings;return{type:"button",icon:t.icon,image:t.image,tooltip:t.tooltip,onclick:me(n,e.name,n.sidebars)}})}]}},ve=function(e){var t=function(){e._skinLoaded=!0,s(e)};return function(){e.initialized?t():e.on("init",t)}},be=v.DOM,ye=function(e){return{type:"panel",name:"iframe",layout:"stack",classes:"edit-area",border:e,html:""}},xe=function(t,e,n){var i,r,o,s,a;if(!1===g(t)&&n.skinUiCss?be.styleSheetLoader.load(n.skinUiCss,ve(t)):ve(t)(),i=e.panel=b.create({type:"panel",role:"application",classes:"tinymce",style:"visibility: hidden",layout:"stack",border:1,items:[{type:"container",classes:"top-part",items:[!1===d(t)?null:{type:"menubar",border:"0 0 1 0",items:se(t)},z(t,f(t))]},ge(t)?(s=t,{type:"panel",layout:"stack",classes:"edit-aria-container",border:"1 0 0 0",items:[ye("0"),pe(s)]}):ye("1 0 0 0")]}),A.setUiContainer(t,i),"none"!==p(t)&&(r={type:"resizehandle",direction:p(t),onResizeStart:function(){var e=t.getContentAreaContainer().firstChild;o={width:e.clientWidth,height:e.clientHeight}},onResize:function(e){"both"===p(t)?ce(t,o.width+e.deltaX,o.height+e.deltaY):ce(t,null,o.height+e.deltaY)}}),t.getParam("statusbar",!0,"boolean")){var l=y.translate(["Powered by {0}",'<a href="https://www.tiny.cloud/?utm_campaign=editor_referral&utm_medium=poweredby&utm_source=tinymce" rel="noopener" target="_blank" role="presentation" tabindex="-1">Tiny</a>']),u=t.getParam("branding",!0,"boolean")?{type:"label",classes:"branding",html:" "+l}:null;i.add({type:"panel",name:"statusbar",classes:"statusbar",layout:"flow",border:"1 0 0 0",ariaRoot:!0,items:[{type:"elementpath",editor:t},r,u]})}return R(t),t.on("SwitchMode",(a=i,function(e){a.find("*").disabled("readonly"===e.mode)})),i.renderBefore(n.targetNode).reflow(),t.getParam("readonly",!1,"boolean")&&t.setMode("readonly"),n.width&&be.setStyle(i.getEl(),"width",n.width),t.on("remove",function(){i.remove(),i=null}),C(t,i),Y(t),{iframeContainer:i.find("#iframe")[0].getEl(),editorContainer:i.getEl()}},we=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),_e=0,Re={id:function(){return"mceu_"+_e++},create:function(e,t,n){var i=_.document.createElement(e);return v.DOM.setAttribs(i,t),"string"==typeof n?i.innerHTML=n:w.each(n,function(e){e.nodeType&&i.appendChild(e)}),i},createFragment:function(e){return v.DOM.createFragment(e)},getWindowSize:function(){return v.DOM.getViewPort()},getSize:function(e){var t,n;if(e.getBoundingClientRect){var i=e.getBoundingClientRect();t=Math.max(i.width||i.right-i.left,e.offsetWidth),n=Math.max(i.height||i.bottom-i.bottom,e.offsetHeight)}else t=e.offsetWidth,n=e.offsetHeight;return{width:t,height:n}},getPos:function(e,t){return v.DOM.getPos(e,t||Re.getContainer())},getContainer:function(){return fe.container?fe.container:_.document.body},getViewPort:function(e){return v.DOM.getViewPort(e)},get:function(e){return _.document.getElementById(e)},addClass:function(e,t){return v.DOM.addClass(e,t)},removeClass:function(e,t){return v.DOM.removeClass(e,t)},hasClass:function(e,t){return v.DOM.hasClass(e,t)},toggleClass:function(e,t,n){return v.DOM.toggleClass(e,t,n)},css:function(e,t,n){return v.DOM.setStyle(e,t,n)},getRuntimeStyle:function(e,t){return v.DOM.getStyle(e,t,!0)},on:function(e,t,n,i){return v.DOM.bind(e,t,n,i)},off:function(e,t,n){return v.DOM.unbind(e,t,n)},fire:function(e,t,n){return v.DOM.fire(e,t,n)},innerHtml:function(e,t){v.DOM.setHTML(e,t)}},Ce=function(e){return"static"===Re.getRuntimeStyle(e,"position")},Ee=function(e){return e.state.get("fixed")};function ke(e,t,n){var i,r,o,s,a,l,u,c,d,f;return d=He(),o=(r=Re.getPos(t,A.getUiContainer(e))).x,s=r.y,Ee(e)&&Ce(_.document.body)&&(o-=d.x,s-=d.y),i=e.getEl(),a=(f=Re.getSize(i)).width,l=f.height,u=(f=Re.getSize(t)).width,c=f.height,"b"===(n=(n||"").split(""))[0]&&(s+=c),"r"===n[1]&&(o+=u),"c"===n[0]&&(s+=Math.round(c/2)),"c"===n[1]&&(o+=Math.round(u/2)),"b"===n[3]&&(s-=l),"r"===n[4]&&(o-=a),"c"===n[3]&&(s-=Math.round(l/2)),"c"===n[4]&&(o-=Math.round(a/2)),{x:o,y:s,w:a,h:l}}var He=function(){var e=_.window;return{x:Math.max(e.pageXOffset,_.document.body.scrollLeft,_.document.documentElement.scrollLeft),y:Math.max(e.pageYOffset,_.document.body.scrollTop,_.document.documentElement.scrollTop),w:e.innerWidth||_.document.documentElement.clientWidth,h:e.innerHeight||_.document.documentElement.clientHeight}},Se=function(e){var t,n=A.getUiContainer(e);return n&&!Ee(e)?{x:0,y:0,w:(t=n).scrollWidth-1,h:t.scrollHeight-1}:He()},Te={testMoveRel:function(e,t){for(var n=Se(this),i=0;i<t.length;i++){var r=ke(this,e,t[i]);if(Ee(this)){if(0<r.x&&r.x+r.w<n.w&&0<r.y&&r.y+r.h<n.h)return t[i]}else if(r.x>n.x&&r.x+r.w<n.w+n.x&&r.y>n.y&&r.y+r.h<n.h+n.y)return t[i]}return t[0]},moveRel:function(e,t){"string"!=typeof t&&(t=this.testMoveRel(e,t));var n=ke(this,e,t);return this.moveTo(n.x,n.y)},moveBy:function(e,t){var n=this.layoutRect();return this.moveTo(n.x+e,n.y+t),this},moveTo:function(e,t){var n=this;function i(e,t,n){return e<0?0:t<e+n&&(e=t-n)<0?0:e}if(n.settings.constrainToViewport){var r=Se(this),o=n.layoutRect();e=i(e,r.w+r.x,o.w),t=i(t,r.h+r.y,o.h)}var s=A.getUiContainer(n);return s&&Ce(s)&&!Ee(n)&&(e-=s.scrollLeft,t-=s.scrollTop),s&&(e+=1,t+=1),n.state.get("rendered")?n.layoutRect({x:e,y:t}).repaint():(n.settings.x=e,n.settings.y=t),n.fire("move",{x:e,y:t}),n}},Me=tinymce.util.Tools.resolve("tinymce.util.Class"),Ne=tinymce.util.Tools.resolve("tinymce.util.EventDispatcher"),Pe=function(e){var t;if(e)return"number"==typeof e?{top:e=e||0,left:e,bottom:e,right:e}:(1===(t=(e=e.split(" ")).length)?e[1]=e[2]=e[3]=e[0]:2===t?(e[2]=e[0],e[3]=e[1]):3===t&&(e[3]=e[1]),{top:parseInt(e[0],10)||0,right:parseInt(e[1],10)||0,bottom:parseInt(e[2],10)||0,left:parseInt(e[3],10)||0})},We=function(i,e){function t(e){var t=parseFloat(function(e){var t=i.ownerDocument.defaultView;if(t){var n=t.getComputedStyle(i,null);return n?(e=e.replace(/[A-Z]/g,function(e){return"-"+e}),n.getPropertyValue(e)):null}return i.currentStyle[e]}(e));return isNaN(t)?0:t}return{top:t(e+"TopWidth"),right:t(e+"RightWidth"),bottom:t(e+"BottomWidth"),left:t(e+"LeftWidth")}};function De(){}function Oe(e){this.cls=[],this.cls._map={},this.onchange=e||De,this.prefix=""}w.extend(Oe.prototype,{add:function(e){return e&&!this.contains(e)&&(this.cls._map[e]=!0,this.cls.push(e),this._change()),this},remove:function(e){if(this.contains(e)){var t=void 0;for(t=0;t<this.cls.length&&this.cls[t]!==e;t++);this.cls.splice(t,1),delete this.cls._map[e],this._change()}return this},toggle:function(e,t){var n=this.contains(e);return n!==t&&(n?this.remove(e):this.add(e),this._change()),this},contains:function(e){return!!this.cls._map[e]},_change:function(){delete this.clsValue,this.onchange.call(this)}}),Oe.prototype.toString=function(){var e;if(this.clsValue)return this.clsValue;e="";for(var t=0;t<this.cls.length;t++)0<t&&(e+=" "),e+=this.prefix+this.cls[t];return e};var Ae,Be,Le,ze=/^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i,Ie=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,Fe=/^\s*|\s*$/g,Ue=Me.extend({init:function(e){var o=this.match;function s(e,t,n){var i;function r(e){e&&t.push(e)}return r(function(t){if(t)return t=t.toLowerCase(),function(e){return"*"===t||e.type===t}}((i=ze.exec(e.replace(Fe,"")))[1])),r(function(t){if(t)return function(e){return e._name===t}}(i[2])),r(function(n){if(n)return n=n.split("."),function(e){for(var t=n.length;t--;)if(!e.classes.contains(n[t]))return!1;return!0}}(i[3])),r(function(n,i,r){if(n)return function(e){var t=e[n]?e[n]():"";return i?"="===i?t===r:"*="===i?0<=t.indexOf(r):"~="===i?0<=(" "+t+" ").indexOf(" "+r+" "):"!="===i?t!==r:"^="===i?0===t.indexOf(r):"$="===i&&t.substr(t.length-r.length)===r:!!r}}(i[4],i[5],i[6])),r(function(i){var t;if(i)return(i=/(?:not\((.+)\))|(.+)/i.exec(i))[1]?(t=a(i[1],[]),function(e){return!o(e,t)}):(i=i[2],function(e,t,n){return"first"===i?0===t:"last"===i?t===n-1:"even"===i?t%2==0:"odd"===i?t%2==1:!!e[i]&&e[i]()})}(i[7])),t.pseudo=!!i[7],t.direct=n,t}function a(e,t){var n,i,r,o=[];do{if(Ie.exec(""),(i=Ie.exec(e))&&(e=i[3],o.push(i[1]),i[2])){n=i[3];break}}while(i);for(n&&a(n,t),e=[],r=0;r<o.length;r++)">"!==o[r]&&e.push(s(o[r],[],">"===o[r-1]));return t.push(e),t}this._selectors=a(e,[])},match:function(e,t){var n,i,r,o,s,a,l,u,c,d,f,h,m;for(n=0,i=(t=t||this._selectors).length;n<i;n++){for(m=e,h=0,r=(o=(s=t[n]).length)-1;0<=r;r--)for(u=s[r];m;){if(u.pseudo)for(c=d=(f=m.parent().items()).length;c--&&f[c]!==m;);for(a=0,l=u.length;a<l;a++)if(!u[a](m,c,d)){a=l+1;break}if(a===l){h++;break}if(r===o-1)break;m=m.parent()}if(h===o)return!0}return!1},find:function(e){var t,n,u=[],i=this._selectors;function c(e,t,n){var i,r,o,s,a,l=t[n];for(i=0,r=e.length;i<r;i++){for(a=e[i],o=0,s=l.length;o<s;o++)if(!l[o](a,i,r)){o=s+1;break}if(o===s)n===t.length-1?u.push(a):a.items&&c(a.items(),t,n+1);else if(l.direct)return;a.items&&c(a.items(),t,n)}}if(e.items){for(t=0,n=i.length;t<n;t++)c(e.items(),i[t],0);1<n&&(u=function(e){for(var t,n=[],i=e.length;i--;)(t=e[i]).__checked||(n.push(t),t.__checked=1);for(i=n.length;i--;)delete n[i].__checked;return n}(u))}return Ae||(Ae=Ue.Collection),new Ae(u)}}),Ve=Array.prototype.push,Ye=Array.prototype.slice;Le={length:0,init:function(e){e&&this.add(e)},add:function(e){return w.isArray(e)?Ve.apply(this,e):e instanceof Be?this.add(e.toArray()):Ve.call(this,e),this},set:function(e){var t,n=this,i=n.length;for(n.length=0,n.add(e),t=n.length;t<i;t++)delete n[t];return n},filter:function(t){var e,n,i,r,o=[];for("string"==typeof t?(t=new Ue(t),r=function(e){return t.match(e)}):r=t,e=0,n=this.length;e<n;e++)r(i=this[e])&&o.push(i);return new Be(o)},slice:function(){return new Be(Ye.apply(this,arguments))},eq:function(e){return-1===e?this.slice(e):this.slice(e,+e+1)},each:function(e){return w.each(this,e),this},toArray:function(){return w.toArray(this)},indexOf:function(e){for(var t=this.length;t--&&this[t]!==e;);return t},reverse:function(){return new Be(w.toArray(this).reverse())},hasClass:function(e){return!!this[0]&&this[0].classes.contains(e)},prop:function(t,n){var e;return n!==undefined?(this.each(function(e){e[t]&&e[t](n)}),this):(e=this[0])&&e[t]?e[t]():void 0},exec:function(t){var n=w.toArray(arguments).slice(1);return this.each(function(e){e[t]&&e[t].apply(e,n)}),this},remove:function(){for(var e=this.length;e--;)this[e].remove();return this},addClass:function(t){return this.each(function(e){e.classes.add(t)})},removeClass:function(t){return this.each(function(e){e.classes.remove(t)})}},w.each("fire on off show hide append prepend before after reflow".split(" "),function(n){Le[n]=function(){var t=w.toArray(arguments);return this.each(function(e){n in e&&e[n].apply(e,t)}),this}}),w.each("text name disabled active selected checked visible parent value data".split(" "),function(t){Le[t]=function(e){return this.prop(t,e)}}),Be=Me.extend(Le);var $e=Ue.Collection=Be,qe=function(e){this.create=e.create};qe.create=function(r,o){return new qe({create:function(t,n){var i,e=function(e){t.set(n,e.value)};return t.on("change:"+n,function(e){r.set(o,e.value)}),r.on("change:"+o,e),(i=t._bindings)||(i=t._bindings=[],t.on("destroy",function(){for(var e=i.length;e--;)i[e]()})),i.push(function(){r.off("change:"+o,e)}),r.get(o)}})};var Xe=tinymce.util.Tools.resolve("tinymce.util.Observable");function je(e){return 0<e.nodeType}var Je,Ge,Ke=Me.extend({Mixins:[Xe],init:function(e){var t,n;for(t in e=e||{})(n=e[t])instanceof qe&&(e[t]=n.create(this,t));this.data=e},set:function(t,n){var i,r,o=this.data[t];if(n instanceof qe&&(n=n.create(this,t)),"object"==typeof t){for(i in t)this.set(i,t[i]);return this}return function e(t,n){var i,r;if(t===n)return!0;if(null===t||null===n)return t===n;if("object"!=typeof t||"object"!=typeof n)return t===n;if(w.isArray(n)){if(t.length!==n.length)return!1;for(i=t.length;i--;)if(!e(t[i],n[i]))return!1}if(je(t)||je(n))return t===n;for(i in r={},n){if(!e(t[i],n[i]))return!1;r[i]=!0}for(i in t)if(!r[i]&&!e(t[i],n[i]))return!1;return!0}(o,n)||(this.data[t]=n,r={target:this,name:t,value:n,oldValue:o},this.fire("change:"+t,r),this.fire("change",r)),this},get:function(e){return this.data[e]},has:function(e){return e in this.data},bind:function(e){return qe.create(this,e)},destroy:function(){this.fire("destroy")}}),Ze={},Qe={add:function(e){var t=e.parent();if(t){if(!t._layout||t._layout.isNative())return;Ze[t._id]||(Ze[t._id]=t),Je||(Je=!0,u.requestAnimationFrame(function(){var e,t;for(e in Je=!1,Ze)(t=Ze[e]).state.get("rendered")&&t.reflow();Ze={}},_.document.body))}},remove:function(e){Ze[e._id]&&delete Ze[e._id]}},et="onmousewheel"in _.document,tt=!1,nt=0,it={Statics:{classPrefix:"mce-"},isRtl:function(){return Ge.rtl},classPrefix:"mce-",init:function(t){var e,n,i=this;function r(e){var t;for(e=e.split(" "),t=0;t<e.length;t++)i.classes.add(e[t])}i.settings=t=w.extend({},i.Defaults,t),i._id=t.id||"mceu_"+nt++,i._aria={role:t.role},i._elmCache={},i.$=we,i.state=new Ke({visible:!0,active:!1,disabled:!1,value:""}),i.data=new Ke(t.data),i.classes=new Oe(function(){i.state.get("rendered")&&(i.getEl().className=this.toString())}),i.classes.prefix=i.classPrefix,(e=t.classes)&&(i.Defaults&&(n=i.Defaults.classes)&&e!==n&&r(n),r(e)),w.each("title text name visible disabled active value".split(" "),function(e){e in t&&i[e](t[e])}),i.on("click",function(){if(i.disabled())return!1}),i.settings=t,i.borderBox=Pe(t.border),i.paddingBox=Pe(t.padding),i.marginBox=Pe(t.margin),t.hidden&&i.hide()},Properties:"parent,name",getContainerElm:function(){var e=A.getUiContainer(this);return e||Re.getContainer()},getParentCtrl:function(e){for(var t,n=this.getRoot().controlIdLookup;e&&n&&!(t=n[e.id]);)e=e.parentNode;return t},initLayoutRect:function(){var e,t,n,i,r,o,s,a,l,u,c=this,d=c.settings,f=c.getEl();e=c.borderBox=c.borderBox||We(f,"border"),c.paddingBox=c.paddingBox||We(f,"padding"),c.marginBox=c.marginBox||We(f,"margin"),u=Re.getSize(f),a=d.minWidth,l=d.minHeight,r=a||u.width,o=l||u.height,n=d.width,i=d.height,s=void 0!==(s=d.autoResize)?s:!n&&!i,n=n||r,i=i||o;var h=e.left+e.right,m=e.top+e.bottom,g=d.maxWidth||65535,p=d.maxHeight||65535;return c._layoutRect=t={x:d.x||0,y:d.y||0,w:n,h:i,deltaW:h,deltaH:m,contentW:n-h,contentH:i-m,innerW:n-h,innerH:i-m,startMinWidth:a||0,startMinHeight:l||0,minW:Math.min(r,g),minH:Math.min(o,p),maxW:g,maxH:p,autoResize:s,scrollW:0},c._lastLayoutRect={},t},layoutRect:function(e){var t,n,i,r,o,s=this,a=s._layoutRect;return a||(a=s.initLayoutRect()),e?(i=a.deltaW,r=a.deltaH,e.x!==undefined&&(a.x=e.x),e.y!==undefined&&(a.y=e.y),e.minW!==undefined&&(a.minW=e.minW),e.minH!==undefined&&(a.minH=e.minH),(n=e.w)!==undefined&&(n=(n=n<a.minW?a.minW:n)>a.maxW?a.maxW:n,a.w=n,a.innerW=n-i),(n=e.h)!==undefined&&(n=(n=n<a.minH?a.minH:n)>a.maxH?a.maxH:n,a.h=n,a.innerH=n-r),(n=e.innerW)!==undefined&&(n=(n=n<a.minW-i?a.minW-i:n)>a.maxW-i?a.maxW-i:n,a.innerW=n,a.w=n+i),(n=e.innerH)!==undefined&&(n=(n=n<a.minH-r?a.minH-r:n)>a.maxH-r?a.maxH-r:n,a.innerH=n,a.h=n+r),e.contentW!==undefined&&(a.contentW=e.contentW),e.contentH!==undefined&&(a.contentH=e.contentH),(t=s._lastLayoutRect).x===a.x&&t.y===a.y&&t.w===a.w&&t.h===a.h||((o=Ge.repaintControls)&&o.map&&!o.map[s._id]&&(o.push(s),o.map[s._id]=!0),t.x=a.x,t.y=a.y,t.w=a.w,t.h=a.h),s):a},repaint:function(){var e,t,n,i,r,o,s,a,l,u,c=this;l=_.document.createRange?function(e){return e}:Math.round,e=c.getEl().style,i=c._layoutRect,a=c._lastRepaintRect||{},o=(r=c.borderBox).left+r.right,s=r.top+r.bottom,i.x!==a.x&&(e.left=l(i.x)+"px",a.x=i.x),i.y!==a.y&&(e.top=l(i.y)+"px",a.y=i.y),i.w!==a.w&&(u=l(i.w-o),e.width=(0<=u?u:0)+"px",a.w=i.w),i.h!==a.h&&(u=l(i.h-s),e.height=(0<=u?u:0)+"px",a.h=i.h),c._hasBody&&i.innerW!==a.innerW&&(u=l(i.innerW),(n=c.getEl("body"))&&((t=n.style).width=(0<=u?u:0)+"px"),a.innerW=i.innerW),c._hasBody&&i.innerH!==a.innerH&&(u=l(i.innerH),(n=n||c.getEl("body"))&&((t=t||n.style).height=(0<=u?u:0)+"px"),a.innerH=i.innerH),c._lastRepaintRect=a,c.fire("repaint",{},!1)},updateLayoutRect:function(){var e=this;e.parent()._lastRect=null,Re.css(e.getEl(),{width:"",height:""}),e._layoutRect=e._lastRepaintRect=e._lastLayoutRect=null,e.initLayoutRect()},on:function(e,t){var n,i,r,o=this;return rt(o).on(e,"string"!=typeof(n=t)?n:function(e){return i||o.parentsAndSelf().each(function(e){var t=e.settings.callbacks;if(t&&(i=t[n]))return r=e,!1}),i?i.call(r,e):(e.action=n,void this.fire("execute",e))}),o},off:function(e,t){return rt(this).off(e,t),this},fire:function(e,t,n){if((t=t||{}).control||(t.control=this),t=rt(this).fire(e,t),!1!==n&&this.parent)for(var i=this.parent();i&&!t.isPropagationStopped();)i.fire(e,t,!1),i=i.parent();return t},hasEventListeners:function(e){return rt(this).has(e)},parents:function(e){var t,n=new $e;for(t=this.parent();t;t=t.parent())n.add(t);return e&&(n=n.filter(e)),n},parentsAndSelf:function(e){return new $e(this).add(this.parents(e))},next:function(){var e=this.parent().items();return e[e.indexOf(this)+1]},prev:function(){var e=this.parent().items();return e[e.indexOf(this)-1]},innerHtml:function(e){return this.$el.html(e),this},getEl:function(e){var t=e?this._id+"-"+e:this._id;return this._elmCache[t]||(this._elmCache[t]=we("#"+t)[0]),this._elmCache[t]},show:function(){return this.visible(!0)},hide:function(){return this.visible(!1)},focus:function(){try{this.getEl().focus()}catch(e){}return this},blur:function(){return this.getEl().blur(),this},aria:function(e,t){var n=this,i=n.getEl(n.ariaTarget);return void 0===t?n._aria[e]:(n._aria[e]=t,n.state.get("rendered")&&i.setAttribute("role"===e?e:"aria-"+e,t),n)},encode:function(e,t){return!1!==t&&(e=this.translate(e)),(e||"").replace(/[&<>"]/g,function(e){return"&#"+e.charCodeAt(0)+";"})},translate:function(e){return Ge.translate?Ge.translate(e):e},before:function(e){var t=this.parent();return t&&t.insert(e,t.items().indexOf(this),!0),this},after:function(e){var t=this.parent();return t&&t.insert(e,t.items().indexOf(this)),this},remove:function(){var t,e,n=this,i=n.getEl(),r=n.parent();if(n.items){var o=n.items().toArray();for(e=o.length;e--;)o[e].remove()}r&&r.items&&(t=[],r.items().each(function(e){e!==n&&t.push(e)}),r.items().set(t),r._lastRect=null),n._eventsRoot&&n._eventsRoot===n&&we(i).off();var s=n.getRoot().controlIdLookup;return s&&delete s[n._id],i&&i.parentNode&&i.parentNode.removeChild(i),n.state.set("rendered",!1),n.state.destroy(),n.fire("remove"),n},renderBefore:function(e){return we(e).before(this.renderHtml()),this.postRender(),this},renderTo:function(e){return we(e||this.getContainerElm()).append(this.renderHtml()),this.postRender(),this},preRender:function(){},render:function(){},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'"></div>'},postRender:function(){var e,t,n,i,r,o=this,s=o.settings;for(i in o.$el=we(o.getEl()),o.state.set("rendered",!0),s)0===i.indexOf("on")&&o.on(i.substr(2),s[i]);if(o._eventsRoot){for(n=o.parent();!r&&n;n=n.parent())r=n._eventsRoot;if(r)for(i in r._nativeEvents)o._nativeEvents[i]=!0}ot(o),s.style&&(e=o.getEl())&&(e.setAttribute("style",s.style),e.style.cssText=s.style),o.settings.border&&(t=o.borderBox,o.$el.css({"border-top-width":t.top,"border-right-width":t.right,"border-bottom-width":t.bottom,"border-left-width":t.left}));var a=o.getRoot();for(var l in a.controlIdLookup||(a.controlIdLookup={}),(a.controlIdLookup[o._id]=o)._aria)o.aria(l,o._aria[l]);!1===o.state.get("visible")&&(o.getEl().style.display="none"),o.bindStates(),o.state.on("change:visible",function(e){var t,n=e.value;o.state.get("rendered")&&(o.getEl().style.display=!1===n?"none":"",o.getEl().getBoundingClientRect()),(t=o.parent())&&(t._lastRect=null),o.fire(n?"show":"hide"),Qe.add(o)}),o.fire("postrender",{},!1)},bindStates:function(){},scrollIntoView:function(e){var t,n,i,r,o,s,a=this.getEl(),l=a.parentNode,u=function(e,t){var n,i,r=e;for(n=i=0;r&&r!==t&&r.nodeType;)n+=r.offsetLeft||0,i+=r.offsetTop||0,r=r.offsetParent;return{x:n,y:i}}(a,l);return t=u.x,n=u.y,i=a.offsetWidth,r=a.offsetHeight,o=l.clientWidth,s=l.clientHeight,"end"===e?(t-=o-i,n-=s-r):"center"===e&&(t-=o/2-i/2,n-=s/2-r/2),l.scrollLeft=t,l.scrollTop=n,this},getRoot:function(){for(var e,t=this,n=[];t;){if(t.rootControl){e=t.rootControl;break}n.push(t),t=(e=t).parent()}e||(e=this);for(var i=n.length;i--;)n[i].rootControl=e;return e},reflow:function(){Qe.remove(this);var e=this.parent();return e&&e._layout&&!e._layout.isNative()&&e.reflow(),this}};function rt(n){return n._eventDispatcher||(n._eventDispatcher=new Ne({scope:n,toggleEvent:function(e,t){t&&Ne.isNative(e)&&(n._nativeEvents||(n._nativeEvents={}),n._nativeEvents[e]=!0,n.state.get("rendered")&&ot(n))}})),n._eventDispatcher}function ot(a){var e,t,n,l,i,r;function o(e){var t=a.getParentCtrl(e.target);t&&t.fire(e.type,e)}function s(){var e=l._lastHoverCtrl;e&&(e.fire("mouseleave",{target:e.getEl()}),e.parents().each(function(e){e.fire("mouseleave",{target:e.getEl()})}),l._lastHoverCtrl=null)}function u(e){var t,n,i,r=a.getParentCtrl(e.target),o=l._lastHoverCtrl,s=0;if(r!==o){if((n=(l._lastHoverCtrl=r).parents().toArray().reverse()).push(r),o){for((i=o.parents().toArray().reverse()).push(o),s=0;s<i.length&&n[s]===i[s];s++);for(t=i.length-1;s<=t;t--)(o=i[t]).fire("mouseleave",{target:o.getEl()})}for(t=s;t<n.length;t++)(r=n[t]).fire("mouseenter",{target:r.getEl()})}}function c(e){e.preventDefault(),"mousewheel"===e.type?(e.deltaY=-.025*e.wheelDelta,e.wheelDeltaX&&(e.deltaX=-.025*e.wheelDeltaX)):(e.deltaX=0,e.deltaY=e.detail),e=a.fire("wheel",e)}if(i=a._nativeEvents){for((n=a.parents().toArray()).unshift(a),e=0,t=n.length;!l&&e<t;e++)l=n[e]._eventsRoot;for(l||(l=n[n.length-1]||a),a._eventsRoot=l,t=e,e=0;e<t;e++)n[e]._eventsRoot=l;var d=l._delegates;for(r in d||(d=l._delegates={}),i){if(!i)return!1;"wheel"!==r||tt?("mouseenter"===r||"mouseleave"===r?l._hasMouseEnter||(we(l.getEl()).on("mouseleave",s).on("mouseover",u),l._hasMouseEnter=1):d[r]||(we(l.getEl()).on(r,o),d[r]=!0),i[r]=!1):et?we(a.getEl()).on("mousewheel",c):we(a.getEl()).on("DOMMouseScroll",c)}}}w.each("text title visible disabled active value".split(" "),function(t){it[t]=function(e){return 0===arguments.length?this.state.get(t):(void 0!==e&&this.state.set(t,e),this)}});var st=Ge=Me.extend(it),at=function(e){return!!e.getAttribute("data-mce-tabstop")};function lt(e){var o,r,n=e.root;function i(e){return e&&1===e.nodeType}try{o=_.document.activeElement}catch(t){o=_.document.body}function s(e){return i(e=e||o)?e.getAttribute("role"):null}function a(e){for(var t,n=e||o;n=n.parentNode;)if(t=s(n))return t}function l(e){var t=o;if(i(t))return t.getAttribute("aria-"+e)}function u(e){var t=e.tagName.toUpperCase();return"INPUT"===t||"TEXTAREA"===t||"SELECT"===t}function c(t){var r=[];return function e(t){if(1===t.nodeType&&"none"!==t.style.display&&!t.disabled){var n;(u(n=t)&&!n.hidden||at(n)||/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(s(n)))&&r.push(t);for(var i=0;i<t.childNodes.length;i++)e(t.childNodes[i])}}(t||n.getEl()),r}function d(e){var t,n;(n=(e=e||r).parents().toArray()).unshift(e);for(var i=0;i<n.length&&!(t=n[i]).settings.ariaRoot;i++);return t}function f(e,t){return e<0?e=t.length-1:e>=t.length&&(e=0),t[e]&&t[e].focus(),e}function h(e,t){var n=-1,i=d();t=t||c(i.getEl());for(var r=0;r<t.length;r++)t[r]===o&&(n=r);n+=e,i.lastAriaIndex=f(n,t)}function m(){"tablist"===a()?h(-1,c(o.parentNode)):r.parent().submenu?b():h(-1)}function g(){var e=s(),t=a();"tablist"===t?h(1,c(o.parentNode)):"menuitem"===e&&"menu"===t&&l("haspopup")?y():h(1)}function p(){h(-1)}function v(){var e=s(),t=a();"menuitem"===e&&"menubar"===t?y():"button"===e&&l("haspopup")?y({key:"down"}):h(1)}function b(){r.fire("cancel")}function y(e){e=e||{},r.fire("click",{target:o,aria:e})}return r=n.getParentCtrl(o),n.on("keydown",function(e){function t(e,t){u(o)||at(o)||"slider"!==s(o)&&!1!==t(e)&&e.preventDefault()}if(!e.isDefaultPrevented())switch(e.keyCode){case 37:t(e,m);break;case 39:t(e,g);break;case 38:t(e,p);break;case 40:t(e,v);break;case 27:b();break;case 14:case 13:case 32:t(e,y);break;case 9:!function(e){if("tablist"===a()){var t=c(r.getEl("body"))[0];t&&t.focus()}else h(e.shiftKey?-1:1)}(e),e.preventDefault()}}),n.on("focusin",function(e){o=e.target,r=e.control}),{focusFirst:function(e){var t=d(e),n=c(t.getEl());t.settings.ariaRemember&&"lastAriaIndex"in t?f(t.lastAriaIndex,n):f(0,n)}}}var ut={},ct=st.extend({init:function(e){var t=this;t._super(e),(e=t.settings).fixed&&t.state.set("fixed",!0),t._items=new $e,t.isRtl()&&t.classes.add("rtl"),t.bodyClasses=new Oe(function(){t.state.get("rendered")&&(t.getEl("body").className=this.toString())}),t.bodyClasses.prefix=t.classPrefix,t.classes.add("container"),t.bodyClasses.add("container-body"),e.containerCls&&t.classes.add(e.containerCls),t._layout=b.create((e.layout||"")+"layout"),t.settings.items?t.add(t.settings.items):t.add(t.render()),t._hasBody=!0},items:function(){return this._items},find:function(e){return(e=ut[e]=ut[e]||new Ue(e)).find(this)},add:function(e){return this.items().add(this.create(e)).parent(this),this},focus:function(e){var t,n,i,r=this;if(!e||!(n=r.keyboardNav||r.parents().eq(-1)[0].keyboardNav))return i=r.find("*"),r.statusbar&&i.add(r.statusbar.items()),i.each(function(e){if(e.settings.autofocus)return t=null,!1;e.canFocus&&(t=t||e)}),t&&t.focus(),r;n.focusFirst(r)},replace:function(e,t){for(var n,i=this.items(),r=i.length;r--;)if(i[r]===e){i[r]=t;break}0<=r&&((n=t.getEl())&&n.parentNode.removeChild(n),(n=e.getEl())&&n.parentNode.removeChild(n)),t.parent(this)},create:function(e){var t,n=this,i=[];return w.isArray(e)||(e=[e]),w.each(e,function(e){e&&(e instanceof st||("string"==typeof e&&(e={type:e}),t=w.extend({},n.settings.defaults,e),e.type=t.type=t.type||e.type||n.settings.defaultType||(t.defaults?t.defaults.type:null),e=b.create(t)),i.push(e))}),i},renderNew:function(){var i=this;return i.items().each(function(e,t){var n;e.parent(i),e.state.get("rendered")||((n=i.getEl("body")).hasChildNodes()&&t<=n.childNodes.length-1?we(n.childNodes[t]).before(e.renderHtml()):we(n).append(e.renderHtml()),e.postRender(),Qe.add(e))}),i._layout.applyClasses(i.items().filter(":visible")),i._lastRect=null,i},append:function(e){return this.add(e).renderNew()},prepend:function(e){return this.items().set(this.create(e).concat(this.items().toArray())),this.renderNew()},insert:function(e,t,n){var i,r,o;return e=this.create(e),i=this.items(),!n&&t<i.length-1&&(t+=1),0<=t&&t<i.length&&(r=i.slice(0,t).toArray(),o=i.slice(t).toArray(),i.set(r.concat(e,o))),this.renderNew()},fromJSON:function(e){for(var t in e)this.find("#"+t).value(e[t]);return this},toJSON:function(){var i={};return this.find("*").each(function(e){var t=e.name(),n=e.value();t&&void 0!==n&&(i[t]=n)}),i},renderHtml:function(){var e=this,t=e._layout,n=this.settings.role;return e.preRender(),t.preRender(e),'<div id="'+e._id+'" class="'+e.classes+'"'+(n?' role="'+this.settings.role+'"':"")+'><div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+(e.settings.html||"")+t.renderHtml(e)+"</div></div>"},postRender:function(){var e,t=this;return t.items().exec("postRender"),t._super(),t._layout.postRender(t),t.state.set("rendered",!0),t.settings.style&&t.$el.css(t.settings.style),t.settings.border&&(e=t.borderBox,t.$el.css({"border-top-width":e.top,"border-right-width":e.right,"border-bottom-width":e.bottom,"border-left-width":e.left})),t.parent()||(t.keyboardNav=lt({root:t})),t},initLayoutRect:function(){var e=this._super();return this._layout.recalc(this),e},recalc:function(){var e=this,t=e._layoutRect,n=e._lastRect;if(!n||n.w!==t.w||n.h!==t.h)return e._layout.recalc(e),t=e.layoutRect(),e._lastRect={x:t.x,y:t.y,w:t.w,h:t.h},!0},reflow:function(){var e;if(Qe.remove(this),this.visible()){for(st.repaintControls=[],st.repaintControls.map={},this.recalc(),e=st.repaintControls.length;e--;)st.repaintControls[e].repaint();"flow"!==this.settings.layout&&"stack"!==this.settings.layout&&this.repaint(),st.repaintControls=[]}return this}});function dt(e){var t,n;if(e.changedTouches)for(t="screenX screenY pageX pageY clientX clientY".split(" "),n=0;n<t.length;n++)e[t[n]]=e.changedTouches[0][t[n]]}function ft(e,h){var m,g,t,p,v,b,y,x=h.document||_.document;h=h||{};var w=x.getElementById(h.handle||e);t=function(e){var t,n,i,r,o,s,a,l,u,c,d,f=(t=x,u=Math.max,n=t.documentElement,i=t.body,r=u(n.scrollWidth,i.scrollWidth),o=u(n.clientWidth,i.clientWidth),s=u(n.offsetWidth,i.offsetWidth),a=u(n.scrollHeight,i.scrollHeight),l=u(n.clientHeight,i.clientHeight),{width:r<s?o:r,height:a<u(n.offsetHeight,i.offsetHeight)?l:a});dt(e),e.preventDefault(),g=e.button,c=w,b=e.screenX,y=e.screenY,d=_.window.getComputedStyle?_.window.getComputedStyle(c,null).getPropertyValue("cursor"):c.runtimeStyle.cursor,m=we("<div></div>").css({position:"absolute",top:0,left:0,width:f.width,height:f.height,zIndex:2147483647,opacity:1e-4,cursor:d}).appendTo(x.body),we(x).on("mousemove touchmove",v).on("mouseup touchend",p),h.start(e)},v=function(e){if(dt(e),e.button!==g)return p(e);e.deltaX=e.screenX-b,e.deltaY=e.screenY-y,e.preventDefault(),h.drag(e)},p=function(e){dt(e),we(x).off("mousemove touchmove",v).off("mouseup touchend",p),m.remove(),h.stop&&h.stop(e)},this.destroy=function(){we(w).off()},we(w).on("mousedown touchstart",t)}var ht,mt,gt,pt,vt={init:function(){this.on("repaint",this.renderScroll)},renderScroll:function(){var p=this,v=2;function n(){var m,g,e;function t(e,t,n,i,r,o){var s,a,l,u,c,d,f,h;if(a=p.getEl("scroll"+e)){if(f=t.toLowerCase(),h=n.toLowerCase(),we(p.getEl("absend")).css(f,p.layoutRect()[i]-1),!r)return void we(a).css("display","none");we(a).css("display","block"),s=p.getEl("body"),l=p.getEl("scroll"+e+"t"),u=s["client"+n]-2*v,c=(u-=m&&g?a["client"+o]:0)/s["scroll"+n],(d={})[f]=s["offset"+t]+v,d[h]=u,we(a).css(d),(d={})[f]=s["scroll"+t]*c,d[h]=u*c,we(l).css(d)}}e=p.getEl("body"),m=e.scrollWidth>e.clientWidth,g=e.scrollHeight>e.clientHeight,t("h","Left","Width","contentW",m,"Height"),t("v","Top","Height","contentH",g,"Width")}p.settings.autoScroll&&(p._hasScroll||(p._hasScroll=!0,function(){function e(s,a,l,u,c){var d,e=p._id+"-scroll"+s,t=p.classPrefix;we(p.getEl()).append('<div id="'+e+'" class="'+t+"scrollbar "+t+"scrollbar-"+s+'"><div id="'+e+'t" class="'+t+'scrollbar-thumb"></div></div>'),p.draghelper=new ft(e+"t",{start:function(){d=p.getEl("body")["scroll"+a],we("#"+e).addClass(t+"active")},drag:function(e){var t,n,i,r,o=p.layoutRect();n=o.contentW>o.innerW,i=o.contentH>o.innerH,r=p.getEl("body")["client"+l]-2*v,t=(r-=n&&i?p.getEl("scroll"+s)["client"+c]:0)/p.getEl("body")["scroll"+l],p.getEl("body")["scroll"+a]=d+e["delta"+u]/t},stop:function(){we("#"+e).removeClass(t+"active")}})}p.classes.add("scroll"),e("v","Top","Height","Y","Width"),e("h","Left","Width","X","Height")}(),p.on("wheel",function(e){var t=p.getEl("body");t.scrollLeft+=10*(e.deltaX||0),t.scrollTop+=10*e.deltaY,n()}),we(p.getEl("body")).on("scroll",n)),n())}},bt=ct.extend({Defaults:{layout:"fit",containerCls:"panel"},Mixins:[vt],renderHtml:function(){var e=this,t=e._layout,n=e.settings.html;return e.preRender(),t.preRender(e),void 0===n?n='<div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+t.renderHtml(e)+"</div>":("function"==typeof n&&(n=n.call(e)),e._hasBody=!1),'<div id="'+e._id+'" class="'+e.classes+'" hidefocus="1" tabindex="-1" role="group">'+(e._preBodyHtml||"")+n+"</div>"}}),yt={resizeToContent:function(){this._layoutRect.autoResize=!0,this._lastRect=null,this.reflow()},resizeTo:function(e,t){if(e<=1||t<=1){var n=Re.getWindowSize();e=e<=1?e*n.w:e,t=t<=1?t*n.h:t}return this._layoutRect.autoResize=!1,this.layoutRect({minW:e,minH:t,w:e,h:t}).reflow()},resizeBy:function(e,t){var n=this.layoutRect();return this.resizeTo(n.w+e,n.h+t)}},xt=[],wt=[];function _t(e,t){for(;e;){if(e===t)return!0;e=e.parent()}}function Rt(){ht||(ht=function(e){2!==e.button&&function(e){for(var t=xt.length;t--;){var n=xt[t],i=n.getParentCtrl(e.target);if(n.settings.autohide){if(i&&(_t(i,n)||n.parent()===i))continue;(e=n.fire("autohide",{target:e.target})).isDefaultPrevented()||n.hide()}}}(e)},we(_.document).on("click touchstart",ht))}function Ct(r){var e=Re.getViewPort().y;function t(e,t){for(var n,i=0;i<xt.length;i++)if(xt[i]!==r)for(n=xt[i].parent();n&&(n=n.parent());)n===r&&xt[i].fixed(e).moveBy(0,t).repaint()}r.settings.autofix&&(r.state.get("fixed")?r._autoFixY>e&&(r.fixed(!1).layoutRect({y:r._autoFixY}).repaint(),t(!1,r._autoFixY-e)):(r._autoFixY=r.layoutRect().y,r._autoFixY<e&&(r.fixed(!0).layoutRect({y:0}).repaint(),t(!0,e-r._autoFixY))))}function Et(e,t){var n,i,r=kt.zIndex||65535;if(e)wt.push(t);else for(n=wt.length;n--;)wt[n]===t&&wt.splice(n,1);if(wt.length)for(n=0;n<wt.length;n++)wt[n].modal&&(r++,i=wt[n]),wt[n].getEl().style.zIndex=r,wt[n].zIndex=r,r++;var o=we("#"+t.classPrefix+"modal-block",t.getContainerElm())[0];i?we(o).css("z-index",i.zIndex-1):o&&(o.parentNode.removeChild(o),pt=!1),kt.currentZIndex=r}var kt=bt.extend({Mixins:[Te,yt],init:function(e){var i=this;i._super(e),(i._eventsRoot=i).classes.add("floatpanel"),e.autohide&&(Rt(),function(){if(!gt){var e=_.document.documentElement,t=e.clientWidth,n=e.clientHeight;gt=function(){_.document.all&&t===e.clientWidth&&n===e.clientHeight||(t=e.clientWidth,n=e.clientHeight,kt.hideAll())},we(_.window).on("resize",gt)}}(),xt.push(i)),e.autofix&&(mt||(mt=function(){var e;for(e=xt.length;e--;)Ct(xt[e])},we(_.window).on("scroll",mt)),i.on("move",function(){Ct(this)})),i.on("postrender show",function(e){if(e.control===i){var t,n=i.classPrefix;i.modal&&!pt&&((t=we("#"+n+"modal-block",i.getContainerElm()))[0]||(t=we('<div id="'+n+'modal-block" class="'+n+"reset "+n+'fade"></div>').appendTo(i.getContainerElm())),u.setTimeout(function(){t.addClass(n+"in"),we(i.getEl()).addClass(n+"in")}),pt=!0),Et(!0,i)}}),i.on("show",function(){i.parents().each(function(e){if(e.state.get("fixed"))return i.fixed(!0),!1})}),e.popover&&(i._preBodyHtml='<div class="'+i.classPrefix+'arrow"></div>',i.classes.add("popover").add("bottom").add(i.isRtl()?"end":"start")),i.aria("label",e.ariaLabel),i.aria("labelledby",i._id),i.aria("describedby",i.describedBy||i._id+"-none")},fixed:function(e){var t=this;if(t.state.get("fixed")!==e){if(t.state.get("rendered")){var n=Re.getViewPort();e?t.layoutRect().y-=n.y:t.layoutRect().y+=n.y}t.classes.toggle("fixed",e),t.state.set("fixed",e)}return t},show:function(){var e,t=this._super();for(e=xt.length;e--&&xt[e]!==this;);return-1===e&&xt.push(this),t},hide:function(){return Ht(this),Et(!1,this),this._super()},hideAll:function(){kt.hideAll()},close:function(){return this.fire("close").isDefaultPrevented()||(this.remove(),Et(!1,this)),this},remove:function(){Ht(this),this._super()},postRender:function(){return this.settings.bodyRole&&this.getEl("body").setAttribute("role",this.settings.bodyRole),this._super()}});function Ht(e){var t;for(t=xt.length;t--;)xt[t]===e&&xt.splice(t,1);for(t=wt.length;t--;)wt[t]===e&&wt.splice(t,1)}kt.hideAll=function(){for(var e=xt.length;e--;){var t=xt[e];t&&t.settings.autohide&&(t.hide(),xt.splice(e,1))}};var St=function(s,n,e){var a,i,l=v.DOM,t=s.getParam("fixed_toolbar_container");t&&(i=l.select(t)[0]);var r=function(){if(a&&a.moveRel&&a.visible()&&!a._fixed){var e=s.selection.getScrollContainer(),t=s.getBody(),n=0,i=0;if(e){var r=l.getPos(t),o=l.getPos(e);n=Math.max(0,o.x-r.x),i=Math.max(0,o.y-r.y)}a.fixed(!1).moveRel(t,s.rtl?["tr-br","br-tr"]:["tl-bl","bl-tl","tr-br"]).moveBy(n,i)}},o=function(){a&&(a.show(),r(),l.addClass(s.getBody(),"mce-edit-focus"))},u=function(){a&&(a.hide(),kt.hideAll(),l.removeClass(s.getBody(),"mce-edit-focus"))},c=function(){var e,t;a?a.visible()||o():(a=n.panel=b.create({type:i?"panel":"floatpanel",role:"application",classes:"tinymce tinymce-inline",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:(e=i,t=s,!(!e||t.settings.ui_container)),border:1,items:[!1===d(s)?null:{type:"menubar",border:"0 0 1 0",items:se(s)},z(s,f(s))]}),A.setUiContainer(s,a),R(s),i?a.renderTo(i).reflow():a.renderTo().reflow(),C(s,a),o(),Y(s),s.on("nodeChange",r),s.on("ResizeWindow",r),s.on("activate",o),s.on("deactivate",u),s.nodeChanged())};return s.settings.content_editable=!0,s.on("focus",function(){!1===g(s)&&e.skinUiCss?l.styleSheetLoader.load(e.skinUiCss,c,c):c()}),s.on("blur hide",u),s.on("remove",function(){a&&(a.remove(),a=null)}),!1===g(s)&&e.skinUiCss?l.styleSheetLoader.load(e.skinUiCss,ve(s)):ve(s)(),{}};function Tt(i,r){var o,s,a=this,l=st.classPrefix;a.show=function(e,t){function n(){o&&(we(i).append('<div class="'+l+"throbber"+(r?" "+l+"throbber-inline":"")+'"></div>'),t&&t())}return a.hide(),o=!0,e?s=u.setTimeout(n,e):n(),a},a.hide=function(){var e=i.lastChild;return u.clearTimeout(s),e&&-1!==e.className.indexOf("throbber")&&e.parentNode.removeChild(e),o=!1,a}}var Mt=function(e,t){var n;e.on("ProgressState",function(e){n=n||new Tt(t.panel.getEl("body")),e.state?n.show(e.time):n.hide()})},Nt=function(e,t,n){var i=function(e){var t=e.settings,n=t.skin,i=t.skin_url;if(!1!==n){var r=n||"lightgray";i=i?e.documentBaseURI.toAbsolute(i):h.baseURL+"/skins/"+r}return i}(e);return i&&(n.skinUiCss=i+"/skin.min.css",e.contentCSS.push(i+"/content"+(e.inline?".inline":"")+".min.css")),Mt(e,t),e.getParam("inline",!1,"boolean")?St(e,t,n):xe(e,t,n)},Pt=st.extend({Mixins:[Te],Defaults:{classes:"widget tooltip tooltip-n"},renderHtml:function(){var e=this,t=e.classPrefix;return'<div id="'+e._id+'" class="'+e.classes+'" role="presentation"><div class="'+t+'tooltip-arrow"></div><div class="'+t+'tooltip-inner">'+e.encode(e.state.get("text"))+"</div></div>"},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.getEl().lastChild.innerHTML=t.encode(e.value)}),t._super()},repaint:function(){var e,t;e=this.getEl().style,t=this._layoutRect,e.left=t.x+"px",e.top=t.y+"px",e.zIndex=131070}}),Wt=st.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.canFocus=!0,i.tooltip&&!1!==Wt.tooltips&&(r.on("mouseenter",function(e){var t=r.tooltip().moveTo(-65535);if(e.control===r){var n=t.text(i.tooltip).show().testMoveRel(r.getEl(),["bc-tc","bc-tl","bc-tr"]);t.classes.toggle("tooltip-n","bc-tc"===n),t.classes.toggle("tooltip-nw","bc-tl"===n),t.classes.toggle("tooltip-ne","bc-tr"===n),t.moveRel(r.getEl(),n)}else t.hide()}),r.on("mouseleave mousedown click",function(){r.tooltip().remove(),r._tooltip=null})),r.aria("label",i.ariaLabel||i.tooltip)},tooltip:function(){return this._tooltip||(this._tooltip=new Pt({type:"tooltip"}),A.inheritUiContainer(this,this._tooltip),this._tooltip.renderTo()),this._tooltip},postRender:function(){var e=this,t=e.settings;e._super(),e.parent()||!t.width&&!t.height||(e.initLayoutRect(),e.repaint()),t.autofocus&&e.focus()},bindStates:function(){var t=this;function n(e){t.aria("disabled",e),t.classes.toggle("disabled",e)}function i(e){t.aria("pressed",e),t.classes.toggle("active",e)}return t.state.on("change:disabled",function(e){n(e.value)}),t.state.on("change:active",function(e){i(e.value)}),t.state.get("disabled")&&n(!0),t.state.get("active")&&i(!0),t._super()},remove:function(){this._super(),this._tooltip&&(this._tooltip.remove(),this._tooltip=null)}}),Dt=Wt.extend({Defaults:{value:0},init:function(e){this._super(e),this.classes.add("progress"),this.settings.filter||(this.settings.filter=function(e){return Math.round(e)})},renderHtml:function(){var e=this._id,t=this.classPrefix;return'<div id="'+e+'" class="'+this.classes+'"><div class="'+t+'bar-container"><div class="'+t+'bar"></div></div><div class="'+t+'text">0%</div></div>'},postRender:function(){return this._super(),this.value(this.settings.value),this},bindStates:function(){var t=this;function n(e){e=t.settings.filter(e),t.getEl().lastChild.innerHTML=e+"%",t.getEl().firstChild.firstChild.style.width=e+"%"}return t.state.on("change:value",function(e){n(e.value)}),n(t.state.get("value")),t._super()}}),Ot=function(e,t){e.getEl().lastChild.textContent=t+(e.progressBar?" "+e.progressBar.value()+"%":"")},At=st.extend({Mixins:[Te],Defaults:{classes:"widget notification"},init:function(e){var t=this;t._super(e),t.maxWidth=e.maxWidth,e.text&&t.text(e.text),e.icon&&(t.icon=e.icon),e.color&&(t.color=e.color),e.type&&t.classes.add("notification-"+e.type),e.timeout&&(e.timeout<0||0<e.timeout)&&!e.closeButton?t.closeButton=!1:(t.classes.add("has-close"),t.closeButton=!0),e.progressBar&&(t.progressBar=new Dt),t.on("click",function(e){-1!==e.target.className.indexOf(t.classPrefix+"close")&&t.close()})},renderHtml:function(){var e,t=this,n=t.classPrefix,i="",r="",o="";return t.icon&&(i='<i class="'+n+"ico "+n+"i-"+t.icon+'"></i>'),e=' style="max-width: '+t.maxWidth+"px;"+(t.color?"background-color: "+t.color+';"':'"'),t.closeButton&&(r='<button type="button" class="'+n+'close" aria-hidden="true">\xd7</button>'),t.progressBar&&(o=t.progressBar.renderHtml()),'<div id="'+t._id+'" class="'+t.classes+'"'+e+' role="presentation">'+i+'<div class="'+n+'notification-inner">'+t.state.get("text")+"</div>"+o+r+'<div style="clip: rect(1px, 1px, 1px, 1px);height: 1px;overflow: hidden;position: absolute;width: 1px;" aria-live="assertive" aria-relevant="additions" aria-atomic="true"></div></div>'},postRender:function(){var e=this;return u.setTimeout(function(){e.$el.addClass(e.classPrefix+"in"),Ot(e,e.state.get("text"))},100),e._super()},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.getEl().firstChild.innerHTML=e.value,Ot(t,e.value)}),t.progressBar&&(t.progressBar.bindStates(),t.progressBar.state.on("change:value",function(e){Ot(t,t.state.get("text"))})),t._super()},close:function(){return this.fire("close").isDefaultPrevented()||this.remove(),this},repaint:function(){var e,t;e=this.getEl().style,t=this._layoutRect,e.left=t.x+"px",e.top=t.y+"px",e.zIndex=65534}});function Bt(o){var s=function(e){return e.inline?e.getElement():e.getContentAreaContainer()};return{open:function(e,t){var n,i=w.extend(e,{maxWidth:(n=s(o),Re.getSize(n).width)}),r=new At(i);return 0<(r.args=i).timeout&&(r.timer=setTimeout(function(){r.close(),t()},i.timeout)),r.on("close",function(){t()}),r.renderTo(),r},close:function(e){e.close()},reposition:function(e){K(e,function(e){e.moveTo(0,0)}),function(n){if(0<n.length){var e=n.slice(0,1)[0],t=s(o);e.moveRel(t,"tc-tc"),K(n,function(e,t){0<t&&e.moveRel(n[t-1].getEl(),"bc-tc")})}}(e)},getArgs:function(e){return e.args}}}var Lt=[],zt="";function It(e){var t,n=we("meta[name=viewport]")[0];!1!==fe.overrideViewPort&&(n||((n=_.document.createElement("meta")).setAttribute("name","viewport"),_.document.getElementsByTagName("head")[0].appendChild(n)),(t=n.getAttribute("content"))&&void 0!==zt&&(zt=t),n.setAttribute("content",e?"width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0":zt))}function Ft(e,t){(function(){for(var e=0;e<Lt.length;e++)if(Lt[e]._fullscreen)return!0;return!1})()&&!1===t&&we([_.document.documentElement,_.document.body]).removeClass(e+"fullscreen")}var Ut=kt.extend({modal:!0,Defaults:{border:1,layout:"flex",containerCls:"panel",role:"dialog",callbacks:{submit:function(){this.fire("submit",{data:this.toJSON()})},close:function(){this.close()}}},init:function(e){var n=this;n._super(e),n.isRtl()&&n.classes.add("rtl"),n.classes.add("window"),n.bodyClasses.add("window-body"),n.state.set("fixed",!0),e.buttons&&(n.statusbar=new bt({layout:"flex",border:"1 0 0 0",spacing:3,padding:10,align:"center",pack:n.isRtl()?"start":"end",defaults:{type:"button"},items:e.buttons}),n.statusbar.classes.add("foot"),n.statusbar.parent(n)),n.on("click",function(e){var t=n.classPrefix+"close";(Re.hasClass(e.target,t)||Re.hasClass(e.target.parentNode,t))&&n.close()}),n.on("cancel",function(){n.close()}),n.on("move",function(e){e.control===n&&kt.hideAll()}),n.aria("describedby",n.describedBy||n._id+"-none"),n.aria("label",e.title),n._fullscreen=!1},recalc:function(){var e,t,n,i,r=this,o=r.statusbar;r._fullscreen&&(r.layoutRect(Re.getWindowSize()),r.layoutRect().contentH=r.layoutRect().innerH),r._super(),e=r.layoutRect(),r.settings.title&&!r._fullscreen&&(t=e.headerW)>e.w&&(n=e.x-Math.max(0,t/2),r.layoutRect({w:t,x:n}),i=!0),o&&(o.layoutRect({w:r.layoutRect().innerW}).recalc(),(t=o.layoutRect().minW+e.deltaW)>e.w&&(n=e.x-Math.max(0,t-e.w),r.layoutRect({w:t,x:n}),i=!0)),i&&r.recalc()},initLayoutRect:function(){var e,t=this,n=t._super(),i=0;if(t.settings.title&&!t._fullscreen){e=t.getEl("head");var r=Re.getSize(e);n.headerW=r.width,n.headerH=r.height,i+=n.headerH}t.statusbar&&(i+=t.statusbar.layoutRect().h),n.deltaH+=i,n.minH+=i,n.h+=i;var o=Re.getWindowSize();return n.x=t.settings.x||Math.max(0,o.w/2-n.w/2),n.y=t.settings.y||Math.max(0,o.h/2-n.h/2),n},renderHtml:function(){var e=this,t=e._layout,n=e._id,i=e.classPrefix,r=e.settings,o="",s="",a=r.html;return e.preRender(),t.preRender(e),r.title&&(o='<div id="'+n+'-head" class="'+i+'window-head"><div id="'+n+'-title" class="'+i+'title">'+e.encode(r.title)+'</div><div id="'+n+'-dragh" class="'+i+'dragh"></div><button type="button" class="'+i+'close" aria-hidden="true"><i class="mce-ico mce-i-remove"></i></button></div>'),r.url&&(a='<iframe src="'+r.url+'" tabindex="-1"></iframe>'),void 0===a&&(a=t.renderHtml(e)),e.statusbar&&(s=e.statusbar.renderHtml()),'<div id="'+n+'" class="'+e.classes+'" hidefocus="1"><div class="'+e.classPrefix+'reset" role="application">'+o+'<div id="'+n+'-body" class="'+e.bodyClasses+'">'+a+"</div>"+s+"</div></div>"},fullscreen:function(e){var n,t,i=this,r=_.document.documentElement,o=i.classPrefix;if(e!==i._fullscreen)if(we(_.window).on("resize",function(){var e;if(i._fullscreen)if(n)i._timer||(i._timer=u.setTimeout(function(){var e=Re.getWindowSize();i.moveTo(0,0).resizeTo(e.w,e.h),i._timer=0},50));else{e=(new Date).getTime();var t=Re.getWindowSize();i.moveTo(0,0).resizeTo(t.w,t.h),50<(new Date).getTime()-e&&(n=!0)}}),t=i.layoutRect(),i._fullscreen=e){i._initial={x:t.x,y:t.y,w:t.w,h:t.h},i.borderBox=Pe("0"),i.getEl("head").style.display="none",t.deltaH-=t.headerH+2,we([r,_.document.body]).addClass(o+"fullscreen"),i.classes.add("fullscreen");var s=Re.getWindowSize();i.moveTo(0,0).resizeTo(s.w,s.h)}else i.borderBox=Pe(i.settings.border),i.getEl("head").style.display="",t.deltaH+=t.headerH,we([r,_.document.body]).removeClass(o+"fullscreen"),i.classes.remove("fullscreen"),i.moveTo(i._initial.x,i._initial.y).resizeTo(i._initial.w,i._initial.h);return i.reflow()},postRender:function(){var t,n=this;setTimeout(function(){n.classes.add("in"),n.fire("open")},0),n._super(),n.statusbar&&n.statusbar.postRender(),n.focus(),this.dragHelper=new ft(n._id+"-dragh",{start:function(){t={x:n.layoutRect().x,y:n.layoutRect().y}},drag:function(e){n.moveTo(t.x+e.deltaX,t.y+e.deltaY)}}),n.on("submit",function(e){e.isDefaultPrevented()||n.close()}),Lt.push(n),It(!0)},submit:function(){return this.fire("submit",{data:this.toJSON()})},remove:function(){var e,t=this;for(t.dragHelper.destroy(),t._super(),t.statusbar&&this.statusbar.remove(),Ft(t.classPrefix,!1),e=Lt.length;e--;)Lt[e]===t&&Lt.splice(e,1);It(0<Lt.length)},getContentWindow:function(){var e=this.getEl().getElementsByTagName("iframe")[0];return e?e.contentWindow:null}});!function(){if(!fe.desktop){var n={w:_.window.innerWidth,h:_.window.innerHeight};u.setInterval(function(){var e=_.window.innerWidth,t=_.window.innerHeight;n.w===e&&n.h===t||(n={w:e,h:t},we(_.window).trigger("resize"))},100)}we(_.window).on("resize",function(){var e,t,n=Re.getWindowSize();for(e=0;e<Lt.length;e++)t=Lt[e].layoutRect(),Lt[e].moveTo(Lt[e].settings.x||Math.max(0,n.w/2-t.w/2),Lt[e].settings.y||Math.max(0,n.h/2-t.h/2))})}();var Vt,Yt,$t,qt=Ut.extend({init:function(e){e={border:1,padding:20,layout:"flex",pack:"center",align:"center",containerCls:"panel",autoScroll:!0,buttons:{type:"button",text:"Ok",action:"ok"},items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200}},this._super(e)},Statics:{OK:1,OK_CANCEL:2,YES_NO:3,YES_NO_CANCEL:4,msgBox:function(e){var t,i=e.callback||function(){};function n(e,t,n){return{type:"button",text:e,subtype:n?"primary":"",onClick:function(e){e.control.parents()[1].close(),i(t)}}}switch(e.buttons){case qt.OK_CANCEL:t=[n("Ok",!0,!0),n("Cancel",!1)];break;case qt.YES_NO:case qt.YES_NO_CANCEL:t=[n("Yes",1,!0),n("No",0)],e.buttons===qt.YES_NO_CANCEL&&t.push(n("Cancel",-1));break;default:t=[n("Ok",!0,!0)]}return new Ut({padding:20,x:e.x,y:e.y,minWidth:300,minHeight:100,layout:"flex",pack:"center",align:"center",buttons:t,title:e.title,role:"alertdialog",items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200,text:e.text},onPostRender:function(){this.aria("describedby",this.items()[0]._id)},onClose:e.onClose,onCancel:function(){i(!1)}}).renderTo(_.document.body).reflow()},alert:function(e,t){return"string"==typeof e&&(e={text:e}),e.callback=t,qt.msgBox(e)},confirm:function(e,t){return"string"==typeof e&&(e={text:e}),e.callback=t,e.buttons=qt.OK_CANCEL,qt.msgBox(e)}}}),Xt=function(n){return{renderUI:function(e){return Nt(n,this,e)},resizeTo:function(e,t){return ce(n,e,t)},resizeBy:function(e,t){return de(n,e,t)},getNotificationManagerImpl:function(){return Bt(n)},getWindowManagerImpl:function(){return{open:function(n,e,t){var i;return n.title=n.title||" ",n.url=n.url||n.file,n.url&&(n.width=parseInt(n.width||320,10),n.height=parseInt(n.height||240,10)),n.body&&(n.items={defaults:n.defaults,type:n.bodyType||"form",items:n.body,data:n.data,callbacks:n.commands}),n.url||n.buttons||(n.buttons=[{text:"Ok",subtype:"primary",onclick:function(){i.find("form")[0].submit()}},{text:"Cancel",onclick:function(){i.close()}}]),(i=new Ut(n)).on("close",function(){t(i)}),n.data&&i.on("postRender",function(){this.find("*").each(function(e){var t=e.name();t in n.data&&e.value(n.data[t])})}),i.features=n||{},i.params=e||{},i=i.renderTo(_.document.body).reflow()},alert:function(e,t,n){var i;return(i=qt.alert(e,function(){t()})).on("close",function(){n(i)}),i},confirm:function(e,t,n){var i;return(i=qt.confirm(e,function(e){t(e)})).on("close",function(){n(i)}),i},close:function(e){e.close()},getParams:function(e){return e.params},setParams:function(e,t){e.params=t}}}}},jt=Me.extend({Defaults:{firstControlClass:"first",lastControlClass:"last"},init:function(e){this.settings=w.extend({},this.Defaults,e)},preRender:function(e){e.bodyClasses.add(this.settings.containerClass)},applyClasses:function(e){var t,n,i,r,o=this.settings;t=o.firstControlClass,n=o.lastControlClass,e.each(function(e){e.classes.remove(t).remove(n).add(o.controlClass),e.visible()&&(i||(i=e),r=e)}),i&&i.classes.add(t),r&&r.classes.add(n)},renderHtml:function(e){var t="";return this.applyClasses(e.items()),e.items().each(function(e){t+=e.renderHtml()}),t},recalc:function(){},postRender:function(){},isNative:function(){return!1}}),Jt=jt.extend({Defaults:{containerClass:"abs-layout",controlClass:"abs-layout-item"},recalc:function(e){e.items().filter(":visible").each(function(e){var t=e.settings;e.layoutRect({x:t.x,y:t.y,w:t.w,h:t.h}),e.recalc&&e.recalc()})},renderHtml:function(e){return'<div id="'+e._id+'-absend" class="'+e.classPrefix+'abs-end"></div>'+this._super(e)}}),Gt=Wt.extend({Defaults:{classes:"widget btn",role:"button"},init:function(e){var t,n=this;n._super(e),e=n.settings,t=n.settings.size,n.on("click mousedown",function(e){e.preventDefault()}),n.on("touchstart",function(e){n.fire("click",e),e.preventDefault()}),e.subtype&&n.classes.add(e.subtype),t&&n.classes.add("btn-"+t),e.icon&&n.icon(e.icon)},icon:function(e){return arguments.length?(this.state.set("icon",e),this):this.state.get("icon")},repaint:function(){var e,t=this.getEl().firstChild;t&&((e=t.style).width=e.height="100%"),this._super()},renderHtml:function(){var e,t,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a="",l=n.settings;return(e=l.image)?(o="none","string"!=typeof e&&(e=_.window.getSelection?e[0]:e[1]),e=" style=\"background-image: url('"+e+"')\""):e="",s&&(n.classes.add("btn-has-text"),a='<span class="'+r+'txt">'+n.encode(s)+"</span>"),o=o?r+"ico "+r+"i-"+o:"",t="boolean"==typeof l.active?' aria-pressed="'+l.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" tabindex="-1"'+t+'><button id="'+i+'-button" role="presentation" type="button" tabindex="-1">'+(o?'<i class="'+o+'"'+e+"></i>":"")+a+"</button></div>"},bindStates:function(){var o=this,n=o.$,i=o.classPrefix+"txt";function s(e){var t=n("span."+i,o.getEl());e?(t[0]||(n("button:first",o.getEl()).append('<span class="'+i+'"></span>'),t=n("span."+i,o.getEl())),t.html(o.encode(e))):t.remove(),o.classes.toggle("btn-has-text",!!e)}return o.state.on("change:text",function(e){s(e.value)}),o.state.on("change:icon",function(e){var t=e.value,n=o.classPrefix;t=(o.settings.icon=t)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];t?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=t):r&&i.removeChild(r),s(o.state.get("text"))}),o._super()}}),Kt=Gt.extend({init:function(e){e=w.extend({text:"Browse...",multiple:!1,accept:null},e),this._super(e),this.classes.add("browsebutton"),e.multiple&&this.classes.add("multiple")},postRender:function(){var n=this,t=Re.create("input",{type:"file",id:n._id+"-browse",accept:n.settings.accept});n._super(),we(t).on("change",function(e){var t=e.target.files;n.value=function(){return t.length?n.settings.multiple?t:t[0]:null},e.preventDefault(),t.length&&n.fire("change",e)}),we(t).on("click",function(e){e.stopPropagation()}),we(n.getEl("button")).on("click touchstart",function(e){e.stopPropagation(),t.click()}),n.getEl().appendChild(t)},remove:function(){we(this.getEl("button")).off(),we(this.getEl("input")).off(),this._super()}}),Zt=ct.extend({Defaults:{defaultType:"button",role:"group"},renderHtml:function(){var e=this,t=e._layout;return e.classes.add("btn-group"),e.preRender(),t.preRender(e),'<div id="'+e._id+'" class="'+e.classes+'"><div id="'+e._id+'-body">'+(e.settings.html||"")+t.renderHtml(e)+"</div></div>"}}),Qt=Wt.extend({Defaults:{classes:"checkbox",role:"checkbox",checked:!1},init:function(e){var t=this;t._super(e),t.on("click mousedown",function(e){e.preventDefault()}),t.on("click",function(e){e.preventDefault(),t.disabled()||t.checked(!t.checked())}),t.checked(t.settings.checked)},checked:function(e){return arguments.length?(this.state.set("checked",e),this):this.state.get("checked")},value:function(e){return arguments.length?this.checked(e):this.checked()},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix;return'<div id="'+t+'" class="'+e.classes+'" unselectable="on" aria-labelledby="'+t+'-al" tabindex="-1"><i class="'+n+"ico "+n+'i-checkbox"></i><span id="'+t+'-al" class="'+n+'label">'+e.encode(e.state.get("text"))+"</span></div>"},bindStates:function(){var o=this;function t(e){o.classes.toggle("checked",e),o.aria("checked",e)}return o.state.on("change:text",function(e){o.getEl("al").firstChild.data=o.translate(e.value)}),o.state.on("change:checked change:value",function(e){o.fire("change"),t(e.value)}),o.state.on("change:icon",function(e){var t=e.value,n=o.classPrefix;if(void 0===t)return o.settings.icon;t=(o.settings.icon=t)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];t?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=t):r&&i.removeChild(r)}),o.state.get("checked")&&t(!0),o._super()}}),en=tinymce.util.Tools.resolve("tinymce.util.VK"),tn=Wt.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.classes.add("combobox"),r.subinput=!0,r.ariaTarget="inp",i.menu=i.menu||i.values,i.menu&&(i.icon="caret"),r.on("click",function(e){var t=e.target,n=r.getEl();if(we.contains(n,t)||t===n)for(;t&&t!==n;)t.id&&-1!==t.id.indexOf("-open")&&(r.fire("action"),i.menu&&(r.showMenu(),e.aria&&r.menu.items()[0].focus())),t=t.parentNode}),r.on("keydown",function(e){var t;13===e.keyCode&&"INPUT"===e.target.nodeName&&(e.preventDefault(),r.parents().reverse().each(function(e){if(e.toJSON)return t=e,!1}),r.fire("submit",{data:t.toJSON()}))}),r.on("keyup",function(e){if("INPUT"===e.target.nodeName){var t=r.state.get("value"),n=e.target.value;n!==t&&(r.state.set("value",n),r.fire("autocomplete",e))}}),r.on("mouseover",function(e){var t=r.tooltip().moveTo(-65535);if(r.statusLevel()&&-1!==e.target.className.indexOf(r.classPrefix+"status")){var n=r.statusMessage()||"Ok",i=t.text(n).show().testMoveRel(e.target,["bc-tc","bc-tl","bc-tr"]);t.classes.toggle("tooltip-n","bc-tc"===i),t.classes.toggle("tooltip-nw","bc-tl"===i),t.classes.toggle("tooltip-ne","bc-tr"===i),t.moveRel(e.target,i)}})},statusLevel:function(e){return 0<arguments.length&&this.state.set("statusLevel",e),this.state.get("statusLevel")},statusMessage:function(e){return 0<arguments.length&&this.state.set("statusMessage",e),this.state.get("statusMessage")},showMenu:function(){var e,t=this,n=t.settings;t.menu||((e=n.menu||[]).length?e={type:"menu",items:e}:e.type=e.type||"menu",t.menu=b.create(e).parent(t).renderTo(t.getContainerElm()),t.fire("createmenu"),t.menu.reflow(),t.menu.on("cancel",function(e){e.control===t.menu&&t.focus()}),t.menu.on("show hide",function(e){e.control.items().each(function(e){e.active(e.value()===t.value())})}).fire("show"),t.menu.on("select",function(e){t.value(e.control.value())}),t.on("focusin",function(e){"INPUT"===e.target.tagName.toUpperCase()&&t.menu.hide()}),t.aria("expanded",!0)),t.menu.show(),t.menu.layoutRect({w:t.layoutRect().w}),t.menu.moveRel(t.getEl(),t.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},focus:function(){this.getEl("inp").focus()},repaint:function(){var e,t,n=this,i=n.getEl(),r=n.getEl("open"),o=n.layoutRect(),s=0,a=i.firstChild;n.statusLevel()&&"none"!==n.statusLevel()&&(s=parseInt(Re.getRuntimeStyle(a,"padding-right"),10)-parseInt(Re.getRuntimeStyle(a,"padding-left"),10)),e=r?o.w-Re.getSize(r).width-10:o.w-10;var l=_.document;return l.all&&(!l.documentMode||l.documentMode<=8)&&(t=n.layoutRect().h-2+"px"),we(a).css({width:e-s,lineHeight:t}),n._super(),n},postRender:function(){var t=this;return we(this.getEl("inp")).on("change",function(e){t.state.set("value",e.target.value),t.fire("change",e)}),t._super()},renderHtml:function(){var e,t,n,i=this,r=i._id,o=i.settings,s=i.classPrefix,a=i.state.get("value")||"",l="",u="";return"spellcheck"in o&&(u+=' spellcheck="'+o.spellcheck+'"'),o.maxLength&&(u+=' maxlength="'+o.maxLength+'"'),o.size&&(u+=' size="'+o.size+'"'),o.subtype&&(u+=' type="'+o.subtype+'"'),n='<i id="'+r+'-status" class="mce-status mce-ico" style="display: none"></i>',i.disabled()&&(u+=' disabled="disabled"'),(e=o.icon)&&"caret"!==e&&(e=s+"ico "+s+"i-"+o.icon),t=i.state.get("text"),(e||t)&&(l='<div id="'+r+'-open" class="'+s+"btn "+s+'open" tabIndex="-1" role="button"><button id="'+r+'-action" type="button" hidefocus="1" tabindex="-1">'+("caret"!==e?'<i class="'+e+'"></i>':'<i class="'+s+'caret"></i>')+(t?(e?" ":"")+t:"")+"</button></div>",i.classes.add("has-open")),'<div id="'+r+'" class="'+i.classes+'"><input id="'+r+'-inp" class="'+s+'textbox" value="'+i.encode(a,!1)+'" hidefocus="1"'+u+' placeholder="'+i.encode(o.placeholder)+'" />'+n+l+"</div>"},value:function(e){return arguments.length?(this.state.set("value",e),this):(this.state.get("rendered")&&this.state.set("value",this.getEl("inp").value),this.state.get("value"))},showAutoComplete:function(e,i){var r=this;if(0!==e.length){r.menu?r.menu.items().remove():r.menu=b.create({type:"menu",classes:"combobox-menu",layout:"flow"}).parent(r).renderTo(),w.each(e,function(e){var t,n;r.menu.add({text:e.title,url:e.previewUrl,match:i,classes:"menu-item-ellipsis",onclick:(t=e.value,n=e.title,function(){r.fire("selectitem",{title:n,value:t})})})}),r.menu.renderNew(),r.hideMenu(),r.menu.on("cancel",function(e){e.control.parent()===r.menu&&(e.stopPropagation(),r.focus(),r.hideMenu())}),r.menu.on("select",function(){r.focus()});var t=r.layoutRect().w;r.menu.layoutRect({w:t,minW:0,maxW:t}),r.menu.repaint(),r.menu.reflow(),r.menu.show(),r.menu.moveRel(r.getEl(),r.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])}else r.hideMenu()},hideMenu:function(){this.menu&&this.menu.hide()},bindStates:function(){var r=this;r.state.on("change:value",function(e){r.getEl("inp").value!==e.value&&(r.getEl("inp").value=e.value)}),r.state.on("change:disabled",function(e){r.getEl("inp").disabled=e.value}),r.state.on("change:statusLevel",function(e){var t=r.getEl("status"),n=r.classPrefix,i=e.value;Re.css(t,"display","none"===i?"none":""),Re.toggleClass(t,n+"i-checkmark","ok"===i),Re.toggleClass(t,n+"i-warning","warn"===i),Re.toggleClass(t,n+"i-error","error"===i),r.classes.toggle("has-status","none"!==i),r.repaint()}),Re.on(r.getEl("status"),"mouseleave",function(){r.tooltip().hide()}),r.on("cancel",function(e){r.menu&&r.menu.visible()&&(e.stopPropagation(),r.hideMenu())});var n=function(e,t){t&&0<t.items().length&&t.items().eq(e)[0].focus()};return r.on("keydown",function(e){var t=e.keyCode;"INPUT"===e.target.nodeName&&(t===en.DOWN?(e.preventDefault(),r.fire("autocomplete"),n(0,r.menu)):t===en.UP&&(e.preventDefault(),n(-1,r.menu)))}),r._super()},remove:function(){we(this.getEl("inp")).off(),this.menu&&this.menu.remove(),this._super()}}),nn=tn.extend({init:function(e){var t=this;e.spellcheck=!1,e.onaction&&(e.icon="none"),t._super(e),t.classes.add("colorbox"),t.on("change keyup postrender",function(){t.repaintColor(t.value())})},repaintColor:function(e){var t=this.getEl("open"),n=t?t.getElementsByTagName("i")[0]:null;if(n)try{n.style.background=e}catch(i){}},bindStates:function(){var t=this;return t.state.on("change:value",function(e){t.state.get("rendered")&&t.repaintColor(e.value)}),t._super()}}),rn=Gt.extend({showPanel:function(){var t=this,e=t.settings;if(t.classes.add("opened"),t.panel)t.panel.show();else{var n=e.panel;n.type&&(n={layout:"grid",items:n}),n.role=n.role||"dialog",n.popover=!0,n.autohide=!0,n.ariaRoot=!0,t.panel=new kt(n).on("hide",function(){t.classes.remove("opened")}).on("cancel",function(e){e.stopPropagation(),t.focus(),t.hidePanel()}).parent(t).renderTo(t.getContainerElm()),t.panel.fire("show"),t.panel.reflow()}var i=t.panel.testMoveRel(t.getEl(),e.popoverAlign||(t.isRtl()?["bc-tc","bc-tl","bc-tr"]:["bc-tc","bc-tr","bc-tl","tc-bc","tc-br","tc-bl"]));t.panel.classes.toggle("start","l"===i.substr(-1)),t.panel.classes.toggle("end","r"===i.substr(-1));var r="t"===i.substr(0,1);t.panel.classes.toggle("bottom",!r),t.panel.classes.toggle("top",r),t.panel.moveRel(t.getEl(),i)},hidePanel:function(){this.panel&&this.panel.hide()},postRender:function(){var t=this;return t.aria("haspopup",!0),t.on("click",function(e){e.control===t&&(t.panel&&t.panel.visible()?t.hidePanel():(t.showPanel(),t.panel.focus(!!e.aria)))}),t._super()},remove:function(){return this.panel&&(this.panel.remove(),this.panel=null),this._super()}}),on=v.DOM,sn=rn.extend({init:function(e){this._super(e),this.classes.add("splitbtn"),this.classes.add("colorbutton")},color:function(e){return e?(this._color=e,this.getEl("preview").style.backgroundColor=e,this):this._color},resetColor:function(){return this._color=null,this.getEl("preview").style.backgroundColor=null,this},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix,i=e.state.get("text"),r=e.settings.icon?n+"ico "+n+"i-"+e.settings.icon:"",o=e.settings.image?" style=\"background-image: url('"+e.settings.image+"')\"":"",s="";return i&&(e.classes.add("btn-has-text"),s='<span class="'+n+'txt">'+e.encode(i)+"</span>"),'<div id="'+t+'" class="'+e.classes+'" role="button" tabindex="-1" aria-haspopup="true"><button role="presentation" hidefocus="1" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+o+"></i>":"")+'<span id="'+t+'-preview" class="'+n+'preview"></span>'+s+'</button><button type="button" class="'+n+'open" hidefocus="1" tabindex="-1"> <i class="'+n+'caret"></i></button></div>'},postRender:function(){var t=this,n=t.settings.onclick;return t.on("click",function(e){e.aria&&"down"===e.aria.key||e.control!==t||on.getParent(e.target,"."+t.classPrefix+"open")||(e.stopImmediatePropagation(),n.call(t,e))}),delete t.settings.onclick,t._super()}}),an=tinymce.util.Tools.resolve("tinymce.util.Color"),ln=Wt.extend({Defaults:{classes:"widget colorpicker"},init:function(e){this._super(e)},postRender:function(){var n,i,r,o,s,a=this,l=a.color();function u(e,t){var n,i,r=Re.getPos(e);return n=t.pageX-r.x,i=t.pageY-r.y,{x:n=Math.max(0,Math.min(n/e.clientWidth,1)),y:i=Math.max(0,Math.min(i/e.clientHeight,1))}}function c(e,t){var n=(360-e.h)/360;Re.css(r,{top:100*n+"%"}),t||Re.css(s,{left:e.s+"%",top:100-e.v+"%"}),o.style.background=an({s:100,v:100,h:e.h}).toHex(),a.color().parse({s:e.s,v:e.v,h:e.h})}function e(e){var t;t=u(o,e),n.s=100*t.x,n.v=100*(1-t.y),c(n),a.fire("change")}function t(e){var t;t=u(i,e),(n=l.toHsv()).h=360*(1-t.y),c(n,!0),a.fire("change")}i=a.getEl("h"),r=a.getEl("hp"),o=a.getEl("sv"),s=a.getEl("svp"),a._repaint=function(){c(n=l.toHsv())},a._super(),a._svdraghelper=new ft(a._id+"-sv",{start:e,drag:e}),a._hdraghelper=new ft(a._id+"-h",{start:t,drag:t}),a._repaint()},rgb:function(){return this.color().toRgb()},value:function(e){if(!arguments.length)return this.color().toHex();this.color().parse(e),this._rendered&&this._repaint()},color:function(){return this._color||(this._color=an()),this._color},renderHtml:function(){var e,t=this._id,o=this.classPrefix,s="#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000";return e='<div id="'+t+'-h" class="'+o+'colorpicker-h" style="background: -ms-linear-gradient(top,'+s+");background: linear-gradient(to bottom,"+s+');">'+function(){var e,t,n,i,r="";for(n="filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=",e=0,t=(i=s.split(",")).length-1;e<t;e++)r+='<div class="'+o+'colorpicker-h-chunk" style="height:'+100/t+"%;"+n+i[e]+",endColorstr="+i[e+1]+");-ms-"+n+i[e]+",endColorstr="+i[e+1]+')"></div>';return r}()+'<div id="'+t+'-hp" class="'+o+'colorpicker-h-marker"></div></div>','<div id="'+t+'" class="'+this.classes+'"><div id="'+t+'-sv" class="'+o+'colorpicker-sv"><div class="'+o+'colorpicker-overlay1"><div class="'+o+'colorpicker-overlay2"><div id="'+t+'-svp" class="'+o+'colorpicker-selector1"><div class="'+o+'colorpicker-selector2"></div></div></div></div></div>'+e+"</div>"}}),un=Wt.extend({init:function(e){e=w.extend({height:100,text:"Drop an image here",multiple:!1,accept:null},e),this._super(e),this.classes.add("dropzone"),e.multiple&&this.classes.add("multiple")},renderHtml:function(){var e,t,n=this.settings;return e={id:this._id,hidefocus:"1"},t=Re.create("div",e,"<span>"+this.translate(n.text)+"</span>"),n.height&&Re.css(t,"height",n.height+"px"),n.width&&Re.css(t,"width",n.width+"px"),t.className=this.classes,t.outerHTML},postRender:function(){var i=this,e=function(e){e.preventDefault(),i.classes.toggle("dragenter"),i.getEl().className=i.classes};i._super(),i.$el.on("dragover",function(e){e.preventDefault()}),i.$el.on("dragenter",e),i.$el.on("dragleave",e),i.$el.on("drop",function(e){if(e.preventDefault(),!i.state.get("disabled")){var t=function(e){var t=i.settings.accept;if("string"!=typeof t)return e;var n=new RegExp("("+t.split(/\s*,\s*/).join("|")+")$","i");return w.grep(e,function(e){return n.test(e.name)})}(e.dataTransfer.files);i.value=function(){return t.length?i.settings.multiple?t:t[0]:null},t.length&&i.fire("change",e)}})},remove:function(){this.$el.off(),this._super()}}),cn=Wt.extend({init:function(e){var n=this;e.delimiter||(e.delimiter="\xbb"),n._super(e),n.classes.add("path"),n.canFocus=!0,n.on("click",function(e){var t;(t=e.target.getAttribute("data-index"))&&n.fire("select",{value:n.row()[t],index:t})}),n.row(n.settings.row)},focus:function(){return this.getEl().firstChild.focus(),this},row:function(e){return arguments.length?(this.state.set("row",e),this):this.state.get("row")},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'">'+this._getDataPathHtml(this.state.get("row"))+"</div>"},bindStates:function(){var t=this;return t.state.on("change:row",function(e){t.innerHtml(t._getDataPathHtml(e.value))}),t._super()},_getDataPathHtml:function(e){var t,n,i=e||[],r="",o=this.classPrefix;for(t=0,n=i.length;t<n;t++)r+=(0<t?'<div class="'+o+'divider" aria-hidden="true"> '+this.settings.delimiter+" </div>":"")+'<div role="button" class="'+o+"path-item"+(t===n-1?" "+o+"last":"")+'" data-index="'+t+'" tabindex="-1" id="'+this._id+"-"+t+'" aria-level="'+(t+1)+'">'+i[t].name+"</div>";return r||(r='<div class="'+o+'path-item">\xa0</div>'),r}}),dn=cn.extend({postRender:function(){var o=this,s=o.settings.editor;function a(e){if(1===e.nodeType){if("BR"===e.nodeName||e.getAttribute("data-mce-bogus"))return!0;if("bookmark"===e.getAttribute("data-mce-type"))return!0}return!1}return!1!==s.settings.elementpath&&(o.on("select",function(e){s.focus(),s.selection.select(this.row()[e.index].element),s.nodeChanged()}),s.on("nodeChange",function(e){for(var t=[],n=e.parents,i=n.length;i--;)if(1===n[i].nodeType&&!a(n[i])){var r=s.fire("ResolveName",{name:n[i].nodeName.toLowerCase(),target:n[i]});if(r.isDefaultPrevented()||t.push({name:r.name,element:n[i]}),r.isPropagationStopped())break}o.row(t)})),o._super()}}),fn=ct.extend({Defaults:{layout:"flex",align:"center",defaults:{flex:1}},renderHtml:function(){var e=this,t=e._layout,n=e.classPrefix;return e.classes.add("formitem"),t.preRender(e),'<div id="'+e._id+'" class="'+e.classes+'" hidefocus="1" tabindex="-1">'+(e.settings.title?'<div id="'+e._id+'-title" class="'+n+'title">'+e.settings.title+"</div>":"")+'<div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+(e.settings.html||"")+t.renderHtml(e)+"</div></div>"}}),hn=ct.extend({Defaults:{containerCls:"form",layout:"flex",direction:"column",align:"stretch",flex:1,padding:15,labelGap:30,spacing:10,callbacks:{submit:function(){this.submit()}}},preRender:function(){var i=this,e=i.items();i.settings.formItemDefaults||(i.settings.formItemDefaults={layout:"flex",autoResize:"overflow",defaults:{flex:1}}),e.each(function(e){var t,n=e.settings.label;n&&((t=new fn(w.extend({items:{type:"label",id:e._id+"-l",text:n,flex:0,forId:e._id,disabled:e.disabled()}},i.settings.formItemDefaults))).type="formitem",e.aria("labelledby",e._id+"-l"),"undefined"==typeof e.settings.flex&&(e.settings.flex=1),i.replace(e,t),t.add(e))})},submit:function(){return this.fire("submit",{data:this.toJSON()})},postRender:function(){this._super(),this.fromJSON(this.settings.data)},bindStates:function(){var n=this;function e(){var e,t,i=0,r=[];if(!1!==n.settings.labelGapCalc)for(("children"===n.settings.labelGapCalc?n.find("formitem"):n.items()).filter("formitem").each(function(e){var t=e.items()[0],n=t.getEl().clientWidth;i=i<n?n:i,r.push(t)}),t=n.settings.labelGap||0,e=r.length;e--;)r[e].settings.minWidth=i+t}n._super(),n.on("show",e),e()}}),mn=hn.extend({Defaults:{containerCls:"fieldset",layout:"flex",direction:"column",align:"stretch",flex:1,padding:"25 15 5 15",labelGap:30,spacing:10,border:1},renderHtml:function(){var e=this,t=e._layout,n=e.classPrefix;return e.preRender(),t.preRender(e),'<fieldset id="'+e._id+'" class="'+e.classes+'" hidefocus="1" tabindex="-1">'+(e.settings.title?'<legend id="'+e._id+'-title" class="'+n+'fieldset-title">'+e.settings.title+"</legend>":"")+'<div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+(e.settings.html||"")+t.renderHtml(e)+"</div></fieldset>"}}),gn=0,pn=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:H(e)}},vn={fromHtml:function(e,t){var n=(t||_.document).createElement("div");if(n.innerHTML=e,!n.hasChildNodes()||1<n.childNodes.length)throw _.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return pn(n.childNodes[0])},fromTag:function(e,t){var n=(t||_.document).createElement(e);return pn(n)},fromText:function(e,t){var n=(t||_.document).createTextNode(e);return pn(n)},fromDom:pn,fromPoint:function(e,t,n){var i=e.dom();return D.from(i.elementFromPoint(t,n)).map(pn)}},bn=(_.Node.ATTRIBUTE_NODE,_.Node.CDATA_SECTION_NODE,_.Node.COMMENT_NODE,_.Node.DOCUMENT_NODE),yn=(_.Node.DOCUMENT_TYPE_NODE,_.Node.DOCUMENT_FRAGMENT_NODE,_.Node.ELEMENT_NODE),xn=(_.Node.TEXT_NODE,_.Node.PROCESSING_INSTRUCTION_NODE,_.Node.ENTITY_REFERENCE_NODE,_.Node.ENTITY_NODE,_.Node.NOTATION_NODE,"undefined"!=typeof _.window?_.window:Function("return this;")(),function(e,t){var n=function(e,t){for(var n=0;n<e.length;n++){var i=e[n];if(i.test(t))return i}return undefined}(e,t);if(!n)return{major:0,minor:0};var i=function(e){return Number(t.replace(n,"$"+e))};return _n(i(1),i(2))}),wn=function(){return _n(0,0)},_n=function(e,t){return{major:e,minor:t}},Rn={nu:_n,detect:function(e,t){var n=String(t).toLowerCase();return 0===e.length?wn():xn(e,n)},unknown:wn},Cn="Firefox",En=function(e,t){return function(){return t===e}},kn=function(e){var t=e.current;return{current:t,version:e.version,isEdge:En("Edge",t),isChrome:En("Chrome",t),isIE:En("IE",t),isOpera:En("Opera",t),isFirefox:En(Cn,t),isSafari:En("Safari",t)}},Hn={unknown:function(){return kn({current:undefined,version:Rn.unknown()})},nu:kn,edge:H("Edge"),chrome:H("Chrome"),ie:H("IE"),opera:H("Opera"),firefox:H(Cn),safari:H("Safari")},Sn="Windows",Tn="Android",Mn="Solaris",Nn="FreeBSD",Pn=function(e,t){return function(){return t===e}},Wn=function(e){var t=e.current;return{current:t,version:e.version,isWindows:Pn(Sn,t),isiOS:Pn("iOS",t),isAndroid:Pn(Tn,t),isOSX:Pn("OSX",t),isLinux:Pn("Linux",t),isSolaris:Pn(Mn,t),isFreeBSD:Pn(Nn,t)}},Dn={unknown:function(){return Wn({current:undefined,version:Rn.unknown()})},nu:Wn,windows:H(Sn),ios:H("iOS"),android:H(Tn),linux:H("Linux"),osx:H("OSX"),solaris:H(Mn),freebsd:H(Nn)},On=function(e,t){var n=String(t).toLowerCase();return function(e,t){for(var n=0,i=e.length;n<i;n++){var r=e[n];if(t(r,n,e))return D.some(r)}return D.none()}(e,function(e){return e.search(n)})},An=function(e,n){return On(e,n).map(function(e){var t=Rn.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Bn=function(e,n){return On(e,n).map(function(e){var t=Rn.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Ln=function(e,t){return-1!==e.indexOf(t)},zn=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,In=function(t){return function(e){return Ln(e,t)}},Fn=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return Ln(e,"edge/")&&Ln(e,"chrome")&&Ln(e,"safari")&&Ln(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,zn],search:function(e){return Ln(e,"chrome")&&!Ln(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return Ln(e,"msie")||Ln(e,"trident")}},{name:"Opera",versionRegexes:[zn,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:In("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:In("firefox")},{name:"Safari",versionRegexes:[zn,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return(Ln(e,"safari")||Ln(e,"mobile/"))&&Ln(e,"applewebkit")}}],Un=[{name:"Windows",search:In("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return Ln(e,"iphone")||Ln(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:In("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:In("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:In("linux"),versionRegexes:[]},{name:"Solaris",search:In("sunos"),versionRegexes:[]},{name:"FreeBSD",search:In("freebsd"),versionRegexes:[]}],Vn={browsers:H(Fn),oses:H(Un)},Yn=function(e){var t,n,i,r,o,s,a,l,u,c,d,f=Vn.browsers(),h=Vn.oses(),m=An(f,e).fold(Hn.unknown,Hn.nu),g=Bn(h,e).fold(Dn.unknown,Dn.nu);return{browser:m,os:g,deviceType:(n=m,i=e,r=(t=g).isiOS()&&!0===/ipad/i.test(i),o=t.isiOS()&&!r,s=t.isAndroid()&&3===t.version.major,a=t.isAndroid()&&4===t.version.major,l=r||s||a&&!0===/mobile/i.test(i),u=t.isiOS()||t.isAndroid(),c=u&&!l,d=n.isSafari()&&t.isiOS()&&!1===/safari/i.test(i),{isiPad:H(r),isiPhone:H(o),isTablet:H(l),isPhone:H(c),isTouch:H(u),isAndroid:t.isAndroid,isiOS:t.isiOS,isWebView:H(d)})}},$n=($t=!(Vt=function(){var e=_.navigator.userAgent;return Yn(e)}),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return $t||($t=!0,Yt=Vt.apply(null,e)),Yt}),qn=yn,Xn=bn,jn=function(e){return e.nodeType!==qn&&e.nodeType!==Xn||0===e.childElementCount},Jn=($n().browser.isIE(),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t]}("element","offset"),w.trim),Gn=function(t){return function(e){if(e&&1===e.nodeType){if(e.contentEditable===t)return!0;if(e.getAttribute("data-mce-contenteditable")===t)return!0}return!1}},Kn=Gn("true"),Zn=Gn("false"),Qn=function(e,t,n,i,r){return{type:e,title:t,url:n,level:i,attach:r}},ei=function(e){return e.innerText||e.textContent},ti=function(e){return e.id?e.id:(t="h",n=(new Date).getTime(),t+"_"+Math.floor(1e9*Math.random())+ ++gn+String(n));var t,n},ni=function(e){return(t=e)&&"A"===t.nodeName&&(t.id||t.name)&&ri(e);var t},ii=function(e){return e&&/^(H[1-6])$/.test(e.nodeName)},ri=function(e){return function(e){for(;e=e.parentNode;){var t=e.contentEditable;if(t&&"inherit"!==t)return Kn(e)}return!1}(e)&&!Zn(e)},oi=function(e){return ii(e)&&ri(e)},si=function(e){var t,n=ti(e);return Qn("header",ei(e),"#"+n,ii(t=e)?parseInt(t.nodeName.substr(1),10):0,function(){e.id=n})},ai=function(e){var t=e.id||e.name,n=ei(e);return Qn("anchor",n||"#"+t,"#"+t,0,k)},li=function(e){var t,n,i,r,o,s;return t="h1,h2,h3,h4,h5,h6,a:not([href])",n=e,G(($n().browser.isIE(),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t]}("element","offset"),i=vn.fromDom(n),r=t,s=(o=i)===undefined?_.document:o.dom(),jn(s)?[]:G(s.querySelectorAll(r),vn.fromDom)),function(e){return e.dom()})},ui=function(e){return 0<Jn(e.title).length},ci=function(e){var t,n=li(e);return Z((t=n,G(Z(t,oi),si)).concat(G(Z(n,ni),ai)),ui)},di={},fi=function(e){return{title:e.title,value:{title:{raw:e.title},url:e.url,attach:e.attach}}},hi=function(e,t){return{title:e,value:{title:e,url:t,attach:k}}},mi=function(e,t,n){var i=t in e?e[t]:n;return!1===i?null:i},gi=function(e,i,r,t){var n,o,s,a,l,u,c={title:"-"},d=function(e){var t=e.hasOwnProperty(r)?e[r]:[],n=Z(t,function(e){return t=e,!J(i,function(e){return e.url===t});var t});return w.map(n,function(e){return{title:e,value:{title:e,url:e,attach:k}}})},f=function(t){var e,n=Z(i,function(e){return e.type===t});return e=n,w.map(e,fi)};return!1===t.typeahead_urls?[]:"file"===r?(n=[vi(e,d(di)),vi(e,f("header")),vi(e,(a=f("anchor"),l=mi(t,"anchor_top","#top"),u=mi(t,"anchor_bottom","#bottom"),null!==l&&a.unshift(hi("<top>",l)),null!==u&&a.push(hi("<bottom>",u)),a))],o=function(e,t){return 0===e.length||0===t.length?e.concat(t):e.concat(c,t)},s=[],K(n,function(e){s=o(s,e)}),s):vi(e,d(di))},pi=function(e,t){var n,i,r,o=di[t];/^https?/.test(e)&&(o?(n=o,i=e,r=j(n,i),-1===r?D.none():D.some(r)).isNone()&&(di[t]=o.slice(0,5).concat(e)):di[t]=[e])},vi=function(e,t){var n=e.toLowerCase(),i=w.grep(t,function(e){return-1!==e.title.toLowerCase().indexOf(n)});return 1===i.length&&i[0].title===e?[]:i},bi=function(o,e,n){var i=e.filepicker_validator_handler;i&&o.state.on("change:value",function(e){var t;0!==(t=e.value).length?i({url:t,type:n},function(e){var t,n,i,r=(n=(t=e).status,i=t.message,"valid"===n?{status:"ok",message:i}:"unknown"===n?{status:"warn",message:i}:"invalid"===n?{status:"warn",message:i}:{status:"none",message:""});o.statusMessage(r.message),o.statusLevel(r.status)}):o.statusLevel("none")})},yi=tn.extend({Statics:{clearHistory:function(){di={}}},init:function(e){var t,n,i,r,o,s,a,l,u=this,c=window.tinymce?window.tinymce.activeEditor:h.activeEditor,d=c.settings,f=e.filetype;e.spellcheck=!1,(i=d.file_picker_types||d.file_browser_callback_types)&&(i=w.makeMap(i,/[, ]/)),i&&!i[f]||(!(n=d.file_picker_callback)||i&&!i[f]?!(n=d.file_browser_callback)||i&&!i[f]||(t=function(){n(u.getEl("inp").id,u.value(),f,window)}):t=function(){var e=u.fire("beforecall").meta;e=w.extend({filetype:f},e),n.call(c,function(e,t){u.value(e).fire("change",{meta:t})},u.value(),e)}),t&&(e.icon="browse",e.onaction=t),u._super(e),u.classes.add("filepicker"),r=u,o=d,s=c.getBody(),a=f,l=function(e){var t=ci(s),n=gi(e,t,a,o);r.showAutoComplete(n,e)},r.on("autocomplete",function(){l(r.value())}),r.on("selectitem",function(e){var t=e.value;r.value(t.url);var n,i=(n=t.title).raw?n.raw:n;"image"===a?r.fire("change",{meta:{alt:i,attach:t.attach}}):r.fire("change",{meta:{text:i,attach:t.attach}}),r.focus()}),r.on("click",function(e){0===r.value().length&&"INPUT"===e.target.nodeName&&l("")}),r.on("PostRender",function(){r.getRoot().on("submit",function(e){e.isDefaultPrevented()||pi(r.value(),a)})}),bi(u,d,f)}}),xi=Jt.extend({recalc:function(e){var t=e.layoutRect(),n=e.paddingBox;e.items().filter(":visible").each(function(e){e.layoutRect({x:n.left,y:n.top,w:t.innerW-n.right-n.left,h:t.innerH-n.top-n.bottom}),e.recalc&&e.recalc()})}}),wi=Jt.extend({recalc:function(e){var t,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,b,y,x,w,_,R,C,E,k,H,S,T,M,N,P,W,D,O,A,B,L=[],z=Math.max,I=Math.min;for(i=e.items().filter(":visible"),r=e.layoutRect(),o=e.paddingBox,s=e.settings,f=e.isRtl()?s.direction||"row-reversed":s.direction,a=s.align,l=e.isRtl()?s.pack||"end":s.pack,u=s.spacing||0,"row-reversed"!==f&&"column-reverse"!==f||(i=i.set(i.toArray().reverse()),f=f.split("-")[0]),"column"===f?(C="y",_="h",R="minH",E="maxH",H="innerH",k="top",S="deltaH",T="contentH",D="left",P="w",M="x",N="innerW",W="minW",O="right",A="deltaW",B="contentW"):(C="x",_="w",R="minW",E="maxW",H="innerW",k="left",S="deltaW",T="contentW",D="top",P="h",M="y",N="innerH",W="minH",O="bottom",A="deltaH",B="contentH"),d=r[H]-o[k]-o[k],w=c=0,t=0,n=i.length;t<n;t++)m=(h=i[t]).layoutRect(),d-=t<n-1?u:0,0<(g=h.settings.flex)&&(c+=g,m[E]&&L.push(h),m.flex=g),d-=m[R],w<(p=o[D]+m[W]+o[O])&&(w=p);if((y={})[R]=d<0?r[R]-d+r[S]:r[H]-d+r[S],y[W]=w+r[A],y[T]=r[H]-d,y[B]=w,y.minW=I(y.minW,r.maxW),y.minH=I(y.minH,r.maxH),y.minW=z(y.minW,r.startMinWidth),y.minH=z(y.minH,r.startMinHeight),!r.autoResize||y.minW===r.minW&&y.minH===r.minH){for(b=d/c,t=0,n=L.length;t<n;t++)(v=(m=(h=L[t]).layoutRect())[E])<(p=m[R]+m.flex*b)?(d-=m[E]-m[R],c-=m.flex,m.flex=0,m.maxFlexSize=v):m.maxFlexSize=0;for(b=d/c,x=o[k],y={},0===c&&("end"===l?x=d+o[k]:"center"===l?(x=Math.round(r[H]/2-(r[H]-d)/2)+o[k])<0&&(x=o[k]):"justify"===l&&(x=o[k],u=Math.floor(d/(i.length-1)))),y[M]=o[D],t=0,n=i.length;t<n;t++)p=(m=(h=i[t]).layoutRect()).maxFlexSize||m[R],"center"===a?y[M]=Math.round(r[N]/2-m[P]/2):"stretch"===a?(y[P]=z(m[W]||0,r[N]-o[D]-o[O]),y[M]=o[D]):"end"===a&&(y[M]=r[N]-m[P]-o.top),0<m.flex&&(p+=m.flex*b),y[_]=p,y[C]=x,h.layoutRect(y),h.recalc&&h.recalc(),x+=p+u}else if(y.w=y.minW,y.h=y.minH,e.layoutRect(y),this.recalc(e),null===e._lastRect){var F=e.parent();F&&(F._lastRect=null,F.recalc())}}}),_i=jt.extend({Defaults:{containerClass:"flow-layout",controlClass:"flow-layout-item",endClass:"break"},recalc:function(e){e.items().filter(":visible").each(function(e){e.recalc&&e.recalc()})},isNative:function(){return!0}}),Ri=function(e,t){return n=t,r=(i=e)===undefined?_.document:i.dom(),jn(r)?D.none():D.from(r.querySelector(n)).map(vn.fromDom);var n,i,r},Ci=function(e,t){return function(){e.execCommand("mceToggleFormat",!1,t)}},Ei=function(e,t,n){var i=function(e){n(e,t)};e.formatter?e.formatter.formatChanged(t,i):e.on("init",function(){e.formatter.formatChanged(t,i)})},ki=function(e,n){return function(t){Ei(e,n,function(e){t.control.active(e)})}},Hi=function(i){var t=["alignleft","aligncenter","alignright","alignjustify"],r="alignleft",e=[{text:"Left",icon:"alignleft",onclick:Ci(i,"alignleft")},{text:"Center",icon:"aligncenter",onclick:Ci(i,"aligncenter")},{text:"Right",icon:"alignright",onclick:Ci(i,"alignright")},{text:"Justify",icon:"alignjustify",onclick:Ci(i,"alignjustify")}];i.addMenuItem("align",{text:"Align",menu:e}),i.addButton("align",{type:"menubutton",icon:r,menu:e,onShowMenu:function(e){var n=e.control.menu;w.each(t,function(t,e){n.items().eq(e).each(function(e){return e.active(i.formatter.match(t))})})},onPostRender:function(e){var n=e.control;w.each(t,function(t,e){Ei(i,t,function(e){n.icon(r),e&&n.icon(t)})})}}),w.each({alignleft:["Align left","JustifyLeft"],aligncenter:["Align center","JustifyCenter"],alignright:["Align right","JustifyRight"],alignjustify:["Justify","JustifyFull"],alignnone:["No alignment","JustifyNone"]},function(e,t){i.addButton(t,{active:!1,tooltip:e[0],cmd:e[1],onPostRender:ki(i,t)})})},Si=function(e){return e?e.split(",")[0]:""},Ti=function(l,u){return function(){var a=this;a.state.set("value",null),l.on("init nodeChange",function(e){var t,n,i,r,o=l.queryCommandValue("FontName"),s=(t=u,r=(n=o)?n.toLowerCase():"",w.each(t,function(e){e.value.toLowerCase()===r&&(i=e.value)}),w.each(t,function(e){i||Si(e.value).toLowerCase()!==Si(r).toLowerCase()||(i=e.value)}),i);a.value(s||null),!s&&o&&a.text(Si(o))})}},Mi=function(n){n.addButton("fontselect",function(){var e,t=(e=function(e){for(var t=(e=e.replace(/;$/,"").split(";")).length;t--;)e[t]=e[t].split("=");return e}(n.settings.font_formats||"Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats"),w.map(e,function(e){return{text:{raw:e[0]},value:e[1],textStyle:-1===e[1].indexOf("dings")?"font-family:"+e[1]:""}}));return{type:"listbox",text:"Font Family",tooltip:"Font Family",values:t,fixedWidth:!0,onPostRender:Ti(n,t),onselect:function(e){e.control.settings.value&&n.execCommand("FontName",!1,e.control.settings.value)}}})},Ni=function(e){Mi(e)},Pi=function(e,t){return/[0-9.]+px$/.test(e)?(n=72*parseInt(e,10)/96,i=t||0,r=Math.pow(10,i),Math.round(n*r)/r+"pt"):e;var n,i,r},Wi=function(e,t,n){var i;return w.each(e,function(e){e.value===n?i=n:e.value===t&&(i=t)}),i},Di=function(n){n.addButton("fontsizeselect",function(){var e,s,a,t=(e=n.settings.fontsize_formats||"8pt 10pt 12pt 14pt 18pt 24pt 36pt",w.map(e.split(" "),function(e){var t=e,n=e,i=e.split("=");return 1<i.length&&(t=i[0],n=i[1]),{text:t,value:n}}));return{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:t,fixedWidth:!0,onPostRender:(s=n,a=t,function(){var o=this;s.on("init nodeChange",function(e){var t,n,i,r;if(t=s.queryCommandValue("FontSize"))for(i=3;!r&&0<=i;i--)n=Pi(t,i),r=Wi(a,n,t);o.value(r||null),r||o.text(n)})}),onclick:function(e){e.control.settings.value&&n.execCommand("FontSize",!1,e.control.settings.value)}}})},Oi=function(e){Di(e)},Ai=function(n,e){var i=e.length;return w.each(e,function(e){e.menu&&(e.hidden=0===Ai(n,e.menu));var t=e.format;t&&(e.hidden=!n.formatter.canApply(t)),e.hidden&&i--}),i},Bi=function(n,e){var i=e.items().length;return e.items().each(function(e){e.menu&&e.visible(0<Bi(n,e.menu)),!e.menu&&e.settings.menu&&e.visible(0<Ai(n,e.settings.menu));var t=e.settings.format;t&&e.visible(n.formatter.canApply(t)),e.visible()||i--}),i},Li=function(e){var i,r,o,t,s,n,a,l,u=(r=0,o=[],t=[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],s=function(e){var i=[];if(e)return w.each(e,function(e){var t={text:e.title,icon:e.icon};if(e.items)t.menu=s(e.items);else{var n=e.format||"custom"+r++;e.format||(e.name=n,o.push(e)),t.format=n,t.cmd=e.cmd}i.push(t)}),i},(i=e).on("init",function(){w.each(o,function(e){i.formatter.register(e.name,e)})}),{type:"menu",items:i.settings.style_formats_merge?i.settings.style_formats?s(t.concat(i.settings.style_formats)):s(t):s(i.settings.style_formats||t),onPostRender:function(e){i.fire("renderFormatsMenu",{control:e.control})},itemDefaults:{preview:!0,textStyle:function(){if(this.settings.format)return i.formatter.getCssText(this.settings.format)},onPostRender:function(){var n=this;n.parent().on("show",function(){var e,t;(e=n.settings.format)&&(n.disabled(!i.formatter.canApply(e)),n.active(i.formatter.match(e))),(t=n.settings.cmd)&&n.active(i.queryCommandState(t))})},onclick:function(){this.settings.format&&Ci(i,this.settings.format)(),this.settings.cmd&&i.execCommand(this.settings.cmd)}}});n=u,e.addMenuItem("formats",{text:"Formats",menu:n}),l=u,(a=e).addButton("styleselect",{type:"menubutton",text:"Formats",menu:l,onShowMenu:function(){a.settings.style_formats_autohide&&Bi(a,this.menu)}})},zi=function(n,e){return function(){var r,o,s,t=[];return w.each(e,function(e){t.push({text:e[0],value:e[1],textStyle:function(){return n.formatter.getCssText(e[1])}})}),{type:"listbox",text:e[0][0],values:t,fixedWidth:!0,onselect:function(e){if(e.control){var t=e.control.value();Ci(n,t)()}},onPostRender:(r=n,o=t,function(){var t=this;r.on("nodeChange",function(e){var n=r.formatter,i=null;w.each(e.parents,function(t){if(w.each(o,function(e){if(s?n.matchNode(t,s,{value:e.value})&&(i=e.value):n.matchNode(t,e.value)&&(i=e.value),i)return!1}),i)return!1}),t.value(i)})})}}},Ii=function(e){var t,n,i=function(e){for(var t=(e=e.replace(/;$/,"").split(";")).length;t--;)e[t]=e[t].split("=");return e}(e.settings.block_formats||"Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre");e.addMenuItem("blockformats",{text:"Blocks",menu:(t=e,n=i,w.map(n,function(e){return{text:e[0],onclick:Ci(t,e[1]),textStyle:function(){return t.formatter.getCssText(e[1])}}}))}),e.addButton("formatselect",zi(e,i))},Fi=function(t,e){var n,i;if("string"==typeof e)i=e.split(" ");else if(w.isArray(e))return function(e){for(var t=[],n=0,i=e.length;n<i;++n){if(!Array.prototype.isPrototypeOf(e[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+e);te.apply(t,e[n])}return t}(w.map(e,function(e){return Fi(t,e)}));return n=w.grep(i,function(e){return"|"===e||e in t.menuItems}),w.map(n,function(e){return"|"===e?{text:"-"}:t.menuItems[e]})},Ui=function(e){return e&&"-"===e.text},Vi=function(e){var t=Z(e,function(e,t,n){return!Ui(e)||!Ui(n[t-1])});return Z(t,function(e,t,n){return!Ui(e)||0<t&&t<n.length-1})},Yi=function(e){var t,n,i,r,o=e.settings.insert_button_items;return Vi(o?Fi(e,o):(t=e,n="insert",i=[{text:"-"}],r=w.grep(t.menuItems,function(e){return e.context===n}),w.each(r,function(e){"before"===e.separator&&i.push({text:"|"}),e.prependToContext?i.unshift(e):i.push(e),"after"===e.separator&&i.push({text:"|"})}),i))},$i=function(e){var t;(t=e).addButton("insert",{type:"menubutton",icon:"insert",menu:[],oncreatemenu:function(){this.menu.add(Yi(t)),this.menu.renderNew()}})},qi=function(e){var n,i,r;n=e,w.each({bold:"Bold",italic:"Italic",underline:"Underline",strikethrough:"Strikethrough",subscript:"Subscript",superscript:"Superscript"},function(e,t){n.addButton(t,{active:!1,tooltip:e,onPostRender:ki(n,t),onclick:Ci(n,t)})}),i=e,w.each({outdent:["Decrease indent","Outdent"],indent:["Increase indent","Indent"],cut:["Cut","Cut"],copy:["Copy","Copy"],paste:["Paste","Paste"],help:["Help","mceHelp"],selectall:["Select all","SelectAll"],visualaid:["Visual aids","mceToggleVisualAid"],newdocument:["New document","mceNewDocument"],removeformat:["Clear formatting","RemoveFormat"],remove:["Remove","Delete"]},function(e,t){i.addButton(t,{tooltip:e[0],cmd:e[1]})}),r=e,w.each({blockquote:["Blockquote","mceBlockQuote"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"]},function(e,t){r.addButton(t,{active:!1,tooltip:e[0],cmd:e[1],onPostRender:ki(r,t)})})},Xi=function(e){var n;qi(e),n=e,w.each({bold:["Bold","Bold","Meta+B"],italic:["Italic","Italic","Meta+I"],underline:["Underline","Underline","Meta+U"],strikethrough:["Strikethrough","Strikethrough"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"],removeformat:["Clear formatting","RemoveFormat"],newdocument:["New document","mceNewDocument"],cut:["Cut","Cut","Meta+X"],copy:["Copy","Copy","Meta+C"],paste:["Paste","Paste","Meta+V"],selectall:["Select all","SelectAll","Meta+A"]},function(e,t){n.addMenuItem(t,{text:e[0],icon:t,shortcut:e[2],cmd:e[1]})}),n.addMenuItem("codeformat",{text:"Code",icon:"code",onclick:Ci(n,"code")})},ji=function(n,i){return function(){var e=this,t=function(){var e="redo"===i?"hasRedo":"hasUndo";return!!n.undoManager&&n.undoManager[e]()};e.disabled(!t()),n.on("Undo Redo AddUndo TypingUndo ClearUndos SwitchMode",function(){e.disabled(n.readonly||!t())})}},Ji=function(e){var t,n;(t=e).addMenuItem("undo",{text:"Undo",icon:"undo",shortcut:"Meta+Z",onPostRender:ji(t,"undo"),cmd:"undo"}),t.addMenuItem("redo",{text:"Redo",icon:"redo",shortcut:"Meta+Y",onPostRender:ji(t,"redo"),cmd:"redo"}),(n=e).addButton("undo",{tooltip:"Undo",onPostRender:ji(n,"undo"),cmd:"undo"}),n.addButton("redo",{tooltip:"Redo",onPostRender:ji(n,"redo"),cmd:"redo"})},Gi=function(e){var t,n;(t=e).addMenuItem("visualaid",{text:"Visual aids",selectable:!0,onPostRender:(n=t,function(){var t=this;n.on("VisualAid",function(e){t.active(e.hasVisual)}),t.active(n.hasVisual)}),cmd:"mceToggleVisualAid"})},Ki={setup:function(e){var t;e.rtl&&(st.rtl=!0),e.on("mousedown progressstate",function(){kt.hideAll()}),(t=e).settings.ui_container&&(fe.container=Ri(vn.fromDom(_.document.body),t.settings.ui_container).fold(H(null),function(e){return e.dom()})),Wt.tooltips=!fe.iOS,st.translate=function(e){return h.translate(e)},Ii(e),Hi(e),Xi(e),Ji(e),Oi(e),Ni(e),Li(e),Gi(e),$i(e)}},Zi=Jt.extend({recalc:function(e){var t,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,b,y,x,w,_,R,C,E,k,H,S,T=[],M=[];t=e.settings,r=e.items().filter(":visible"),o=e.layoutRect(),i=t.columns||Math.ceil(Math.sqrt(r.length)),n=Math.ceil(r.length/i),b=t.spacingH||t.spacing||0,y=t.spacingV||t.spacing||0,x=t.alignH||t.align,w=t.alignV||t.align,p=e.paddingBox,S="reverseRows"in t?t.reverseRows:e.isRtl(),x&&"string"==typeof x&&(x=[x]),w&&"string"==typeof w&&(w=[w]);for(d=0;d<i;d++)T.push(0);for(f=0;f<n;f++)M.push(0);for(f=0;f<n;f++)for(d=0;d<i&&(c=r[f*i+d]);d++)C=(u=c.layoutRect()).minW,E=u.minH,T[d]=C>T[d]?C:T[d],M[f]=E>M[f]?E:M[f];for(k=o.innerW-p.left-p.right,d=_=0;d<i;d++)_+=T[d]+(0<d?b:0),k-=(0<d?b:0)+T[d];for(H=o.innerH-p.top-p.bottom,f=R=0;f<n;f++)R+=M[f]+(0<f?y:0),H-=(0<f?y:0)+M[f];if(_+=p.left+p.right,R+=p.top+p.bottom,(l={}).minW=_+(o.w-o.innerW),l.minH=R+(o.h-o.innerH),l.contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH,l.minW=Math.min(l.minW,o.maxW),l.minH=Math.min(l.minH,o.maxH),l.minW=Math.max(l.minW,o.startMinWidth),l.minH=Math.max(l.minH,o.startMinHeight),!o.autoResize||l.minW===o.minW&&l.minH===o.minH){var N;o.autoResize&&((l=e.layoutRect(l)).contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH),N="start"===t.packV?0:0<H?Math.floor(H/n):0;var P=0,W=t.flexWidths;if(W)for(d=0;d<W.length;d++)P+=W[d];else P=i;var D=k/P;for(d=0;d<i;d++)T[d]+=W?W[d]*D:D;for(m=p.top,f=0;f<n;f++){for(h=p.left,a=M[f]+N,d=0;d<i&&(c=r[S?f*i+i-1-d:f*i+d]);d++)g=c.settings,u=c.layoutRect(),s=Math.max(T[d],u.startMinWidth),u.x=h,u.y=m,"center"===(v=g.alignH||(x?x[d]||x[0]:null))?u.x=h+s/2-u.w/2:"right"===v?u.x=h+s-u.w:"stretch"===v&&(u.w=s),"center"===(v=g.alignV||(w?w[d]||w[0]:null))?u.y=m+a/2-u.h/2:"bottom"===v?u.y=m+a-u.h:"stretch"===v&&(u.h=a),c.layoutRect(u),h+=s+b,c.recalc&&c.recalc();m+=a+y}}else if(l.w=l.minW,l.h=l.minH,e.layoutRect(l),this.recalc(e),null===e._lastRect){var O=e.parent();O&&(O._lastRect=null,O.recalc())}}}),Qi=Wt.extend({renderHtml:function(){var e=this;return e.classes.add("iframe"),e.canFocus=!1,'<iframe id="'+e._id+'" class="'+e.classes+'" tabindex="-1" src="'+(e.settings.url||"javascript:''")+'" frameborder="0"></iframe>'},src:function(e){this.getEl().src=e},html:function(e,t){var n=this,i=this.getEl().contentWindow.document.body;return i?(i.innerHTML=e,t&&t()):u.setTimeout(function(){n.html(e)}),this}}),er=Wt.extend({init:function(e){this._super(e),this.classes.add("widget").add("infobox"),this.canFocus=!1},severity:function(e){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(e)},help:function(e){this.state.set("help",e)},renderHtml:function(){var e=this,t=e.classPrefix;return'<div id="'+e._id+'" class="'+e.classes+'"><div id="'+e._id+'-body">'+e.encode(e.state.get("text"))+'<button role="button" tabindex="-1"><i class="'+t+"ico "+t+'i-help"></i></button></div></div>'},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.getEl("body").firstChild.data=t.encode(e.value),t.state.get("rendered")&&t.updateLayoutRect()}),t.state.on("change:help",function(e){t.classes.toggle("has-help",e.value),t.state.get("rendered")&&t.updateLayoutRect()}),t._super()}}),tr=Wt.extend({init:function(e){var t=this;t._super(e),t.classes.add("widget").add("label"),t.canFocus=!1,e.multiline&&t.classes.add("autoscroll"),e.strong&&t.classes.add("strong")},initLayoutRect:function(){var e=this,t=e._super();return e.settings.multiline&&(Re.getSize(e.getEl()).width>t.maxW&&(t.minW=t.maxW,e.classes.add("multiline")),e.getEl().style.width=t.minW+"px",t.startMinH=t.h=t.minH=Math.min(t.maxH,Re.getSize(e.getEl()).height)),t},repaint:function(){return this.settings.multiline||(this.getEl().style.lineHeight=this.layoutRect().h+"px"),this._super()},severity:function(e){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(e)},renderHtml:function(){var e,t,n=this,i=n.settings.forId,r=n.settings.html?n.settings.html:n.encode(n.state.get("text"));return!i&&(t=n.settings.forName)&&(e=n.getRoot().find("#"+t)[0])&&(i=e._id),i?'<label id="'+n._id+'" class="'+n.classes+'"'+(i?' for="'+i+'"':"")+">"+r+"</label>":'<span id="'+n._id+'" class="'+n.classes+'">'+r+"</span>"},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.innerHtml(t.encode(e.value)),t.state.get("rendered")&&t.updateLayoutRect()}),t._super()}}),nr=ct.extend({Defaults:{role:"toolbar",layout:"flow"},init:function(e){this._super(e),this.classes.add("toolbar")},postRender:function(){return this.items().each(function(e){e.classes.add("toolbar-item")}),this._super()}}),ir=nr.extend({Defaults:{role:"menubar",containerCls:"menubar",ariaRoot:!0,defaults:{type:"menubutton"}}}),rr=Gt.extend({init:function(e){var t=this;t._renderOpen=!0,t._super(e),e=t.settings,t.classes.add("menubtn"),e.fixedWidth&&t.classes.add("fixed-width"),t.aria("haspopup",!0),t.state.set("menu",e.menu||t.render())},showMenu:function(e){var t,n=this;if(n.menu&&n.menu.visible()&&!1!==e)return n.hideMenu();n.menu||(t=n.state.get("menu")||[],n.classes.add("opened"),t.length?t={type:"menu",animate:!0,items:t}:(t.type=t.type||"menu",t.animate=!0),t.renderTo?n.menu=t.parent(n).show().renderTo():n.menu=b.create(t).parent(n).renderTo(),n.fire("createmenu"),n.menu.reflow(),n.menu.on("cancel",function(e){e.control.parent()===n.menu&&(e.stopPropagation(),n.focus(),n.hideMenu())}),n.menu.on("select",function(){n.focus()}),n.menu.on("show hide",function(e){"hide"===e.type&&e.control.parent()===n&&n.classes.remove("opened-under"),e.control===n.menu&&(n.activeMenu("show"===e.type),n.classes.toggle("opened","show"===e.type)),n.aria("expanded","show"===e.type)}).fire("show")),n.menu.show(),n.menu.layoutRect({w:n.layoutRect().w}),n.menu.repaint(),n.menu.moveRel(n.getEl(),n.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"]);var i=n.menu.layoutRect(),r=n.$el.offset().top+n.layoutRect().h;r>i.y&&r<i.y+i.h&&n.classes.add("opened-under"),n.fire("showmenu")},hideMenu:function(){this.menu&&(this.menu.items().each(function(e){e.hideMenu&&e.hideMenu()}),this.menu.hide())},activeMenu:function(e){this.classes.toggle("active",e)},renderHtml:function(){var e,t=this,n=t._id,i=t.classPrefix,r=t.settings.icon,o=t.state.get("text"),s="";return(e=t.settings.image)?(r="none","string"!=typeof e&&(e=_.window.getSelection?e[0]:e[1]),e=" style=\"background-image: url('"+e+"')\""):e="",o&&(t.classes.add("btn-has-text"),s='<span class="'+i+'txt">'+t.encode(o)+"</span>"),r=t.settings.icon?i+"ico "+i+"i-"+r:"",t.aria("role",t.parent()instanceof ir?"menuitem":"button"),'<div id="'+n+'" class="'+t.classes+'" tabindex="-1" aria-labelledby="'+n+'"><button id="'+n+'-open" role="presentation" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+e+"></i>":"")+s+' <i class="'+i+'caret"></i></button></div>'},postRender:function(){var r=this;return r.on("click",function(e){e.control===r&&function(e,t){for(;e;){if(t===e)return!0;e=e.parentNode}return!1}(e.target,r.getEl())&&(r.focus(),r.showMenu(!e.aria),e.aria&&r.menu.items().filter(":visible")[0].focus())}),r.on("mouseenter",function(e){var t,n=e.control,i=r.parent();n&&i&&n instanceof rr&&n.parent()===i&&(i.items().filter("MenuButton").each(function(e){e.hideMenu&&e!==n&&(e.menu&&e.menu.visible()&&(t=!0),e.hideMenu())}),t&&(n.focus(),n.showMenu()))}),r._super()},bindStates:function(){var e=this;return e.state.on("change:menu",function(){e.menu&&e.menu.remove(),e.menu=null}),e._super()},remove:function(){this._super(),this.menu&&this.menu.remove()}}),or=kt.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(e){if(e.autohide=!0,e.constrainToViewport=!0,"function"==typeof e.items&&(e.itemsFactory=e.items,e.items=[]),e.itemDefaults)for(var t=e.items,n=t.length;n--;)t[n]=w.extend({},e.itemDefaults,t[n]);this._super(e),this.classes.add("menu"),e.animate&&11!==fe.ie&&this.classes.add("animate")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){this.hideAll(),this.fire("select")},load:function(){var t,n=this;function i(){n.throbber&&(n.throbber.hide(),n.throbber=null)}n.settings.itemsFactory&&(n.throbber||(n.throbber=new Tt(n.getEl("body"),!0),0===n.items().length?(n.throbber.show(),n.fire("loading")):n.throbber.show(100,function(){n.items().remove(),n.fire("loading")}),n.on("hide close",i)),n.requestTime=t=(new Date).getTime(),n.settings.itemsFactory(function(e){0!==e.length?n.requestTime===t&&(n.getEl().style.width="",n.getEl("body").style.width="",i(),n.items().remove(),n.getEl("body").innerHTML="",n.add(e),n.renderNew(),n.fire("loaded")):n.hide()}))},hideAll:function(){return this.find("menuitem").exec("hideMenu"),this._super()},preRender:function(){var n=this;return n.items().each(function(e){var t=e.settings;if(t.icon||t.image||t.selectable)return!(n._hasIcons=!0)}),n.settings.itemsFactory&&n.on("postrender",function(){n.settings.itemsFactory&&n.load()}),n.on("show hide",function(e){e.control===n&&("show"===e.type?u.setTimeout(function(){n.classes.add("in")},0):n.classes.remove("in"))}),n._super()}}),sr=rr.extend({init:function(i){var t,r,o,n,s=this;s._super(i),i=s.settings,s._values=t=i.values,t&&("undefined"!=typeof i.value&&function e(t){for(var n=0;n<t.length;n++){if(r=t[n].selected||i.value===t[n].value)return o=o||t[n].text,s.state.set("value",t[n].value),!0;if(t[n].menu&&e(t[n].menu))return!0}}(t),!r&&0<t.length&&(o=t[0].text,s.state.set("value",t[0].value)),s.state.set("menu",t)),s.state.set("text",i.text||o),s.classes.add("listbox"),s.on("select",function(e){var t=e.control;n&&(e.lastControl=n),i.multiple?t.active(!t.active()):s.value(e.control.value()),n=t})},value:function(n){return 0===arguments.length?this.state.get("value"):(void 0===n||(this.settings.values&&!function t(e){return J(e,function(e){return e.menu?t(e.menu):e.value===n})}(this.settings.values)?null===n&&this.state.set("value",null):this.state.set("value",n)),this)},bindStates:function(){var i=this;return i.on("show",function(e){var t,n;t=e.control,n=i.value(),t instanceof or&&t.items().each(function(e){e.hasMenus()||e.active(e.value()===n)})}),i.state.on("change:value",function(t){var n=function e(t,n){var i;if(t)for(var r=0;r<t.length;r++){if(t[r].value===n)return t[r];if(t[r].menu&&(i=e(t[r].menu,n)))return i}}(i.state.get("menu"),t.value);n?i.text(n.text):i.text(i.settings.text)}),i._super()}}),ar=Wt.extend({Defaults:{border:0,role:"menuitem"},init:function(e){var t,n=this;n._super(e),e=n.settings,n.classes.add("menu-item"),e.menu&&n.classes.add("menu-item-expand"),e.preview&&n.classes.add("menu-item-preview"),"-"!==(t=n.state.get("text"))&&"|"!==t||(n.classes.add("menu-item-sep"),n.aria("role","separator"),n.state.set("text","-")),e.selectable&&(n.aria("role","menuitemcheckbox"),n.classes.add("menu-item-checkbox"),e.icon="selected"),e.preview||e.selectable||n.classes.add("menu-item-normal"),n.on("mousedown",function(e){e.preventDefault()}),e.menu&&!e.ariaHideMenu&&n.aria("haspopup",!0)},hasMenus:function(){return!!this.settings.menu},showMenu:function(){var t,n=this,e=n.settings,i=n.parent();if(i.items().each(function(e){e!==n&&e.hideMenu()}),e.menu){(t=n.menu)?t.show():((t=e.menu).length?t={type:"menu",items:t}:t.type=t.type||"menu",i.settings.itemDefaults&&(t.itemDefaults=i.settings.itemDefaults),(t=n.menu=b.create(t).parent(n).renderTo()).reflow(),t.on("cancel",function(e){e.stopPropagation(),n.focus(),t.hide()}),t.on("show hide",function(e){e.control.items&&e.control.items().each(function(e){e.active(e.settings.selected)})}).fire("show"),t.on("hide",function(e){e.control===t&&n.classes.remove("selected")}),t.submenu=!0),t._parentMenu=i,t.classes.add("menu-sub");var r=t.testMoveRel(n.getEl(),n.isRtl()?["tl-tr","bl-br","tr-tl","br-bl"]:["tr-tl","br-bl","tl-tr","bl-br"]);t.moveRel(n.getEl(),r),r="menu-sub-"+(t.rel=r),t.classes.remove(t._lastRel).add(r),t._lastRel=r,n.classes.add("selected"),n.aria("expanded",!0)}},hideMenu:function(){var e=this;return e.menu&&(e.menu.items().each(function(e){e.hideMenu&&e.hideMenu()}),e.menu.hide(),e.aria("expanded",!1)),e},renderHtml:function(){var e,t=this,n=t._id,i=t.settings,r=t.classPrefix,o=t.state.get("text"),s=t.settings.icon,a="",l=i.shortcut,u=t.encode(i.url);function c(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function d(e){var t=i.match||"";return t?e.replace(new RegExp(c(t),"gi"),function(e){return"!mce~match["+e+"]mce~match!"}):e}function f(e){return e.replace(new RegExp(c("!mce~match["),"g"),"<b>").replace(new RegExp(c("]mce~match!"),"g"),"</b>")}return s&&t.parent().classes.add("menu-has-icons"),i.image&&(a=" style=\"background-image: url('"+i.image+"')\""),l&&(l=function(e){var t,n,i={};for(i=fe.mac?{alt:"⌥",ctrl:"⌘",shift:"⇧",meta:"⌘"}:{meta:"Ctrl"},e=e.split("+"),t=0;t<e.length;t++)(n=i[e[t].toLowerCase()])&&(e[t]=n);return e.join("+")}(l)),s=r+"ico "+r+"i-"+(t.settings.icon||"none"),e="-"!==o?'<i class="'+s+'"'+a+"></i>\xa0":"",o=f(t.encode(d(o))),u=f(t.encode(d(u))),'<div id="'+n+'" class="'+t.classes+'" tabindex="-1">'+e+("-"!==o?'<span id="'+n+'-text" class="'+r+'text">'+o+"</span>":"")+(l?'<div id="'+n+'-shortcut" class="'+r+'menu-shortcut">'+l+"</div>":"")+(i.menu?'<div class="'+r+'caret"></div>':"")+(u?'<div class="'+r+'menu-item-link">'+u+"</div>":"")+"</div>"},postRender:function(){var t=this,n=t.settings,e=n.textStyle;if("function"==typeof e&&(e=e.call(this)),e){var i=t.getEl("text");i&&(i.setAttribute("style",e),t._textStyle=e)}return t.on("mouseenter click",function(e){e.control===t&&(n.menu||"click"!==e.type?(t.showMenu(),e.aria&&t.menu.focus(!0)):(t.fire("select"),u.requestAnimationFrame(function(){t.parent().hideAll()})))}),t._super(),t},hover:function(){return this.parent().items().each(function(e){e.classes.remove("selected")}),this.classes.toggle("selected",!0),this},active:function(e){return function(e,t){var n=e._textStyle;if(n){var i=e.getEl("text");i.setAttribute("style",n),t&&(i.style.color="",i.style.backgroundColor="")}}(this,e),void 0!==e&&this.aria("checked",e),this._super(e)},remove:function(){this._super(),this.menu&&this.menu.remove()}}),lr=Qt.extend({Defaults:{classes:"radio",role:"radio"}}),ur=Wt.extend({renderHtml:function(){var e=this,t=e.classPrefix;return e.classes.add("resizehandle"),"both"===e.settings.direction&&e.classes.add("resizehandle-both"),e.canFocus=!1,'<div id="'+e._id+'" class="'+e.classes+'"><i class="'+t+"ico "+t+'i-resize"></i></div>'},postRender:function(){var t=this;t._super(),t.resizeDragHelper=new ft(this._id,{start:function(){t.fire("ResizeStart")},drag:function(e){"both"!==t.settings.direction&&(e.deltaX=0),t.fire("Resize",e)},stop:function(){t.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}});function cr(e){var t="";if(e)for(var n=0;n<e.length;n++)t+='<option value="'+e[n]+'">'+e[n]+"</option>";return t}var dr=Wt.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(e){var n=this;n._super(e),n.settings.size&&(n.size=n.settings.size),n.settings.options&&(n._options=n.settings.options),n.on("keydown",function(e){var t;13===e.keyCode&&(e.preventDefault(),n.parents().reverse().each(function(e){if(e.toJSON)return t=e,!1}),n.fire("submit",{data:t.toJSON()}))})},options:function(e){return arguments.length?(this.state.set("options",e),this):this.state.get("options")},renderHtml:function(){var e,t=this,n="";return e=cr(t._options),t.size&&(n=' size = "'+t.size+'"'),'<select id="'+t._id+'" class="'+t.classes+'"'+n+">"+e+"</select>"},bindStates:function(){var t=this;return t.state.on("change:options",function(e){t.getEl().innerHTML=cr(e.value)}),t._super()}});function fr(e,t,n){return e<t&&(e=t),n<e&&(e=n),e}function hr(e,t,n){e.setAttribute("aria-"+t,n)}function mr(e,t){var n,i,r,o,s;"v"===e.settings.orientation?(r="top",i="height",n="h"):(r="left",i="width",n="w"),s=e.getEl("handle"),o=((e.layoutRect()[n]||100)-Re.getSize(s)[i])*((t-e._minValue)/(e._maxValue-e._minValue))+"px",s.style[r]=o,s.style.height=e.layoutRect().h+"px",hr(s,"valuenow",t),hr(s,"valuetext",""+e.settings.previewFilter(t)),hr(s,"valuemin",e._minValue),hr(s,"valuemax",e._maxValue)}var gr=Wt.extend({init:function(e){var t=this;e.previewFilter||(e.previewFilter=function(e){return Math.round(100*e)/100}),t._super(e),t.classes.add("slider"),"v"===e.orientation&&t.classes.add("vertical"),t._minValue=X(e.minValue)?e.minValue:0,t._maxValue=X(e.maxValue)?e.maxValue:100,t._initValue=t.state.get("value")},renderHtml:function(){var e=this._id,t=this.classPrefix;return'<div id="'+e+'" class="'+this.classes+'"><div id="'+e+'-handle" class="'+t+'slider-handle" role="slider" tabindex="-1"></div></div>'},reset:function(){this.value(this._initValue).repaint()},postRender:function(){var e,t,n,i,r,o,s,a,l,u,c,d,f,h,m=this;e=m._minValue,t=m._maxValue,"v"===m.settings.orientation?(n="screenY",i="top",r="height",o="h"):(n="screenX",i="left",r="width",o="w"),m._super(),function(o,s){function t(e){var t,n,i,r;t=fr(t=(((t=m.value())+(r=n=o))/((i=s)-r)+.05*e)*(i-n)-n,o,s),m.value(t),m.fire("dragstart",{value:t}),m.fire("drag",{value:t}),m.fire("dragend",{value:t})}m.on("keydown",function(e){switch(e.keyCode){case 37:case 38:t(-1);break;case 39:case 40:t(1)}})}(e,t),s=e,a=t,l=m.getEl("handle"),m._dragHelper=new ft(m._id,{handle:m._id+"-handle",start:function(e){u=e[n],c=parseInt(m.getEl("handle").style[i],10),d=(m.layoutRect()[o]||100)-Re.getSize(l)[r],m.fire("dragstart",{value:h})},drag:function(e){var t=e[n]-u;f=fr(c+t,0,d),l.style[i]=f+"px",h=s+f/d*(a-s),m.value(h),m.tooltip().text(""+m.settings.previewFilter(h)).show().moveRel(l,"bc tc"),m.fire("drag",{value:h})},stop:function(){m.tooltip().hide(),m.fire("dragend",{value:h})}})},repaint:function(){this._super(),mr(this,this.value())},bindStates:function(){var t=this;return t.state.on("change:value",function(e){mr(t,e.value)}),t._super()}}),pr=Wt.extend({renderHtml:function(){return this.classes.add("spacer"),this.canFocus=!1,'<div id="'+this._id+'" class="'+this.classes+'"></div>'}}),vr=rr.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var e,t,n=this.getEl(),i=this.layoutRect();return this._super(),e=n.firstChild,t=n.lastChild,we(e).css({width:i.w-Re.getSize(t).width,height:i.h-2}),we(t).css({height:i.h-2}),this},activeMenu:function(e){we(this.getEl().lastChild).toggleClass(this.classPrefix+"active",e)},renderHtml:function(){var e,t,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a=n.settings,l="";return(e=a.image)?(o="none","string"!=typeof e&&(e=_.window.getSelection?e[0]:e[1]),e=" style=\"background-image: url('"+e+"')\""):e="",o=a.icon?r+"ico "+r+"i-"+o:"",s&&(n.classes.add("btn-has-text"),l='<span class="'+r+'txt">'+n.encode(s)+"</span>"),t="boolean"==typeof a.active?' aria-pressed="'+a.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" role="button"'+t+' tabindex="-1"><button type="button" hidefocus="1" tabindex="-1">'+(o?'<i class="'+o+'"'+e+"></i>":"")+l+'</button><button type="button" class="'+r+'open" hidefocus="1" tabindex="-1">'+(n._menuBtnText?(o?"\xa0":"")+n._menuBtnText:"")+' <i class="'+r+'caret"></i></button></div>'},postRender:function(){var n=this.settings.onclick;return this.on("click",function(e){var t=e.target;if(e.control===this)for(;t;){if(e.aria&&"down"!==e.aria.key||"BUTTON"===t.nodeName&&-1===t.className.indexOf("open"))return e.stopImmediatePropagation(),void(n&&n.call(this,e));t=t.parentNode}}),delete this.settings.onclick,this._super()}}),br=_i.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}}),yr=bt.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(n){var e;this.activeTabId&&(e=this.getEl(this.activeTabId),we(e).removeClass(this.classPrefix+"active"),e.setAttribute("aria-selected","false")),this.activeTabId="t"+n,(e=this.getEl("t"+n)).setAttribute("aria-selected","true"),we(e).addClass(this.classPrefix+"active"),this.items()[n].show().fire("showtab"),this.reflow(),this.items().each(function(e,t){n!==t&&e.hide()})},renderHtml:function(){var i=this,e=i._layout,r="",o=i.classPrefix;return i.preRender(),e.preRender(i),i.items().each(function(e,t){var n=i._id+"-t"+t;e.aria("role","tabpanel"),e.aria("labelledby",n),r+='<div id="'+n+'" class="'+o+'tab" unselectable="on" role="tab" aria-controls="'+e._id+'" aria-selected="false" tabIndex="-1">'+i.encode(e.settings.title)+"</div>"}),'<div id="'+i._id+'" class="'+i.classes+'" hidefocus="1" tabindex="-1"><div id="'+i._id+'-head" class="'+o+'tabs" role="tablist">'+r+'</div><div id="'+i._id+'-body" class="'+i.bodyClasses+'">'+e.renderHtml(i)+"</div></div>"},postRender:function(){var i=this;i._super(),i.settings.activeTab=i.settings.activeTab||0,i.activateTab(i.settings.activeTab),this.on("click",function(e){var t=e.target.parentNode;if(t&&t.id===i._id+"-head")for(var n=t.childNodes.length;n--;)t.childNodes[n]===e.target&&i.activateTab(n)})},initLayoutRect:function(){var e,t,n,i=this;t=(t=Re.getSize(i.getEl("head")).width)<0?0:t,n=0,i.items().each(function(e){t=Math.max(t,e.layoutRect().minW),n=Math.max(n,e.layoutRect().minH)}),i.items().each(function(e){e.settings.x=0,e.settings.y=0,e.settings.w=t,e.settings.h=n,e.layoutRect({x:0,y:0,w:t,h:n})});var r=Re.getSize(i.getEl("head")).height;return i.settings.minWidth=t,i.settings.minHeight=n+r,(e=i._super()).deltaH+=r,e.innerH=e.h-e.deltaH,e}}),xr=Wt.extend({init:function(e){var n=this;n._super(e),n.classes.add("textbox"),e.multiline?n.classes.add("multiline"):(n.on("keydown",function(e){var t;13===e.keyCode&&(e.preventDefault(),n.parents().reverse().each(function(e){if(e.toJSON)return t=e,!1}),n.fire("submit",{data:t.toJSON()}))}),n.on("keyup",function(e){n.state.set("value",e.target.value)}))},repaint:function(){var e,t,n,i,r,o=this,s=0;e=o.getEl().style,t=o._layoutRect,r=o._lastRepaintRect||{};var a=_.document;return!o.settings.multiline&&a.all&&(!a.documentMode||a.documentMode<=8)&&(e.lineHeight=t.h-s+"px"),i=(n=o.borderBox).left+n.right+8,s=n.top+n.bottom+(o.settings.multiline?8:0),t.x!==r.x&&(e.left=t.x+"px",r.x=t.x),t.y!==r.y&&(e.top=t.y+"px",r.y=t.y),t.w!==r.w&&(e.width=t.w-i+"px",r.w=t.w),t.h!==r.h&&(e.height=t.h-s+"px",r.h=t.h),o._lastRepaintRect=r,o.fire("repaint",{},!1),o},renderHtml:function(){var t,e,n=this,i=n.settings;return t={id:n._id,hidefocus:"1"},w.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(e){t[e]=i[e]}),n.disabled()&&(t.disabled="disabled"),i.subtype&&(t.type=i.subtype),(e=Re.create(i.multiline?"textarea":"input",t)).value=n.state.get("value"),e.className=n.classes.toString(),e.outerHTML},value:function(e){return arguments.length?(this.state.set("value",e),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var t=this;t.getEl().value=t.state.get("value"),t._super(),t.$el.on("change",function(e){t.state.set("value",e.target.value),t.fire("change",e)})},bindStates:function(){var t=this;return t.state.on("change:value",function(e){t.getEl().value!==e.value&&(t.getEl().value=e.value)}),t.state.on("change:disabled",function(e){t.getEl().disabled=e.value}),t._super()},remove:function(){this.$el.off(),this._super()}}),wr=function(){return{Selector:Ue,Collection:$e,ReflowQueue:Qe,Control:st,Factory:b,KeyboardNavigation:lt,Container:ct,DragHelper:ft,Scrollable:vt,Panel:bt,Movable:Te,Resizable:yt,FloatPanel:kt,Window:Ut,MessageBox:qt,Tooltip:Pt,Widget:Wt,Progress:Dt,Notification:At,Layout:jt,AbsoluteLayout:Jt,Button:Gt,ButtonGroup:Zt,Checkbox:Qt,ComboBox:tn,ColorBox:nn,PanelButton:rn,ColorButton:sn,ColorPicker:ln,Path:cn,ElementPath:dn,FormItem:fn,Form:hn,FieldSet:mn,FilePicker:yi,FitLayout:xi,FlexLayout:wi,FlowLayout:_i,FormatControls:Ki,GridLayout:Zi,Iframe:Qi,InfoBox:er,Label:tr,Toolbar:nr,MenuBar:ir,MenuButton:rr,MenuItem:ar,Throbber:Tt,Menu:or,ListBox:sr,Radio:lr,ResizeHandle:ur,SelectBox:dr,Slider:gr,Spacer:pr,SplitButton:vr,StackLayout:br,TabPanel:yr,TextBox:xr,DropZone:un,BrowseButton:Kt}},_r=function(n){n.ui?w.each(wr(),function(e,t){n.ui[t]=e}):n.ui=wr()};w.each(wr(),function(e,t){b.add(t,e)}),_r(window.tinymce?window.tinymce:{}),o.add("modern",function(e){return Ki.setup(e),Xt(e)})}(window); \ No newline at end of file +!function(_){"use strict";var e,t,n,i,r=tinymce.util.Tools.resolve("tinymce.ThemeManager"),l=tinymce.util.Tools.resolve("tinymce.EditorManager"),w=tinymce.util.Tools.resolve("tinymce.util.Tools"),d=function(e){return!1!==c(e)},c=function(e){return e.getParam("menubar")},f=function(e){return e.getParam("toolbar_items_size")},h=function(e){return e.getParam("menu")},m=function(e){return!1===e.settings.skin},g=function(e){var t=e.getParam("resize","vertical");return!1===t?"none":"both"===t?"both":"vertical"},p=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),v=tinymce.util.Tools.resolve("tinymce.ui.Factory"),b=tinymce.util.Tools.resolve("tinymce.util.I18n"),o=function(e){return e.fire("SkinLoaded")},y=function(e){return e.fire("ResizeEditor")},x=function(e){return e.fire("BeforeRenderUI")},s=function(t,n){return function(){var e=t.find(n)[0];e&&e.focus(!0)}},R=function(e,t){e.shortcuts.add("Alt+F9","",s(t,"menubar")),e.shortcuts.add("Alt+F10,F10","",s(t,"toolbar")),e.shortcuts.add("Alt+F11","",s(t,"elementpath")),t.on("cancel",function(){e.focus()})},C=tinymce.util.Tools.resolve("tinymce.geom.Rect"),u=tinymce.util.Tools.resolve("tinymce.util.Delay"),E=function(){},k=function(e){return function(){return e}},a=k(!1),H=k(!0),S=function(){return T},T=(e=function(e){return e.isNone()},i={fold:function(e,t){return e()},is:a,isSome:a,isNone:H,getOr:n=function(e){return e},getOrThunk:t=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:k(null),getOrUndefined:k(undefined),or:n,orThunk:t,map:S,each:E,bind:S,exists:a,forall:H,filter:S,equals:e,equals_:e,toArray:function(){return[]},toString:k("none()")},Object.freeze&&Object.freeze(i),i),M=function(n){var e=k(n),t=function(){return r},i=function(e){return e(n)},r={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:H,isNone:a,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return M(e(n))},each:function(e){e(n)},bind:i,exists:i,forall:i,filter:function(e){return e(n)?r:T},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(a,function(e){return t(n,e)})}};return r},N={some:M,none:S,from:function(e){return null===e||e===undefined?T:M(e)}},P=function(e){return e?e.getRoot().uiContainer:null},W={getUiContainerDelta:function(e){var t=P(e);if(t&&"static"!==p.DOM.getStyle(t,"position",!0)){var n=p.DOM.getPos(t),i=t.scrollLeft-n.x,r=t.scrollTop-n.y;return N.some({x:i,y:r})}return N.none()},setUiContainer:function(e,t){var n=p.DOM.select(e.settings.ui_container)[0];t.getRoot().uiContainer=n},getUiContainer:P,inheritUiContainer:function(e,t){return t.uiContainer=P(e)}},D=function(i,e,r){var o,s=[];if(e)return w.each(e.split(/[ ,]/),function(t){var e,n=function(){var e=i.selection;t.settings.stateSelector&&e.selectorChanged(t.settings.stateSelector,function(e){t.active(e)},!0),t.settings.disabledStateSelector&&e.selectorChanged(t.settings.disabledStateSelector,function(e){t.disabled(e)})};"|"===t?o=null:(o||(o={type:"buttongroup",items:[]},s.push(o)),i.buttons[t]&&(e=t,"function"==typeof(t=i.buttons[e])&&(t=t()),t.type=t.type||"button",t.size=r,t=v.create(t),o.items.push(t),i.initialized?n():i.on("init",n)))}),{type:"toolbar",layout:"flow",items:s}},O=D,A=function(n,i){var e,t,r=[];if(w.each(!1===(t=(e=n).getParam("toolbar"))?[]:w.isArray(t)?w.grep(t,function(e){return 0<e.length}):function(e,t){for(var n=[],i=1;i<10;i++){var r=e["toolbar"+i];if(!r)break;n.push(r)}var o=e.toolbar?[e.toolbar]:[t];return 0<n.length?n:o}(e.settings,"undo redo | styleselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image"),function(e){var t;(t=e)&&r.push(D(n,t,i))}),r.length)return{type:"panel",layout:"stack",classes:"toolbar-grp",ariaRoot:!0,ariaRemember:!0,items:r}},B=p.DOM,L=function(e){return{left:e.x,top:e.y,width:e.w,height:e.h,right:e.x+e.w,bottom:e.y+e.h}},z=function(e,t){e.moveTo(t.left,t.top)},I=function(e,t,n,i,r,o){return o=L({x:t,y:n,w:o.w,h:o.h}),e&&(o=e({elementRect:L(i),contentAreaRect:L(r),panelRect:o})),o},F=function(x){var i,o=function(){return x.contextToolbars||[]},n=function(e,t){var n,i,r,o,s,a,l,u=x.getParam("inline_toolbar_position_handler");if(!x.removed){if(!e||!e.toolbar.panel)return c=x,void w.each(c.contextToolbars,function(e){e.panel&&e.panel.hide()});var c,d,f,h,m;l=["bc-tc","tc-bc","tl-bl","bl-tl","tr-br","br-tr"],s=e.toolbar.panel,t&&s.show(),d=e.element,f=B.getPos(x.getContentAreaContainer()),h=x.dom.getRect(d),"BODY"===(m=x.dom.getRoot()).nodeName&&(h.x-=m.ownerDocument.documentElement.scrollLeft||m.scrollLeft,h.y-=m.ownerDocument.documentElement.scrollTop||m.scrollTop),h.x+=f.x,h.y+=f.y,r=h,i=B.getRect(s.getEl()),o=B.getRect(x.getContentAreaContainer()||x.getBody());var g,p,v,b=W.getUiContainerDelta(s).getOr({x:0,y:0});if(r.x+=b.x,r.y+=b.y,i.x+=b.x,i.y+=b.y,o.x+=b.x,o.y+=b.y,"inline"!==B.getStyle(e.element,"display",!0)){var y=e.element.getBoundingClientRect();r.w=y.width,r.h=y.height}x.inline||(o.w=x.getDoc().documentElement.offsetWidth),x.selection.controlSelection.isResizable(e.element)&&r.w<25&&(r=C.inflate(r,0,8)),n=C.findBestRelativePosition(i,r,o,l),r=C.clamp(r,o),n?(a=C.relativePosition(i,r,n),z(s,I(u,a.x,a.y,r,o,i))):(o.h+=i.h,(r=C.intersect(o,r))?(n=C.findBestRelativePosition(i,r,o,["bc-tc","bl-tl","br-tr"]))?(a=C.relativePosition(i,r,n),z(s,I(u,a.x,a.y,r,o,i))):z(s,I(u,r.x,r.y,r,o,i)):s.hide()),g=s,v=function(e,t){return e===t},p=(p=n)?p.substr(0,2):"",w.each({t:"down",b:"up"},function(e,t){g.classes.toggle("arrow-"+e,v(t,p.substr(0,1)))}),w.each({l:"left",r:"right"},function(e,t){g.classes.toggle("arrow-"+e,v(t,p.substr(1,1)))})}},r=function(e){return function(){u.requestAnimationFrame(function(){x.selection&&n(a(x.selection.getNode()),e)})}},t=function(e){var t;if(e.toolbar.panel)return e.toolbar.panel.show(),void n(e);t=v.create({type:"floatpanel",role:"dialog",classes:"tinymce tinymce-inline arrow",ariaLabel:"Inline toolbar",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:!0,border:1,items:O(x,e.toolbar.items),oncancel:function(){x.focus()}}),W.setUiContainer(x,t),function(e){if(!i){var t=r(!0),n=W.getUiContainer(e);i=x.selection.getScrollContainer()||x.getWin(),B.bind(i,"scroll",t),B.bind(n,"scroll",t),x.on("remove",function(){B.unbind(i,"scroll",t),B.unbind(n,"scroll",t)})}}(t),(e.toolbar.panel=t).renderTo().reflow(),n(e)},s=function(){w.each(o(),function(e){e.panel&&e.panel.hide()})},a=function(e){var t,n,i,r=o();for(t=(i=x.$(e).parents().add(e)).length-1;0<=t;t--)for(n=r.length-1;0<=n;n--)if(r[n].predicate(i[t]))return{toolbar:r[n],element:i[t]};return null};x.on("click keyup setContent ObjectResized",function(e){("setcontent"!==e.type||e.selection)&&u.setEditorTimeout(x,function(){var e;(e=a(x.selection.getNode()))?(s(),t(e)):s()})}),x.on("blur hide contextmenu",s),x.on("ObjectResizeStart",function(){var e=a(x.selection.getNode());e&&e.toolbar.panel&&e.toolbar.panel.hide()}),x.on("ResizeEditor ResizeWindow",r(!0)),x.on("nodeChange",r(!1)),x.on("remove",function(){w.each(o(),function(e){e.panel&&e.panel.remove()}),x.contextToolbars={}}),x.shortcuts.add("ctrl+F9","",function(){var e=a(x.selection.getNode());e&&e.toolbar.panel&&e.toolbar.panel.items()[0].focus()})},U=function(t){return function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===t}},V=U("array"),Y=U("function"),$=U("number"),q=(Array.prototype.slice,Array.prototype.indexOf),X=Array.prototype.push,j=function(e,t){var n,i,r=(n=e,i=t,q.call(n,i));return-1===r?N.none():N.some(r)},J=function(e,t){for(var n=0,i=e.length;n<i;n++)if(t(e[n],n))return!0;return!1},G=function(e,t){for(var n=e.length,i=new Array(n),r=0;r<n;r++){var o=e[r];i[r]=t(o,r)}return i},K=function(e,t){for(var n=0,i=e.length;n<i;n++)t(e[n],n)},Z=function(e,t){for(var n=[],i=0,r=e.length;i<r;i++){var o=e[i];t(o,i)&&n.push(o)}return n},Q=(Y(Array.from)&&Array.from,{file:{title:"File",items:"newdocument restoredraft | preview | print"},edit:{title:"Edit",items:"undo redo | cut copy paste pastetext | selectall"},view:{title:"View",items:"code | visualaid visualchars visualblocks | spellchecker | preview fullscreen"},insert:{title:"Insert",items:"image link media template codesample inserttable | charmap hr | pagebreak nonbreaking anchor toc | insertdatetime"},format:{title:"Format",items:"bold italic underline strikethrough superscript subscript codeformat | blockformats align | removeformat"},tools:{title:"Tools",items:"spellchecker spellcheckerlanguage | a11ycheck code"},table:{title:"Table"},help:{title:"Help"}}),ee=function(e,t){return"|"===e?{name:"|",item:{text:"|"}}:t?{name:e,item:t}:null},te=function(e,t){return function(e,t){for(var n=0,i=e.length;n<i;n++)if(t(e[n],n))return N.some(n);return N.none()}(e,function(e){return e.name===t}).isSome()},ne=function(e){return e&&"|"===e.item.text},ie=function(n,e,t,i){var r,o,s,a,l,u,c;return e?(o=e[i],a=!0):o=Q[i],o&&(r={text:o.title},s=[],w.each((o.items||"").split(/[ ,]/),function(e){var t=ee(e,n[e]);t&&s.push(t)}),a||w.each(n,function(e,t){e.context!==i||te(s,t)||("before"===e.separator&&s.push({name:"|",item:{text:"|"}}),e.prependToContext?s.unshift(ee(t,e)):s.push(ee(t,e)),"after"===e.separator&&s.push({name:"|",item:{text:"|"}}))}),r.menu=G((l=t,u=Z(s,function(e){return!1===l.hasOwnProperty(e.name)}),c=Z(u,function(e,t){return!ne(e)||!ne(u[t-1])}),Z(c,function(e,t){return!ne(e)||0<t&&t<c.length-1})),function(e){return e.item}),!r.menu.length)?null:r},re=function(e){for(var t,n=[],i=function(e){var t,n=[],i=h(e);if(i)for(t in i)n.push(t);else for(t in Q)n.push(t);return n}(e),r=w.makeMap((t=e,t.getParam("removed_menuitems","")).split(/[ ,]/)),o=c(e),s="string"==typeof o?o.split(/[ ,]/):i,a=0;a<s.length;a++){var l=s[a],u=ie(e.menuItems,h(e),r,l);u&&n.push(u)}return n},oe=p.DOM,se=function(e){return{width:e.clientWidth,height:e.clientHeight}},ae=function(e,t,n){var i,r,o,s;i=e.getContainer(),r=e.getContentAreaContainer().firstChild,o=se(i),s=se(r),null!==t&&(t=Math.max(e.getParam("min_width",100,"number"),t),t=Math.min(e.getParam("max_width",65535,"number"),t),oe.setStyle(i,"width",t+(o.width-s.width)),oe.setStyle(r,"width",t)),n=Math.max(e.getParam("min_height",100,"number"),n),n=Math.min(e.getParam("max_height",65535,"number"),n),oe.setStyle(r,"height",n),y(e)},le=ae,ue=function(e,t,n){var i=e.getContentAreaContainer();ae(e,i.clientWidth+t,i.clientHeight+n)},ce=tinymce.util.Tools.resolve("tinymce.Env"),de=function(e,t,n){var i,r=e.settings[n];r&&r((i=t.getEl("body"),{element:function(){return i}}))},fe=function(c,d,f){return function(e){var t,n,i,r,o,s=e.control,a=s.parents().filter("panel")[0],l=a.find("#"+d)[0],u=(t=f,n=d,w.grep(t,function(e){return e.name===n})[0]);i=d,r=a,o=f,w.each(o,function(e){var t=r.items().filter("#"+e.name)[0];t&&t.visible()&&e.name!==i&&(de(e,t,"onhide"),t.visible(!1))}),s.parent().items().each(function(e){e.active(!1)}),l&&l.visible()?(de(u,l,"onhide"),l.hide(),s.active(!1)):(l?l.show():(l=v.create({type:"container",name:d,layout:"stack",classes:"sidebar-panel",html:""}),a.prepend(l),de(u,l,"onrender")),de(u,l,"onshow"),s.active(!0)),y(c)}},he=function(e){return!(ce.ie&&!(11<=ce.ie)||!e.sidebars)&&0<e.sidebars.length},me=function(n){return{type:"panel",name:"sidebar",layout:"stack",classes:"sidebar",items:[{type:"toolbar",layout:"stack",classes:"sidebar-toolbar",items:w.map(n.sidebars,function(e){var t=e.settings;return{type:"button",icon:t.icon,image:t.image,tooltip:t.tooltip,onclick:fe(n,e.name,n.sidebars)}})}]}},ge=function(e){var t=function(){e._skinLoaded=!0,o(e)};return function(){e.initialized?t():e.on("init",t)}},pe=p.DOM,ve=function(e){return{type:"panel",name:"iframe",layout:"stack",classes:"edit-area",border:e,html:""}},be=function(t,e,n){var i,r,o,s,a;if(!1===m(t)&&n.skinUiCss?pe.styleSheetLoader.load(n.skinUiCss,ge(t)):ge(t)(),i=e.panel=v.create({type:"panel",role:"application",classes:"tinymce",style:"visibility: hidden",layout:"stack",border:1,items:[{type:"container",classes:"top-part",items:[!1===d(t)?null:{type:"menubar",border:"0 0 1 0",items:re(t)},A(t,f(t))]},he(t)?(s=t,{type:"panel",layout:"stack",classes:"edit-aria-container",border:"1 0 0 0",items:[ve("0"),me(s)]}):ve("1 0 0 0")]}),W.setUiContainer(t,i),"none"!==g(t)&&(r={type:"resizehandle",direction:g(t),onResizeStart:function(){var e=t.getContentAreaContainer().firstChild;o={width:e.clientWidth,height:e.clientHeight}},onResize:function(e){"both"===g(t)?le(t,o.width+e.deltaX,o.height+e.deltaY):le(t,null,o.height+e.deltaY)}}),t.getParam("statusbar",!0,"boolean")){var l=b.translate(["Powered by {0}",'<a href="https://www.tiny.cloud/?utm_campaign=editor_referral&utm_medium=poweredby&utm_source=tinymce" rel="noopener" target="_blank" role="presentation" tabindex="-1">Tiny</a>']),u=t.getParam("branding",!0,"boolean")?{type:"label",classes:"branding",html:" "+l}:null;i.add({type:"panel",name:"statusbar",classes:"statusbar",layout:"flow",border:"1 0 0 0",ariaRoot:!0,items:[{type:"elementpath",editor:t},r,u]})}return x(t),t.on("SwitchMode",(a=i,function(e){a.find("*").disabled("readonly"===e.mode)})),i.renderBefore(n.targetNode).reflow(),t.getParam("readonly",!1,"boolean")&&t.setMode("readonly"),n.width&&pe.setStyle(i.getEl(),"width",n.width),t.on("remove",function(){i.remove(),i=null}),R(t,i),F(t),{iframeContainer:i.find("#iframe")[0].getEl(),editorContainer:i.getEl()}},ye=tinymce.util.Tools.resolve("tinymce.dom.DomQuery"),xe=0,we={id:function(){return"mceu_"+xe++},create:function(e,t,n){var i=_.document.createElement(e);return p.DOM.setAttribs(i,t),"string"==typeof n?i.innerHTML=n:w.each(n,function(e){e.nodeType&&i.appendChild(e)}),i},createFragment:function(e){return p.DOM.createFragment(e)},getWindowSize:function(){return p.DOM.getViewPort()},getSize:function(e){var t,n;if(e.getBoundingClientRect){var i=e.getBoundingClientRect();t=Math.max(i.width||i.right-i.left,e.offsetWidth),n=Math.max(i.height||i.bottom-i.bottom,e.offsetHeight)}else t=e.offsetWidth,n=e.offsetHeight;return{width:t,height:n}},getPos:function(e,t){return p.DOM.getPos(e,t||we.getContainer())},getContainer:function(){return ce.container?ce.container:_.document.body},getViewPort:function(e){return p.DOM.getViewPort(e)},get:function(e){return _.document.getElementById(e)},addClass:function(e,t){return p.DOM.addClass(e,t)},removeClass:function(e,t){return p.DOM.removeClass(e,t)},hasClass:function(e,t){return p.DOM.hasClass(e,t)},toggleClass:function(e,t,n){return p.DOM.toggleClass(e,t,n)},css:function(e,t,n){return p.DOM.setStyle(e,t,n)},getRuntimeStyle:function(e,t){return p.DOM.getStyle(e,t,!0)},on:function(e,t,n,i){return p.DOM.bind(e,t,n,i)},off:function(e,t,n){return p.DOM.unbind(e,t,n)},fire:function(e,t,n){return p.DOM.fire(e,t,n)},innerHtml:function(e,t){p.DOM.setHTML(e,t)}},_e=function(e){return"static"===we.getRuntimeStyle(e,"position")},Re=function(e){return e.state.get("fixed")};function Ce(e,t,n){var i,r,o,s,a,l,u,c,d,f;return d=Ee(),o=(r=we.getPos(t,W.getUiContainer(e))).x,s=r.y,Re(e)&&_e(_.document.body)&&(o-=d.x,s-=d.y),i=e.getEl(),a=(f=we.getSize(i)).width,l=f.height,u=(f=we.getSize(t)).width,c=f.height,"b"===(n=(n||"").split(""))[0]&&(s+=c),"r"===n[1]&&(o+=u),"c"===n[0]&&(s+=Math.round(c/2)),"c"===n[1]&&(o+=Math.round(u/2)),"b"===n[3]&&(s-=l),"r"===n[4]&&(o-=a),"c"===n[3]&&(s-=Math.round(l/2)),"c"===n[4]&&(o-=Math.round(a/2)),{x:o,y:s,w:a,h:l}}var Ee=function(){var e=_.window;return{x:Math.max(e.pageXOffset,_.document.body.scrollLeft,_.document.documentElement.scrollLeft),y:Math.max(e.pageYOffset,_.document.body.scrollTop,_.document.documentElement.scrollTop),w:e.innerWidth||_.document.documentElement.clientWidth,h:e.innerHeight||_.document.documentElement.clientHeight}},ke=function(e){var t,n=W.getUiContainer(e);return n&&!Re(e)?{x:0,y:0,w:(t=n).scrollWidth-1,h:t.scrollHeight-1}:Ee()},He={testMoveRel:function(e,t){for(var n=ke(this),i=0;i<t.length;i++){var r=Ce(this,e,t[i]);if(Re(this)){if(0<r.x&&r.x+r.w<n.w&&0<r.y&&r.y+r.h<n.h)return t[i]}else if(r.x>n.x&&r.x+r.w<n.w+n.x&&r.y>n.y&&r.y+r.h<n.h+n.y)return t[i]}return t[0]},moveRel:function(e,t){"string"!=typeof t&&(t=this.testMoveRel(e,t));var n=Ce(this,e,t);return this.moveTo(n.x,n.y)},moveBy:function(e,t){var n=this.layoutRect();return this.moveTo(n.x+e,n.y+t),this},moveTo:function(e,t){var n=this;function i(e,t,n){return e<0?0:t<e+n&&(e=t-n)<0?0:e}if(n.settings.constrainToViewport){var r=ke(this),o=n.layoutRect();e=i(e,r.w+r.x,o.w),t=i(t,r.h+r.y,o.h)}var s=W.getUiContainer(n);return s&&_e(s)&&!Re(n)&&(e-=s.scrollLeft,t-=s.scrollTop),s&&(e+=1,t+=1),n.state.get("rendered")?n.layoutRect({x:e,y:t}).repaint():(n.settings.x=e,n.settings.y=t),n.fire("move",{x:e,y:t}),n}},Se=tinymce.util.Tools.resolve("tinymce.util.Class"),Te=tinymce.util.Tools.resolve("tinymce.util.EventDispatcher"),Me=function(e){var t;if(e)return"number"==typeof e?{top:e=e||0,left:e,bottom:e,right:e}:(1===(t=(e=e.split(" ")).length)?e[1]=e[2]=e[3]=e[0]:2===t?(e[2]=e[0],e[3]=e[1]):3===t&&(e[3]=e[1]),{top:parseInt(e[0],10)||0,right:parseInt(e[1],10)||0,bottom:parseInt(e[2],10)||0,left:parseInt(e[3],10)||0})},Ne=function(i,e){function t(e){var t=parseFloat(function(e){var t=i.ownerDocument.defaultView;if(t){var n=t.getComputedStyle(i,null);return n?(e=e.replace(/[A-Z]/g,function(e){return"-"+e}),n.getPropertyValue(e)):null}return i.currentStyle[e]}(e));return isNaN(t)?0:t}return{top:t(e+"TopWidth"),right:t(e+"RightWidth"),bottom:t(e+"BottomWidth"),left:t(e+"LeftWidth")}};function Pe(){}function We(e){this.cls=[],this.cls._map={},this.onchange=e||Pe,this.prefix=""}w.extend(We.prototype,{add:function(e){return e&&!this.contains(e)&&(this.cls._map[e]=!0,this.cls.push(e),this._change()),this},remove:function(e){if(this.contains(e)){var t=void 0;for(t=0;t<this.cls.length&&this.cls[t]!==e;t++);this.cls.splice(t,1),delete this.cls._map[e],this._change()}return this},toggle:function(e,t){var n=this.contains(e);return n!==t&&(n?this.remove(e):this.add(e),this._change()),this},contains:function(e){return!!this.cls._map[e]},_change:function(){delete this.clsValue,this.onchange.call(this)}}),We.prototype.toString=function(){var e;if(this.clsValue)return this.clsValue;e="";for(var t=0;t<this.cls.length;t++)0<t&&(e+=" "),e+=this.prefix+this.cls[t];return e};var De,Oe,Ae,Be=/^([\w\\*]+)?(?:#([\w\-\\]+))?(?:\.([\w\\\.]+))?(?:\[\@?([\w\\]+)([\^\$\*!~]?=)([\w\\]+)\])?(?:\:(.+))?/i,Le=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,ze=/^\s*|\s*$/g,Ie=Se.extend({init:function(e){var o=this.match;function s(e,t,n){var i;function r(e){e&&t.push(e)}return r(function(t){if(t)return t=t.toLowerCase(),function(e){return"*"===t||e.type===t}}((i=Be.exec(e.replace(ze,"")))[1])),r(function(t){if(t)return function(e){return e._name===t}}(i[2])),r(function(n){if(n)return n=n.split("."),function(e){for(var t=n.length;t--;)if(!e.classes.contains(n[t]))return!1;return!0}}(i[3])),r(function(n,i,r){if(n)return function(e){var t=e[n]?e[n]():"";return i?"="===i?t===r:"*="===i?0<=t.indexOf(r):"~="===i?0<=(" "+t+" ").indexOf(" "+r+" "):"!="===i?t!==r:"^="===i?0===t.indexOf(r):"$="===i&&t.substr(t.length-r.length)===r:!!r}}(i[4],i[5],i[6])),r(function(i){var t;if(i)return(i=/(?:not\((.+)\))|(.+)/i.exec(i))[1]?(t=a(i[1],[]),function(e){return!o(e,t)}):(i=i[2],function(e,t,n){return"first"===i?0===t:"last"===i?t===n-1:"even"===i?t%2==0:"odd"===i?t%2==1:!!e[i]&&e[i]()})}(i[7])),t.pseudo=!!i[7],t.direct=n,t}function a(e,t){var n,i,r,o=[];do{if(Le.exec(""),(i=Le.exec(e))&&(e=i[3],o.push(i[1]),i[2])){n=i[3];break}}while(i);for(n&&a(n,t),e=[],r=0;r<o.length;r++)">"!==o[r]&&e.push(s(o[r],[],">"===o[r-1]));return t.push(e),t}this._selectors=a(e,[])},match:function(e,t){var n,i,r,o,s,a,l,u,c,d,f,h,m;for(n=0,i=(t=t||this._selectors).length;n<i;n++){for(m=e,h=0,r=(o=(s=t[n]).length)-1;0<=r;r--)for(u=s[r];m;){if(u.pseudo)for(c=d=(f=m.parent().items()).length;c--&&f[c]!==m;);for(a=0,l=u.length;a<l;a++)if(!u[a](m,c,d)){a=l+1;break}if(a===l){h++;break}if(r===o-1)break;m=m.parent()}if(h===o)return!0}return!1},find:function(e){var t,n,u=[],i=this._selectors;function c(e,t,n){var i,r,o,s,a,l=t[n];for(i=0,r=e.length;i<r;i++){for(a=e[i],o=0,s=l.length;o<s;o++)if(!l[o](a,i,r)){o=s+1;break}if(o===s)n===t.length-1?u.push(a):a.items&&c(a.items(),t,n+1);else if(l.direct)return;a.items&&c(a.items(),t,n)}}if(e.items){for(t=0,n=i.length;t<n;t++)c(e.items(),i[t],0);1<n&&(u=function(e){for(var t,n=[],i=e.length;i--;)(t=e[i]).__checked||(n.push(t),t.__checked=1);for(i=n.length;i--;)delete n[i].__checked;return n}(u))}return De||(De=Ie.Collection),new De(u)}}),Fe=Array.prototype.push,Ue=Array.prototype.slice;Ae={length:0,init:function(e){e&&this.add(e)},add:function(e){return w.isArray(e)?Fe.apply(this,e):e instanceof Oe?this.add(e.toArray()):Fe.call(this,e),this},set:function(e){var t,n=this,i=n.length;for(n.length=0,n.add(e),t=n.length;t<i;t++)delete n[t];return n},filter:function(t){var e,n,i,r,o=[];for("string"==typeof t?(t=new Ie(t),r=function(e){return t.match(e)}):r=t,e=0,n=this.length;e<n;e++)r(i=this[e])&&o.push(i);return new Oe(o)},slice:function(){return new Oe(Ue.apply(this,arguments))},eq:function(e){return-1===e?this.slice(e):this.slice(e,+e+1)},each:function(e){return w.each(this,e),this},toArray:function(){return w.toArray(this)},indexOf:function(e){for(var t=this.length;t--&&this[t]!==e;);return t},reverse:function(){return new Oe(w.toArray(this).reverse())},hasClass:function(e){return!!this[0]&&this[0].classes.contains(e)},prop:function(t,n){var e;return n!==undefined?(this.each(function(e){e[t]&&e[t](n)}),this):(e=this[0])&&e[t]?e[t]():void 0},exec:function(t){var n=w.toArray(arguments).slice(1);return this.each(function(e){e[t]&&e[t].apply(e,n)}),this},remove:function(){for(var e=this.length;e--;)this[e].remove();return this},addClass:function(t){return this.each(function(e){e.classes.add(t)})},removeClass:function(t){return this.each(function(e){e.classes.remove(t)})}},w.each("fire on off show hide append prepend before after reflow".split(" "),function(n){Ae[n]=function(){var t=w.toArray(arguments);return this.each(function(e){n in e&&e[n].apply(e,t)}),this}}),w.each("text name disabled active selected checked visible parent value data".split(" "),function(t){Ae[t]=function(e){return this.prop(t,e)}}),Oe=Se.extend(Ae);var Ve=Ie.Collection=Oe,Ye=function(e){this.create=e.create};Ye.create=function(r,o){return new Ye({create:function(t,n){var i,e=function(e){t.set(n,e.value)};return t.on("change:"+n,function(e){r.set(o,e.value)}),r.on("change:"+o,e),(i=t._bindings)||(i=t._bindings=[],t.on("destroy",function(){for(var e=i.length;e--;)i[e]()})),i.push(function(){r.off("change:"+o,e)}),r.get(o)}})};var $e=tinymce.util.Tools.resolve("tinymce.util.Observable");function qe(e){return 0<e.nodeType}var Xe,je,Je=Se.extend({Mixins:[$e],init:function(e){var t,n;for(t in e=e||{})(n=e[t])instanceof Ye&&(e[t]=n.create(this,t));this.data=e},set:function(t,n){var i,r,o=this.data[t];if(n instanceof Ye&&(n=n.create(this,t)),"object"==typeof t){for(i in t)this.set(i,t[i]);return this}return function e(t,n){var i,r;if(t===n)return!0;if(null===t||null===n)return t===n;if("object"!=typeof t||"object"!=typeof n)return t===n;if(w.isArray(n)){if(t.length!==n.length)return!1;for(i=t.length;i--;)if(!e(t[i],n[i]))return!1}if(qe(t)||qe(n))return t===n;for(i in r={},n){if(!e(t[i],n[i]))return!1;r[i]=!0}for(i in t)if(!r[i]&&!e(t[i],n[i]))return!1;return!0}(o,n)||(this.data[t]=n,r={target:this,name:t,value:n,oldValue:o},this.fire("change:"+t,r),this.fire("change",r)),this},get:function(e){return this.data[e]},has:function(e){return e in this.data},bind:function(e){return Ye.create(this,e)},destroy:function(){this.fire("destroy")}}),Ge={},Ke={add:function(e){var t=e.parent();if(t){if(!t._layout||t._layout.isNative())return;Ge[t._id]||(Ge[t._id]=t),Xe||(Xe=!0,u.requestAnimationFrame(function(){var e,t;for(e in Xe=!1,Ge)(t=Ge[e]).state.get("rendered")&&t.reflow();Ge={}},_.document.body))}},remove:function(e){Ge[e._id]&&delete Ge[e._id]}},Ze="onmousewheel"in _.document,Qe=!1,et=0,tt={Statics:{classPrefix:"mce-"},isRtl:function(){return je.rtl},classPrefix:"mce-",init:function(t){var e,n,i=this;function r(e){var t;for(e=e.split(" "),t=0;t<e.length;t++)i.classes.add(e[t])}i.settings=t=w.extend({},i.Defaults,t),i._id=t.id||"mceu_"+et++,i._aria={role:t.role},i._elmCache={},i.$=ye,i.state=new Je({visible:!0,active:!1,disabled:!1,value:""}),i.data=new Je(t.data),i.classes=new We(function(){i.state.get("rendered")&&(i.getEl().className=this.toString())}),i.classes.prefix=i.classPrefix,(e=t.classes)&&(i.Defaults&&(n=i.Defaults.classes)&&e!==n&&r(n),r(e)),w.each("title text name visible disabled active value".split(" "),function(e){e in t&&i[e](t[e])}),i.on("click",function(){if(i.disabled())return!1}),i.settings=t,i.borderBox=Me(t.border),i.paddingBox=Me(t.padding),i.marginBox=Me(t.margin),t.hidden&&i.hide()},Properties:"parent,name",getContainerElm:function(){var e=W.getUiContainer(this);return e||we.getContainer()},getParentCtrl:function(e){for(var t,n=this.getRoot().controlIdLookup;e&&n&&!(t=n[e.id]);)e=e.parentNode;return t},initLayoutRect:function(){var e,t,n,i,r,o,s,a,l,u,c=this,d=c.settings,f=c.getEl();e=c.borderBox=c.borderBox||Ne(f,"border"),c.paddingBox=c.paddingBox||Ne(f,"padding"),c.marginBox=c.marginBox||Ne(f,"margin"),u=we.getSize(f),a=d.minWidth,l=d.minHeight,r=a||u.width,o=l||u.height,n=d.width,i=d.height,s=void 0!==(s=d.autoResize)?s:!n&&!i,n=n||r,i=i||o;var h=e.left+e.right,m=e.top+e.bottom,g=d.maxWidth||65535,p=d.maxHeight||65535;return c._layoutRect=t={x:d.x||0,y:d.y||0,w:n,h:i,deltaW:h,deltaH:m,contentW:n-h,contentH:i-m,innerW:n-h,innerH:i-m,startMinWidth:a||0,startMinHeight:l||0,minW:Math.min(r,g),minH:Math.min(o,p),maxW:g,maxH:p,autoResize:s,scrollW:0},c._lastLayoutRect={},t},layoutRect:function(e){var t,n,i,r,o,s=this,a=s._layoutRect;return a||(a=s.initLayoutRect()),e?(i=a.deltaW,r=a.deltaH,e.x!==undefined&&(a.x=e.x),e.y!==undefined&&(a.y=e.y),e.minW!==undefined&&(a.minW=e.minW),e.minH!==undefined&&(a.minH=e.minH),(n=e.w)!==undefined&&(n=(n=n<a.minW?a.minW:n)>a.maxW?a.maxW:n,a.w=n,a.innerW=n-i),(n=e.h)!==undefined&&(n=(n=n<a.minH?a.minH:n)>a.maxH?a.maxH:n,a.h=n,a.innerH=n-r),(n=e.innerW)!==undefined&&(n=(n=n<a.minW-i?a.minW-i:n)>a.maxW-i?a.maxW-i:n,a.innerW=n,a.w=n+i),(n=e.innerH)!==undefined&&(n=(n=n<a.minH-r?a.minH-r:n)>a.maxH-r?a.maxH-r:n,a.innerH=n,a.h=n+r),e.contentW!==undefined&&(a.contentW=e.contentW),e.contentH!==undefined&&(a.contentH=e.contentH),(t=s._lastLayoutRect).x===a.x&&t.y===a.y&&t.w===a.w&&t.h===a.h||((o=je.repaintControls)&&o.map&&!o.map[s._id]&&(o.push(s),o.map[s._id]=!0),t.x=a.x,t.y=a.y,t.w=a.w,t.h=a.h),s):a},repaint:function(){var e,t,n,i,r,o,s,a,l,u,c=this;l=_.document.createRange?function(e){return e}:Math.round,e=c.getEl().style,i=c._layoutRect,a=c._lastRepaintRect||{},o=(r=c.borderBox).left+r.right,s=r.top+r.bottom,i.x!==a.x&&(e.left=l(i.x)+"px",a.x=i.x),i.y!==a.y&&(e.top=l(i.y)+"px",a.y=i.y),i.w!==a.w&&(u=l(i.w-o),e.width=(0<=u?u:0)+"px",a.w=i.w),i.h!==a.h&&(u=l(i.h-s),e.height=(0<=u?u:0)+"px",a.h=i.h),c._hasBody&&i.innerW!==a.innerW&&(u=l(i.innerW),(n=c.getEl("body"))&&((t=n.style).width=(0<=u?u:0)+"px"),a.innerW=i.innerW),c._hasBody&&i.innerH!==a.innerH&&(u=l(i.innerH),(n=n||c.getEl("body"))&&((t=t||n.style).height=(0<=u?u:0)+"px"),a.innerH=i.innerH),c._lastRepaintRect=a,c.fire("repaint",{},!1)},updateLayoutRect:function(){var e=this;e.parent()._lastRect=null,we.css(e.getEl(),{width:"",height:""}),e._layoutRect=e._lastRepaintRect=e._lastLayoutRect=null,e.initLayoutRect()},on:function(e,t){var n,i,r,o=this;return nt(o).on(e,"string"!=typeof(n=t)?n:function(e){return i||o.parentsAndSelf().each(function(e){var t=e.settings.callbacks;if(t&&(i=t[n]))return r=e,!1}),i?i.call(r,e):(e.action=n,void this.fire("execute",e))}),o},off:function(e,t){return nt(this).off(e,t),this},fire:function(e,t,n){if((t=t||{}).control||(t.control=this),t=nt(this).fire(e,t),!1!==n&&this.parent)for(var i=this.parent();i&&!t.isPropagationStopped();)i.fire(e,t,!1),i=i.parent();return t},hasEventListeners:function(e){return nt(this).has(e)},parents:function(e){var t,n=new Ve;for(t=this.parent();t;t=t.parent())n.add(t);return e&&(n=n.filter(e)),n},parentsAndSelf:function(e){return new Ve(this).add(this.parents(e))},next:function(){var e=this.parent().items();return e[e.indexOf(this)+1]},prev:function(){var e=this.parent().items();return e[e.indexOf(this)-1]},innerHtml:function(e){return this.$el.html(e),this},getEl:function(e){var t=e?this._id+"-"+e:this._id;return this._elmCache[t]||(this._elmCache[t]=ye("#"+t)[0]),this._elmCache[t]},show:function(){return this.visible(!0)},hide:function(){return this.visible(!1)},focus:function(){try{this.getEl().focus()}catch(e){}return this},blur:function(){return this.getEl().blur(),this},aria:function(e,t){var n=this,i=n.getEl(n.ariaTarget);return void 0===t?n._aria[e]:(n._aria[e]=t,n.state.get("rendered")&&i.setAttribute("role"===e?e:"aria-"+e,t),n)},encode:function(e,t){return!1!==t&&(e=this.translate(e)),(e||"").replace(/[&<>"]/g,function(e){return"&#"+e.charCodeAt(0)+";"})},translate:function(e){return je.translate?je.translate(e):e},before:function(e){var t=this.parent();return t&&t.insert(e,t.items().indexOf(this),!0),this},after:function(e){var t=this.parent();return t&&t.insert(e,t.items().indexOf(this)),this},remove:function(){var t,e,n=this,i=n.getEl(),r=n.parent();if(n.items){var o=n.items().toArray();for(e=o.length;e--;)o[e].remove()}r&&r.items&&(t=[],r.items().each(function(e){e!==n&&t.push(e)}),r.items().set(t),r._lastRect=null),n._eventsRoot&&n._eventsRoot===n&&ye(i).off();var s=n.getRoot().controlIdLookup;return s&&delete s[n._id],i&&i.parentNode&&i.parentNode.removeChild(i),n.state.set("rendered",!1),n.state.destroy(),n.fire("remove"),n},renderBefore:function(e){return ye(e).before(this.renderHtml()),this.postRender(),this},renderTo:function(e){return ye(e||this.getContainerElm()).append(this.renderHtml()),this.postRender(),this},preRender:function(){},render:function(){},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'"></div>'},postRender:function(){var e,t,n,i,r,o=this,s=o.settings;for(i in o.$el=ye(o.getEl()),o.state.set("rendered",!0),s)0===i.indexOf("on")&&o.on(i.substr(2),s[i]);if(o._eventsRoot){for(n=o.parent();!r&&n;n=n.parent())r=n._eventsRoot;if(r)for(i in r._nativeEvents)o._nativeEvents[i]=!0}it(o),s.style&&(e=o.getEl())&&(e.setAttribute("style",s.style),e.style.cssText=s.style),o.settings.border&&(t=o.borderBox,o.$el.css({"border-top-width":t.top,"border-right-width":t.right,"border-bottom-width":t.bottom,"border-left-width":t.left}));var a=o.getRoot();for(var l in a.controlIdLookup||(a.controlIdLookup={}),(a.controlIdLookup[o._id]=o)._aria)o.aria(l,o._aria[l]);!1===o.state.get("visible")&&(o.getEl().style.display="none"),o.bindStates(),o.state.on("change:visible",function(e){var t,n=e.value;o.state.get("rendered")&&(o.getEl().style.display=!1===n?"none":"",o.getEl().getBoundingClientRect()),(t=o.parent())&&(t._lastRect=null),o.fire(n?"show":"hide"),Ke.add(o)}),o.fire("postrender",{},!1)},bindStates:function(){},scrollIntoView:function(e){var t,n,i,r,o,s,a=this.getEl(),l=a.parentNode,u=function(e,t){var n,i,r=e;for(n=i=0;r&&r!==t&&r.nodeType;)n+=r.offsetLeft||0,i+=r.offsetTop||0,r=r.offsetParent;return{x:n,y:i}}(a,l);return t=u.x,n=u.y,i=a.offsetWidth,r=a.offsetHeight,o=l.clientWidth,s=l.clientHeight,"end"===e?(t-=o-i,n-=s-r):"center"===e&&(t-=o/2-i/2,n-=s/2-r/2),l.scrollLeft=t,l.scrollTop=n,this},getRoot:function(){for(var e,t=this,n=[];t;){if(t.rootControl){e=t.rootControl;break}n.push(t),t=(e=t).parent()}e||(e=this);for(var i=n.length;i--;)n[i].rootControl=e;return e},reflow:function(){Ke.remove(this);var e=this.parent();return e&&e._layout&&!e._layout.isNative()&&e.reflow(),this}};function nt(n){return n._eventDispatcher||(n._eventDispatcher=new Te({scope:n,toggleEvent:function(e,t){t&&Te.isNative(e)&&(n._nativeEvents||(n._nativeEvents={}),n._nativeEvents[e]=!0,n.state.get("rendered")&&it(n))}})),n._eventDispatcher}function it(a){var e,t,n,l,i,r;function o(e){var t=a.getParentCtrl(e.target);t&&t.fire(e.type,e)}function s(){var e=l._lastHoverCtrl;e&&(e.fire("mouseleave",{target:e.getEl()}),e.parents().each(function(e){e.fire("mouseleave",{target:e.getEl()})}),l._lastHoverCtrl=null)}function u(e){var t,n,i,r=a.getParentCtrl(e.target),o=l._lastHoverCtrl,s=0;if(r!==o){if((n=(l._lastHoverCtrl=r).parents().toArray().reverse()).push(r),o){for((i=o.parents().toArray().reverse()).push(o),s=0;s<i.length&&n[s]===i[s];s++);for(t=i.length-1;s<=t;t--)(o=i[t]).fire("mouseleave",{target:o.getEl()})}for(t=s;t<n.length;t++)(r=n[t]).fire("mouseenter",{target:r.getEl()})}}function c(e){e.preventDefault(),"mousewheel"===e.type?(e.deltaY=-.025*e.wheelDelta,e.wheelDeltaX&&(e.deltaX=-.025*e.wheelDeltaX)):(e.deltaX=0,e.deltaY=e.detail),e=a.fire("wheel",e)}if(i=a._nativeEvents){for((n=a.parents().toArray()).unshift(a),e=0,t=n.length;!l&&e<t;e++)l=n[e]._eventsRoot;for(l||(l=n[n.length-1]||a),a._eventsRoot=l,t=e,e=0;e<t;e++)n[e]._eventsRoot=l;var d=l._delegates;for(r in d||(d=l._delegates={}),i){if(!i)return!1;"wheel"!==r||Qe?("mouseenter"===r||"mouseleave"===r?l._hasMouseEnter||(ye(l.getEl()).on("mouseleave",s).on("mouseover",u),l._hasMouseEnter=1):d[r]||(ye(l.getEl()).on(r,o),d[r]=!0),i[r]=!1):Ze?ye(a.getEl()).on("mousewheel",c):ye(a.getEl()).on("DOMMouseScroll",c)}}}w.each("text title visible disabled active value".split(" "),function(t){tt[t]=function(e){return 0===arguments.length?this.state.get(t):(void 0!==e&&this.state.set(t,e),this)}});var rt=je=Se.extend(tt),ot=function(e){return!!e.getAttribute("data-mce-tabstop")};function st(e){var o,r,n=e.root;function i(e){return e&&1===e.nodeType}try{o=_.document.activeElement}catch(t){o=_.document.body}function s(e){return i(e=e||o)?e.getAttribute("role"):null}function a(e){for(var t,n=e||o;n=n.parentNode;)if(t=s(n))return t}function l(e){var t=o;if(i(t))return t.getAttribute("aria-"+e)}function u(e){var t=e.tagName.toUpperCase();return"INPUT"===t||"TEXTAREA"===t||"SELECT"===t}function c(t){var r=[];return function e(t){if(1===t.nodeType&&"none"!==t.style.display&&!t.disabled){var n;(u(n=t)&&!n.hidden||ot(n)||/^(button|menuitem|checkbox|tab|menuitemcheckbox|option|gridcell|slider)$/.test(s(n)))&&r.push(t);for(var i=0;i<t.childNodes.length;i++)e(t.childNodes[i])}}(t||n.getEl()),r}function d(e){var t,n;(n=(e=e||r).parents().toArray()).unshift(e);for(var i=0;i<n.length&&!(t=n[i]).settings.ariaRoot;i++);return t}function f(e,t){return e<0?e=t.length-1:e>=t.length&&(e=0),t[e]&&t[e].focus(),e}function h(e,t){var n=-1,i=d();t=t||c(i.getEl());for(var r=0;r<t.length;r++)t[r]===o&&(n=r);n+=e,i.lastAriaIndex=f(n,t)}function m(){"tablist"===a()?h(-1,c(o.parentNode)):r.parent().submenu?b():h(-1)}function g(){var e=s(),t=a();"tablist"===t?h(1,c(o.parentNode)):"menuitem"===e&&"menu"===t&&l("haspopup")?y():h(1)}function p(){h(-1)}function v(){var e=s(),t=a();"menuitem"===e&&"menubar"===t?y():"button"===e&&l("haspopup")?y({key:"down"}):h(1)}function b(){r.fire("cancel")}function y(e){e=e||{},r.fire("click",{target:o,aria:e})}return r=n.getParentCtrl(o),n.on("keydown",function(e){function t(e,t){u(o)||ot(o)||"slider"!==s(o)&&!1!==t(e)&&e.preventDefault()}if(!e.isDefaultPrevented())switch(e.keyCode){case 37:t(e,m);break;case 39:t(e,g);break;case 38:t(e,p);break;case 40:t(e,v);break;case 27:b();break;case 14:case 13:case 32:t(e,y);break;case 9:!function(e){if("tablist"===a()){var t=c(r.getEl("body"))[0];t&&t.focus()}else h(e.shiftKey?-1:1)}(e),e.preventDefault()}}),n.on("focusin",function(e){o=e.target,r=e.control}),{focusFirst:function(e){var t=d(e),n=c(t.getEl());t.settings.ariaRemember&&"lastAriaIndex"in t?f(t.lastAriaIndex,n):f(0,n)}}}var at={},lt=rt.extend({init:function(e){var t=this;t._super(e),(e=t.settings).fixed&&t.state.set("fixed",!0),t._items=new Ve,t.isRtl()&&t.classes.add("rtl"),t.bodyClasses=new We(function(){t.state.get("rendered")&&(t.getEl("body").className=this.toString())}),t.bodyClasses.prefix=t.classPrefix,t.classes.add("container"),t.bodyClasses.add("container-body"),e.containerCls&&t.classes.add(e.containerCls),t._layout=v.create((e.layout||"")+"layout"),t.settings.items?t.add(t.settings.items):t.add(t.render()),t._hasBody=!0},items:function(){return this._items},find:function(e){return(e=at[e]=at[e]||new Ie(e)).find(this)},add:function(e){return this.items().add(this.create(e)).parent(this),this},focus:function(e){var t,n,i,r=this;if(!e||!(n=r.keyboardNav||r.parents().eq(-1)[0].keyboardNav))return i=r.find("*"),r.statusbar&&i.add(r.statusbar.items()),i.each(function(e){if(e.settings.autofocus)return t=null,!1;e.canFocus&&(t=t||e)}),t&&t.focus(),r;n.focusFirst(r)},replace:function(e,t){for(var n,i=this.items(),r=i.length;r--;)if(i[r]===e){i[r]=t;break}0<=r&&((n=t.getEl())&&n.parentNode.removeChild(n),(n=e.getEl())&&n.parentNode.removeChild(n)),t.parent(this)},create:function(e){var t,n=this,i=[];return w.isArray(e)||(e=[e]),w.each(e,function(e){e&&(e instanceof rt||("string"==typeof e&&(e={type:e}),t=w.extend({},n.settings.defaults,e),e.type=t.type=t.type||e.type||n.settings.defaultType||(t.defaults?t.defaults.type:null),e=v.create(t)),i.push(e))}),i},renderNew:function(){var i=this;return i.items().each(function(e,t){var n;e.parent(i),e.state.get("rendered")||((n=i.getEl("body")).hasChildNodes()&&t<=n.childNodes.length-1?ye(n.childNodes[t]).before(e.renderHtml()):ye(n).append(e.renderHtml()),e.postRender(),Ke.add(e))}),i._layout.applyClasses(i.items().filter(":visible")),i._lastRect=null,i},append:function(e){return this.add(e).renderNew()},prepend:function(e){return this.items().set(this.create(e).concat(this.items().toArray())),this.renderNew()},insert:function(e,t,n){var i,r,o;return e=this.create(e),i=this.items(),!n&&t<i.length-1&&(t+=1),0<=t&&t<i.length&&(r=i.slice(0,t).toArray(),o=i.slice(t).toArray(),i.set(r.concat(e,o))),this.renderNew()},fromJSON:function(e){for(var t in e)this.find("#"+t).value(e[t]);return this},toJSON:function(){var i={};return this.find("*").each(function(e){var t=e.name(),n=e.value();t&&void 0!==n&&(i[t]=n)}),i},renderHtml:function(){var e=this,t=e._layout,n=this.settings.role;return e.preRender(),t.preRender(e),'<div id="'+e._id+'" class="'+e.classes+'"'+(n?' role="'+this.settings.role+'"':"")+'><div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+(e.settings.html||"")+t.renderHtml(e)+"</div></div>"},postRender:function(){var e,t=this;return t.items().exec("postRender"),t._super(),t._layout.postRender(t),t.state.set("rendered",!0),t.settings.style&&t.$el.css(t.settings.style),t.settings.border&&(e=t.borderBox,t.$el.css({"border-top-width":e.top,"border-right-width":e.right,"border-bottom-width":e.bottom,"border-left-width":e.left})),t.parent()||(t.keyboardNav=st({root:t})),t},initLayoutRect:function(){var e=this._super();return this._layout.recalc(this),e},recalc:function(){var e=this,t=e._layoutRect,n=e._lastRect;if(!n||n.w!==t.w||n.h!==t.h)return e._layout.recalc(e),t=e.layoutRect(),e._lastRect={x:t.x,y:t.y,w:t.w,h:t.h},!0},reflow:function(){var e;if(Ke.remove(this),this.visible()){for(rt.repaintControls=[],rt.repaintControls.map={},this.recalc(),e=rt.repaintControls.length;e--;)rt.repaintControls[e].repaint();"flow"!==this.settings.layout&&"stack"!==this.settings.layout&&this.repaint(),rt.repaintControls=[]}return this}});function ut(e){var t,n;if(e.changedTouches)for(t="screenX screenY pageX pageY clientX clientY".split(" "),n=0;n<t.length;n++)e[t[n]]=e.changedTouches[0][t[n]]}function ct(e,h){var m,g,t,p,v,b,y,x=h.document||_.document;h=h||{};var w=x.getElementById(h.handle||e);t=function(e){var t,n,i,r,o,s,a,l,u,c,d,f=(t=x,u=Math.max,n=t.documentElement,i=t.body,r=u(n.scrollWidth,i.scrollWidth),o=u(n.clientWidth,i.clientWidth),s=u(n.offsetWidth,i.offsetWidth),a=u(n.scrollHeight,i.scrollHeight),l=u(n.clientHeight,i.clientHeight),{width:r<s?o:r,height:a<u(n.offsetHeight,i.offsetHeight)?l:a});ut(e),e.preventDefault(),g=e.button,c=w,b=e.screenX,y=e.screenY,d=_.window.getComputedStyle?_.window.getComputedStyle(c,null).getPropertyValue("cursor"):c.runtimeStyle.cursor,m=ye("<div></div>").css({position:"absolute",top:0,left:0,width:f.width,height:f.height,zIndex:2147483647,opacity:1e-4,cursor:d}).appendTo(x.body),ye(x).on("mousemove touchmove",v).on("mouseup touchend",p),h.start(e)},v=function(e){if(ut(e),e.button!==g)return p(e);e.deltaX=e.screenX-b,e.deltaY=e.screenY-y,e.preventDefault(),h.drag(e)},p=function(e){ut(e),ye(x).off("mousemove touchmove",v).off("mouseup touchend",p),m.remove(),h.stop&&h.stop(e)},this.destroy=function(){ye(w).off()},ye(w).on("mousedown touchstart",t)}var dt,ft,ht,mt,gt={init:function(){this.on("repaint",this.renderScroll)},renderScroll:function(){var p=this,v=2;function n(){var m,g,e;function t(e,t,n,i,r,o){var s,a,l,u,c,d,f,h;if(a=p.getEl("scroll"+e)){if(f=t.toLowerCase(),h=n.toLowerCase(),ye(p.getEl("absend")).css(f,p.layoutRect()[i]-1),!r)return void ye(a).css("display","none");ye(a).css("display","block"),s=p.getEl("body"),l=p.getEl("scroll"+e+"t"),u=s["client"+n]-2*v,c=(u-=m&&g?a["client"+o]:0)/s["scroll"+n],(d={})[f]=s["offset"+t]+v,d[h]=u,ye(a).css(d),(d={})[f]=s["scroll"+t]*c,d[h]=u*c,ye(l).css(d)}}e=p.getEl("body"),m=e.scrollWidth>e.clientWidth,g=e.scrollHeight>e.clientHeight,t("h","Left","Width","contentW",m,"Height"),t("v","Top","Height","contentH",g,"Width")}p.settings.autoScroll&&(p._hasScroll||(p._hasScroll=!0,function(){function e(s,a,l,u,c){var d,e=p._id+"-scroll"+s,t=p.classPrefix;ye(p.getEl()).append('<div id="'+e+'" class="'+t+"scrollbar "+t+"scrollbar-"+s+'"><div id="'+e+'t" class="'+t+'scrollbar-thumb"></div></div>'),p.draghelper=new ct(e+"t",{start:function(){d=p.getEl("body")["scroll"+a],ye("#"+e).addClass(t+"active")},drag:function(e){var t,n,i,r,o=p.layoutRect();n=o.contentW>o.innerW,i=o.contentH>o.innerH,r=p.getEl("body")["client"+l]-2*v,t=(r-=n&&i?p.getEl("scroll"+s)["client"+c]:0)/p.getEl("body")["scroll"+l],p.getEl("body")["scroll"+a]=d+e["delta"+u]/t},stop:function(){ye("#"+e).removeClass(t+"active")}})}p.classes.add("scroll"),e("v","Top","Height","Y","Width"),e("h","Left","Width","X","Height")}(),p.on("wheel",function(e){var t=p.getEl("body");t.scrollLeft+=10*(e.deltaX||0),t.scrollTop+=10*e.deltaY,n()}),ye(p.getEl("body")).on("scroll",n)),n())}},pt=lt.extend({Defaults:{layout:"fit",containerCls:"panel"},Mixins:[gt],renderHtml:function(){var e=this,t=e._layout,n=e.settings.html;return e.preRender(),t.preRender(e),void 0===n?n='<div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+t.renderHtml(e)+"</div>":("function"==typeof n&&(n=n.call(e)),e._hasBody=!1),'<div id="'+e._id+'" class="'+e.classes+'" hidefocus="1" tabindex="-1" role="group">'+(e._preBodyHtml||"")+n+"</div>"}}),vt={resizeToContent:function(){this._layoutRect.autoResize=!0,this._lastRect=null,this.reflow()},resizeTo:function(e,t){if(e<=1||t<=1){var n=we.getWindowSize();e=e<=1?e*n.w:e,t=t<=1?t*n.h:t}return this._layoutRect.autoResize=!1,this.layoutRect({minW:e,minH:t,w:e,h:t}).reflow()},resizeBy:function(e,t){var n=this.layoutRect();return this.resizeTo(n.w+e,n.h+t)}},bt=[],yt=[];function xt(e,t){for(;e;){if(e===t)return!0;e=e.parent()}}function wt(){dt||(dt=function(e){2!==e.button&&function(e){for(var t=bt.length;t--;){var n=bt[t],i=n.getParentCtrl(e.target);if(n.settings.autohide){if(i&&(xt(i,n)||n.parent()===i))continue;(e=n.fire("autohide",{target:e.target})).isDefaultPrevented()||n.hide()}}}(e)},ye(_.document).on("click touchstart",dt))}function _t(r){var e=we.getViewPort().y;function t(e,t){for(var n,i=0;i<bt.length;i++)if(bt[i]!==r)for(n=bt[i].parent();n&&(n=n.parent());)n===r&&bt[i].fixed(e).moveBy(0,t).repaint()}r.settings.autofix&&(r.state.get("fixed")?r._autoFixY>e&&(r.fixed(!1).layoutRect({y:r._autoFixY}).repaint(),t(!1,r._autoFixY-e)):(r._autoFixY=r.layoutRect().y,r._autoFixY<e&&(r.fixed(!0).layoutRect({y:0}).repaint(),t(!0,e-r._autoFixY))))}function Rt(e,t){var n,i,r=Ct.zIndex||65535;if(e)yt.push(t);else for(n=yt.length;n--;)yt[n]===t&&yt.splice(n,1);if(yt.length)for(n=0;n<yt.length;n++)yt[n].modal&&(r++,i=yt[n]),yt[n].getEl().style.zIndex=r,yt[n].zIndex=r,r++;var o=ye("#"+t.classPrefix+"modal-block",t.getContainerElm())[0];i?ye(o).css("z-index",i.zIndex-1):o&&(o.parentNode.removeChild(o),mt=!1),Ct.currentZIndex=r}var Ct=pt.extend({Mixins:[He,vt],init:function(e){var i=this;i._super(e),(i._eventsRoot=i).classes.add("floatpanel"),e.autohide&&(wt(),function(){if(!ht){var e=_.document.documentElement,t=e.clientWidth,n=e.clientHeight;ht=function(){_.document.all&&t===e.clientWidth&&n===e.clientHeight||(t=e.clientWidth,n=e.clientHeight,Ct.hideAll())},ye(_.window).on("resize",ht)}}(),bt.push(i)),e.autofix&&(ft||(ft=function(){var e;for(e=bt.length;e--;)_t(bt[e])},ye(_.window).on("scroll",ft)),i.on("move",function(){_t(this)})),i.on("postrender show",function(e){if(e.control===i){var t,n=i.classPrefix;i.modal&&!mt&&((t=ye("#"+n+"modal-block",i.getContainerElm()))[0]||(t=ye('<div id="'+n+'modal-block" class="'+n+"reset "+n+'fade"></div>').appendTo(i.getContainerElm())),u.setTimeout(function(){t.addClass(n+"in"),ye(i.getEl()).addClass(n+"in")}),mt=!0),Rt(!0,i)}}),i.on("show",function(){i.parents().each(function(e){if(e.state.get("fixed"))return i.fixed(!0),!1})}),e.popover&&(i._preBodyHtml='<div class="'+i.classPrefix+'arrow"></div>',i.classes.add("popover").add("bottom").add(i.isRtl()?"end":"start")),i.aria("label",e.ariaLabel),i.aria("labelledby",i._id),i.aria("describedby",i.describedBy||i._id+"-none")},fixed:function(e){var t=this;if(t.state.get("fixed")!==e){if(t.state.get("rendered")){var n=we.getViewPort();e?t.layoutRect().y-=n.y:t.layoutRect().y+=n.y}t.classes.toggle("fixed",e),t.state.set("fixed",e)}return t},show:function(){var e,t=this._super();for(e=bt.length;e--&&bt[e]!==this;);return-1===e&&bt.push(this),t},hide:function(){return Et(this),Rt(!1,this),this._super()},hideAll:function(){Ct.hideAll()},close:function(){return this.fire("close").isDefaultPrevented()||(this.remove(),Rt(!1,this)),this},remove:function(){Et(this),this._super()},postRender:function(){return this.settings.bodyRole&&this.getEl("body").setAttribute("role",this.settings.bodyRole),this._super()}});function Et(e){var t;for(t=bt.length;t--;)bt[t]===e&&bt.splice(t,1);for(t=yt.length;t--;)yt[t]===e&&yt.splice(t,1)}Ct.hideAll=function(){for(var e=bt.length;e--;){var t=bt[e];t&&t.settings.autohide&&(t.hide(),bt.splice(e,1))}};var kt=function(s,n,e){var a,i,l=p.DOM,t=s.getParam("fixed_toolbar_container");t&&(i=l.select(t)[0]);var r=function(){if(a&&a.moveRel&&a.visible()&&!a._fixed){var e=s.selection.getScrollContainer(),t=s.getBody(),n=0,i=0;if(e){var r=l.getPos(t),o=l.getPos(e);n=Math.max(0,o.x-r.x),i=Math.max(0,o.y-r.y)}a.fixed(!1).moveRel(t,s.rtl?["tr-br","br-tr"]:["tl-bl","bl-tl","tr-br"]).moveBy(n,i)}},o=function(){a&&(a.show(),r(),l.addClass(s.getBody(),"mce-edit-focus"))},u=function(){a&&(a.hide(),Ct.hideAll(),l.removeClass(s.getBody(),"mce-edit-focus"))},c=function(){var e,t;a?a.visible()||o():(a=n.panel=v.create({type:i?"panel":"floatpanel",role:"application",classes:"tinymce tinymce-inline",layout:"flex",direction:"column",align:"stretch",autohide:!1,autofix:!0,fixed:(e=i,t=s,!(!e||t.settings.ui_container)),border:1,items:[!1===d(s)?null:{type:"menubar",border:"0 0 1 0",items:re(s)},A(s,f(s))]}),W.setUiContainer(s,a),x(s),i?a.renderTo(i).reflow():a.renderTo().reflow(),R(s,a),o(),F(s),s.on("nodeChange",r),s.on("ResizeWindow",r),s.on("activate",o),s.on("deactivate",u),s.nodeChanged())};return s.settings.content_editable=!0,s.on("focus",function(){!1===m(s)&&e.skinUiCss?l.styleSheetLoader.load(e.skinUiCss,c,c):c()}),s.on("blur hide",u),s.on("remove",function(){a&&(a.remove(),a=null)}),!1===m(s)&&e.skinUiCss?l.styleSheetLoader.load(e.skinUiCss,ge(s)):ge(s)(),{}};function Ht(i,r){var o,s,a=this,l=rt.classPrefix;a.show=function(e,t){function n(){o&&(ye(i).append('<div class="'+l+"throbber"+(r?" "+l+"throbber-inline":"")+'"></div>'),t&&t())}return a.hide(),o=!0,e?s=u.setTimeout(n,e):n(),a},a.hide=function(){var e=i.lastChild;return u.clearTimeout(s),e&&-1!==e.className.indexOf("throbber")&&e.parentNode.removeChild(e),o=!1,a}}var St=function(e,t){var n;e.on("ProgressState",function(e){n=n||new Ht(t.panel.getEl("body")),e.state?n.show(e.time):n.hide()})},Tt=function(e,t,n){var i=function(e){var t=e.settings,n=t.skin,i=t.skin_url;if(!1!==n){var r=n||"lightgray";i=i?e.documentBaseURI.toAbsolute(i):l.baseURL+"/skins/"+r}return i}(e);return i&&(n.skinUiCss=i+"/skin.min.css",e.contentCSS.push(i+"/content"+(e.inline?".inline":"")+".min.css")),St(e,t),e.getParam("inline",!1,"boolean")?kt(e,t,n):be(e,t,n)},Mt=rt.extend({Mixins:[He],Defaults:{classes:"widget tooltip tooltip-n"},renderHtml:function(){var e=this,t=e.classPrefix;return'<div id="'+e._id+'" class="'+e.classes+'" role="presentation"><div class="'+t+'tooltip-arrow"></div><div class="'+t+'tooltip-inner">'+e.encode(e.state.get("text"))+"</div></div>"},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.getEl().lastChild.innerHTML=t.encode(e.value)}),t._super()},repaint:function(){var e,t;e=this.getEl().style,t=this._layoutRect,e.left=t.x+"px",e.top=t.y+"px",e.zIndex=131070}}),Nt=rt.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.canFocus=!0,i.tooltip&&!1!==Nt.tooltips&&(r.on("mouseenter",function(e){var t=r.tooltip().moveTo(-65535);if(e.control===r){var n=t.text(i.tooltip).show().testMoveRel(r.getEl(),["bc-tc","bc-tl","bc-tr"]);t.classes.toggle("tooltip-n","bc-tc"===n),t.classes.toggle("tooltip-nw","bc-tl"===n),t.classes.toggle("tooltip-ne","bc-tr"===n),t.moveRel(r.getEl(),n)}else t.hide()}),r.on("mouseleave mousedown click",function(){r.tooltip().remove(),r._tooltip=null})),r.aria("label",i.ariaLabel||i.tooltip)},tooltip:function(){return this._tooltip||(this._tooltip=new Mt({type:"tooltip"}),W.inheritUiContainer(this,this._tooltip),this._tooltip.renderTo()),this._tooltip},postRender:function(){var e=this,t=e.settings;e._super(),e.parent()||!t.width&&!t.height||(e.initLayoutRect(),e.repaint()),t.autofocus&&e.focus()},bindStates:function(){var t=this;function n(e){t.aria("disabled",e),t.classes.toggle("disabled",e)}function i(e){t.aria("pressed",e),t.classes.toggle("active",e)}return t.state.on("change:disabled",function(e){n(e.value)}),t.state.on("change:active",function(e){i(e.value)}),t.state.get("disabled")&&n(!0),t.state.get("active")&&i(!0),t._super()},remove:function(){this._super(),this._tooltip&&(this._tooltip.remove(),this._tooltip=null)}}),Pt=Nt.extend({Defaults:{value:0},init:function(e){this._super(e),this.classes.add("progress"),this.settings.filter||(this.settings.filter=function(e){return Math.round(e)})},renderHtml:function(){var e=this._id,t=this.classPrefix;return'<div id="'+e+'" class="'+this.classes+'"><div class="'+t+'bar-container"><div class="'+t+'bar"></div></div><div class="'+t+'text">0%</div></div>'},postRender:function(){return this._super(),this.value(this.settings.value),this},bindStates:function(){var t=this;function n(e){e=t.settings.filter(e),t.getEl().lastChild.innerHTML=e+"%",t.getEl().firstChild.firstChild.style.width=e+"%"}return t.state.on("change:value",function(e){n(e.value)}),n(t.state.get("value")),t._super()}}),Wt=function(e,t){e.getEl().lastChild.textContent=t+(e.progressBar?" "+e.progressBar.value()+"%":"")},Dt=rt.extend({Mixins:[He],Defaults:{classes:"widget notification"},init:function(e){var t=this;t._super(e),t.maxWidth=e.maxWidth,e.text&&t.text(e.text),e.icon&&(t.icon=e.icon),e.color&&(t.color=e.color),e.type&&t.classes.add("notification-"+e.type),e.timeout&&(e.timeout<0||0<e.timeout)&&!e.closeButton?t.closeButton=!1:(t.classes.add("has-close"),t.closeButton=!0),e.progressBar&&(t.progressBar=new Pt),t.on("click",function(e){-1!==e.target.className.indexOf(t.classPrefix+"close")&&t.close()})},renderHtml:function(){var e,t=this,n=t.classPrefix,i="",r="",o="";return t.icon&&(i='<i class="'+n+"ico "+n+"i-"+t.icon+'"></i>'),e=' style="max-width: '+t.maxWidth+"px;"+(t.color?"background-color: "+t.color+';"':'"'),t.closeButton&&(r='<button type="button" class="'+n+'close" aria-hidden="true">\xd7</button>'),t.progressBar&&(o=t.progressBar.renderHtml()),'<div id="'+t._id+'" class="'+t.classes+'"'+e+' role="presentation">'+i+'<div class="'+n+'notification-inner">'+t.state.get("text")+"</div>"+o+r+'<div style="clip: rect(1px, 1px, 1px, 1px);height: 1px;overflow: hidden;position: absolute;width: 1px;" aria-live="assertive" aria-relevant="additions" aria-atomic="true"></div></div>'},postRender:function(){var e=this;return u.setTimeout(function(){e.$el.addClass(e.classPrefix+"in"),Wt(e,e.state.get("text"))},100),e._super()},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.getEl().firstChild.innerHTML=e.value,Wt(t,e.value)}),t.progressBar&&(t.progressBar.bindStates(),t.progressBar.state.on("change:value",function(e){Wt(t,t.state.get("text"))})),t._super()},close:function(){return this.fire("close").isDefaultPrevented()||this.remove(),this},repaint:function(){var e,t;e=this.getEl().style,t=this._layoutRect,e.left=t.x+"px",e.top=t.y+"px",e.zIndex=65534}});function Ot(o){var s=function(e){return e.inline?e.getElement():e.getContentAreaContainer()};return{open:function(e,t){var n,i=w.extend(e,{maxWidth:(n=s(o),we.getSize(n).width)}),r=new Dt(i);return 0<(r.args=i).timeout&&(r.timer=setTimeout(function(){r.close(),t()},i.timeout)),r.on("close",function(){t()}),r.renderTo(),r},close:function(e){e.close()},reposition:function(e){K(e,function(e){e.moveTo(0,0)}),function(n){if(0<n.length){var e=n.slice(0,1)[0],t=s(o);e.moveRel(t,"tc-tc"),K(n,function(e,t){0<t&&e.moveRel(n[t-1].getEl(),"bc-tc")})}}(e)},getArgs:function(e){return e.args}}}var At=[],Bt="";function Lt(e){var t,n=ye("meta[name=viewport]")[0];!1!==ce.overrideViewPort&&(n||((n=_.document.createElement("meta")).setAttribute("name","viewport"),_.document.getElementsByTagName("head")[0].appendChild(n)),(t=n.getAttribute("content"))&&void 0!==Bt&&(Bt=t),n.setAttribute("content",e?"width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0":Bt))}function zt(e,t){(function(){for(var e=0;e<At.length;e++)if(At[e]._fullscreen)return!0;return!1})()&&!1===t&&ye([_.document.documentElement,_.document.body]).removeClass(e+"fullscreen")}var It=Ct.extend({modal:!0,Defaults:{border:1,layout:"flex",containerCls:"panel",role:"dialog",callbacks:{submit:function(){this.fire("submit",{data:this.toJSON()})},close:function(){this.close()}}},init:function(e){var n=this;n._super(e),n.isRtl()&&n.classes.add("rtl"),n.classes.add("window"),n.bodyClasses.add("window-body"),n.state.set("fixed",!0),e.buttons&&(n.statusbar=new pt({layout:"flex",border:"1 0 0 0",spacing:3,padding:10,align:"center",pack:n.isRtl()?"start":"end",defaults:{type:"button"},items:e.buttons}),n.statusbar.classes.add("foot"),n.statusbar.parent(n)),n.on("click",function(e){var t=n.classPrefix+"close";(we.hasClass(e.target,t)||we.hasClass(e.target.parentNode,t))&&n.close()}),n.on("cancel",function(){n.close()}),n.on("move",function(e){e.control===n&&Ct.hideAll()}),n.aria("describedby",n.describedBy||n._id+"-none"),n.aria("label",e.title),n._fullscreen=!1},recalc:function(){var e,t,n,i,r=this,o=r.statusbar;r._fullscreen&&(r.layoutRect(we.getWindowSize()),r.layoutRect().contentH=r.layoutRect().innerH),r._super(),e=r.layoutRect(),r.settings.title&&!r._fullscreen&&(t=e.headerW)>e.w&&(n=e.x-Math.max(0,t/2),r.layoutRect({w:t,x:n}),i=!0),o&&(o.layoutRect({w:r.layoutRect().innerW}).recalc(),(t=o.layoutRect().minW+e.deltaW)>e.w&&(n=e.x-Math.max(0,t-e.w),r.layoutRect({w:t,x:n}),i=!0)),i&&r.recalc()},initLayoutRect:function(){var e,t=this,n=t._super(),i=0;if(t.settings.title&&!t._fullscreen){e=t.getEl("head");var r=we.getSize(e);n.headerW=r.width,n.headerH=r.height,i+=n.headerH}t.statusbar&&(i+=t.statusbar.layoutRect().h),n.deltaH+=i,n.minH+=i,n.h+=i;var o=we.getWindowSize();return n.x=t.settings.x||Math.max(0,o.w/2-n.w/2),n.y=t.settings.y||Math.max(0,o.h/2-n.h/2),n},renderHtml:function(){var e=this,t=e._layout,n=e._id,i=e.classPrefix,r=e.settings,o="",s="",a=r.html;return e.preRender(),t.preRender(e),r.title&&(o='<div id="'+n+'-head" class="'+i+'window-head"><div id="'+n+'-title" class="'+i+'title">'+e.encode(r.title)+'</div><div id="'+n+'-dragh" class="'+i+'dragh"></div><button type="button" class="'+i+'close" aria-hidden="true"><i class="mce-ico mce-i-remove"></i></button></div>'),r.url&&(a='<iframe src="'+r.url+'" tabindex="-1"></iframe>'),void 0===a&&(a=t.renderHtml(e)),e.statusbar&&(s=e.statusbar.renderHtml()),'<div id="'+n+'" class="'+e.classes+'" hidefocus="1"><div class="'+e.classPrefix+'reset" role="application">'+o+'<div id="'+n+'-body" class="'+e.bodyClasses+'">'+a+"</div>"+s+"</div></div>"},fullscreen:function(e){var n,t,i=this,r=_.document.documentElement,o=i.classPrefix;if(e!==i._fullscreen)if(ye(_.window).on("resize",function(){var e;if(i._fullscreen)if(n)i._timer||(i._timer=u.setTimeout(function(){var e=we.getWindowSize();i.moveTo(0,0).resizeTo(e.w,e.h),i._timer=0},50));else{e=(new Date).getTime();var t=we.getWindowSize();i.moveTo(0,0).resizeTo(t.w,t.h),50<(new Date).getTime()-e&&(n=!0)}}),t=i.layoutRect(),i._fullscreen=e){i._initial={x:t.x,y:t.y,w:t.w,h:t.h},i.borderBox=Me("0"),i.getEl("head").style.display="none",t.deltaH-=t.headerH+2,ye([r,_.document.body]).addClass(o+"fullscreen"),i.classes.add("fullscreen");var s=we.getWindowSize();i.moveTo(0,0).resizeTo(s.w,s.h)}else i.borderBox=Me(i.settings.border),i.getEl("head").style.display="",t.deltaH+=t.headerH,ye([r,_.document.body]).removeClass(o+"fullscreen"),i.classes.remove("fullscreen"),i.moveTo(i._initial.x,i._initial.y).resizeTo(i._initial.w,i._initial.h);return i.reflow()},postRender:function(){var t,n=this;setTimeout(function(){n.classes.add("in"),n.fire("open")},0),n._super(),n.statusbar&&n.statusbar.postRender(),n.focus(),this.dragHelper=new ct(n._id+"-dragh",{start:function(){t={x:n.layoutRect().x,y:n.layoutRect().y}},drag:function(e){n.moveTo(t.x+e.deltaX,t.y+e.deltaY)}}),n.on("submit",function(e){e.isDefaultPrevented()||n.close()}),At.push(n),Lt(!0)},submit:function(){return this.fire("submit",{data:this.toJSON()})},remove:function(){var e,t=this;for(t.dragHelper.destroy(),t._super(),t.statusbar&&this.statusbar.remove(),zt(t.classPrefix,!1),e=At.length;e--;)At[e]===t&&At.splice(e,1);Lt(0<At.length)},getContentWindow:function(){var e=this.getEl().getElementsByTagName("iframe")[0];return e?e.contentWindow:null}});!function(){if(!ce.desktop){var n={w:_.window.innerWidth,h:_.window.innerHeight};u.setInterval(function(){var e=_.window.innerWidth,t=_.window.innerHeight;n.w===e&&n.h===t||(n={w:e,h:t},ye(_.window).trigger("resize"))},100)}ye(_.window).on("resize",function(){var e,t,n=we.getWindowSize();for(e=0;e<At.length;e++)t=At[e].layoutRect(),At[e].moveTo(At[e].settings.x||Math.max(0,n.w/2-t.w/2),At[e].settings.y||Math.max(0,n.h/2-t.h/2))})}();var Ft,Ut,Vt,Yt=It.extend({init:function(e){e={border:1,padding:20,layout:"flex",pack:"center",align:"center",containerCls:"panel",autoScroll:!0,buttons:{type:"button",text:"Ok",action:"ok"},items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200}},this._super(e)},Statics:{OK:1,OK_CANCEL:2,YES_NO:3,YES_NO_CANCEL:4,msgBox:function(e){var t,i=e.callback||function(){};function n(e,t,n){return{type:"button",text:e,subtype:n?"primary":"",onClick:function(e){e.control.parents()[1].close(),i(t)}}}switch(e.buttons){case Yt.OK_CANCEL:t=[n("Ok",!0,!0),n("Cancel",!1)];break;case Yt.YES_NO:case Yt.YES_NO_CANCEL:t=[n("Yes",1,!0),n("No",0)],e.buttons===Yt.YES_NO_CANCEL&&t.push(n("Cancel",-1));break;default:t=[n("Ok",!0,!0)]}return new It({padding:20,x:e.x,y:e.y,minWidth:300,minHeight:100,layout:"flex",pack:"center",align:"center",buttons:t,title:e.title,role:"alertdialog",items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200,text:e.text},onPostRender:function(){this.aria("describedby",this.items()[0]._id)},onClose:e.onClose,onCancel:function(){i(!1)}}).renderTo(_.document.body).reflow()},alert:function(e,t){return"string"==typeof e&&(e={text:e}),e.callback=t,Yt.msgBox(e)},confirm:function(e,t){return"string"==typeof e&&(e={text:e}),e.callback=t,e.buttons=Yt.OK_CANCEL,Yt.msgBox(e)}}}),$t=function(n){return{renderUI:function(e){return Tt(n,this,e)},resizeTo:function(e,t){return le(n,e,t)},resizeBy:function(e,t){return ue(n,e,t)},getNotificationManagerImpl:function(){return Ot(n)},getWindowManagerImpl:function(){return{open:function(n,e,t){var i;return n.title=n.title||" ",n.url=n.url||n.file,n.url&&(n.width=parseInt(n.width||320,10),n.height=parseInt(n.height||240,10)),n.body&&(n.items={defaults:n.defaults,type:n.bodyType||"form",items:n.body,data:n.data,callbacks:n.commands}),n.url||n.buttons||(n.buttons=[{text:"Ok",subtype:"primary",onclick:function(){i.find("form")[0].submit()}},{text:"Cancel",onclick:function(){i.close()}}]),(i=new It(n)).on("close",function(){t(i)}),n.data&&i.on("postRender",function(){this.find("*").each(function(e){var t=e.name();t in n.data&&e.value(n.data[t])})}),i.features=n||{},i.params=e||{},i=i.renderTo(_.document.body).reflow()},alert:function(e,t,n){var i;return(i=Yt.alert(e,function(){t()})).on("close",function(){n(i)}),i},confirm:function(e,t,n){var i;return(i=Yt.confirm(e,function(e){t(e)})).on("close",function(){n(i)}),i},close:function(e){e.close()},getParams:function(e){return e.params},setParams:function(e,t){e.params=t}}}}},qt=Se.extend({Defaults:{firstControlClass:"first",lastControlClass:"last"},init:function(e){this.settings=w.extend({},this.Defaults,e)},preRender:function(e){e.bodyClasses.add(this.settings.containerClass)},applyClasses:function(e){var t,n,i,r,o=this.settings;t=o.firstControlClass,n=o.lastControlClass,e.each(function(e){e.classes.remove(t).remove(n).add(o.controlClass),e.visible()&&(i||(i=e),r=e)}),i&&i.classes.add(t),r&&r.classes.add(n)},renderHtml:function(e){var t="";return this.applyClasses(e.items()),e.items().each(function(e){t+=e.renderHtml()}),t},recalc:function(){},postRender:function(){},isNative:function(){return!1}}),Xt=qt.extend({Defaults:{containerClass:"abs-layout",controlClass:"abs-layout-item"},recalc:function(e){e.items().filter(":visible").each(function(e){var t=e.settings;e.layoutRect({x:t.x,y:t.y,w:t.w,h:t.h}),e.recalc&&e.recalc()})},renderHtml:function(e){return'<div id="'+e._id+'-absend" class="'+e.classPrefix+'abs-end"></div>'+this._super(e)}}),jt=Nt.extend({Defaults:{classes:"widget btn",role:"button"},init:function(e){var t,n=this;n._super(e),e=n.settings,t=n.settings.size,n.on("click mousedown",function(e){e.preventDefault()}),n.on("touchstart",function(e){n.fire("click",e),e.preventDefault()}),e.subtype&&n.classes.add(e.subtype),t&&n.classes.add("btn-"+t),e.icon&&n.icon(e.icon)},icon:function(e){return arguments.length?(this.state.set("icon",e),this):this.state.get("icon")},repaint:function(){var e,t=this.getEl().firstChild;t&&((e=t.style).width=e.height="100%"),this._super()},renderHtml:function(){var e,t,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a="",l=n.settings;return(e=l.image)?(o="none","string"!=typeof e&&(e=_.window.getSelection?e[0]:e[1]),e=" style=\"background-image: url('"+e+"')\""):e="",s&&(n.classes.add("btn-has-text"),a='<span class="'+r+'txt">'+n.encode(s)+"</span>"),o=o?r+"ico "+r+"i-"+o:"",t="boolean"==typeof l.active?' aria-pressed="'+l.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" tabindex="-1"'+t+'><button id="'+i+'-button" role="presentation" type="button" tabindex="-1">'+(o?'<i class="'+o+'"'+e+"></i>":"")+a+"</button></div>"},bindStates:function(){var o=this,n=o.$,i=o.classPrefix+"txt";function s(e){var t=n("span."+i,o.getEl());e?(t[0]||(n("button:first",o.getEl()).append('<span class="'+i+'"></span>'),t=n("span."+i,o.getEl())),t.html(o.encode(e))):t.remove(),o.classes.toggle("btn-has-text",!!e)}return o.state.on("change:text",function(e){s(e.value)}),o.state.on("change:icon",function(e){var t=e.value,n=o.classPrefix;t=(o.settings.icon=t)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];t?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=t):r&&i.removeChild(r),s(o.state.get("text"))}),o._super()}}),Jt=jt.extend({init:function(e){e=w.extend({text:"Browse...",multiple:!1,accept:null},e),this._super(e),this.classes.add("browsebutton"),e.multiple&&this.classes.add("multiple")},postRender:function(){var n=this,t=we.create("input",{type:"file",id:n._id+"-browse",accept:n.settings.accept});n._super(),ye(t).on("change",function(e){var t=e.target.files;n.value=function(){return t.length?n.settings.multiple?t:t[0]:null},e.preventDefault(),t.length&&n.fire("change",e)}),ye(t).on("click",function(e){e.stopPropagation()}),ye(n.getEl("button")).on("click touchstart",function(e){e.stopPropagation(),t.click(),e.preventDefault()}),n.getEl().appendChild(t)},remove:function(){ye(this.getEl("button")).off(),ye(this.getEl("input")).off(),this._super()}}),Gt=lt.extend({Defaults:{defaultType:"button",role:"group"},renderHtml:function(){var e=this,t=e._layout;return e.classes.add("btn-group"),e.preRender(),t.preRender(e),'<div id="'+e._id+'" class="'+e.classes+'"><div id="'+e._id+'-body">'+(e.settings.html||"")+t.renderHtml(e)+"</div></div>"}}),Kt=Nt.extend({Defaults:{classes:"checkbox",role:"checkbox",checked:!1},init:function(e){var t=this;t._super(e),t.on("click mousedown",function(e){e.preventDefault()}),t.on("click",function(e){e.preventDefault(),t.disabled()||t.checked(!t.checked())}),t.checked(t.settings.checked)},checked:function(e){return arguments.length?(this.state.set("checked",e),this):this.state.get("checked")},value:function(e){return arguments.length?this.checked(e):this.checked()},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix;return'<div id="'+t+'" class="'+e.classes+'" unselectable="on" aria-labelledby="'+t+'-al" tabindex="-1"><i class="'+n+"ico "+n+'i-checkbox"></i><span id="'+t+'-al" class="'+n+'label">'+e.encode(e.state.get("text"))+"</span></div>"},bindStates:function(){var o=this;function t(e){o.classes.toggle("checked",e),o.aria("checked",e)}return o.state.on("change:text",function(e){o.getEl("al").firstChild.data=o.translate(e.value)}),o.state.on("change:checked change:value",function(e){o.fire("change"),t(e.value)}),o.state.on("change:icon",function(e){var t=e.value,n=o.classPrefix;if(void 0===t)return o.settings.icon;t=(o.settings.icon=t)?n+"ico "+n+"i-"+o.settings.icon:"";var i=o.getEl().firstChild,r=i.getElementsByTagName("i")[0];t?(r&&r===i.firstChild||(r=_.document.createElement("i"),i.insertBefore(r,i.firstChild)),r.className=t):r&&i.removeChild(r)}),o.state.get("checked")&&t(!0),o._super()}}),Zt=tinymce.util.Tools.resolve("tinymce.util.VK"),Qt=Nt.extend({init:function(i){var r=this;r._super(i),i=r.settings,r.classes.add("combobox"),r.subinput=!0,r.ariaTarget="inp",i.menu=i.menu||i.values,i.menu&&(i.icon="caret"),r.on("click",function(e){var t=e.target,n=r.getEl();if(ye.contains(n,t)||t===n)for(;t&&t!==n;)t.id&&-1!==t.id.indexOf("-open")&&(r.fire("action"),i.menu&&(r.showMenu(),e.aria&&r.menu.items()[0].focus())),t=t.parentNode}),r.on("keydown",function(e){var t;13===e.keyCode&&"INPUT"===e.target.nodeName&&(e.preventDefault(),r.parents().reverse().each(function(e){if(e.toJSON)return t=e,!1}),r.fire("submit",{data:t.toJSON()}))}),r.on("keyup",function(e){if("INPUT"===e.target.nodeName){var t=r.state.get("value"),n=e.target.value;n!==t&&(r.state.set("value",n),r.fire("autocomplete",e))}}),r.on("mouseover",function(e){var t=r.tooltip().moveTo(-65535);if(r.statusLevel()&&-1!==e.target.className.indexOf(r.classPrefix+"status")){var n=r.statusMessage()||"Ok",i=t.text(n).show().testMoveRel(e.target,["bc-tc","bc-tl","bc-tr"]);t.classes.toggle("tooltip-n","bc-tc"===i),t.classes.toggle("tooltip-nw","bc-tl"===i),t.classes.toggle("tooltip-ne","bc-tr"===i),t.moveRel(e.target,i)}})},statusLevel:function(e){return 0<arguments.length&&this.state.set("statusLevel",e),this.state.get("statusLevel")},statusMessage:function(e){return 0<arguments.length&&this.state.set("statusMessage",e),this.state.get("statusMessage")},showMenu:function(){var e,t=this,n=t.settings;t.menu||((e=n.menu||[]).length?e={type:"menu",items:e}:e.type=e.type||"menu",t.menu=v.create(e).parent(t).renderTo(t.getContainerElm()),t.fire("createmenu"),t.menu.reflow(),t.menu.on("cancel",function(e){e.control===t.menu&&t.focus()}),t.menu.on("show hide",function(e){e.control.items().each(function(e){e.active(e.value()===t.value())})}).fire("show"),t.menu.on("select",function(e){t.value(e.control.value())}),t.on("focusin",function(e){"INPUT"===e.target.tagName.toUpperCase()&&t.menu.hide()}),t.aria("expanded",!0)),t.menu.show(),t.menu.layoutRect({w:t.layoutRect().w}),t.menu.moveRel(t.getEl(),t.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},focus:function(){this.getEl("inp").focus()},repaint:function(){var e,t,n=this,i=n.getEl(),r=n.getEl("open"),o=n.layoutRect(),s=0,a=i.firstChild;n.statusLevel()&&"none"!==n.statusLevel()&&(s=parseInt(we.getRuntimeStyle(a,"padding-right"),10)-parseInt(we.getRuntimeStyle(a,"padding-left"),10)),e=r?o.w-we.getSize(r).width-10:o.w-10;var l=_.document;return l.all&&(!l.documentMode||l.documentMode<=8)&&(t=n.layoutRect().h-2+"px"),ye(a).css({width:e-s,lineHeight:t}),n._super(),n},postRender:function(){var t=this;return ye(this.getEl("inp")).on("change",function(e){t.state.set("value",e.target.value),t.fire("change",e)}),t._super()},renderHtml:function(){var e,t,n,i=this,r=i._id,o=i.settings,s=i.classPrefix,a=i.state.get("value")||"",l="",u="";return"spellcheck"in o&&(u+=' spellcheck="'+o.spellcheck+'"'),o.maxLength&&(u+=' maxlength="'+o.maxLength+'"'),o.size&&(u+=' size="'+o.size+'"'),o.subtype&&(u+=' type="'+o.subtype+'"'),n='<i id="'+r+'-status" class="mce-status mce-ico" style="display: none"></i>',i.disabled()&&(u+=' disabled="disabled"'),(e=o.icon)&&"caret"!==e&&(e=s+"ico "+s+"i-"+o.icon),t=i.state.get("text"),(e||t)&&(l='<div id="'+r+'-open" class="'+s+"btn "+s+'open" tabIndex="-1" role="button"><button id="'+r+'-action" type="button" hidefocus="1" tabindex="-1">'+("caret"!==e?'<i class="'+e+'"></i>':'<i class="'+s+'caret"></i>')+(t?(e?" ":"")+t:"")+"</button></div>",i.classes.add("has-open")),'<div id="'+r+'" class="'+i.classes+'"><input id="'+r+'-inp" class="'+s+'textbox" value="'+i.encode(a,!1)+'" hidefocus="1"'+u+' placeholder="'+i.encode(o.placeholder)+'" />'+n+l+"</div>"},value:function(e){return arguments.length?(this.state.set("value",e),this):(this.state.get("rendered")&&this.state.set("value",this.getEl("inp").value),this.state.get("value"))},showAutoComplete:function(e,i){var r=this;if(0!==e.length){r.menu?r.menu.items().remove():r.menu=v.create({type:"menu",classes:"combobox-menu",layout:"flow"}).parent(r).renderTo(),w.each(e,function(e){var t,n;r.menu.add({text:e.title,url:e.previewUrl,match:i,classes:"menu-item-ellipsis",onclick:(t=e.value,n=e.title,function(){r.fire("selectitem",{title:n,value:t})})})}),r.menu.renderNew(),r.hideMenu(),r.menu.on("cancel",function(e){e.control.parent()===r.menu&&(e.stopPropagation(),r.focus(),r.hideMenu())}),r.menu.on("select",function(){r.focus()});var t=r.layoutRect().w;r.menu.layoutRect({w:t,minW:0,maxW:t}),r.menu.repaint(),r.menu.reflow(),r.menu.show(),r.menu.moveRel(r.getEl(),r.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])}else r.hideMenu()},hideMenu:function(){this.menu&&this.menu.hide()},bindStates:function(){var r=this;r.state.on("change:value",function(e){r.getEl("inp").value!==e.value&&(r.getEl("inp").value=e.value)}),r.state.on("change:disabled",function(e){r.getEl("inp").disabled=e.value}),r.state.on("change:statusLevel",function(e){var t=r.getEl("status"),n=r.classPrefix,i=e.value;we.css(t,"display","none"===i?"none":""),we.toggleClass(t,n+"i-checkmark","ok"===i),we.toggleClass(t,n+"i-warning","warn"===i),we.toggleClass(t,n+"i-error","error"===i),r.classes.toggle("has-status","none"!==i),r.repaint()}),we.on(r.getEl("status"),"mouseleave",function(){r.tooltip().hide()}),r.on("cancel",function(e){r.menu&&r.menu.visible()&&(e.stopPropagation(),r.hideMenu())});var n=function(e,t){t&&0<t.items().length&&t.items().eq(e)[0].focus()};return r.on("keydown",function(e){var t=e.keyCode;"INPUT"===e.target.nodeName&&(t===Zt.DOWN?(e.preventDefault(),r.fire("autocomplete"),n(0,r.menu)):t===Zt.UP&&(e.preventDefault(),n(-1,r.menu)))}),r._super()},remove:function(){ye(this.getEl("inp")).off(),this.menu&&this.menu.remove(),this._super()}}),en=Qt.extend({init:function(e){var t=this;e.spellcheck=!1,e.onaction&&(e.icon="none"),t._super(e),t.classes.add("colorbox"),t.on("change keyup postrender",function(){t.repaintColor(t.value())})},repaintColor:function(e){var t=this.getEl("open"),n=t?t.getElementsByTagName("i")[0]:null;if(n)try{n.style.background=e}catch(i){}},bindStates:function(){var t=this;return t.state.on("change:value",function(e){t.state.get("rendered")&&t.repaintColor(e.value)}),t._super()}}),tn=jt.extend({showPanel:function(){var t=this,e=t.settings;if(t.classes.add("opened"),t.panel)t.panel.show();else{var n=e.panel;n.type&&(n={layout:"grid",items:n}),n.role=n.role||"dialog",n.popover=!0,n.autohide=!0,n.ariaRoot=!0,t.panel=new Ct(n).on("hide",function(){t.classes.remove("opened")}).on("cancel",function(e){e.stopPropagation(),t.focus(),t.hidePanel()}).parent(t).renderTo(t.getContainerElm()),t.panel.fire("show"),t.panel.reflow()}var i=t.panel.testMoveRel(t.getEl(),e.popoverAlign||(t.isRtl()?["bc-tc","bc-tl","bc-tr"]:["bc-tc","bc-tr","bc-tl","tc-bc","tc-br","tc-bl"]));t.panel.classes.toggle("start","l"===i.substr(-1)),t.panel.classes.toggle("end","r"===i.substr(-1));var r="t"===i.substr(0,1);t.panel.classes.toggle("bottom",!r),t.panel.classes.toggle("top",r),t.panel.moveRel(t.getEl(),i)},hidePanel:function(){this.panel&&this.panel.hide()},postRender:function(){var t=this;return t.aria("haspopup",!0),t.on("click",function(e){e.control===t&&(t.panel&&t.panel.visible()?t.hidePanel():(t.showPanel(),t.panel.focus(!!e.aria)))}),t._super()},remove:function(){return this.panel&&(this.panel.remove(),this.panel=null),this._super()}}),nn=p.DOM,rn=tn.extend({init:function(e){this._super(e),this.classes.add("splitbtn"),this.classes.add("colorbutton")},color:function(e){return e?(this._color=e,this.getEl("preview").style.backgroundColor=e,this):this._color},resetColor:function(){return this._color=null,this.getEl("preview").style.backgroundColor=null,this},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix,i=e.state.get("text"),r=e.settings.icon?n+"ico "+n+"i-"+e.settings.icon:"",o=e.settings.image?" style=\"background-image: url('"+e.settings.image+"')\"":"",s="";return i&&(e.classes.add("btn-has-text"),s='<span class="'+n+'txt">'+e.encode(i)+"</span>"),'<div id="'+t+'" class="'+e.classes+'" role="button" tabindex="-1" aria-haspopup="true"><button role="presentation" hidefocus="1" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+o+"></i>":"")+'<span id="'+t+'-preview" class="'+n+'preview"></span>'+s+'</button><button type="button" class="'+n+'open" hidefocus="1" tabindex="-1"> <i class="'+n+'caret"></i></button></div>'},postRender:function(){var t=this,n=t.settings.onclick;return t.on("click",function(e){e.aria&&"down"===e.aria.key||e.control!==t||nn.getParent(e.target,"."+t.classPrefix+"open")||(e.stopImmediatePropagation(),n.call(t,e))}),delete t.settings.onclick,t._super()}}),on=tinymce.util.Tools.resolve("tinymce.util.Color"),sn=Nt.extend({Defaults:{classes:"widget colorpicker"},init:function(e){this._super(e)},postRender:function(){var n,i,r,o,s,a=this,l=a.color();function u(e,t){var n,i,r=we.getPos(e);return n=t.pageX-r.x,i=t.pageY-r.y,{x:n=Math.max(0,Math.min(n/e.clientWidth,1)),y:i=Math.max(0,Math.min(i/e.clientHeight,1))}}function c(e,t){var n=(360-e.h)/360;we.css(r,{top:100*n+"%"}),t||we.css(s,{left:e.s+"%",top:100-e.v+"%"}),o.style.background=on({s:100,v:100,h:e.h}).toHex(),a.color().parse({s:e.s,v:e.v,h:e.h})}function e(e){var t;t=u(o,e),n.s=100*t.x,n.v=100*(1-t.y),c(n),a.fire("change")}function t(e){var t;t=u(i,e),(n=l.toHsv()).h=360*(1-t.y),c(n,!0),a.fire("change")}i=a.getEl("h"),r=a.getEl("hp"),o=a.getEl("sv"),s=a.getEl("svp"),a._repaint=function(){c(n=l.toHsv())},a._super(),a._svdraghelper=new ct(a._id+"-sv",{start:e,drag:e}),a._hdraghelper=new ct(a._id+"-h",{start:t,drag:t}),a._repaint()},rgb:function(){return this.color().toRgb()},value:function(e){if(!arguments.length)return this.color().toHex();this.color().parse(e),this._rendered&&this._repaint()},color:function(){return this._color||(this._color=on()),this._color},renderHtml:function(){var e,t=this._id,o=this.classPrefix,s="#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000";return e='<div id="'+t+'-h" class="'+o+'colorpicker-h" style="background: -ms-linear-gradient(top,'+s+");background: linear-gradient(to bottom,"+s+');">'+function(){var e,t,n,i,r="";for(n="filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=",e=0,t=(i=s.split(",")).length-1;e<t;e++)r+='<div class="'+o+'colorpicker-h-chunk" style="height:'+100/t+"%;"+n+i[e]+",endColorstr="+i[e+1]+");-ms-"+n+i[e]+",endColorstr="+i[e+1]+')"></div>';return r}()+'<div id="'+t+'-hp" class="'+o+'colorpicker-h-marker"></div></div>','<div id="'+t+'" class="'+this.classes+'"><div id="'+t+'-sv" class="'+o+'colorpicker-sv"><div class="'+o+'colorpicker-overlay1"><div class="'+o+'colorpicker-overlay2"><div id="'+t+'-svp" class="'+o+'colorpicker-selector1"><div class="'+o+'colorpicker-selector2"></div></div></div></div></div>'+e+"</div>"}}),an=Nt.extend({init:function(e){e=w.extend({height:100,text:"Drop an image here",multiple:!1,accept:null},e),this._super(e),this.classes.add("dropzone"),e.multiple&&this.classes.add("multiple")},renderHtml:function(){var e,t,n=this.settings;return e={id:this._id,hidefocus:"1"},t=we.create("div",e,"<span>"+this.translate(n.text)+"</span>"),n.height&&we.css(t,"height",n.height+"px"),n.width&&we.css(t,"width",n.width+"px"),t.className=this.classes,t.outerHTML},postRender:function(){var i=this,e=function(e){e.preventDefault(),i.classes.toggle("dragenter"),i.getEl().className=i.classes};i._super(),i.$el.on("dragover",function(e){e.preventDefault()}),i.$el.on("dragenter",e),i.$el.on("dragleave",e),i.$el.on("drop",function(e){if(e.preventDefault(),!i.state.get("disabled")){var t=function(e){var t=i.settings.accept;if("string"!=typeof t)return e;var n=new RegExp("("+t.split(/\s*,\s*/).join("|")+")$","i");return w.grep(e,function(e){return n.test(e.name)})}(e.dataTransfer.files);i.value=function(){return t.length?i.settings.multiple?t:t[0]:null},t.length&&i.fire("change",e)}})},remove:function(){this.$el.off(),this._super()}}),ln=Nt.extend({init:function(e){var n=this;e.delimiter||(e.delimiter="\xbb"),n._super(e),n.classes.add("path"),n.canFocus=!0,n.on("click",function(e){var t;(t=e.target.getAttribute("data-index"))&&n.fire("select",{value:n.row()[t],index:t})}),n.row(n.settings.row)},focus:function(){return this.getEl().firstChild.focus(),this},row:function(e){return arguments.length?(this.state.set("row",e),this):this.state.get("row")},renderHtml:function(){return'<div id="'+this._id+'" class="'+this.classes+'">'+this._getDataPathHtml(this.state.get("row"))+"</div>"},bindStates:function(){var t=this;return t.state.on("change:row",function(e){t.innerHtml(t._getDataPathHtml(e.value))}),t._super()},_getDataPathHtml:function(e){var t,n,i=e||[],r="",o=this.classPrefix;for(t=0,n=i.length;t<n;t++)r+=(0<t?'<div class="'+o+'divider" aria-hidden="true"> '+this.settings.delimiter+" </div>":"")+'<div role="button" class="'+o+"path-item"+(t===n-1?" "+o+"last":"")+'" data-index="'+t+'" tabindex="-1" id="'+this._id+"-"+t+'" aria-level="'+(t+1)+'">'+i[t].name+"</div>";return r||(r='<div class="'+o+'path-item">\xa0</div>'),r}}),un=ln.extend({postRender:function(){var o=this,s=o.settings.editor;function a(e){if(1===e.nodeType){if("BR"===e.nodeName||e.getAttribute("data-mce-bogus"))return!0;if("bookmark"===e.getAttribute("data-mce-type"))return!0}return!1}return!1!==s.settings.elementpath&&(o.on("select",function(e){s.focus(),s.selection.select(this.row()[e.index].element),s.nodeChanged()}),s.on("nodeChange",function(e){for(var t=[],n=e.parents,i=n.length;i--;)if(1===n[i].nodeType&&!a(n[i])){var r=s.fire("ResolveName",{name:n[i].nodeName.toLowerCase(),target:n[i]});if(r.isDefaultPrevented()||t.push({name:r.name,element:n[i]}),r.isPropagationStopped())break}o.row(t)})),o._super()}}),cn=lt.extend({Defaults:{layout:"flex",align:"center",defaults:{flex:1}},renderHtml:function(){var e=this,t=e._layout,n=e.classPrefix;return e.classes.add("formitem"),t.preRender(e),'<div id="'+e._id+'" class="'+e.classes+'" hidefocus="1" tabindex="-1">'+(e.settings.title?'<div id="'+e._id+'-title" class="'+n+'title">'+e.settings.title+"</div>":"")+'<div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+(e.settings.html||"")+t.renderHtml(e)+"</div></div>"}}),dn=lt.extend({Defaults:{containerCls:"form",layout:"flex",direction:"column",align:"stretch",flex:1,padding:15,labelGap:30,spacing:10,callbacks:{submit:function(){this.submit()}}},preRender:function(){var i=this,e=i.items();i.settings.formItemDefaults||(i.settings.formItemDefaults={layout:"flex",autoResize:"overflow",defaults:{flex:1}}),e.each(function(e){var t,n=e.settings.label;n&&((t=new cn(w.extend({items:{type:"label",id:e._id+"-l",text:n,flex:0,forId:e._id,disabled:e.disabled()}},i.settings.formItemDefaults))).type="formitem",e.aria("labelledby",e._id+"-l"),"undefined"==typeof e.settings.flex&&(e.settings.flex=1),i.replace(e,t),t.add(e))})},submit:function(){return this.fire("submit",{data:this.toJSON()})},postRender:function(){this._super(),this.fromJSON(this.settings.data)},bindStates:function(){var n=this;function e(){var e,t,i=0,r=[];if(!1!==n.settings.labelGapCalc)for(("children"===n.settings.labelGapCalc?n.find("formitem"):n.items()).filter("formitem").each(function(e){var t=e.items()[0],n=t.getEl().clientWidth;i=i<n?n:i,r.push(t)}),t=n.settings.labelGap||0,e=r.length;e--;)r[e].settings.minWidth=i+t}n._super(),n.on("show",e),e()}}),fn=dn.extend({Defaults:{containerCls:"fieldset",layout:"flex",direction:"column",align:"stretch",flex:1,padding:"25 15 5 15",labelGap:30,spacing:10,border:1},renderHtml:function(){var e=this,t=e._layout,n=e.classPrefix;return e.preRender(),t.preRender(e),'<fieldset id="'+e._id+'" class="'+e.classes+'" hidefocus="1" tabindex="-1">'+(e.settings.title?'<legend id="'+e._id+'-title" class="'+n+'fieldset-title">'+e.settings.title+"</legend>":"")+'<div id="'+e._id+'-body" class="'+e.bodyClasses+'">'+(e.settings.html||"")+t.renderHtml(e)+"</div></fieldset>"}}),hn=0,mn=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:k(e)}},gn={fromHtml:function(e,t){var n=(t||_.document).createElement("div");if(n.innerHTML=e,!n.hasChildNodes()||1<n.childNodes.length)throw _.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return mn(n.childNodes[0])},fromTag:function(e,t){var n=(t||_.document).createElement(e);return mn(n)},fromText:function(e,t){var n=(t||_.document).createTextNode(e);return mn(n)},fromDom:mn,fromPoint:function(e,t,n){var i=e.dom();return N.from(i.elementFromPoint(t,n)).map(mn)}},pn=(_.Node.ATTRIBUTE_NODE,_.Node.CDATA_SECTION_NODE,_.Node.COMMENT_NODE,_.Node.DOCUMENT_NODE),vn=(_.Node.DOCUMENT_TYPE_NODE,_.Node.DOCUMENT_FRAGMENT_NODE,_.Node.ELEMENT_NODE),bn=(_.Node.TEXT_NODE,_.Node.PROCESSING_INSTRUCTION_NODE,_.Node.ENTITY_REFERENCE_NODE,_.Node.ENTITY_NODE,_.Node.NOTATION_NODE,"undefined"!=typeof _.window?_.window:Function("return this;")(),function(e,t){var n=function(e,t){for(var n=0;n<e.length;n++){var i=e[n];if(i.test(t))return i}return undefined}(e,t);if(!n)return{major:0,minor:0};var i=function(e){return Number(t.replace(n,"$"+e))};return xn(i(1),i(2))}),yn=function(){return xn(0,0)},xn=function(e,t){return{major:e,minor:t}},wn={nu:xn,detect:function(e,t){var n=String(t).toLowerCase();return 0===e.length?yn():bn(e,n)},unknown:yn},_n="Firefox",Rn=function(e,t){return function(){return t===e}},Cn=function(e){var t=e.current;return{current:t,version:e.version,isEdge:Rn("Edge",t),isChrome:Rn("Chrome",t),isIE:Rn("IE",t),isOpera:Rn("Opera",t),isFirefox:Rn(_n,t),isSafari:Rn("Safari",t)}},En={unknown:function(){return Cn({current:undefined,version:wn.unknown()})},nu:Cn,edge:k("Edge"),chrome:k("Chrome"),ie:k("IE"),opera:k("Opera"),firefox:k(_n),safari:k("Safari")},kn="Windows",Hn="Android",Sn="Solaris",Tn="FreeBSD",Mn=function(e,t){return function(){return t===e}},Nn=function(e){var t=e.current;return{current:t,version:e.version,isWindows:Mn(kn,t),isiOS:Mn("iOS",t),isAndroid:Mn(Hn,t),isOSX:Mn("OSX",t),isLinux:Mn("Linux",t),isSolaris:Mn(Sn,t),isFreeBSD:Mn(Tn,t)}},Pn={unknown:function(){return Nn({current:undefined,version:wn.unknown()})},nu:Nn,windows:k(kn),ios:k("iOS"),android:k(Hn),linux:k("Linux"),osx:k("OSX"),solaris:k(Sn),freebsd:k(Tn)},Wn=function(e,t){var n=String(t).toLowerCase();return function(e,t){for(var n=0,i=e.length;n<i;n++){var r=e[n];if(t(r,n))return N.some(r)}return N.none()}(e,function(e){return e.search(n)})},Dn=function(e,n){return Wn(e,n).map(function(e){var t=wn.detect(e.versionRegexes,n);return{current:e.name,version:t}})},On=function(e,n){return Wn(e,n).map(function(e){var t=wn.detect(e.versionRegexes,n);return{current:e.name,version:t}})},An=function(e,t){return-1!==e.indexOf(t)},Bn=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,Ln=function(t){return function(e){return An(e,t)}},zn=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return An(e,"edge/")&&An(e,"chrome")&&An(e,"safari")&&An(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,Bn],search:function(e){return An(e,"chrome")&&!An(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return An(e,"msie")||An(e,"trident")}},{name:"Opera",versionRegexes:[Bn,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:Ln("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:Ln("firefox")},{name:"Safari",versionRegexes:[Bn,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return(An(e,"safari")||An(e,"mobile/"))&&An(e,"applewebkit")}}],In=[{name:"Windows",search:Ln("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return An(e,"iphone")||An(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:Ln("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:Ln("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:Ln("linux"),versionRegexes:[]},{name:"Solaris",search:Ln("sunos"),versionRegexes:[]},{name:"FreeBSD",search:Ln("freebsd"),versionRegexes:[]}],Fn={browsers:k(zn),oses:k(In)},Un=function(e){var t,n,i,r,o,s,a,l,u,c,d,f=Fn.browsers(),h=Fn.oses(),m=Dn(f,e).fold(En.unknown,En.nu),g=On(h,e).fold(Pn.unknown,Pn.nu);return{browser:m,os:g,deviceType:(n=m,i=e,r=(t=g).isiOS()&&!0===/ipad/i.test(i),o=t.isiOS()&&!r,s=t.isAndroid()&&3===t.version.major,a=t.isAndroid()&&4===t.version.major,l=r||s||a&&!0===/mobile/i.test(i),u=t.isiOS()||t.isAndroid(),c=u&&!l,d=n.isSafari()&&t.isiOS()&&!1===/safari/i.test(i),{isiPad:k(r),isiPhone:k(o),isTablet:k(l),isPhone:k(c),isTouch:k(u),isAndroid:t.isAndroid,isiOS:t.isiOS,isWebView:k(d)})}},Vn=(Vt=!(Ft=function(){var e=_.navigator.userAgent;return Un(e)}),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return Vt||(Vt=!0,Ut=Ft.apply(null,e)),Ut}),Yn=vn,$n=pn,qn=function(e){return e.nodeType!==Yn&&e.nodeType!==$n||0===e.childElementCount},Xn=(Vn().browser.isIE(),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t]}("element","offset"),w.trim),jn=function(t){return function(e){if(e&&1===e.nodeType){if(e.contentEditable===t)return!0;if(e.getAttribute("data-mce-contenteditable")===t)return!0}return!1}},Jn=jn("true"),Gn=jn("false"),Kn=function(e,t,n,i,r){return{type:e,title:t,url:n,level:i,attach:r}},Zn=function(e){return e.innerText||e.textContent},Qn=function(e){return e.id?e.id:(t="h",n=(new Date).getTime(),t+"_"+Math.floor(1e9*Math.random())+ ++hn+String(n));var t,n},ei=function(e){return(t=e)&&"A"===t.nodeName&&(t.id||t.name)&&ni(e);var t},ti=function(e){return e&&/^(H[1-6])$/.test(e.nodeName)},ni=function(e){return function(e){for(;e=e.parentNode;){var t=e.contentEditable;if(t&&"inherit"!==t)return Jn(e)}return!1}(e)&&!Gn(e)},ii=function(e){return ti(e)&&ni(e)},ri=function(e){var t,n=Qn(e);return Kn("header",Zn(e),"#"+n,ti(t=e)?parseInt(t.nodeName.substr(1),10):0,function(){e.id=n})},oi=function(e){var t=e.id||e.name,n=Zn(e);return Kn("anchor",n||"#"+t,"#"+t,0,E)},si=function(e){var t,n,i,r,o,s;return t="h1,h2,h3,h4,h5,h6,a:not([href])",n=e,G((Vn().browser.isIE(),function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t]}("element","offset"),i=gn.fromDom(n),r=t,s=(o=i)===undefined?_.document:o.dom(),qn(s)?[]:G(s.querySelectorAll(r),gn.fromDom)),function(e){return e.dom()})},ai=function(e){return 0<Xn(e.title).length},li=function(e){var t,n=si(e);return Z((t=n,G(Z(t,ii),ri)).concat(G(Z(n,ei),oi)),ai)},ui={},ci=function(e){return{title:e.title,value:{title:{raw:e.title},url:e.url,attach:e.attach}}},di=function(e,t){return{title:e,value:{title:e,url:t,attach:E}}},fi=function(e,t,n){var i=t in e?e[t]:n;return!1===i?null:i},hi=function(e,i,r,t){var n,o,s,a,l,u,c={title:"-"},d=function(e){var t=e.hasOwnProperty(r)?e[r]:[],n=Z(t,function(e){return t=e,!J(i,function(e){return e.url===t});var t});return w.map(n,function(e){return{title:e,value:{title:e,url:e,attach:E}}})},f=function(t){var e,n=Z(i,function(e){return e.type===t});return e=n,w.map(e,ci)};return!1===t.typeahead_urls?[]:"file"===r?(n=[mi(e,d(ui)),mi(e,f("header")),mi(e,(a=f("anchor"),l=fi(t,"anchor_top","#top"),u=fi(t,"anchor_bottom","#bottom"),null!==l&&a.unshift(di("<top>",l)),null!==u&&a.push(di("<bottom>",u)),a))],o=function(e,t){return 0===e.length||0===t.length?e.concat(t):e.concat(c,t)},s=[],K(n,function(e){s=o(s,e)}),s):mi(e,d(ui))},mi=function(e,t){var n=e.toLowerCase(),i=w.grep(t,function(e){return-1!==e.title.toLowerCase().indexOf(n)});return 1===i.length&&i[0].title===e?[]:i},gi=function(r,i,o,s){var t=function(e){var t=li(o),n=hi(e,t,s,i);r.showAutoComplete(n,e)};r.on("autocomplete",function(){t(r.value())}),r.on("selectitem",function(e){var t=e.value;r.value(t.url);var n,i=(n=t.title).raw?n.raw:n;"image"===s?r.fire("change",{meta:{alt:i,attach:t.attach}}):r.fire("change",{meta:{text:i,attach:t.attach}}),r.focus()}),r.on("click",function(e){0===r.value().length&&"INPUT"===e.target.nodeName&&t("")}),r.on("PostRender",function(){r.getRoot().on("submit",function(e){var t,n,i;e.isDefaultPrevented()||(t=r.value(),i=ui[n=s],/^https?/.test(t)&&(i?j(i,t).isNone()&&(ui[n]=i.slice(0,5).concat(t)):ui[n]=[t]))})})},pi=function(o,e,n){var i=e.filepicker_validator_handler;i&&o.state.on("change:value",function(e){var t;0!==(t=e.value).length?i({url:t,type:n},function(e){var t,n,i,r=(n=(t=e).status,i=t.message,"valid"===n?{status:"ok",message:i}:"unknown"===n?{status:"warn",message:i}:"invalid"===n?{status:"warn",message:i}:{status:"none",message:""});o.statusMessage(r.message),o.statusLevel(r.status)}):o.statusLevel("none")})},vi=Qt.extend({Statics:{clearHistory:function(){ui={}}},init:function(e){var t,n,i,r=this,o=window.tinymce?window.tinymce.activeEditor:l.activeEditor,s=o.settings,a=e.filetype;e.spellcheck=!1,(i=s.file_picker_types||s.file_browser_callback_types)&&(i=w.makeMap(i,/[, ]/)),i&&!i[a]||(!(n=s.file_picker_callback)||i&&!i[a]?!(n=s.file_browser_callback)||i&&!i[a]||(t=function(){n(r.getEl("inp").id,r.value(),a,window)}):t=function(){var e=r.fire("beforecall").meta;e=w.extend({filetype:a},e),n.call(o,function(e,t){r.value(e).fire("change",{meta:t})},r.value(),e)}),t&&(e.icon="browse",e.onaction=t),r._super(e),r.classes.add("filepicker"),gi(r,s,o.getBody(),a),pi(r,s,a)}}),bi=Xt.extend({recalc:function(e){var t=e.layoutRect(),n=e.paddingBox;e.items().filter(":visible").each(function(e){e.layoutRect({x:n.left,y:n.top,w:t.innerW-n.right-n.left,h:t.innerH-n.top-n.bottom}),e.recalc&&e.recalc()})}}),yi=Xt.extend({recalc:function(e){var t,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,b,y,x,w,_,R,C,E,k,H,S,T,M,N,P,W,D,O,A,B,L=[],z=Math.max,I=Math.min;for(i=e.items().filter(":visible"),r=e.layoutRect(),o=e.paddingBox,s=e.settings,f=e.isRtl()?s.direction||"row-reversed":s.direction,a=s.align,l=e.isRtl()?s.pack||"end":s.pack,u=s.spacing||0,"row-reversed"!==f&&"column-reverse"!==f||(i=i.set(i.toArray().reverse()),f=f.split("-")[0]),"column"===f?(C="y",_="h",R="minH",E="maxH",H="innerH",k="top",S="deltaH",T="contentH",D="left",P="w",M="x",N="innerW",W="minW",O="right",A="deltaW",B="contentW"):(C="x",_="w",R="minW",E="maxW",H="innerW",k="left",S="deltaW",T="contentW",D="top",P="h",M="y",N="innerH",W="minH",O="bottom",A="deltaH",B="contentH"),d=r[H]-o[k]-o[k],w=c=0,t=0,n=i.length;t<n;t++)m=(h=i[t]).layoutRect(),d-=t<n-1?u:0,0<(g=h.settings.flex)&&(c+=g,m[E]&&L.push(h),m.flex=g),d-=m[R],w<(p=o[D]+m[W]+o[O])&&(w=p);if((y={})[R]=d<0?r[R]-d+r[S]:r[H]-d+r[S],y[W]=w+r[A],y[T]=r[H]-d,y[B]=w,y.minW=I(y.minW,r.maxW),y.minH=I(y.minH,r.maxH),y.minW=z(y.minW,r.startMinWidth),y.minH=z(y.minH,r.startMinHeight),!r.autoResize||y.minW===r.minW&&y.minH===r.minH){for(b=d/c,t=0,n=L.length;t<n;t++)(v=(m=(h=L[t]).layoutRect())[E])<(p=m[R]+m.flex*b)?(d-=m[E]-m[R],c-=m.flex,m.flex=0,m.maxFlexSize=v):m.maxFlexSize=0;for(b=d/c,x=o[k],y={},0===c&&("end"===l?x=d+o[k]:"center"===l?(x=Math.round(r[H]/2-(r[H]-d)/2)+o[k])<0&&(x=o[k]):"justify"===l&&(x=o[k],u=Math.floor(d/(i.length-1)))),y[M]=o[D],t=0,n=i.length;t<n;t++)p=(m=(h=i[t]).layoutRect()).maxFlexSize||m[R],"center"===a?y[M]=Math.round(r[N]/2-m[P]/2):"stretch"===a?(y[P]=z(m[W]||0,r[N]-o[D]-o[O]),y[M]=o[D]):"end"===a&&(y[M]=r[N]-m[P]-o.top),0<m.flex&&(p+=m.flex*b),y[_]=p,y[C]=x,h.layoutRect(y),h.recalc&&h.recalc(),x+=p+u}else if(y.w=y.minW,y.h=y.minH,e.layoutRect(y),this.recalc(e),null===e._lastRect){var F=e.parent();F&&(F._lastRect=null,F.recalc())}}}),xi=qt.extend({Defaults:{containerClass:"flow-layout",controlClass:"flow-layout-item",endClass:"break"},recalc:function(e){e.items().filter(":visible").each(function(e){e.recalc&&e.recalc()})},isNative:function(){return!0}}),wi=function(e,t){return n=t,r=(i=e)===undefined?_.document:i.dom(),qn(r)?N.none():N.from(r.querySelector(n)).map(gn.fromDom);var n,i,r},_i=function(e,t){return function(){e.execCommand("mceToggleFormat",!1,t)}},Ri=function(e,t,n){var i=function(e){n(e,t)};e.formatter?e.formatter.formatChanged(t,i):e.on("init",function(){e.formatter.formatChanged(t,i)})},Ci=function(e,n){return function(t){Ri(e,n,function(e){t.control.active(e)})}},Ei=function(i){var t=["alignleft","aligncenter","alignright","alignjustify"],r="alignleft",e=[{text:"Left",icon:"alignleft",onclick:_i(i,"alignleft")},{text:"Center",icon:"aligncenter",onclick:_i(i,"aligncenter")},{text:"Right",icon:"alignright",onclick:_i(i,"alignright")},{text:"Justify",icon:"alignjustify",onclick:_i(i,"alignjustify")}];i.addMenuItem("align",{text:"Align",menu:e}),i.addButton("align",{type:"menubutton",icon:r,menu:e,onShowMenu:function(e){var n=e.control.menu;w.each(t,function(t,e){n.items().eq(e).each(function(e){return e.active(i.formatter.match(t))})})},onPostRender:function(e){var n=e.control;w.each(t,function(t,e){Ri(i,t,function(e){n.icon(r),e&&n.icon(t)})})}}),w.each({alignleft:["Align left","JustifyLeft"],aligncenter:["Align center","JustifyCenter"],alignright:["Align right","JustifyRight"],alignjustify:["Justify","JustifyFull"],alignnone:["No alignment","JustifyNone"]},function(e,t){i.addButton(t,{active:!1,tooltip:e[0],cmd:e[1],onPostRender:Ci(i,t)})})},ki=function(e){return e?e.split(",")[0]:""},Hi=function(l,u){return function(){var a=this;a.state.set("value",null),l.on("init nodeChange",function(e){var t,n,i,r,o=l.queryCommandValue("FontName"),s=(t=u,r=(n=o)?n.toLowerCase():"",w.each(t,function(e){e.value.toLowerCase()===r&&(i=e.value)}),w.each(t,function(e){i||ki(e.value).toLowerCase()!==ki(r).toLowerCase()||(i=e.value)}),i);a.value(s||null),!s&&o&&a.text(ki(o))})}},Si=function(n){n.addButton("fontselect",function(){var e,t=(e=function(e){for(var t=(e=e.replace(/;$/,"").split(";")).length;t--;)e[t]=e[t].split("=");return e}(n.settings.font_formats||"Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats"),w.map(e,function(e){return{text:{raw:e[0]},value:e[1],textStyle:-1===e[1].indexOf("dings")?"font-family:"+e[1]:""}}));return{type:"listbox",text:"Font Family",tooltip:"Font Family",values:t,fixedWidth:!0,onPostRender:Hi(n,t),onselect:function(e){e.control.settings.value&&n.execCommand("FontName",!1,e.control.settings.value)}}})},Ti=function(e){Si(e)},Mi=function(e,t){return/[0-9.]+px$/.test(e)?(n=72*parseInt(e,10)/96,i=t||0,r=Math.pow(10,i),Math.round(n*r)/r+"pt"):e;var n,i,r},Ni=function(e,t,n){var i;return w.each(e,function(e){e.value===n?i=n:e.value===t&&(i=t)}),i},Pi=function(n){n.addButton("fontsizeselect",function(){var e,s,a,t=(e=n.settings.fontsize_formats||"8pt 10pt 12pt 14pt 18pt 24pt 36pt",w.map(e.split(" "),function(e){var t=e,n=e,i=e.split("=");return 1<i.length&&(t=i[0],n=i[1]),{text:t,value:n}}));return{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:t,fixedWidth:!0,onPostRender:(s=n,a=t,function(){var o=this;s.on("init nodeChange",function(e){var t,n,i,r;if(t=s.queryCommandValue("FontSize"))for(i=3;!r&&0<=i;i--)n=Mi(t,i),r=Ni(a,n,t);o.value(r||null),r||o.text(n)})}),onclick:function(e){e.control.settings.value&&n.execCommand("FontSize",!1,e.control.settings.value)}}})},Wi=function(e){Pi(e)},Di=function(n,e){var i=e.length;return w.each(e,function(e){e.menu&&(e.hidden=0===Di(n,e.menu));var t=e.format;t&&(e.hidden=!n.formatter.canApply(t)),e.hidden&&i--}),i},Oi=function(n,e){var i=e.items().length;return e.items().each(function(e){e.menu&&e.visible(0<Oi(n,e.menu)),!e.menu&&e.settings.menu&&e.visible(0<Di(n,e.settings.menu));var t=e.settings.format;t&&e.visible(n.formatter.canApply(t)),e.visible()||i--}),i},Ai=function(e){var i,r,o,t,s,n,a,l,u=(r=0,o=[],t=[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}],s=function(e){var i=[];if(e)return w.each(e,function(e){var t={text:e.title,icon:e.icon};if(e.items)t.menu=s(e.items);else{var n=e.format||"custom"+r++;e.format||(e.name=n,o.push(e)),t.format=n,t.cmd=e.cmd}i.push(t)}),i},(i=e).on("init",function(){w.each(o,function(e){i.formatter.register(e.name,e)})}),{type:"menu",items:i.settings.style_formats_merge?i.settings.style_formats?s(t.concat(i.settings.style_formats)):s(t):s(i.settings.style_formats||t),onPostRender:function(e){i.fire("renderFormatsMenu",{control:e.control})},itemDefaults:{preview:!0,textStyle:function(){if(this.settings.format)return i.formatter.getCssText(this.settings.format)},onPostRender:function(){var n=this;n.parent().on("show",function(){var e,t;(e=n.settings.format)&&(n.disabled(!i.formatter.canApply(e)),n.active(i.formatter.match(e))),(t=n.settings.cmd)&&n.active(i.queryCommandState(t))})},onclick:function(){this.settings.format&&_i(i,this.settings.format)(),this.settings.cmd&&i.execCommand(this.settings.cmd)}}});n=u,e.addMenuItem("formats",{text:"Formats",menu:n}),l=u,(a=e).addButton("styleselect",{type:"menubutton",text:"Formats",menu:l,onShowMenu:function(){a.settings.style_formats_autohide&&Oi(a,this.menu)}})},Bi=function(n,e){return function(){var r,o,s,t=[];return w.each(e,function(e){t.push({text:e[0],value:e[1],textStyle:function(){return n.formatter.getCssText(e[1])}})}),{type:"listbox",text:e[0][0],values:t,fixedWidth:!0,onselect:function(e){if(e.control){var t=e.control.value();_i(n,t)()}},onPostRender:(r=n,o=t,function(){var t=this;r.on("nodeChange",function(e){var n=r.formatter,i=null;w.each(e.parents,function(t){if(w.each(o,function(e){if(s?n.matchNode(t,s,{value:e.value})&&(i=e.value):n.matchNode(t,e.value)&&(i=e.value),i)return!1}),i)return!1}),t.value(i)})})}}},Li=function(e){var t,n,i=function(e){for(var t=(e=e.replace(/;$/,"").split(";")).length;t--;)e[t]=e[t].split("=");return e}(e.settings.block_formats||"Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre");e.addMenuItem("blockformats",{text:"Blocks",menu:(t=e,n=i,w.map(n,function(e){return{text:e[0],onclick:_i(t,e[1]),textStyle:function(){return t.formatter.getCssText(e[1])}}}))}),e.addButton("formatselect",Bi(e,i))},zi=function(t,e){var n,i;if("string"==typeof e)i=e.split(" ");else if(w.isArray(e))return function(e){for(var t=[],n=0,i=e.length;n<i;++n){if(!V(e[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+e);X.apply(t,e[n])}return t}(w.map(e,function(e){return zi(t,e)}));return n=w.grep(i,function(e){return"|"===e||e in t.menuItems}),w.map(n,function(e){return"|"===e?{text:"-"}:t.menuItems[e]})},Ii=function(e){return e&&"-"===e.text},Fi=function(n){var i=Z(n,function(e,t){return!Ii(e)||!Ii(n[t-1])});return Z(i,function(e,t){return!Ii(e)||0<t&&t<i.length-1})},Ui=function(e){var t,n,i,r,o=e.settings.insert_button_items;return Fi(o?zi(e,o):(t=e,n="insert",i=[{text:"-"}],r=w.grep(t.menuItems,function(e){return e.context===n}),w.each(r,function(e){"before"===e.separator&&i.push({text:"|"}),e.prependToContext?i.unshift(e):i.push(e),"after"===e.separator&&i.push({text:"|"})}),i))},Vi=function(e){var t;(t=e).addButton("insert",{type:"menubutton",icon:"insert",menu:[],oncreatemenu:function(){this.menu.add(Ui(t)),this.menu.renderNew()}})},Yi=function(e){var n,i,r;n=e,w.each({bold:"Bold",italic:"Italic",underline:"Underline",strikethrough:"Strikethrough",subscript:"Subscript",superscript:"Superscript"},function(e,t){n.addButton(t,{active:!1,tooltip:e,onPostRender:Ci(n,t),onclick:_i(n,t)})}),i=e,w.each({outdent:["Decrease indent","Outdent"],indent:["Increase indent","Indent"],cut:["Cut","Cut"],copy:["Copy","Copy"],paste:["Paste","Paste"],help:["Help","mceHelp"],selectall:["Select all","SelectAll"],visualaid:["Visual aids","mceToggleVisualAid"],newdocument:["New document","mceNewDocument"],removeformat:["Clear formatting","RemoveFormat"],remove:["Remove","Delete"]},function(e,t){i.addButton(t,{tooltip:e[0],cmd:e[1]})}),r=e,w.each({blockquote:["Blockquote","mceBlockQuote"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"]},function(e,t){r.addButton(t,{active:!1,tooltip:e[0],cmd:e[1],onPostRender:Ci(r,t)})})},$i=function(e){var n;Yi(e),n=e,w.each({bold:["Bold","Bold","Meta+B"],italic:["Italic","Italic","Meta+I"],underline:["Underline","Underline","Meta+U"],strikethrough:["Strikethrough","Strikethrough"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"],removeformat:["Clear formatting","RemoveFormat"],newdocument:["New document","mceNewDocument"],cut:["Cut","Cut","Meta+X"],copy:["Copy","Copy","Meta+C"],paste:["Paste","Paste","Meta+V"],selectall:["Select all","SelectAll","Meta+A"]},function(e,t){n.addMenuItem(t,{text:e[0],icon:t,shortcut:e[2],cmd:e[1]})}),n.addMenuItem("codeformat",{text:"Code",icon:"code",onclick:_i(n,"code")})},qi=function(n,i){return function(){var e=this,t=function(){var e="redo"===i?"hasRedo":"hasUndo";return!!n.undoManager&&n.undoManager[e]()};e.disabled(!t()),n.on("Undo Redo AddUndo TypingUndo ClearUndos SwitchMode",function(){e.disabled(n.readonly||!t())})}},Xi=function(e){var t,n;(t=e).addMenuItem("undo",{text:"Undo",icon:"undo",shortcut:"Meta+Z",onPostRender:qi(t,"undo"),cmd:"undo"}),t.addMenuItem("redo",{text:"Redo",icon:"redo",shortcut:"Meta+Y",onPostRender:qi(t,"redo"),cmd:"redo"}),(n=e).addButton("undo",{tooltip:"Undo",onPostRender:qi(n,"undo"),cmd:"undo"}),n.addButton("redo",{tooltip:"Redo",onPostRender:qi(n,"redo"),cmd:"redo"})},ji=function(e){var t,n;(t=e).addMenuItem("visualaid",{text:"Visual aids",selectable:!0,onPostRender:(n=t,function(){var t=this;n.on("VisualAid",function(e){t.active(e.hasVisual)}),t.active(n.hasVisual)}),cmd:"mceToggleVisualAid"})},Ji={setup:function(e){var t;e.rtl&&(rt.rtl=!0),e.on("mousedown progressstate",function(){Ct.hideAll()}),(t=e).settings.ui_container&&(ce.container=wi(gn.fromDom(_.document.body),t.settings.ui_container).fold(k(null),function(e){return e.dom()})),Nt.tooltips=!ce.iOS,rt.translate=function(e){return l.translate(e)},Li(e),Ei(e),$i(e),Xi(e),Wi(e),Ti(e),Ai(e),ji(e),Vi(e)}},Gi=Xt.extend({recalc:function(e){var t,n,i,r,o,s,a,l,u,c,d,f,h,m,g,p,v,b,y,x,w,_,R,C,E,k,H,S,T=[],M=[];t=e.settings,r=e.items().filter(":visible"),o=e.layoutRect(),i=t.columns||Math.ceil(Math.sqrt(r.length)),n=Math.ceil(r.length/i),b=t.spacingH||t.spacing||0,y=t.spacingV||t.spacing||0,x=t.alignH||t.align,w=t.alignV||t.align,p=e.paddingBox,S="reverseRows"in t?t.reverseRows:e.isRtl(),x&&"string"==typeof x&&(x=[x]),w&&"string"==typeof w&&(w=[w]);for(d=0;d<i;d++)T.push(0);for(f=0;f<n;f++)M.push(0);for(f=0;f<n;f++)for(d=0;d<i&&(c=r[f*i+d]);d++)C=(u=c.layoutRect()).minW,E=u.minH,T[d]=C>T[d]?C:T[d],M[f]=E>M[f]?E:M[f];for(k=o.innerW-p.left-p.right,d=_=0;d<i;d++)_+=T[d]+(0<d?b:0),k-=(0<d?b:0)+T[d];for(H=o.innerH-p.top-p.bottom,f=R=0;f<n;f++)R+=M[f]+(0<f?y:0),H-=(0<f?y:0)+M[f];if(_+=p.left+p.right,R+=p.top+p.bottom,(l={}).minW=_+(o.w-o.innerW),l.minH=R+(o.h-o.innerH),l.contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH,l.minW=Math.min(l.minW,o.maxW),l.minH=Math.min(l.minH,o.maxH),l.minW=Math.max(l.minW,o.startMinWidth),l.minH=Math.max(l.minH,o.startMinHeight),!o.autoResize||l.minW===o.minW&&l.minH===o.minH){var N;o.autoResize&&((l=e.layoutRect(l)).contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH),N="start"===t.packV?0:0<H?Math.floor(H/n):0;var P=0,W=t.flexWidths;if(W)for(d=0;d<W.length;d++)P+=W[d];else P=i;var D=k/P;for(d=0;d<i;d++)T[d]+=W?W[d]*D:D;for(m=p.top,f=0;f<n;f++){for(h=p.left,a=M[f]+N,d=0;d<i&&(c=r[S?f*i+i-1-d:f*i+d]);d++)g=c.settings,u=c.layoutRect(),s=Math.max(T[d],u.startMinWidth),u.x=h,u.y=m,"center"===(v=g.alignH||(x?x[d]||x[0]:null))?u.x=h+s/2-u.w/2:"right"===v?u.x=h+s-u.w:"stretch"===v&&(u.w=s),"center"===(v=g.alignV||(w?w[d]||w[0]:null))?u.y=m+a/2-u.h/2:"bottom"===v?u.y=m+a-u.h:"stretch"===v&&(u.h=a),c.layoutRect(u),h+=s+b,c.recalc&&c.recalc();m+=a+y}}else if(l.w=l.minW,l.h=l.minH,e.layoutRect(l),this.recalc(e),null===e._lastRect){var O=e.parent();O&&(O._lastRect=null,O.recalc())}}}),Ki=Nt.extend({renderHtml:function(){var e=this;return e.classes.add("iframe"),e.canFocus=!1,'<iframe id="'+e._id+'" class="'+e.classes+'" tabindex="-1" src="'+(e.settings.url||"javascript:''")+'" frameborder="0"></iframe>'},src:function(e){this.getEl().src=e},html:function(e,t){var n=this,i=this.getEl().contentWindow.document.body;return i?(i.innerHTML=e,t&&t()):u.setTimeout(function(){n.html(e)}),this}}),Zi=Nt.extend({init:function(e){this._super(e),this.classes.add("widget").add("infobox"),this.canFocus=!1},severity:function(e){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(e)},help:function(e){this.state.set("help",e)},renderHtml:function(){var e=this,t=e.classPrefix;return'<div id="'+e._id+'" class="'+e.classes+'"><div id="'+e._id+'-body">'+e.encode(e.state.get("text"))+'<button role="button" tabindex="-1"><i class="'+t+"ico "+t+'i-help"></i></button></div></div>'},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.getEl("body").firstChild.data=t.encode(e.value),t.state.get("rendered")&&t.updateLayoutRect()}),t.state.on("change:help",function(e){t.classes.toggle("has-help",e.value),t.state.get("rendered")&&t.updateLayoutRect()}),t._super()}}),Qi=Nt.extend({init:function(e){var t=this;t._super(e),t.classes.add("widget").add("label"),t.canFocus=!1,e.multiline&&t.classes.add("autoscroll"),e.strong&&t.classes.add("strong")},initLayoutRect:function(){var e=this,t=e._super();return e.settings.multiline&&(we.getSize(e.getEl()).width>t.maxW&&(t.minW=t.maxW,e.classes.add("multiline")),e.getEl().style.width=t.minW+"px",t.startMinH=t.h=t.minH=Math.min(t.maxH,we.getSize(e.getEl()).height)),t},repaint:function(){return this.settings.multiline||(this.getEl().style.lineHeight=this.layoutRect().h+"px"),this._super()},severity:function(e){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(e)},renderHtml:function(){var e,t,n=this,i=n.settings.forId,r=n.settings.html?n.settings.html:n.encode(n.state.get("text"));return!i&&(t=n.settings.forName)&&(e=n.getRoot().find("#"+t)[0])&&(i=e._id),i?'<label id="'+n._id+'" class="'+n.classes+'"'+(i?' for="'+i+'"':"")+">"+r+"</label>":'<span id="'+n._id+'" class="'+n.classes+'">'+r+"</span>"},bindStates:function(){var t=this;return t.state.on("change:text",function(e){t.innerHtml(t.encode(e.value)),t.state.get("rendered")&&t.updateLayoutRect()}),t._super()}}),er=lt.extend({Defaults:{role:"toolbar",layout:"flow"},init:function(e){this._super(e),this.classes.add("toolbar")},postRender:function(){return this.items().each(function(e){e.classes.add("toolbar-item")}),this._super()}}),tr=er.extend({Defaults:{role:"menubar",containerCls:"menubar",ariaRoot:!0,defaults:{type:"menubutton"}}}),nr=jt.extend({init:function(e){var t=this;t._renderOpen=!0,t._super(e),e=t.settings,t.classes.add("menubtn"),e.fixedWidth&&t.classes.add("fixed-width"),t.aria("haspopup",!0),t.state.set("menu",e.menu||t.render())},showMenu:function(e){var t,n=this;if(n.menu&&n.menu.visible()&&!1!==e)return n.hideMenu();n.menu||(t=n.state.get("menu")||[],n.classes.add("opened"),t.length?t={type:"menu",animate:!0,items:t}:(t.type=t.type||"menu",t.animate=!0),t.renderTo?n.menu=t.parent(n).show().renderTo():n.menu=v.create(t).parent(n).renderTo(),n.fire("createmenu"),n.menu.reflow(),n.menu.on("cancel",function(e){e.control.parent()===n.menu&&(e.stopPropagation(),n.focus(),n.hideMenu())}),n.menu.on("select",function(){n.focus()}),n.menu.on("show hide",function(e){"hide"===e.type&&e.control.parent()===n&&n.classes.remove("opened-under"),e.control===n.menu&&(n.activeMenu("show"===e.type),n.classes.toggle("opened","show"===e.type)),n.aria("expanded","show"===e.type)}).fire("show")),n.menu.show(),n.menu.layoutRect({w:n.layoutRect().w}),n.menu.repaint(),n.menu.moveRel(n.getEl(),n.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"]);var i=n.menu.layoutRect(),r=n.$el.offset().top+n.layoutRect().h;r>i.y&&r<i.y+i.h&&n.classes.add("opened-under"),n.fire("showmenu")},hideMenu:function(){this.menu&&(this.menu.items().each(function(e){e.hideMenu&&e.hideMenu()}),this.menu.hide())},activeMenu:function(e){this.classes.toggle("active",e)},renderHtml:function(){var e,t=this,n=t._id,i=t.classPrefix,r=t.settings.icon,o=t.state.get("text"),s="";return(e=t.settings.image)?(r="none","string"!=typeof e&&(e=_.window.getSelection?e[0]:e[1]),e=" style=\"background-image: url('"+e+"')\""):e="",o&&(t.classes.add("btn-has-text"),s='<span class="'+i+'txt">'+t.encode(o)+"</span>"),r=t.settings.icon?i+"ico "+i+"i-"+r:"",t.aria("role",t.parent()instanceof tr?"menuitem":"button"),'<div id="'+n+'" class="'+t.classes+'" tabindex="-1" aria-labelledby="'+n+'"><button id="'+n+'-open" role="presentation" type="button" tabindex="-1">'+(r?'<i class="'+r+'"'+e+"></i>":"")+s+' <i class="'+i+'caret"></i></button></div>'},postRender:function(){var r=this;return r.on("click",function(e){e.control===r&&function(e,t){for(;e;){if(t===e)return!0;e=e.parentNode}return!1}(e.target,r.getEl())&&(r.focus(),r.showMenu(!e.aria),e.aria&&r.menu.items().filter(":visible")[0].focus())}),r.on("mouseenter",function(e){var t,n=e.control,i=r.parent();n&&i&&n instanceof nr&&n.parent()===i&&(i.items().filter("MenuButton").each(function(e){e.hideMenu&&e!==n&&(e.menu&&e.menu.visible()&&(t=!0),e.hideMenu())}),t&&(n.focus(),n.showMenu()))}),r._super()},bindStates:function(){var e=this;return e.state.on("change:menu",function(){e.menu&&e.menu.remove(),e.menu=null}),e._super()},remove:function(){this._super(),this.menu&&this.menu.remove()}}),ir=Ct.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(e){if(e.autohide=!0,e.constrainToViewport=!0,"function"==typeof e.items&&(e.itemsFactory=e.items,e.items=[]),e.itemDefaults)for(var t=e.items,n=t.length;n--;)t[n]=w.extend({},e.itemDefaults,t[n]);this._super(e),this.classes.add("menu"),e.animate&&11!==ce.ie&&this.classes.add("animate")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){this.hideAll(),this.fire("select")},load:function(){var t,n=this;function i(){n.throbber&&(n.throbber.hide(),n.throbber=null)}n.settings.itemsFactory&&(n.throbber||(n.throbber=new Ht(n.getEl("body"),!0),0===n.items().length?(n.throbber.show(),n.fire("loading")):n.throbber.show(100,function(){n.items().remove(),n.fire("loading")}),n.on("hide close",i)),n.requestTime=t=(new Date).getTime(),n.settings.itemsFactory(function(e){0!==e.length?n.requestTime===t&&(n.getEl().style.width="",n.getEl("body").style.width="",i(),n.items().remove(),n.getEl("body").innerHTML="",n.add(e),n.renderNew(),n.fire("loaded")):n.hide()}))},hideAll:function(){return this.find("menuitem").exec("hideMenu"),this._super()},preRender:function(){var n=this;return n.items().each(function(e){var t=e.settings;if(t.icon||t.image||t.selectable)return!(n._hasIcons=!0)}),n.settings.itemsFactory&&n.on("postrender",function(){n.settings.itemsFactory&&n.load()}),n.on("show hide",function(e){e.control===n&&("show"===e.type?u.setTimeout(function(){n.classes.add("in")},0):n.classes.remove("in"))}),n._super()}}),rr=nr.extend({init:function(i){var t,r,o,n,s=this;s._super(i),i=s.settings,s._values=t=i.values,t&&("undefined"!=typeof i.value&&function e(t){for(var n=0;n<t.length;n++){if(r=t[n].selected||i.value===t[n].value)return o=o||t[n].text,s.state.set("value",t[n].value),!0;if(t[n].menu&&e(t[n].menu))return!0}}(t),!r&&0<t.length&&(o=t[0].text,s.state.set("value",t[0].value)),s.state.set("menu",t)),s.state.set("text",i.text||o),s.classes.add("listbox"),s.on("select",function(e){var t=e.control;n&&(e.lastControl=n),i.multiple?t.active(!t.active()):s.value(e.control.value()),n=t})},value:function(n){return 0===arguments.length?this.state.get("value"):(void 0===n||(this.settings.values&&!function t(e){return J(e,function(e){return e.menu?t(e.menu):e.value===n})}(this.settings.values)?null===n&&this.state.set("value",null):this.state.set("value",n)),this)},bindStates:function(){var i=this;return i.on("show",function(e){var t,n;t=e.control,n=i.value(),t instanceof ir&&t.items().each(function(e){e.hasMenus()||e.active(e.value()===n)})}),i.state.on("change:value",function(t){var n=function e(t,n){var i;if(t)for(var r=0;r<t.length;r++){if(t[r].value===n)return t[r];if(t[r].menu&&(i=e(t[r].menu,n)))return i}}(i.state.get("menu"),t.value);n?i.text(n.text):i.text(i.settings.text)}),i._super()}}),or=Nt.extend({Defaults:{border:0,role:"menuitem"},init:function(e){var t,n=this;n._super(e),e=n.settings,n.classes.add("menu-item"),e.menu&&n.classes.add("menu-item-expand"),e.preview&&n.classes.add("menu-item-preview"),"-"!==(t=n.state.get("text"))&&"|"!==t||(n.classes.add("menu-item-sep"),n.aria("role","separator"),n.state.set("text","-")),e.selectable&&(n.aria("role","menuitemcheckbox"),n.classes.add("menu-item-checkbox"),e.icon="selected"),e.preview||e.selectable||n.classes.add("menu-item-normal"),n.on("mousedown",function(e){e.preventDefault()}),e.menu&&!e.ariaHideMenu&&n.aria("haspopup",!0)},hasMenus:function(){return!!this.settings.menu},showMenu:function(){var t,n=this,e=n.settings,i=n.parent();if(i.items().each(function(e){e!==n&&e.hideMenu()}),e.menu){(t=n.menu)?t.show():((t=e.menu).length?t={type:"menu",items:t}:t.type=t.type||"menu",i.settings.itemDefaults&&(t.itemDefaults=i.settings.itemDefaults),(t=n.menu=v.create(t).parent(n).renderTo()).reflow(),t.on("cancel",function(e){e.stopPropagation(),n.focus(),t.hide()}),t.on("show hide",function(e){e.control.items&&e.control.items().each(function(e){e.active(e.settings.selected)})}).fire("show"),t.on("hide",function(e){e.control===t&&n.classes.remove("selected")}),t.submenu=!0),t._parentMenu=i,t.classes.add("menu-sub");var r=t.testMoveRel(n.getEl(),n.isRtl()?["tl-tr","bl-br","tr-tl","br-bl"]:["tr-tl","br-bl","tl-tr","bl-br"]);t.moveRel(n.getEl(),r),r="menu-sub-"+(t.rel=r),t.classes.remove(t._lastRel).add(r),t._lastRel=r,n.classes.add("selected"),n.aria("expanded",!0)}},hideMenu:function(){var e=this;return e.menu&&(e.menu.items().each(function(e){e.hideMenu&&e.hideMenu()}),e.menu.hide(),e.aria("expanded",!1)),e},renderHtml:function(){var e,t=this,n=t._id,i=t.settings,r=t.classPrefix,o=t.state.get("text"),s=t.settings.icon,a="",l=i.shortcut,u=t.encode(i.url);function c(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}function d(e){var t=i.match||"";return t?e.replace(new RegExp(c(t),"gi"),function(e){return"!mce~match["+e+"]mce~match!"}):e}function f(e){return e.replace(new RegExp(c("!mce~match["),"g"),"<b>").replace(new RegExp(c("]mce~match!"),"g"),"</b>")}return s&&t.parent().classes.add("menu-has-icons"),i.image&&(a=" style=\"background-image: url('"+i.image+"')\""),l&&(l=function(e){var t,n,i={};for(i=ce.mac?{alt:"⌥",ctrl:"⌘",shift:"⇧",meta:"⌘"}:{meta:"Ctrl"},e=e.split("+"),t=0;t<e.length;t++)(n=i[e[t].toLowerCase()])&&(e[t]=n);return e.join("+")}(l)),s=r+"ico "+r+"i-"+(t.settings.icon||"none"),e="-"!==o?'<i class="'+s+'"'+a+"></i>\xa0":"",o=f(t.encode(d(o))),u=f(t.encode(d(u))),'<div id="'+n+'" class="'+t.classes+'" tabindex="-1">'+e+("-"!==o?'<span id="'+n+'-text" class="'+r+'text">'+o+"</span>":"")+(l?'<div id="'+n+'-shortcut" class="'+r+'menu-shortcut">'+l+"</div>":"")+(i.menu?'<div class="'+r+'caret"></div>':"")+(u?'<div class="'+r+'menu-item-link">'+u+"</div>":"")+"</div>"},postRender:function(){var t=this,n=t.settings,e=n.textStyle;if("function"==typeof e&&(e=e.call(this)),e){var i=t.getEl("text");i&&(i.setAttribute("style",e),t._textStyle=e)}return t.on("mouseenter click",function(e){e.control===t&&(n.menu||"click"!==e.type?(t.showMenu(),e.aria&&t.menu.focus(!0)):(t.fire("select"),u.requestAnimationFrame(function(){t.parent().hideAll()})))}),t._super(),t},hover:function(){return this.parent().items().each(function(e){e.classes.remove("selected")}),this.classes.toggle("selected",!0),this},active:function(e){return function(e,t){var n=e._textStyle;if(n){var i=e.getEl("text");i.setAttribute("style",n),t&&(i.style.color="",i.style.backgroundColor="")}}(this,e),void 0!==e&&this.aria("checked",e),this._super(e)},remove:function(){this._super(),this.menu&&this.menu.remove()}}),sr=Kt.extend({Defaults:{classes:"radio",role:"radio"}}),ar=Nt.extend({renderHtml:function(){var e=this,t=e.classPrefix;return e.classes.add("resizehandle"),"both"===e.settings.direction&&e.classes.add("resizehandle-both"),e.canFocus=!1,'<div id="'+e._id+'" class="'+e.classes+'"><i class="'+t+"ico "+t+'i-resize"></i></div>'},postRender:function(){var t=this;t._super(),t.resizeDragHelper=new ct(this._id,{start:function(){t.fire("ResizeStart")},drag:function(e){"both"!==t.settings.direction&&(e.deltaX=0),t.fire("Resize",e)},stop:function(){t.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}});function lr(e){var t="";if(e)for(var n=0;n<e.length;n++)t+='<option value="'+e[n]+'">'+e[n]+"</option>";return t}var ur=Nt.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(e){var n=this;n._super(e),n.settings.size&&(n.size=n.settings.size),n.settings.options&&(n._options=n.settings.options),n.on("keydown",function(e){var t;13===e.keyCode&&(e.preventDefault(),n.parents().reverse().each(function(e){if(e.toJSON)return t=e,!1}),n.fire("submit",{data:t.toJSON()}))})},options:function(e){return arguments.length?(this.state.set("options",e),this):this.state.get("options")},renderHtml:function(){var e,t=this,n="";return e=lr(t._options),t.size&&(n=' size = "'+t.size+'"'),'<select id="'+t._id+'" class="'+t.classes+'"'+n+">"+e+"</select>"},bindStates:function(){var t=this;return t.state.on("change:options",function(e){t.getEl().innerHTML=lr(e.value)}),t._super()}});function cr(e,t,n){return e<t&&(e=t),n<e&&(e=n),e}function dr(e,t,n){e.setAttribute("aria-"+t,n)}function fr(e,t){var n,i,r,o,s;"v"===e.settings.orientation?(r="top",i="height",n="h"):(r="left",i="width",n="w"),s=e.getEl("handle"),o=((e.layoutRect()[n]||100)-we.getSize(s)[i])*((t-e._minValue)/(e._maxValue-e._minValue))+"px",s.style[r]=o,s.style.height=e.layoutRect().h+"px",dr(s,"valuenow",t),dr(s,"valuetext",""+e.settings.previewFilter(t)),dr(s,"valuemin",e._minValue),dr(s,"valuemax",e._maxValue)}var hr=Nt.extend({init:function(e){var t=this;e.previewFilter||(e.previewFilter=function(e){return Math.round(100*e)/100}),t._super(e),t.classes.add("slider"),"v"===e.orientation&&t.classes.add("vertical"),t._minValue=$(e.minValue)?e.minValue:0,t._maxValue=$(e.maxValue)?e.maxValue:100,t._initValue=t.state.get("value")},renderHtml:function(){var e=this._id,t=this.classPrefix;return'<div id="'+e+'" class="'+this.classes+'"><div id="'+e+'-handle" class="'+t+'slider-handle" role="slider" tabindex="-1"></div></div>'},reset:function(){this.value(this._initValue).repaint()},postRender:function(){var e,t,n,i,r,o,s,a,l,u,c,d,f,h,m=this;e=m._minValue,t=m._maxValue,"v"===m.settings.orientation?(n="screenY",i="top",r="height",o="h"):(n="screenX",i="left",r="width",o="w"),m._super(),function(o,s){function t(e){var t,n,i,r;t=cr(t=(((t=m.value())+(r=n=o))/((i=s)-r)+.05*e)*(i-n)-n,o,s),m.value(t),m.fire("dragstart",{value:t}),m.fire("drag",{value:t}),m.fire("dragend",{value:t})}m.on("keydown",function(e){switch(e.keyCode){case 37:case 38:t(-1);break;case 39:case 40:t(1)}})}(e,t),s=e,a=t,l=m.getEl("handle"),m._dragHelper=new ct(m._id,{handle:m._id+"-handle",start:function(e){u=e[n],c=parseInt(m.getEl("handle").style[i],10),d=(m.layoutRect()[o]||100)-we.getSize(l)[r],m.fire("dragstart",{value:h})},drag:function(e){var t=e[n]-u;f=cr(c+t,0,d),l.style[i]=f+"px",h=s+f/d*(a-s),m.value(h),m.tooltip().text(""+m.settings.previewFilter(h)).show().moveRel(l,"bc tc"),m.fire("drag",{value:h})},stop:function(){m.tooltip().hide(),m.fire("dragend",{value:h})}})},repaint:function(){this._super(),fr(this,this.value())},bindStates:function(){var t=this;return t.state.on("change:value",function(e){fr(t,e.value)}),t._super()}}),mr=Nt.extend({renderHtml:function(){return this.classes.add("spacer"),this.canFocus=!1,'<div id="'+this._id+'" class="'+this.classes+'"></div>'}}),gr=nr.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var e,t,n=this.getEl(),i=this.layoutRect();return this._super(),e=n.firstChild,t=n.lastChild,ye(e).css({width:i.w-we.getSize(t).width,height:i.h-2}),ye(t).css({height:i.h-2}),this},activeMenu:function(e){ye(this.getEl().lastChild).toggleClass(this.classPrefix+"active",e)},renderHtml:function(){var e,t,n=this,i=n._id,r=n.classPrefix,o=n.state.get("icon"),s=n.state.get("text"),a=n.settings,l="";return(e=a.image)?(o="none","string"!=typeof e&&(e=_.window.getSelection?e[0]:e[1]),e=" style=\"background-image: url('"+e+"')\""):e="",o=a.icon?r+"ico "+r+"i-"+o:"",s&&(n.classes.add("btn-has-text"),l='<span class="'+r+'txt">'+n.encode(s)+"</span>"),t="boolean"==typeof a.active?' aria-pressed="'+a.active+'"':"",'<div id="'+i+'" class="'+n.classes+'" role="button"'+t+' tabindex="-1"><button type="button" hidefocus="1" tabindex="-1">'+(o?'<i class="'+o+'"'+e+"></i>":"")+l+'</button><button type="button" class="'+r+'open" hidefocus="1" tabindex="-1">'+(n._menuBtnText?(o?"\xa0":"")+n._menuBtnText:"")+' <i class="'+r+'caret"></i></button></div>'},postRender:function(){var n=this.settings.onclick;return this.on("click",function(e){var t=e.target;if(e.control===this)for(;t;){if(e.aria&&"down"!==e.aria.key||"BUTTON"===t.nodeName&&-1===t.className.indexOf("open"))return e.stopImmediatePropagation(),void(n&&n.call(this,e));t=t.parentNode}}),delete this.settings.onclick,this._super()}}),pr=xi.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}}),vr=pt.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(n){var e;this.activeTabId&&(e=this.getEl(this.activeTabId),ye(e).removeClass(this.classPrefix+"active"),e.setAttribute("aria-selected","false")),this.activeTabId="t"+n,(e=this.getEl("t"+n)).setAttribute("aria-selected","true"),ye(e).addClass(this.classPrefix+"active"),this.items()[n].show().fire("showtab"),this.reflow(),this.items().each(function(e,t){n!==t&&e.hide()})},renderHtml:function(){var i=this,e=i._layout,r="",o=i.classPrefix;return i.preRender(),e.preRender(i),i.items().each(function(e,t){var n=i._id+"-t"+t;e.aria("role","tabpanel"),e.aria("labelledby",n),r+='<div id="'+n+'" class="'+o+'tab" unselectable="on" role="tab" aria-controls="'+e._id+'" aria-selected="false" tabIndex="-1">'+i.encode(e.settings.title)+"</div>"}),'<div id="'+i._id+'" class="'+i.classes+'" hidefocus="1" tabindex="-1"><div id="'+i._id+'-head" class="'+o+'tabs" role="tablist">'+r+'</div><div id="'+i._id+'-body" class="'+i.bodyClasses+'">'+e.renderHtml(i)+"</div></div>"},postRender:function(){var i=this;i._super(),i.settings.activeTab=i.settings.activeTab||0,i.activateTab(i.settings.activeTab),this.on("click",function(e){var t=e.target.parentNode;if(t&&t.id===i._id+"-head")for(var n=t.childNodes.length;n--;)t.childNodes[n]===e.target&&i.activateTab(n)})},initLayoutRect:function(){var e,t,n,i=this;t=(t=we.getSize(i.getEl("head")).width)<0?0:t,n=0,i.items().each(function(e){t=Math.max(t,e.layoutRect().minW),n=Math.max(n,e.layoutRect().minH)}),i.items().each(function(e){e.settings.x=0,e.settings.y=0,e.settings.w=t,e.settings.h=n,e.layoutRect({x:0,y:0,w:t,h:n})});var r=we.getSize(i.getEl("head")).height;return i.settings.minWidth=t,i.settings.minHeight=n+r,(e=i._super()).deltaH+=r,e.innerH=e.h-e.deltaH,e}}),br=Nt.extend({init:function(e){var n=this;n._super(e),n.classes.add("textbox"),e.multiline?n.classes.add("multiline"):(n.on("keydown",function(e){var t;13===e.keyCode&&(e.preventDefault(),n.parents().reverse().each(function(e){if(e.toJSON)return t=e,!1}),n.fire("submit",{data:t.toJSON()}))}),n.on("keyup",function(e){n.state.set("value",e.target.value)}))},repaint:function(){var e,t,n,i,r,o=this,s=0;e=o.getEl().style,t=o._layoutRect,r=o._lastRepaintRect||{};var a=_.document;return!o.settings.multiline&&a.all&&(!a.documentMode||a.documentMode<=8)&&(e.lineHeight=t.h-s+"px"),i=(n=o.borderBox).left+n.right+8,s=n.top+n.bottom+(o.settings.multiline?8:0),t.x!==r.x&&(e.left=t.x+"px",r.x=t.x),t.y!==r.y&&(e.top=t.y+"px",r.y=t.y),t.w!==r.w&&(e.width=t.w-i+"px",r.w=t.w),t.h!==r.h&&(e.height=t.h-s+"px",r.h=t.h),o._lastRepaintRect=r,o.fire("repaint",{},!1),o},renderHtml:function(){var t,e,n=this,i=n.settings;return t={id:n._id,hidefocus:"1"},w.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(e){t[e]=i[e]}),n.disabled()&&(t.disabled="disabled"),i.subtype&&(t.type=i.subtype),(e=we.create(i.multiline?"textarea":"input",t)).value=n.state.get("value"),e.className=n.classes.toString(),e.outerHTML},value:function(e){return arguments.length?(this.state.set("value",e),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var t=this;t.getEl().value=t.state.get("value"),t._super(),t.$el.on("change",function(e){t.state.set("value",e.target.value),t.fire("change",e)})},bindStates:function(){var t=this;return t.state.on("change:value",function(e){t.getEl().value!==e.value&&(t.getEl().value=e.value)}),t.state.on("change:disabled",function(e){t.getEl().disabled=e.value}),t._super()},remove:function(){this.$el.off(),this._super()}}),yr=function(){return{Selector:Ie,Collection:Ve,ReflowQueue:Ke,Control:rt,Factory:v,KeyboardNavigation:st,Container:lt,DragHelper:ct,Scrollable:gt,Panel:pt,Movable:He,Resizable:vt,FloatPanel:Ct,Window:It,MessageBox:Yt,Tooltip:Mt,Widget:Nt,Progress:Pt,Notification:Dt,Layout:qt,AbsoluteLayout:Xt,Button:jt,ButtonGroup:Gt,Checkbox:Kt,ComboBox:Qt,ColorBox:en,PanelButton:tn,ColorButton:rn,ColorPicker:sn,Path:ln,ElementPath:un,FormItem:cn,Form:dn,FieldSet:fn,FilePicker:vi,FitLayout:bi,FlexLayout:yi,FlowLayout:xi,FormatControls:Ji,GridLayout:Gi,Iframe:Ki,InfoBox:Zi,Label:Qi,Toolbar:er,MenuBar:tr,MenuButton:nr,MenuItem:or,Throbber:Ht,Menu:ir,ListBox:rr,Radio:sr,ResizeHandle:ar,SelectBox:ur,Slider:hr,Spacer:mr,SplitButton:gr,StackLayout:pr,TabPanel:vr,TextBox:br,DropZone:an,BrowseButton:Jt}},xr=function(n){n.ui?w.each(yr(),function(e,t){n.ui[t]=e}):n.ui=yr()};w.each(yr(),function(e,t){v.add(t,e)}),xr(window.tinymce?window.tinymce:{}),r.add("modern",function(e){return Ji.setup(e),$t(e)})}(window); \ No newline at end of file diff --git a/lib/web/tiny_mce_4/tinymce.min.js b/lib/web/tiny_mce_4/tinymce.min.js index 369dd7c6c872b..a9128ab30f04a 100644 --- a/lib/web/tiny_mce_4/tinymce.min.js +++ b/lib/web/tiny_mce_4/tinymce.min.js @@ -1,2 +1,2 @@ -// 4.9.5 (2019-07-02) -!function(H){"use strict";var o=function(){},j=function(n,r){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return n(r.apply(null,e))}},q=function(e){return function(){return e}},$=function(e){return e};function d(r){for(var o=[],e=1;e<arguments.length;e++)o[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=o.concat(e);return r.apply(null,n)}}var e,t,n,r,i,a,u,s,c,l,f,m,g,p,h,v,b,y=function(n){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return!n.apply(null,e)}},C=q(!1),x=q(!0),w=C,N=x,E=function(){return S},S=(r={fold:function(e,t){return e()},is:w,isSome:w,isNone:N,getOr:n=function(e){return e},getOrThunk:t=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:function(){return null},getOrUndefined:function(){return undefined},or:n,orThunk:t,map:E,ap:E,each:function(){},bind:E,flatten:E,exists:w,forall:N,filter:E,equals:e=function(e){return e.isNone()},equals_:e,toArray:function(){return[]},toString:q("none()")},Object.freeze&&Object.freeze(r),r),k=function(n){var e=function(){return n},t=function(){return o},r=function(e){return e(n)},o={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:N,isNone:w,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return k(e(n))},ap:function(e){return e.fold(E,function(e){return k(e(n))})},each:function(e){e(n)},bind:r,flatten:e,exists:r,forall:r,filter:function(e){return e(n)?o:S},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(w,function(e){return t(n,e)})},toArray:function(){return[n]},toString:function(){return"some("+n+")"}};return o},A={some:k,none:E,from:function(e){return null===e||e===undefined?S:k(e)}},T=function(t){return function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===t}},R=T("string"),_=T("object"),D=T("array"),B=T("null"),O=T("boolean"),P=T("function"),I=T("number"),L=Array.prototype.slice,M=(i=Array.prototype.indexOf)===undefined?function(e,t){return J(e,t)}:function(e,t){return i.call(e,t)},F=function(e,t){return-1<M(e,t)},z=function(e,t){return G(e,t).isSome()},W=function(e,t){for(var n=e.length,r=new Array(n),o=0;o<n;o++){var i=e[o];r[o]=t(i,o,e)}return r},U=function(e,t){for(var n=0,r=e.length;n<r;n++)t(e[n],n,e)},K=function(e,t){for(var n=[],r=[],o=0,i=e.length;o<i;o++){var a=e[o];(t(a,o,e)?n:r).push(a)}return{pass:n,fail:r}},V=function(e,t){for(var n=[],r=0,o=e.length;r<o;r++){var i=e[r];t(i,r,e)&&n.push(i)}return n},X=function(e,t,n){return U(e,function(e){n=t(n,e)}),n},Y=function(e,t){for(var n=0,r=e.length;n<r;n++){var o=e[n];if(t(o,n,e))return A.some(o)}return A.none()},G=function(e,t){for(var n=0,r=e.length;n<r;n++)if(t(e[n],n,e))return A.some(n);return A.none()},J=function(e,t){for(var n=0,r=e.length;n<r;++n)if(e[n]===t)return n;return-1},Q=Array.prototype.push,Z=function(e,t){return function(e){for(var t=[],n=0,r=e.length;n<r;++n){if(!Array.prototype.isPrototypeOf(e[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+e);Q.apply(t,e[n])}return t}(W(e,t))},ee=function(e,t){for(var n=0,r=e.length;n<r;++n)if(!0!==t(e[n],n,e))return!1;return!0},te=function(e,t){return V(e,function(e){return!F(t,e)})},ne=function(e){return 0===e.length?A.none():A.some(e[0])},re=function(e){return 0===e.length?A.none():A.some(e[e.length-1])},oe=P(Array.from)?Array.from:function(e){return L.call(e)},ie="undefined"!=typeof H.window?H.window:Function("return this;")(),ae=function(e,t){return function(e,t){for(var n=t!==undefined&&null!==t?t:ie,r=0;r<e.length&&n!==undefined&&null!==n;++r)n=n[e[r]];return n}(e.split("."),t)},ue={getOrDie:function(e,t){var n=ae(e,t);if(n===undefined||null===n)throw e+" not available on this browser";return n}},se=function(){return ue.getOrDie("URL")},ce={createObjectURL:function(e){return se().createObjectURL(e)},revokeObjectURL:function(e){se().revokeObjectURL(e)}},le=H.navigator,fe=le.userAgent,de=function(e){return"matchMedia"in H.window&&H.matchMedia(e).matches};g=/Android/.test(fe),u=(u=!(a=/WebKit/.test(fe))&&/MSIE/gi.test(fe)&&/Explorer/gi.test(le.appName))&&/MSIE (\w+)\./.exec(fe)[1],s=-1!==fe.indexOf("Trident/")&&(-1!==fe.indexOf("rv:")||-1!==le.appName.indexOf("Netscape"))&&11,c=-1!==fe.indexOf("Edge/")&&!u&&!s&&12,u=u||s||c,l=!a&&!s&&/Gecko/.test(fe),f=-1!==fe.indexOf("Mac"),m=/(iPad|iPhone)/.test(fe),p="FormData"in H.window&&"FileReader"in H.window&&"URL"in H.window&&!!ce.createObjectURL,h=de("only screen and (max-device-width: 480px)")&&(g||m),v=de("only screen and (min-width: 800px)")&&(g||m),b=-1!==fe.indexOf("Windows Phone"),c&&(a=!1);var me,ge={opera:!1,webkit:a,ie:u,gecko:l,mac:f,iOS:m,android:g,contentEditable:!m||p||534<=parseInt(fe.match(/AppleWebKit\/(\d*)/)[1],10),transparentSrc:"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",caretAfter:8!==u,range:H.window.getSelection&&"Range"in H.window,documentMode:u&&!c?H.document.documentMode||7:10,fileApi:p,ceFalse:!1===u||8<u,cacheSuffix:null,container:null,overrideViewPort:null,experimentalShadowDom:!1,canHaveCSP:!1===u||11<u,desktop:!h&&!v,windowsPhone:b},pe=window.Promise?window.Promise:function(){function r(e,t){return function(){e.apply(t,arguments)}}var e=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)},i=function(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],l(e,r(o,this),r(u,this))},t=i.immediateFn||"function"==typeof setImmediate&&setImmediate||function(e){setTimeout(e,1)};function a(r){var o=this;null!==this._state?t(function(){var e=o._state?r.onFulfilled:r.onRejected;if(null!==e){var t;try{t=e(o._value)}catch(n){return void r.reject(n)}r.resolve(t)}else(o._state?r.resolve:r.reject)(o._value)}):this._deferreds.push(r)}function o(e){try{if(e===this)throw new TypeError("A promise cannot be resolved with itself.");if(e&&("object"==typeof e||"function"==typeof e)){var t=e.then;if("function"==typeof t)return void l(r(t,e),r(o,this),r(u,this))}this._state=!0,this._value=e,s.call(this)}catch(n){u.call(this,n)}}function u(e){this._state=!1,this._value=e,s.call(this)}function s(){for(var e=0,t=this._deferreds.length;e<t;e++)a.call(this,this._deferreds[e]);this._deferreds=null}function c(e,t,n,r){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof t?t:null,this.resolve=n,this.reject=r}function l(e,t,n){var r=!1;try{e(function(e){r||(r=!0,t(e))},function(e){r||(r=!0,n(e))})}catch(o){if(r)return;r=!0,n(o)}}return i.prototype["catch"]=function(e){return this.then(null,e)},i.prototype.then=function(n,r){var o=this;return new i(function(e,t){a.call(o,new c(n,r,e,t))})},i.all=function(){var s=Array.prototype.slice.call(1===arguments.length&&e(arguments[0])?arguments[0]:arguments);return new i(function(o,i){if(0===s.length)return o([]);var a=s.length;function u(t,e){try{if(e&&("object"==typeof e||"function"==typeof e)){var n=e.then;if("function"==typeof n)return void n.call(e,function(e){u(t,e)},i)}s[t]=e,0==--a&&o(s)}catch(r){i(r)}}for(var e=0;e<s.length;e++)u(e,s[e])})},i.resolve=function(t){return t&&"object"==typeof t&&t.constructor===i?t:new i(function(e){e(t)})},i.reject=function(n){return new i(function(e,t){t(n)})},i.race=function(o){return new i(function(e,t){for(var n=0,r=o.length;n<r;n++)o[n].then(e,t)})},i}(),he=function(e,t){return"number"!=typeof t&&(t=0),setTimeout(e,t)},ve=function(e,t){return"number"!=typeof t&&(t=1),setInterval(e,t)},be=function(t,n){var r,e;return(e=function(){var e=arguments;clearTimeout(r),r=he(function(){t.apply(this,e)},n)}).stop=function(){clearTimeout(r)},e},ye={requestAnimationFrame:function(e,t){me?me.then(e):me=new pe(function(e){t||(t=H.document.body),function(e,t){var n,r=H.window.requestAnimationFrame,o=["ms","moz","webkit"];for(n=0;n<o.length&&!r;n++)r=H.window[o[n]+"RequestAnimationFrame"];r||(r=function(e){H.window.setTimeout(e,0)}),r(e,t)}(e,t)}).then(e)},setTimeout:he,setInterval:ve,setEditorTimeout:function(e,t,n){return he(function(){e.removed||t()},n)},setEditorInterval:function(e,t,n){var r;return r=ve(function(){e.removed?clearInterval(r):t()},n)},debounce:be,throttle:be,clearInterval:function(e){return clearInterval(e)},clearTimeout:function(e){return clearTimeout(e)}},Ce=/^(?:mouse|contextmenu)|click/,xe={keyLocation:1,layerX:1,layerY:1,returnValue:1,webkitMovementX:1,webkitMovementY:1,keyIdentifier:1},we=function(){return!1},Ne=function(){return!0},Ee=function(e,t,n,r){e.addEventListener?e.addEventListener(t,n,r||!1):e.attachEvent&&e.attachEvent("on"+t,n)},Se=function(e,t,n,r){e.removeEventListener?e.removeEventListener(t,n,r||!1):e.detachEvent&&e.detachEvent("on"+t,n)},ke=function(e,t){var n,r,o=t||{};for(n in e)xe[n]||(o[n]=e[n]);if(o.target||(o.target=o.srcElement||H.document),ge.experimentalShadowDom&&(o.target=function(e,t){if(e.composedPath){var n=e.composedPath();if(n&&0<n.length)return n[0]}return t}(e,o.target)),e&&Ce.test(e.type)&&e.pageX===undefined&&e.clientX!==undefined){var i=o.target.ownerDocument||H.document,a=i.documentElement,u=i.body;o.pageX=e.clientX+(a&&a.scrollLeft||u&&u.scrollLeft||0)-(a&&a.clientLeft||u&&u.clientLeft||0),o.pageY=e.clientY+(a&&a.scrollTop||u&&u.scrollTop||0)-(a&&a.clientTop||u&&u.clientTop||0)}return o.preventDefault=function(){o.isDefaultPrevented=Ne,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},o.stopPropagation=function(){o.isPropagationStopped=Ne,e&&(e.stopPropagation?e.stopPropagation():e.cancelBubble=!0)},!(o.stopImmediatePropagation=function(){o.isImmediatePropagationStopped=Ne,o.stopPropagation()})==((r=o).isDefaultPrevented===Ne||r.isDefaultPrevented===we)&&(o.isDefaultPrevented=we,o.isPropagationStopped=we,o.isImmediatePropagationStopped=we),"undefined"==typeof o.metaKey&&(o.metaKey=!1),o},Te=function(e,t,n){var r=e.document,o={type:"ready"};if(n.domLoaded)t(o);else{var i=function(){return"complete"===r.readyState||"interactive"===r.readyState&&r.body},a=function(){n.domLoaded||(n.domLoaded=!0,t(o))},u=function(){i()&&(Se(r,"readystatechange",u),a())},s=function(){try{r.documentElement.doScroll("left")}catch(e){return void ye.setTimeout(s)}a()};!r.addEventListener||ge.ie&&ge.ie<11?(Ee(r,"readystatechange",u),r.documentElement.doScroll&&e.self===e.top&&s()):i()?a():Ee(e,"DOMContentLoaded",a),Ee(e,"load",a)}},Ae=function(){var m,g,p,h,v,b=this,y={};g="mce-data-"+(+new Date).toString(32),h="onmouseenter"in H.document.documentElement,p="onfocusin"in H.document.documentElement,v={mouseenter:"mouseover",mouseleave:"mouseout"},m=1,b.domLoaded=!1,b.events=y;var C=function(e,t){var n,r,o,i,a=y[t];if(n=a&&a[e.type])for(r=0,o=n.length;r<o;r++)if((i=n[r])&&!1===i.func.call(i.scope,e)&&e.preventDefault(),e.isImmediatePropagationStopped())return};b.bind=function(e,t,n,r){var o,i,a,u,s,c,l,f=H.window,d=function(e){C(ke(e||f.event),o)};if(e&&3!==e.nodeType&&8!==e.nodeType){for(e[g]?o=e[g]:(o=m++,e[g]=o,y[o]={}),r=r||e,a=(t=t.split(" ")).length;a--;)c=d,s=l=!1,"DOMContentLoaded"===(u=t[a])&&(u="ready"),b.domLoaded&&"ready"===u&&"complete"===e.readyState?n.call(r,ke({type:u})):(h||(s=v[u])&&(c=function(e){var t,n;if(t=e.currentTarget,(n=e.relatedTarget)&&t.contains)n=t.contains(n);else for(;n&&n!==t;)n=n.parentNode;n||((e=ke(e||f.event)).type="mouseout"===e.type?"mouseleave":"mouseenter",e.target=t,C(e,o))}),p||"focusin"!==u&&"focusout"!==u||(l=!0,s="focusin"===u?"focus":"blur",c=function(e){(e=ke(e||f.event)).type="focus"===e.type?"focusin":"focusout",C(e,o)}),(i=y[o][u])?"ready"===u&&b.domLoaded?n({type:u}):i.push({func:n,scope:r}):(y[o][u]=i=[{func:n,scope:r}],i.fakeName=s,i.capture=l,i.nativeHandler=c,"ready"===u?Te(e,c,b):Ee(e,s||u,c,l)));return e=i=0,n}},b.unbind=function(e,t,n){var r,o,i,a,u,s;if(!e||3===e.nodeType||8===e.nodeType)return b;if(r=e[g]){if(s=y[r],t){for(i=(t=t.split(" ")).length;i--;)if(o=s[u=t[i]]){if(n)for(a=o.length;a--;)if(o[a].func===n){var c=o.nativeHandler,l=o.fakeName,f=o.capture;(o=o.slice(0,a).concat(o.slice(a+1))).nativeHandler=c,o.fakeName=l,o.capture=f,s[u]=o}n&&0!==o.length||(delete s[u],Se(e,o.fakeName||u,o.nativeHandler,o.capture))}}else{for(u in s)o=s[u],Se(e,o.fakeName||u,o.nativeHandler,o.capture);s={}}for(u in s)return b;delete y[r];try{delete e[g]}catch(d){e[g]=null}}return b},b.fire=function(e,t,n){var r;if(!e||3===e.nodeType||8===e.nodeType)return b;for((n=ke(null,n)).type=t,n.target=e;(r=e[g])&&C(n,r),(e=e.parentNode||e.ownerDocument||e.defaultView||e.parentWindow)&&!n.isPropagationStopped(););return b},b.clean=function(e){var t,n,r=b.unbind;if(!e||3===e.nodeType||8===e.nodeType)return b;if(e[g]&&r(e),e.getElementsByTagName||(e=e.document),e&&e.getElementsByTagName)for(r(e),t=(n=e.getElementsByTagName("*")).length;t--;)(e=n[t])[g]&&r(e);return b},b.destroy=function(){y={}},b.cancel=function(e){return e&&(e.preventDefault(),e.stopImmediatePropagation()),!1}};Ae.Event=new Ae,Ae.Event.bind(H.window,"ready",function(){});var Re,_e,De,Be,Oe,Pe,Ie,Le,Me,Fe,ze,Ue,Ve,He,je,qe,$e,We,Ke="sizzle"+-new Date,Xe=H.window.document,Ye=0,Ge=0,Je=Rt(),Qe=Rt(),Ze=Rt(),et=function(e,t){return e===t&&(ze=!0),0},tt=typeof undefined,nt={}.hasOwnProperty,rt=[],ot=rt.pop,it=rt.push,at=rt.push,ut=rt.slice,st=rt.indexOf||function(e){for(var t=0,n=this.length;t<n;t++)if(this[t]===e)return t;return-1},ct="[\\x20\\t\\r\\n\\f]",lt="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",ft="\\["+ct+"*("+lt+")(?:"+ct+"*([*^$|!~]?=)"+ct+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+lt+"))|)"+ct+"*\\]",dt=":("+lt+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+ft+")*)|.*)\\)|)",mt=new RegExp("^"+ct+"+|((?:^|[^\\\\])(?:\\\\.)*)"+ct+"+$","g"),gt=new RegExp("^"+ct+"*,"+ct+"*"),pt=new RegExp("^"+ct+"*([>+~]|"+ct+")"+ct+"*"),ht=new RegExp("="+ct+"*([^\\]'\"]*?)"+ct+"*\\]","g"),vt=new RegExp(dt),bt=new RegExp("^"+lt+"$"),yt={ID:new RegExp("^#("+lt+")"),CLASS:new RegExp("^\\.("+lt+")"),TAG:new RegExp("^("+lt+"|[*])"),ATTR:new RegExp("^"+ft),PSEUDO:new RegExp("^"+dt),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ct+"*(even|odd|(([+-]|)(\\d*)n|)"+ct+"*(?:([+-]|)"+ct+"*(\\d+)|))"+ct+"*\\)|)","i"),bool:new RegExp("^(?:checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$","i"),needsContext:new RegExp("^"+ct+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ct+"*((?:-\\d)?\\d*)"+ct+"*\\)|)(?=[^-]|$)","i")},Ct=/^(?:input|select|textarea|button)$/i,xt=/^h\d$/i,wt=/^[^{]+\{\s*\[native \w/,Nt=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,Et=/[+~]/,St=/'|\\/g,kt=new RegExp("\\\\([\\da-f]{1,6}"+ct+"?|("+ct+")|.)","ig"),Tt=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{at.apply(rt=ut.call(Xe.childNodes),Xe.childNodes),rt[Xe.childNodes.length].nodeType}catch(ZN){at={apply:rt.length?function(e,t){it.apply(e,ut.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}var At=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m;if((t?t.ownerDocument||t:Xe)!==Ve&&Ue(t),n=n||[],!e||"string"!=typeof e)return n;if(1!==(u=(t=t||Ve).nodeType)&&9!==u)return[];if(je&&!r){if(o=Nt.exec(e))if(a=o[1]){if(9===u){if(!(i=t.getElementById(a))||!i.parentNode)return n;if(i.id===a)return n.push(i),n}else if(t.ownerDocument&&(i=t.ownerDocument.getElementById(a))&&We(t,i)&&i.id===a)return n.push(i),n}else{if(o[2])return at.apply(n,t.getElementsByTagName(e)),n;if((a=o[3])&&_e.getElementsByClassName)return at.apply(n,t.getElementsByClassName(a)),n}if(_e.qsa&&(!qe||!qe.test(e))){if(f=l=Ke,d=t,m=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){for(c=Pe(e),(l=t.getAttribute("id"))?f=l.replace(St,"\\$&"):t.setAttribute("id",f),f="[id='"+f+"'] ",s=c.length;s--;)c[s]=f+Mt(c[s]);d=Et.test(e)&&It(t.parentNode)||t,m=c.join(",")}if(m)try{return at.apply(n,d.querySelectorAll(m)),n}catch(g){}finally{l||t.removeAttribute("id")}}}return Le(e.replace(mt,"$1"),t,n,r)};function Rt(){var r=[];return function e(t,n){return r.push(t+" ")>De.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function _t(e){return e[Ke]=!0,e}function Dt(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||1<<31)-(~e.sourceIndex||1<<31);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function Bt(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function Ot(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function Pt(a){return _t(function(i){return i=+i,_t(function(e,t){for(var n,r=a([],e.length,i),o=r.length;o--;)e[n=r[o]]&&(e[n]=!(t[n]=e[n]))})})}function It(e){return e&&typeof e.getElementsByTagName!==tt&&e}for(Re in _e=At.support={},Oe=At.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},Ue=At.setDocument=function(e){var t,s=e?e.ownerDocument||e:Xe,n=s.defaultView;return s!==Ve&&9===s.nodeType&&s.documentElement?(He=(Ve=s).documentElement,je=!Oe(s),n&&n!==function(e){try{return e.top}catch(t){}return null}(n)&&(n.addEventListener?n.addEventListener("unload",function(){Ue()},!1):n.attachEvent&&n.attachEvent("onunload",function(){Ue()})),_e.attributes=!0,_e.getElementsByTagName=!0,_e.getElementsByClassName=wt.test(s.getElementsByClassName),_e.getById=!0,De.find.ID=function(e,t){if(typeof t.getElementById!==tt&&je){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},De.filter.ID=function(e){var t=e.replace(kt,Tt);return function(e){return e.getAttribute("id")===t}},De.find.TAG=_e.getElementsByTagName?function(e,t){if(typeof t.getElementsByTagName!==tt)return t.getElementsByTagName(e)}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if("*"===e){for(;n=i[o++];)1===n.nodeType&&r.push(n);return r}return i},De.find.CLASS=_e.getElementsByClassName&&function(e,t){if(je)return t.getElementsByClassName(e)},$e=[],qe=[],_e.disconnectedMatch=!0,qe=qe.length&&new RegExp(qe.join("|")),$e=$e.length&&new RegExp($e.join("|")),t=wt.test(He.compareDocumentPosition),We=t||wt.test(He.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},et=t?function(e,t){if(e===t)return ze=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!_e.sortDetached&&t.compareDocumentPosition(e)===n?e===s||e.ownerDocument===Xe&&We(Xe,e)?-1:t===s||t.ownerDocument===Xe&&We(Xe,t)?1:Fe?st.call(Fe,e)-st.call(Fe,t):0:4&n?-1:1)}:function(e,t){if(e===t)return ze=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],u=[t];if(!o||!i)return e===s?-1:t===s?1:o?-1:i?1:Fe?st.call(Fe,e)-st.call(Fe,t):0;if(o===i)return Dt(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;a[r]===u[r];)r++;return r?Dt(a[r],u[r]):a[r]===Xe?-1:u[r]===Xe?1:0},s):Ve},At.matches=function(e,t){return At(e,null,null,t)},At.matchesSelector=function(e,t){if((e.ownerDocument||e)!==Ve&&Ue(e),t=t.replace(ht,"='$1']"),_e.matchesSelector&&je&&(!$e||!$e.test(t))&&(!qe||!qe.test(t)))try{var n=(void 0).call(e,t);if(n||_e.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(ZN){}return 0<At(t,Ve,null,[e]).length},At.contains=function(e,t){return(e.ownerDocument||e)!==Ve&&Ue(e),We(e,t)},At.attr=function(e,t){(e.ownerDocument||e)!==Ve&&Ue(e);var n=De.attrHandle[t.toLowerCase()],r=n&&nt.call(De.attrHandle,t.toLowerCase())?n(e,t,!je):undefined;return r!==undefined?r:_e.attributes||!je?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},At.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},At.uniqueSort=function(e){var t,n=[],r=0,o=0;if(ze=!_e.detectDuplicates,Fe=!_e.sortStable&&e.slice(0),e.sort(et),ze){for(;t=e[o++];)t===e[o]&&(r=n.push(o));for(;r--;)e.splice(n[r],1)}return Fe=null,e},Be=At.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=Be(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r++];)n+=Be(t);return n},(De=At.selectors={cacheLength:50,createPseudo:_t,match:yt,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(kt,Tt),e[3]=(e[3]||e[4]||e[5]||"").replace(kt,Tt),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||At.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&At.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return yt.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&vt.test(n)&&(t=Pe(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(kt,Tt).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=Je[e+" "];return t||(t=new RegExp("(^|"+ct+")"+e+"("+ct+"|$)"))&&Je(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==tt&&e.getAttribute("class")||"")})},ATTR:function(n,r,o){return function(e){var t=At.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===o:"!="===r?t!==o:"^="===r?o&&0===t.indexOf(o):"*="===r?o&&-1<t.indexOf(o):"$="===r?o&&t.slice(-o.length)===o:"~="===r?-1<(" "+t+" ").indexOf(o):"|="===r&&(t===o||t.slice(0,o.length+1)===o+"-"))}},CHILD:function(m,e,t,g,p){var h="nth"!==m.slice(0,3),v="last"!==m.slice(-4),b="of-type"===e;return 1===g&&0===p?function(e){return!!e.parentNode}:function(e,t,n){var r,o,i,a,u,s,c=h!==v?"nextSibling":"previousSibling",l=e.parentNode,f=b&&e.nodeName.toLowerCase(),d=!n&&!b;if(l){if(h){for(;c;){for(i=e;i=i[c];)if(b?i.nodeName.toLowerCase()===f:1===i.nodeType)return!1;s=c="only"===m&&!s&&"nextSibling"}return!0}if(s=[v?l.firstChild:l.lastChild],v&&d){for(u=(r=(o=l[Ke]||(l[Ke]={}))[m]||[])[0]===Ye&&r[1],a=r[0]===Ye&&r[2],i=u&&l.childNodes[u];i=++u&&i&&i[c]||(a=u=0)||s.pop();)if(1===i.nodeType&&++a&&i===e){o[m]=[Ye,u,a];break}}else if(d&&(r=(e[Ke]||(e[Ke]={}))[m])&&r[0]===Ye)a=r[1];else for(;(i=++u&&i&&i[c]||(a=u=0)||s.pop())&&((b?i.nodeName.toLowerCase()!==f:1!==i.nodeType)||!++a||(d&&((i[Ke]||(i[Ke]={}))[m]=[Ye,a]),i!==e)););return(a-=p)===g||a%g==0&&0<=a/g}}},PSEUDO:function(e,i){var t,a=De.pseudos[e]||De.setFilters[e.toLowerCase()]||At.error("unsupported pseudo: "+e);return a[Ke]?a(i):1<a.length?(t=[e,e,"",i],De.setFilters.hasOwnProperty(e.toLowerCase())?_t(function(e,t){for(var n,r=a(e,i),o=r.length;o--;)e[n=st.call(e,r[o])]=!(t[n]=r[o])}):function(e){return a(e,0,t)}):a}},pseudos:{not:_t(function(e){var r=[],o=[],u=Ie(e.replace(mt,"$1"));return u[Ke]?_t(function(e,t,n,r){for(var o,i=u(e,null,r,[]),a=e.length;a--;)(o=i[a])&&(e[a]=!(t[a]=o))}):function(e,t,n){return r[0]=e,u(r,null,n,o),!o.pop()}}),has:_t(function(t){return function(e){return 0<At(t,e).length}}),contains:_t(function(t){return t=t.replace(kt,Tt),function(e){return-1<(e.textContent||e.innerText||Be(e)).indexOf(t)}}),lang:_t(function(n){return bt.test(n||"")||At.error("unsupported lang: "+n),n=n.replace(kt,Tt).toLowerCase(),function(e){var t;do{if(t=je?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=H.window.location&&H.window.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===He},focus:function(e){return e===Ve.activeElement&&(!Ve.hasFocus||Ve.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return!1===e.disabled},disabled:function(e){return!0===e.disabled},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!De.pseudos.empty(e)},header:function(e){return xt.test(e.nodeName)},input:function(e){return Ct.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:Pt(function(){return[0]}),last:Pt(function(e,t){return[t-1]}),eq:Pt(function(e,t,n){return[n<0?n+t:n]}),even:Pt(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:Pt(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:Pt(function(e,t,n){for(var r=n<0?n+t:n;0<=--r;)e.push(r);return e}),gt:Pt(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=De.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})De.pseudos[Re]=Bt(Re);for(Re in{submit:!0,reset:!0})De.pseudos[Re]=Ot(Re);function Lt(){}function Mt(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function Ft(a,e,t){var u=e.dir,s=t&&"parentNode"===u,c=Ge++;return e.first?function(e,t,n){for(;e=e[u];)if(1===e.nodeType||s)return a(e,t,n)}:function(e,t,n){var r,o,i=[Ye,c];if(n){for(;e=e[u];)if((1===e.nodeType||s)&&a(e,t,n))return!0}else for(;e=e[u];)if(1===e.nodeType||s){if((r=(o=e[Ke]||(e[Ke]={}))[u])&&r[0]===Ye&&r[1]===c)return i[2]=r[2];if((o[u]=i)[2]=a(e,t,n))return!0}}}function zt(o){return 1<o.length?function(e,t,n){for(var r=o.length;r--;)if(!o[r](e,t,n))return!1;return!0}:o[0]}function Ut(e,t,n,r,o){for(var i,a=[],u=0,s=e.length,c=null!=t;u<s;u++)(i=e[u])&&(n&&!n(i,r,o)||(a.push(i),c&&t.push(u)));return a}function Vt(m,g,p,h,v,e){return h&&!h[Ke]&&(h=Vt(h)),v&&!v[Ke]&&(v=Vt(v,e)),_t(function(e,t,n,r){var o,i,a,u=[],s=[],c=t.length,l=e||function(e,t,n){for(var r=0,o=t.length;r<o;r++)At(e,t[r],n);return n}(g||"*",n.nodeType?[n]:n,[]),f=!m||!e&&g?l:Ut(l,u,m,n,r),d=p?v||(e?m:c||h)?[]:t:f;if(p&&p(f,d,n,r),h)for(o=Ut(d,s),h(o,[],n,r),i=o.length;i--;)(a=o[i])&&(d[s[i]]=!(f[s[i]]=a));if(e){if(v||m){if(v){for(o=[],i=d.length;i--;)(a=d[i])&&o.push(f[i]=a);v(null,d=[],o,r)}for(i=d.length;i--;)(a=d[i])&&-1<(o=v?st.call(e,a):u[i])&&(e[o]=!(t[o]=a))}}else d=Ut(d===t?d.splice(c,d.length):d),v?v(null,t,d,r):at.apply(t,d)})}function Ht(e){for(var r,t,n,o=e.length,i=De.relative[e[0].type],a=i||De.relative[" "],u=i?1:0,s=Ft(function(e){return e===r},a,!0),c=Ft(function(e){return-1<st.call(r,e)},a,!0),l=[function(e,t,n){return!i&&(n||t!==Me)||((r=t).nodeType?s(e,t,n):c(e,t,n))}];u<o;u++)if(t=De.relative[e[u].type])l=[Ft(zt(l),t)];else{if((t=De.filter[e[u].type].apply(null,e[u].matches))[Ke]){for(n=++u;n<o&&!De.relative[e[n].type];n++);return Vt(1<u&&zt(l),1<u&&Mt(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(mt,"$1"),t,u<n&&Ht(e.slice(u,n)),n<o&&Ht(e=e.slice(n)),n<o&&Mt(e))}l.push(t)}return zt(l)}Lt.prototype=De.filters=De.pseudos,De.setFilters=new Lt,Pe=At.tokenize=function(e,t){var n,r,o,i,a,u,s,c=Qe[e+" "];if(c)return t?0:c.slice(0);for(a=e,u=[],s=De.preFilter;a;){for(i in n&&!(r=gt.exec(a))||(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=pt.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(mt," ")}),a=a.slice(n.length)),De.filter)!(r=yt[i].exec(a))||s[i]&&!(r=s[i](r))||(n=r.shift(),o.push({value:n,type:i,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?At.error(e):Qe(e,u).slice(0)},Ie=At.compile=function(e,t){var n,h,v,b,y,r,o=[],i=[],a=Ze[e+" "];if(!a){for(t||(t=Pe(e)),n=t.length;n--;)(a=Ht(t[n]))[Ke]?o.push(a):i.push(a);(a=Ze(e,(h=i,b=0<(v=o).length,y=0<h.length,r=function(e,t,n,r,o){var i,a,u,s=0,c="0",l=e&&[],f=[],d=Me,m=e||y&&De.find.TAG("*",o),g=Ye+=null==d?1:Math.random()||.1,p=m.length;for(o&&(Me=t!==Ve&&t);c!==p&&null!=(i=m[c]);c++){if(y&&i){for(a=0;u=h[a++];)if(u(i,t,n)){r.push(i);break}o&&(Ye=g)}b&&((i=!u&&i)&&s--,e&&l.push(i))}if(s+=c,b&&c!==s){for(a=0;u=v[a++];)u(l,f,t,n);if(e){if(0<s)for(;c--;)l[c]||f[c]||(f[c]=ot.call(r));f=Ut(f)}at.apply(r,f),o&&!e&&0<f.length&&1<s+v.length&&At.uniqueSort(r)}return o&&(Ye=g,Me=d),l},b?_t(r):r))).selector=e}return a},Le=At.select=function(e,t,n,r){var o,i,a,u,s,c="function"==typeof e&&e,l=!r&&Pe(e=c.selector||e);if(n=n||[],1===l.length){if(2<(i=l[0]=l[0].slice(0)).length&&"ID"===(a=i[0]).type&&_e.getById&&9===t.nodeType&&je&&De.relative[i[1].type]){if(!(t=(De.find.ID(a.matches[0].replace(kt,Tt),t)||[])[0]))return n;c&&(t=t.parentNode),e=e.slice(i.shift().value.length)}for(o=yt.needsContext.test(e)?0:i.length;o--&&(a=i[o],!De.relative[u=a.type]);)if((s=De.find[u])&&(r=s(a.matches[0].replace(kt,Tt),Et.test(i[0].type)&&It(t.parentNode)||t))){if(i.splice(o,1),!(e=r.length&&Mt(i)))return at.apply(n,r),n;break}}return(c||Ie(e,l))(r,t,!je,n,Et.test(e)&&It(t.parentNode)||t),n},_e.sortStable=Ke.split("").sort(et).join("")===Ke,_e.detectDuplicates=!!ze,Ue(),_e.sortDetached=!0;var jt=Array.isArray,qt=function(e,t,n){var r,o;if(!e)return 0;if(n=n||e,e.length!==undefined){for(r=0,o=e.length;r<o;r++)if(!1===t.call(n,e[r],r,e))return 0}else for(r in e)if(e.hasOwnProperty(r)&&!1===t.call(n,e[r],r,e))return 0;return 1},$t=function(e,t,n){var r,o;for(r=0,o=e.length;r<o;r++)if(t.call(n,e[r],r,e))return r;return-1},Wt={isArray:jt,toArray:function(e){var t,n,r=e;if(!jt(e))for(r=[],t=0,n=e.length;t<n;t++)r[t]=e[t];return r},each:qt,map:function(n,r){var o=[];return qt(n,function(e,t){o.push(r(e,t,n))}),o},filter:function(n,r){var o=[];return qt(n,function(e,t){r&&!r(e,t,n)||o.push(e)}),o},indexOf:function(e,t){var n,r;if(e)for(n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},reduce:function(e,t,n,r){var o=0;for(arguments.length<3&&(n=e[0]);o<e.length;o++)n=t.call(r,n,e[o],o);return n},findIndex:$t,find:function(e,t,n){var r=$t(e,t,n);return-1!==r?e[r]:undefined},last:function(e){return e[e.length-1]}},Kt=/^\s*|\s*$/g,Xt=function(e){return null===e||e===undefined?"":(""+e).replace(Kt,"")},Yt=function(e,t){return t?!("array"!==t||!Wt.isArray(e))||typeof e===t:e!==undefined},Gt=function(e,n,r,o){o=o||this,e&&(r&&(e=e[r]),Wt.each(e,function(e,t){if(!1===n.call(o,e,t,r))return!1;Gt(e,n,r,o)}))},Jt={trim:Xt,isArray:Wt.isArray,is:Yt,toArray:Wt.toArray,makeMap:function(e,t,n){var r;for(t=t||",","string"==typeof(e=e||[])&&(e=e.split(t)),n=n||{},r=e.length;r--;)n[e[r]]={};return n},each:Wt.each,map:Wt.map,grep:Wt.filter,inArray:Wt.indexOf,hasOwn:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},extend:function(e,t){for(var n,r,o,i=[],a=2;a<arguments.length;a++)i[a-2]=arguments[a];var u,s=arguments;for(n=1,r=s.length;n<r;n++)for(o in t=s[n])t.hasOwnProperty(o)&&(u=t[o])!==undefined&&(e[o]=u);return e},create:function(e,t,n){var r,o,i,a,u,s=this,c=0;if(e=/^((static) )?([\w.]+)(:([\w.]+))?/.exec(e),i=e[3].match(/(^|\.)(\w+)$/i)[2],!(o=s.createNS(e[3].replace(/\.\w+$/,""),n))[i]){if("static"===e[2])return o[i]=t,void(this.onCreate&&this.onCreate(e[2],e[3],o[i]));t[i]||(t[i]=function(){},c=1),o[i]=t[i],s.extend(o[i].prototype,t),e[5]&&(r=s.resolve(e[5]).prototype,a=e[5].match(/\.(\w+)$/i)[1],u=o[i],o[i]=c?function(){return r[a].apply(this,arguments)}:function(){return this.parent=r[a],u.apply(this,arguments)},o[i].prototype[i]=o[i],s.each(r,function(e,t){o[i].prototype[t]=r[t]}),s.each(t,function(e,t){r[t]?o[i].prototype[t]=function(){return this.parent=r[t],e.apply(this,arguments)}:t!==i&&(o[i].prototype[t]=e)})),s.each(t["static"],function(e,t){o[i][t]=e})}},walk:Gt,createNS:function(e,t){var n,r;for(t=t||H.window,e=e.split("."),n=0;n<e.length;n++)t[r=e[n]]||(t[r]={}),t=t[r];return t},resolve:function(e,t){var n,r;for(t=t||H.window,n=0,r=(e=e.split(".")).length;n<r&&(t=t[e[n]]);n++);return t},explode:function(e,t){return!e||Yt(e,"array")?e:Wt.map(e.split(t||","),Xt)},_addCacheSuffix:function(e){var t=ge.cacheSuffix;return t&&(e+=(-1===e.indexOf("?")?"?":"&")+t),e}},Qt=H.document,Zt=Array.prototype.push,en=Array.prototype.slice,tn=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,nn=Ae.Event,rn=Jt.makeMap("children,contents,next,prev"),on=function(e){return void 0!==e},an=function(e){return"string"==typeof e},un=function(e,t){var n,r,o;for(o=(t=t||Qt).createElement("div"),n=t.createDocumentFragment(),o.innerHTML=e;r=o.firstChild;)n.appendChild(r);return n},sn=function(e,t,n,r){var o;if(an(t))t=un(t,wn(e[0]));else if(t.length&&!t.nodeType){if(t=vn.makeArray(t),r)for(o=t.length-1;0<=o;o--)sn(e,t[o],n,r);else for(o=0;o<t.length;o++)sn(e,t[o],n,r);return e}if(t.nodeType)for(o=e.length;o--;)n.call(e[o],t);return e},cn=function(e,t){return e&&t&&-1!==(" "+e.className+" ").indexOf(" "+t+" ")},ln=function(e,t,n){var r,o;return t=vn(t)[0],e.each(function(){var e=this;n&&r===e.parentNode||(r=e.parentNode,o=t.cloneNode(!1),e.parentNode.insertBefore(o,e)),o.appendChild(e)}),e},fn=Jt.makeMap("fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom"," "),dn=Jt.makeMap("checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected"," "),mn={"for":"htmlFor","class":"className",readonly:"readOnly"},gn={"float":"cssFloat"},pn={},hn={},vn=function(e,t){return new vn.fn.init(e,t)},bn=/^\s*|\s*$/g,yn=function(e){return null===e||e===undefined?"":(""+e).replace(bn,"")},Cn=function(e,t){var n,r,o,i;if(e)if((n=e.length)===undefined){for(r in e)if(e.hasOwnProperty(r)&&(i=e[r],!1===t.call(i,r,i)))break}else for(o=0;o<n&&(i=e[o],!1!==t.call(i,o,i));o++);return e},xn=function(e,n){var r=[];return Cn(e,function(e,t){n(t,e)&&r.push(t)}),r},wn=function(e){return e?9===e.nodeType?e:e.ownerDocument:Qt};vn.fn=vn.prototype={constructor:vn,selector:"",context:null,length:0,init:function(e,t){var n,r,o=this;if(!e)return o;if(e.nodeType)return o.context=o[0]=e,o.length=1,o;if(t&&t.nodeType)o.context=t;else{if(t)return vn(e).attr(t);o.context=t=H.document}if(an(e)){if(!(n="<"===(o.selector=e).charAt(0)&&">"===e.charAt(e.length-1)&&3<=e.length?[null,e,null]:tn.exec(e)))return vn(t).find(e);if(n[1])for(r=un(e,wn(t)).firstChild;r;)Zt.call(o,r),r=r.nextSibling;else{if(!(r=wn(t).getElementById(n[2])))return o;if(r.id!==n[2])return o.find(e);o.length=1,o[0]=r}}else this.add(e,!1);return o},toArray:function(){return Jt.toArray(this)},add:function(e,t){var n,r,o=this;if(an(e))return o.add(vn(e));if(!1!==t)for(n=vn.unique(o.toArray().concat(vn.makeArray(e))),o.length=n.length,r=0;r<n.length;r++)o[r]=n[r];else Zt.apply(o,vn.makeArray(e));return o},attr:function(t,n){var e,r=this;if("object"==typeof t)Cn(t,function(e,t){r.attr(e,t)});else{if(!on(n)){if(r[0]&&1===r[0].nodeType){if((e=pn[t])&&e.get)return e.get(r[0],t);if(dn[t])return r.prop(t)?t:undefined;null===(n=r[0].getAttribute(t,2))&&(n=undefined)}return n}this.each(function(){var e;if(1===this.nodeType){if((e=pn[t])&&e.set)return void e.set(this,n);null===n?this.removeAttribute(t,2):this.setAttribute(t,n,2)}})}return r},removeAttr:function(e){return this.attr(e,null)},prop:function(e,t){var n=this;if("object"==typeof(e=mn[e]||e))Cn(e,function(e,t){n.prop(e,t)});else{if(!on(t))return n[0]&&n[0].nodeType&&e in n[0]?n[0][e]:t;this.each(function(){1===this.nodeType&&(this[e]=t)})}return n},css:function(n,r){var e,o,i=this,t=function(e){return e.replace(/-(\D)/g,function(e,t){return t.toUpperCase()})},a=function(e){return e.replace(/[A-Z]/g,function(e){return"-"+e})};if("object"==typeof n)Cn(n,function(e,t){i.css(e,t)});else if(on(r))n=t(n),"number"!=typeof r||fn[n]||(r=r.toString()+"px"),i.each(function(){var e=this.style;if((o=hn[n])&&o.set)o.set(this,r);else{try{this.style[gn[n]||n]=r}catch(t){}null!==r&&""!==r||(e.removeProperty?e.removeProperty(a(n)):e.removeAttribute(n))}});else{if(e=i[0],(o=hn[n])&&o.get)return o.get(e);if(!e.ownerDocument.defaultView)return e.currentStyle?e.currentStyle[t(n)]:"";try{return e.ownerDocument.defaultView.getComputedStyle(e,null).getPropertyValue(a(n))}catch(u){return undefined}}return i},remove:function(){for(var e,t=this.length;t--;)e=this[t],nn.clean(e),e.parentNode&&e.parentNode.removeChild(e);return this},empty:function(){for(var e,t=this.length;t--;)for(e=this[t];e.firstChild;)e.removeChild(e.firstChild);return this},html:function(e){var t,n=this;if(on(e)){t=n.length;try{for(;t--;)n[t].innerHTML=e}catch(r){vn(n[t]).empty().append(e)}return n}return n[0]?n[0].innerHTML:""},text:function(e){var t,n=this;if(on(e)){for(t=n.length;t--;)"innerText"in n[t]?n[t].innerText=e:n[0].textContent=e;return n}return n[0]?n[0].innerText||n[0].textContent:""},append:function(){return sn(this,arguments,function(e){(1===this.nodeType||this.host&&1===this.host.nodeType)&&this.appendChild(e)})},prepend:function(){return sn(this,arguments,function(e){(1===this.nodeType||this.host&&1===this.host.nodeType)&&this.insertBefore(e,this.firstChild)},!0)},before:function(){return this[0]&&this[0].parentNode?sn(this,arguments,function(e){this.parentNode.insertBefore(e,this)}):this},after:function(){return this[0]&&this[0].parentNode?sn(this,arguments,function(e){this.parentNode.insertBefore(e,this.nextSibling)},!0):this},appendTo:function(e){return vn(e).append(this),this},prependTo:function(e){return vn(e).prepend(this),this},replaceWith:function(e){return this.before(e).remove()},wrap:function(e){return ln(this,e)},wrapAll:function(e){return ln(this,e,!0)},wrapInner:function(e){return this.each(function(){vn(this).contents().wrapAll(e)}),this},unwrap:function(){return this.parent().each(function(){vn(this).replaceWith(this.childNodes)})},clone:function(){var e=[];return this.each(function(){e.push(this.cloneNode(!0))}),vn(e)},addClass:function(e){return this.toggleClass(e,!0)},removeClass:function(e){return this.toggleClass(e,!1)},toggleClass:function(o,i){var e=this;return"string"!=typeof o||(-1!==o.indexOf(" ")?Cn(o.split(" "),function(){e.toggleClass(this,i)}):e.each(function(e,t){var n,r;(r=cn(t,o))!==i&&(n=t.className,r?t.className=yn((" "+n+" ").replace(" "+o+" "," ")):t.className+=n?" "+o:o)})),e},hasClass:function(e){return cn(this[0],e)},each:function(e){return Cn(this,e)},on:function(e,t){return this.each(function(){nn.bind(this,e,t)})},off:function(e,t){return this.each(function(){nn.unbind(this,e,t)})},trigger:function(e){return this.each(function(){"object"==typeof e?nn.fire(this,e.type,e):nn.fire(this,e)})},show:function(){return this.css("display","")},hide:function(){return this.css("display","none")},slice:function(){return new vn(en.apply(this,arguments))},eq:function(e){return-1===e?this.slice(e):this.slice(e,+e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},find:function(e){var t,n,r=[];for(t=0,n=this.length;t<n;t++)vn.find(e,this[t],r);return vn(r)},filter:function(n){return vn("function"==typeof n?xn(this.toArray(),function(e,t){return n(t,e)}):vn.filter(n,this.toArray()))},closest:function(n){var r=[];return n instanceof vn&&(n=n[0]),this.each(function(e,t){for(;t;){if("string"==typeof n&&vn(t).is(n)){r.push(t);break}if(t===n){r.push(t);break}t=t.parentNode}}),vn(r)},offset:function(e){var t,n,r,o,i=0,a=0;return e?this.css(e):((t=this[0])&&(r=(n=t.ownerDocument).documentElement,t.getBoundingClientRect&&(i=(o=t.getBoundingClientRect()).left+(r.scrollLeft||n.body.scrollLeft)-r.clientLeft,a=o.top+(r.scrollTop||n.body.scrollTop)-r.clientTop)),{left:i,top:a})},push:Zt,sort:[].sort,splice:[].splice},Jt.extend(vn,{extend:Jt.extend,makeArray:function(e){return(t=e)&&t===t.window||e.nodeType?[e]:Jt.toArray(e);var t},inArray:function(e,t){var n;if(t.indexOf)return t.indexOf(e);for(n=t.length;n--;)if(t[n]===e)return n;return-1},isArray:Jt.isArray,each:Cn,trim:yn,grep:xn,find:At,expr:At.selectors,unique:At.uniqueSort,text:At.getText,contains:At.contains,filter:function(e,t,n){var r=t.length;for(n&&(e=":not("+e+")");r--;)1!==t[r].nodeType&&t.splice(r,1);return t=1===t.length?vn.find.matchesSelector(t[0],e)?[t[0]]:[]:vn.find.matches(e,t)}});var Nn=function(e,t,n){var r=[],o=e[t];for("string"!=typeof n&&n instanceof vn&&(n=n[0]);o&&9!==o.nodeType;){if(n!==undefined){if(o===n)break;if("string"==typeof n&&vn(o).is(n))break}1===o.nodeType&&r.push(o),o=o[t]}return r},En=function(e,t,n,r){var o=[];for(r instanceof vn&&(r=r[0]);e;e=e[t])if(!n||e.nodeType===n){if(r!==undefined){if(e===r)break;if("string"==typeof r&&vn(e).is(r))break}o.push(e)}return o},Sn=function(e,t,n){for(e=e[t];e;e=e[t])if(e.nodeType===n)return e;return null};Cn({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return Nn(e,"parentNode")},next:function(e){return Sn(e,"nextSibling",1)},prev:function(e){return Sn(e,"previousSibling",1)},children:function(e){return En(e.firstChild,"nextSibling",1)},contents:function(e){return Jt.toArray(("iframe"===e.nodeName?e.contentDocument||e.contentWindow.document:e).childNodes)}},function(e,r){vn.fn[e]=function(t){var n=[];return this.each(function(){var e=r.call(n,this,t,n);e&&(vn.isArray(e)?n.push.apply(n,e):n.push(e))}),1<this.length&&(rn[e]||(n=vn.unique(n)),0===e.indexOf("parents")&&(n=n.reverse())),n=vn(n),t?n.filter(t):n}}),Cn({parentsUntil:function(e,t){return Nn(e,"parentNode",t)},nextUntil:function(e,t){return En(e,"nextSibling",1,t).slice(1)},prevUntil:function(e,t){return En(e,"previousSibling",1,t).slice(1)}},function(r,o){vn.fn[r]=function(t,e){var n=[];return this.each(function(){var e=o.call(n,this,t,n);e&&(vn.isArray(e)?n.push.apply(n,e):n.push(e))}),1<this.length&&(n=vn.unique(n),0!==r.indexOf("parents")&&"prevUntil"!==r||(n=n.reverse())),n=vn(n),e?n.filter(e):n}}),vn.fn.is=function(e){return!!e&&0<this.filter(e).length},vn.fn.init.prototype=vn.fn,vn.overrideDefaults=function(n){var r,o=function(e,t){return r=r||n(),0===arguments.length&&(e=r.element),t||(t=r.context),new o.fn.init(e,t)};return vn.extend(o,this),o};var kn=function(n,r,e){Cn(e,function(e,t){n[e]=n[e]||{},n[e][r]=t})};ge.ie&&ge.ie<8&&(kn(pn,"get",{maxlength:function(e){var t=e.maxLength;return 2147483647===t?undefined:t},size:function(e){var t=e.size;return 20===t?undefined:t},"class":function(e){return e.className},style:function(e){var t=e.style.cssText;return 0===t.length?undefined:t}}),kn(pn,"set",{"class":function(e,t){e.className=t},style:function(e,t){e.style.cssText=t}})),ge.ie&&ge.ie<9&&(gn["float"]="styleFloat",kn(hn,"set",{opacity:function(e,t){var n=e.style;null===t||""===t?n.removeAttribute("filter"):(n.zoom=1,n.filter="alpha(opacity="+100*t+")")}})),vn.attrHooks=pn,vn.cssHooks=hn;var Tn,An,Rn,_n=function(e,t){var n=function(e,t){for(var n=0;n<e.length;n++){var r=e[n];if(r.test(t))return r}return undefined}(e,t);if(!n)return{major:0,minor:0};var r=function(e){return Number(t.replace(n,"$"+e))};return Bn(r(1),r(2))},Dn=function(){return Bn(0,0)},Bn=function(e,t){return{major:e,minor:t}},On={nu:Bn,detect:function(e,t){var n=String(t).toLowerCase();return 0===e.length?Dn():_n(e,n)},unknown:Dn},Pn="Firefox",In=function(e,t){return function(){return t===e}},Ln=function(e){var t=e.current;return{current:t,version:e.version,isEdge:In("Edge",t),isChrome:In("Chrome",t),isIE:In("IE",t),isOpera:In("Opera",t),isFirefox:In(Pn,t),isSafari:In("Safari",t)}},Mn={unknown:function(){return Ln({current:undefined,version:On.unknown()})},nu:Ln,edge:q("Edge"),chrome:q("Chrome"),ie:q("IE"),opera:q("Opera"),firefox:q(Pn),safari:q("Safari")},Fn="Windows",zn="Android",Un="Solaris",Vn="FreeBSD",Hn=function(e,t){return function(){return t===e}},jn=function(e){var t=e.current;return{current:t,version:e.version,isWindows:Hn(Fn,t),isiOS:Hn("iOS",t),isAndroid:Hn(zn,t),isOSX:Hn("OSX",t),isLinux:Hn("Linux",t),isSolaris:Hn(Un,t),isFreeBSD:Hn(Vn,t)}},qn={unknown:function(){return jn({current:undefined,version:On.unknown()})},nu:jn,windows:q(Fn),ios:q("iOS"),android:q(zn),linux:q("Linux"),osx:q("OSX"),solaris:q(Un),freebsd:q(Vn)},$n=function(e,t){var n=String(t).toLowerCase();return Y(e,function(e){return e.search(n)})},Wn=function(e,n){return $n(e,n).map(function(e){var t=On.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Kn=function(e,n){return $n(e,n).map(function(e){var t=On.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Xn=function(e,t){return-1!==e.indexOf(t)},Yn=function(e){return e.replace(/^\s+|\s+$/g,"")},Gn=function(e){return e.replace(/\s+$/g,"")},Jn=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,Qn=function(t){return function(e){return Xn(e,t)}},Zn=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return Xn(e,"edge/")&&Xn(e,"chrome")&&Xn(e,"safari")&&Xn(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,Jn],search:function(e){return Xn(e,"chrome")&&!Xn(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return Xn(e,"msie")||Xn(e,"trident")}},{name:"Opera",versionRegexes:[Jn,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:Qn("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:Qn("firefox")},{name:"Safari",versionRegexes:[Jn,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return(Xn(e,"safari")||Xn(e,"mobile/"))&&Xn(e,"applewebkit")}}],er=[{name:"Windows",search:Qn("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return Xn(e,"iphone")||Xn(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:Qn("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:Qn("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:Qn("linux"),versionRegexes:[]},{name:"Solaris",search:Qn("sunos"),versionRegexes:[]},{name:"FreeBSD",search:Qn("freebsd"),versionRegexes:[]}],tr={browsers:q(Zn),oses:q(er)},nr=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=tr.browsers(),m=tr.oses(),g=Wn(d,e).fold(Mn.unknown,Mn.nu),p=Kn(m,e).fold(qn.unknown,qn.nu);return{browser:g,os:p,deviceType:(n=g,r=e,o=(t=p).isiOS()&&!0===/ipad/i.test(r),i=t.isiOS()&&!o,a=t.isAndroid()&&3===t.version.major,u=t.isAndroid()&&4===t.version.major,s=o||a||u&&!0===/mobile/i.test(r),c=t.isiOS()||t.isAndroid(),l=c&&!s,f=n.isSafari()&&t.isiOS()&&!1===/safari/i.test(r),{isiPad:q(o),isiPhone:q(i),isTablet:q(s),isPhone:q(l),isTouch:q(c),isAndroid:t.isAndroid,isiOS:t.isiOS,isWebView:q(f)})}},rr={detect:(Tn=function(){var e=H.navigator.userAgent;return nr(e)},Rn=!1,function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return Rn||(Rn=!0,An=Tn.apply(null,e)),An})},or=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:q(e)}},ir={fromHtml:function(e,t){var n=(t||H.document).createElement("div");if(n.innerHTML=e,!n.hasChildNodes()||1<n.childNodes.length)throw H.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return or(n.childNodes[0])},fromTag:function(e,t){var n=(t||H.document).createElement(e);return or(n)},fromText:function(e,t){var n=(t||H.document).createTextNode(e);return or(n)},fromDom:or,fromPoint:function(e,t,n){var r=e.dom();return A.from(r.elementFromPoint(t,n)).map(or)}},ar=(H.Node.ATTRIBUTE_NODE,H.Node.CDATA_SECTION_NODE,H.Node.COMMENT_NODE,H.Node.DOCUMENT_NODE),ur=(H.Node.DOCUMENT_TYPE_NODE,H.Node.DOCUMENT_FRAGMENT_NODE,H.Node.ELEMENT_NODE),sr=H.Node.TEXT_NODE,cr=(H.Node.PROCESSING_INSTRUCTION_NODE,H.Node.ENTITY_REFERENCE_NODE,H.Node.ENTITY_NODE,H.Node.NOTATION_NODE,function(e){return e.dom().nodeName.toLowerCase()}),lr=function(t){return function(e){return e.dom().nodeType===t}},fr=lr(ur),dr=lr(sr),mr=Object.keys,gr=Object.hasOwnProperty,pr=function(e,t){for(var n=mr(e),r=0,o=n.length;r<o;r++){var i=n[r];t(e[i],i,e)}},hr=function(r,o){var i={};return pr(r,function(e,t){var n=o(e,t,r);i[n.k]=n.v}),i},vr=function(e){return e.style!==undefined&&P(e.style.getPropertyValue)},br=function(e,t,n){if(!(R(n)||O(n)||I(n)))throw H.console.error("Invalid call to Attr.set. Key ",t,":: Value ",n,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,n+"")},yr=function(e,t,n){br(e.dom(),t,n)},Cr=function(e,t){var n=e.dom();pr(t,function(e,t){br(n,t,e)})},xr=function(e,t){var n=e.dom().getAttribute(t);return null===n?undefined:n},wr=function(e,t){e.dom().removeAttribute(t)},Nr=function(e,t){var n=e.dom();pr(t,function(e,t){!function(e,t,n){if(!R(n))throw H.console.error("Invalid call to CSS.set. Property ",t,":: Value ",n,":: Element ",e),new Error("CSS value must be a string: "+n);vr(e)&&e.style.setProperty(t,n)}(n,t,e)})},Er=function(e,t){var n,r,o=e.dom(),i=H.window.getComputedStyle(o).getPropertyValue(t),a=""!==i||(r=dr(n=e)?n.dom().parentNode:n.dom())!==undefined&&null!==r&&r.ownerDocument.body.contains(r)?i:Sr(o,t);return null===a?undefined:a},Sr=function(e,t){return vr(e)?e.style.getPropertyValue(t):""},kr=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];if(t.length!==n.length)throw new Error('Wrong number of arguments to struct. Expected "['+t.length+']", got '+n.length+" arguments");var r={};return U(t,function(e,t){r[e]=q(n[t])}),r}},Tr=function(e,t){for(var n=[],r=function(e){return n.push(e),t(e)},o=t(e);(o=o.bind(r)).isSome(););return n},Ar=function(){return ue.getOrDie("Node")},Rr=function(e,t,n){return 0!=(e.compareDocumentPosition(t)&n)},_r=function(e,t){return Rr(e,t,Ar().DOCUMENT_POSITION_CONTAINED_BY)},Dr=ur,Br=ar,Or=function(e,t){var n=e.dom();if(n.nodeType!==Dr)return!1;if(n.matches!==undefined)return n.matches(t);if(n.msMatchesSelector!==undefined)return n.msMatchesSelector(t);if(n.webkitMatchesSelector!==undefined)return n.webkitMatchesSelector(t);if(n.mozMatchesSelector!==undefined)return n.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")},Pr=function(e){return e.nodeType!==Dr&&e.nodeType!==Br||0===e.childElementCount},Ir=function(e,t){return e.dom()===t.dom()},Lr=rr.detect().browser.isIE()?function(e,t){return _r(e.dom(),t.dom())}:function(e,t){var n=e.dom(),r=t.dom();return n!==r&&n.contains(r)},Mr=function(e){return ir.fromDom(e.dom().ownerDocument)},Fr=function(e){var t=e.dom().ownerDocument.defaultView;return ir.fromDom(t)},zr=function(e){var t=e.dom();return A.from(t.parentNode).map(ir.fromDom)},Ur=function(e){var t=e.dom();return A.from(t.previousSibling).map(ir.fromDom)},Vr=function(e){var t=e.dom();return A.from(t.nextSibling).map(ir.fromDom)},Hr=function(e){return t=Tr(e,Ur),(n=L.call(t,0)).reverse(),n;var t,n},jr=function(e){return Tr(e,Vr)},qr=function(e){var t=e.dom();return W(t.childNodes,ir.fromDom)},$r=function(e,t){var n=e.dom().childNodes;return A.from(n[t]).map(ir.fromDom)},Wr=function(e){return $r(e,0)},Kr=function(e){return $r(e,e.dom().childNodes.length-1)},Xr=(kr("element","offset"),rr.detect().browser),Yr=function(e){return Y(e,fr)},Gr={getPos:function(e,t,n){var r,o,i,a=0,u=0,s=e.ownerDocument;if(n=n||e,t){if(n===e&&t.getBoundingClientRect&&"static"===Er(ir.fromDom(e),"position"))return{x:a=(o=t.getBoundingClientRect()).left+(s.documentElement.scrollLeft||e.scrollLeft)-s.documentElement.clientLeft,y:u=o.top+(s.documentElement.scrollTop||e.scrollTop)-s.documentElement.clientTop};for(r=t;r&&r!==n&&r.nodeType;)a+=r.offsetLeft||0,u+=r.offsetTop||0,r=r.offsetParent;for(r=t.parentNode;r&&r!==n&&r.nodeType;)a-=r.scrollLeft||0,u-=r.scrollTop||0,r=r.parentNode;u+=(i=ir.fromDom(t),Xr.isFirefox()&&"table"===cr(i)?Yr(qr(i)).filter(function(e){return"caption"===cr(e)}).bind(function(o){return Yr(jr(o)).map(function(e){var t=e.dom().offsetTop,n=o.dom().offsetTop,r=o.dom().offsetHeight;return t<=n?-r:0})}).getOr(0):0)}return{x:a,y:u}}},Jr=function(e){var n=A.none(),t=[],r=function(e){o()?a(e):t.push(e)},o=function(){return n.isSome()},i=function(e){U(e,a)},a=function(t){n.each(function(e){H.setTimeout(function(){t(e)},0)})};return e(function(e){n=A.some(e),i(t),t=[]}),{get:r,map:function(n){return Jr(function(t){r(function(e){t(n(e))})})},isReady:o}},Qr={nu:Jr,pure:function(t){return Jr(function(e){e(t)})}},Zr=function(t){var e=function(e){var r;t((r=e,function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=this;H.setTimeout(function(){r.apply(n,e)},0)}))},n=function(){return Qr.nu(e)};return{map:function(r){return Zr(function(n){e(function(e){var t=r(e);n(t)})})},bind:function(n){return Zr(function(t){e(function(e){n(e).get(t)})})},anonBind:function(n){return Zr(function(t){e(function(e){n.get(t)})})},toLazy:n,toCached:function(){var t=null;return Zr(function(e){null===t&&(t=n()),t.get(e)})},get:e}},eo={nu:Zr,pure:function(t){return Zr(function(e){e(t)})}},to=function(a,e){return e(function(r){var o=[],i=0;0===a.length?r([]):U(a,function(e,t){var n;e.get((n=t,function(e){o[n]=e,++i>=a.length&&r(o)}))})})},no=function(e){return to(e,eo.nu)},ro=function(n){return{is:function(e){return n===e},isValue:x,isError:C,getOr:q(n),getOrThunk:q(n),getOrDie:q(n),or:function(e){return ro(n)},orThunk:function(e){return ro(n)},fold:function(e,t){return t(n)},map:function(e){return ro(e(n))},mapError:function(e){return ro(n)},each:function(e){e(n)},bind:function(e){return e(n)},exists:function(e){return e(n)},forall:function(e){return e(n)},toOption:function(){return A.some(n)}}},oo=function(n){return{is:C,isValue:C,isError:x,getOr:$,getOrThunk:function(e){return e()},getOrDie:function(){return e=String(n),function(){throw new Error(e)}();var e},or:function(e){return e},orThunk:function(e){return e()},fold:function(e,t){return e(n)},map:function(e){return oo(n)},mapError:function(e){return oo(e(n))},each:o,bind:function(e){return oo(n)},exists:C,forall:x,toOption:A.none}},io={value:ro,error:oo,fromOption:function(e,t){return e.fold(function(){return oo(t)},ro)}};function ao(e,u){var t=e,n=function(e,t,n,r){var o,i;if(e){if(!r&&e[t])return e[t];if(e!==u){if(o=e[n])return o;for(i=e.parentNode;i&&i!==u;i=i.parentNode)if(o=i[n])return o}}};this.current=function(){return t},this.next=function(e){return t=n(t,"firstChild","nextSibling",e)},this.prev=function(e){return t=n(t,"lastChild","previousSibling",e)},this.prev2=function(e){return t=function(e,t,n,r){var o,i,a;if(e){if(o=e[n],u&&o===u)return;if(o){if(!r)for(a=o[t];a;a=a[t])if(!a[t])return a;return o}if((i=e.parentNode)&&i!==u)return i}}(t,"lastChild","previousSibling",e)}}var uo,so,co,lo=function(t){var n;return function(e){return(n=n||function(e,t){for(var n={},r=0,o=e.length;r<o;r++){var i=e[r];n[String(i)]=t(i,r)}return n}(t,q(!0))).hasOwnProperty(cr(e))}},fo=lo(["h1","h2","h3","h4","h5","h6"]),mo=lo(["article","aside","details","div","dt","figcaption","footer","form","fieldset","header","hgroup","html","main","nav","section","summary","body","p","dl","multicol","dd","figure","address","center","blockquote","h1","h2","h3","h4","h5","h6","listing","xmp","pre","plaintext","menu","dir","ul","ol","li","hr","table","tbody","thead","tfoot","th","tr","td","caption"]),go=function(e){return fr(e)&&!mo(e)},po=function(e){return fr(e)&&"br"===cr(e)},ho=lo(["h1","h2","h3","h4","h5","h6","p","div","address","pre","form","blockquote","center","dir","fieldset","header","footer","article","section","hgroup","aside","nav","figure"]),vo=lo(["ul","ol","dl"]),bo=lo(["li","dd","dt"]),yo=lo(["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param","embed","source","wbr","track"]),Co=lo(["thead","tbody","tfoot"]),xo=lo(["td","th"]),wo=lo(["pre","script","textarea","style"]),No=function(t){return function(e){return!!e&&e.nodeType===t}},Eo=No(1),So=function(e){var r=e.toLowerCase().split(" ");return function(e){var t,n;if(e&&e.nodeType)for(n=e.nodeName.toLowerCase(),t=0;t<r.length;t++)if(n===r[t])return!0;return!1}},ko=function(t){return function(e){if(Eo(e)){if(e.contentEditable===t)return!0;if(e.getAttribute("data-mce-contenteditable")===t)return!0}return!1}},To=No(3),Ao=No(8),Ro=No(9),_o=No(11),Do=So("br"),Bo=ko("true"),Oo=ko("false"),Po={isText:To,isElement:Eo,isComment:Ao,isDocument:Ro,isDocumentFragment:_o,isBr:Do,isContentEditableTrue:Bo,isContentEditableFalse:Oo,isRestrictedNode:function(e){return!!e&&!Object.getPrototypeOf(e)},matchNodeNames:So,hasPropValue:function(t,n){return function(e){return Eo(e)&&e[t]===n}},hasAttribute:function(t,e){return function(e){return Eo(e)&&e.hasAttribute(t)}},hasAttributeValue:function(t,n){return function(e){return Eo(e)&&e.getAttribute(t)===n}},matchStyleValues:function(r,e){var o=e.toLowerCase().split(" ");return function(e){var t;if(Eo(e))for(t=0;t<o.length;t++){var n=e.ownerDocument.defaultView.getComputedStyle(e,null);if((n?n.getPropertyValue(r):null)===o[t])return!0}return!1}},isBogus:function(e){return Eo(e)&&e.hasAttribute("data-mce-bogus")},isBogusAll:function(e){return Eo(e)&&"all"===e.getAttribute("data-mce-bogus")},isTable:function(e){return Eo(e)&&"TABLE"===e.tagName}},Io=function(e){return e&&"SPAN"===e.tagName&&"bookmark"===e.getAttribute("data-mce-type")},Lo=function(e,t){var n,r=t.childNodes;if(!Po.isElement(t)||!Io(t)){for(n=r.length-1;0<=n;n--)Lo(e,r[n]);if(!1===Po.isDocument(t)){if(Po.isText(t)&&0<t.nodeValue.length){var o=Jt.trim(t.nodeValue).length;if(e.isBlock(t.parentNode)||0<o)return;if(0===o&&(a=(i=t).previousSibling&&"SPAN"===i.previousSibling.nodeName,u=i.nextSibling&&"SPAN"===i.nextSibling.nodeName,a&&u))return}else if(Po.isElement(t)&&(1===(r=t.childNodes).length&&Io(r[0])&&t.parentNode.insertBefore(r[0],t),r.length||yo(ir.fromDom(t))))return;e.remove(t)}var i,a,u;return t}},Mo={trimNode:Lo},Fo=Jt.makeMap,zo=/[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Uo=/[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Vo=/[<>&\"\']/g,Ho=/&#([a-z0-9]+);?|&([a-z0-9]+);/gi,jo={128:"\u20ac",130:"\u201a",131:"\u0192",132:"\u201e",133:"\u2026",134:"\u2020",135:"\u2021",136:"\u02c6",137:"\u2030",138:"\u0160",139:"\u2039",140:"\u0152",142:"\u017d",145:"\u2018",146:"\u2019",147:"\u201c",148:"\u201d",149:"\u2022",150:"\u2013",151:"\u2014",152:"\u02dc",153:"\u2122",154:"\u0161",155:"\u203a",156:"\u0153",158:"\u017e",159:"\u0178"};so={'"':""","'":"'","<":"<",">":">","&":"&","`":"`"},co={"<":"<",">":">","&":"&",""":'"',"'":"'"};var qo=function(e,t){var n,r,o,i={};if(e){for(e=e.split(","),t=t||10,n=0;n<e.length;n+=2)r=String.fromCharCode(parseInt(e[n],t)),so[r]||(o="&"+e[n+1]+";",i[r]=o,i[o]=r);return i}};uo=qo("50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,t9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro",32);var $o=function(e,t){return e.replace(t?zo:Uo,function(e){return so[e]||e})},Wo=function(e,t){return e.replace(t?zo:Uo,function(e){return 1<e.length?"&#"+(1024*(e.charCodeAt(0)-55296)+(e.charCodeAt(1)-56320)+65536)+";":so[e]||"&#"+e.charCodeAt(0)+";"})},Ko=function(e,t,n){return n=n||uo,e.replace(t?zo:Uo,function(e){return so[e]||n[e]||e})},Xo={encodeRaw:$o,encodeAllRaw:function(e){return(""+e).replace(Vo,function(e){return so[e]||e})},encodeNumeric:Wo,encodeNamed:Ko,getEncodeFunc:function(e,t){var n=qo(t)||uo,r=Fo(e.replace(/\+/g,","));return r.named&&r.numeric?function(e,t){return e.replace(t?zo:Uo,function(e){return so[e]!==undefined?so[e]:n[e]!==undefined?n[e]:1<e.length?"&#"+(1024*(e.charCodeAt(0)-55296)+(e.charCodeAt(1)-56320)+65536)+";":"&#"+e.charCodeAt(0)+";"})}:r.named?t?function(e,t){return Ko(e,t,n)}:Ko:r.numeric?Wo:$o},decode:function(e){return e.replace(Ho,function(e,t){return t?65535<(t="x"===t.charAt(0).toLowerCase()?parseInt(t.substr(1),16):parseInt(t,10))?(t-=65536,String.fromCharCode(55296+(t>>10),56320+(1023&t))):jo[t]||String.fromCharCode(t):co[e]||uo[e]||(n=e,(r=ir.fromTag("div").dom()).innerHTML=n,r.textContent||r.innerText||n);var n,r})}},Yo={},Go={},Jo=Jt.makeMap,Qo=Jt.each,Zo=Jt.extend,ei=Jt.explode,ti=Jt.inArray,ni=function(e,t){return(e=Jt.trim(e))?e.split(t||" "):[]},ri=function(e){var u,t,n,r,o,i,s={},a=function(e,t,n){var r,o,i,a=function(e,t){var n,r,o={};for(n=0,r=e.length;n<r;n++)o[e[n]]=t||{};return o};for(t=t||"","string"==typeof(n=n||[])&&(n=ni(n)),r=(e=ni(e)).length;r--;)i={attributes:a(o=ni([u,t].join(" "))),attributesOrder:o,children:a(n,Go)},s[e[r]]=i},c=function(e,t){var n,r,o,i;for(n=(e=ni(e)).length,t=ni(t);n--;)for(r=s[e[n]],o=0,i=t.length;o<i;o++)r.attributes[t[o]]={},r.attributesOrder.push(t[o])};return Yo[e]?Yo[e]:(u="id accesskey class dir lang style tabindex title role",t="address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul",n="a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd label map noscript object q s samp script select small span strong sub sup textarea u var #text #comment","html4"!==e&&(u+=" contenteditable contextmenu draggable dropzone hidden spellcheck translate",t+=" article aside details dialog figure main header footer hgroup section nav",n+=" audio canvas command datalist mark meter output picture progress time wbr video ruby bdi keygen"),"html5-strict"!==e&&(u+=" xml:lang",n=[n,i="acronym applet basefont big font strike tt"].join(" "),Qo(ni(i),function(e){a(e,"",n)}),t=[t,o="center dir isindex noframes"].join(" "),r=[t,n].join(" "),Qo(ni(o),function(e){a(e,"",r)})),r=r||[t,n].join(" "),a("html","manifest","head body"),a("head","","base command link meta noscript script style title"),a("title hr noscript br"),a("base","href target"),a("link","href rel media hreflang type sizes hreflang"),a("meta","name http-equiv content charset"),a("style","media type scoped"),a("script","src async defer type charset"),a("body","onafterprint onbeforeprint onbeforeunload onblur onerror onfocus onhashchange onload onmessage onoffline ononline onpagehide onpageshow onpopstate onresize onscroll onstorage onunload",r),a("address dt dd div caption","",r),a("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn","",n),a("blockquote","cite",r),a("ol","reversed start type","li"),a("ul","","li"),a("li","value",r),a("dl","","dt dd"),a("a","href target rel media hreflang type",n),a("q","cite",n),a("ins del","cite datetime",r),a("img","src sizes srcset alt usemap ismap width height"),a("iframe","src name width height",r),a("embed","src type width height"),a("object","data type typemustmatch name usemap form width height",[r,"param"].join(" ")),a("param","name value"),a("map","name",[r,"area"].join(" ")),a("area","alt coords shape href target rel media hreflang type"),a("table","border","caption colgroup thead tfoot tbody tr"+("html4"===e?" col":"")),a("colgroup","span","col"),a("col","span"),a("tbody thead tfoot","","tr"),a("tr","","td th"),a("td","colspan rowspan headers",r),a("th","colspan rowspan headers scope abbr",r),a("form","accept-charset action autocomplete enctype method name novalidate target",r),a("fieldset","disabled form name",[r,"legend"].join(" ")),a("label","form for",n),a("input","accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"),a("button","disabled form formaction formenctype formmethod formnovalidate formtarget name type value","html4"===e?r:n),a("select","disabled form multiple name required size","option optgroup"),a("optgroup","disabled label","option"),a("option","disabled label selected value"),a("textarea","cols dirname disabled form maxlength name readonly required rows wrap"),a("menu","type label",[r,"li"].join(" ")),a("noscript","",r),"html4"!==e&&(a("wbr"),a("ruby","",[n,"rt rp"].join(" ")),a("figcaption","",r),a("mark rt rp summary bdi","",n),a("canvas","width height",r),a("video","src crossorigin poster preload autoplay mediagroup loop muted controls width height buffered",[r,"track source"].join(" ")),a("audio","src crossorigin preload autoplay mediagroup loop muted controls buffered volume",[r,"track source"].join(" ")),a("picture","","img source"),a("source","src srcset type media sizes"),a("track","kind src srclang label default"),a("datalist","",[n,"option"].join(" ")),a("article section nav aside main header footer","",r),a("hgroup","","h1 h2 h3 h4 h5 h6"),a("figure","",[r,"figcaption"].join(" ")),a("time","datetime",n),a("dialog","open",r),a("command","type label icon disabled checked radiogroup command"),a("output","for form name",n),a("progress","value max",n),a("meter","value min max low high optimum",n),a("details","open",[r,"summary"].join(" ")),a("keygen","autofocus challenge disabled form keytype name")),"html5-strict"!==e&&(c("script","language xml:space"),c("style","xml:space"),c("object","declare classid code codebase codetype archive standby align border hspace vspace"),c("embed","align name hspace vspace"),c("param","valuetype type"),c("a","charset name rev shape coords"),c("br","clear"),c("applet","codebase archive code object alt name width height align hspace vspace"),c("img","name longdesc align border hspace vspace"),c("iframe","longdesc frameborder marginwidth marginheight scrolling align"),c("font basefont","size color face"),c("input","usemap align"),c("select","onchange"),c("textarea"),c("h1 h2 h3 h4 h5 h6 div p legend caption","align"),c("ul","type compact"),c("li","type"),c("ol dl menu dir","compact"),c("pre","width xml:space"),c("hr","align noshade size width"),c("isindex","prompt"),c("table","summary width frame rules cellspacing cellpadding align bgcolor"),c("col","width align char charoff valign"),c("colgroup","width align char charoff valign"),c("thead","align char charoff valign"),c("tr","align char charoff valign bgcolor"),c("th","axis align char charoff valign nowrap bgcolor width height"),c("form","accept"),c("td","abbr axis scope align char charoff valign nowrap bgcolor width height"),c("tfoot","align char charoff valign"),c("tbody","align char charoff valign"),c("area","nohref"),c("body","background bgcolor text link vlink alink")),"html4"!==e&&(c("input button select textarea","autofocus"),c("input textarea","placeholder"),c("a","download"),c("link script img","crossorigin"),c("iframe","sandbox seamless allowfullscreen")),Qo(ni("a form meter progress dfn"),function(e){s[e]&&delete s[e].children[e]}),delete s.caption.children.table,delete s.script,Yo[e]=s)},oi=function(e,n){var r;return e&&(r={},"string"==typeof e&&(e={"*":e}),Qo(e,function(e,t){r[t]=r[t.toUpperCase()]="map"===n?Jo(e,/[, ]/):ei(e,/[, ]/)})),r};function ii(i){var e,t,n,r,o,a,u,s,c,l,f,d,m,N={},g={},E=[],p={},h={},v=function(e,t,n){var r=i[e];return r?r=Jo(r,/[, ]/,Jo(r.toUpperCase(),/[, ]/)):(r=Yo[e])||(r=Jo(t," ",Jo(t.toUpperCase()," ")),r=Zo(r,n),Yo[e]=r),r};n=ri((i=i||{}).schema),!1===i.verify_html&&(i.valid_elements="*[*]"),e=oi(i.valid_styles),t=oi(i.invalid_styles,"map"),s=oi(i.valid_classes,"map"),r=v("whitespace_elements","pre script noscript style textarea video audio iframe object code"),o=v("self_closing_elements","colgroup dd dt li option p td tfoot th thead tr"),a=v("short_ended_elements","area base basefont br col frame hr img input isindex link meta param embed source wbr track"),u=v("boolean_attributes","checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls"),l=v("non_empty_elements","td th iframe video audio object script pre code",a),f=v("move_caret_before_on_enter_elements","table",l),d=v("text_block_elements","h1 h2 h3 h4 h5 h6 p div address pre form blockquote center dir fieldset header footer article section hgroup aside main nav figure"),c=v("block_elements","hr table tbody thead tfoot th tr td li ol ul caption dl dt dd noscript menu isindex option datalist select optgroup figcaption details summary",d),m=v("text_inline_elements","span strong b em i font strike u var cite dfn code mark q sup sub samp"),Qo((i.special||"script noscript noframes noembed title style textarea xmp").split(" "),function(e){h[e]=new RegExp("</"+e+"[^>]*>","gi")});var S=function(e){return new RegExp("^"+e.replace(/([?+*])/g,".$1")+"$")},b=function(e){var t,n,r,o,i,a,u,s,c,l,f,d,m,g,p,h,v,b,y,C=/^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,x=/^([!\-])?(\w+[\\:]:\w+|[^=:<]+)?(?:([=:<])(.*))?$/,w=/[*?+]/;if(e)for(e=ni(e,","),N["@"]&&(h=N["@"].attributes,v=N["@"].attributesOrder),t=0,n=e.length;t<n;t++)if(i=C.exec(e[t])){if(g=i[1],c=i[2],p=i[3],s=i[5],a={attributes:d={},attributesOrder:m=[]},"#"===g&&(a.paddEmpty=!0),"-"===g&&(a.removeEmpty=!0),"!"===i[4]&&(a.removeEmptyAttrs=!0),h){for(b in h)d[b]=h[b];m.push.apply(m,v)}if(s)for(r=0,o=(s=ni(s,"|")).length;r<o;r++)if(i=x.exec(s[r])){if(u={},f=i[1],l=i[2].replace(/[\\:]:/g,":"),g=i[3],y=i[4],"!"===f&&(a.attributesRequired=a.attributesRequired||[],a.attributesRequired.push(l),u.required=!0),"-"===f){delete d[l],m.splice(ti(m,l),1);continue}g&&("="===g&&(a.attributesDefault=a.attributesDefault||[],a.attributesDefault.push({name:l,value:y}),u.defaultValue=y),":"===g&&(a.attributesForced=a.attributesForced||[],a.attributesForced.push({name:l,value:y}),u.forcedValue=y),"<"===g&&(u.validValues=Jo(y,"?"))),w.test(l)?(a.attributePatterns=a.attributePatterns||[],u.pattern=S(l),a.attributePatterns.push(u)):(d[l]||m.push(l),d[l]=u)}h||"@"!==c||(h=d,v=m),p&&(a.outputName=c,N[p]=a),w.test(c)?(a.pattern=S(c),E.push(a)):N[c]=a}},y=function(e){N={},E=[],b(e),Qo(n,function(e,t){g[t]=e.children})},C=function(e){var a=/^(~)?(.+)$/;e&&(Yo.text_block_elements=Yo.block_elements=null,Qo(ni(e,","),function(e){var t=a.exec(e),n="~"===t[1],r=n?"span":"div",o=t[2];if(g[o]=g[r],p[o]=r,n||(c[o.toUpperCase()]={},c[o]={}),!N[o]){var i=N[r];delete(i=Zo({},i)).removeEmptyAttrs,delete i.removeEmpty,N[o]=i}Qo(g,function(e,t){e[r]&&(g[t]=e=Zo({},g[t]),e[o]=e[r])})}))},x=function(e){var o=/^([+\-]?)(\w+)\[([^\]]+)\]$/;Yo[i.schema]=null,e&&Qo(ni(e,","),function(e){var t,n,r=o.exec(e);r&&(n=r[1],t=n?g[r[2]]:g[r[2]]={"#comment":{}},t=g[r[2]],Qo(ni(r[3],"|"),function(e){"-"===n?delete t[e]:t[e]={}}))})},w=function(e){var t,n=N[e];if(n)return n;for(t=E.length;t--;)if((n=E[t]).pattern.test(e))return n};return i.valid_elements?y(i.valid_elements):(Qo(n,function(e,t){N[t]={attributes:e.attributes,attributesOrder:e.attributesOrder},g[t]=e.children}),"html5"!==i.schema&&Qo(ni("strong/b em/i"),function(e){e=ni(e,"/"),N[e[1]].outputName=e[0]}),Qo(ni("ol ul sub sup blockquote span font a table tbody tr strong em b i"),function(e){N[e]&&(N[e].removeEmpty=!0)}),Qo(ni("p h1 h2 h3 h4 h5 h6 th td pre div address caption li"),function(e){N[e].paddEmpty=!0}),Qo(ni("span"),function(e){N[e].removeEmptyAttrs=!0})),C(i.custom_elements),x(i.valid_children),b(i.extended_valid_elements),x("+ol[ul|ol],+ul[ul|ol]"),Qo({dd:"dl",dt:"dl",li:"ul ol",td:"tr",th:"tr",tr:"tbody thead tfoot",tbody:"table",thead:"table",tfoot:"table",legend:"fieldset",area:"map",param:"video audio object"},function(e,t){N[t]&&(N[t].parentsRequired=ni(e))}),i.invalid_elements&&Qo(ei(i.invalid_elements),function(e){N[e]&&delete N[e]}),w("span")||b("span[!data-mce-type|*]"),{children:g,elements:N,getValidStyles:function(){return e},getValidClasses:function(){return s},getBlockElements:function(){return c},getInvalidStyles:function(){return t},getShortEndedElements:function(){return a},getTextBlockElements:function(){return d},getTextInlineElements:function(){return m},getBoolAttrs:function(){return u},getElementRule:w,getSelfClosingElements:function(){return o},getNonEmptyElements:function(){return l},getMoveCaretBeforeOnEnterElements:function(){return f},getWhiteSpaceElements:function(){return r},getSpecialElements:function(){return h},isValidChild:function(e,t){var n=g[e.toLowerCase()];return!(!n||!n[t.toLowerCase()])},isValid:function(e,t){var n,r,o=w(e);if(o){if(!t)return!0;if(o.attributes[t])return!0;if(n=o.attributePatterns)for(r=n.length;r--;)if(n[r].pattern.test(e))return!0}return!1},getCustomElements:function(){return p},addValidElements:b,setValidElements:y,addCustomElements:C,addValidChildren:x}}var ai=function(e,t,n,r){var o=function(e){return 1<(e=parseInt(e,10).toString(16)).length?e:"0"+e};return"#"+o(t)+o(n)+o(r)};function ui(y,e){var C,t,c,l,x=/rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,w=/(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,N=/\s*([^:]+):\s*([^;]+);?/g,E=/\s+$/,S={},k="\ufeff";for(y=y||{},e&&(c=e.getValidStyles(),l=e.getInvalidStyles()),t=("\\\" \\' \\; \\: ; : "+k).split(" "),C=0;C<t.length;C++)S[t[C]]=k+C,S[k+C]=t[C];return{toHex:function(e){return e.replace(x,ai)},parse:function(e){var t,n,r,o,i,a,u,s,c={},l=y.url_converter,f=y.url_converter_scope||this,d=function(e,t,n){var r,o,i,a;if((r=c[e+"-top"+t])&&(o=c[e+"-right"+t])&&(i=c[e+"-bottom"+t])&&(a=c[e+"-left"+t])){var u=[r,o,i,a];for(C=u.length-1;C--&&u[C]===u[C+1];);-1<C&&n||(c[e+t]=-1===C?u[0]:u.join(" "),delete c[e+"-top"+t],delete c[e+"-right"+t],delete c[e+"-bottom"+t],delete c[e+"-left"+t])}},m=function(e){var t,n=c[e];if(n){for(t=(n=n.split(" ")).length;t--;)if(n[t]!==n[0])return!1;return c[e]=n[0],!0}},g=function(e){return o=!0,S[e]},p=function(e,t){return o&&(e=e.replace(/\uFEFF[0-9]/g,function(e){return S[e]})),t||(e=e.replace(/\\([\'\";:])/g,"$1")),e},h=function(e){return String.fromCharCode(parseInt(e.slice(1),16))},v=function(e){return e.replace(/\\[0-9a-f]+/gi,h)},b=function(e,t,n,r,o,i){if(o=o||i)return"'"+(o=p(o)).replace(/\'/g,"\\'")+"'";if(t=p(t||n||r),!y.allow_script_urls){var a=t.replace(/[\s\r\n]+/g,"");if(/(java|vb)script:/i.test(a))return"";if(!y.allow_svg_data_urls&&/^data:image\/svg/i.test(a))return""}return l&&(t=l.call(f,t,"style")),"url('"+t.replace(/\'/g,"\\'")+"')"};if(e){for(e=(e=e.replace(/[\u0000-\u001F]/g,"")).replace(/\\[\"\';:\uFEFF]/g,g).replace(/\"[^\"]+\"|\'[^\']+\'/g,function(e){return e.replace(/[;:]/g,g)});t=N.exec(e);)if(N.lastIndex=t.index+t[0].length,n=t[1].replace(E,"").toLowerCase(),r=t[2].replace(E,""),n&&r){if(n=v(n),r=v(r),-1!==n.indexOf(k)||-1!==n.indexOf('"'))continue;if(!y.allow_script_urls&&("behavior"===n||/expression\s*\(|\/\*|\*\//.test(r)))continue;"font-weight"===n&&"700"===r?r="bold":"color"!==n&&"background-color"!==n||(r=r.toLowerCase()),r=(r=r.replace(x,ai)).replace(w,b),c[n]=o?p(r,!0):r}d("border","",!0),d("border","-width"),d("border","-color"),d("border","-style"),d("padding",""),d("margin",""),i="border",u="border-style",s="border-color",m(a="border-width")&&m(u)&&m(s)&&(c[i]=c[a]+" "+c[u]+" "+c[s],delete c[a],delete c[u],delete c[s]),"medium none"===c.border&&delete c.border,"none"===c["border-image"]&&delete c["border-image"]}return c},serialize:function(i,e){var t,n,r,o,a,u="",s=function(e){var t,n,r,o;if(t=c[e])for(n=0,r=t.length;n<r;n++)e=t[n],(o=i[e])&&(u+=(0<u.length?" ":"")+e+": "+o+";")};if(e&&c)s("*"),s(e);else for(t in i)!(n=i[t])||l&&(r=t,o=e,a=void 0,(a=l["*"])&&a[r]||(a=l[o])&&a[r])||(u+=(0<u.length?" ":"")+t+": "+n+";");return u}}}var si,ci=Jt.each,li=Jt.grep,fi=ge.ie,di=/^([a-z0-9],?)+$/i,mi=/^[ \t\r\n]*$/,gi=function(n,r,o){var e={},i=r.keep_values,t={set:function(e,t,n){r.url_converter&&(t=r.url_converter.call(r.url_converter_scope||o(),t,n,e[0])),e.attr("data-mce-"+n,t).attr(n,t)},get:function(e,t){return e.attr("data-mce-"+t)||e.attr(t)}};return e={style:{set:function(e,t){null===t||"object"!=typeof t?(i&&e.attr("data-mce-style",t),e.attr("style",t)):e.css(t)},get:function(e){var t=e.attr("data-mce-style")||e.attr("style");return t=n.serialize(n.parse(t),e[0].nodeName)}}},i&&(e.href=e.src=t),e},pi=function(e,t){var n=t.attr("style"),r=e.serialize(e.parse(n),t[0].nodeName);r||(r=null),t.attr("data-mce-style",r)},hi=function(e,t){var n,r,o=0;if(e)for(n=e.nodeType,e=e.previousSibling;e;e=e.previousSibling)r=e.nodeType,(!t||3!==r||r!==n&&e.nodeValue.length)&&(o++,n=r);return o};function vi(a,u){var s,c=this;void 0===u&&(u={});var r={},i=H.window,o={},t=0,e=function(m,g){void 0===g&&(g={});var p,h=0,v={};p=g.maxLoadTime||5e3;var b=function(e){m.getElementsByTagName("head")[0].appendChild(e)},n=function(e,t,n){var o,r,i,a,u=function(){for(var e=a.passed,t=e.length;t--;)e[t]();a.status=2,a.passed=[],a.failed=[]},s=function(){for(var e=a.failed,t=e.length;t--;)e[t]();a.status=3,a.passed=[],a.failed=[]},c=function(e,t){e()||((new Date).getTime()-i<p?ye.setTimeout(t):s())},l=function(){c(function(){for(var e,t,n=m.styleSheets,r=n.length;r--;)if((t=(e=n[r]).ownerNode?e.ownerNode:e.owningElement)&&t.id===o.id)return u(),!0},l)},f=function(){c(function(){try{var e=r.sheet.cssRules;return u(),!!e}catch(t){}},f)};if(e=Jt._addCacheSuffix(e),v[e]?a=v[e]:(a={passed:[],failed:[]},v[e]=a),t&&a.passed.push(t),n&&a.failed.push(n),1!==a.status)if(2!==a.status)if(3!==a.status){if(a.status=1,(o=m.createElement("link")).rel="stylesheet",o.type="text/css",o.id="u"+h++,o.async=!1,o.defer=!1,i=(new Date).getTime(),g.contentCssCors&&(o.crossOrigin="anonymous"),"onload"in o&&!((d=H.navigator.userAgent.match(/WebKit\/(\d*)/))&&parseInt(d[1],10)<536))o.onload=l,o.onerror=s;else{if(0<H.navigator.userAgent.indexOf("Firefox"))return(r=m.createElement("style")).textContent='@import "'+e+'"',f(),void b(r);l()}var d;b(o),o.href=e}else s();else u()},t=function(t){return eo.nu(function(e){n(t,j(e,q(io.value(t))),j(e,q(io.error(t))))})},o=function(e){return e.fold($,$)};return{load:n,loadAll:function(e,n,r){no(W(e,t)).get(function(e){var t=K(e,function(e){return e.isValue()});0<t.fail.length?r(t.fail.map(o)):n(t.pass.map(o))})}}}(a,{contentCssCors:u.contentCssCors}),l=[],f=u.schema?u.schema:ii({}),d=ui({url_converter:u.url_converter,url_converter_scope:u.url_converter_scope},u.schema),m=u.ownEvents?new Ae(u.proxy):Ae.Event,n=f.getBlockElements(),g=vn.overrideDefaults(function(){return{context:a,element:V.getRoot()}}),p=function(e){if(e&&a&&"string"==typeof e){var t=a.getElementById(e);return t&&t.id!==e?a.getElementsByName(e)[1]:t}return e},h=function(e){return"string"==typeof e&&(e=p(e)),g(e)},v=function(e,t,n){var r,o,i=h(e);return i.length&&(o=(r=s[t])&&r.get?r.get(i,t):i.attr(t)),void 0===o&&(o=n||""),o},b=function(e){var t=p(e);return t?t.attributes:[]},y=function(e,t,n){var r,o;""===n&&(n=null);var i=h(e);r=i.attr(t),i.length&&((o=s[t])&&o.set?o.set(i,n,t):i.attr(t,n),r!==n&&u.onSetAttrib&&u.onSetAttrib({attrElm:i,attrName:t,attrValue:n}))},C=function(){return u.root_element||a.body},x=function(e,t){return Gr.getPos(a.body,p(e),t)},w=function(e,t,n){var r=h(e);return n?r.css(t):("float"===(t=t.replace(/-(\D)/g,function(e,t){return t.toUpperCase()}))&&(t=ge.ie&&ge.ie<12?"styleFloat":"cssFloat"),r[0]&&r[0].style?r[0].style[t]:undefined)},N=function(e){var t,n;return e=p(e),t=w(e,"width"),n=w(e,"height"),-1===t.indexOf("px")&&(t=0),-1===n.indexOf("px")&&(n=0),{w:parseInt(t,10)||e.offsetWidth||e.clientWidth,h:parseInt(n,10)||e.offsetHeight||e.clientHeight}},E=function(e,t){var n;if(!e)return!1;if(!Array.isArray(e)){if("*"===t)return 1===e.nodeType;if(di.test(t)){var r=t.toLowerCase().split(/,/),o=e.nodeName.toLowerCase();for(n=r.length-1;0<=n;n--)if(r[n]===o)return!0;return!1}if(e.nodeType&&1!==e.nodeType)return!1}var i=Array.isArray(e)?e:[e];return 0<At(t,i[0].ownerDocument||i[0],null,i).length},S=function(e,t,n,r){var o,i=[],a=p(e);for(r=r===undefined,n=n||("BODY"!==C().nodeName?C().parentNode:null),Jt.is(t,"string")&&(t="*"===(o=t)?function(e){return 1===e.nodeType}:function(e){return E(e,o)});a&&a!==n&&a.nodeType&&9!==a.nodeType;){if(!t||"function"==typeof t&&t(a)){if(!r)return[a];i.push(a)}a=a.parentNode}return r?i:null},k=function(e,t,n){var r=t;if(e)for("string"==typeof t&&(r=function(e){return E(e,t)}),e=e[n];e;e=e[n])if("function"==typeof r&&r(e))return e;return null},T=function(e,n,r){var o,t="string"==typeof e?p(e):e;if(!t)return!1;if(Jt.isArray(t)&&(t.length||0===t.length))return o=[],ci(t,function(e,t){e&&("string"==typeof e&&(e=p(e)),o.push(n.call(r,e,t)))}),o;var i=r||c;return n.call(i,t)},A=function(e,t){h(e).each(function(e,n){ci(t,function(e,t){y(n,t,e)})})},R=function(e,r){var t=h(e);fi?t.each(function(e,t){if(!1!==t.canHaveHTML){for(;t.firstChild;)t.removeChild(t.firstChild);try{t.innerHTML="<br>"+r,t.removeChild(t.firstChild)}catch(n){vn("<div></div>").html("<br>"+r).contents().slice(1).appendTo(t)}return r}}):t.html(r)},_=function(e,n,r,o,i){return T(e,function(e){var t="string"==typeof n?a.createElement(n):n;return A(t,r),o&&("string"!=typeof o&&o.nodeType?t.appendChild(o):"string"==typeof o&&R(t,o)),i?t:e.appendChild(t)})},D=function(e,t,n){return _(a.createElement(e),e,t,n,!0)},B=Xo.decode,O=Xo.encodeAllRaw,P=function(e,t){var n=h(e);return t?n.each(function(){for(var e;e=this.firstChild;)3===e.nodeType&&0===e.data.length?this.removeChild(e):this.parentNode.insertBefore(e,this)}).remove():n.remove(),1<n.length?n.toArray():n[0]},I=function(e,t,n){h(e).toggleClass(t,n).each(function(){""===this.className&&vn(this).attr("class",null)})},L=function(t,e,n){return T(e,function(e){return Jt.is(e,"array")&&(t=t.cloneNode(!0)),n&&ci(li(e.childNodes),function(e){t.appendChild(e)}),e.parentNode.replaceChild(t,e)})},M=function(){return a.createRange()},F=function(e,t,n,r){if(Jt.isArray(e)){for(var o=e.length;o--;)e[o]=F(e[o],t,n,r);return e}return!u.collect||e!==a&&e!==i||l.push([e,t,n,r]),m.bind(e,t,n,r||V)},z=function(e,t,n){var r;if(Jt.isArray(e)){for(r=e.length;r--;)e[r]=z(e[r],t,n);return e}if(l&&(e===a||e===i))for(r=l.length;r--;){var o=l[r];e!==o[0]||t&&t!==o[1]||n&&n!==o[2]||m.unbind(o[0],o[1],o[2])}return m.unbind(e,t,n)},U=function(e){if(e&&Po.isElement(e)){var t=e.getAttribute("data-mce-contenteditable");return t&&"inherit"!==t?t:"inherit"!==e.contentEditable?e.contentEditable:null}return null},V={doc:a,settings:u,win:i,files:o,stdMode:!0,boxModel:!0,styleSheetLoader:e,boundEvents:l,styles:d,schema:f,events:m,isBlock:function(e){if("string"==typeof e)return!!n[e];if(e){var t=e.nodeType;if(t)return!(1!==t||!n[e.nodeName])}return!1},$:g,$$:h,root:null,clone:function(t,e){if(!fi||1!==t.nodeType||e)return t.cloneNode(e);if(!e){var n=a.createElement(t.nodeName);return ci(b(t),function(e){y(n,e.nodeName,v(t,e.nodeName))}),n}return null},getRoot:C,getViewPort:function(e){var t=e||i,n=t.document.documentElement;return{x:t.pageXOffset||n.scrollLeft,y:t.pageYOffset||n.scrollTop,w:t.innerWidth||n.clientWidth,h:t.innerHeight||n.clientHeight}},getRect:function(e){var t,n;return e=p(e),t=x(e),n=N(e),{x:t.x,y:t.y,w:n.w,h:n.h}},getSize:N,getParent:function(e,t,n){var r=S(e,t,n,!1);return r&&0<r.length?r[0]:null},getParents:S,get:p,getNext:function(e,t){return k(e,t,"nextSibling")},getPrev:function(e,t){return k(e,t,"previousSibling")},select:function(e,t){return At(e,p(t)||u.root_element||a,[])},is:E,add:_,create:D,createHTML:function(e,t,n){var r,o="";for(r in o+="<"+e,t)t.hasOwnProperty(r)&&null!==t[r]&&"undefined"!=typeof t[r]&&(o+=" "+r+'="'+O(t[r])+'"');return void 0!==n?o+">"+n+"</"+e+">":o+" />"},createFragment:function(e){var t,n=a.createElement("div"),r=a.createDocumentFragment();for(e&&(n.innerHTML=e);t=n.firstChild;)r.appendChild(t);return r},remove:P,setStyle:function(e,t,n){var r=h(e).css(t,n);u.update_styles&&pi(d,r)},getStyle:w,setStyles:function(e,t){var n=h(e).css(t);u.update_styles&&pi(d,n)},removeAllAttribs:function(e){return T(e,function(e){var t,n=e.attributes;for(t=n.length-1;0<=t;t--)e.removeAttributeNode(n.item(t))})},setAttrib:y,setAttribs:A,getAttrib:v,getPos:x,parseStyle:function(e){return d.parse(e)},serializeStyle:function(e,t){return d.serialize(e,t)},addStyle:function(e){var t,n;if(V!==vi.DOM&&a===H.document){if(r[e])return;r[e]=!0}(n=a.getElementById("mceDefaultStyles"))||((n=a.createElement("style")).id="mceDefaultStyles",n.type="text/css",(t=a.getElementsByTagName("head")[0]).firstChild?t.insertBefore(n,t.firstChild):t.appendChild(n)),n.styleSheet?n.styleSheet.cssText+=e:n.appendChild(a.createTextNode(e))},loadCSS:function(e){var n;V===vi.DOM||a!==H.document?(e||(e=""),n=a.getElementsByTagName("head")[0],ci(e.split(","),function(e){var t;e=Jt._addCacheSuffix(e),o[e]||(o[e]=!0,t=D("link",{rel:"stylesheet",href:e}),n.appendChild(t))})):vi.DOM.loadCSS(e)},addClass:function(e,t){h(e).addClass(t)},removeClass:function(e,t){I(e,t,!1)},hasClass:function(e,t){return h(e).hasClass(t)},toggleClass:I,show:function(e){h(e).show()},hide:function(e){h(e).hide()},isHidden:function(e){return"none"===h(e).css("display")},uniqueId:function(e){return(e||"mce_")+t++},setHTML:R,getOuterHTML:function(e){var t="string"==typeof e?p(e):e;return Po.isElement(t)?t.outerHTML:vn("<div></div>").append(vn(t).clone()).html()},setOuterHTML:function(e,t){h(e).each(function(){try{if("outerHTML"in this)return void(this.outerHTML=t)}catch(e){}P(vn(this).html(t),!0)})},decode:B,encode:O,insertAfter:function(e,t){var r=p(t);return T(e,function(e){var t,n;return t=r.parentNode,(n=r.nextSibling)?t.insertBefore(e,n):t.appendChild(e),e})},replace:L,rename:function(t,e){var n;return t.nodeName!==e.toUpperCase()&&(n=D(e),ci(b(t),function(e){y(n,e.nodeName,v(t,e.nodeName))}),L(n,t,!0)),n||t},findCommonAncestor:function(e,t){for(var n,r=e;r;){for(n=t;n&&r!==n;)n=n.parentNode;if(r===n)break;r=r.parentNode}return!r&&e.ownerDocument?e.ownerDocument.documentElement:r},toHex:function(e){return d.toHex(Jt.trim(e))},run:T,getAttribs:b,isEmpty:function(e,t){var n,r,o,i,a,u,s=0;if(e=e.firstChild){a=new ao(e,e.parentNode),t=t||(f?f.getNonEmptyElements():null),i=f?f.getWhiteSpaceElements():{};do{if(o=e.nodeType,Po.isElement(e)){var c=e.getAttribute("data-mce-bogus");if(c){e=a.next("all"===c);continue}if(u=e.nodeName.toLowerCase(),t&&t[u]){if("br"===u){s++,e=a.next();continue}return!1}for(n=(r=b(e)).length;n--;)if("name"===(u=r[n].nodeName)||"data-mce-bookmark"===u)return!1}if(8===o)return!1;if(3===o&&!mi.test(e.nodeValue))return!1;if(3===o&&e.parentNode&&i[e.parentNode.nodeName]&&mi.test(e.nodeValue))return!1;e=a.next()}while(e)}return s<=1},createRng:M,nodeIndex:hi,split:function(e,t,n){var r,o,i,a=M();if(e&&t)return a.setStart(e.parentNode,hi(e)),a.setEnd(t.parentNode,hi(t)),r=a.extractContents(),(a=M()).setStart(t.parentNode,hi(t)+1),a.setEnd(e.parentNode,hi(e)+1),o=a.extractContents(),(i=e.parentNode).insertBefore(Mo.trimNode(V,r),e),n?i.insertBefore(n,e):i.insertBefore(t,e),i.insertBefore(Mo.trimNode(V,o),e),P(e),n||t},bind:F,unbind:z,fire:function(e,t,n){return m.fire(e,t,n)},getContentEditable:U,getContentEditableParent:function(e){for(var t=C(),n=null;e&&e!==t&&null===(n=U(e));e=e.parentNode);return n},destroy:function(){if(l)for(var e=l.length;e--;){var t=l[e];m.unbind(t[0],t[1],t[2])}At.setDocument&&At.setDocument()},isChildOf:function(e,t){for(;e;){if(t===e)return!0;e=e.parentNode}return!1},dumpRng:function(e){return"startContainer: "+e.startContainer.nodeName+", startOffset: "+e.startOffset+", endContainer: "+e.endContainer.nodeName+", endOffset: "+e.endOffset}};return s=gi(d,u,function(){return V}),V}(si=vi||(vi={})).DOM=si(H.document),si.nodeIndex=hi;var bi=vi,yi=bi.DOM,Ci=Jt.each,xi=Jt.grep,wi=function(e){return"function"==typeof e},Ni=function(){var l={},o=[],i={},a=[],f=0;this.isDone=function(e){return 2===l[e]},this.markDone=function(e){l[e]=2},this.add=this.load=function(e,t,n,r){l[e]===undefined&&(o.push(e),l[e]=0),t&&(i[e]||(i[e]=[]),i[e].push({success:t,failure:r,scope:n||this}))},this.remove=function(e){delete l[e],delete i[e]},this.loadQueue=function(e,t,n){this.loadScripts(o,e,t,n)},this.loadScripts=function(n,e,t,r){var u,s=[],c=function(t,e){Ci(i[e],function(e){wi(e[t])&&e[t].call(e.scope)}),i[e]=undefined};a.push({success:e,failure:r,scope:t||this}),(u=function(){var e=xi(n);if(n.length=0,Ci(e,function(e){var t,n,r,o,i,a;2!==l[e]?3!==l[e]?1!==l[e]&&(l[e]=1,f++,t=e,n=function(){l[e]=2,f--,c("success",e),u()},r=function(){l[e]=3,f--,s.push(e),c("failure",e),u()},i=(a=yi).uniqueId(),(o=H.document.createElement("script")).id=i,o.type="text/javascript",o.src=Jt._addCacheSuffix(t),o.onload=function(){a.remove(i),o&&(o.onreadystatechange=o.onload=o=null),n()},o.onerror=function(){wi(r)?r():"undefined"!=typeof console&&console.log&&console.log("Failed to load script: "+t)},(H.document.getElementsByTagName("head")[0]||H.document.body).appendChild(o)):c("failure",e):c("success",e)}),!f){var t=a.slice(0);a.length=0,Ci(t,function(e){0===s.length?wi(e.success)&&e.success.call(e.scope):wi(e.failure)&&e.failure.call(e.scope,s)})}})()}};Ni.ScriptLoader=new Ni;var Ei,Si=Jt.each;function ki(){var r=this,o=[],a={},u={},i=[],s=function(e){var t;return u[e]&&(t=u[e].dependencies),t||[]},c=function(e,t){return"object"==typeof t?t:"string"==typeof e?{prefix:"",resource:t,suffix:""}:{prefix:e.prefix,resource:t,suffix:e.suffix}},l=function(e,n,t,r){var o=s(e);Si(o,function(e){var t=c(n,e);f(t.resource,t,undefined,undefined)}),t&&(r?t.call(r):t.call(Ni))},f=function(e,t,n,r,o){if(!a[e]){var i="string"==typeof t?t:t.prefix+t.resource+t.suffix;0!==i.indexOf("/")&&-1===i.indexOf("://")&&(i=ki.baseURL+"/"+i),a[e]=i.substring(0,i.lastIndexOf("/")),u[e]?l(e,t,n,r):Ni.ScriptLoader.add(i,function(){return l(e,t,n,r)},r,o)}};return{items:o,urls:a,lookup:u,_listeners:i,get:function(e){return u[e]?u[e].instance:undefined},dependencies:s,requireLangPack:function(e,t){var n=ki.language;if(n&&!1!==ki.languageLoad){if(t)if(-1!==(t=","+t+",").indexOf(","+n.substr(0,2)+","))n=n.substr(0,2);else if(-1===t.indexOf(","+n+","))return;Ni.ScriptLoader.add(a[e]+"/langs/"+n+".js")}},add:function(t,e,n){o.push(e),u[t]={instance:e,dependencies:n};var r=K(i,function(e){return e.name===t});return i=r.fail,Si(r.pass,function(e){e.callback()}),e},remove:function(e){delete a[e],delete u[e]},createUrl:c,addComponents:function(e,t){var n=r.urls[e];Si(t,function(e){Ni.ScriptLoader.add(n+"/"+e)})},load:f,waitFor:function(e,t){u.hasOwnProperty(e)?t():i.push({name:e,callback:t})}}}(Ei=ki||(ki={})).PluginManager=Ei(),Ei.ThemeManager=Ei();var Ti=function(t,n){zr(t).each(function(e){e.dom().insertBefore(n.dom(),t.dom())})},Ai=function(e,t){Vr(e).fold(function(){zr(e).each(function(e){_i(e,t)})},function(e){Ti(e,t)})},Ri=function(t,n){Wr(t).fold(function(){_i(t,n)},function(e){t.dom().insertBefore(n.dom(),e.dom())})},_i=function(e,t){e.dom().appendChild(t.dom())},Di=function(t,e){U(e,function(e){_i(t,e)})},Bi=function(e){e.dom().textContent="",U(qr(e),function(e){Oi(e)})},Oi=function(e){var t=e.dom();null!==t.parentNode&&t.parentNode.removeChild(t)},Pi=function(e){var t,n=qr(e);0<n.length&&(t=e,U(n,function(e){Ti(t,e)})),Oi(e)},Ii=function(n,r){var o=null;return{cancel:function(){null!==o&&(H.clearTimeout(o),o=null)},throttle:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];null===o&&(o=H.setTimeout(function(){n.apply(null,e),o=null},r))}}},Li=function(e){var t=e,n=function(){return t};return{get:n,set:function(e){t=e},clone:function(){return Li(n())}}},Mi=function(e,t){var n=xr(e,t);return n===undefined||""===n?[]:n.split(" ")},Fi=function(e){return e.dom().classList!==undefined},zi=function(e,t){return o=t,i=Mi(n=e,r="class").concat([o]),yr(n,r,i.join(" ")),!0;var n,r,o,i},Ui=function(e,t){return o=t,0<(i=V(Mi(n=e,r="class"),function(e){return e!==o})).length?yr(n,r,i.join(" ")):wr(n,r),!1;var n,r,o,i},Vi=function(e,t){Fi(e)?e.dom().classList.add(t):zi(e,t)},Hi=function(e){0===(Fi(e)?e.dom().classList:Mi(e,"class")).length&&wr(e,"class")},ji=function(e,t){return Fi(e)&&e.dom().classList.contains(t)},qi=function(e,t){var n=[];return U(qr(e),function(e){t(e)&&(n=n.concat([e])),n=n.concat(qi(e,t))}),n},$i=function(e,t){return n=t,o=(r=e)===undefined?H.document:r.dom(),Pr(o)?[]:W(o.querySelectorAll(n),ir.fromDom);var n,r,o};function Wi(e,t,n,r,o){return e(n,r)?A.some(n):P(o)&&o(n)?A.none():t(n,r,o)}var Ki,Xi=function(e,t,n){for(var r=e.dom(),o=P(n)?n:q(!1);r.parentNode;){r=r.parentNode;var i=ir.fromDom(r);if(t(i))return A.some(i);if(o(i))break}return A.none()},Yi=function(e,t,n){return Wi(function(e){return t(e)},Xi,e,t,n)},Gi=function(e,t,n){return Xi(e,function(e){return Or(e,t)},n)},Ji=function(e,t){return n=t,o=(r=e)===undefined?H.document:r.dom(),Pr(o)?A.none():A.from(o.querySelector(n)).map(ir.fromDom);var n,r,o},Qi=function(e,t,n){return Wi(Or,Gi,e,t,n)},Zi=q("mce-annotation"),ea=q("data-mce-annotation"),ta=q("data-mce-annotation-uid"),na=function(r,e){var t=r.selection.getRng(),n=ir.fromDom(t.startContainer),o=ir.fromDom(r.getBody()),i=e.fold(function(){return"."+Zi()},function(e){return"["+ea()+'="'+e+'"]'}),a=$r(n,t.startOffset).getOr(n),u=Qi(a,i,function(e){return Ir(e,o)}),s=function(e,t){return n=t,(r=e.dom())&&r.hasAttribute&&r.hasAttribute(n)?A.some(xr(e,t)):A.none();var n,r};return u.bind(function(e){return s(e,""+ta()).bind(function(n){return s(e,""+ea()).map(function(e){var t=ra(r,n);return{uid:n,name:e,elements:t}})})})},ra=function(e,t){var n=ir.fromDom(e.getBody());return $i(n,"["+ta()+'="'+t+'"]')},oa=function(i,e){var n,r,o,a=Li({}),c=function(e,t){u(e,function(e){return t(e),e})},u=function(e,t){var n=a.get(),r=t(n.hasOwnProperty(e)?n[e]:{listeners:[],previous:Li(A.none())});n[e]=r,a.set(n)},t=(n=function(){var e,t,n,r=a.get(),o=(e=mr(r),(n=L.call(e,0)).sort(t),n);U(o,function(e){u(e,function(u){var s=u.previous.get();return na(i,A.some(e)).fold(function(){var t;s.isSome()&&(c(t=e,function(e){U(e.listeners,function(e){return e(!1,t)})}),u.previous.set(A.none()))},function(e){var t,n,r,o=e.uid,i=e.name,a=e.elements;s.is(o)||(n=o,r=a,c(t=i,function(e){U(e.listeners,function(e){return e(!0,t,{uid:n,nodes:W(r,function(e){return e.dom()})})})}),u.previous.set(A.some(o)))}),{previous:u.previous,listeners:u.listeners}})})},r=30,o=null,{cancel:function(){null!==o&&(H.clearTimeout(o),o=null)},throttle:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];null!==o&&H.clearTimeout(o),o=H.setTimeout(function(){n.apply(null,e),o=null},r)}});return i.on("remove",function(){t.cancel()}),i.on("nodeChange",function(){t.throttle()}),{addListener:function(e,t){u(e,function(e){return{previous:e.previous,listeners:e.listeners.concat([t])}})}}},ia=function(e,n){e.on("init",function(){e.serializer.addNodeFilter("span",function(e){U(e,function(t){var e;(e=t,A.from(e.attributes.map[ea()]).bind(n.lookup)).each(function(e){!1===e.persistent&&t.unwrap()})})})})},aa=0,ua=function(e,t){return ir.fromDom(e.dom().cloneNode(t))},sa=function(e){return ua(e,!1)},ca=function(e){return ua(e,!0)},la=function(e,t){var n,r,o=Mr(e).dom(),i=ir.fromDom(o.createDocumentFragment()),a=(n=t,(r=(o||H.document).createElement("div")).innerHTML=n,qr(ir.fromDom(r)));Di(i,a),Bi(e),_i(e,i)},fa="\ufeff",da=function(e){return e===fa},ma=fa,ga=function(e){return e.replace(new RegExp(fa,"g"),"")},pa=Po.isElement,ha=Po.isText,va=function(e){return ha(e)&&(e=e.parentNode),pa(e)&&e.hasAttribute("data-mce-caret")},ba=function(e){return ha(e)&&da(e.data)},ya=function(e){return va(e)||ba(e)},Ca=function(e){return e.firstChild!==e.lastChild||!Po.isBr(e.firstChild)},xa=function(e){var t=e.container();return!(!e||!Po.isText(t))&&(t.data.charAt(e.offset())===ma||e.isAtStart()&&ba(t.previousSibling))},wa=function(e){var t=e.container();return!(!e||!Po.isText(t))&&(t.data.charAt(e.offset()-1)===ma||e.isAtEnd()&&ba(t.nextSibling))},Na=function(e,t,n){var r,o,i;return(r=t.ownerDocument.createElement(e)).setAttribute("data-mce-caret",n?"before":"after"),r.setAttribute("data-mce-bogus","all"),r.appendChild(((i=H.document.createElement("br")).setAttribute("data-mce-bogus","1"),i)),o=t.parentNode,n?o.insertBefore(r,t):t.nextSibling?o.insertBefore(r,t.nextSibling):o.appendChild(r),r},Ea=function(e){return ha(e)&&e.data[0]===ma},Sa=function(e){return ha(e)&&e.data[e.data.length-1]===ma},ka=function(e){return e&&e.hasAttribute("data-mce-caret")?(t=e.getElementsByTagName("br"),n=t[t.length-1],Po.isBogus(n)&&n.parentNode.removeChild(n),e.removeAttribute("data-mce-caret"),e.removeAttribute("data-mce-bogus"),e.removeAttribute("style"),e.removeAttribute("_moz_abspos"),e):null;var t,n},Ta=Po.isContentEditableTrue,Aa=Po.isContentEditableFalse,Ra=Po.isBr,_a=Po.isText,Da=Po.matchNodeNames("script style textarea"),Ba=Po.matchNodeNames("img input textarea hr iframe video audio object"),Oa=Po.matchNodeNames("table"),Pa=ya,Ia=function(e){return!Pa(e)&&(_a(e)?!Da(e.parentNode):Ba(e)||Ra(e)||Oa(e)||La(e))},La=function(e){return!1===(t=e,Po.isElement(t)&&"true"===t.getAttribute("unselectable"))&&Aa(e);var t},Ma=function(e,t){return Ia(e)&&function(e,t){for(e=e.parentNode;e&&e!==t;e=e.parentNode){if(La(e))return!1;if(Ta(e))return!0}return!0}(e,t)},Fa=Math.round,za=function(e){return e?{left:Fa(e.left),top:Fa(e.top),bottom:Fa(e.bottom),right:Fa(e.right),width:Fa(e.width),height:Fa(e.height)}:{left:0,top:0,bottom:0,right:0,width:0,height:0}},Ua=function(e,t){return e=za(e),t||(e.left=e.left+e.width),e.right=e.left,e.width=0,e},Va=function(e,t,n){return 0<=e&&e<=Math.min(t.height,n.height)/2},Ha=function(e,t){return e.bottom-e.height/2<t.top||!(e.top>t.bottom)&&Va(t.top-e.bottom,e,t)},ja=function(e,t){return e.top>t.bottom||!(e.bottom<t.top)&&Va(t.bottom-e.top,e,t)},qa=function(e,t,n){return t>=e.left&&t<=e.right&&n>=e.top&&n<=e.bottom},$a=function(e){var t=e.startContainer,n=e.startOffset;return t.hasChildNodes()&&e.endOffset===n+1?t.childNodes[n]:null},Wa=function(e,t){return 1===e.nodeType&&e.hasChildNodes()&&(t>=e.childNodes.length&&(t=e.childNodes.length-1),e=e.childNodes[t]),e},Ka=new RegExp("[\u0300-\u036f\u0483-\u0487\u0488-\u0489\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08e3-\u0902\u093a\u093c\u0941-\u0948\u094d\u0951-\u0957\u0962-\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2-\u09e3\u0a01-\u0a02\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a70-\u0a71\u0a75\u0a81-\u0a82\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd\u0ae2-\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62-\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c00\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0c81\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc-\u0ccd\u0cd5-\u0cd6\u0ce2-\u0ce3\u0d01\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62-\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108d\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b4-\u17b5\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193b\u1a17-\u1a18\u1a1b\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1ab0-\u1abd\u1abe\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1bab-\u1bad\u1be6\u1be8-\u1be9\u1bed\u1bef-\u1bf1\u1c2c-\u1c33\u1c36-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1cf4\u1cf8-\u1cf9\u1dc0-\u1df5\u1dfc-\u1dff\u200c-\u200d\u20d0-\u20dc\u20dd-\u20e0\u20e1\u20e2-\u20e4\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302d\u302e-\u302f\u3099-\u309a\ua66f\ua670-\ua672\ua674-\ua67d\ua69e-\ua69f\ua6f0-\ua6f1\ua802\ua806\ua80b\ua825-\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\ua9e5\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43\uaa4c\uaa7c\uaab0\uaab2-\uaab4\uaab7-\uaab8\uaabe-\uaabf\uaac1\uaaec-\uaaed\uaaf6\uabe5\uabe8\uabed\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\uff9e-\uff9f]"),Xa=function(e){return"string"==typeof e&&768<=e.charCodeAt(0)&&Ka.test(e)},Ya=function(e,t){for(var n=[],r=0;r<e.length;r++){var o=e[r];if(!o.isSome())return A.none();n.push(o.getOrDie())}return A.some(t.apply(null,n))},Ga=[].slice,Ja=function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=Ga.call(arguments);return function(e){for(var t=0;t<n.length;t++)if(!n[t](e))return!1;return!0}},Qa=function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=Ga.call(arguments);return function(e){for(var t=0;t<n.length;t++)if(n[t](e))return!0;return!1}},Za=Po.isElement,eu=Ia,tu=Po.matchStyleValues("display","block table"),nu=Po.matchStyleValues("float","left right"),ru=Ja(Za,eu,y(nu)),ou=y(Po.matchStyleValues("white-space","pre pre-line pre-wrap")),iu=Po.isText,au=Po.isBr,uu=bi.nodeIndex,su=Wa,cu=function(e){return"createRange"in e?e.createRange():bi.DOM.createRng()},lu=function(e){return e&&/[\r\n\t ]/.test(e)},fu=function(e){return!!e.setStart&&!!e.setEnd},du=function(e){var t,n=e.startContainer,r=e.startOffset;return!!(lu(e.toString())&&ou(n.parentNode)&&Po.isText(n)&&(t=n.data,lu(t[r-1])||lu(t[r+1])))},mu=function(e){return 0===e.left&&0===e.right&&0===e.top&&0===e.bottom},gu=function(e){var t,n,r,o,i,a,u,s;return t=0<(n=e.getClientRects()).length?za(n[0]):za(e.getBoundingClientRect()),!fu(e)&&au(e)&&mu(t)?(i=(r=e).ownerDocument,a=cu(i),u=i.createTextNode("\xa0"),(s=r.parentNode).insertBefore(u,r),a.setStart(u,0),a.setEnd(u,1),o=za(a.getBoundingClientRect()),s.removeChild(u),o):mu(t)&&fu(e)?function(e){var t=e.startContainer,n=e.endContainer,r=e.startOffset,o=e.endOffset;if(t===n&&Po.isText(n)&&0===r&&1===o){var i=e.cloneRange();return i.setEndAfter(n),gu(i)}return null}(e):t},pu=function(e,t){var n=Ua(e,t);return n.width=1,n.right=n.left+1,n},hu=function(e){var t,n,r=[],o=function(e){var t,n;0!==e.height&&(0<r.length&&(t=e,n=r[r.length-1],t.left===n.left&&t.top===n.top&&t.bottom===n.bottom&&t.right===n.right)||r.push(e))},i=function(e,t){var n=cu(e.ownerDocument);if(t<e.data.length){if(Xa(e.data[t]))return r;if(Xa(e.data[t-1])&&(n.setStart(e,t),n.setEnd(e,t+1),!du(n)))return o(pu(gu(n),!1)),r}0<t&&(n.setStart(e,t-1),n.setEnd(e,t),du(n)||o(pu(gu(n),!1))),t<e.data.length&&(n.setStart(e,t),n.setEnd(e,t+1),du(n)||o(pu(gu(n),!0)))};if(iu(e.container()))return i(e.container(),e.offset()),r;if(Za(e.container()))if(e.isAtEnd())n=su(e.container(),e.offset()),iu(n)&&i(n,n.data.length),ru(n)&&!au(n)&&o(pu(gu(n),!1));else{if(n=su(e.container(),e.offset()),iu(n)&&i(n,0),ru(n)&&e.isAtEnd())return o(pu(gu(n),!1)),r;t=su(e.container(),e.offset()-1),ru(t)&&!au(t)&&(tu(t)||tu(n)||!ru(n))&&o(pu(gu(t),!1)),ru(n)&&o(pu(gu(n),!0))}return r};function vu(t,n,e){var r=function(){return e||(e=hu(vu(t,n))),e};return{container:q(t),offset:q(n),toRange:function(){var e;return(e=cu(t.ownerDocument)).setStart(t,n),e.setEnd(t,n),e},getClientRects:r,isVisible:function(){return 0<r().length},isAtStart:function(){return iu(t),0===n},isAtEnd:function(){return iu(t)?n>=t.data.length:n>=t.childNodes.length},isEqual:function(e){return e&&t===e.container()&&n===e.offset()},getNode:function(e){return su(t,e?n-1:n)}}}(Ki=vu||(vu={})).fromRangeStart=function(e){return Ki(e.startContainer,e.startOffset)},Ki.fromRangeEnd=function(e){return Ki(e.endContainer,e.endOffset)},Ki.after=function(e){return Ki(e.parentNode,uu(e)+1)},Ki.before=function(e){return Ki(e.parentNode,uu(e))},Ki.isAbove=function(e,t){return Ya([ne(t.getClientRects()),re(e.getClientRects())],Ha).getOr(!1)},Ki.isBelow=function(e,t){return Ya([re(t.getClientRects()),ne(e.getClientRects())],ja).getOr(!1)},Ki.isAtStart=function(e){return!!e&&e.isAtStart()},Ki.isAtEnd=function(e){return!!e&&e.isAtEnd()},Ki.isTextPosition=function(e){return!!e&&Po.isText(e.container())},Ki.isElementPosition=function(e){return!1===Ki.isTextPosition(e)};var bu,yu,Cu=vu,xu=Po.isText,wu=Po.isBogus,Nu=bi.nodeIndex,Eu=function(e){var t=e.parentNode;return wu(t)?Eu(t):t},Su=function(e){return e?Wt.reduce(e.childNodes,function(e,t){return wu(t)&&"BR"!==t.nodeName?e=e.concat(Su(t)):e.push(t),e},[]):[]},ku=function(t){return function(e){return t===e}},Tu=function(e){var t,r,n,o;return(xu(e)?"text()":e.nodeName.toLowerCase())+"["+(r=Su(Eu(t=e)),n=Wt.findIndex(r,ku(t),t),r=r.slice(0,n+1),o=Wt.reduce(r,function(e,t,n){return xu(t)&&xu(r[n-1])&&e++,e},0),r=Wt.filter(r,Po.matchNodeNames(t.nodeName)),(n=Wt.findIndex(r,ku(t),t))-o)+"]"},Au=function(e,t){var n,r,o,i,a,u=[];return n=t.container(),r=t.offset(),xu(n)?o=function(e,t){for(;(e=e.previousSibling)&&xu(e);)t+=e.data.length;return t}(n,r):(r>=(i=n.childNodes).length?(o="after",r=i.length-1):o="before",n=i[r]),u.push(Tu(n)),a=function(e,t,n){var r=[];for(t=t.parentNode;!(t===e||n&&n(t));t=t.parentNode)r.push(t);return r}(e,n),a=Wt.filter(a,y(Po.isBogus)),(u=u.concat(Wt.map(a,function(e){return Tu(e)}))).reverse().join("/")+","+o},Ru=function(e,t){var n,r,o;return t?(t=(n=t.split(","))[0].split("/"),o=1<n.length?n[1]:"before",(r=Wt.reduce(t,function(e,t){return(t=/([\w\-\(\)]+)\[([0-9]+)\]/.exec(t))?("text()"===t[1]&&(t[1]="#text"),n=e,r=t[1],o=parseInt(t[2],10),i=Su(n),i=Wt.filter(i,function(e,t){return!xu(e)||!xu(i[t-1])}),(i=Wt.filter(i,Po.matchNodeNames(r)))[o]):null;var n,r,o,i},e))?xu(r)?function(e,t){for(var n,r=e,o=0;xu(r);){if(n=r.data.length,o<=t&&t<=o+n){e=r,t-=o;break}if(!xu(r.nextSibling)){e=r,t=n;break}o+=n,r=r.nextSibling}return xu(e)&&t>e.data.length&&(t=e.data.length),Cu(e,t)}(r,parseInt(o,10)):(o="after"===o?Nu(r)+1:Nu(r),Cu(r.parentNode,o)):null):null},_u=function(e,t){Po.isText(t)&&0===t.data.length&&e.remove(t)},Du=function(e,t,n){var r,o,i,a,u,s,c;Po.isDocumentFragment(n)?(i=e,a=t,u=n,s=A.from(u.firstChild),c=A.from(u.lastChild),a.insertNode(u),s.each(function(e){return _u(i,e.previousSibling)}),c.each(function(e){return _u(i,e.nextSibling)})):(r=e,o=n,t.insertNode(o),_u(r,o.previousSibling),_u(r,o.nextSibling))},Bu=Po.isContentEditableFalse,Ou=function(e,t,n,r,o){var i,a=r[o?"startContainer":"endContainer"],u=r[o?"startOffset":"endOffset"],s=[],c=0,l=e.getRoot();for(Po.isText(a)?s.push(n?function(e,t,n){var r,o;for(o=e(t.data.slice(0,n)).length,r=t.previousSibling;r&&Po.isText(r);r=r.previousSibling)o+=e(r.data).length;return o}(t,a,u):u):(u>=(i=a.childNodes).length&&i.length&&(c=1,u=Math.max(0,i.length-1)),s.push(e.nodeIndex(i[u],n)+c));a&&a!==l;a=a.parentNode)s.push(e.nodeIndex(a,n));return s},Pu=function(e,t,n){var r=0;return Jt.each(e.select(t),function(e){if("all"!==e.getAttribute("data-mce-bogus"))return e!==n&&void r++}),r},Iu=function(e,t){var n,r,o,i=t?"start":"end";n=e[i+"Container"],r=e[i+"Offset"],Po.isElement(n)&&"TR"===n.nodeName&&(n=(o=n.childNodes)[Math.min(t?r:r-1,o.length-1)])&&(r=t?0:n.childNodes.length,e["set"+(t?"Start":"End")](n,r))},Lu=function(e){return Iu(e,!0),Iu(e,!1),e},Mu=function(e,t){var n;if(Po.isElement(e)&&(e=Wa(e,t),Bu(e)))return e;if(ya(e)){if(Po.isText(e)&&va(e)&&(e=e.parentNode),n=e.previousSibling,Bu(n))return n;if(n=e.nextSibling,Bu(n))return n}},Fu=function(e,t,n){var r=n.getNode(),o=r?r.nodeName:null,i=n.getRng();if(Bu(r)||"IMG"===o)return{name:o,index:Pu(n.dom,o,r)};var a,u,s,c,l,f,d,m=Mu((a=i).startContainer,a.startOffset)||Mu(a.endContainer,a.endOffset);return m?{name:o=m.tagName,index:Pu(n.dom,o,m)}:(u=e,c=t,l=i,f=(s=n).dom,(d={}).start=Ou(f,u,c,l,!0),s.isCollapsed()||(d.end=Ou(f,u,c,l,!1)),d)},zu=function(e,t,n){var r={"data-mce-type":"bookmark",id:t,style:"overflow:hidden;line-height:0px"};return n?e.create("span",r,""):e.create("span",r)},Uu=function(e,t){var n=e.dom,r=e.getRng(),o=n.uniqueId(),i=e.isCollapsed(),a=e.getNode(),u=a.nodeName;if("IMG"===u)return{name:u,index:Pu(n,u,a)};var s=Lu(r.cloneRange());if(!i){s.collapse(!1);var c=zu(n,o+"_end",t);Du(n,s,c)}(r=Lu(r)).collapse(!0);var l=zu(n,o+"_start",t);return Du(n,r,l),e.moveToBookmark({id:o,keep:1}),{id:o}},Vu={getBookmark:function(e,t,n){return 2===t?Fu(ga,n,e):3===t?(o=(r=e).getRng(),{start:Au(r.dom.getRoot(),Cu.fromRangeStart(o)),end:Au(r.dom.getRoot(),Cu.fromRangeEnd(o))}):t?{rng:e.getRng()}:Uu(e,!1);var r,o},getUndoBookmark:d(Fu,$,!0),getPersistentBookmark:Uu},Hu="_mce_caret",ju=function(e){return Po.isElement(e)&&e.id===Hu},qu=function(e,t){for(;t&&t!==e;){if(t.id===Hu)return t;t=t.parentNode}return null},$u=Po.isElement,Wu=Po.isText,Ku=function(e){var t=e.parentNode;t&&t.removeChild(e)},Xu=function(e,t){0===t.length?Ku(e):e.nodeValue=t},Yu=function(e){var t=ga(e);return{count:e.length-t.length,text:t}},Gu=function(e,t){return Zu(e),t},Ju=function(e,t){var n,r,o,i=t.container(),a=(n=oe(i.childNodes),r=e,o=M(n,r),-1===o?A.none():A.some(o)).map(function(e){return e<t.offset()?Cu(i,t.offset()-1):t}).getOr(t);return Zu(e),a},Qu=function(e,t){return Wu(e)&&t.container()===e?(r=t,o=Yu((n=e).data.substr(0,r.offset())),i=Yu(n.data.substr(r.offset())),0<(a=o.text+i.text).length?(Xu(n,a),Cu(n,r.offset()-o.count)):r):Gu(e,t);var n,r,o,i,a},Zu=function(e){if($u(e)&&ya(e)&&(Ca(e)?e.removeAttribute("data-mce-caret"):Ku(e)),Wu(e)){var t=ga(function(e){try{return e.nodeValue}catch(t){return""}}(e));Xu(e,t)}},es={removeAndReposition:function(e,t){return Cu.isTextPosition(t)?Qu(e,t):(n=e,(r=t).container()===n.parentNode?Ju(n,r):Gu(n,r));var n,r},remove:Zu},ts=rr.detect().browser,ns=Po.isContentEditableFalse,rs=function(e,t,n){var r,o,i,a,u,s=Ua(t.getBoundingClientRect(),n);return"BODY"===e.tagName?(r=e.ownerDocument.documentElement,o=e.scrollLeft||r.scrollLeft,i=e.scrollTop||r.scrollTop):(u=e.getBoundingClientRect(),o=e.scrollLeft-u.left,i=e.scrollTop-u.top),s.left+=o,s.right+=o,s.top+=i,s.bottom+=i,s.width=1,0<(a=t.offsetWidth-t.clientWidth)&&(n&&(a*=-1),s.left+=a,s.right+=a),s},os=function(a,u,e){var t,s,c=Li(A.none()),l=function(){!function(e){var t,n,r,o,i;for(t=vn("*[contentEditable=false]",e),o=0;o<t.length;o++)r=(n=t[o]).previousSibling,Sa(r)&&(1===(i=r.data).length?r.parentNode.removeChild(r):r.deleteData(i.length-1,1)),r=n.nextSibling,Ea(r)&&(1===(i=r.data).length?r.parentNode.removeChild(r):r.deleteData(0,1))}(a),s&&(es.remove(s),s=null),c.get().each(function(e){vn(e.caret).remove(),c.set(A.none())}),clearInterval(t)},f=function(){t=ye.setInterval(function(){e()?vn("div.mce-visual-caret",a).toggleClass("mce-visual-caret-hidden"):vn("div.mce-visual-caret",a).addClass("mce-visual-caret-hidden")},500)};return{show:function(t,e){var n,r,o;if(l(),o=e,Po.isElement(o)&&/^(TD|TH)$/i.test(o.tagName))return null;if(!u(e))return s=function(e,t){var n,r,o;if(r=e.ownerDocument.createTextNode(ma),o=e.parentNode,t){if(n=e.previousSibling,ha(n)){if(ya(n))return n;if(Sa(n))return n.splitText(n.data.length-1)}o.insertBefore(r,e)}else{if(n=e.nextSibling,ha(n)){if(ya(n))return n;if(Ea(n))return n.splitText(1),n}e.nextSibling?o.insertBefore(r,e.nextSibling):o.appendChild(r)}return r}(e,t),r=e.ownerDocument.createRange(),ns(s.nextSibling)?(r.setStart(s,0),r.setEnd(s,0)):(r.setStart(s,1),r.setEnd(s,1)),r;s=Na("p",e,t),n=rs(a,e,t),vn(s).css("top",n.top);var i=vn('<div class="mce-visual-caret" data-mce-bogus="all"></div>').css(n).appendTo(a)[0];return c.set(A.some({caret:i,element:e,before:t})),c.get().each(function(e){t&&vn(e.caret).addClass("mce-visual-caret-before")}),f(),(r=e.ownerDocument.createRange()).setStart(s,0),r.setEnd(s,0),r},hide:l,getCss:function(){return".mce-visual-caret {position: absolute;background-color: black;background-color: currentcolor;}.mce-visual-caret-hidden {display: none;}*[data-mce-caret] {position: absolute;left: -1000px;right: auto;top: 0;margin: 0;padding: 0;}"},reposition:function(){c.get().each(function(e){var t=rs(a,e.element,e.before);vn(e.caret).css(t)})},destroy:function(){return ye.clearInterval(t)}}},is=function(){return ts.isIE()||ts.isEdge()||ts.isFirefox()},as=function(e){return ns(e)||Po.isTable(e)&&is()},us=Po.isContentEditableFalse,ss=Po.matchStyleValues("display","block table table-cell table-caption list-item"),cs=ya,ls=va,fs=Po.isElement,ds=Ia,ms=function(e){return 0<e},gs=function(e){return e<0},ps=function(e,t){for(var n;n=e(t);)if(!ls(n))return n;return null},hs=function(e,t,n,r,o){var i=new ao(e,r);if(gs(t)){if((us(e)||ls(e))&&n(e=ps(i.prev,!0)))return e;for(;e=ps(i.prev,o);)if(n(e))return e}if(ms(t)){if((us(e)||ls(e))&&n(e=ps(i.next,!0)))return e;for(;e=ps(i.next,o);)if(n(e))return e}return null},vs=function(e,t){for(;e&&e!==t;){if(ss(e))return e;e=e.parentNode}return null},bs=function(e,t,n){return vs(e.container(),n)===vs(t.container(),n)},ys=function(e,t){var n,r;return t?(n=t.container(),r=t.offset(),fs(n)?n.childNodes[r+e]:null):null},Cs=function(e,t){var n=t.ownerDocument.createRange();return e?(n.setStartBefore(t),n.setEndBefore(t)):(n.setStartAfter(t),n.setEndAfter(t)),n},xs=function(e,t,n){var r,o,i,a;for(o=e?"previousSibling":"nextSibling";n&&n!==t;){if(r=n[o],cs(r)&&(r=r[o]),us(r)){if(a=n,vs(r,i=t)===vs(a,i))return r;break}if(ds(r))break;n=n.parentNode}return null},ws=d(Cs,!0),Ns=d(Cs,!1),Es=function(e,t,n){var r,o,i,a,u=d(xs,!0,t),s=d(xs,!1,t);if(o=n.startContainer,i=n.startOffset,va(o)){if(fs(o)||(o=o.parentNode),"before"===(a=o.getAttribute("data-mce-caret"))&&(r=o.nextSibling,as(r)))return ws(r);if("after"===a&&(r=o.previousSibling,as(r)))return Ns(r)}if(!n.collapsed)return n;if(Po.isText(o)){if(cs(o)){if(1===e){if(r=s(o))return ws(r);if(r=u(o))return Ns(r)}if(-1===e){if(r=u(o))return Ns(r);if(r=s(o))return ws(r)}return n}if(Sa(o)&&i>=o.data.length-1)return 1===e&&(r=s(o))?ws(r):n;if(Ea(o)&&i<=1)return-1===e&&(r=u(o))?Ns(r):n;if(i===o.data.length)return(r=s(o))?ws(r):n;if(0===i)return(r=u(o))?Ns(r):n}return n},Ss=function(e,t){return A.from(ys(e?0:-1,t)).filter(us)},ks=function(e,t,n){var r=Es(e,t,n);return-1===e?vu.fromRangeStart(r):vu.fromRangeEnd(r)},Ts=function(e){return A.from(e.getNode()).map(ir.fromDom)},As=function(e,t){for(;t=e(t);)if(t.isVisible())return t;return t},Rs=function(e,t){var n=bs(e,t);return!(n||!Po.isBr(e.getNode()))||n};(yu=bu||(bu={}))[yu.Backwards=-1]="Backwards",yu[yu.Forwards=1]="Forwards";var _s,Ds,Bs,Os,Ps,Is=Po.isContentEditableFalse,Ls=Po.isText,Ms=Po.isElement,Fs=Po.isBr,zs=Ia,Us=function(e){return Ba(e)||!!La(t=e)&&!0!==X(oe(t.getElementsByTagName("*")),function(e,t){return e||Ta(t)},!1);var t},Vs=Ma,Hs=function(e,t){return e.hasChildNodes()&&t<e.childNodes.length?e.childNodes[t]:null},js=function(e,t){if(ms(e)){if(zs(t.previousSibling)&&!Ls(t.previousSibling))return Cu.before(t);if(Ls(t))return Cu(t,0)}if(gs(e)){if(zs(t.nextSibling)&&!Ls(t.nextSibling))return Cu.after(t);if(Ls(t))return Cu(t,t.data.length)}return gs(e)?Fs(t)?Cu.before(t):Cu.after(t):Cu.before(t)},qs=function(e,t,n){var r,o,i,a,u;if(!Ms(n)||!t)return null;if(t.isEqual(Cu.after(n))&&n.lastChild){if(u=Cu.after(n.lastChild),gs(e)&&zs(n.lastChild)&&Ms(n.lastChild))return Fs(n.lastChild)?Cu.before(n.lastChild):u}else u=t;var s,c,l,f=u.container(),d=u.offset();if(Ls(f)){if(gs(e)&&0<d)return Cu(f,--d);if(ms(e)&&d<f.length)return Cu(f,++d);r=f}else{if(gs(e)&&0<d&&(o=Hs(f,d-1),zs(o)))return!Us(o)&&(i=hs(o,e,Vs,o))?Ls(i)?Cu(i,i.data.length):Cu.after(i):Ls(o)?Cu(o,o.data.length):Cu.before(o);if(ms(e)&&d<f.childNodes.length&&(o=Hs(f,d),zs(o)))return Fs(o)?(s=n,(l=(c=o).nextSibling)&&zs(l)?Ls(l)?Cu(l,0):Cu.before(l):qs(bu.Forwards,Cu.after(c),s)):!Us(o)&&(i=hs(o,e,Vs,o))?Ls(i)?Cu(i,0):Cu.before(i):Ls(o)?Cu(o,0):Cu.after(o);r=o||u.getNode()}return(ms(e)&&u.isAtEnd()||gs(e)&&u.isAtStart())&&(r=hs(r,e,q(!0),n,!0),Vs(r,n))?js(e,r):(o=hs(r,e,Vs,n),!(a=Wt.last(V(function(e,t){for(var n=[];e&&e!==t;)n.push(e),e=e.parentNode;return n}(f,n),Is)))||o&&a.contains(o)?o?js(e,o):null:u=ms(e)?Cu.after(a):Cu.before(a))},$s=function(t){return{next:function(e){return qs(bu.Forwards,e,t)},prev:function(e){return qs(bu.Backwards,e,t)}}},Ws=function(e){return Cu.isTextPosition(e)?0===e.offset():Ia(e.getNode())},Ks=function(e){if(Cu.isTextPosition(e)){var t=e.container();return e.offset()===t.data.length}return Ia(e.getNode(!0))},Xs=function(e,t){return!Cu.isTextPosition(e)&&!Cu.isTextPosition(t)&&e.getNode()===t.getNode(!0)},Ys=function(e,t,n){return e?!Xs(t,n)&&(r=t,!(!Cu.isTextPosition(r)&&Po.isBr(r.getNode())))&&Ks(t)&&Ws(n):!Xs(n,t)&&Ws(t)&&Ks(n);var r},Gs=function(e,t,n){var r=$s(t);return A.from(e?r.next(n):r.prev(n))},Js=function(t,n,r){return Gs(t,n,r).bind(function(e){return bs(r,e,n)&&Ys(t,r,e)?Gs(t,n,e):A.some(e)})},Qs=function(t,n,e,r){return Js(t,n,e).bind(function(e){return r(e)?Qs(t,n,e,r):A.some(e)})},Zs=function(e,t){var n,r,o,i,a,u=e?t.firstChild:t.lastChild;return Po.isText(u)?A.some(Cu(u,e?0:u.data.length)):u?Ia(u)?A.some(e?Cu.before(u):(a=u,Po.isBr(a)?Cu.before(a):Cu.after(a))):(r=t,o=u,i=(n=e)?Cu.before(o):Cu.after(o),Gs(n,r,i)):A.none()},ec=d(Gs,!0),tc=d(Gs,!1),nc={fromPosition:Gs,nextPosition:ec,prevPosition:tc,navigate:Js,navigateIgnore:Qs,positionIn:Zs,firstPositionIn:d(Zs,!0),lastPositionIn:d(Zs,!1)},rc=function(e,t){return!e.isBlock(t)||t.innerHTML||ge.ie||(t.innerHTML='<br data-mce-bogus="1" />'),t},oc=function(e,t){return nc.lastPositionIn(e).fold(function(){return!1},function(e){return t.setStart(e.container(),e.offset()),t.setEnd(e.container(),e.offset()),!0})},ic=function(e,t,n){return!(!1!==t.hasChildNodes()||!qu(e,t)||(o=n,i=(r=t).ownerDocument.createTextNode(ma),r.appendChild(i),o.setStart(i,0),o.setEnd(i,0),0));var r,o,i},ac=function(e,t,n,r){var o,i,a,u,s=n[t?"start":"end"],c=e.getRoot();if(s){for(a=s[0],i=c,o=s.length-1;1<=o;o--){if(u=i.childNodes,ic(c,i,r))return!0;if(s[o]>u.length-1)return!!ic(c,i,r)||oc(i,r);i=u[s[o]]}3===i.nodeType&&(a=Math.min(s[0],i.nodeValue.length)),1===i.nodeType&&(a=Math.min(s[0],i.childNodes.length)),t?r.setStart(i,a):r.setEnd(i,a)}return!0},uc=function(e){return Po.isText(e)&&0<e.data.length},sc=function(e,t,n){var r,o,i,a,u,s,c=e.get(n.id+"_"+t),l=n.keep;if(c){if(r=c.parentNode,"start"===t?l?c.hasChildNodes()?(r=c.firstChild,o=1):uc(c.nextSibling)?(r=c.nextSibling,o=0):uc(c.previousSibling)?(r=c.previousSibling,o=c.previousSibling.data.length):(r=c.parentNode,o=e.nodeIndex(c)+1):o=e.nodeIndex(c):l?c.hasChildNodes()?(r=c.firstChild,o=1):uc(c.previousSibling)?(r=c.previousSibling,o=c.previousSibling.data.length):(r=c.parentNode,o=e.nodeIndex(c)):o=e.nodeIndex(c),u=r,s=o,!l){for(a=c.previousSibling,i=c.nextSibling,Jt.each(Jt.grep(c.childNodes),function(e){Po.isText(e)&&(e.nodeValue=e.nodeValue.replace(/\uFEFF/g,""))});c=e.get(n.id+"_"+t);)e.remove(c,!0);a&&i&&a.nodeType===i.nodeType&&Po.isText(a)&&!ge.opera&&(o=a.nodeValue.length,a.appendData(i.nodeValue),e.remove(i),u=a,s=o)}return A.some(Cu(u,s))}return A.none()},cc=function(e,t){var n,r,o,i,a,u,s,c,l,f,d,m,g,p,h,v,b=e.dom;if(t){if(v=t,Jt.isArray(v.start))return p=t,h=(g=b).createRng(),ac(g,!0,p,h)&&ac(g,!1,p,h)?A.some(h):A.none();if("string"==typeof t.start)return A.some((f=t,d=(l=b).createRng(),m=Ru(l.getRoot(),f.start),d.setStart(m.container(),m.offset()),m=Ru(l.getRoot(),f.end),d.setEnd(m.container(),m.offset()),d));if(t.hasOwnProperty("id"))return s=sc(o=b,"start",i=t),c=sc(o,"end",i),Ya([s,(a=c,u=s,a.isSome()?a:u)],function(e,t){var n=o.createRng();return n.setStart(rc(o,e.container()),e.offset()),n.setEnd(rc(o,t.container()),t.offset()),n});if(t.hasOwnProperty("name"))return n=b,r=t,A.from(n.select(r.name)[r.index]).map(function(e){var t=n.createRng();return t.selectNode(e),t});if(t.hasOwnProperty("rng"))return A.some(t.rng)}return A.none()},lc=function(e,t,n){return Vu.getBookmark(e,t,n)},fc=function(t,e){cc(t,e).each(function(e){t.setRng(e)})},dc=function(e){return Po.isElement(e)&&"SPAN"===e.tagName&&"bookmark"===e.getAttribute("data-mce-type")},mc=function(e){return e&&/^(IMG)$/.test(e.nodeName)},gc=function(e){return e&&3===e.nodeType&&/^([\t \r\n]+|)$/.test(e.nodeValue)},pc=function(e,t,n){return"color"!==n&&"backgroundColor"!==n||(t=e.toHex(t)),"fontWeight"===n&&700===t&&(t="bold"),"fontFamily"===n&&(t=t.replace(/[\'\"]/g,"").replace(/,\s+/g,",")),""+t},hc={isInlineBlock:mc,moveStart:function(e,t,n){var r,o,i,a=n.startOffset,u=n.startContainer;if((n.startContainer!==n.endContainer||!mc(n.startContainer.childNodes[n.startOffset]))&&1===u.nodeType)for(a<(i=u.childNodes).length?r=new ao(u=i[a],e.getParent(u,e.isBlock)):(r=new ao(u=i[i.length-1],e.getParent(u,e.isBlock))).next(!0),o=r.current();o;o=r.next())if(3===o.nodeType&&!gc(o))return n.setStart(o,0),void t.setRng(n)},getNonWhiteSpaceSibling:function(e,t,n){if(e)for(t=t?"nextSibling":"previousSibling",e=n?e:e[t];e;e=e[t])if(1===e.nodeType||!gc(e))return e},isTextBlock:function(e,t){return t.nodeType&&(t=t.nodeName),!!e.schema.getTextBlockElements()[t.toLowerCase()]},isValid:function(e,t,n){return e.schema.isValidChild(t,n)},isWhiteSpaceNode:gc,replaceVars:function(e,n){return"string"!=typeof e?e=e(n):n&&(e=e.replace(/%(\w+)/g,function(e,t){return n[t]||e})),e},isEq:function(e,t){return t=t||"",e=""+((e=e||"").nodeName||e),t=""+(t.nodeName||t),e.toLowerCase()===t.toLowerCase()},normalizeStyleValue:pc,getStyle:function(e,t,n){return pc(e,e.getStyle(t,n),n)},getTextDecoration:function(t,e){var n;return t.getParent(e,function(e){return(n=t.getStyle(e,"text-decoration"))&&"none"!==n}),n},getParents:function(e,t,n){return e.getParents(t,n,e.getRoot())}},vc=dc,bc=hc.getParents,yc=hc.isWhiteSpaceNode,Cc=hc.isTextBlock,xc=function(e,t){for(void 0===t&&(t=3===e.nodeType?e.length:e.childNodes.length);e&&e.hasChildNodes();)(e=e.childNodes[t])&&(t=3===e.nodeType?e.length:e.childNodes.length);return{node:e,offset:t}},wc=function(e,t){for(var n=t;n;){if(1===n.nodeType&&e.getContentEditable(n))return"false"===e.getContentEditable(n)?n:t;n=n.parentNode}return t},Nc=function(e,t,n,r){var o,i,a=n.nodeValue;return void 0===r&&(r=e?a.length:0),e?(o=a.lastIndexOf(" ",r),-1!==(o=(i=a.lastIndexOf("\xa0",r))<o?o:i)&&!t&&(o<r||!e)&&o<=a.length&&o++):(o=a.indexOf(" ",r),i=a.indexOf("\xa0",r),o=-1!==o&&(-1===i||o<i)?o:i),o},Ec=function(e,t,n,r,o,i){var a,u,s,c;if(3===n.nodeType){if(-1!==(s=Nc(o,i,n,r)))return{container:n,offset:s};c=n}for(a=new ao(n,e.getParent(n,e.isBlock)||t);u=a[o?"prev":"next"]();)if(3!==u.nodeType||vc(u.parentNode)){if(e.isBlock(u)||hc.isEq(u,"BR"))break}else if(-1!==(s=Nc(o,i,c=u)))return{container:u,offset:s};if(c)return{container:c,offset:r=o?0:c.length}},Sc=function(e,t,n,r,o){var i,a,u,s;for(3===r.nodeType&&0===r.nodeValue.length&&r[o]&&(r=r[o]),i=bc(e,r),a=0;a<i.length;a++)for(u=0;u<t.length;u++)if(!("collapsed"in(s=t[u])&&s.collapsed!==n.collapsed)&&e.is(i[a],s.selector))return i[a];return r},kc=function(t,e,n,r){var o,i=t.dom,a=i.getRoot();if(e[0].wrapper||(o=i.getParent(n,e[0].block,a)),!o){var u=i.getParent(n,"LI,TD,TH");o=i.getParent(3===n.nodeType?n.parentNode:n,function(e){return e!==a&&Cc(t,e)},u)}if(o&&e[0].wrapper&&(o=bc(i,o,"ul,ol").reverse()[0]||o),!o)for(o=n;o[r]&&!i.isBlock(o[r])&&(o=o[r],!hc.isEq(o,"br")););return o||n},Tc=function(e,t,n,r,o,i,a){var u,s,c,l,f,d;if(u=s=a?n:o,l=a?"previousSibling":"nextSibling",f=e.getRoot(),3===u.nodeType&&!yc(u)&&(a?0<r:i<u.nodeValue.length))return u;for(;;){if(!t[0].block_expand&&e.isBlock(s))return s;for(c=s[l];c;c=c[l])if(!vc(c)&&!yc(c)&&("BR"!==(d=c).nodeName||!d.getAttribute("data-mce-bogus")||d.nextSibling))return s;if(s===f||s.parentNode===f){u=s;break}s=s.parentNode}return u},Ac=function(e,t,n,r){var o,i=t.startContainer,a=t.startOffset,u=t.endContainer,s=t.endOffset,c=e.dom;return 1===i.nodeType&&i.hasChildNodes()&&3===(i=Wa(i,a)).nodeType&&(a=0),1===u.nodeType&&u.hasChildNodes()&&3===(u=Wa(u,t.collapsed?s:s-1)).nodeType&&(s=u.nodeValue.length),i=wc(c,i),u=wc(c,u),(vc(i.parentNode)||vc(i))&&(i=vc(i)?i:i.parentNode,3===(i=t.collapsed?i.previousSibling||i:i.nextSibling||i).nodeType&&(a=t.collapsed?i.length:0)),(vc(u.parentNode)||vc(u))&&(u=vc(u)?u:u.parentNode,3===(u=t.collapsed?u.nextSibling||u:u.previousSibling||u).nodeType&&(s=t.collapsed?0:u.length)),t.collapsed&&((o=Ec(c,e.getBody(),i,a,!0,r))&&(i=o.container,a=o.offset),(o=Ec(c,e.getBody(),u,s,!1,r))&&(u=o.container,s=o.offset)),n[0].inline&&(u=r?u:function(e,t){var n=xc(e,t);if(n.node){for(;n.node&&0===n.offset&&n.node.previousSibling;)n=xc(n.node.previousSibling);n.node&&0<n.offset&&3===n.node.nodeType&&" "===n.node.nodeValue.charAt(n.offset-1)&&1<n.offset&&(e=n.node).splitText(n.offset-1)}return e}(u,s)),(n[0].inline||n[0].block_expand)&&(n[0].inline&&3===i.nodeType&&0!==a||(i=Tc(c,n,i,a,u,s,!0)),n[0].inline&&3===u.nodeType&&s!==u.nodeValue.length||(u=Tc(c,n,i,a,u,s,!1))),n[0].selector&&!1!==n[0].expand&&!n[0].inline&&(i=Sc(c,n,t,i,"previousSibling"),u=Sc(c,n,t,u,"nextSibling")),(n[0].block||n[0].selector)&&(i=kc(e,n,i,"previousSibling"),u=kc(e,n,u,"nextSibling"),n[0].block&&(c.isBlock(i)||(i=Tc(c,n,i,a,u,s,!0)),c.isBlock(u)||(u=Tc(c,n,i,a,u,s,!1)))),1===i.nodeType&&(a=c.nodeIndex(i),i=i.parentNode),1===u.nodeType&&(s=c.nodeIndex(u)+1,u=u.parentNode),{startContainer:i,startOffset:a,endContainer:u,endOffset:s}},Rc=Jt.each,_c=function(e,t,o){var n,r,i,a,u,s,c,l=t.startContainer,f=t.startOffset,d=t.endContainer,m=t.endOffset;if(0<(c=e.select("td[data-mce-selected],th[data-mce-selected]")).length)Rc(c,function(e){o([e])});else{var g,p,h,v=function(e){var t;return 3===(t=e[0]).nodeType&&t===l&&f>=t.nodeValue.length&&e.splice(0,1),t=e[e.length-1],0===m&&0<e.length&&t===d&&3===t.nodeType&&e.splice(e.length-1,1),e},b=function(e,t,n){for(var r=[];e&&e!==n;e=e[t])r.push(e);return r},y=function(e,t){do{if(e.parentNode===t)return e;e=e.parentNode}while(e)},C=function(e,t,n){var r=n?"nextSibling":"previousSibling";for(u=(a=e).parentNode;a&&a!==t;a=u)u=a.parentNode,(s=b(a===e?a:a[r],r)).length&&(n||s.reverse(),o(v(s)))};if(1===l.nodeType&&l.hasChildNodes()&&(l=l.childNodes[f]),1===d.nodeType&&d.hasChildNodes()&&(p=m,h=(g=d).childNodes,--p>h.length-1?p=h.length-1:p<0&&(p=0),d=h[p]||g),l===d)return o(v([l]));for(n=e.findCommonAncestor(l,d),a=l;a;a=a.parentNode){if(a===d)return C(l,n,!0);if(a===n)break}for(a=d;a;a=a.parentNode){if(a===l)return C(d,n);if(a===n)break}r=y(l,n)||l,i=y(d,n)||d,C(l,r,!0),(s=b(r===l?r:r.nextSibling,"nextSibling",i===d?i.nextSibling:i)).length&&o(v(s)),C(d,i)}},Dc=(_s=dr,Ds="text",Bs=function(e){return _s(e)?A.from(e.dom().nodeValue):A.none()},Os=rr.detect().browser,{get:function(e){if(!_s(e))throw new Error("Can only get "+Ds+" value of a "+Ds+" node");return Ps(e).getOr("")},getOption:Ps=Os.isIE()&&10===Os.version.major?function(e){try{return Bs(e)}catch(ZN){return A.none()}}:Bs,set:function(e,t){if(!_s(e))throw new Error("Can only set raw "+Ds+" value of a "+Ds+" node");e.dom().nodeValue=t}}),Bc=function(e){return Dc.get(e)},Oc=function(r,o,i,a){return zr(o).fold(function(){return"skipping"},function(e){return"br"===a||dr(n=o)&&"\ufeff"===Bc(n)?"valid":fr(t=o)&&ji(t,Zi())?"existing":ju(o)?"caret":hc.isValid(r,i,a)&&hc.isValid(r,cr(e),i)?"valid":"invalid-child";var t,n})},Pc=function(e,t,n,r){var o,i,a=t.uid,u=void 0===a?(o="mce-annotation",i=(new Date).getTime(),o+"_"+Math.floor(1e9*Math.random())+ ++aa+String(i)):a,s=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var o=0;for(r=Object.getOwnPropertySymbols(e);o<r.length;o++)t.indexOf(r[o])<0&&Object.prototype.propertyIsEnumerable.call(e,r[o])&&(n[r[o]]=e[r[o]])}return n}(t,["uid"]),c=ir.fromTag("span",e);Vi(c,Zi()),yr(c,""+ta(),u),yr(c,""+ea(),n);var l,f=r(u,s),d=f.attributes,m=void 0===d?{}:d,g=f.classes,p=void 0===g?[]:g;return Cr(c,m),l=c,U(p,function(e){Vi(l,e)}),c},Ic=function(i,e,t,n,r){var a=[],u=Pc(i.getDoc(),r,t,n),s=Li(A.none()),c=function(){s.set(A.none())},l=function(e){U(e,o)},o=function(e){var t,n;switch(Oc(i,e,"span",cr(e))){case"invalid-child":c();var r=qr(e);l(r),c();break;case"valid":var o=s.get().getOrThunk(function(){var e=sa(u);return a.push(e),s.set(A.some(e)),e});Ti(t=e,n=o),_i(n,t)}};return _c(i.dom,e,function(e){var t;c(),t=W(e,ir.fromDom),l(t)}),a},Lc=function(s,c,l,f){s.undoManager.transact(function(){var e,t,n,r,o=s.selection.getRng();if(o.collapsed&&(r=Ac(e=s,t=o,[{inline:!0}],3===(n=t).startContainer.nodeType&&n.startContainer.nodeValue.length>=n.startOffset&&"\xa0"===n.startContainer.nodeValue[n.startOffset]),t.setStart(r.startContainer,r.startOffset),t.setEnd(r.endContainer,r.endOffset),e.selection.setRng(t)),s.selection.getRng().collapsed){var i=Pc(s.getDoc(),f,c,l.decorate);la(i,"\xa0"),s.selection.getRng().insertNode(i.dom()),s.selection.select(i.dom())}else{var a=Vu.getPersistentBookmark(s.selection,!1),u=s.selection.getRng();Ic(s,u,c,l.decorate,f),s.selection.moveToBookmark(a)}})};function Mc(s){var n,r=(n={},{register:function(e,t){n[e]={name:e,settings:t}},lookup:function(e){return n.hasOwnProperty(e)?A.from(n[e]).map(function(e){return e.settings}):A.none()}});ia(s,r);var o=oa(s);return{register:function(e,t){r.register(e,t)},annotate:function(t,n){r.lookup(t).each(function(e){Lc(s,t,e,n)})},annotationChanged:function(e,t){o.addListener(e,t)},remove:function(e){na(s,A.some(e)).each(function(e){var t=e.elements;U(t,Pi)})},getAll:function(e){var t,n,r,o,i,a,u=(t=s,n=e,r=ir.fromDom(t.getBody()),o=$i(r,"["+ea()+'="'+n+'"]'),i={},U(o,function(e){var t=xr(e,ta()),n=i.hasOwnProperty(t)?i[t]:[];i[t]=n.concat([e])}),i);return a=function(e){return W(e,function(e){return e.dom()})},hr(u,function(e,t,n){return{k:t,v:a(e,t,n)}})}}}var Fc=function(e){return Jt.grep(e.childNodes,function(e){return"LI"===e.nodeName})},zc=function(e){return e&&e.firstChild&&e.firstChild===e.lastChild&&("\xa0"===(t=e.firstChild).data||Po.isBr(t));var t},Uc=function(e){return 0<e.length&&(!(t=e[e.length-1]).firstChild||zc(t))?e.slice(0,-1):e;var t},Vc=function(e,t){var n=e.getParent(t,e.isBlock);return n&&"LI"===n.nodeName?n:null},Hc=function(e,t){var n=Cu.after(e),r=$s(t).prev(n);return r?r.toRange():null},jc=function(t,e,n){var r,o,i,a,u=t.parentNode;return Jt.each(e,function(e){u.insertBefore(e,t)}),r=t,o=n,i=Cu.before(r),(a=$s(o).next(i))?a.toRange():null},qc=function(e,t){var n,r,o,i,a,u,s=t.firstChild,c=t.lastChild;return s&&"meta"===s.name&&(s=s.next),c&&"mce_marker"===c.attr("id")&&(c=c.prev),r=c,u=(n=e).getNonEmptyElements(),r&&(r.isEmpty(u)||(o=r,n.getBlockElements()[o.name]&&(a=o).firstChild&&a.firstChild===a.lastChild&&("br"===(i=o.firstChild).name||"\xa0"===i.value)))&&(c=c.prev),!(!s||s!==c||"ul"!==s.name&&"ol"!==s.name)},$c=function(e,o,i,t){var n,r,a,u,s,c,l,f,d,m,g,p,h,v,b,y,C,x,w,N=(n=o,r=t,c=e.serialize(r),l=n.createFragment(c),u=(a=l).firstChild,s=a.lastChild,u&&"META"===u.nodeName&&u.parentNode.removeChild(u),s&&"mce_marker"===s.id&&s.parentNode.removeChild(s),a),E=Vc(o,i.startContainer),S=Uc(Fc(N.firstChild)),k=o.getRoot(),T=function(e){var t=Cu.fromRangeStart(i),n=$s(o.getRoot()),r=1===e?n.prev(t):n.next(t);return!r||Vc(o,r.getNode())!==E};return T(1)?jc(E,S,k):T(2)?(f=E,d=S,m=k,o.insertAfter(d.reverse(),f),Hc(d[0],m)):(p=S,h=k,v=g=E,y=(b=i).cloneRange(),C=b.cloneRange(),y.setStartBefore(v),C.setEndAfter(v),x=[y.cloneContents(),C.cloneContents()],(w=g.parentNode).insertBefore(x[0],g),Jt.each(p,function(e){w.insertBefore(e,g)}),w.insertBefore(x[1],g),w.removeChild(g),Hc(p[p.length-1],h))},Wc=function(e,t){return!!Vc(e,t)},Kc=Jt.each,Xc=function(o){this.compare=function(e,t){if(e.nodeName!==t.nodeName)return!1;var n=function(n){var r={};return Kc(o.getAttribs(n),function(e){var t=e.nodeName.toLowerCase();0!==t.indexOf("_")&&"style"!==t&&0!==t.indexOf("data-")&&(r[t]=o.getAttrib(n,t))}),r},r=function(e,t){var n,r;for(r in e)if(e.hasOwnProperty(r)){if(void 0===(n=t[r]))return!1;if(e[r]!==n)return!1;delete t[r]}for(r in t)if(t.hasOwnProperty(r))return!1;return!0};return!(!r(n(e),n(t))||!r(o.parseStyle(o.getAttrib(e,"style")),o.parseStyle(o.getAttrib(t,"style")))||dc(e)||dc(t))}},Yc=function(e){var t=$i(e,"br"),n=V(function(e){for(var t=[],n=e.dom();n;)t.push(ir.fromDom(n)),n=n.lastChild;return t}(e).slice(-1),po);t.length===n.length&&U(n,Oi)},Gc=function(e){Bi(e),_i(e,ir.fromHtml('<br data-mce-bogus="1">'))},Jc=function(n){Kr(n).each(function(t){Ur(t).each(function(e){mo(n)&&po(t)&&mo(e)&&Oi(t)})})},Qc=Jt.makeMap;function Zc(e){var u,s,c,l,f,d=[];return u=(e=e||{}).indent,s=Qc(e.indent_before||""),c=Qc(e.indent_after||""),l=Xo.getEncodeFunc(e.entity_encoding||"raw",e.entities),f="html"===e.element_format,{start:function(e,t,n){var r,o,i,a;if(u&&s[e]&&0<d.length&&0<(a=d[d.length-1]).length&&"\n"!==a&&d.push("\n"),d.push("<",e),t)for(r=0,o=t.length;r<o;r++)i=t[r],d.push(" ",i.name,'="',l(i.value,!0),'"');d[d.length]=!n||f?">":" />",n&&u&&c[e]&&0<d.length&&0<(a=d[d.length-1]).length&&"\n"!==a&&d.push("\n")},end:function(e){var t;d.push("</",e,">"),u&&c[e]&&0<d.length&&0<(t=d[d.length-1]).length&&"\n"!==t&&d.push("\n")},text:function(e,t){0<e.length&&(d[d.length]=t?e:l(e))},cdata:function(e){d.push("<![CDATA[",e,"]]>")},comment:function(e){d.push("\x3c!--",e,"--\x3e")},pi:function(e,t){t?d.push("<?",e," ",l(t),"?>"):d.push("<?",e,"?>"),u&&d.push("\n")},doctype:function(e){d.push("<!DOCTYPE",e,">",u?"\n":"")},reset:function(){d.length=0},getContent:function(){return d.join("").replace(/\n$/,"")}}}function el(t,g){void 0===g&&(g=ii());var p=Zc(t);return(t=t||{}).validate=!("validate"in t)||t.validate,{serialize:function(e){var f,d;d=t.validate,f={3:function(e){p.text(e.value,e.raw)},8:function(e){p.comment(e.value)},7:function(e){p.pi(e.name,e.value)},10:function(e){p.doctype(e.value)},4:function(e){p.cdata(e.value)},11:function(e){if(e=e.firstChild)for(;m(e),e=e.next;);}},p.reset();var m=function(e){var t,n,r,o,i,a,u,s,c,l=f[e.type];if(l)l(e);else{if(t=e.name,n=e.shortEnded,r=e.attributes,d&&r&&1<r.length&&((a=[]).map={},c=g.getElementRule(e.name))){for(u=0,s=c.attributesOrder.length;u<s;u++)(o=c.attributesOrder[u])in r.map&&(i=r.map[o],a.map[o]=i,a.push({name:o,value:i}));for(u=0,s=r.length;u<s;u++)(o=r[u].name)in a.map||(i=r.map[o],a.map[o]=i,a.push({name:o,value:i}));r=a}if(p.start(e.name,r,n),!n){if(e=e.firstChild)for(;m(e),e=e.next;);p.end(t)}}};return 1!==e.type||t.inner?f[11](e):m(e),p.getContent()}}}var tl,nl=function(a){var u=Cu.fromRangeStart(a),s=Cu.fromRangeEnd(a),c=a.commonAncestorContainer;return nc.fromPosition(!1,c,s).map(function(e){return!bs(u,s,c)&&bs(u,e,c)?(t=u.container(),n=u.offset(),r=e.container(),o=e.offset(),(i=H.document.createRange()).setStart(t,n),i.setEnd(r,o),i):a;var t,n,r,o,i}).getOr(a)},rl=function(e){return e.collapsed?e:nl(e)},ol=Po.matchNodeNames("td th"),il=function(e,t){var n,r,o=e.selection.getRng(),i=o.startContainer,a=o.startOffset;o.collapsed&&(n=i,r=a,Po.isText(n)&&"\xa0"===n.nodeValue[r-1])&&Po.isText(i)&&(i.insertData(a-1," "),i.deleteData(a,1),o.setStart(i,a),o.setEnd(i,a),e.selection.setRng(o)),e.selection.setContent(t)},al=function(e,t,n){var r,o,i,a,u,s,c,l,f,d,m,g=e.selection,p=e.dom;if(/^ | $/.test(t)&&(t=function(e,t){var n,r;n=e.startContainer,r=e.startOffset;var o=function(e){return n[e]&&3===n[e].nodeType};return 3===n.nodeType&&(0<r?t=t.replace(/^ /," "):o("previousSibling")||(t=t.replace(/^ /," ")),r<n.length?t=t.replace(/ (<br>|)$/," "):o("nextSibling")||(t=t.replace(/( | )(<br>|)$/," "))),t}(g.getRng(),t)),r=e.parser,m=n.merge,o=el({validate:e.settings.validate},e.schema),d='<span id="mce_marker" data-mce-type="bookmark">​</span>',s={content:t,format:"html",selection:!0,paste:n.paste},(s=e.fire("BeforeSetContent",s)).isDefaultPrevented())e.fire("SetContent",{content:s.content,format:"html",selection:!0,paste:n.paste});else{-1===(t=s.content).indexOf("{$caret}")&&(t+="{$caret}"),t=t.replace(/\{\$caret\}/,d);var h,v,b,y,C,x,w=(l=g.getRng()).startContainer||(l.parentElement?l.parentElement():null),N=e.getBody();w===N&&g.isCollapsed()&&p.isBlock(N.firstChild)&&(h=e,(v=N.firstChild)&&!h.schema.getShortEndedElements()[v.nodeName])&&p.isEmpty(N.firstChild)&&((l=p.createRng()).setStart(N.firstChild,0),l.setEnd(N.firstChild,0),g.setRng(l)),g.isCollapsed()||(e.selection.setRng(rl(e.selection.getRng())),e.getDoc().execCommand("Delete",!1,null),b=e.selection.getRng(),y=t,C=b.startContainer,x=b.startOffset,3===C.nodeType&&b.collapsed&&("\xa0"===C.data[x]?(C.deleteData(x,1),/[\u00a0| ]$/.test(y)||(y+=" ")):"\xa0"===C.data[x-1]&&(C.deleteData(x-1,1),/[\u00a0| ]$/.test(y)||(y=" "+y))),t=y);var E,S,k,T={context:(i=g.getNode()).nodeName.toLowerCase(),data:n.data,insert:!0};if(u=r.parse(t,T),!0===n.paste&&qc(e.schema,u)&&Wc(p,i))return l=$c(o,p,e.selection.getRng(),u),e.selection.setRng(l),void e.fire("SetContent",s);if(function(e){for(var t=e;t=t.walk();)1===t.type&&t.attr("data-mce-fragment","1")}(u),"mce_marker"===(f=u.lastChild).attr("id"))for(f=(c=f).prev;f;f=f.walk(!0))if(3===f.type||!p.isBlock(f.name)){e.schema.isValidChild(f.parent.name,"span")&&f.parent.insert(c,f,"br"===f.name);break}if(e._selectionOverrides.showBlockCaretContainer(i),T.invalid){for(il(e,d),i=g.getNode(),a=e.getBody(),9===i.nodeType?i=f=a:f=i;f!==a;)f=(i=f).parentNode;t=i===a?a.innerHTML:p.getOuterHTML(i),t=o.serialize(r.parse(t.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i,function(){return o.serialize(u)}))),i===a?p.setHTML(a,t):p.setOuterHTML(i,t)}else!function(e,t,n){if("all"===n.getAttribute("data-mce-bogus"))n.parentNode.insertBefore(e.dom.createFragment(t),n);else{var r=n.firstChild,o=n.lastChild;!r||r===o&&"BR"===r.nodeName?e.dom.setHTML(n,t):il(e,t)}}(e,t=o.serialize(u),i);!function(e,t){var n=e.schema.getTextInlineElements(),r=e.dom;if(t){var o=e.getBody(),i=new Xc(r);Jt.each(r.select("*[data-mce-fragment]"),function(e){for(var t=e.parentNode;t&&t!==o;t=t.parentNode)n[e.nodeName.toLowerCase()]&&i.compare(t,e)&&r.remove(e,!0)})}}(e,m),function(n,e){var t,r,o,i,a,u=n.dom,s=n.selection;if(e){if(n.selection.scrollIntoView(e),t=function(e){for(var t=n.getBody();e&&e!==t;e=e.parentNode)if("false"===n.dom.getContentEditable(e))return e;return null}(e))return u.remove(e),s.select(t);var c=u.createRng();(i=e.previousSibling)&&3===i.nodeType?(c.setStart(i,i.nodeValue.length),ge.ie||(a=e.nextSibling)&&3===a.nodeType&&(i.appendData(a.data),a.parentNode.removeChild(a))):(c.setStartBefore(e),c.setEndBefore(e)),r=u.getParent(e,u.isBlock),u.remove(e),r&&u.isEmpty(r)&&(n.$(r).empty(),c.setStart(r,0),c.setEnd(r,0),ol(r)||r.getAttribute("data-mce-fragment")||!(o=function(e){var t=Cu.fromRangeStart(e);if(t=$s(n.getBody()).next(t))return t.toRange()}(c))?u.add(r,u.create("br",{"data-mce-bogus":"1"})):(c=o,u.remove(r))),s.setRng(c)}}(e,p.get("mce_marker")),E=e.getBody(),Jt.each(E.getElementsByTagName("*"),function(e){e.removeAttribute("data-mce-fragment")}),S=e.dom,k=e.selection.getStart(),A.from(S.getParent(k,"td,th")).map(ir.fromDom).each(Jc),e.fire("SetContent",s),e.addVisual()}},ul=function(e,t){var n,r,o="string"!=typeof(n=t)?(r=Jt.extend({paste:n.paste,data:{paste:n.paste}},n),{content:n.content,details:r}):{content:n,details:{}};al(e,o.content,o.details)},sl=/[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/,cl=function(e,t,n){var r=e.getParam(t,n);if(-1!==r.indexOf("=")){var o=e.getParam(t,"","hash");return o.hasOwnProperty(e.id)?o[e.id]:n}return r},ll=function(e){return e.getParam("iframe_attrs",{})},fl=function(e){return e.getParam("doctype","<!DOCTYPE html>")},dl=function(e){return e.getParam("document_base_url","")},ml=function(e){return cl(e,"body_id","tinymce")},gl=function(e){return cl(e,"body_class","")},pl=function(e){return e.getParam("content_security_policy","")},hl=function(e){return e.getParam("br_in_pre",!0)},vl=function(e){if(e.getParam("force_p_newlines",!1))return"p";var t=e.getParam("forced_root_block","p");return!1===t?"":t},bl=function(e){return e.getParam("forced_root_block_attrs",{})},yl=function(e){return e.getParam("br_newline_selector",".mce-toc h2,figcaption,caption")},Cl=function(e){return e.getParam("no_newline_selector","")},xl=function(e){return e.getParam("keep_styles",!0)},wl=function(e){return e.getParam("end_container_on_empty_block",!1)},Nl=function(e){return Jt.explode(e.getParam("font_size_style_values",""))},El=function(e){return Jt.explode(e.getParam("font_size_classes",""))},Sl=function(e){return e.getParam("images_dataimg_filter",q(!0),"function")},kl=function(e){return e.getParam("automatic_uploads",!0,"boolean")},Tl=function(e){return e.getParam("images_reuse_filename",!1,"boolean")},Al=function(e){return e.getParam("images_replace_blob_uris",!0,"boolean")},Rl=function(e){return e.getParam("images_upload_url","","string")},_l=function(e){return e.getParam("images_upload_base_path","","string")},Dl=function(e){return e.getParam("images_upload_credentials",!1,"boolean")},Bl=function(e){return e.getParam("images_upload_handler",null,"function")},Ol=function(e){return e.getParam("content_css_cors",!1,"boolean")},Pl=function(e){return e.getParam("inline_boundaries_selector","a[href],code,.mce-annotation","string")},Il=function(e,t){if(!t)return t;var n=t.container(),r=t.offset();return e?ba(n)?Po.isText(n.nextSibling)?Cu(n.nextSibling,0):Cu.after(n):xa(t)?Cu(n,r+1):t:ba(n)?Po.isText(n.previousSibling)?Cu(n.previousSibling,n.previousSibling.data.length):Cu.before(n):wa(t)?Cu(n,r-1):t},Ll={isInlineTarget:function(e,t){return Or(ir.fromDom(t),Pl(e))},findRootInline:function(e,t,n){var r,o,i,a=(r=e,o=t,i=n,V(bi.DOM.getParents(i.container(),"*",o),r));return A.from(a[a.length-1])},isRtl:function(e){return"rtl"===bi.DOM.getStyle(e,"direction",!0)||(t=e.textContent,sl.test(t));var t},isAtZwsp:function(e){return xa(e)||wa(e)},normalizePosition:Il,normalizeForwards:d(Il,!0),normalizeBackwards:d(Il,!1),hasSameParentBlock:function(e,t,n){var r=vs(t,e),o=vs(n,e);return r&&r===o}},Ml=function(e,t){return Lr(e,t)?Yi(t,function(e){return ho(e)||bo(e)},(n=e,function(e){return Ir(n,ir.fromDom(e.dom().parentNode))})):A.none();var n},Fl=function(e){var t,n,r;e.dom.isEmpty(e.getBody())&&(e.setContent(""),n=(t=e).getBody(),r=n.firstChild&&t.dom.isBlock(n.firstChild)?n.firstChild:n,t.selection.setCursorLocation(r,0))},zl=function(i,a,u){return Ya([nc.firstPositionIn(u),nc.lastPositionIn(u)],function(e,t){var n=Ll.normalizePosition(!0,e),r=Ll.normalizePosition(!1,t),o=Ll.normalizePosition(!1,a);return i?nc.nextPosition(u,o).map(function(e){return e.isEqual(r)&&a.isEqual(n)}).getOr(!1):nc.prevPosition(u,o).map(function(e){return e.isEqual(n)&&a.isEqual(r)}).getOr(!1)}).getOr(!0)},Ul=function(e,t){var n,r,o,i=ir.fromDom(e),a=ir.fromDom(t);return n=a,r="pre,code",o=d(Ir,i),Gi(n,r,o).isSome()},Vl=function(e,t){return Ia(t)&&!1===(r=e,o=t,Po.isText(o)&&/^[ \t\r\n]*$/.test(o.data)&&!1===Ul(r,o))||(n=t,Po.isElement(n)&&"A"===n.nodeName&&n.hasAttribute("name"))||Hl(t);var n,r,o},Hl=Po.hasAttribute("data-mce-bookmark"),jl=Po.hasAttribute("data-mce-bogus"),ql=Po.hasAttributeValue("data-mce-bogus","all"),$l=function(e){return function(e){var t,n,r=0;if(Vl(e,e))return!1;if(!(n=e.firstChild))return!0;t=new ao(n,e);do{if(ql(n))n=t.next(!0);else if(jl(n))n=t.next();else if(Po.isBr(n))r++,n=t.next();else{if(Vl(e,n))return!1;n=t.next()}}while(n);return r<=1}(e.dom())},Wl=kr("block","position"),Kl=kr("from","to"),Xl=function(e,t){var n=ir.fromDom(e),r=ir.fromDom(t.container());return Ml(n,r).map(function(e){return Wl(e,t)})},Yl=function(o,i,e){var t=Xl(o,Cu.fromRangeStart(e)),n=t.bind(function(e){return nc.fromPosition(i,o,e.position()).bind(function(e){return Xl(o,e).map(function(e){return t=o,n=i,r=e,Po.isBr(r.position().getNode())&&!1===$l(r.block())?nc.positionIn(!1,r.block().dom()).bind(function(e){return e.isEqual(r.position())?nc.fromPosition(n,t,e).bind(function(e){return Xl(t,e)}):A.some(r)}).getOr(r):r;var t,n,r})})});return Ya([t,n],Kl).filter(function(e){return!1===Ir((r=e).from().block(),r.to().block())&&zr((n=e).from().block()).bind(function(t){return zr(n.to().block()).filter(function(e){return Ir(t,e)})}).isSome()&&(t=e,!1===Po.isContentEditableFalse(t.from().block())&&!1===Po.isContentEditableFalse(t.to().block()));var t,n,r})},Gl=function(e,t,n){return n.collapsed?Yl(e,t,n):A.none()},Jl=function(e,t,n){return Lr(t,e)?function(e,t){for(var n=P(t)?t:q(!1),r=e.dom(),o=[];null!==r.parentNode&&r.parentNode!==undefined;){var i=r.parentNode,a=ir.fromDom(i);if(o.push(a),!0===n(a))break;r=i}return o}(e,function(e){return n(e)||Ir(e,t)}).slice(0,-1):[]},Ql=function(e,t){return Jl(e,t,q(!1))},Zl=Ql,ef=function(e,t){return[e].concat(Ql(e,t))},tf=function(e){var t,n=(t=qr(e),G(t,mo).fold(function(){return t},function(e){return t.slice(0,e)}));return U(n,Oi),n},nf=function(e,t){var n=ef(t,e);return Y(n.reverse(),$l).each(Oi)},rf=function(e,t,n,r){if($l(n))return Gc(n),nc.firstPositionIn(n.dom());0===V(Hr(r),function(e){return!$l(e)}).length&&$l(t)&&Ti(r,ir.fromTag("br"));var o=nc.prevPosition(n.dom(),Cu.before(r.dom()));return U(tf(t),function(e){Ti(r,e)}),nf(e,t),o},of=function(e,t,n){if($l(n))return Oi(n),$l(t)&&Gc(t),nc.firstPositionIn(t.dom());var r=nc.lastPositionIn(n.dom());return U(tf(t),function(e){_i(n,e)}),nf(e,t),r},af=function(e,t){return Lr(t,e)?(n=ef(e,t),A.from(n[n.length-1])):A.none();var n},uf=function(e,t){nc.positionIn(e,t.dom()).map(function(e){return e.getNode()}).map(ir.fromDom).filter(po).each(Oi)},sf=function(e,t,n){return uf(!0,t),uf(!1,n),af(t,n).fold(d(of,e,t,n),d(rf,e,t,n))},cf=function(e,t,n,r){return t?sf(e,r,n):sf(e,n,r)},lf=function(t,n){var e,r=ir.fromDom(t.getBody());return(e=Gl(r.dom(),n,t.selection.getRng()).bind(function(e){return cf(r,n,e.from().block(),e.to().block())})).each(function(e){t.selection.setRng(e.toRange())}),e.isSome()},ff=function(e,t){var n=ir.fromDom(t),r=d(Ir,e);return Xi(n,xo,r).isSome()},df=function(e,t){var n,r,o=nc.prevPosition(e.dom(),Cu.fromRangeStart(t)).isNone(),i=nc.nextPosition(e.dom(),Cu.fromRangeEnd(t)).isNone();return!(ff(n=e,(r=t).startContainer)||ff(n,r.endContainer))&&o&&i},mf=function(e){var n,r,o,t,i=ir.fromDom(e.getBody()),a=e.selection.getRng();return df(i,a)?((t=e).setContent(""),t.selection.setCursorLocation(),!0):(n=i,r=e.selection,o=r.getRng(),Ya([Ml(n,ir.fromDom(o.startContainer)),Ml(n,ir.fromDom(o.endContainer))],function(e,t){return!1===Ir(e,t)&&(o.deleteContents(),cf(n,!0,e,t).each(function(e){r.setRng(e.toRange())}),!0)}).getOr(!1))},gf=function(e,t){return!e.selection.isCollapsed()&&mf(e)},pf=function(a){if(!D(a))throw new Error("cases must be an array");if(0===a.length)throw new Error("there must be at least one case");var u=[],n={};return U(a,function(e,r){var t=mr(e);if(1!==t.length)throw new Error("one and only one name per case");var o=t[0],i=e[o];if(n[o]!==undefined)throw new Error("duplicate key detected:"+o);if("cata"===o)throw new Error("cannot have a case named cata (sorry)");if(!D(i))throw new Error("case arguments must be an array");u.push(o),n[o]=function(){var e=arguments.length;if(e!==i.length)throw new Error("Wrong number of arguments to case "+o+". Expected "+i.length+" ("+i+"), got "+e);for(var n=new Array(e),t=0;t<n.length;t++)n[t]=arguments[t];return{fold:function(){if(arguments.length!==a.length)throw new Error("Wrong number of arguments to fold. Expected "+a.length+", got "+arguments.length);return arguments[r].apply(null,n)},match:function(e){var t=mr(e);if(u.length!==t.length)throw new Error("Wrong number of arguments to match. Expected: "+u.join(",")+"\nActual: "+t.join(","));if(!ee(u,function(e){return F(t,e)}))throw new Error("Not all branches were specified when using match. Specified: "+t.join(", ")+"\nRequired: "+u.join(", "));return e[o].apply(null,n)},log:function(e){H.console.log(e,{constructors:u,constructor:o,params:n})}}}}),n},hf=function(e){return Ts(e).exists(po)},vf=function(e,t,n){var r=V(ef(ir.fromDom(n.container()),t),mo),o=ne(r).getOr(t);return nc.fromPosition(e,o.dom(),n).filter(hf)},bf=function(e,t){return Ts(t).exists(po)||vf(!0,e,t).isSome()},yf=function(e,t){return(n=t,A.from(n.getNode(!0)).map(ir.fromDom)).exists(po)||vf(!1,e,t).isSome();var n},Cf=d(vf,!1),xf=d(vf,!0),wf=(tl="\xa0",function(e){return tl===e}),Nf=function(e){return/^[\r\n\t ]$/.test(e)},Ef=function(e){return!Nf(e)&&!wf(e)},Sf=function(n,r,o){return A.from(o.container()).filter(Po.isText).exists(function(e){var t=n?0:-1;return r(e.data.charAt(o.offset()+t))})},kf=d(Sf,!0,Nf),Tf=d(Sf,!1,Nf),Af=function(e){var t=e.container();return Po.isText(t)&&0===t.data.length},Rf=function(e,t){var n=ys(e,t);return Po.isContentEditableFalse(n)&&!Po.isBogusAll(n)},_f=d(Rf,0),Df=d(Rf,-1),Bf=function(e,t){return Po.isTable(ys(e,t))},Of=d(Bf,0),Pf=d(Bf,-1),If=pf([{remove:["element"]},{moveToElement:["element"]},{moveToPosition:["position"]}]),Lf=function(e,t,n,r){var o=r.getNode(!1===t);return Ml(ir.fromDom(e),ir.fromDom(n.getNode())).map(function(e){return $l(e)?If.remove(e.dom()):If.moveToElement(o)}).orThunk(function(){return A.some(If.moveToElement(o))})},Mf=function(u,s,c){return nc.fromPosition(s,u,c).bind(function(e){return a=e.getNode(),xo(ir.fromDom(a))||bo(ir.fromDom(a))?A.none():(t=u,o=e,i=function(e){return go(ir.fromDom(e))&&!bs(r,o,t)},Ss(!(n=s),r=c).fold(function(){return Ss(n,o).fold(q(!1),i)},i)?A.none():s&&Po.isContentEditableFalse(e.getNode())?Lf(u,s,c,e):!1===s&&Po.isContentEditableFalse(e.getNode(!0))?Lf(u,s,c,e):s&&Df(c)?A.some(If.moveToPosition(e)):!1===s&&_f(c)?A.some(If.moveToPosition(e)):A.none());var t,n,r,o,i,a})},Ff=function(r,e,o){return i=e,a=o.getNode(!1===i),u=i?"after":"before",Po.isElement(a)&&a.getAttribute("data-mce-caret")===u?(t=e,n=o.getNode(!1===e),t&&Po.isContentEditableFalse(n.nextSibling)?A.some(If.moveToElement(n.nextSibling)):!1===t&&Po.isContentEditableFalse(n.previousSibling)?A.some(If.moveToElement(n.previousSibling)):A.none()).fold(function(){return Mf(r,e,o)},A.some):Mf(r,e,o).bind(function(e){return t=r,n=o,e.fold(function(e){return A.some(If.remove(e))},function(e){return A.some(If.moveToElement(e))},function(e){return bs(n,e,t)?A.none():A.some(If.moveToPosition(e))});var t,n});var t,n,i,a,u},zf=function(e,t,n){if(0!==n){var r,o,i,a=e.data.slice(t,t+n),u=t+n>=e.data.length,s=0===t;e.replaceData(t,n,(o=s,i=u,X((r=a).split(""),function(e,t){return-1!==" \f\n\r\t\x0B".indexOf(t)||"\xa0"===t?e.previousCharIsSpace||""===e.str&&o||e.str.length===r.length-1&&i?{previousCharIsSpace:!1,str:e.str+"\xa0"}:{previousCharIsSpace:!0,str:e.str+" "}:{previousCharIsSpace:!1,str:e.str+t}},{previousCharIsSpace:!1,str:""}).str))}},Uf=function(e,t){var n,r=e.data.slice(t),o=r.length-(n=r,n.replace(/^\s+/g,"")).length;return zf(e,t,o)},Vf=function(e,t){return r=e,o=(n=t).container(),i=n.offset(),!1===Cu.isTextPosition(n)&&o===r.parentNode&&i>Cu.before(r).offset()?Cu(t.container(),t.offset()-1):t;var n,r,o,i},Hf=function(e){return Ia(e.previousSibling)?A.some((t=e.previousSibling,Po.isText(t)?Cu(t,t.data.length):Cu.after(t))):e.previousSibling?nc.lastPositionIn(e.previousSibling):A.none();var t},jf=function(e){return Ia(e.nextSibling)?A.some((t=e.nextSibling,Po.isText(t)?Cu(t,0):Cu.before(t))):e.nextSibling?nc.firstPositionIn(e.nextSibling):A.none();var t},qf=function(r,o){return Hf(o).orThunk(function(){return jf(o)}).orThunk(function(){return e=r,t=o,n=Cu.before(t.previousSibling?t.previousSibling:t.parentNode),nc.prevPosition(e,n).fold(function(){return nc.nextPosition(e,Cu.after(t))},A.some);var e,t,n})},$f=function(n,r){return jf(r).orThunk(function(){return Hf(r)}).orThunk(function(){return e=n,t=r,nc.nextPosition(e,Cu.after(t)).fold(function(){return nc.prevPosition(e,Cu.before(t))},A.some);var e,t})},Wf=function(e,t,n){return(r=e,o=t,i=n,r?$f(o,i):qf(o,i)).map(d(Vf,n));var r,o,i},Kf=function(t,n,e){e.fold(function(){t.focus()},function(e){t.selection.setRng(e.toRange(),n)})},Xf=function(e,t){return t&&e.schema.getBlockElements().hasOwnProperty(cr(t))},Yf=function(e){if($l(e)){var t=ir.fromHtml('<br data-mce-bogus="1">');return Bi(e),_i(e,t),A.some(Cu.before(t.dom()))}return A.none()},Gf=function(e,t,l){var n=Ur(e).filter(function(e){return Po.isText(e.dom())}),r=Vr(e).filter(function(e){return Po.isText(e.dom())});return Oi(e),Ya([n,r,t],function(e,t,n){var r,o,i,a,u=e.dom(),s=t.dom(),c=u.data.length;return o=s,i=l,a=Gn((r=u).data).length,r.appendData(o.data),Oi(ir.fromDom(o)),i&&Uf(r,a),n.container()===s?Cu(u,c):n}).orThunk(function(){return l&&(n.each(function(e){return t=e.dom(),n=e.dom().length,r=t.data.slice(0,n),o=r.length-Gn(r).length,zf(t,n-o,o);var t,n,r,o}),r.each(function(e){return Uf(e.dom(),0)})),t})},Jf=function(e,t){return n=e.schema.getTextInlineElements(),r=cr(t),gr.call(n,r);var n,r},Qf=function(t,n,e,r){void 0===r&&(r=!0);var o,i=Wf(n,t.getBody(),e.dom()),a=Xi(e,d(Xf,t),(o=t.getBody(),function(e){return e.dom()===o})),u=Gf(e,i,Jf(t,e));t.dom.isEmpty(t.getBody())?(t.setContent(""),t.selection.setCursorLocation()):a.bind(Yf).fold(function(){r&&Kf(t,n,u)},function(e){r&&Kf(t,n,A.some(e))})},Zf=function(a,u){var e,t,n,r,o,i;return(e=a.getBody(),t=u,n=a.selection.getRng(),r=Es(t?1:-1,e,n),o=Cu.fromRangeStart(r),i=ir.fromDom(e),!1===t&&Df(o)?A.some(If.remove(o.getNode(!0))):t&&_f(o)?A.some(If.remove(o.getNode())):!1===t&&_f(o)&&yf(i,o)?Cf(i,o).map(function(e){return If.remove(e.getNode())}):t&&Df(o)&&bf(i,o)?xf(i,o).map(function(e){return If.remove(e.getNode())}):Ff(e,t,o)).map(function(e){return e.fold((o=a,i=u,function(e){return o._selectionOverrides.hideFakeCaret(),Qf(o,i,ir.fromDom(e)),!0}),(n=a,r=u,function(e){var t=r?Cu.before(e):Cu.after(e);return n.selection.setRng(t.toRange()),!0}),(t=a,function(e){return t.selection.setRng(e.toRange()),!0}));var t,n,r,o,i}).getOr(!1)},ed=function(e,t){var n,r=e.selection.getNode();return!!Po.isContentEditableFalse(r)&&(n=ir.fromDom(e.getBody()),U($i(n,".mce-offscreen-selection"),Oi),Qf(e,t,ir.fromDom(e.selection.getNode())),Fl(e),!0)},td=function(e,t){return e.selection.isCollapsed()?Zf(e,t):ed(e,t)},nd=function(e){var t,n=function(e,t){for(;t&&t!==e;){if(Po.isContentEditableTrue(t)||Po.isContentEditableFalse(t))return t;t=t.parentNode}return null}(e.getBody(),e.selection.getNode());return Po.isContentEditableTrue(n)&&e.dom.isBlock(n)&&e.dom.isEmpty(n)&&(t=e.dom.create("br",{"data-mce-bogus":"1"}),e.dom.setHTML(n,""),n.appendChild(t),e.selection.setRng(Cu.before(t).toRange())),!0},rd=Po.isText,od=function(e){return rd(e)&&e.data[0]===ma},id=function(e){return rd(e)&&e.data[e.data.length-1]===ma},ad=function(e){return e.ownerDocument.createTextNode(ma)},ud=function(e,t){return e?function(e){if(rd(e.previousSibling))return id(e.previousSibling)||e.previousSibling.appendData(ma),e.previousSibling;if(rd(e))return od(e)||e.insertData(0,ma),e;var t=ad(e);return e.parentNode.insertBefore(t,e),t}(t):function(e){if(rd(e.nextSibling))return od(e.nextSibling)||e.nextSibling.insertData(0,ma),e.nextSibling;if(rd(e))return id(e)||e.appendData(ma),e;var t=ad(e);return e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t),t}(t)},sd=d(ud,!0),cd=d(ud,!1),ld=function(e,t){return Po.isText(e.container())?ud(t,e.container()):ud(t,e.getNode())},fd=function(e,t){var n=t.get();return n&&e.container()===n&&ba(n)},dd=function(n,e){return e.fold(function(e){es.remove(n.get());var t=sd(e);return n.set(t),A.some(Cu(t,t.length-1))},function(e){return nc.firstPositionIn(e).map(function(e){if(fd(e,n))return Cu(n.get(),1);es.remove(n.get());var t=ld(e,!0);return n.set(t),Cu(t,1)})},function(e){return nc.lastPositionIn(e).map(function(e){if(fd(e,n))return Cu(n.get(),n.get().length-1);es.remove(n.get());var t=ld(e,!1);return n.set(t),Cu(t,t.length-1)})},function(e){es.remove(n.get());var t=cd(e);return n.set(t),A.some(Cu(t,1))})},md=function(e,t){for(var n=0;n<e.length;n++){var r=e[n].apply(null,t);if(r.isSome())return r}return A.none()},gd=pf([{before:["element"]},{start:["element"]},{end:["element"]},{after:["element"]}]),pd=function(e,t){var n=vs(t,e);return n||e},hd=function(e,t,n){var r=Ll.normalizeForwards(n),o=pd(t,r.container());return Ll.findRootInline(e,o,r).fold(function(){return nc.nextPosition(o,r).bind(d(Ll.findRootInline,e,o)).map(function(e){return gd.before(e)})},A.none)},vd=function(e,t){return null===qu(e,t)},bd=function(e,t,n){return Ll.findRootInline(e,t,n).filter(d(vd,t))},yd=function(e,t,n){var r=Ll.normalizeBackwards(n);return bd(e,t,r).bind(function(e){return nc.prevPosition(e,r).isNone()?A.some(gd.start(e)):A.none()})},Cd=function(e,t,n){var r=Ll.normalizeForwards(n);return bd(e,t,r).bind(function(e){return nc.nextPosition(e,r).isNone()?A.some(gd.end(e)):A.none()})},xd=function(e,t,n){var r=Ll.normalizeBackwards(n),o=pd(t,r.container());return Ll.findRootInline(e,o,r).fold(function(){return nc.prevPosition(o,r).bind(d(Ll.findRootInline,e,o)).map(function(e){return gd.after(e)})},A.none)},wd=function(e){return!1===Ll.isRtl(Ed(e))},Nd=function(e,t,n){return md([hd,yd,Cd,xd],[e,t,n]).filter(wd)},Ed=function(e){return e.fold($,$,$,$)},Sd=function(e){return e.fold(q("before"),q("start"),q("end"),q("after"))},kd=function(e){return e.fold(gd.before,gd.before,gd.after,gd.after)},Td=function(n,e,r,t,o,i){return Ya([Ll.findRootInline(e,r,t),Ll.findRootInline(e,r,o)],function(e,t){return e!==t&&Ll.hasSameParentBlock(r,e,t)?gd.after(n?e:t):i}).getOr(i)},Ad=function(e,r){return e.fold(q(!0),function(e){return n=r,!(Sd(t=e)===Sd(n)&&Ed(t)===Ed(n));var t,n})},Rd=function(e,t){return e?t.fold(j(A.some,gd.start),A.none,j(A.some,gd.after),A.none):t.fold(A.none,j(A.some,gd.before),A.none,j(A.some,gd.end))},_d=function(a,u,s,c){var e=Ll.normalizePosition(a,c),l=Nd(u,s,e);return Nd(u,s,e).bind(d(Rd,a)).orThunk(function(){return t=a,n=u,r=s,o=l,e=c,i=Ll.normalizePosition(t,e),nc.fromPosition(t,r,i).map(d(Ll.normalizePosition,t)).fold(function(){return o.map(kd)},function(e){return Nd(n,r,e).map(d(Td,t,n,r,i,e)).filter(d(Ad,o))}).filter(wd);var t,n,r,o,e,i})},Dd=Nd,Bd=_d,Od=(d(_d,!1),d(_d,!0),kd),Pd=function(e){return e.fold(gd.start,gd.start,gd.end,gd.end)},Id=function(e){return P(e.selection.getSel().modify)},Ld=function(e,t,n){var r=e?1:-1;return t.setRng(Cu(n.container(),n.offset()+r).toRange()),t.getSel().modify("move",e?"forward":"backward","word"),!0},Md=function(e,t){var n=t.selection.getRng(),r=e?Cu.fromRangeEnd(n):Cu.fromRangeStart(n);return!!Id(t)&&(e&&xa(r)?Ld(!0,t.selection,r):!(e||!wa(r))&&Ld(!1,t.selection,r))},Fd=function(e,t){var n=e.dom.createRng();n.setStart(t.container(),t.offset()),n.setEnd(t.container(),t.offset()),e.selection.setRng(n)},zd=function(e){return!1!==e.settings.inline_boundaries},Ud=function(e,t){e?t.setAttribute("data-mce-selected","inline-boundary"):t.removeAttribute("data-mce-selected")},Vd=function(t,e,n){return dd(e,n).map(function(e){return Fd(t,e),n})},Hd=function(e,t,n){return function(){return!!zd(t)&&Md(e,t)}},jd={move:function(a,u,s){return function(){return!!zd(a)&&(t=a,n=u,e=s,r=t.getBody(),o=Cu.fromRangeStart(t.selection.getRng()),i=d(Ll.isInlineTarget,t),Bd(e,i,r,o).bind(function(e){return Vd(t,n,e)})).isSome();var t,n,e,r,o,i}},moveNextWord:d(Hd,!0),movePrevWord:d(Hd,!1),setupSelectedState:function(a){var u=Li(null),s=d(Ll.isInlineTarget,a);return a.on("NodeChange",function(e){var t,n,r,o,i;zd(a)&&(t=s,n=a.dom,r=e.parents,o=V(n.select('*[data-mce-selected="inline-boundary"]'),t),i=V(r,t),U(te(o,i),d(Ud,!1)),U(te(i,o),d(Ud,!0)),function(e,t){if(e.selection.isCollapsed()&&!0!==e.composing&&t.get()){var n=Cu.fromRangeStart(e.selection.getRng());Cu.isTextPosition(n)&&!1===Ll.isAtZwsp(n)&&(Fd(e,es.removeAndReposition(t.get(),n)),t.set(null))}}(a,u),function(n,r,o,e){if(r.selection.isCollapsed()){var t=V(e,n);U(t,function(e){var t=Cu.fromRangeStart(r.selection.getRng());Dd(n,r.getBody(),t).bind(function(e){return Vd(r,o,e)})})}}(s,a,u,e.parents))}),u},setCaretPosition:Fd},qd=function(t,n){return function(e){return dd(n,e).map(function(e){return jd.setCaretPosition(t,e),!0}).getOr(!1)}},$d=function(r,o,i,a){var u=r.getBody(),s=d(Ll.isInlineTarget,r);r.undoManager.ignore(function(){var e,t,n;r.selection.setRng((e=i,t=a,(n=H.document.createRange()).setStart(e.container(),e.offset()),n.setEnd(t.container(),t.offset()),n)),r.execCommand("Delete"),Dd(s,u,Cu.fromRangeStart(r.selection.getRng())).map(Pd).map(qd(r,o))}),r.nodeChanged()},Wd=function(n,r,i,o){var e,t,a=(e=n.getBody(),t=o.container(),vs(t,e)||e),u=d(Ll.isInlineTarget,n),s=Dd(u,a,o);return s.bind(function(e){return i?e.fold(q(A.some(Pd(e))),A.none,q(A.some(Od(e))),A.none):e.fold(A.none,q(A.some(Od(e))),A.none,q(A.some(Pd(e))))}).map(qd(n,r)).getOrThunk(function(){var t=nc.navigate(i,a,o),e=t.bind(function(e){return Dd(u,a,e)});return s.isSome()&&e.isSome()?Ll.findRootInline(u,a,o).map(function(e){return o=e,!!Ya([nc.firstPositionIn(o),nc.lastPositionIn(o)],function(e,t){var n=Ll.normalizePosition(!0,e),r=Ll.normalizePosition(!1,t);return nc.nextPosition(o,n).map(function(e){return e.isEqual(r)}).getOr(!0)}).getOr(!0)&&(Qf(n,i,ir.fromDom(e)),!0);var o}).getOr(!1):e.bind(function(e){return t.map(function(e){return i?$d(n,r,o,e):$d(n,r,e,o),!0})}).getOr(!1)})},Kd=function(e,t,n){if(e.selection.isCollapsed()&&!1!==e.settings.inline_boundaries){var r=Cu.fromRangeStart(e.selection.getRng());return Wd(e,t,n,r)}return!1},Xd=kr("start","end"),Yd=kr("rng","table","cells"),Gd=pf([{removeTable:["element"]},{emptyCells:["cells"]}]),Jd=function(e,t){return Qi(ir.fromDom(e),"td,th",t)},Qd=function(e,t){return Gi(e,"table",t)},Zd=function(e){return!1===Ir(e.start(),e.end())},em=function(e,n){return Qd(e.start(),n).bind(function(t){return Qd(e.end(),n).bind(function(e){return Ir(t,e)?A.some(t):A.none()})})},tm=function(e){return $i(e,"td,th")},nm=function(r,e){var t=Jd(e.startContainer,r),n=Jd(e.endContainer,r);return e.collapsed?A.none():Ya([t,n],Xd).fold(function(){return t.fold(function(){return n.bind(function(t){return Qd(t,r).bind(function(e){return ne(tm(e)).map(function(e){return Xd(e,t)})})})},function(t){return Qd(t,r).bind(function(e){return re(tm(e)).map(function(e){return Xd(t,e)})})})},function(e){return rm(r,e)?A.none():(n=r,Qd((t=e).start(),n).bind(function(e){return re(tm(e)).map(function(e){return Xd(t.start(),e)})}));var t,n})},rm=function(e,t){return em(t,e).isSome()},om=function(e,t){var n,r,o,i,a=d(Ir,e);return(n=t,r=a,o=Jd(n.startContainer,r),i=Jd(n.endContainer,r),Ya([o,i],Xd).filter(Zd).filter(function(e){return rm(r,e)}).orThunk(function(){return nm(r,n)})).bind(function(e){return em(t=e,a).map(function(e){return Yd(t,e,tm(e))});var t})},im=function(e,t){return G(e,function(e){return Ir(e,t)})},am=function(n){return(r=n,Ya([im(r.cells(),r.rng().start()),im(r.cells(),r.rng().end())],function(e,t){return r.cells().slice(e,t+1)})).map(function(e){var t=n.cells();return e.length===t.length?Gd.removeTable(n.table()):Gd.emptyCells(e)});var r},um=function(e,t){return om(e,t).bind(am)},sm=function(e){var t=[];if(e)for(var n=0;n<e.rangeCount;n++)t.push(e.getRangeAt(n));return t},cm=sm,lm=function(e){return Z(e,function(e){var t=$a(e);return t?[ir.fromDom(t)]:[]})},fm=function(e){return 1<sm(e).length},dm=function(e){return V(lm(e),xo)},mm=function(e){return $i(e,"td[data-mce-selected],th[data-mce-selected]")},gm=function(e,t){var n=mm(t),r=dm(e);return 0<n.length?n:r},pm=gm,hm=function(e){return gm(cm(e.selection.getSel()),ir.fromDom(e.getBody()))},vm=function(e,t){return U(t,Gc),e.selection.setCursorLocation(t[0].dom(),0),!0},bm=function(e,t){return Qf(e,!1,t),!0},ym=function(n,e,r,t){return xm(e,t).fold(function(){return t=n,um(e,r).map(function(e){return e.fold(d(bm,t),d(vm,t))});var t},function(e){return wm(n,e)}).getOr(!1)},Cm=function(e,t){return Y(ef(t,e),xo)},xm=function(e,t){return Y(ef(t,e),function(e){return"caption"===cr(e)})},wm=function(e,t){return Gc(t),e.selection.setCursorLocation(t.dom(),0),A.some(!0)},Nm=function(u,s,c,l,f){return nc.navigate(c,u.getBody(),f).bind(function(e){return r=l,o=c,i=f,a=e,nc.firstPositionIn(r.dom()).bind(function(t){return nc.lastPositionIn(r.dom()).map(function(e){return o?i.isEqual(t)&&a.isEqual(e):i.isEqual(e)&&a.isEqual(t)})}).getOr(!0)?wm(u,l):(t=l,n=e,xm(s,ir.fromDom(n.getNode())).map(function(e){return!1===Ir(e,t)}));var t,n,r,o,i,a}).or(A.some(!0))},Em=function(a,u,s,e){var c=Cu.fromRangeStart(a.selection.getRng());return Cm(s,e).bind(function(e){return $l(e)?wm(a,e):(t=a,n=s,r=u,o=e,i=c,nc.navigate(r,t.getBody(),i).bind(function(e){return Cm(n,ir.fromDom(e.getNode())).map(function(e){return!1===Ir(e,o)})}));var t,n,r,o,i})},Sm=function(a,u,e){var s=ir.fromDom(a.getBody());return xm(s,e).fold(function(){return Em(a,u,s,e)},function(e){return t=a,n=u,r=s,o=e,i=Cu.fromRangeStart(t.selection.getRng()),$l(o)?wm(t,o):Nm(t,r,n,o,i);var t,n,r,o,i}).getOr(!1)},km=function(e,t){var n,r,o,i,a,u=ir.fromDom(e.selection.getStart(!0)),s=hm(e);return e.selection.isCollapsed()&&0===s.length?Sm(e,t,u):(n=e,r=u,o=ir.fromDom(n.getBody()),i=n.selection.getRng(),0!==(a=hm(n)).length?vm(n,a):ym(n,o,i,r))},Tm=hc.isEq,Am=function(e,t,n){var r=e.formatter.get(n);if(r)for(var o=0;o<r.length;o++)if(!1===r[o].inherit&&e.dom.is(t,r[o].selector))return!0;return!1},Rm=function(t,e,n,r){var o=t.dom.getRoot();return e!==o&&(e=t.dom.getParent(e,function(e){return!!Am(t,e,n)||e.parentNode===o||!!Bm(t,e,n,r,!0)}),Bm(t,e,n,r))},_m=function(e,t,n){return!!Tm(t,n.inline)||!!Tm(t,n.block)||(n.selector?1===t.nodeType&&e.is(t,n.selector):void 0)},Dm=function(e,t,n,r,o,i){var a,u,s,c=n[r];if(n.onmatch)return n.onmatch(t,n,r);if(c)if("undefined"==typeof c.length){for(a in c)if(c.hasOwnProperty(a)){if(u="attributes"===r?e.getAttrib(t,a):hc.getStyle(e,t,a),o&&!u&&!n.exact)return;if((!o||n.exact)&&!Tm(u,hc.normalizeStyleValue(e,hc.replaceVars(c[a],i),a)))return}}else for(s=0;s<c.length;s++)if("attributes"===r?e.getAttrib(t,c[s]):hc.getStyle(e,t,c[s]))return n;return n},Bm=function(e,t,n,r,o){var i,a,u,s,c=e.formatter.get(n),l=e.dom;if(c&&t)for(a=0;a<c.length;a++)if(i=c[a],_m(e.dom,t,i)&&Dm(l,t,i,"attributes",o,r)&&Dm(l,t,i,"styles",o,r)){if(s=i.classes)for(u=0;u<s.length;u++)if(!e.dom.hasClass(t,s[u]))return;return i}},Om={matchNode:Bm,matchName:_m,match:function(e,t,n,r){var o;return r?Rm(e,r,t,n):(r=e.selection.getNode(),!!Rm(e,r,t,n)||!((o=e.selection.getStart())===r||!Rm(e,o,t,n)))},matchAll:function(r,o,i){var e,a=[],u={};return e=r.selection.getStart(),r.dom.getParent(e,function(e){var t,n;for(t=0;t<o.length;t++)n=o[t],!u[n]&&Bm(r,e,n,i)&&(u[n]=!0,a.push(n))},r.dom.getRoot()),a},canApply:function(e,t){var n,r,o,i,a,u=e.formatter.get(t),s=e.dom;if(u)for(n=e.selection.getStart(),r=hc.getParents(s,n),i=u.length-1;0<=i;i--){if(!(a=u[i].selector)||u[i].defaultBlock)return!0;for(o=r.length-1;0<=o;o--)if(s.is(r[o],a))return!0}return!1},matchesUnInheritedFormatSelector:Am},Pm=function(e,t){return e.splitText(t)},Im=function(e){var t=e.startContainer,n=e.startOffset,r=e.endContainer,o=e.endOffset;return t===r&&Po.isText(t)?0<n&&n<t.nodeValue.length&&(t=(r=Pm(t,n)).previousSibling,n<o?(t=r=Pm(r,o-=n).previousSibling,o=r.nodeValue.length,n=0):o=0):(Po.isText(t)&&0<n&&n<t.nodeValue.length&&(t=Pm(t,n),n=0),Po.isText(r)&&0<o&&o<r.nodeValue.length&&(o=(r=Pm(r,o).previousSibling).nodeValue.length)),{startContainer:t,startOffset:n,endContainer:r,endOffset:o}},Lm=ma,Mm="_mce_caret",Fm=function(e){return 0<function(e){for(var t=[];e;){if(3===e.nodeType&&e.nodeValue!==Lm||1<e.childNodes.length)return[];1===e.nodeType&&t.push(e),e=e.firstChild}return t}(e).length},zm=function(e){var t;if(e)for(e=(t=new ao(e,e)).current();e;e=t.next())if(3===e.nodeType)return e;return null},Um=function(e){var t=ir.fromTag("span");return Cr(t,{id:Mm,"data-mce-bogus":"1","data-mce-type":"format-caret"}),e&&_i(t,ir.fromText(Lm)),t},Vm=function(e,t,n){void 0===n&&(n=!0);var r,o=e.dom,i=e.selection;if(Fm(t))Qf(e,!1,ir.fromDom(t),n);else{var a=i.getRng(),u=o.getParent(t,o.isBlock),s=((r=zm(t))&&r.nodeValue.charAt(0)===Lm&&r.deleteData(0,1),r);a.startContainer===s&&0<a.startOffset&&a.setStart(s,a.startOffset-1),a.endContainer===s&&0<a.endOffset&&a.setEnd(s,a.endOffset-1),o.remove(t,!0),u&&o.isEmpty(u)&&Gc(ir.fromDom(u)),i.setRng(a)}},Hm=function(e,t,n){void 0===n&&(n=!0);var r=e.dom,o=e.selection;if(t)Vm(e,t,n);else if(!(t=qu(e.getBody(),o.getStart())))for(;t=r.get(Mm);)Vm(e,t,!1)},jm=function(e,t,n){var r=e.dom,o=r.getParent(n,d(hc.isTextBlock,e));o&&r.isEmpty(o)?n.parentNode.replaceChild(t,n):(Yc(ir.fromDom(n)),r.isEmpty(n)?n.parentNode.replaceChild(t,n):r.insertAfter(t,n))},qm=function(e,t){return e.appendChild(t),t},$m=function(e,t){var n,r,o=(n=function(e,t){return qm(e,t.cloneNode(!1))},r=t,function(e,t){for(var n=e.length-1;0<=n;n--)t(e[n],n,e)}(e,function(e){r=n(r,e)}),r);return qm(o,o.ownerDocument.createTextNode(Lm))},Wm=function(i){i.on("mouseup keydown",function(e){var t,n,r,o;t=i,n=e.keyCode,r=t.selection,o=t.getBody(),Hm(t,null,!1),8!==n&&46!==n||!r.isCollapsed()||r.getStart().innerHTML!==Lm||Hm(t,qu(o,r.getStart())),37!==n&&39!==n||Hm(t,qu(o,r.getStart()))})},Km=function(e,t){return e.schema.getTextInlineElements().hasOwnProperty(cr(t))&&!ju(t.dom())&&!Po.isBogus(t.dom())},Xm=function(e){return 1===qr(e).length},Ym=function(e,t,n,r){var o,i,a,u,s=d(Km,t),c=W(V(r,s),function(e){return e.dom()});if(0===c.length)Qf(t,e,n);else{var l=(o=n.dom(),i=c,a=Um(!1),u=$m(i,a.dom()),Ti(ir.fromDom(o),a),Oi(ir.fromDom(o)),Cu(u,0));t.selection.setRng(l.toRange())}},Gm=function(r,o){var t,e=ir.fromDom(r.getBody()),n=ir.fromDom(r.selection.getStart()),i=V((t=ef(n,e),G(t,mo).fold(q(t),function(e){return t.slice(0,e)})),Xm);return re(i).map(function(e){var t,n=Cu.fromRangeStart(r.selection.getRng());return!(!zl(o,n,e.dom())||ju((t=e).dom())&&Fm(t.dom())||(Ym(o,r,e,i),0))}).getOr(!1)},Jm=function(e,t){return!!e.selection.isCollapsed()&&Gm(e,t)},Qm=Po.isContentEditableTrue,Zm=Po.isContentEditableFalse,eg=function(e,t,n,r,o){return t._selectionOverrides.showCaret(e,n,r,o)},tg=function(e,t){var n,r;return e.fire("BeforeObjectSelected",{target:t}).isDefaultPrevented()?null:((r=(n=t).ownerDocument.createRange()).selectNode(n),r)},ng=function(e,t,n){var r=Es(1,e.getBody(),t),o=Cu.fromRangeStart(r),i=o.getNode();if(Zm(i))return eg(1,e,i,!o.isAtEnd(),!1);var a=o.getNode(!0);if(Zm(a))return eg(1,e,a,!1,!1);var u=e.dom.getParent(o.getNode(),function(e){return Zm(e)||Qm(e)});return Zm(u)?eg(1,e,u,!1,n):null},rg=function(e,t,n){if(!t||!t.collapsed)return t;var r=ng(e,t,n);return r||t},og=function(e,t,n,r,o,i){var a,u,s=eg(r,e,i.getNode(!o),o,!0);if(t.collapsed){var c=t.cloneRange();o?c.setEnd(s.startContainer,s.startOffset):c.setStart(s.endContainer,s.endOffset),c.deleteContents()}else t.deleteContents();return e.selection.setRng(s),a=e.dom,u=n,Po.isText(u)&&0===u.data.length&&a.remove(u),!0},ig=function(e,t){return function(e,t){var n=e.selection.getRng();if(!Po.isText(n.commonAncestorContainer))return!1;var r=t?bu.Forwards:bu.Backwards,o=$s(e.getBody()),i=d(As,o.next),a=d(As,o.prev),u=t?i:a,s=t?_f:Df,c=ks(r,e.getBody(),n),l=Ll.normalizePosition(t,u(c));if(!l)return!1;if(s(l))return og(e,n,c.getNode(),r,t,l);var f=u(l);return!!(f&&s(f)&&Rs(l,f))&&og(e,n,c.getNode(),r,t,f)}(e,t)},ag=function(e,t){e.getDoc().execCommand(t,!1,null)},ug=function(e){td(e,!1)||ig(e,!1)||Kd(e,!1)||lf(e,!1)||km(e)||gf(e,!1)||Jm(e,!1)||(ag(e,"Delete"),Fl(e))},sg=function(e){td(e,!0)||ig(e,!0)||Kd(e,!0)||lf(e,!0)||km(e)||gf(e,!0)||Jm(e,!0)||ag(e,"ForwardDelete")},cg=function(o,t,e){var n=function(e){return t=o,n=e.dom(),r=Sr(n,t),A.from(r).filter(function(e){return 0<e.length});var t,n,r};return Yi(ir.fromDom(e),function(e){return n(e).isSome()},function(e){return Ir(ir.fromDom(t),e)}).bind(n)},lg=function(o){return function(r,e){return A.from(e).map(ir.fromDom).filter(fr).bind(function(e){return cg(o,r,e.dom()).or((t=o,n=e.dom(),A.from(bi.DOM.getStyle(n,t,!0))));var t,n}).getOr("")}},fg={getFontSize:lg("font-size"),getFontFamily:j(function(e){return e.replace(/[\'\"\\]/g,"").replace(/,\s+/g,",")},lg("font-family")),toPt:function(e,t){return/[0-9.]+px$/.test(e)?(n=72*parseInt(e,10)/96,r=t||0,o=Math.pow(10,r),Math.round(n*o)/o+"pt"):e;var n,r,o}},dg=function(e){return nc.firstPositionIn(e.getBody()).map(function(e){var t=e.container();return Po.isText(t)?t.parentNode:t})},mg=function(o){return A.from(o.selection.getRng()).bind(function(e){var t,n,r=o.getBody();return n=r,(t=e).startContainer===n&&0===t.startOffset?A.none():A.from(o.selection.getStart(!0))})},gg=function(e,t){if(/^[0-9\.]+$/.test(t)){var n=parseInt(t,10);if(1<=n&&n<=7){var r=Nl(e),o=El(e);return o?o[n-1]||t:r[n-1]||t}return t}return t},pg=function(e,t){return e&&t&&e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset},hg=function(e,t,n){return null!==function(e,t,n){for(;e&&e!==t;){if(n(e))return e;e=e.parentNode}return null}(e,t,n)},vg=function(e,t,n){return hg(e,t,function(e){return e.nodeName===n})},bg=function(e){return e&&"TABLE"===e.nodeName},yg=function(e,t,n){for(var r=new ao(t,e.getParent(t.parentNode,e.isBlock)||e.getRoot());t=r[n?"prev":"next"]();)if(Po.isBr(t))return!0},Cg=function(e,t,n,r,o){var i,a,u,s,c,l,f=e.getRoot(),d=e.schema.getNonEmptyElements();if(u=e.getParent(o.parentNode,e.isBlock)||f,r&&Po.isBr(o)&&t&&e.isEmpty(u))return A.some(vu(o.parentNode,e.nodeIndex(o)));for(i=new ao(o,u);s=i[r?"prev":"next"]();){if("false"===e.getContentEditableParent(s)||(l=f,ya(c=s)&&!1===hg(c,l,ju)))return A.none();if(Po.isText(s)&&0<s.nodeValue.length)return!1===vg(s,f,"A")?A.some(vu(s,r?s.nodeValue.length:0)):A.none();if(e.isBlock(s)||d[s.nodeName.toLowerCase()])return A.none();a=s}return n&&a?A.some(vu(a,0)):A.none()},xg=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m,g=e.getRoot(),p=!1;if(o=r[(n?"start":"end")+"Container"],i=r[(n?"start":"end")+"Offset"],l=Po.isElement(o)&&i===o.childNodes.length,s=e.schema.getNonEmptyElements(),c=n,ya(o))return A.none();if(Po.isElement(o)&&i>o.childNodes.length-1&&(c=!1),Po.isDocument(o)&&(o=g,i=0),o===g){if(c&&(u=o.childNodes[0<i?i-1:0])){if(ya(u))return A.none();if(s[u.nodeName]||bg(u))return A.none()}if(o.hasChildNodes()){if(i=Math.min(!c&&0<i?i-1:i,o.childNodes.length-1),o=o.childNodes[i],i=Po.isText(o)&&l?o.data.length:0,!t&&o===g.lastChild&&bg(o))return A.none();if(function(e,t){for(;t&&t!==e;){if(Po.isContentEditableFalse(t))return!0;t=t.parentNode}return!1}(g,o)||ya(o))return A.none();if(o.hasChildNodes()&&!1===bg(o)){a=new ao(u=o,g);do{if(Po.isContentEditableFalse(u)||ya(u)){p=!1;break}if(Po.isText(u)&&0<u.nodeValue.length){i=c?0:u.nodeValue.length,o=u,p=!0;break}if(s[u.nodeName.toLowerCase()]&&(!(f=u)||!/^(TD|TH|CAPTION)$/.test(f.nodeName))){i=e.nodeIndex(u),o=u.parentNode,c||i++,p=!0;break}}while(u=c?a.next():a.prev())}}}return t&&(Po.isText(o)&&0===i&&Cg(e,l,t,!0,o).each(function(e){o=e.container(),i=e.offset(),p=!0}),Po.isElement(o)&&((u=o.childNodes[i])||(u=o.childNodes[i-1]),!u||!Po.isBr(u)||(m="A",(d=u).previousSibling&&d.previousSibling.nodeName===m)||yg(e,u,!1)||yg(e,u,!0)||Cg(e,l,t,!0,u).each(function(e){o=e.container(),i=e.offset(),p=!0}))),c&&!t&&Po.isText(o)&&i===o.nodeValue.length&&Cg(e,l,t,!1,o).each(function(e){o=e.container(),i=e.offset(),p=!0}),p?A.some(vu(o,i)):A.none()},wg=function(e,t){var n=t.collapsed,r=t.cloneRange(),o=vu.fromRangeStart(t);return xg(e,n,!0,r).each(function(e){n&&vu.isAbove(o,e)||r.setStart(e.container(),e.offset())}),n||xg(e,n,!1,r).each(function(e){r.setEnd(e.container(),e.offset())}),n&&r.collapse(!0),pg(t,r)?A.none():A.some(r)},Ng=function(e,t,n){var r=e.create("span",{}," ");n.parentNode.insertBefore(r,n),t.scrollIntoView(r),e.remove(r)},Eg=function(e,t,n,r){var o=e.createRng();r?(o.setStartBefore(n),o.setEndBefore(n)):(o.setStartAfter(n),o.setEndAfter(n)),t.setRng(o)},Sg=function(e,t){var n,r,o=e.selection,i=e.dom,a=o.getRng();wg(i,a).each(function(e){a.setStart(e.startContainer,e.startOffset),a.setEnd(e.endContainer,e.endOffset)});var u=a.startOffset,s=a.startContainer;if(1===s.nodeType&&s.hasChildNodes()){var c=u>s.childNodes.length-1;s=s.childNodes[Math.min(u,s.childNodes.length-1)]||s,u=c&&3===s.nodeType?s.nodeValue.length:0}var l=i.getParent(s,i.isBlock),f=l?i.getParent(l.parentNode,i.isBlock):null,d=f?f.nodeName.toUpperCase():"",m=t&&t.ctrlKey;"LI"!==d||m||(l=f),s&&3===s.nodeType&&u>=s.nodeValue.length&&(function(e,t,n){for(var r,o=new ao(t,n),i=e.getNonEmptyElements();r=o.next();)if(i[r.nodeName.toLowerCase()]||0<r.length)return!0}(e.schema,s,l)||(n=i.create("br"),a.insertNode(n),a.setStartAfter(n),a.setEndAfter(n),r=!0)),n=i.create("br"),Du(i,a,n),Ng(i,o,n),Eg(i,o,n,r),e.undoManager.add()},kg=function(e,t){var n=ir.fromTag("br");Ti(ir.fromDom(t),n),e.undoManager.add()},Tg=function(e,t){Ag(e.getBody(),t)||Ai(ir.fromDom(t),ir.fromTag("br"));var n=ir.fromTag("br");Ai(ir.fromDom(t),n),Ng(e.dom,e.selection,n.dom()),Eg(e.dom,e.selection,n.dom(),!1),e.undoManager.add()},Ag=function(e,t){return n=Cu.after(t),!!Po.isBr(n.getNode())||nc.nextPosition(e,Cu.after(t)).map(function(e){return Po.isBr(e.getNode())}).getOr(!1);var n},Rg=function(e){return e&&"A"===e.nodeName&&"href"in e},_g=function(e){return e.fold(q(!1),Rg,Rg,q(!1))},Dg=function(e,t){t.fold(o,d(kg,e),d(Tg,e),o)},Bg=function(e,t){var n,r,o,i=(n=e,r=d(Ll.isInlineTarget,n),o=Cu.fromRangeStart(n.selection.getRng()),Dd(r,n.getBody(),o).filter(_g));i.isSome()?i.each(d(Dg,e)):Sg(e,t)},Og={create:kr("start","soffset","finish","foffset")},Pg=pf([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),Ig=(Pg.before,Pg.on,Pg.after,function(e){return e.fold($,$,$)}),Lg=pf([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),Mg={domRange:Lg.domRange,relative:Lg.relative,exact:Lg.exact,exactFromRange:function(e){return Lg.exact(e.start(),e.soffset(),e.finish(),e.foffset())},getWin:function(e){var t=e.match({domRange:function(e){return ir.fromDom(e.startContainer)},relative:function(e,t){return Ig(e)},exact:function(e,t,n,r){return e}});return Fr(t)},range:Og.create},Fg=rr.detect().browser,zg=function(e,t){var n=dr(t)?Bc(t).length:qr(t).length+1;return n<e?n:e<0?0:e},Ug=function(e){return Mg.range(e.start(),zg(e.soffset(),e.start()),e.finish(),zg(e.foffset(),e.finish()))},Vg=function(e,t){return!Po.isRestrictedNode(t.dom())&&(Lr(e,t)||Ir(e,t))},Hg=function(t){return function(e){return Vg(t,e.start())&&Vg(t,e.finish())}},jg=function(e){return!0===e.inline||Fg.isIE()},qg=function(e){return Mg.range(ir.fromDom(e.startContainer),e.startOffset,ir.fromDom(e.endContainer),e.endOffset)},$g=function(e){var t=e.getSelection();return(t&&0!==t.rangeCount?A.from(t.getRangeAt(0)):A.none()).map(qg)},Wg=function(e){var t=Fr(e);return $g(t.dom()).filter(Hg(e))},Kg=function(e,t){return A.from(t).filter(Hg(e)).map(Ug)},Xg=function(e){var t=H.document.createRange();try{return t.setStart(e.start().dom(),e.soffset()),t.setEnd(e.finish().dom(),e.foffset()),A.some(t)}catch(n){return A.none()}},Yg=function(e){return(e.bookmark?e.bookmark:A.none()).bind(d(Kg,ir.fromDom(e.getBody()))).bind(Xg)},Gg=function(e){var t=jg(e)?Wg(ir.fromDom(e.getBody())):A.none();e.bookmark=t.isSome()?t:e.bookmark},Jg=function(t){Yg(t).each(function(e){t.selection.setRng(e)})},Qg=Yg,Zg=function(e){return vo(e)||bo(e)},ep=function(e){return V(W(e.selection.getSelectedBlocks(),ir.fromDom),function(e){return!Zg(e)&&!zr(e).map(Zg).getOr(!1)})},tp=function(e,t){var n=e.settings,r=e.dom,o=e.selection,i=e.formatter,a=/[a-z%]+$/i.exec(n.indentation)[0],u=parseInt(n.indentation,10),s=e.getParam("indent_use_margin",!1);e.queryCommandState("InsertUnorderedList")||e.queryCommandState("InsertOrderedList")||n.forced_root_block||r.getParent(o.getNode(),r.isBlock)||i.apply("div"),U(ep(e),function(e){!function(e,t,n,r,o,i){if("false"!==e.getContentEditable(i)){var a=n?"margin":"padding";if(a="TABLE"===i.nodeName?"margin":a,a+="rtl"===e.getStyle(i,"direction",!0)?"Right":"Left","outdent"===t){var u=Math.max(0,parseInt(i.style[a]||0,10)-r);e.setStyle(i,a,u?u+o:"")}else u=parseInt(i.style[a]||0,10)+r+o,e.setStyle(i,a,u)}}(r,t,s,u,a,e.dom())})},np=Jt.each,rp=Jt.extend,op=Jt.map,ip=Jt.inArray;function ap(s){var o,i,a,t,c={state:{},exec:{},value:{}},n=s.settings;s.on("PreInit",function(){o=s.dom,i=s.selection,n=s.settings,a=s.formatter});var r=function(e){var t;if(!s.quirks.isHidden()&&!s.removed){if(e=e.toLowerCase(),t=c.state[e])return t(e);try{return s.getDoc().queryCommandState(e)}catch(n){}return!1}},e=function(e,n){n=n||"exec",np(e,function(t,e){np(e.toLowerCase().split(","),function(e){c[n][e]=t})})},u=function(e,t,n){e=e.toLowerCase(),c.value[e]=function(){return t.call(n||s)}};rp(this,{execCommand:function(t,n,r,e){var o,i,a=!1;if(!s.removed){if(/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(t)||e&&e.skip_focus?Jg(s):s.focus(),(e=s.fire("BeforeExecCommand",{command:t,ui:n,value:r})).isDefaultPrevented())return!1;if(i=t.toLowerCase(),o=c.exec[i])return o(i,n,r),s.fire("ExecCommand",{command:t,ui:n,value:r}),!0;if(np(s.plugins,function(e){if(e.execCommand&&e.execCommand(t,n,r))return s.fire("ExecCommand",{command:t,ui:n,value:r}),!(a=!0)}),a)return a;if(s.theme&&s.theme.execCommand&&s.theme.execCommand(t,n,r))return s.fire("ExecCommand",{command:t,ui:n,value:r}),!0;try{a=s.getDoc().execCommand(t,n,r)}catch(u){}return!!a&&(s.fire("ExecCommand",{command:t,ui:n,value:r}),!0)}},queryCommandState:r,queryCommandValue:function(e){var t;if(!s.quirks.isHidden()&&!s.removed){if(e=e.toLowerCase(),t=c.value[e])return t(e);try{return s.getDoc().queryCommandValue(e)}catch(n){}}},queryCommandSupported:function(e){if(e=e.toLowerCase(),c.exec[e])return!0;try{return s.getDoc().queryCommandSupported(e)}catch(t){}return!1},addCommands:e,addCommand:function(e,o,i){e=e.toLowerCase(),c.exec[e]=function(e,t,n,r){return o.call(i||s,t,n,r)}},addQueryStateHandler:function(e,t,n){e=e.toLowerCase(),c.state[e]=function(){return t.call(n||s)}},addQueryValueHandler:u,hasCustomCommand:function(e){return e=e.toLowerCase(),!!c.exec[e]}});var l=function(e,t,n){return t===undefined&&(t=!1),n===undefined&&(n=null),s.getDoc().execCommand(e,t,n)},f=function(e){return a.match(e)},d=function(e,t){a.toggle(e,t?{value:t}:undefined),s.nodeChanged()},m=function(e){t=i.getBookmark(e)},g=function(){i.moveToBookmark(t)};e({"mceResetDesignMode,mceBeginUndoLevel":function(){},"mceEndUndoLevel,mceAddUndoLevel":function(){s.undoManager.add()},"Cut,Copy,Paste":function(e){var t,n=s.getDoc();try{l(e)}catch(o){t=!0}if("paste"!==e||n.queryCommandEnabled(e)||(t=!0),t||!n.queryCommandSupported(e)){var r=s.translate("Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.");ge.mac&&(r=r.replace(/Ctrl\+/g,"\u2318+")),s.notificationManager.open({text:r,type:"error"})}},unlink:function(){if(i.isCollapsed()){var e=s.dom.getParent(s.selection.getStart(),"a");e&&s.dom.remove(e,!0)}else a.remove("link")},"JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone":function(e){var t=e.substring(7);"full"===t&&(t="justify"),np("left,center,right,justify".split(","),function(e){t!==e&&a.remove("align"+e)}),"none"!==t&&d("align"+t)},"InsertUnorderedList,InsertOrderedList":function(e){var t,n;l(e),(t=o.getParent(i.getNode(),"ol,ul"))&&(n=t.parentNode,/^(H[1-6]|P|ADDRESS|PRE)$/.test(n.nodeName)&&(m(),o.split(n,t),g()))},"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){d(e)},"ForeColor,HiliteColor":function(e,t,n){d(e,n)},FontName:function(e,t,n){var r,o;o=n,(r=s).formatter.toggle("fontname",{value:gg(r,o)}),r.nodeChanged()},FontSize:function(e,t,n){var r,o;o=n,(r=s).formatter.toggle("fontsize",{value:gg(r,o)}),r.nodeChanged()},RemoveFormat:function(e){a.remove(e)},mceBlockQuote:function(){d("blockquote")},FormatBlock:function(e,t,n){return d(n||"p")},mceCleanup:function(){var e=i.getBookmark();s.setContent(s.getContent()),i.moveToBookmark(e)},mceRemoveNode:function(e,t,n){var r=n||i.getNode();r!==s.getBody()&&(m(),s.dom.remove(r,!0),g())},mceSelectNodeDepth:function(e,t,n){var r=0;o.getParent(i.getNode(),function(e){if(1===e.nodeType&&r++===n)return i.select(e),!1},s.getBody())},mceSelectNode:function(e,t,n){i.select(n)},mceInsertContent:function(e,t,n){ul(s,n)},mceInsertRawHTML:function(e,t,n){i.setContent("tiny_mce_marker");var r=s.getContent();s.setContent(r.replace(/tiny_mce_marker/g,function(){return n}))},mceToggleFormat:function(e,t,n){d(n)},mceSetContent:function(e,t,n){s.setContent(n)},"Indent,Outdent":function(e){tp(s,e)},mceRepaint:function(){},InsertHorizontalRule:function(){s.execCommand("mceInsertContent",!1,"<hr />")},mceToggleVisualAid:function(){s.hasVisual=!s.hasVisual,s.addVisual()},mceReplaceContent:function(e,t,n){s.execCommand("mceInsertContent",!1,n.replace(/\{\$selection\}/g,i.getContent({format:"text"})))},mceInsertLink:function(e,t,n){var r;"string"==typeof n&&(n={href:n}),r=o.getParent(i.getNode(),"a"),n.href=n.href.replace(" ","%20"),r&&n.href||a.remove("link"),n.href&&a.apply("link",n,r)},selectAll:function(){var e=o.getParent(i.getStart(),Po.isContentEditableTrue);if(e){var t=o.createRng();t.selectNodeContents(e),i.setRng(t)}},"delete":function(){ug(s)},forwardDelete:function(){sg(s)},mceNewDocument:function(){s.setContent("")},InsertLineBreak:function(e,t,n){return Bg(s,n),!0}});var p=function(n){return function(){var e=i.isCollapsed()?[o.getParent(i.getNode(),o.isBlock)]:i.getSelectedBlocks(),t=op(e,function(e){return!!a.matchNode(e,n)});return-1!==ip(t,!0)}};e({JustifyLeft:p("alignleft"),JustifyCenter:p("aligncenter"),JustifyRight:p("alignright"),JustifyFull:p("alignjustify"),"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){return f(e)},mceBlockQuote:function(){return f("blockquote")},Outdent:function(){var e;if(n.inline_styles){if((e=o.getParent(i.getStart(),o.isBlock))&&0<parseInt(e.style.paddingLeft,10))return!0;if((e=o.getParent(i.getEnd(),o.isBlock))&&0<parseInt(e.style.paddingLeft,10))return!0}return r("InsertUnorderedList")||r("InsertOrderedList")||!n.inline_styles&&!!o.getParent(i.getNode(),"BLOCKQUOTE")},"InsertUnorderedList,InsertOrderedList":function(e){var t=o.getParent(i.getNode(),"ul,ol");return t&&("insertunorderedlist"===e&&"UL"===t.tagName||"insertorderedlist"===e&&"OL"===t.tagName)}},"state"),e({Undo:function(){s.undoManager.undo()},Redo:function(){s.undoManager.redo()}}),u("FontName",function(){return mg(t=s).fold(function(){return dg(t).map(function(e){return fg.getFontFamily(t.getBody(),e)}).getOr("")},function(e){return fg.getFontFamily(t.getBody(),e)});var t},this),u("FontSize",function(){return mg(t=s).fold(function(){return dg(t).map(function(e){return fg.getFontSize(t.getBody(),e)}).getOr("")},function(e){return fg.getFontSize(t.getBody(),e)});var t},this)}var up=Jt.makeMap("focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover draggesture dragdrop drop drag submit compositionstart compositionend compositionupdate touchstart touchmove touchend"," "),sp=function(a){var u,s,c=this,l={},f=function(){return!1},d=function(){return!0};u=(a=a||{}).scope||c,s=a.toggleEvent||f;var r=function(e,t,n,r){var o,i,a;if(!1===t&&(t=f),t)for(t={func:t},r&&Jt.extend(t,r),a=(i=e.toLowerCase().split(" ")).length;a--;)e=i[a],(o=l[e])||(o=l[e]=[],s(e,!0)),n?o.unshift(t):o.push(t);return c},m=function(e,t){var n,r,o,i,a;if(e)for(n=(i=e.toLowerCase().split(" ")).length;n--;){if(e=i[n],r=l[e],!e){for(o in l)s(o,!1),delete l[o];return c}if(r){if(t)for(a=r.length;a--;)r[a].func===t&&(r=r.slice(0,a).concat(r.slice(a+1)),l[e]=r);else r.length=0;r.length||(s(e,!1),delete l[e])}}else{for(e in l)s(e,!1);l={}}return c};c.fire=function(e,t){var n,r,o,i;if(e=e.toLowerCase(),(t=t||{}).type=e,t.target||(t.target=u),t.preventDefault||(t.preventDefault=function(){t.isDefaultPrevented=d},t.stopPropagation=function(){t.isPropagationStopped=d},t.stopImmediatePropagation=function(){t.isImmediatePropagationStopped=d},t.isDefaultPrevented=f,t.isPropagationStopped=f,t.isImmediatePropagationStopped=f),a.beforeFire&&a.beforeFire(t),n=l[e])for(r=0,o=n.length;r<o;r++){if((i=n[r]).once&&m(e,i.func),t.isImmediatePropagationStopped())return t.stopPropagation(),t;if(!1===i.func.call(u,t))return t.preventDefault(),t}return t},c.on=r,c.off=m,c.once=function(e,t,n){return r(e,t,n,{once:!0})},c.has=function(e){return e=e.toLowerCase(),!(!l[e]||0===l[e].length)}};sp.isNative=function(e){return!!up[e.toLowerCase()]};var cp,lp=function(n){return n._eventDispatcher||(n._eventDispatcher=new sp({scope:n,toggleEvent:function(e,t){sp.isNative(e)&&n.toggleNativeEvent&&n.toggleNativeEvent(e,t)}})),n._eventDispatcher},fp={fire:function(e,t,n){if(this.removed&&"remove"!==e&&"detach"!==e)return t;if(t=lp(this).fire(e,t,n),!1!==n&&this.parent)for(var r=this.parent();r&&!t.isPropagationStopped();)r.fire(e,t,!1),r=r.parent();return t},on:function(e,t,n){return lp(this).on(e,t,n)},off:function(e,t){return lp(this).off(e,t)},once:function(e,t){return lp(this).once(e,t)},hasEventListeners:function(e){return lp(this).has(e)}},dp=function(e,t){return e.fire("PreProcess",t)},mp=function(e,t){return e.fire("PostProcess",t)},gp=function(e){return e.fire("remove")},pp=function(e){return e.fire("detach")},hp=function(e,t){return e.fire("SwitchMode",{mode:t})},vp=function(e,t,n,r){e.fire("ObjectResizeStart",{target:t,width:n,height:r})},bp=function(e,t,n,r){e.fire("ObjectResized",{target:t,width:n,height:r})},yp=function(e,t,n){try{e.getDoc().execCommand(t,!1,n)}catch(r){}},Cp=function(e,t,n){var r,o;ji(e,t)&&!1===n?(o=t,Fi(r=e)?r.dom().classList.remove(o):Ui(r,o),Hi(r)):n&&Vi(e,t)},xp=function(e,t){Cp(ir.fromDom(e.getBody()),"mce-content-readonly",t),t?(e.selection.controlSelection.hideResizeRect(),e.readonly=!0,e.getBody().contentEditable="false"):(e.readonly=!1,e.getBody().contentEditable="true",yp(e,"StyleWithCSS",!1),yp(e,"enableInlineTableEditing",!1),yp(e,"enableObjectResizing",!1),e.focus(),e.nodeChanged())},wp=function(e){return e.readonly?"readonly":"design"},Np=bi.DOM,Ep=function(e,t){return"selectionchange"===t?e.getDoc():!e.inline&&/^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(t)?e.getDoc().documentElement:e.settings.event_root?(e.eventRoot||(e.eventRoot=Np.select(e.settings.event_root)[0]),e.eventRoot):e.getBody()},Sp=function(e,t,n){var r;(r=e).hidden||r.readonly?!0===e.readonly&&n.preventDefault():e.fire(t,n)},kp=function(i,a){var e,t;if(i.delegates||(i.delegates={}),!i.delegates[a]&&!i.removed)if(e=Ep(i,a),i.settings.event_root){if(cp||(cp={},i.editorManager.on("removeEditor",function(){var e;if(!i.editorManager.activeEditor&&cp){for(e in cp)i.dom.unbind(Ep(i,e));cp=null}})),cp[a])return;t=function(e){for(var t=e.target,n=i.editorManager.get(),r=n.length;r--;){var o=n[r].getBody();(o===t||Np.isChildOf(t,o))&&Sp(n[r],a,e)}},cp[a]=t,Np.bind(e,a,t)}else t=function(e){Sp(i,a,e)},Np.bind(e,a,t),i.delegates[a]=t},Tp={bindPendingEventDelegates:function(){var t=this;Jt.each(t._pendingNativeEvents,function(e){kp(t,e)})},toggleNativeEvent:function(e,t){var n=this;"focus"!==e&&"blur"!==e&&(t?n.initialized?kp(n,e):n._pendingNativeEvents?n._pendingNativeEvents.push(e):n._pendingNativeEvents=[e]:n.initialized&&(n.dom.unbind(Ep(n,e),e,n.delegates[e]),delete n.delegates[e]))},unbindAllNativeEvents:function(){var e,t=this,n=t.getBody(),r=t.dom;if(t.delegates){for(e in t.delegates)t.dom.unbind(Ep(t,e),e,t.delegates[e]);delete t.delegates}!t.inline&&n&&r&&(n.onload=null,r.unbind(t.getWin()),r.unbind(t.getDoc())),r&&(r.unbind(n),r.unbind(t.getContainer()))}},Ap=Tp=Jt.extend({},fp,Tp),Rp=kr("sections","settings"),_p=rr.detect().deviceType.isTouch(),Dp=["lists","autolink","autosave"],Bp={theme:"mobile"},Op=function(e){var t=D(e)?e.join(" "):e,n=W(R(t)?t.split(" "):[],Yn);return V(n,function(e){return 0<e.length})},Pp=function(n,e){var r,o,i,t=(r=function(e,t){return F(n,t)},o={},i={},pr(e,function(e,t){(r(e,t)?o:i)[t]=e}),{t:o,f:i});return Rp(t.t,t.f)},Ip=function(e,t){return e.sections().hasOwnProperty(t)},Lp=function(e,t,n,r){var o,i=Op(n.forced_plugins),a=Op(r.plugins),u=e&&Ip(t,"mobile")?V(a,d(F,Dp)):a,s=(o=u,[].concat(Op(i)).concat(Op(o)));return Jt.extend(r,{plugins:s.join(" ")})},Mp=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m,g=Pp(["mobile"],r),p=Jt.extend(t,n,g.settings(),(f=e,m=(d=g).settings().inline,f&&Ip(d,"mobile")&&!m?(u="mobile",s=Bp,c=g.sections(),l=c.hasOwnProperty(u)?c[u]:{},Jt.extend({},s,l)):{}),{validate:!0,content_editable:g.settings().inline,external_plugins:(o=n,i=g.settings(),a=i.external_plugins?i.external_plugins:{},o&&o.external_plugins?Jt.extend({},o.external_plugins,a):a)});return Lp(e,g,n,p)},Fp=function(e,t,n){return A.from(t.settings[n]).filter(e)},zp=function(e,t,n,r){var o,i,a,u=t in e.settings?e.settings[t]:n;return"hash"===r?(a={},"string"==typeof(i=u)?U(0<i.indexOf("=")?i.split(/[;,](?![^=;,]*(?:[;,]|$))/):i.split(","),function(e){var t=e.split("=");1<t.length?a[Jt.trim(t[0])]=Jt.trim(t[1]):a[Jt.trim(t[0])]=Jt.trim(t)}):a=i,a):"string"===r?Fp(R,e,t).getOr(n):"number"===r?Fp(I,e,t).getOr(n):"boolean"===r?Fp(O,e,t).getOr(n):"object"===r?Fp(_,e,t).getOr(n):"array"===r?Fp(D,e,t).getOr(n):"string[]"===r?Fp((o=R,function(e){return D(e)&&ee(e,o)}),e,t).getOr(n):"function"===r?Fp(P,e,t).getOr(n):u},Up=Jt.each,Vp=Jt.explode,Hp={f1:112,f2:113,f3:114,f4:115,f5:116,f6:117,f7:118,f8:119,f9:120,f10:121,f11:122,f12:123},jp=Jt.makeMap("alt,ctrl,shift,meta,access");function qp(i){var a={},r=[],u=function(e){var t,n,r={};for(n in Up(Vp(e,"+"),function(e){e in jp?r[e]=!0:/^[0-9]{2,}$/.test(e)?r.keyCode=parseInt(e,10):(r.charCode=e.charCodeAt(0),r.keyCode=Hp[e]||e.toUpperCase().charCodeAt(0))}),t=[r.keyCode],jp)r[n]?t.push(n):r[n]=!1;return r.id=t.join(","),r.access&&(r.alt=!0,ge.mac?r.ctrl=!0:r.shift=!0),r.meta&&(ge.mac?r.meta=!0:(r.ctrl=!0,r.meta=!1)),r},s=function(e,t,n,r){var o;return(o=Jt.map(Vp(e,">"),u))[o.length-1]=Jt.extend(o[o.length-1],{func:n,scope:r||i}),Jt.extend(o[0],{desc:i.translate(t),subpatterns:o.slice(1)})},o=function(e,t){return!!t&&t.ctrl===e.ctrlKey&&t.meta===e.metaKey&&t.alt===e.altKey&&t.shift===e.shiftKey&&!!(e.keyCode===t.keyCode||e.charCode&&e.charCode===t.charCode)&&(e.preventDefault(),!0)},c=function(e){return e.func?e.func.call(e.scope):null};i.on("keyup keypress keydown",function(t){var e,n;((n=t).altKey||n.ctrlKey||n.metaKey||"keydown"===(e=t).type&&112<=e.keyCode&&e.keyCode<=123)&&!t.isDefaultPrevented()&&(Up(a,function(e){if(o(t,e))return r=e.subpatterns.slice(0),"keydown"===t.type&&c(e),!0}),o(t,r[0])&&(1===r.length&&"keydown"===t.type&&c(r[0]),r.shift()))}),this.add=function(e,n,r,o){var t;return"string"==typeof(t=r)?r=function(){i.execCommand(t,!1,null)}:Jt.isArray(t)&&(r=function(){i.execCommand(t[0],t[1],t[2])}),Up(Vp(Jt.trim(e.toLowerCase())),function(e){var t=s(e,n,r,o);a[t.id]=t}),!0},this.remove=function(e){var t=s(e);return!!a[t.id]&&(delete a[t.id],!0)}}var $p=function(e){var t=Mr(e).dom();return e.dom()===t.activeElement},Wp=function(t){return(e=Mr(t),n=e!==undefined?e.dom():H.document,A.from(n.activeElement).map(ir.fromDom)).filter(function(e){return t.dom().contains(e.dom())});var e,n},Kp=function(t,e){return(n=e,n.collapsed?A.from(Wa(n.startContainer,n.startOffset)).map(ir.fromDom):A.none()).bind(function(e){return Co(e)?A.some(e):!1===Lr(t,e)?A.some(t):A.none()});var n},Xp=function(t,e){Kp(ir.fromDom(t.getBody()),e).bind(function(e){return nc.firstPositionIn(e.dom())}).fold(function(){t.selection.normalize()},function(e){return t.selection.setRng(e.toRange())})},Yp=function(e){if(e.setActive)try{e.setActive()}catch(t){e.focus()}else e.focus()},Gp=function(e){var t,n=e.getBody();return n&&(t=ir.fromDom(n),$p(t)||Wp(t).isSome())},Jp=function(e){return e.inline?Gp(e):(t=e).iframeElement&&$p(ir.fromDom(t.iframeElement));var t},Qp=function(e){return e.editorManager.setActive(e)},Zp=function(e,t){e.removed||(t?Qp(e):function(t){var e=t.selection,n=t.settings.content_editable,r=t.getBody(),o=e.getRng();t.quirks.refreshContentEditable();var i,a,u=(i=t,a=e.getNode(),i.dom.getParent(a,function(e){return"true"===i.dom.getContentEditable(e)}));if(t.$.contains(r,u))return Yp(u),Xp(t,o),Qp(t);t.bookmark!==undefined&&!1===Jp(t)&&Qg(t).each(function(e){t.selection.setRng(e),o=e}),n||(ge.opera||Yp(r),t.getWin().focus()),(ge.gecko||n)&&(Yp(r),Xp(t,o)),Qp(t)}(e))},eh=Jp,th=function(e,t){return t.dom()[e]},nh=function(e,t){return parseInt(Er(t,e),10)},rh=d(th,"clientWidth"),oh=d(th,"clientHeight"),ih=d(nh,"margin-top"),ah=d(nh,"margin-left"),uh=function(e,t,n){var r,o,i,a,u,s,c,l,f,d,m,g=ir.fromDom(e.getBody()),p=e.inline?g:(r=g,ir.fromDom(r.dom().ownerDocument.documentElement)),h=(o=e.inline,a=t,u=n,s=(i=p).dom().getBoundingClientRect(),{x:a-(o?s.left+i.dom().clientLeft+ah(i):0),y:u-(o?s.top+i.dom().clientTop+ih(i):0)});return l=h.x,f=h.y,d=rh(c=p),m=oh(c),0<=l&&0<=f&&l<=d&&f<=m},sh=function(e){var t,n=e.inline?e.getBody():e.getContentAreaContainer();return(t=n,A.from(t).map(ir.fromDom)).map(function(e){return Lr(Mr(e),e)}).getOr(!1)};function ch(n){var t,o=[],i=function(){var e,t=n.theme;return t&&t.getNotificationManagerImpl?t.getNotificationManagerImpl():{open:e=function(){throw new Error("Theme did not provide a NotificationManager implementation.")},close:e,reposition:e,getArgs:e}},a=function(){0<o.length&&i().reposition(o)},u=function(t){G(o,function(e){return e===t}).each(function(e){o.splice(e,1)})},r=function(r){if(!n.removed&&sh(n))return Y(o,function(e){return t=i().getArgs(e),n=r,!(t.type!==n.type||t.text!==n.text||t.progressBar||t.timeout||n.progressBar||n.timeout);var t,n}).getOrThunk(function(){n.editorManager.setActive(n);var e,t=i().open(r,function(){u(t),a()});return e=t,o.push(e),a(),t})};return(t=n).on("SkinLoaded",function(){var e=t.settings.service_message;e&&r({text:e,type:"warning",timeout:0,icon:""})}),t.on("ResizeEditor ResizeWindow",function(){ye.requestAnimationFrame(a)}),t.on("remove",function(){U(o.slice(),function(e){i().close(e)})}),{open:r,close:function(){A.from(o[0]).each(function(e){i().close(e),u(e),a()})},getNotifications:function(){return o}}}function lh(r){var o=[],i=function(){var e,t=r.theme;return t&&t.getWindowManagerImpl?t.getWindowManagerImpl():{open:e=function(){throw new Error("Theme did not provide a WindowManager implementation.")},alert:e,confirm:e,close:e,getParams:e,setParams:e}},a=function(e,t){return function(){return t?t.apply(e,arguments):undefined}},u=function(e){var t;o.push(e),t=e,r.fire("OpenWindow",{win:t})},s=function(n){G(o,function(e){return e===n}).each(function(e){var t;o.splice(e,1),t=n,r.fire("CloseWindow",{win:t}),0===o.length&&r.focus()})},e=function(){return A.from(o[o.length-1])};return r.on("remove",function(){U(o.slice(0),function(e){i().close(e)})}),{windows:o,open:function(e,t){r.editorManager.setActive(r),Gg(r);var n=i().open(e,t,s);return u(n),n},alert:function(e,t,n){var r=i().alert(e,a(n||this,t),s);u(r)},confirm:function(e,t,n){var r=i().confirm(e,a(n||this,t),s);u(r)},close:function(){e().each(function(e){i().close(e),s(e)})},getParams:function(){return e().map(i().getParams).getOr(null)},setParams:function(t){e().each(function(e){i().setParams(e,t)})},getWindows:function(){return o}}}var fh={},dh="en",mh={setCode:function(e){e&&(dh=e,this.rtl=!!this.data[e]&&"rtl"===this.data[e]._dir)},getCode:function(){return dh},rtl:!1,add:function(e,t){var n=fh[e];for(var r in n||(fh[e]=n={}),t)n[r]=t[r];this.setCode(e)},translate:function(e){var t=fh[dh]||{},n=function(e){return Jt.is(e,"function")?Object.prototype.toString.call(e):r(e)?"":""+e},r=function(e){return""===e||null===e||Jt.is(e,"undefined")},o=function(e){return e=n(e),Jt.hasOwn(t,e)?n(t[e]):e};if(r(e))return"";if(Jt.is(e,"object")&&Jt.hasOwn(e,"raw"))return n(e.raw);if(Jt.is(e,"array")){var i=e.slice(1);e=o(e[0]).replace(/\{([0-9]+)\}/g,function(e,t){return Jt.hasOwn(i,t)?n(i[t]):e})}return o(e).replace(/{context:\w+}$/,"")},data:fh},gh=ki.PluginManager,ph=function(e,t){var n=function(e,t){for(var n in gh.urls)if(gh.urls[n]+"/plugin"+t+".js"===e)return n;return null}(t,e.suffix);return n?mh.translate(["Failed to load plugin: {0} from url {1}",n,t]):mh.translate(["Failed to load plugin url: {0}",t])},hh=function(e,t){e.notificationManager.open({type:"error",text:t})},vh=function(e,t){e._skinLoaded?hh(e,t):e.on("SkinLoaded",function(){hh(e,t)})},bh=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r=H.window.console;r&&(r.error?r.error.apply(r,arguments):r.log.apply(r,arguments))},yh={pluginLoadError:function(e,t){vh(e,ph(e,t))},pluginInitError:function(e,t,n){var r=mh.translate(["Failed to initialize plugin: {0}",t]);bh(r,n),vh(e,r)},uploadError:function(e,t){vh(e,mh.translate(["Failed to upload image: {0}",t]))},displayError:vh,initError:bh},Ch=ki.PluginManager,xh=ki.ThemeManager;function wh(){return new(ue.getOrDie("XMLHttpRequest"))}function Nh(u,s){var r={},n=function(e,r,o,t){var i,n;(i=wh()).open("POST",s.url),i.withCredentials=s.credentials,i.upload.onprogress=function(e){t(e.loaded/e.total*100)},i.onerror=function(){o("Image upload failed due to a XHR Transport error. Code: "+i.status)},i.onload=function(){var e,t,n;i.status<200||300<=i.status?o("HTTP Error: "+i.status):(e=JSON.parse(i.responseText))&&"string"==typeof e.location?r((t=s.basePath,n=e.location,t?t.replace(/\/$/,"")+"/"+n.replace(/^\//,""):n)):o("Invalid JSON: "+i.responseText)},(n=new H.FormData).append("file",e.blob(),e.filename()),i.send(n)},c=function(e,t){return{url:t,blobInfo:e,status:!0}},l=function(e,t){return{url:"",blobInfo:e,status:!1,error:t}},f=function(e,t){Jt.each(r[e],function(e){e(t)}),delete r[e]},o=function(e,n){return e=Jt.grep(e,function(e){return!u.isUploaded(e.blobUri())}),pe.all(Jt.map(e,function(e){return u.isPending(e.blobUri())?(t=e.blobUri(),new pe(function(e){r[t]=r[t]||[],r[t].push(e)})):(o=e,i=s.handler,a=n,u.markPending(o.blobUri()),new pe(function(t){var n;try{var r=function(){n&&n.close()};i(o,function(e){r(),u.markUploaded(o.blobUri(),e),f(o.blobUri(),c(o,e)),t(c(o,e))},function(e){r(),u.removeFailed(o.blobUri()),f(o.blobUri(),l(o,e)),t(l(o,e))},function(e){e<0||100<e||(n||(n=a()),n.progressBar.value(e))})}catch(e){t(l(o,e.message))}}));var o,i,a,t}))};return!1===P(s.handler)&&(s.handler=n),{upload:function(e,t){return s.url||s.handler!==n?o(e,t):new pe(function(e){e([])})}}}var Eh=function(e){return ue.getOrDie("atob")(e)},Sh=function(e){var t,n,r=decodeURIComponent(e).split(",");return(n=/data:([^;]+)/.exec(r[0]))&&(t=n[1]),{type:t,data:r[1]}},kh=function(a){return new pe(function(e){var t,n,r,o,i=Sh(a);try{t=Eh(i.data)}catch(ZN){return void e(new H.Blob([]))}for(o=t.length,n=new(ue.getOrDie("Uint8Array"))(o),r=0;r<n.length;r++)n[r]=t.charCodeAt(r);e(new H.Blob([n],{type:i.type}))})},Th=function(e){return 0===e.indexOf("blob:")?(i=e,new pe(function(e,t){var n=function(){t("Cannot convert "+i+" to Blob. Resource might not exist or is inaccessible.")};try{var r=wh();r.open("GET",i,!0),r.responseType="blob",r.onload=function(){200===this.status?e(this.response):n()},r.onerror=n,r.send()}catch(o){n()}})):0===e.indexOf("data:")?kh(e):null;var i},Ah=function(n){return new pe(function(e){var t=new(ue.getOrDie("FileReader"));t.onloadend=function(){e(t.result)},t.readAsDataURL(n)})},Rh=Sh,_h=0,Dh=function(e){return(e||"blobid")+_h++},Bh=function(n,r,o,t){var i,a;0!==r.src.indexOf("blob:")?(i=Rh(r.src).data,(a=n.findFirst(function(e){return e.base64()===i}))?o({image:r,blobInfo:a}):Th(r.src).then(function(e){a=n.create(Dh(),e,i),n.add(a),o({image:r,blobInfo:a})},function(e){t(e)})):(a=n.getByUri(r.src))?o({image:r,blobInfo:a}):Th(r.src).then(function(t){Ah(t).then(function(e){i=Rh(e).data,a=n.create(Dh(),t,i),n.add(a),o({image:r,blobInfo:a})})},function(e){t(e)})},Oh=function(e){return e?oe(e.getElementsByTagName("img")):[]},Ph=0,Ih={uuid:function(e){return e+Ph+++(t=function(){return Math.round(4294967295*Math.random()).toString(36)},"s"+(new Date).getTime().toString(36)+t()+t()+t());var t}};function Lh(u){var n,o,t,e,i,r,a,s,c,l=(n=[],o=function(e){var t,n,r;if(!e.blob||!e.base64)throw new Error("blob and base64 representations of the image are required for BlobInfo to be created");return t=e.id||Ih.uuid("blobid"),n=e.name||t,{id:q(t),name:q(n),filename:q(n+"."+(r=e.blob.type,{"image/jpeg":"jpg","image/jpg":"jpg","image/gif":"gif","image/png":"png"}[r.toLowerCase()]||"dat")),blob:q(e.blob),base64:q(e.base64),blobUri:q(e.blobUri||ce.createObjectURL(e.blob)),uri:q(e.uri)}},{create:function(e,t,n,r){if(R(e))return o({id:e,name:r,blob:t,base64:n});if(_(e))return o(e);throw new Error("Unknown input type")},add:function(e){t(e.id())||n.push(e)},get:t=function(t){return e(function(e){return e.id()===t})},getByUri:function(t){return e(function(e){return e.blobUri()===t})},findFirst:e=function(e){return V(n,e)[0]},removeByUri:function(t){n=V(n,function(e){return e.blobUri()!==t||(ce.revokeObjectURL(e.blobUri()),!1)})},destroy:function(){U(n,function(e){ce.revokeObjectURL(e.blobUri())}),n=[]}}),f=(a={},s=function(e,t){return{status:e,resultUri:t}},{hasBlobUri:c=function(e){return e in a},getResultUri:function(e){var t=a[e];return t?t.resultUri:null},isPending:function(e){return!!c(e)&&1===a[e].status},isUploaded:function(e){return!!c(e)&&2===a[e].status},markPending:function(e){a[e]=s(1,null)},markUploaded:function(e,t){a[e]=s(2,t)},removeFailed:function(e){delete a[e]},destroy:function(){a={}}}),d=[],m=function(t){return function(e){return u.selection?t(e):[]}},g=function(e,t,n){for(var r=0;-1!==(r=e.indexOf(t,r))&&(e=e.substring(0,r)+n+e.substr(r+t.length),r+=n.length-t.length+1),-1!==r;);return e},p=function(e,t,n){return e=g(e,'src="'+t+'"','src="'+n+'"'),e=g(e,'data-mce-src="'+t+'"','data-mce-src="'+n+'"')},h=function(t,n){U(u.undoManager.data,function(e){"fragmented"===e.type?e.fragments=W(e.fragments,function(e){return p(e,t,n)}):e.content=p(e.content,t,n)})},v=function(){return u.notificationManager.open({text:u.translate("Image uploading..."),type:"info",timeout:-1,progressBar:!0})},b=function(e,t){l.removeByUri(e.src),h(e.src,t),u.$(e).attr({src:Tl(u)?t+"?"+(new Date).getTime():t,"data-mce-src":u.convertURL(t,"src")})},y=function(n){return i||(i=Nh(f,{url:Rl(u),basePath:_l(u),credentials:Dl(u),handler:Bl(u)})),w().then(m(function(r){var e;return e=W(r,function(e){return e.blobInfo}),i.upload(e,v).then(m(function(e){var t=W(e,function(e,t){var n=r[t].image;return e.status&&Al(u)?b(n,e.url):e.error&&yh.uploadError(u,e.error),{element:n,status:e.status}});return n&&n(t),t}))}))},C=function(e){if(kl(u))return y(e)},x=function(t){return!1!==ee(d,function(e){return e(t)})&&(0!==t.getAttribute("src").indexOf("data:")||Sl(u)(t))},w=function(){var o,i,a;return r||(o=f,i=l,a={},r={findAll:function(e,n){var t;n||(n=q(!0)),t=V(Oh(e),function(e){var t=e.src;return!!ge.fileApi&&!e.hasAttribute("data-mce-bogus")&&!e.hasAttribute("data-mce-placeholder")&&!(!t||t===ge.transparentSrc)&&(0===t.indexOf("blob:")?!o.isUploaded(t)&&n(e):0===t.indexOf("data:")&&n(e))});var r=W(t,function(n){if(a[n.src])return new pe(function(t){a[n.src].then(function(e){if("string"==typeof e)return e;t({image:n,blobInfo:e.blobInfo})})});var e=new pe(function(e,t){Bh(i,n,e,t)}).then(function(e){return delete a[e.image.src],e})["catch"](function(e){return delete a[n.src],e});return a[n.src]=e});return pe.all(r)}}),r.findAll(u.getBody(),x).then(m(function(e){return e=V(e,function(e){return"string"!=typeof e||(yh.displayError(u,e),!1)}),U(e,function(e){h(e.image.src,e.blobInfo.blobUri()),e.image.src=e.blobInfo.blobUri(),e.image.removeAttribute("data-mce-src")}),e}))},N=function(e){return e.replace(/src="(blob:[^"]+)"/g,function(e,n){var t=f.getResultUri(n);if(t)return'src="'+t+'"';var r=l.getByUri(n);return r||(r=X(u.editorManager.get(),function(e,t){return e||t.editorUpload&&t.editorUpload.blobCache.getByUri(n)},null)),r?'src="data:'+r.blob().type+";base64,"+r.base64()+'"':e})};return u.on("setContent",function(){kl(u)?C():w()}),u.on("RawSaveContent",function(e){e.content=N(e.content)}),u.on("getContent",function(e){e.source_view||"raw"===e.format||(e.content=N(e.content))}),u.on("PostRender",function(){u.parser.addNodeFilter("img",function(e){U(e,function(e){var t=e.attr("src");if(!l.getByUri(t)){var n=f.getResultUri(t);n&&e.attr("src",n)}})})}),{blobCache:l,addFilter:function(e){d.push(e)},uploadImages:y,uploadImagesAuto:C,scanForImages:w,destroy:function(){l.destroy(),f.destroy(),r=i=null}}}var Mh=function(e,t){return e.hasOwnProperty(t.nodeName)},Fh=function(e,t){if(Po.isText(t)){if(0===t.nodeValue.length)return!0;if(/^\s+$/.test(t.nodeValue)&&(!t.nextSibling||Mh(e,t.nextSibling)))return!0}return!1},zh=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=e.settings,m=e.dom,g=e.selection,p=e.schema,h=p.getBlockElements(),v=g.getStart(),b=e.getBody();if(f=d.forced_root_block,v&&Po.isElement(v)&&f&&(l=b.nodeName.toLowerCase(),p.isValidChild(l,f.toLowerCase())&&(y=h,C=b,x=v,!z(Zl(ir.fromDom(x),ir.fromDom(C)),function(e){return Mh(y,e.dom())})))){var y,C,x,w,N;for(n=(t=g.getRng()).startContainer,r=t.startOffset,o=t.endContainer,i=t.endOffset,c=eh(e),v=b.firstChild;v;)if(w=h,N=v,Po.isText(N)||Po.isElement(N)&&!Mh(w,N)&&!dc(N)){if(Fh(h,v)){v=(u=v).nextSibling,m.remove(u);continue}a||(a=m.create(f,e.settings.forced_root_block_attrs),v.parentNode.insertBefore(a,v),s=!0),v=(u=v).nextSibling,a.appendChild(u)}else a=null,v=v.nextSibling;s&&c&&(t.setStart(n,r),t.setEnd(o,i),g.setRng(t),e.nodeChanged())}},Uh=function(e){e.settings.forced_root_block&&e.on("NodeChange",d(zh,e))},Vh=function(t){return Wr(t).fold(q([t]),function(e){return[t].concat(Vh(e))})},Hh=function(t){return Kr(t).fold(q([t]),function(e){return"br"===cr(e)?Ur(e).map(function(e){return[t].concat(Hh(e))}).getOr([]):[t].concat(Hh(e))})},jh=function(o,e){return Ya([(i=e,a=i.startContainer,u=i.startOffset,Po.isText(a)?0===u?A.some(ir.fromDom(a)):A.none():A.from(a.childNodes[u]).map(ir.fromDom)),(t=e,n=t.endContainer,r=t.endOffset,Po.isText(n)?r===n.data.length?A.some(ir.fromDom(n)):A.none():A.from(n.childNodes[r-1]).map(ir.fromDom))],function(e,t){var n=Y(Vh(o),d(Ir,e)),r=Y(Hh(o),d(Ir,t));return n.isSome()&&r.isSome()}).getOr(!1);var t,n,r,i,a,u},qh=function(e,t,n,r){var o=n,i=new ao(n,o),a=e.schema.getNonEmptyElements();do{if(3===n.nodeType&&0!==Jt.trim(n.nodeValue).length)return void(r?t.setStart(n,0):t.setEnd(n,n.nodeValue.length));if(a[n.nodeName]&&!/^(TD|TH)$/.test(n.nodeName))return void(r?t.setStartBefore(n):"BR"===n.nodeName?t.setEndBefore(n):t.setEndAfter(n));if(ge.ie&&ge.ie<11&&e.isBlock(n)&&e.isEmpty(n))return void(r?t.setStart(n,0):t.setEnd(n,0))}while(n=r?i.next():i.prev());"BODY"===o.nodeName&&(r?t.setStart(o,0):t.setEnd(o,o.childNodes.length))},$h=function(e){var t=e.selection.getSel();return t&&0<t.rangeCount};function Wh(i){var r,o=[];"onselectionchange"in i.getDoc()||i.on("NodeChange Click MouseUp KeyUp Focus",function(e){var t,n;n={startContainer:(t=i.selection.getRng()).startContainer,startOffset:t.startOffset,endContainer:t.endContainer,endOffset:t.endOffset},"nodechange"!==e.type&&pg(n,r)||i.fire("SelectionChange"),r=n}),i.on("contextmenu",function(){i.fire("SelectionChange")}),i.on("SelectionChange",function(){var e=i.selection.getStart(!0);!e||!ge.range&&i.selection.isCollapsed()||$h(i)&&!function(e){var t,n;if((n=i.$(e).parentsUntil(i.getBody()).add(e)).length===o.length){for(t=n.length;0<=t&&n[t]===o[t];t--);if(-1===t)return o=n,!0}return o=n,!1}(e)&&i.dom.isChildOf(e,i.getBody())&&i.nodeChanged({selectionChange:!0})}),i.on("MouseUp",function(e){!e.isDefaultPrevented()&&$h(i)&&("IMG"===i.selection.getNode().nodeName?ye.setEditorTimeout(i,function(){i.nodeChanged()}):i.nodeChanged())}),this.nodeChanged=function(e){var t,n,r,o=i.selection;i.initialized&&o&&!i.settings.disable_nodechange&&!i.readonly&&(r=i.getBody(),(t=o.getStart(!0)||r).ownerDocument===i.getDoc()&&i.dom.isChildOf(t,r)||(t=r),n=[],i.dom.getParent(t,function(e){if(e===r)return!0;n.push(e)}),(e=e||{}).element=t,e.parents=n,i.fire("NodeChange",e))}}var Kh,Xh,Yh=function(e){var t,n,r,o;return o=e.getBoundingClientRect(),n=(t=e.ownerDocument).documentElement,r=t.defaultView,{top:o.top+r.pageYOffset-n.clientTop,left:o.left+r.pageXOffset-n.clientLeft}},Gh=function(e,t){return n=(u=e).inline?Yh(u.getBody()):{left:0,top:0},a=(i=e).getBody(),r=i.inline?{left:a.scrollLeft,top:a.scrollTop}:{left:0,top:0},{pageX:(o=function(e,t){if(t.target.ownerDocument!==e.getDoc()){var n=Yh(e.getContentAreaContainer()),r=(i=(o=e).getBody(),a=o.getDoc().documentElement,u={left:i.scrollLeft,top:i.scrollTop},s={left:i.scrollLeft||a.scrollLeft,top:i.scrollTop||a.scrollTop},o.inline?u:s);return{left:t.pageX-n.left+r.left,top:t.pageY-n.top+r.top}}var o,i,a,u,s;return{left:t.pageX,top:t.pageY}}(e,t)).left-n.left+r.left,pageY:o.top-n.top+r.top};var n,r,o,i,a,u},Jh=Po.isContentEditableFalse,Qh=Po.isContentEditableTrue,Zh=function(e){e&&e.parentNode&&e.parentNode.removeChild(e)},ev=function(u,s){return function(e){if(0===e.button){var t=Y(s.dom.getParents(e.target),Qa(Jh,Qh)).getOr(null);if(i=s.getBody(),Jh(a=t)&&a!==i){var n=s.dom.getPos(t),r=s.getBody(),o=s.getDoc().documentElement;u.element=t,u.screenX=e.screenX,u.screenY=e.screenY,u.maxX=(s.inline?r.scrollWidth:o.offsetWidth)-2,u.maxY=(s.inline?r.scrollHeight:o.offsetHeight)-2,u.relX=e.pageX-n.x,u.relY=e.pageY-n.y,u.width=t.offsetWidth,u.height=t.offsetHeight,u.ghost=function(e,t,n,r){var o=t.cloneNode(!0);e.dom.setStyles(o,{width:n,height:r}),e.dom.setAttrib(o,"data-mce-selected",null);var i=e.dom.create("div",{"class":"mce-drag-container","data-mce-bogus":"all",unselectable:"on",contenteditable:"false"});return e.dom.setStyles(i,{position:"absolute",opacity:.5,overflow:"hidden",border:0,padding:0,margin:0,width:n,height:r}),e.dom.setStyles(o,{margin:0,boxSizing:"border-box"}),i.appendChild(o),i}(s,t,u.width,u.height)}}var i,a}},tv=function(l,f){return function(e){if(l.dragging&&(s=(i=f).selection,c=s.getSel().getRangeAt(0).startContainer,a=3===c.nodeType?c.parentNode:c,u=l.element,a!==u&&!i.dom.isChildOf(a,u)&&!Jh(a))){var t=(r=l.element,(o=r.cloneNode(!0)).removeAttribute("data-mce-selected"),o),n=f.fire("drop",{targetClone:t,clientX:e.clientX,clientY:e.clientY});n.isDefaultPrevented()||(t=n.targetClone,f.undoManager.transact(function(){Zh(l.element),f.insertContent(f.dom.getOuterHTML(t)),f._selectionOverrides.hideFakeCaret()}))}var r,o,i,a,u,s,c;nv(l)}},nv=function(e){e.dragging=!1,e.element=null,Zh(e.ghost)},rv=function(e){var t,n,r,o,i,a,p,h,v,u,s,c={};t=bi.DOM,a=H.document,n=ev(c,e),p=c,h=e,v=ye.throttle(function(e,t){h._selectionOverrides.hideFakeCaret(),h.selection.placeCaretAt(e,t)},0),r=function(e){var t,n,r,o,i,a,u,s,c,l,f,d,m=Math.max(Math.abs(e.screenX-p.screenX),Math.abs(e.screenY-p.screenY));if(p.element&&!p.dragging&&10<m){if(h.fire("dragstart",{target:p.element}).isDefaultPrevented())return;p.dragging=!0,h.focus()}if(p.dragging){var g=(f=p,{pageX:(d=Gh(h,e)).pageX-f.relX,pageY:d.pageY+5});c=p.ghost,l=h.getBody(),c.parentNode!==l&&l.appendChild(c),t=p.ghost,n=g,r=p.width,o=p.height,i=p.maxX,a=p.maxY,s=u=0,t.style.left=n.pageX+"px",t.style.top=n.pageY+"px",n.pageX+r>i&&(u=n.pageX+r-i),n.pageY+o>a&&(s=n.pageY+o-a),t.style.width=r-u+"px",t.style.height=o-s+"px",v(e.clientX,e.clientY)}},o=tv(c,e),u=c,i=function(){u.dragging&&s.fire("dragend"),nv(u)},(s=e).on("mousedown",n),e.on("mousemove",r),e.on("mouseup",o),t.bind(a,"mousemove",r),t.bind(a,"mouseup",i),e.on("remove",function(){t.unbind(a,"mousemove",r),t.unbind(a,"mouseup",i)})},ov=function(e){var n;rv(e),(n=e).on("drop",function(e){var t="undefined"!=typeof e.clientX?n.getDoc().elementFromPoint(e.clientX,e.clientY):null;(Jh(t)||Jh(n.dom.getContentEditableParent(t)))&&e.preventDefault()})},iv=function(e){return X(e,function(e,t){return e.concat(function(t){var e=function(e){return W(e,function(e){return(e=za(e)).node=t,e})};if(Po.isElement(t))return e(t.getClientRects());if(Po.isText(t)){var n=t.ownerDocument.createRange();return n.setStart(t,0),n.setEnd(t,t.data.length),e(n.getClientRects())}}(t))},[])};(Xh=Kh||(Kh={}))[Xh.Up=-1]="Up",Xh[Xh.Down=1]="Down";var av=function(o,i,a,e,u,t){var n,s,c=0,l=[],r=function(e){var t,n,r;for(r=iv([e]),-1===o&&(r=r.reverse()),t=0;t<r.length;t++)if(n=r[t],!a(n,s)){if(0<l.length&&i(n,Wt.last(l))&&c++,n.line=c,u(n))return!0;l.push(n)}};return(s=Wt.last(t.getClientRects()))&&(r(n=t.getNode()),function(e,t,n,r){for(;r=hs(r,e,Ma,t);)if(n(r))return}(o,e,r,n)),l},uv=d(av,Kh.Up,Ha,ja),sv=d(av,Kh.Down,ja,Ha),cv=function(n){return function(e){return t=n,e.line>t;var t}},lv=function(n){return function(e){return t=n,e.line===t;var t}},fv=Po.isContentEditableFalse,dv=hs,mv=function(e,t){return Math.abs(e.left-t)},gv=function(e,t){return Math.abs(e.right-t)},pv=function(e,t){return e>=t.left&&e<=t.right},hv=function(e,o){return Wt.reduce(e,function(e,t){var n,r;return n=Math.min(mv(e,o),gv(e,o)),r=Math.min(mv(t,o),gv(t,o)),pv(o,t)?t:pv(o,e)?e:r===n&&fv(t.node)?t:r<n?t:e})},vv=function(e,t,n,r){for(;r=dv(r,e,Ma,t);)if(n(r))return},bv=function(e,t,n){var r,o,i,a,u,s,c,l=iv(V(oe(e.getElementsByTagName("*")),as)),f=V(l,function(e){return n>=e.top&&n<=e.bottom});return(r=hv(f,t))&&(r=hv((a=e,c=function(t,e){var n;return n=V(iv([e]),function(e){return!t(e,u)}),s=s.concat(n),0===n.length},(s=[]).push(u=r),vv(Kh.Up,a,d(c,Ha),u.node),vv(Kh.Down,a,d(c,ja),u.node),s),t))&&as(r.node)?(i=t,{node:(o=r).node,before:mv(o,i)<gv(o,i)}):null},yv=function(t,n,e){if(e.collapsed)return!1;if(ge.ie&&ge.ie<=11&&e.startOffset===e.endOffset-1&&e.startContainer===e.endContainer){var r=e.startContainer.childNodes[e.startOffset];if(Po.isElement(r))return z(r.getClientRects(),function(e){return qa(e,t,n)})}return z(e.getClientRects(),function(e){return qa(e,t,n)})},Cv=function(t){var e=Ii(function(){if(!t.removed&&t.selection.getRng().collapsed){var e=rg(t,t.selection.getRng(),!1);t.selection.setRng(e)}},0);t.on("focus",function(){e.throttle()}),t.on("blur",function(){e.cancel()})},xv={BACKSPACE:8,DELETE:46,DOWN:40,ENTER:13,LEFT:37,RIGHT:39,SPACEBAR:32,TAB:9,UP:38,END:35,HOME:36,modifierPressed:function(e){return e.shiftKey||e.ctrlKey||e.altKey||this.metaKeyPressed(e)},metaKeyPressed:function(e){return ge.mac?e.metaKey:e.ctrlKey&&!e.altKey}},wv=Po.isContentEditableTrue,Nv=Po.isContentEditableFalse,Ev=function(e,t){for(var n=e.getBody();t&&t!==n;){if(wv(t)||Nv(t))return t;t=t.parentNode}return null},Sv=function(g){var p,e,t,a=g.getBody(),o=os(g.getBody(),function(e){return g.dom.isBlock(e)},function(){return eh(g)}),h="sel-"+g.dom.uniqueId(),u=function(e){e&&g.selection.setRng(e)},s=function(){return g.selection.getRng()},v=function(e,t,n,r){return void 0===r&&(r=!0),g.fire("ShowCaret",{target:t,direction:e,before:n}).isDefaultPrevented()?null:(r&&g.selection.scrollIntoView(t,-1===e),o.show(n,t))},b=function(e,t){return t=Es(e,a,t),-1===e?Cu.fromRangeStart(t):Cu.fromRangeEnd(t)},n=function(e){return ya(e)||Ea(e)||Sa(e)},y=function(e){return n(e.startContainer)||n(e.endContainer)},c=function(e,t){var n,r,o,i,a,u,s,c,l,f,d=g.$,m=g.dom;if(!e)return null;if(e.collapsed){if(!y(e))if(!1===t){if(c=b(-1,e),as(c.getNode(!0)))return v(-1,c.getNode(!0),!1,!1);if(as(c.getNode()))return v(-1,c.getNode(),!c.isAtEnd(),!1)}else{if(c=b(1,e),as(c.getNode()))return v(1,c.getNode(),!c.isAtEnd(),!1);if(as(c.getNode(!0)))return v(1,c.getNode(!0),!1,!1)}return null}return i=e.startContainer,a=e.startOffset,u=e.endOffset,3===i.nodeType&&0===a&&Nv(i.parentNode)&&(i=i.parentNode,a=m.nodeIndex(i),i=i.parentNode),1!==i.nodeType?null:(u===a+1&&i===e.endContainer&&(n=i.childNodes[a]),Nv(n)?(l=f=n.cloneNode(!0),(s=g.fire("ObjectSelected",{target:n,targetClone:l})).isDefaultPrevented()?null:(r=Ji(ir.fromDom(g.getBody()),"#"+h).fold(function(){return d([])},function(e){return d([e.dom()])}),l=s.targetClone,0===r.length&&(r=d('<div data-mce-bogus="all" class="mce-offscreen-selection"></div>').attr("id",h)).appendTo(g.getBody()),e=g.dom.createRng(),l===f&&ge.ie?(r.empty().append('<p style="font-size: 0" data-mce-bogus="all">\xa0</p>').append(l),e.setStartAfter(r[0].firstChild.firstChild),e.setEndAfter(l)):(r.empty().append("\xa0").append(l).append("\xa0"),e.setStart(r[0].firstChild,1),e.setEnd(r[0].lastChild,0)),r.css({top:m.getPos(n,g.getBody()).y}),r[0].focus(),(o=g.selection.getSel()).removeAllRanges(),o.addRange(e),U($i(ir.fromDom(g.getBody()),"*[data-mce-selected]"),function(e){wr(e,"data-mce-selected")}),n.setAttribute("data-mce-selected","1"),p=n,C(),e)):null)},l=function(){p&&(p.removeAttribute("data-mce-selected"),Ji(ir.fromDom(g.getBody()),"#"+h).each(Oi),p=null),Ji(ir.fromDom(g.getBody()),"#"+h).each(Oi),p=null},C=function(){o.hide()};return ge.ceFalse&&(function(){g.on("mouseup",function(e){var t=s();t.collapsed&&uh(g,e.clientX,e.clientY)&&u(ng(g,t,!1))}),g.on("click",function(e){var t;(t=Ev(g,e.target))&&(Nv(t)&&(e.preventDefault(),g.focus()),wv(t)&&g.dom.isChildOf(t,g.selection.getNode())&&l())}),g.on("blur NewBlock",function(){l()}),g.on("ResizeWindow FullscreenStateChanged",function(){return o.reposition()});var n,r,i=function(e,t){var n,r,o=g.dom.getParent(e,g.dom.isBlock),i=g.dom.getParent(t,g.dom.isBlock);return!(!o||!g.dom.isChildOf(o,i)||!1!==Nv(Ev(g,o)))||o&&(n=o,r=i,!(g.dom.getParent(n,g.dom.isBlock)===g.dom.getParent(r,g.dom.isBlock)))&&function(e){var t=$s(e);if(!e.firstChild)return!1;var n=Cu.before(e.firstChild),r=t.next(n);return r&&!_f(r)&&!Df(r)}(o)};r=!1,(n=g).on("touchstart",function(){r=!1}),n.on("touchmove",function(){r=!0}),n.on("touchend",function(e){var t=Ev(n,e.target);Nv(t)&&(r||(e.preventDefault(),c(tg(n,t))))}),g.on("mousedown",function(e){var t,n=e.target;if((n===a||"HTML"===n.nodeName||g.dom.isChildOf(n,a))&&!1!==uh(g,e.clientX,e.clientY))if(t=Ev(g,n))Nv(t)?(e.preventDefault(),c(tg(g,t))):(l(),wv(t)&&e.shiftKey||yv(e.clientX,e.clientY,g.selection.getRng())||(C(),g.selection.placeCaretAt(e.clientX,e.clientY)));else if(!1===as(n)){l(),C();var r=bv(a,e.clientX,e.clientY);if(r&&!i(e.target,r.node)){e.preventDefault();var o=v(1,r.node,r.before,!1);g.getBody().focus(),u(o)}}}),g.on("keypress",function(e){xv.modifierPressed(e)||(e.keyCode,Nv(g.selection.getNode())&&e.preventDefault())}),g.on("getSelectionRange",function(e){var t=e.range;if(p){if(!p.parentNode)return void(p=null);(t=t.cloneRange()).selectNode(p),e.range=t}}),g.on("setSelectionRange",function(e){var t;(t=c(e.range,e.forward))&&(e.range=t)}),g.on("AfterSetSelectionRange",function(e){var t,n=e.range;y(n)||"mcepastebin"===n.startContainer.parentNode.id||C(),t=n.startContainer.parentNode,g.dom.hasClass(t,"mce-offscreen-selection")||l()}),g.on("copy",function(e){var t,n=e.clipboardData;if(!e.isDefaultPrevented()&&e.clipboardData&&!ge.ie){var r=(t=g.dom.get(h))?t.getElementsByTagName("*")[0]:t;r&&(e.preventDefault(),n.clearData(),n.setData("text/html",r.outerHTML),n.setData("text/plain",r.outerText))}}),ov(g),Cv(g)}(),e=g.contentStyles,t=".mce-content-body",e.push(o.getCss()),e.push(t+" .mce-offscreen-selection {position: absolute;left: -9999999999px;max-width: 1000000px;}"+t+" *[contentEditable=false] {cursor: default;}"+t+" *[contentEditable=true] {cursor: text;}")),{showCaret:v,showBlockCaretContainer:function(e){e.hasAttribute("data-mce-caret")&&(ka(e),u(s()),g.selection.scrollIntoView(e[0]))},hideFakeCaret:C,destroy:function(){o.destroy(),p=null}}},kv=function(e,t,n){var r,o,i,a,u=1;for(a=e.getShortEndedElements(),(i=/<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g).lastIndex=r=n;o=i.exec(t);){if(r=i.lastIndex,"/"===o[1])u--;else if(!o[1]){if(o[2]in a)continue;u++}if(0===u)break}return r},Tv=function(e,t){var n=e.exec(t);if(n){var r=n[1],o=n[2];return"string"==typeof r&&"data-mce-bogus"===r.toLowerCase()?o:null}return null};function Av(z,U){void 0===U&&(U=ii());var e=function(){};!1!==(z=z||{}).fix_self_closing&&(z.fix_self_closing=!0);var V=z.comment?z.comment:e,H=z.cdata?z.cdata:e,j=z.text?z.text:e,q=z.start?z.start:e,$=z.end?z.end:e,W=z.pi?z.pi:e,K=z.doctype?z.doctype:e;return{parse:function(e){var t,n,r,d,o,i,a,m,u,s,g,c,p,l,f,h,v,b,y,C,x,w,N,E,S,k,T,A,R,_=0,D=[],B=0,O=Xo.decode,P=Jt.makeMap("src,href,data,background,formaction,poster,xlink:href"),I=/((java|vb)script|mhtml):/i,L=function(e){var t,n;for(t=D.length;t--&&D[t].name!==e;);if(0<=t){for(n=D.length-1;t<=n;n--)(e=D[n]).valid&&$(e.name);D.length=t}},M=function(e,t,n,r,o){var i,a,u,s,c;if(n=(t=t.toLowerCase())in g?t:O(n||r||o||""),p&&!m&&0==(0===(u=t).indexOf("data-")||0===u.indexOf("aria-"))){if(!(i=b[t])&&y){for(a=y.length;a--&&!(i=y[a]).pattern.test(t););-1===a&&(i=null)}if(!i)return;if(i.validValues&&!(n in i.validValues))return}if(P[t]&&!z.allow_script_urls){var l=n.replace(/[\s\u0000-\u001F]+/g,"");try{l=decodeURIComponent(l)}catch(f){l=unescape(l)}if(I.test(l))return;if(c=l,!(s=z).allow_html_data_urls&&(/^data:image\//i.test(c)?!1===s.allow_svg_data_urls&&/^data:image\/svg\+xml/i.test(c):/^data:/i.test(c)))return}m&&(t in P||0===t.indexOf("on"))||(d.map[t]=n,d.push({name:t,value:n}))};for(S=new RegExp("<(?:(?:!--([\\w\\W]*?)--\x3e)|(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|(?:!DOCTYPE([\\w\\W]*?)>)|(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^\"'>]+(?:(?:\"[^\"]*\")|(?:'[^']*')|[^>]*))*|\\/|\\s+)>))","g"),k=/([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g,s=U.getShortEndedElements(),E=z.self_closing_elements||U.getSelfClosingElements(),g=U.getBoolAttrs(),p=z.validate,u=z.remove_internals,R=z.fix_self_closing,T=U.getSpecialElements(),N=e+">";t=S.exec(N);){if(_<t.index&&j(O(e.substr(_,t.index-_))),n=t[6])":"===(n=n.toLowerCase()).charAt(0)&&(n=n.substr(1)),L(n);else if(n=t[7]){if(t.index+t[0].length>e.length){j(O(e.substr(t.index))),_=t.index+t[0].length;continue}":"===(n=n.toLowerCase()).charAt(0)&&(n=n.substr(1)),c=n in s,R&&E[n]&&0<D.length&&D[D.length-1].name===n&&L(n);var F=Tv(k,t[8]);if(null!==F){if("all"===F){_=kv(U,e,S.lastIndex),S.lastIndex=_;continue}f=!1}if(!p||(l=U.getElementRule(n))){if(f=!0,p&&(b=l.attributes,y=l.attributePatterns),(v=t[8])?((m=-1!==v.indexOf("data-mce-type"))&&u&&(f=!1),(d=[]).map={},v.replace(k,M)):(d=[]).map={},p&&!m){if(C=l.attributesRequired,x=l.attributesDefault,w=l.attributesForced,l.removeEmptyAttrs&&!d.length&&(f=!1),w)for(o=w.length;o--;)a=(h=w[o]).name,"{$uid}"===(A=h.value)&&(A="mce_"+B++),d.map[a]=A,d.push({name:a,value:A});if(x)for(o=x.length;o--;)(a=(h=x[o]).name)in d.map||("{$uid}"===(A=h.value)&&(A="mce_"+B++),d.map[a]=A,d.push({name:a,value:A}));if(C){for(o=C.length;o--&&!(C[o]in d.map););-1===o&&(f=!1)}if(h=d.map["data-mce-bogus"]){if("all"===h){_=kv(U,e,S.lastIndex),S.lastIndex=_;continue}f=!1}}f&&q(n,d,c)}else f=!1;if(r=T[n]){r.lastIndex=_=t.index+t[0].length,(t=r.exec(e))?(f&&(i=e.substr(_,t.index-_)),_=t.index+t[0].length):(i=e.substr(_),_=e.length),f&&(0<i.length&&j(i,!0),$(n)),S.lastIndex=_;continue}c||(v&&v.indexOf("/")===v.length-1?f&&$(n):D.push({name:n,valid:f}))}else(n=t[1])?(">"===n.charAt(0)&&(n=" "+n),z.allow_conditional_comments||"[if"!==n.substr(0,3).toLowerCase()||(n=" "+n),V(n)):(n=t[2])?H(n.replace(/<!--|-->/g,"")):(n=t[3])?K(n):(n=t[4])&&W(n,t[5]);_=t.index+t[0].length}for(_<e.length&&j(O(e.substr(_))),o=D.length-1;0<=o;o--)(n=D[o]).valid&&$(n.name)}}}(Av||(Av={})).findEndTag=kv;var Rv=Av,_v=function(e,t){var n,r,o,i,a,u,s,c,l=t,f=/<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g,d=e.schema;for(u=e.getTempAttrs(),s=l,c=new RegExp(["\\s?("+u.join("|")+')="[^"]+"'].join("|"),"gi"),l=s.replace(c,""),a=d.getShortEndedElements();i=f.exec(l);)r=f.lastIndex,o=i[0].length,n=a[i[1]]?r:Rv.findEndTag(d,l,r),l=l.substring(0,r-o)+l.substring(n),f.lastIndex=r-o;return ga(l)},Dv={trimExternal:_v,trimInternal:_v},Bv=0,Ov=2,Pv=1,Iv=function(g,p){var e=g.length+p.length+2,h=new Array(e),v=new Array(e),c=function(e,t,n,r,o){var i=l(e,t,n,r);if(null===i||i.start===t&&i.diag===t-r||i.end===e&&i.diag===e-n)for(var a=e,u=n;a<t||u<r;)a<t&&u<r&&g[a]===p[u]?(o.push([0,g[a]]),++a,++u):r-n<t-e?(o.push([2,g[a]]),++a):(o.push([1,p[u]]),++u);else{c(e,i.start,n,i.start-i.diag,o);for(var s=i.start;s<i.end;++s)o.push([0,g[s]]);c(i.end,t,i.end-i.diag,r,o)}},b=function(e,t,n,r){for(var o=e;o-t<r&&o<n&&g[o]===p[o-t];)++o;return{start:e,end:o,diag:t}},l=function(e,t,n,r){var o=t-e,i=r-n;if(0===o||0===i)return null;var a,u,s,c,l,f=o-i,d=i+o,m=(d%2==0?d:d+1)/2;for(h[1+m]=e,v[1+m]=t+1,a=0;a<=m;++a){for(u=-a;u<=a;u+=2){for(s=u+m,u===-a||u!==a&&h[s-1]<h[s+1]?h[s]=h[s+1]:h[s]=h[s-1]+1,l=(c=h[s])-e+n-u;c<t&&l<r&&g[c]===p[l];)h[s]=++c,++l;if(f%2!=0&&f-a<=u&&u<=f+a&&v[s-f]<=h[s])return b(v[s-f],u+e-n,t,r)}for(u=f-a;u<=f+a;u+=2){for(s=u+m-f,u===f-a||u!==f+a&&v[s+1]<=v[s-1]?v[s]=v[s+1]-1:v[s]=v[s-1],l=(c=v[s]-1)-e+n-u;e<=c&&n<=l&&g[c]===p[l];)v[s]=c--,l--;if(f%2==0&&-a<=u&&u<=a&&v[s]<=h[s+f])return b(v[s],u+e-n,t,r)}}},t=[];return c(0,g.length,0,p.length,t),t},Lv=function(e){return Po.isElement(e)?e.outerHTML:Po.isText(e)?Xo.encodeRaw(e.data,!1):Po.isComment(e)?"\x3c!--"+e.data+"--\x3e":""},Mv=function(e,t,n){var r=function(e){var t,n,r;for(r=H.document.createElement("div"),t=H.document.createDocumentFragment(),e&&(r.innerHTML=e);n=r.firstChild;)t.appendChild(n);return t}(t);if(e.hasChildNodes()&&n<e.childNodes.length){var o=e.childNodes[n];o.parentNode.insertBefore(r,o)}else e.appendChild(r)},Fv=function(e){return V(W(oe(e.childNodes),Lv),function(e){return 0<e.length})},zv=function(e,t){var n,r,o,i=W(oe(t.childNodes),Lv);return n=Iv(i,e),r=t,o=0,U(n,function(e){e[0]===Bv?o++:e[0]===Pv?(Mv(r,e[1],o),o++):e[0]===Ov&&function(e,t){if(e.hasChildNodes()&&t<e.childNodes.length){var n=e.childNodes[t];n.parentNode.removeChild(n)}}(r,o)}),t},Uv=Li(A.none()),Vv=function(e){return{type:"fragmented",fragments:e,content:"",bookmark:null,beforeBookmark:null}},Hv=function(e){return{type:"complete",fragments:null,content:e,bookmark:null,beforeBookmark:null}},jv=function(e){return"fragmented"===e.type?e.fragments.join(""):e.content},qv=function(e){var t=ir.fromTag("body",Uv.get().getOrThunk(function(){var e=H.document.implementation.createHTMLDocument("undo");return Uv.set(A.some(e)),e}));return la(t,jv(e)),U($i(t,"*[data-mce-bogus]"),Pi),t.dom().innerHTML},$v=function(n){var e,t,r;return e=Fv(n.getBody()),-1!==(t=(r=Z(e,function(e){var t=Dv.trimInternal(n.serializer,e);return 0<t.length?[t]:[]})).join("")).indexOf("</iframe>")?Vv(r):Hv(t)},Wv=function(e,t,n){"fragmented"===t.type?zv(t.fragments,e.getBody()):e.setContent(t.content,{format:"raw"}),e.selection.moveToBookmark(n?t.beforeBookmark:t.bookmark)},Kv=function(e,t){return!(!e||!t)&&(r=t,jv(e)===jv(r)||(n=t,qv(e)===qv(n)));var n,r};function Xv(u){var s,r,o=this,c=0,l=[],t=0,f=function(){return 0===t},i=function(e){f()&&(o.typing=e)},d=function(e){u.setDirty(e)},a=function(e){i(!1),o.add({},e)},n=function(){o.typing&&(i(!1),o.add())};return u.on("init",function(){o.add()}),u.on("BeforeExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&(n(),o.beforeChange())}),u.on("ExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&a(e)}),u.on("ObjectResizeStart Cut",function(){o.beforeChange()}),u.on("SaveContent ObjectResized blur",a),u.on("DragEnd",a),u.on("KeyUp",function(e){var t=e.keyCode;e.isDefaultPrevented()||((33<=t&&t<=36||37<=t&&t<=40||45===t||e.ctrlKey)&&(a(),u.nodeChanged()),46!==t&&8!==t||u.nodeChanged(),r&&o.typing&&!1===Kv($v(u),l[0])&&(!1===u.isDirty()&&(d(!0),u.fire("change",{level:l[0],lastLevel:null})),u.fire("TypingUndo"),r=!1,u.nodeChanged()))}),u.on("KeyDown",function(e){var t=e.keyCode;if(!e.isDefaultPrevented())if(33<=t&&t<=36||37<=t&&t<=40||45===t)o.typing&&a(e);else{var n=e.ctrlKey&&!e.altKey||e.metaKey;!(t<16||20<t)||224===t||91===t||o.typing||n||(o.beforeChange(),i(!0),o.add({},e),r=!0)}}),u.on("MouseDown",function(e){o.typing&&a(e)}),u.on("input",function(e){var t;e.inputType&&("insertReplacementText"===e.inputType||"insertText"===(t=e).inputType&&null===t.data)&&a(e)}),u.addShortcut("meta+z","","Undo"),u.addShortcut("meta+y,meta+shift+z","","Redo"),u.on("AddUndo Undo Redo ClearUndos",function(e){e.isDefaultPrevented()||u.nodeChanged()}),o={data:l,typing:!1,beforeChange:function(){f()&&(s=Vu.getUndoBookmark(u.selection))},add:function(e,t){var n,r,o,i=u.settings;if(o=$v(u),e=e||{},e=Jt.extend(e,o),!1===f()||u.removed)return null;if(r=l[c],u.fire("BeforeAddUndo",{level:e,lastLevel:r,originalEvent:t}).isDefaultPrevented())return null;if(r&&Kv(r,e))return null;if(l[c]&&(l[c].beforeBookmark=s),i.custom_undo_redo_levels&&l.length>i.custom_undo_redo_levels){for(n=0;n<l.length-1;n++)l[n]=l[n+1];l.length--,c=l.length}e.bookmark=Vu.getUndoBookmark(u.selection),c<l.length-1&&(l.length=c+1),l.push(e),c=l.length-1;var a={level:e,lastLevel:r,originalEvent:t};return u.fire("AddUndo",a),0<c&&(d(!0),u.fire("change",a)),e},undo:function(){var e;return o.typing&&(o.add(),o.typing=!1,i(!1)),0<c&&(e=l[--c],Wv(u,e,!0),d(!0),u.fire("undo",{level:e})),e},redo:function(){var e;return c<l.length-1&&(e=l[++c],Wv(u,e,!1),d(!0),u.fire("redo",{level:e})),e},clear:function(){l=[],c=0,o.typing=!1,o.data=l,u.fire("ClearUndos")},hasUndo:function(){return 0<c||o.typing&&l[0]&&!Kv($v(u),l[0])},hasRedo:function(){return c<l.length-1&&!o.typing},transact:function(e){return n(),o.beforeChange(),o.ignore(e),o.add()},ignore:function(e){try{t++,e()}finally{t--}},extra:function(e,t){var n,r;o.transact(e)&&(r=l[c].bookmark,n=l[c-1],Wv(u,n,!0),o.transact(t)&&(l[c-1].beforeBookmark=r))}}}var Yv,Gv,Jv={},Qv=Wt.filter,Zv=Wt.each;Gv=function(e){var t,n,r=e.selection.getRng();t=Po.matchNodeNames("pre"),r.collapsed||(n=e.selection.getSelectedBlocks(),Zv(Qv(Qv(n,t),function(e){return t(e.previousSibling)&&-1!==Wt.indexOf(n,e.previousSibling)}),function(e){var t,n;t=e.previousSibling,vn(n=e).remove(),vn(t).append("<br><br>").append(n.childNodes)}))},Jv[Yv="pre"]||(Jv[Yv]=[]),Jv[Yv].push(Gv);var eb=function(e,t){Zv(Jv[e],function(e){e(t)})},tb=/^(src|href|style)$/,nb=Jt.each,rb=hc.isEq,ob=function(e,t,n){return e.isChildOf(t,n)&&t!==n&&!e.isBlock(n)},ib=function(e,t,n){var r,o,i;return r=t[n?"startContainer":"endContainer"],o=t[n?"startOffset":"endOffset"],Po.isElement(r)&&(i=r.childNodes.length-1,!n&&o&&o--,r=r.childNodes[i<o?i:o]),Po.isText(r)&&n&&o>=r.nodeValue.length&&(r=new ao(r,e.getBody()).next()||r),Po.isText(r)&&!n&&0===o&&(r=new ao(r,e.getBody()).prev()||r),r},ab=function(e,t,n,r){var o=e.create(n,r);return t.parentNode.insertBefore(o,t),o.appendChild(t),o},ub=function(e,t,n,r,o){var i=ir.fromDom(t),a=ir.fromDom(e.create(r,o)),u=n?jr(i):Hr(i);return Di(a,u),n?(Ti(i,a),Ri(a,i)):(Ai(i,a),_i(a,i)),a.dom()},sb=function(e,t,n,r){return!(t=hc.getNonWhiteSpaceSibling(t,n,r))||"BR"===t.nodeName||e.isBlock(t)},cb=function(e,n,r,o,i){var t,a,u,s,c,l,f,d,m,g,p,h,v,b,y=e.dom;if(c=y,!(rb(l=o,(f=n).inline)||rb(l,f.block)||(f.selector?Po.isElement(l)&&c.is(l,f.selector):void 0)||(s=o,n.links&&"A"===s.tagName)))return!1;if("all"!==n.remove)for(nb(n.styles,function(e,t){e=hc.normalizeStyleValue(y,hc.replaceVars(e,r),t),"number"==typeof t&&(t=e,i=0),(n.remove_similar||!i||rb(hc.getStyle(y,i,t),e))&&y.setStyle(o,t,""),u=1}),u&&""===y.getAttrib(o,"style")&&(o.removeAttribute("style"),o.removeAttribute("data-mce-style")),nb(n.attributes,function(e,t){var n;if(e=hc.replaceVars(e,r),"number"==typeof t&&(t=e,i=0),!i||rb(y.getAttrib(i,t),e)){if("class"===t&&(e=y.getAttrib(o,t))&&(n="",nb(e.split(/\s+/),function(e){/mce\-\w+/.test(e)&&(n+=(n?" ":"")+e)}),n))return void y.setAttrib(o,t,n);"class"===t&&o.removeAttribute("className"),tb.test(t)&&o.removeAttribute("data-mce-"+t),o.removeAttribute(t)}}),nb(n.classes,function(e){e=hc.replaceVars(e,r),i&&!y.hasClass(i,e)||y.removeClass(o,e)}),a=y.getAttribs(o),t=0;t<a.length;t++){var C=a[t].nodeName;if(0!==C.indexOf("_")&&0!==C.indexOf("data-"))return!1}return"none"!==n.remove?(d=e,g=n,h=(m=o).parentNode,v=d.dom,b=d.settings.forced_root_block,g.block&&(b?h===v.getRoot()&&(g.list_block&&rb(m,g.list_block)||nb(Jt.grep(m.childNodes),function(e){hc.isValid(d,b,e.nodeName.toLowerCase())?p?p.appendChild(e):(p=ab(v,e,b),v.setAttribs(p,d.settings.forced_root_block_attrs)):p=0})):v.isBlock(m)&&!v.isBlock(h)&&(sb(v,m,!1)||sb(v,m.firstChild,!0,1)||m.insertBefore(v.create("br"),m.firstChild),sb(v,m,!0)||sb(v,m.lastChild,!1,1)||m.appendChild(v.create("br")))),g.selector&&g.inline&&!rb(g.inline,m)||v.remove(m,1),!0):void 0},lb=cb,fb=function(s,c,l,e,f){var t,n,d=s.formatter.get(c),m=d[0],a=!0,u=s.dom,r=s.selection,i=function(e){var n,t,r,o,i,a,u=(n=s,t=e,r=c,o=l,i=f,nb(hc.getParents(n.dom,t.parentNode).reverse(),function(e){var t;a||"_start"===e.id||"_end"===e.id||(t=Om.matchNode(n,e,r,o,i))&&!1!==t.split&&(a=e)}),a);return function(e,t,n,r,o,i,a,u){var s,c,l,f,d,m,g=e.dom;if(n){for(m=n.parentNode,s=r.parentNode;s&&s!==m;s=s.parentNode){for(c=g.clone(s,!1),d=0;d<t.length;d++)if(cb(e,t[d],u,c,c)){c=0;break}c&&(l&&c.appendChild(l),f||(f=c),l=c)}!i||a.mixed&&g.isBlock(n)||(r=g.split(n,r)),l&&(o.parentNode.insertBefore(l,o),f.appendChild(o))}return r}(s,d,u,e,e,!0,m,l)},g=function(e){var t,n,r,o,i;if(Po.isElement(e)&&u.getContentEditable(e)&&(o=a,a="true"===u.getContentEditable(e),i=!0),t=Jt.grep(e.childNodes),a&&!i)for(n=0,r=d.length;n<r&&!cb(s,d[n],l,e,e);n++);if(m.deep&&t.length){for(n=0,r=t.length;n<r;n++)g(t[n]);i&&(a=o)}},p=function(e){var t,n=u.get(e?"_start":"_end"),r=n[e?"firstChild":"lastChild"];return dc(t=r)&&Po.isElement(t)&&("_start"===t.id||"_end"===t.id)&&(r=r[e?"firstChild":"lastChild"]),Po.isText(r)&&0===r.data.length&&(r=e?n.previousSibling||n.nextSibling:n.nextSibling||n.previousSibling),u.remove(n,!0),r},o=function(e){var t,n,r=e.commonAncestorContainer;if(e=Ac(s,e,d,!0),m.split){if(e=Im(e),(t=ib(s,e,!0))!==(n=ib(s,e))){if(/^(TR|TH|TD)$/.test(t.nodeName)&&t.firstChild&&(t="TR"===t.nodeName?t.firstChild.firstChild||t:t.firstChild||t),r&&/^T(HEAD|BODY|FOOT|R)$/.test(r.nodeName)&&/^(TH|TD)$/.test(n.nodeName)&&n.firstChild&&(n=n.firstChild||n),ob(u,t,n)){var o=A.from(t.firstChild).getOr(t);return i(ub(u,o,!0,"span",{id:"_start","data-mce-type":"bookmark"})),void p(!0)}if(ob(u,n,t))return o=A.from(n.lastChild).getOr(n),i(ub(u,o,!1,"span",{id:"_end","data-mce-type":"bookmark"})),void p(!1);t=ab(u,t,"span",{id:"_start","data-mce-type":"bookmark"}),n=ab(u,n,"span",{id:"_end","data-mce-type":"bookmark"}),i(t),i(n),t=p(!0),n=p()}else t=n=i(t);e.startContainer=t.parentNode?t.parentNode:t,e.startOffset=u.nodeIndex(t),e.endContainer=n.parentNode?n.parentNode:n,e.endOffset=u.nodeIndex(n)+1}_c(u,e,function(e){nb(e,function(e){g(e),Po.isElement(e)&&"underline"===s.dom.getStyle(e,"text-decoration")&&e.parentNode&&"underline"===hc.getTextDecoration(u,e.parentNode)&&cb(s,{deep:!1,exact:!0,inline:"span",styles:{textDecoration:"underline"}},null,e)})})};if(e)e.nodeType?((n=u.createRng()).setStartBefore(e),n.setEndAfter(e),o(n)):o(e);else if("false"!==u.getContentEditable(r.getNode()))r.isCollapsed()&&m.inline&&!u.select("td[data-mce-selected],th[data-mce-selected]").length?function(e,t,n,r){var o,i,a,u,s,c,l,f=e.dom,d=e.selection,m=[],g=d.getRng();for(o=g.startContainer,i=g.startOffset,3===(s=o).nodeType&&(i!==o.nodeValue.length&&(u=!0),s=s.parentNode);s;){if(Om.matchNode(e,s,t,n,r)){c=s;break}s.nextSibling&&(u=!0),m.push(s),s=s.parentNode}if(c)if(u){a=d.getBookmark(),g.collapse(!0);var p=Ac(e,g,e.formatter.get(t),!0);p=Im(p),e.formatter.remove(t,n,p),d.moveToBookmark(a)}else{l=qu(e.getBody(),c);var h=Um(!1).dom(),v=$m(m,h);jm(e,h,l||c),Vm(e,l,!1),d.setCursorLocation(v,1),f.isEmpty(c)&&f.remove(c)}}(s,c,l,f):(t=Vu.getPersistentBookmark(s.selection,!0),o(r.getRng()),r.moveToBookmark(t),m.inline&&Om.match(s,c,l,r.getStart())&&hc.moveStart(u,r,r.getRng()),s.nodeChanged());else{e=r.getNode();for(var h=0,v=d.length;h<v&&(!d[h].ceFalseOverride||!cb(s,d[h],l,e,e));h++);}},db=Jt.each,mb=function(e){return e&&1===e.nodeType&&!dc(e)&&!ju(e)&&!Po.isBogus(e)},gb=function(e,t){var n;for(n=e;n;n=n[t]){if(3===n.nodeType&&0!==n.nodeValue.length)return e;if(1===n.nodeType&&!dc(n))return n}return e},pb=function(e,t,n){var r,o,i=new Xc(e);if(t&&n&&(t=gb(t,"previousSibling"),n=gb(n,"nextSibling"),i.compare(t,n))){for(r=t.nextSibling;r&&r!==n;)r=(o=r).nextSibling,t.appendChild(o);return e.remove(n),Jt.each(Jt.grep(n.childNodes),function(e){t.appendChild(e)}),t}return n},hb=function(e,t,n){db(e.childNodes,function(e){mb(e)&&(t(e)&&n(e),e.hasChildNodes()&&hb(e,t,n))})},vb=function(n,e){return d(function(e,t){return!(!t||!hc.getStyle(n,t,e))},e)},bb=function(r,e,t){return d(function(e,t,n){r.setStyle(n,e,t),""===n.getAttribute("style")&&n.removeAttribute("style"),yb(r,n)},e,t)},yb=function(e,t){"SPAN"===t.nodeName&&0===e.getAttribs(t).length&&e.remove(t,!0)},Cb=function(e,t){var n;1===t.nodeType&&t.parentNode&&1===t.parentNode.nodeType&&(n=hc.getTextDecoration(e,t.parentNode),e.getStyle(t,"color")&&n?e.setStyle(t,"text-decoration",n):e.getStyle(t,"text-decoration")===n&&e.setStyle(t,"text-decoration",null))},xb=function(n,e,r,o){db(e,function(t){db(n.dom.select(t.inline,o),function(e){mb(e)&&lb(n,t,r,e,t.exact?e:null)}),function(r,e,t){if(e.clear_child_styles){var n=e.links?"*:not(a)":"*";db(r.select(n,t),function(n){mb(n)&&db(e.styles,function(e,t){r.setStyle(n,t,"")})})}}(n.dom,t,o)})},wb=function(e,t,n,r){(t.styles.color||t.styles.textDecoration)&&(Jt.walk(r,d(Cb,e),"childNodes"),Cb(e,r))},Nb=function(e,t,n,r){t.styles&&t.styles.backgroundColor&&hb(r,vb(e,"fontSize"),bb(e,"backgroundColor",hc.replaceVars(t.styles.backgroundColor,n)))},Eb=function(e,t,n,r){"sub"!==t.inline&&"sup"!==t.inline||(hb(r,vb(e,"fontSize"),bb(e,"fontSize","")),e.remove(e.select("sup"===t.inline?"sub":"sup",r),!0))},Sb=function(e,t,n,r){r&&!1!==t.merge_siblings&&(r=pb(e,hc.getNonWhiteSpaceSibling(r),r),r=pb(e,r,hc.getNonWhiteSpaceSibling(r,!0)))},kb=function(t,n,r,o,i){Om.matchNode(t,i.parentNode,r,o)&&lb(t,n,o,i)||n.merge_with_parents&&t.dom.getParent(i.parentNode,function(e){if(Om.matchNode(t,e,r,o))return lb(t,n,o,i),!0})},Tb=Jt.each,Ab=function(g,p,h,r){var e,t,v=g.formatter.get(p),b=v[0],o=!r&&g.selection.isCollapsed(),i=g.dom,n=g.selection,y=function(n,e){if(e=e||b,n){if(e.onformat&&e.onformat(n,e,h,r),Tb(e.styles,function(e,t){i.setStyle(n,t,hc.replaceVars(e,h))}),e.styles){var t=i.getAttrib(n,"style");t&&n.setAttribute("data-mce-style",t)}Tb(e.attributes,function(e,t){i.setAttrib(n,t,hc.replaceVars(e,h))}),Tb(e.classes,function(e){e=hc.replaceVars(e,h),i.hasClass(n,e)||i.addClass(n,e)})}},C=function(e,t){var n=!1;return!!b.selector&&(Tb(e,function(e){if(!("collapsed"in e&&e.collapsed!==o))return i.is(t,e.selector)&&!ju(t)?(y(t,e),!(n=!0)):void 0}),n)},a=function(s,e,t,c){var l,f,d=[],m=!0;l=b.inline||b.block,f=s.create(l),y(f),_c(s,e,function(e){var a,u=function(e){var t,n,r,o;if(o=m,t=e.nodeName.toLowerCase(),n=e.parentNode.nodeName.toLowerCase(),1===e.nodeType&&s.getContentEditable(e)&&(o=m,m="true"===s.getContentEditable(e),r=!0),hc.isEq(t,"br"))return a=0,void(b.block&&s.remove(e));if(b.wrapper&&Om.matchNode(g,e,p,h))a=0;else{if(m&&!r&&b.block&&!b.wrapper&&hc.isTextBlock(g,t)&&hc.isValid(g,n,l))return e=s.rename(e,l),y(e),d.push(e),void(a=0);if(b.selector){var i=C(v,e);if(!b.inline||i)return void(a=0)}!m||r||!hc.isValid(g,l,t)||!hc.isValid(g,n,l)||!c&&3===e.nodeType&&1===e.nodeValue.length&&65279===e.nodeValue.charCodeAt(0)||ju(e)||b.inline&&s.isBlock(e)?(a=0,Tb(Jt.grep(e.childNodes),u),r&&(m=o),a=0):(a||(a=s.clone(f,!1),e.parentNode.insertBefore(a,e),d.push(a)),a.appendChild(e))}};Tb(e,u)}),!0===b.links&&Tb(d,function(e){var t=function(e){"A"===e.nodeName&&y(e,b),Tb(Jt.grep(e.childNodes),t)};t(e)}),Tb(d,function(e){var t,n,r,o,i,a=function(e){var n=!1;return Tb(e.childNodes,function(e){if((t=e)&&1===t.nodeType&&!dc(t)&&!ju(t)&&!Po.isBogus(t))return n=e,!1;var t}),n};n=0,Tb(e.childNodes,function(e){hc.isWhiteSpaceNode(e)||dc(e)||n++}),t=n,!(1<d.length)&&s.isBlock(e)||0!==t?(b.inline||b.wrapper)&&(b.exact||1!==t||((o=a(r=e))&&!dc(o)&&Om.matchName(s,o,b)&&(i=s.clone(o,!1),y(i),s.replace(i,r,!0),s.remove(o,1)),e=i||r),xb(g,v,h,e),kb(g,b,p,h,e),Nb(s,b,h,e),Eb(s,b,h,e),Sb(s,b,h,e)):s.remove(e,1)})};if("false"!==i.getContentEditable(n.getNode())){if(b){if(r)r.nodeType?C(v,r)||((t=i.createRng()).setStartBefore(r),t.setEndAfter(r),a(i,Ac(g,t,v),0,!0)):a(i,r,0,!0);else if(o&&b.inline&&!i.select("td[data-mce-selected],th[data-mce-selected]").length)!function(e,t,n){var r,o,i,a,u,s,c=e.selection;a=(r=c.getRng(!0)).startOffset,s=r.startContainer.nodeValue,(o=qu(e.getBody(),c.getStart()))&&(i=zm(o));var l,f,d=/[^\s\u00a0\u00ad\u200b\ufeff]/;s&&0<a&&a<s.length&&d.test(s.charAt(a))&&d.test(s.charAt(a-1))?(u=c.getBookmark(),r.collapse(!0),r=Ac(e,r,e.formatter.get(t)),r=Im(r),e.formatter.apply(t,n,r),c.moveToBookmark(u)):(o&&i.nodeValue===Lm||(l=e.getDoc(),f=Um(!0).dom(),i=(o=l.importNode(f,!0)).firstChild,r.insertNode(o),a=1),e.formatter.apply(t,n,o),c.setCursorLocation(i,a))}(g,p,h);else{var u=g.selection.getNode();g.settings.forced_root_block||!v[0].defaultBlock||i.getParent(u,i.isBlock)||Ab(g,v[0].defaultBlock),g.selection.setRng(rl(g.selection.getRng())),e=Vu.getPersistentBookmark(g.selection,!0),a(i,Ac(g,n.getRng(),v)),b.styles&&wb(i,b,h,u),n.moveToBookmark(e),hc.moveStart(i,n,n.getRng()),g.nodeChanged()}eb(p,g)}}else{r=n.getNode();for(var s=0,c=v.length;s<c;s++)if(v[s].ceFalseOverride&&i.is(r,v[s].selector))return void y(r,v[s])}},Rb={applyFormat:Ab},_b=Jt.each,Db=function(e,t,n,r,o){var i,a,u,s,c,l,f,d;null===t.get()&&(a=e,u={},(i=t).set({}),a.on("NodeChange",function(n){var r=hc.getParents(a.dom,n.element),o={};r=Jt.grep(r,function(e){return 1===e.nodeType&&!e.getAttribute("data-mce-bogus")}),_b(i.get(),function(e,n){_b(r,function(t){return a.formatter.matchNode(t,n,{},e.similar)?(u[n]||(_b(e,function(e){e(!0,{node:t,format:n,parents:r})}),u[n]=e),o[n]=e,!1):!Om.matchesUnInheritedFormatSelector(a,t,n)&&void 0})}),_b(u,function(e,t){o[t]||(delete u[t],_b(e,function(e){e(!1,{node:n.element,format:t,parents:r})}))})})),c=n,l=r,f=o,d=(s=t).get(),_b(c.split(","),function(e){d[e]||(d[e]=[],d[e].similar=f),d[e].push(l)}),s.set(d)},Bb={get:function(r){var t={valigntop:[{selector:"td,th",styles:{verticalAlign:"top"}}],valignmiddle:[{selector:"td,th",styles:{verticalAlign:"middle"}}],valignbottom:[{selector:"td,th",styles:{verticalAlign:"bottom"}}],alignleft:[{selector:"figure.image",collapsed:!1,classes:"align-left",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"left"},inherit:!1,preview:!1,defaultBlock:"div"},{selector:"img,table",collapsed:!1,styles:{"float":"left"},preview:"font-family font-size"}],aligncenter:[{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"center"},inherit:!1,preview:"font-family font-size",defaultBlock:"div"},{selector:"figure.image",collapsed:!1,classes:"align-center",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"img",collapsed:!1,styles:{display:"block",marginLeft:"auto",marginRight:"auto"},preview:!1},{selector:"table",collapsed:!1,styles:{marginLeft:"auto",marginRight:"auto"},preview:"font-family font-size"}],alignright:[{selector:"figure.image",collapsed:!1,classes:"align-right",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"right"},inherit:!1,preview:"font-family font-size",defaultBlock:"div"},{selector:"img,table",collapsed:!1,styles:{"float":"right"},preview:"font-family font-size"}],alignjustify:[{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"justify"},inherit:!1,defaultBlock:"div",preview:"font-family font-size"}],bold:[{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}},{inline:"b",remove:"all"}],italic:[{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}},{inline:"i",remove:"all"}],underline:[{inline:"span",styles:{textDecoration:"underline"},exact:!0},{inline:"u",remove:"all"}],strikethrough:[{inline:"span",styles:{textDecoration:"line-through"},exact:!0},{inline:"strike",remove:"all"}],forecolor:{inline:"span",styles:{color:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0},hilitecolor:{inline:"span",styles:{backgroundColor:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0},fontname:{inline:"span",toggle:!1,styles:{fontFamily:"%value"},clear_child_styles:!0},fontsize:{inline:"span",toggle:!1,styles:{fontSize:"%value"},clear_child_styles:!0},fontsize_class:{inline:"span",attributes:{"class":"%value"}},blockquote:{block:"blockquote",wrapper:1,remove:"all"},subscript:{inline:"sub"},superscript:{inline:"sup"},code:{inline:"code"},link:{inline:"a",selector:"a",remove:"all",split:!0,deep:!0,onmatch:function(){return!0},onformat:function(n,e,t){Jt.each(t,function(e,t){r.setAttrib(n,t,e)})}},removeformat:[{selector:"b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins",remove:"all",split:!0,expand:!1,block_expand:!0,deep:!0},{selector:"span",attributes:["style","class"],remove:"empty",split:!0,expand:!1,deep:!0},{selector:"*",attributes:["style","class"],split:!1,expand:!1,deep:!0}]};return Jt.each("p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp".split(/\s/),function(e){t[e]={block:e,remove:"all"}}),t}},Ob=Jt.each,Pb=bi.DOM,Ib=function(e,t){var n,o,r,m=t&&t.schema||ii({}),g=function(e){var t,n,r;return o="string"==typeof e?{name:e,classes:[],attrs:{}}:e,t=Pb.create(o.name),n=t,(r=o).classes.length&&Pb.addClass(n,r.classes.join(" ")),Pb.setAttribs(n,r.attrs),t},p=function(n,e,t){var r,o,i,a,u,s,c,l,f=0<e.length&&e[0],d=f&&f.name;if(u=d,s="string"!=typeof(a=n)?a.nodeName.toLowerCase():a,c=m.getElementRule(s),i=!(!(l=c&&c.parentsRequired)||!l.length)&&(u&&-1!==Jt.inArray(l,u)?u:l[0]))d===i?(o=e[0],e=e.slice(1)):o=i;else if(f)o=e[0],e=e.slice(1);else if(!t)return n;return o&&(r=g(o)).appendChild(n),t&&(r||(r=Pb.create("div")).appendChild(n),Jt.each(t,function(e){var t=g(e);r.insertBefore(t,n)})),p(r,e,o&&o.siblings)};return e&&e.length?(o=e[0],n=g(o),(r=Pb.create("div")).appendChild(p(n,e.slice(1),o.siblings)),r):""},Lb=function(e){var t,a={classes:[],attrs:{}};return"*"!==(e=a.selector=Jt.trim(e))&&(t=e.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g,function(e,t,n,r,o){switch(t){case"#":a.attrs.id=n;break;case".":a.classes.push(n);break;case":":-1!==Jt.inArray("checked disabled enabled read-only required".split(" "),n)&&(a.attrs[n]=n)}if("["===r){var i=o.match(/([\w\-]+)(?:\=\"([^\"]+))?/);i&&(a.attrs[i[1]]=i[2])}return""})),a.name=t||"div",a},Mb=function(e){return e&&"string"==typeof e?(e=(e=e.split(/\s*,\s*/)[0]).replace(/\s*(~\+|~|\+|>)\s*/g,"$1"),Jt.map(e.split(/(?:>|\s+(?![^\[\]]+\]))/),function(e){var t=Jt.map(e.split(/(?:~\+|~|\+)/),Lb),n=t.pop();return t.length&&(n.siblings=t),n}).reverse()):[]},Fb=function(n,e){var t,r,o,i,a,u,s="";if(!1===(u=n.settings.preview_styles))return"";"string"!=typeof u&&(u="font-family font-size font-weight font-style text-decoration text-transform color background-color border border-radius outline text-shadow");var c=function(e){return e.replace(/%(\w+)/g,"")};if("string"==typeof e){if(!(e=n.formatter.get(e)))return;e=e[0]}return"preview"in e&&!1===(u=e.preview)?"":(t=e.block||e.inline||"span",(i=Mb(e.selector)).length?(i[0].name||(i[0].name=t),t=e.selector,r=Ib(i,n)):r=Ib([t],n),o=Pb.select(t,r)[0]||r.firstChild,Ob(e.styles,function(e,t){(e=c(e))&&Pb.setStyle(o,t,e)}),Ob(e.attributes,function(e,t){(e=c(e))&&Pb.setAttrib(o,t,e)}),Ob(e.classes,function(e){e=c(e),Pb.hasClass(o,e)||Pb.addClass(o,e)}),n.fire("PreviewFormats"),Pb.setStyles(r,{position:"absolute",left:-65535}),n.getBody().appendChild(r),a=Pb.getStyle(n.getBody(),"fontSize",!0),a=/px$/.test(a)?parseInt(a,10):0,Ob(u.split(" "),function(e){var t=Pb.getStyle(o,e,!0);if(!("background-color"===e&&/transparent|rgba\s*\([^)]+,\s*0\)/.test(t)&&(t=Pb.getStyle(n.getBody(),e,!0),"#ffffff"===Pb.toHex(t).toLowerCase())||"color"===e&&"#000000"===Pb.toHex(t).toLowerCase())){if("font-size"===e&&/em|%$/.test(t)){if(0===a)return;t=parseFloat(t)/(/%$/.test(t)?100:1)*a+"px"}"border"===e&&t&&(s+="padding:0 2px;"),s+=e+":"+t+";"}}),n.fire("AfterPreviewFormats"),Pb.remove(r),s)},zb=function(e,t,n,r,o){var i=t.get(n);!Om.match(e,n,r,o)||"toggle"in i[0]&&!i[0].toggle?Rb.applyFormat(e,n,r,o):fb(e,n,r,o)},Ub=function(e){e.addShortcut("meta+b","","Bold"),e.addShortcut("meta+i","","Italic"),e.addShortcut("meta+u","","Underline");for(var t=1;t<=6;t++)e.addShortcut("access+"+t,"",["FormatBlock",!1,"h"+t]);e.addShortcut("access+7","",["FormatBlock",!1,"p"]),e.addShortcut("access+8","",["FormatBlock",!1,"div"]),e.addShortcut("access+9","",["FormatBlock",!1,"address"])};function Vb(e){var t,n,r,o=(t=e,n={},(r=function(e,t){e&&("string"!=typeof e?Jt.each(e,function(e,t){r(t,e)}):(t=t.length?t:[t],Jt.each(t,function(e){"undefined"==typeof e.deep&&(e.deep=!e.selector),"undefined"==typeof e.split&&(e.split=!e.selector||e.inline),"undefined"==typeof e.remove&&e.selector&&!e.inline&&(e.remove="none"),e.selector&&e.inline&&(e.mixed=!0,e.block_expand=!0),"string"==typeof e.classes&&(e.classes=e.classes.split(/\s+/))}),n[e]=t))})(Bb.get(t.dom)),r(t.settings.formats),{get:function(e){return e?n[e]:n},register:r,unregister:function(e){return e&&n[e]&&delete n[e],n}}),i=Li(null);return Ub(e),Wm(e),{get:o.get,register:o.register,unregister:o.unregister,apply:d(Rb.applyFormat,e),remove:d(fb,e),toggle:d(zb,e,o),match:d(Om.match,e),matchAll:d(Om.matchAll,e),matchNode:d(Om.matchNode,e),canApply:d(Om.canApply,e),formatChanged:d(Db,e,i),getCssText:d(Fb,e)}}var Hb,jb=Object.prototype.hasOwnProperty,qb=(Hb=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t<e.length;t++)e[t]=arguments[t];if(0===e.length)throw new Error("Can't merge zero objects");for(var n={},r=0;r<e.length;r++){var o=e[r];for(var i in o)jb.call(o,i)&&(n[i]=Hb(n[i],o[i]))}return n}),$b={register:function(t,s,c){t.addAttributeFilter("data-mce-tabindex",function(e,t){for(var n,r=e.length;r--;)(n=e[r]).attr("tabindex",n.attributes.map["data-mce-tabindex"]),n.attr(t,null)}),t.addAttributeFilter("src,href,style",function(e,t){for(var n,r,o=e.length,i="data-mce-"+t,a=s.url_converter,u=s.url_converter_scope;o--;)(r=(n=e[o]).attributes.map[i])!==undefined?(n.attr(t,0<r.length?r:null),n.attr(i,null)):(r=n.attributes.map[t],"style"===t?r=c.serializeStyle(c.parseStyle(r),n.name):a&&(r=a.call(u,r,t,n.name)),n.attr(t,0<r.length?r:null))}),t.addAttributeFilter("class",function(e){for(var t,n,r=e.length;r--;)(n=(t=e[r]).attr("class"))&&(n=t.attr("class").replace(/(?:^|\s)mce-item-\w+(?!\S)/g,""),t.attr("class",0<n.length?n:null))}),t.addAttributeFilter("data-mce-type",function(e,t,n){for(var r,o=e.length;o--;)"bookmark"!==(r=e[o]).attributes.map["data-mce-type"]||n.cleanup||(A.from(r.firstChild).exists(function(e){return!da(e.value)})?r.unwrap():r.remove())}),t.addNodeFilter("noscript",function(e){for(var t,n=e.length;n--;)(t=e[n].firstChild)&&(t.value=Xo.decode(t.value))}),t.addNodeFilter("script,style",function(e,t){for(var n,r,o,i=e.length,a=function(e){return e.replace(/(<!--\[CDATA\[|\]\]-->)/g,"\n").replace(/^[\r\n]*|[\r\n]*$/g,"").replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi,"").replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g,"")};i--;)r=(n=e[i]).firstChild?n.firstChild.value:"","script"===t?((o=n.attr("type"))&&n.attr("type","mce-no/type"===o?null:o.replace(/^mce\-/,"")),"xhtml"===s.element_format&&0<r.length&&(n.firstChild.value="// <![CDATA[\n"+a(r)+"\n// ]]>")):"xhtml"===s.element_format&&0<r.length&&(n.firstChild.value="\x3c!--\n"+a(r)+"\n--\x3e")}),t.addNodeFilter("#comment",function(e){for(var t,n=e.length;n--;)0===(t=e[n]).value.indexOf("[CDATA[")?(t.name="#cdata",t.type=4,t.value=t.value.replace(/^\[CDATA\[|\]\]$/g,"")):0===t.value.indexOf("mce:protected ")&&(t.name="#text",t.type=3,t.raw=!0,t.value=unescape(t.value).substr(14))}),t.addNodeFilter("xml:namespace,input",function(e,t){for(var n,r=e.length;r--;)7===(n=e[r]).type?n.remove():1===n.type&&("input"!==t||"type"in n.attributes.map||n.attr("type","text"))}),t.addAttributeFilter("data-mce-type",function(e){U(e,function(e){"format-caret"===e.attr("data-mce-type")&&(e.isEmpty(t.schema.getNonEmptyElements())?e.remove():e.unwrap())})}),t.addAttributeFilter("data-mce-src,data-mce-href,data-mce-style,data-mce-selected,data-mce-expando,data-mce-type,data-mce-resize",function(e,t){for(var n=e.length;n--;)e[n].attr(t,null)})},trimTrailingBr:function(e){var t,n,r=function(e){return e&&"br"===e.name};r(t=e.lastChild)&&r(n=t.prev)&&(t.remove(),n.remove())}},Wb={process:function(e,t,n){return f=n,(l=e)&&l.hasEventListeners("PreProcess")&&!f.no_events?(o=t,i=n,c=(r=e).dom,o=o.cloneNode(!0),(a=H.document.implementation).createHTMLDocument&&(u=a.createHTMLDocument(""),Jt.each("BODY"===o.nodeName?o.childNodes:[o],function(e){u.body.appendChild(u.importNode(e,!0))}),o="BODY"!==o.nodeName?u.body.firstChild:u.body,s=c.doc,c.doc=u),dp(r,qb(i,{node:o})),s&&(c.doc=s),o):t;var r,o,i,a,u,s,c,l,f}},Kb=function(e,a,u){e.addNodeFilter("font",function(e){U(e,function(e){var t,n=a.parse(e.attr("style")),r=e.attr("color"),o=e.attr("face"),i=e.attr("size");r&&(n.color=r),o&&(n["font-family"]=o),i&&(n["font-size"]=u[parseInt(e.attr("size"),10)-1]),e.name="span",e.attr("style",a.serialize(n)),t=e,U(["color","face","size"],function(e){t.attr(e,null)})})})},Xb=function(e,t){var n,r=ui();t.convert_fonts_to_spans&&Kb(e,r,Jt.explode(t.font_size_legacy_values)),n=r,e.addNodeFilter("strike",function(e){U(e,function(e){var t=n.parse(e.attr("style"));t["text-decoration"]="line-through",e.name="span",e.attr("style",n.serialize(t))})})},Yb={register:function(e,t){t.inline_styles&&Xb(e,t)}},Gb=/^[ \t\r\n]*$/,Jb={"#text":3,"#comment":8,"#cdata":4,"#pi":7,"#doctype":10,"#document-fragment":11},Qb=function(e,t,n){var r,o,i=n?"lastChild":"firstChild",a=n?"prev":"next";if(e[i])return e[i];if(e!==t){if(r=e[a])return r;for(o=e.parent;o&&o!==t;o=o.parent)if(r=o[a])return r}},Zb=function(){function a(e,t){this.name=e,1===(this.type=t)&&(this.attributes=[],this.attributes.map={})}return a.create=function(e,t){var n,r;if(n=new a(e,Jb[e]||1),t)for(r in t)n.attr(r,t[r]);return n},a.prototype.replace=function(e){return e.parent&&e.remove(),this.insert(e,this),this.remove(),this},a.prototype.attr=function(e,t){var n,r;if("string"!=typeof e){for(r in e)this.attr(r,e[r]);return this}if(n=this.attributes){if(t!==undefined){if(null===t){if(e in n.map)for(delete n.map[e],r=n.length;r--;)if(n[r].name===e)return n=n.splice(r,1),this;return this}if(e in n.map){for(r=n.length;r--;)if(n[r].name===e){n[r].value=t;break}}else n.push({name:e,value:t});return n.map[e]=t,this}return n.map[e]}},a.prototype.clone=function(){var e,t,n,r,o,i=new a(this.name,this.type);if(n=this.attributes){for((o=[]).map={},e=0,t=n.length;e<t;e++)"id"!==(r=n[e]).name&&(o[o.length]={name:r.name,value:r.value},o.map[r.name]=r.value);i.attributes=o}return i.value=this.value,i.shortEnded=this.shortEnded,i},a.prototype.wrap=function(e){return this.parent.insert(e,this),e.append(this),this},a.prototype.unwrap=function(){var e,t;for(e=this.firstChild;e;)t=e.next,this.insert(e,this,!0),e=t;this.remove()},a.prototype.remove=function(){var e=this.parent,t=this.next,n=this.prev;return e&&(e.firstChild===this?(e.firstChild=t)&&(t.prev=null):n.next=t,e.lastChild===this?(e.lastChild=n)&&(n.next=null):t.prev=n,this.parent=this.next=this.prev=null),this},a.prototype.append=function(e){var t;return e.parent&&e.remove(),(t=this.lastChild)?((t.next=e).prev=t,this.lastChild=e):this.lastChild=this.firstChild=e,e.parent=this,e},a.prototype.insert=function(e,t,n){var r;return e.parent&&e.remove(),r=t.parent||this,n?(t===r.firstChild?r.firstChild=e:t.prev.next=e,e.prev=t.prev,(e.next=t).prev=e):(t===r.lastChild?r.lastChild=e:t.next.prev=e,e.next=t.next,(e.prev=t).next=e),e.parent=r,e},a.prototype.getAll=function(e){var t,n=[];for(t=this.firstChild;t;t=Qb(t,this))t.name===e&&n.push(t);return n},a.prototype.empty=function(){var e,t,n;if(this.firstChild){for(e=[],n=this.firstChild;n;n=Qb(n,this))e.push(n);for(t=e.length;t--;)(n=e[t]).parent=n.firstChild=n.lastChild=n.next=n.prev=null}return this.firstChild=this.lastChild=null,this},a.prototype.isEmpty=function(e,t,n){var r,o,i=this.firstChild;if(t=t||{},i)do{if(1===i.type){if(i.attributes.map["data-mce-bogus"])continue;if(e[i.name])return!1;for(r=i.attributes.length;r--;)if("name"===(o=i.attributes[r].name)||0===o.indexOf("data-mce-bookmark"))return!1}if(8===i.type)return!1;if(3===i.type&&!Gb.test(i.value))return!1;if(3===i.type&&i.parent&&t[i.parent.name]&&Gb.test(i.value))return!1;if(n&&n(i))return!1}while(i=Qb(i,this));return!0},a.prototype.walk=function(e){return Qb(this,null,e)},a}(),ey=function(e,t,n,r){(e.padd_empty_with_br||t.insert)&&n[r.name]?r.empty().append(new Zb("br",1)).shortEnded=!0:r.empty().append(new Zb("#text",3)).value="\xa0"},ty=function(e){return ny(e,"#text")&&"\xa0"===e.firstChild.value},ny=function(e,t){return e&&e.firstChild&&e.firstChild===e.lastChild&&e.firstChild.name===t},ry=function(r,e,t,n){return n.isEmpty(e,t,function(e){return t=e,(n=r.getElementRule(t.name))&&n.paddEmpty;var t,n})},oy=function(e,t){return e&&(t[e.name]||"br"===e.name)},iy=function(e,p){var h=e.schema;p.remove_trailing_brs&&e.addNodeFilter("br",function(e,t,n){var r,o,i,a,u,s,c,l,f=e.length,d=Jt.extend({},h.getBlockElements()),m=h.getNonEmptyElements(),g=h.getNonEmptyElements();for(d.body=1,r=0;r<f;r++)if(i=(o=e[r]).parent,d[o.parent.name]&&o===i.lastChild){for(u=o.prev;u;){if("span"!==(s=u.name)||"bookmark"!==u.attr("data-mce-type")){if("br"!==s)break;if("br"===s){o=null;break}}u=u.prev}o&&(o.remove(),ry(h,m,g,i)&&(c=h.getElementRule(i.name))&&(c.removeEmpty?i.remove():c.paddEmpty&&ey(p,n,d,i)))}else{for(a=o;i&&i.firstChild===a&&i.lastChild===a&&!d[(a=i).name];)i=i.parent;a===i&&!0!==p.padd_empty_with_br&&((l=new Zb("#text",3)).value="\xa0",o.replace(l))}}),e.addAttributeFilter("href",function(e){var t,n,r,o=e.length;if(!p.allow_unsafe_link_target)for(;o--;)"a"===(t=e[o]).name&&"_blank"===t.attr("target")&&t.attr("rel",(n=t.attr("rel"),r=n?Jt.trim(n):"",/\b(noopener)\b/g.test(r)?r:r.split(" ").filter(function(e){return 0<e.length}).concat(["noopener"]).sort().join(" ")))}),p.allow_html_in_named_anchor||e.addAttributeFilter("id,name",function(e){for(var t,n,r,o,i=e.length;i--;)if("a"===(o=e[i]).name&&o.firstChild&&!o.attr("href"))for(r=o.parent,t=o.lastChild;n=t.prev,r.insert(t,o),t=n;);}),p.fix_list_elements&&e.addNodeFilter("ul,ol",function(e){for(var t,n,r=e.length;r--;)if("ul"===(n=(t=e[r]).parent).name||"ol"===n.name)if(t.prev&&"li"===t.prev.name)t.prev.append(t);else{var o=new Zb("li",1);o.attr("style","list-style-type: none"),t.wrap(o)}}),p.validate&&h.getValidClasses()&&e.addAttributeFilter("class",function(e){for(var t,n,r,o,i,a,u,s=e.length,c=h.getValidClasses();s--;){for(n=(t=e[s]).attr("class").split(" "),i="",r=0;r<n.length;r++)o=n[r],u=!1,(a=c["*"])&&a[o]&&(u=!0),a=c[t.name],!u&&a&&a[o]&&(u=!0),u&&(i&&(i+=" "),i+=o);i.length||(i=null),t.attr("class",i)}})},ay=Jt.makeMap,uy=Jt.each,sy=Jt.explode,cy=Jt.extend;function ly(k,T){void 0===T&&(T=ii());var A={},R=[],_={},D={};(k=k||{}).validate=!("validate"in k)||k.validate,k.root_name=k.root_name||"body";var B=function(e){var t,n,r;(n=e.name)in A&&((r=_[n])?r.push(e):_[n]=[e]),t=R.length;for(;t--;)(n=R[t].name)in e.attributes.map&&((r=D[n])?r.push(e):D[n]=[e]);return e},e={schema:T,addAttributeFilter:function(e,n){uy(sy(e),function(e){var t;for(t=0;t<R.length;t++)if(R[t].name===e)return void R[t].callbacks.push(n);R.push({name:e,callbacks:[n]})})},getAttributeFilters:function(){return[].concat(R)},addNodeFilter:function(e,n){uy(sy(e),function(e){var t=A[e];t||(A[e]=t=[]),t.push(n)})},getNodeFilters:function(){var e=[];for(var t in A)A.hasOwnProperty(t)&&e.push({name:t,callbacks:A[t]});return e},filterNode:B,parse:function(e,a){var t,n,r,o,i,u,s,c,l,f,d,m=[];a=a||{},_={},D={},l=cy(ay("script,style,head,html,body,title,meta,param"),T.getBlockElements());var g=T.getNonEmptyElements(),p=T.children,h=k.validate,v="forced_root_block"in a?a.forced_root_block:k.forced_root_block,b=T.getWhiteSpaceElements(),y=/^[ \t\r\n]+/,C=/[ \t\r\n]+$/,x=/[ \t\r\n]+/g,w=/^[ \t\r\n]+$/;f=b.hasOwnProperty(a.context)||b.hasOwnProperty(k.root_name);var N=function(e,t){var n,r=new Zb(e,t);return e in A&&((n=_[e])?n.push(r):_[e]=[r]),r},E=function(e){var t,n,r,o,i=T.getBlockElements();for(t=e.prev;t&&3===t.type;){if(0<(r=t.value.replace(C,"")).length)return void(t.value=r);if(n=t.next){if(3===n.type&&n.value.length){t=t.prev;continue}if(!i[n.name]&&"script"!==n.name&&"style"!==n.name){t=t.prev;continue}}o=t.prev,t.remove(),t=o}};t=Rv({validate:h,allow_script_urls:k.allow_script_urls,allow_conditional_comments:k.allow_conditional_comments,self_closing_elements:function(e){var t,n={};for(t in e)"li"!==t&&"p"!==t&&(n[t]=e[t]);return n}(T.getSelfClosingElements()),cdata:function(e){d.append(N("#cdata",4)).value=e},text:function(e,t){var n;f||(e=e.replace(x," "),oy(d.lastChild,l)&&(e=e.replace(y,""))),0!==e.length&&((n=N("#text",3)).raw=!!t,d.append(n).value=e)},comment:function(e){d.append(N("#comment",8)).value=e},pi:function(e,t){d.append(N(e,7)).value=t,E(d)},doctype:function(e){d.append(N("#doctype",10)).value=e,E(d)},start:function(e,t,n){var r,o,i,a,u;if(i=h?T.getElementRule(e):{}){for((r=N(i.outputName||e,1)).attributes=t,r.shortEnded=n,d.append(r),(u=p[d.name])&&p[r.name]&&!u[r.name]&&m.push(r),o=R.length;o--;)(a=R[o].name)in t.map&&((s=D[a])?s.push(r):D[a]=[r]);l[e]&&E(r),n||(d=r),!f&&b[e]&&(f=!0)}},end:function(e){var t,n,r,o,i;if(n=h?T.getElementRule(e):{}){if(l[e]&&!f){if((t=d.firstChild)&&3===t.type)if(0<(r=t.value.replace(y,"")).length)t.value=r,t=t.next;else for(o=t.next,t.remove(),t=o;t&&3===t.type;)r=t.value,o=t.next,(0===r.length||w.test(r))&&(t.remove(),t=o),t=o;if((t=d.lastChild)&&3===t.type)if(0<(r=t.value.replace(C,"")).length)t.value=r,t=t.prev;else for(o=t.prev,t.remove(),t=o;t&&3===t.type;)r=t.value,o=t.prev,(0===r.length||w.test(r))&&(t.remove(),t=o),t=o}if(f&&b[e]&&(f=!1),n.removeEmpty&&ry(T,g,b,d)&&!d.attributes.map.name&&!d.attr("id"))return i=d.parent,l[d.name]?d.empty().remove():d.unwrap(),void(d=i);n.paddEmpty&&(ty(d)||ry(T,g,b,d))&&ey(k,a,l,d),d=d.parent}}},T);var S=d=new Zb(a.context||k.root_name,11);if(t.parse(e),h&&m.length&&(a.context?a.invalid=!0:function(e){var t,n,r,o,i,a,u,s,c,l,f,d,m,g,p,h;for(d=ay("tr,td,th,tbody,thead,tfoot,table"),l=T.getNonEmptyElements(),f=T.getWhiteSpaceElements(),m=T.getTextBlockElements(),g=T.getSpecialElements(),t=0;t<e.length;t++)if((n=e[t]).parent&&!n.fixed)if(m[n.name]&&"li"===n.parent.name){for(p=n.next;p&&m[p.name];)p.name="li",p.fixed=!0,n.parent.insert(p,n.parent),p=p.next;n.unwrap(n)}else{for(o=[n],r=n.parent;r&&!T.isValidChild(r.name,n.name)&&!d[r.name];r=r.parent)o.push(r);if(r&&1<o.length){for(o.reverse(),i=a=B(o[0].clone()),c=0;c<o.length-1;c++){for(T.isValidChild(a.name,o[c].name)?(u=B(o[c].clone()),a.append(u)):u=a,s=o[c].firstChild;s&&s!==o[c+1];)h=s.next,u.append(s),s=h;a=u}ry(T,l,f,i)?r.insert(n,o[0],!0):(r.insert(i,o[0],!0),r.insert(n,i)),r=o[0],(ry(T,l,f,r)||ny(r,"br"))&&r.empty().remove()}else if(n.parent){if("li"===n.name){if((p=n.prev)&&("ul"===p.name||"ul"===p.name)){p.append(n);continue}if((p=n.next)&&("ul"===p.name||"ul"===p.name)){p.insert(n,p.firstChild,!0);continue}n.wrap(B(new Zb("ul",1)));continue}T.isValidChild(n.parent.name,"div")&&T.isValidChild("div",n.name)?n.wrap(B(new Zb("div",1))):g[n.name]?n.empty().remove():n.unwrap()}}}(m)),v&&("body"===S.name||a.isRootContent)&&function(){var e,t,n=S.firstChild,r=function(e){e&&((n=e.firstChild)&&3===n.type&&(n.value=n.value.replace(y,"")),(n=e.lastChild)&&3===n.type&&(n.value=n.value.replace(C,"")))};if(T.isValidChild(S.name,v.toLowerCase())){for(;n;)e=n.next,3===n.type||1===n.type&&"p"!==n.name&&!l[n.name]&&!n.attr("data-mce-type")?(t||((t=N(v,1)).attr(k.forced_root_block_attrs),S.insert(t,n)),t.append(n)):(r(t),t=null),n=e;r(t)}}(),!a.invalid){for(c in _){for(s=A[c],i=(n=_[c]).length;i--;)n[i].parent||n.splice(i,1);for(r=0,o=s.length;r<o;r++)s[r](n,c,a)}for(r=0,o=R.length;r<o;r++)if((s=R[r]).name in D){for(i=(n=D[s.name]).length;i--;)n[i].parent||n.splice(i,1);for(i=0,u=s.callbacks.length;i<u;i++)s.callbacks[i](n,s.name,a)}}return S}};return iy(e,k),Yb.register(e,k),e}var fy=function(e,t,n){-1===Jt.inArray(t,n)&&(e.addAttributeFilter(n,function(e,t){for(var n=e.length;n--;)e[n].attr(t,null)}),t.push(n))},dy=function(e,t,n){var r=ga(n.getInner?t.innerHTML:e.getOuterHTML(t));return n.selection||wo(ir.fromDom(t))?r:Jt.trim(r)},my=function(e,t,n){var r=n.selection?qb({forced_root_block:!1},n):n,o=e.parse(t,r);return $b.trimTrailingBr(o),o},gy=function(e,t,n,r,o){var i,a,u,s,c=(i=r,el(t,n).serialize(i));return a=e,s=c,!(u=o).no_events&&a?mp(a,qb(u,{content:s})).content:s};function py(e,t){var a,u,s,c,l,n,r=(a=e,n=["data-mce-selected"],s=(u=t)&&u.dom?u.dom:bi.DOM,c=u&&u.schema?u.schema:ii(a),a.entity_encoding=a.entity_encoding||"named",a.remove_trailing_brs=!("remove_trailing_brs"in a)||a.remove_trailing_brs,l=ly(a,c),$b.register(l,a,s),{schema:c,addNodeFilter:l.addNodeFilter,addAttributeFilter:l.addAttributeFilter,serialize:function(e,t){var n=qb({format:"html"},t||{}),r=Wb.process(u,e,n),o=dy(s,r,n),i=my(l,o,n);return"tree"===n.format?i:gy(u,a,c,i,n)},addRules:function(e){c.addValidElements(e)},setRules:function(e){c.setValidElements(e)},addTempAttr:d(fy,l,n),getTempAttrs:function(){return n}});return{schema:r.schema,addNodeFilter:r.addNodeFilter,addAttributeFilter:r.addAttributeFilter,serialize:r.serialize,addRules:r.addRules,setRules:r.setRules,addTempAttr:r.addTempAttr,getTempAttrs:r.getTempAttrs}}function hy(e){return{getBookmark:d(lc,e),moveToBookmark:d(fc,e)}}(hy||(hy={})).isBookmarkNode=dc;var vy,by,yy=hy,Cy=Po.isContentEditableFalse,xy=Po.isContentEditableTrue,wy=function(r,a){var u,s,c,l,f,d,m,g,p,h,v,b,i,y,C,x,w,N=a.dom,E=Jt.each,S=a.getDoc(),k=H.document,T=Math.abs,A=Math.round,R=a.getBody();l={nw:[0,0,-1,-1],ne:[1,0,1,-1],se:[1,1,1,1],sw:[0,1,-1,1]};var e=".mce-content-body";a.contentStyles.push(e+" div.mce-resizehandle {position: absolute;border: 1px solid black;box-sizing: content-box;background: #FFF;width: 7px;height: 7px;z-index: 10000}"+e+" .mce-resizehandle:hover {background: #000}"+e+" img[data-mce-selected],"+e+" hr[data-mce-selected] {outline: 1px solid black;resize: none}"+e+" .mce-clonedresizable {position: absolute;"+(ge.gecko?"":"outline: 1px dashed black;")+"opacity: .5;filter: alpha(opacity=50);z-index: 10000}"+e+" .mce-resize-helper {background: #555;background: rgba(0,0,0,0.75);border-radius: 3px;border: 1px;color: white;display: none;font-family: sans-serif;font-size: 12px;white-space: nowrap;line-height: 14px;margin: 5px 10px;padding: 5px;position: absolute;z-index: 10001}");var _=function(e){return e&&("IMG"===e.nodeName||a.dom.is(e,"figure.image"))},n=function(e){var t,n,r=e.target;t=e,n=a.selection.getRng(),!_(t.target)||yv(t.clientX,t.clientY,n)||e.isDefaultPrevented()||a.selection.select(r)},D=function(e){return a.dom.is(e,"figure.image")?e.querySelector("img"):e},B=function(e){var t=a.settings.object_resizing;return!1!==t&&!ge.iOS&&("string"!=typeof t&&(t="table,img,figure.image,div"),"false"!==e.getAttribute("data-mce-resize")&&e!==a.getBody()&&Or(ir.fromDom(e),t))},O=function(e){var t,n,r,o;t=e.screenX-d,n=e.screenY-m,y=t*f[2]+h,C=n*f[3]+v,y=y<5?5:y,C=C<5?5:C,(_(u)&&!1!==a.settings.resize_img_proportional?!xv.modifierPressed(e):xv.modifierPressed(e)||_(u)&&f[2]*f[3]!=0)&&(T(t)>T(n)?(C=A(y*b),y=A(C/b)):(y=A(C/b),C=A(y*b))),N.setStyles(D(s),{width:y,height:C}),r=0<(r=f.startPos.x+t)?r:0,o=0<(o=f.startPos.y+n)?o:0,N.setStyles(c,{left:r,top:o,display:"block"}),c.innerHTML=y+" × "+C,f[2]<0&&s.clientWidth<=y&&N.setStyle(s,"left",g+(h-y)),f[3]<0&&s.clientHeight<=C&&N.setStyle(s,"top",p+(v-C)),(t=R.scrollWidth-x)+(n=R.scrollHeight-w)!=0&&N.setStyles(c,{left:r-t,top:o-n}),i||(vp(a,u,h,v),i=!0)},P=function(){i=!1;var e=function(e,t){t&&(u.style[e]||!a.schema.isValid(u.nodeName.toLowerCase(),e)?N.setStyle(D(u),e,t):N.setAttrib(D(u),e,t))};e("width",y),e("height",C),N.unbind(S,"mousemove",O),N.unbind(S,"mouseup",P),k!==S&&(N.unbind(k,"mousemove",O),N.unbind(k,"mouseup",P)),N.remove(s),N.remove(c),o(u),bp(a,u,y,C),N.setAttrib(u,"style",N.getAttrib(u,"style")),a.nodeChanged()},o=function(e){var t,r,o,n,i;I(),F(),t=N.getPos(e,R),g=t.x,p=t.y,i=e.getBoundingClientRect(),r=i.width||i.right-i.left,o=i.height||i.bottom-i.top,u!==e&&(u=e,y=C=0),n=a.fire("ObjectSelected",{target:e}),B(e)&&!n.isDefaultPrevented()?E(l,function(n,e){var t;(t=N.get("mceResizeHandle"+e))&&N.remove(t),t=N.add(R,"div",{id:"mceResizeHandle"+e,"data-mce-bogus":"all","class":"mce-resizehandle",unselectable:!0,style:"cursor:"+e+"-resize; margin:0; padding:0"}),11===ge.ie&&(t.contentEditable=!1),N.bind(t,"mousedown",function(e){var t;e.stopImmediatePropagation(),e.preventDefault(),d=(t=e).screenX,m=t.screenY,h=D(u).clientWidth,v=D(u).clientHeight,b=v/h,(f=n).startPos={x:r*n[0]+g,y:o*n[1]+p},x=R.scrollWidth,w=R.scrollHeight,s=u.cloneNode(!0),N.addClass(s,"mce-clonedresizable"),N.setAttrib(s,"data-mce-bogus","all"),s.contentEditable=!1,s.unSelectabe=!0,N.setStyles(s,{left:g,top:p,margin:0}),s.removeAttribute("data-mce-selected"),R.appendChild(s),N.bind(S,"mousemove",O),N.bind(S,"mouseup",P),k!==S&&(N.bind(k,"mousemove",O),N.bind(k,"mouseup",P)),c=N.add(R,"div",{"class":"mce-resize-helper","data-mce-bogus":"all"},h+" × "+v)}),n.elm=t,N.setStyles(t,{left:r*n[0]+g-t.offsetWidth/2,top:o*n[1]+p-t.offsetHeight/2})}):I(),u.setAttribute("data-mce-selected","1")},I=function(){var e,t;for(e in F(),u&&u.removeAttribute("data-mce-selected"),l)(t=N.get("mceResizeHandle"+e))&&(N.unbind(t),N.remove(t))},L=function(e){var t,n=function(e,t){if(e)do{if(e===t)return!0}while(e=e.parentNode)};i||a.removed||(E(N.select("img[data-mce-selected],hr[data-mce-selected]"),function(e){e.removeAttribute("data-mce-selected")}),t="mousedown"===e.type?e.target:r.getNode(),n(t=N.$(t).closest("table,img,figure.image,hr")[0],R)&&(z(),n(r.getStart(!0),t)&&n(r.getEnd(!0),t))?o(t):I())},M=function(e){return Cy(function(e,t){for(;t&&t!==e;){if(xy(t)||Cy(t))return t;t=t.parentNode}return null}(a.getBody(),e))},F=function(){for(var e in l){var t=l[e];t.elm&&(N.unbind(t.elm),delete t.elm)}},z=function(){try{a.getDoc().execCommand("enableObjectResizing",!1,!1)}catch(e){}};return a.on("init",function(){z(),ge.ie&&11<=ge.ie&&(a.on("mousedown click",function(e){var t=e.target,n=t.nodeName;i||!/^(TABLE|IMG|HR)$/.test(n)||M(t)||(2!==e.button&&a.selection.select(t,"TABLE"===n),"mousedown"===e.type&&a.nodeChanged())}),a.dom.bind(R,"mscontrolselect",function(e){var t=function(e){ye.setEditorTimeout(a,function(){a.selection.select(e)})};if(M(e.target))return e.preventDefault(),void t(e.target);/^(TABLE|IMG|HR)$/.test(e.target.nodeName)&&(e.preventDefault(),"IMG"===e.target.tagName&&t(e.target))}));var t=ye.throttle(function(e){a.composing||L(e)});a.on("nodechange ResizeEditor ResizeWindow drop FullscreenStateChanged",t),a.on("keyup compositionend",function(e){u&&"TABLE"===u.nodeName&&t(e)}),a.on("hide blur",I),a.on("contextmenu",n)}),a.on("remove",F),{isResizable:B,showResizeRect:o,hideResizeRect:I,updateResizeRect:L,destroy:function(){u=s=null}}},Ny=function(e){for(var t=0,n=0,r=e;r&&r.nodeType;)t+=r.offsetLeft||0,n+=r.offsetTop||0,r=r.offsetParent;return{x:t,y:n}},Ey=function(e,t,n){var r,o,i,a,u,s=e.dom,c=s.getRoot(),l=0;if(u={elm:t,alignToTop:n},e.fire("scrollIntoView",u),!u.isDefaultPrevented()&&Po.isElement(t)){if(!1===n&&(l=t.offsetHeight),"BODY"!==c.nodeName){var f=e.selection.getScrollContainer();if(f)return r=Ny(t).y-Ny(f).y+l,a=f.clientHeight,void((r<(i=f.scrollTop)||i+a<r+25)&&(f.scrollTop=r<i?r:r-a+25))}o=s.getViewPort(e.getWin()),r=s.getPos(t).y+l,i=o.y,a=o.h,(r<o.y||i+a<r+25)&&e.getWin().scrollTo(0,r<i?r:r-a+25)}},Sy=function(d,e){ne(vu.fromRangeStart(e).getClientRects()).each(function(e){var t,n,r,o,i,a,u,s,c,l=function(e){if(e.inline)return e.getBody().getBoundingClientRect();var t=e.getWin();return{left:0,right:t.innerWidth,top:0,bottom:t.innerHeight,width:t.innerWidth,height:t.innerHeight}}(d),f={x:(i=t=l,a=n=e,a.left>i.left&&a.right<i.right?0:a.left<i.left?a.left-i.left:a.right-i.right),y:(r=t,o=n,o.top>r.top&&o.bottom<r.bottom?0:o.top<r.top?o.top-r.top:o.bottom-r.bottom)};s=0!==f.x?0<f.x?f.x+4:f.x-4:0,c=0!==f.y?0<f.y?f.y+4:f.y-4:0,(u=d).inline?(u.getBody().scrollLeft+=s,u.getBody().scrollTop+=c):u.getWin().scrollBy(s,c)})},ky=function(e){return Po.isContentEditableTrue(e)||Po.isContentEditableFalse(e)},Ty=function(e,t,n){var r,o,i,a,u,s=n;if(s.caretPositionFromPoint)(o=s.caretPositionFromPoint(e,t))&&((r=n.createRange()).setStart(o.offsetNode,o.offset),r.collapse(!0));else if(n.caretRangeFromPoint)r=n.caretRangeFromPoint(e,t);else if(s.body.createTextRange){r=s.body.createTextRange();try{r.moveToPoint(e,t),r.collapse(!0)}catch(c){r=function(e,n,t){var r,o,i;if(r=t.elementFromPoint(e,n),o=t.body.createTextRange(),r&&"HTML"!==r.tagName||(r=t.body),o.moveToElementText(r),0<(i=(i=Jt.toArray(o.getClientRects())).sort(function(e,t){return(e=Math.abs(Math.max(e.top-n,e.bottom-n)))-(t=Math.abs(Math.max(t.top-n,t.bottom-n)))})).length){n=(i[0].bottom+i[0].top)/2;try{return o.moveToPoint(e,n),o.collapse(!0),o}catch(a){}}return null}(e,t,n)}return i=r,a=n.body,u=i&&i.parentElement?i.parentElement():null,Po.isContentEditableFalse(function(e,t,n){for(;e&&e!==t;){if(n(e))return e;e=e.parentNode}return null}(u,a,ky))?null:i}return r},Ay=function(n,e){return W(e,function(e){var t=n.fire("GetSelectionRange",{range:e});return t.range!==e?t.range:e})},Ry=function(e,t){var n=(t||H.document).createDocumentFragment();return U(e,function(e){n.appendChild(e.dom())}),ir.fromDom(n)},_y=kr("element","width","rows"),Dy=kr("element","cells"),By=kr("x","y"),Oy=function(e,t){var n=parseInt(xr(e,t),10);return isNaN(n)?1:n},Py=function(e){return X(e,function(e,t){return t.cells().length>e?t.cells().length:e},0)},Iy=function(e,t){for(var n=e.rows(),r=0;r<n.length;r++)for(var o=n[r].cells(),i=0;i<o.length;i++)if(Ir(o[i],t))return A.some(By(i,r));return A.none()},Ly=function(e,t,n,r,o){for(var i=[],a=e.rows(),u=n;u<=o;u++){var s=a[u].cells(),c=t<r?s.slice(t,r+1):s.slice(r,t+1);i.push(Dy(a[u].element(),c))}return i},My=function(e){var o=_y(sa(e),0,[]);return U($i(e,"tr"),function(n,r){U($i(n,"td,th"),function(e,t){!function(e,t,n,r,o){for(var i=Oy(o,"rowspan"),a=Oy(o,"colspan"),u=e.rows(),s=n;s<n+i;s++){u[s]||(u[s]=Dy(ca(r),[]));for(var c=t;c<t+a;c++)u[s].cells()[c]=s===n&&c===t?o:sa(o)}}(o,function(e,t,n){for(;r=t,o=n,i=void 0,((i=e.rows())[o]?i[o].cells():[])[r];)t++;var r,o,i;return t}(o,t,r),r,n,e)})}),_y(o.element(),Py(o.rows()),o.rows())},Fy=function(e){return n=W((t=e).rows(),function(e){var t=W(e.cells(),function(e){var t=ca(e);return wr(t,"colspan"),wr(t,"rowspan"),t}),n=sa(e.element());return Di(n,t),n}),r=sa(t.element()),o=ir.fromTag("tbody"),Di(o,n),_i(r,o),r;var t,n,r,o},zy=function(l,e,t){return Iy(l,e).bind(function(c){return Iy(l,t).map(function(e){return t=l,r=e,o=(n=c).x(),i=n.y(),a=r.x(),u=r.y(),s=i<u?Ly(t,o,i,a,u):Ly(t,o,u,a,i),_y(t.element(),Py(s),s);var t,n,r,o,i,a,u,s})})},Uy=function(n,t){return Y(n,function(e){return"li"===cr(e)&&jh(e,t)}).fold(q([]),function(e){return(t=n,Y(t,function(e){return"ul"===cr(e)||"ol"===cr(e)})).map(function(e){return[ir.fromTag("li"),ir.fromTag(cr(e))]}).getOr([]);var t})},Vy=function(e,t){var n,r=ir.fromDom(t.commonAncestorContainer),o=ef(r,e),i=V(o,function(e){return go(e)||fo(e)}),a=Uy(o,t),u=i.concat(a.length?a:bo(n=r)?zr(n).filter(vo).fold(q([]),function(e){return[n,e]}):vo(n)?[n]:[]);return W(u,sa)},Hy=function(){return Ry([])},jy=function(e,t){return n=ir.fromDom(t.cloneContents()),r=Vy(e,t),o=X(r,function(e,t){return _i(t,e),t},n),0<r.length?Ry([o]):o;var n,r,o},qy=function(e,o){return(t=e,n=o[0],Gi(n,"table",d(Ir,t))).bind(function(e){var t=o[0],n=o[o.length-1],r=My(e);return zy(r,t,n).map(function(e){return Ry([Fy(e)])})}).getOrThunk(Hy);var t,n},$y=function(e,t){var n,r,o=pm(t,e);return 0<o.length?qy(e,o):(n=e,0<(r=t).length&&r[0].collapsed?Hy():jy(n,r[0]))},Wy=function(e,t){if(void 0===t&&(t={}),t.get=!0,t.format=t.format||"html",t.selection=!0,(t=e.fire("BeforeGetContent",t)).isDefaultPrevented())return e.fire("GetContent",t),t.content;if("text"===t.format)return c=e,A.from(c.selection.getRng()).map(function(e){var t=c.dom.add(c.getBody(),"div",{"data-mce-bogus":"all",style:"overflow: hidden; opacity: 0;"},e.cloneContents()),n=ga(t.innerText);return c.dom.remove(t),n}).getOr("");t.getInner=!0;var n,r,o,i,a,u,s,c,l=(r=t,i=(n=e).selection.getRng(),a=n.dom.create("body"),u=n.selection.getSel(),s=Ay(n,cm(u)),(o=r.contextual?$y(ir.fromDom(n.getBody()),s).dom():i.cloneContents())&&a.appendChild(o),n.selection.serializer.serialize(a,r));return"tree"===t.format?l:(t.content=e.selection.isCollapsed()?"":l,e.fire("GetContent",t),t.content)},Ky=function(e,t,n){var r,o,i,a=e.selection.getRng(),u=e.getDoc();if((n=n||{format:"html"}).set=!0,n.selection=!0,n.content=t,n.no_events||!(n=e.fire("BeforeSetContent",n)).isDefaultPrevented()){if(t=n.content,a.insertNode){t+='<span id="__caret">_</span>',a.startContainer===u&&a.endContainer===u?u.body.innerHTML=t:(a.deleteContents(),0===u.body.childNodes.length?u.body.innerHTML=t:a.createContextualFragment?a.insertNode(a.createContextualFragment(t)):(o=u.createDocumentFragment(),i=u.createElement("div"),o.appendChild(i),i.outerHTML=t,a.insertNode(o))),r=e.dom.get("__caret"),(a=u.createRange()).setStartBefore(r),a.setEndBefore(r),e.selection.setRng(a),e.dom.remove("__caret");try{e.selection.setRng(a)}catch(s){}}else a.item&&(u.execCommand("Delete",!1,null),a=e.getRng()),/^\s+/.test(t)?(a.pasteHTML('<span id="__mce_tmp">_</span>'+t),e.dom.remove("__mce_tmp")):a.pasteHTML(t);n.no_events||e.fire("SetContent",n)}else e.fire("SetContent",n)},Xy=function(e,t,n,r,o){var i=n?t.startContainer:t.endContainer,a=n?t.startOffset:t.endOffset;return A.from(i).map(ir.fromDom).map(function(e){return r&&t.collapsed?e:$r(e,o(e,a)).getOr(e)}).bind(function(e){return fr(e)?A.some(e):zr(e)}).map(function(e){return e.dom()}).getOr(e)},Yy=function(e,t,n){return Xy(e,t,!0,n,function(e,t){return Math.min(e.dom().childNodes.length,t)})},Gy=function(e,t,n){return Xy(e,t,!1,n,function(e,t){return 0<t?t-1:t})},Jy=function(e,t){for(var n=e;e&&Po.isText(e)&&0===e.length;)e=t?e.nextSibling:e.previousSibling;return e||n},Qy=Jt.each,Zy=function(e){return!!e.select},eC=function(e){return!(!e||!e.ownerDocument)&&Lr(ir.fromDom(e.ownerDocument),ir.fromDom(e))},tC=function(u,s,e,c){var n,t,l,f,a,r=function(e,t){return Ky(c,e,t)},o=function(e){var t=m();t.collapse(!!e),i(t)},d=function(){return s.getSelection?s.getSelection():s.document.selection},m=function(){var e,t,n,r,o=function(e,t,n){try{return t.compareBoundaryPoints(e,n)}catch(r){return-1}};if(!s)return null;if(null==(r=s.document))return null;if(c.bookmark!==undefined&&!1===eh(c)){var i=Qg(c);if(i.isSome())return i.map(function(e){return Ay(c,[e])[0]}).getOr(r.createRange())}try{(e=d())&&!Po.isRestrictedNode(e.anchorNode)&&(t=0<e.rangeCount?e.getRangeAt(0):e.createRange?e.createRange():r.createRange())}catch(a){}return(t=Ay(c,[t])[0])||(t=r.createRange?r.createRange():r.body.createTextRange()),t.setStart&&9===t.startContainer.nodeType&&t.collapsed&&(n=u.getRoot(),t.setStart(n,0),t.setEnd(n,0)),l&&f&&(0===o(t.START_TO_START,t,l)&&0===o(t.END_TO_END,t,l)?t=f:f=l=null),t},i=function(e,t){var n,r;if((o=e)&&(Zy(o)||eC(o.startContainer)&&eC(o.endContainer))){var o,i=Zy(e)?e:null;if(i){f=null;try{i.select()}catch(a){}}else{if(n=d(),e=c.fire("SetSelectionRange",{range:e,forward:t}).range,n){f=e;try{n.removeAllRanges(),n.addRange(e)}catch(a){}!1===t&&n.extend&&(n.collapse(e.endContainer,e.endOffset),n.extend(e.startContainer,e.startOffset)),l=0<n.rangeCount?n.getRangeAt(0):null}e.collapsed||e.startContainer!==e.endContainer||!n.setBaseAndExtent||ge.ie||e.endOffset-e.startOffset<2&&e.startContainer.hasChildNodes()&&(r=e.startContainer.childNodes[e.startOffset])&&"IMG"===r.tagName&&(n.setBaseAndExtent(e.startContainer,e.startOffset,e.endContainer,e.endOffset),n.anchorNode===e.startContainer&&n.focusNode===e.endContainer||n.setBaseAndExtent(r,0,r,1)),c.fire("AfterSetSelectionRange",{range:e,forward:t})}}},g=function(){var e,t,n=d();return!(n&&n.anchorNode&&n.focusNode)||((e=u.createRng()).setStart(n.anchorNode,n.anchorOffset),e.collapse(!0),(t=u.createRng()).setStart(n.focusNode,n.focusOffset),t.collapse(!0),e.compareBoundaryPoints(e.START_TO_START,t)<=0)},p={bookmarkManager:null,controlSelection:null,dom:u,win:s,serializer:e,editor:c,collapse:o,setCursorLocation:function(e,t){var n=u.createRng();e?(n.setStart(e,t),n.setEnd(e,t),i(n),o(!1)):(qh(u,n,c.getBody(),!0),i(n))},getContent:function(e){return Wy(c,e)},setContent:r,getBookmark:function(e,t){return n.getBookmark(e,t)},moveToBookmark:function(e){return n.moveToBookmark(e)},select:function(e,t){var r,n,o;return(r=u,n=e,o=t,A.from(n).map(function(e){var t=r.nodeIndex(e),n=r.createRng();return n.setStart(e.parentNode,t),n.setEnd(e.parentNode,t+1),o&&(qh(r,n,e,!0),qh(r,n,e,!1)),n})).each(i),e},isCollapsed:function(){var e=m(),t=d();return!(!e||e.item)&&(e.compareEndPoints?0===e.compareEndPoints("StartToEnd",e):!t||e.collapsed)},isForward:g,setNode:function(e){return r(u.getOuterHTML(e)),e},getNode:function(){return e=c.getBody(),(t=m())?(r=t.startContainer,o=t.endContainer,i=t.startOffset,a=t.endOffset,n=t.commonAncestorContainer,!t.collapsed&&(r===o&&a-i<2&&r.hasChildNodes()&&(n=r.childNodes[i]),3===r.nodeType&&3===o.nodeType&&(r=r.length===i?Jy(r.nextSibling,!0):r.parentNode,o=0===a?Jy(o.previousSibling,!1):o.parentNode,r&&r===o))?r:n&&3===n.nodeType?n.parentNode:n):e;var e,t,n,r,o,i,a},getSel:d,setRng:i,getRng:m,getStart:function(e){return Yy(c.getBody(),m(),e)},getEnd:function(e){return Gy(c.getBody(),m(),e)},getSelectedBlocks:function(e,t){return function(e,t,n,r){var o,i,a=[];if(i=e.getRoot(),n=e.getParent(n||Yy(i,t,t.collapsed),e.isBlock),r=e.getParent(r||Gy(i,t,t.collapsed),e.isBlock),n&&n!==i&&a.push(n),n&&r&&n!==r)for(var u=new ao(o=n,i);(o=u.next())&&o!==r;)e.isBlock(o)&&a.push(o);return r&&n!==r&&r!==i&&a.push(r),a}(u,m(),e,t)},normalize:function(){var e=m(),t=d();if(!fm(t)&&$h(c)){var n=wg(u,e);return n.each(function(e){i(e,g())}),n.getOr(e)}return e},selectorChanged:function(e,t){var i;return a||(a={},i={},c.on("NodeChange",function(e){var n=e.element,r=u.getParents(n,null,u.getRoot()),o={};Qy(a,function(e,n){Qy(r,function(t){if(u.is(t,n))return i[n]||(Qy(e,function(e){e(!0,{node:t,selector:n,parents:r})}),i[n]=e),o[n]=e,!1})}),Qy(i,function(e,t){o[t]||(delete i[t],Qy(e,function(e){e(!1,{node:n,selector:t,parents:r})}))})})),a[e]||(a[e]=[]),a[e].push(t),p},getScrollContainer:function(){for(var e,t=u.getRoot();t&&"BODY"!==t.nodeName;){if(t.scrollHeight>t.clientHeight){e=t;break}t=t.parentNode}return e},scrollIntoView:function(e,t){return Ey(c,e,t)},placeCaretAt:function(e,t){return i(Ty(e,t,c.getDoc()))},getBoundingClientRect:function(){var e=m();return e.collapsed?Cu.fromRangeStart(e).getClientRects()[0]:e.getBoundingClientRect()},destroy:function(){s=l=f=null,t.destroy()}};return n=yy(p),t=wy(p,c),p.bookmarkManager=n,p.controlSelection=t,p};(by=vy||(vy={}))[by.Br=0]="Br",by[by.Block=1]="Block",by[by.Wrap=2]="Wrap",by[by.Eol=3]="Eol";var nC=function(e,t){return e===bu.Backwards?t.reverse():t},rC=function(e,t,n,r){for(var o,i,a,u,s,c,l=$s(n),f=r,d=[];f&&(s=l,c=f,o=t===bu.Forwards?s.next(c):s.prev(c));){if(Po.isBr(o.getNode(!1)))return t===bu.Forwards?{positions:nC(t,d).concat([o]),breakType:vy.Br,breakAt:A.some(o)}:{positions:nC(t,d),breakType:vy.Br,breakAt:A.some(o)};if(o.isVisible()){if(e(f,o)){var m=(i=t,a=f,u=o,Po.isBr(u.getNode(i===bu.Forwards))?vy.Br:!1===bs(a,u)?vy.Block:vy.Wrap);return{positions:nC(t,d),breakType:m,breakAt:A.some(o)}}d.push(o),f=o}else f=o}return{positions:nC(t,d),breakType:vy.Eol,breakAt:A.none()}},oC=function(n,r,o,e){return r(o,e).breakAt.map(function(e){var t=r(o,e).positions;return n===bu.Backwards?t.concat(e):[e].concat(t)}).getOr([])},iC=function(e,i){return X(e,function(e,o){return e.fold(function(){return A.some(o)},function(r){return Ya([ne(r.getClientRects()),ne(o.getClientRects())],function(e,t){var n=Math.abs(i-e.left);return Math.abs(i-t.left)<=n?o:r}).or(e)})},A.none())},aC=function(t,e){return ne(e.getClientRects()).bind(function(e){return iC(t,e.left)})},uC=d(rC,vu.isAbove,-1),sC=d(rC,vu.isBelow,1),cC=d(oC,-1,uC),lC=d(oC,1,sC),fC=Po.isContentEditableFalse,dC=$a,mC=function(e,t,n,r){var o=e===bu.Forwards,i=o?_f:Df;if(!r.collapsed){var a=dC(r);if(fC(a))return eg(e,t,a,e===bu.Backwards,!0)}var u=va(r.startContainer),s=ks(e,t.getBody(),r);if(i(s))return tg(t,s.getNode(!o));var c=Ll.normalizePosition(o,n(s));if(!c)return u?r:null;if(i(c))return eg(e,t,c.getNode(!o),o,!0);var l=n(c);return l&&i(l)&&Rs(c,l)?eg(e,t,l.getNode(!o),o,!0):u?rg(t,c.toRange(),!0):null},gC=function(e,t,n,r){var o,i,a,u,s,c,l,f,d;if(d=dC(r),o=ks(e,t.getBody(),r),i=n(t.getBody(),cv(1),o),a=V(i,lv(1)),s=Wt.last(o.getClientRects()),(_f(o)||Of(o))&&(d=o.getNode()),(Df(o)||Pf(o))&&(d=o.getNode(!0)),!s)return null;if(c=s.left,(u=hv(a,c))&&fC(u.node))return l=Math.abs(c-u.left),f=Math.abs(c-u.right),eg(e,t,u.node,l<f,!0);if(d){var m=function(e,t,n,r){var o,i,a,u,s,c,l=$s(t),f=[],d=0,m=function(e){return Wt.last(e.getClientRects())};1===e?(o=l.next,i=ja,a=Ha,u=Cu.after(r)):(o=l.prev,i=Ha,a=ja,u=Cu.before(r)),c=m(u);do{if(u.isVisible()&&!a(s=m(u),c)){if(0<f.length&&i(s,Wt.last(f))&&d++,(s=za(s)).position=u,s.line=d,n(s))return f;f.push(s)}}while(u=o(u));return f}(e,t.getBody(),cv(1),d);if(u=hv(V(m,lv(1)),c))return rg(t,u.position.toRange(),!0);if(u=Wt.last(V(m,lv(0))))return rg(t,u.position.toRange(),!0)}},pC=function(e,t,n){var r,o,i,a,u=$s(e.getBody()),s=d(As,u.next),c=d(As,u.prev);if(n.collapsed&&e.settings.forced_root_block){if(!(r=e.dom.getParent(n.startContainer,"PRE")))return;(1===t?s(Cu.fromRangeStart(n)):c(Cu.fromRangeStart(n)))||(a=(i=e).dom.create(vl(i)),(!ge.ie||11<=ge.ie)&&(a.innerHTML='<br data-mce-bogus="1">'),o=a,1===t?e.$(r).after(o):e.$(r).before(o),e.selection.select(o,!0),e.selection.collapse())}},hC=function(l,f){return function(){var e,t,n,r,o,i,a,u,s,c=(t=f,r=$s((e=l).getBody()),o=d(As,r.next),i=d(As,r.prev),a=t?bu.Forwards:bu.Backwards,u=t?o:i,s=e.selection.getRng(),(n=mC(a,e,u,s))?n:(n=pC(e,a,s))||null);return!!c&&(l.selection.setRng(c),!0)}},vC=function(u,s){return function(){var e,t,n,r,o,i,a=(r=(t=s)?1:-1,o=t?sv:uv,i=(e=u).selection.getRng(),(n=gC(r,e,o,i))?n:(n=pC(e,r,i))||null);return!!a&&(u.selection.setRng(a),!0)}},bC=function(r,o){return function(){var t,e=o?Cu.fromRangeEnd(r.selection.getRng()):Cu.fromRangeStart(r.selection.getRng()),n=o?sC(r.getBody(),e):uC(r.getBody(),e);return(o?re(n.positions):ne(n.positions)).filter((t=o,function(e){return t?Df(e):_f(e)})).fold(q(!1),function(e){return r.selection.setRng(e.toRange()),!0})}},yC=function(e,t,n,r,o){var i,a,u,s,c=$i(ir.fromDom(n),"td,th,caption").map(function(e){return e.dom()}),l=V((i=e,Z(c,function(e){var t,n,r=(t=za(e.getBoundingClientRect()),n=-1,{left:t.left-n,top:t.top-n,right:t.right+2*n,bottom:t.bottom+2*n,width:t.width+n,height:t.height+n});return[{x:r.left,y:i(r),cell:e},{x:r.right,y:i(r),cell:e}]})),function(e){return t(e,o)});return(a=l,u=r,s=o,X(a,function(e,r){return e.fold(function(){return A.some(r)},function(e){var t=Math.sqrt(Math.abs(e.x-u)+Math.abs(e.y-s)),n=Math.sqrt(Math.abs(r.x-u)+Math.abs(r.y-s));return A.some(n<t?r:e)})},A.none())).map(function(e){return e.cell})},CC=d(yC,function(e){return e.bottom},function(e,t){return e.y<t}),xC=d(yC,function(e){return e.top},function(e,t){return e.y>t}),wC=function(t,n){return ne(n.getClientRects()).bind(function(e){return CC(t,e.left,e.top)}).bind(function(e){return aC((t=e,nc.lastPositionIn(t).map(function(e){return uC(t,e).positions.concat(e)}).getOr([])),n);var t})},NC=function(t,n){return re(n.getClientRects()).bind(function(e){return xC(t,e.left,e.top)}).bind(function(e){return aC((t=e,nc.firstPositionIn(t).map(function(e){return[e].concat(sC(t,e).positions)}).getOr([])),n);var t})},EC=function(e,t){e.selection.setRng(t),Sy(e,t)},SC=function(e,t,n){var r,o,i,a,u=e(t,n);return(a=u).breakType===vy.Wrap&&0===a.positions.length||!Po.isBr(n.getNode())&&(i=u).breakType===vy.Br&&1===i.positions.length?(r=e,o=t,!u.breakAt.map(function(e){return r(o,e).breakAt.isSome()}).getOr(!1)):u.breakAt.isNone()},kC=d(SC,uC),TC=d(SC,sC),AC=function(e,t,n,r){var o,i,a,u,s=e.selection.getRng(),c=t?1:-1;if(is()&&(o=t,i=s,a=n,u=Cu.fromRangeStart(i),nc.positionIn(!o,a).map(function(e){return e.isEqual(u)}).getOr(!1))){var l=eg(c,e,n,!t,!0);return EC(e,l),!0}return!1},RC=function(e,t){var n=t.getNode(e);return Po.isElement(n)&&"TABLE"===n.nodeName?A.some(n):A.none()},_C=function(u,s,c){var e=RC(!!s,c),t=!1===s;e.fold(function(){return EC(u,c.toRange())},function(a){return nc.positionIn(t,u.getBody()).filter(function(e){return e.isEqual(c)}).fold(function(){return EC(u,c.toRange())},function(e){return n=s,o=a,t=c,void((i=vl(r=u))?r.undoManager.transact(function(){var e=ir.fromTag(i);Cr(e,bl(r)),_i(e,ir.fromTag("br")),n?Ai(ir.fromDom(o),e):Ti(ir.fromDom(o),e);var t=r.dom.createRng();t.setStart(e.dom(),0),t.setEnd(e.dom(),0),EC(r,t)}):EC(r,t.toRange()));var n,r,o,t,i})})},DC=function(e,t,n,r){var o,i,a,u,s,c,l=e.selection.getRng(),f=Cu.fromRangeStart(l),d=e.getBody();if(!t&&kC(r,f)){var m=(u=d,wC(s=n,c=f).orThunk(function(){return ne(c.getClientRects()).bind(function(e){return iC(cC(u,Cu.before(s)),e.left)})}).getOr(Cu.before(s)));return _C(e,t,m),!0}return!(!t||!TC(r,f))&&(o=d,m=NC(i=n,a=f).orThunk(function(){return ne(a.getClientRects()).bind(function(e){return iC(lC(o,Cu.after(i)),e.left)})}).getOr(Cu.after(i)),_C(e,t,m),!0)},BC=function(t,n){return function(){return A.from(t.dom.getParent(t.selection.getNode(),"td,th")).bind(function(e){return A.from(t.dom.getParent(e,"table")).map(function(e){return AC(t,n,e)})}).getOr(!1)}},OC=function(n,r){return function(){return A.from(n.dom.getParent(n.selection.getNode(),"td,th")).bind(function(t){return A.from(n.dom.getParent(t,"table")).map(function(e){return DC(n,r,e,t)})}).getOr(!1)}},PC=function(e){return F(["figcaption"],cr(e))},IC=function(e){var t=H.document.createRange();return t.setStartBefore(e.dom()),t.setEndBefore(e.dom()),t},LC=function(e,t,n){n?_i(e,t):Ri(e,t)},MC=function(e,t,n,r){return""===t?(l=e,f=r,d=ir.fromTag("br"),LC(l,d,f),IC(d)):(o=e,i=r,a=t,u=n,s=ir.fromTag(a),c=ir.fromTag("br"),Cr(s,u),_i(s,c),LC(o,s,i),IC(c));var o,i,a,u,s,c,l,f,d},FC=function(e,t,n){return t?(o=e.dom(),sC(o,n).breakAt.isNone()):(r=e.dom(),uC(r,n).breakAt.isNone());var r,o},zC=function(t,n){var e,r,o,i=ir.fromDom(t.getBody()),a=Cu.fromRangeStart(t.selection.getRng()),u=vl(t),s=bl(t);return(e=a,r=i,o=d(Ir,r),Yi(ir.fromDom(e.container()),mo,o).filter(PC)).exists(function(){if(FC(i,n,a)){var e=MC(i,u,s,n);return t.selection.setRng(e),!0}return!1})},UC=function(e,t){return function(){return!!e.selection.isCollapsed()&&zC(e,t)}},VC=function(e,r){return Z(W(e,function(e){return qb({shiftKey:!1,altKey:!1,ctrlKey:!1,metaKey:!1,keyCode:0,action:o},e)}),function(e){return t=e,(n=r).keyCode===t.keyCode&&n.shiftKey===t.shiftKey&&n.altKey===t.altKey&&n.ctrlKey===t.ctrlKey&&n.metaKey===t.metaKey?[e]:[];var t,n})},HC=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r=Array.prototype.slice.call(arguments,1);return function(){return e.apply(null,r)}},jC=function(e,t){return Y(VC(e,t),function(e){return e.action()})},qC=function(i,a){i.on("keydown",function(e){var t,n,r,o;!1===e.isDefaultPrevented()&&(t=i,n=a,r=e,o=rr.detect().os,jC([{keyCode:xv.RIGHT,action:hC(t,!0)},{keyCode:xv.LEFT,action:hC(t,!1)},{keyCode:xv.UP,action:vC(t,!1)},{keyCode:xv.DOWN,action:vC(t,!0)},{keyCode:xv.RIGHT,action:BC(t,!0)},{keyCode:xv.LEFT,action:BC(t,!1)},{keyCode:xv.UP,action:OC(t,!1)},{keyCode:xv.DOWN,action:OC(t,!0)},{keyCode:xv.RIGHT,action:jd.move(t,n,!0)},{keyCode:xv.LEFT,action:jd.move(t,n,!1)},{keyCode:xv.RIGHT,ctrlKey:!o.isOSX(),altKey:o.isOSX(),action:jd.moveNextWord(t,n)},{keyCode:xv.LEFT,ctrlKey:!o.isOSX(),altKey:o.isOSX(),action:jd.movePrevWord(t,n)},{keyCode:xv.UP,action:UC(t,!1)},{keyCode:xv.DOWN,action:UC(t,!0)}],r).each(function(e){r.preventDefault()}))})},$C=function(o,i){o.on("keydown",function(e){var t,n,r;!1===e.isDefaultPrevented()&&(t=o,n=i,r=e,jC([{keyCode:xv.BACKSPACE,action:HC(td,t,!1)},{keyCode:xv.DELETE,action:HC(td,t,!0)},{keyCode:xv.BACKSPACE,action:HC(ig,t,!1)},{keyCode:xv.DELETE,action:HC(ig,t,!0)},{keyCode:xv.BACKSPACE,action:HC(Kd,t,n,!1)},{keyCode:xv.DELETE,action:HC(Kd,t,n,!0)},{keyCode:xv.BACKSPACE,action:HC(km,t,!1)},{keyCode:xv.DELETE,action:HC(km,t,!0)},{keyCode:xv.BACKSPACE,action:HC(gf,t,!1)},{keyCode:xv.DELETE,action:HC(gf,t,!0)},{keyCode:xv.BACKSPACE,action:HC(lf,t,!1)},{keyCode:xv.DELETE,action:HC(lf,t,!0)},{keyCode:xv.BACKSPACE,action:HC(Jm,t,!1)},{keyCode:xv.DELETE,action:HC(Jm,t,!0)}],r).each(function(e){r.preventDefault()}))}),o.on("keyup",function(e){var t,n;!1===e.isDefaultPrevented()&&(t=o,n=e,jC([{keyCode:xv.BACKSPACE,action:HC(nd,t)},{keyCode:xv.DELETE,action:HC(nd,t)}],n))})},WC=function(e){return A.from(e.dom.getParent(e.selection.getStart(!0),e.dom.isBlock))},KC=function(e,t){var n,r,o,i=t,a=e.dom,u=e.schema.getMoveCaretBeforeOnEnterElements();if(t){if(/^(LI|DT|DD)$/.test(t.nodeName)){var s=function(e){for(;e;){if(1===e.nodeType||3===e.nodeType&&e.data&&/[\r\n\s]/.test(e.data))return e;e=e.nextSibling}}(t.firstChild);s&&/^(UL|OL|DL)$/.test(s.nodeName)&&t.insertBefore(a.doc.createTextNode("\xa0"),t.firstChild)}if(o=a.createRng(),t.normalize(),t.hasChildNodes()){for(n=new ao(t,t);r=n.current();){if(Po.isText(r)){o.setStart(r,0),o.setEnd(r,0);break}if(u[r.nodeName.toLowerCase()]){o.setStartBefore(r),o.setEndBefore(r);break}i=r,r=n.next()}r||(o.setStart(i,0),o.setEnd(i,0))}else Po.isBr(t)?t.nextSibling&&a.isBlock(t.nextSibling)?(o.setStartBefore(t),o.setEndBefore(t)):(o.setStartAfter(t),o.setEndAfter(t)):(o.setStart(t,0),o.setEnd(t,0));e.selection.setRng(o),a.remove(void 0),e.selection.scrollIntoView(t)}},XC=function(e,t){var n,r,o=e.getRoot();for(n=t;n!==o&&"false"!==e.getContentEditable(n);)"true"===e.getContentEditable(n)&&(r=n),n=n.parentNode;return n!==o?r:o},YC=WC,GC=function(e){return WC(e).fold(q(""),function(e){return e.nodeName.toUpperCase()})},JC=function(e){return WC(e).filter(function(e){return bo(ir.fromDom(e))}).isSome()},QC=function(e,t){return e&&e.parentNode&&e.parentNode.nodeName===t},ZC=function(e){return e&&/^(OL|UL|LI)$/.test(e.nodeName)},ex=function(e){var t=e.parentNode;return/^(LI|DT|DD)$/.test(t.nodeName)?t:e},tx=function(e,t,n){for(var r=e[n?"firstChild":"lastChild"];r&&!Po.isElement(r);)r=r[n?"nextSibling":"previousSibling"];return r===t},nx=function(e,t,n,r,o){var i=e.dom,a=e.selection.getRng();if(n!==e.getBody()){var u;ZC(u=n)&&ZC(u.parentNode)&&(o="LI");var s,c,l=o?t(o):i.create("BR");if(tx(n,r,!0)&&tx(n,r,!1))QC(n,"LI")?i.insertAfter(l,ex(n)):i.replace(l,n);else if(tx(n,r,!0))QC(n,"LI")?(i.insertAfter(l,ex(n)),l.appendChild(i.doc.createTextNode(" ")),l.appendChild(n)):n.parentNode.insertBefore(l,n);else if(tx(n,r,!1))i.insertAfter(l,ex(n));else{n=ex(n);var f=a.cloneRange();f.setStartAfter(r),f.setEndAfter(n);var d=f.extractContents();"LI"===o&&(c="LI",(s=d).firstChild&&s.firstChild.nodeName===c)?(l=d.firstChild,i.insertAfter(d,n)):(i.insertAfter(d,n),i.insertAfter(l,n))}i.remove(r),KC(e,l)}},rx=function(e){e.innerHTML='<br data-mce-bogus="1">'},ox=function(e,t){return e.nodeName===t||e.previousSibling&&e.previousSibling.nodeName===t},ix=function(e,t){return t&&e.isBlock(t)&&!/^(TD|TH|CAPTION|FORM)$/.test(t.nodeName)&&!/^(fixed|absolute)/i.test(t.style.position)&&"true"!==e.getContentEditable(t)},ax=function(e,t,n){return!1===Po.isText(t)?n:e?1===n&&t.data.charAt(n-1)===ma?0:n:n===t.data.length-1&&t.data.charAt(n)===ma?t.data.length:n},ux=function(e,t){var n,r,o=e.getRoot();for(n=t;n!==o&&"false"!==e.getContentEditable(n);)"true"===e.getContentEditable(n)&&(r=n),n=n.parentNode;return n!==o?r:o},sx=function(e,t){var n=vl(e);n&&n.toLowerCase()===t.tagName.toLowerCase()&&e.dom.setAttribs(t,bl(e))},cx=function(a,e){var t,u,s,i,c,n,r,o,l,f,d,m,g,p,h,v,b,y,C,x=a.dom,w=a.schema,N=w.getNonEmptyElements(),E=a.selection.getRng(),S=function(e){var t,n,r,o=s,i=w.getTextInlineElements();if(e||"TABLE"===f||"HR"===f?(t=x.create(e||m),sx(a,t)):t=c.cloneNode(!1),r=t,!1===xl(a))x.setAttrib(t,"style",null),x.setAttrib(t,"class",null);else do{if(i[o.nodeName]){if(ju(o)||dc(o))continue;n=o.cloneNode(!1),x.setAttrib(n,"id",""),t.hasChildNodes()?n.appendChild(t.firstChild):r=n,t.appendChild(n)}}while((o=o.parentNode)&&o!==u);return rx(r),t},k=function(e){var t,n,r,o;if(o=ax(e,s,i),Po.isText(s)&&(e?0<o:o<s.nodeValue.length))return!1;if(s.parentNode===c&&g&&!e)return!0;if(e&&Po.isElement(s)&&s===c.firstChild)return!0;if(ox(s,"TABLE")||ox(s,"HR"))return g&&!e||!g&&e;for(t=new ao(s,c),Po.isText(s)&&(e&&0===o?t.prev():e||o!==s.nodeValue.length||t.next());n=t.current();){if(Po.isElement(n)){if(!n.getAttribute("data-mce-bogus")&&(r=n.nodeName.toLowerCase(),N[r]&&"br"!==r))return!1}else if(Po.isText(n)&&!/^[ \t\r\n]*$/.test(n.nodeValue))return!1;e?t.prev():t.next()}return!0},T=function(){r=/^(H[1-6]|PRE|FIGURE)$/.test(f)&&"HGROUP"!==d?S(m):S(),wl(a)&&ix(x,l)&&x.isEmpty(c)?r=x.split(l,c):x.insertAfter(r,c),KC(a,r)};wg(x,E).each(function(e){E.setStart(e.startContainer,e.startOffset),E.setEnd(e.endContainer,e.endOffset)}),s=E.startContainer,i=E.startOffset,m=vl(a),n=e.shiftKey,Po.isElement(s)&&s.hasChildNodes()&&(g=i>s.childNodes.length-1,s=s.childNodes[Math.min(i,s.childNodes.length-1)]||s,i=g&&Po.isText(s)?s.nodeValue.length:0),(u=ux(x,s))&&((m&&!n||!m&&n)&&(s=function(e,t,n,r,o){var i,a,u,s,c,l,f,d=t||"P",m=e.dom,g=ux(m,r);if(!(a=m.getParent(r,m.isBlock))||!ix(m,a)){if(l=(a=a||g)===e.getBody()||(f=a)&&/^(TD|TH|CAPTION)$/.test(f.nodeName)?a.nodeName.toLowerCase():a.parentNode.nodeName.toLowerCase(),!a.hasChildNodes())return i=m.create(d),sx(e,i),a.appendChild(i),n.setStart(i,0),n.setEnd(i,0),i;for(s=r;s.parentNode!==a;)s=s.parentNode;for(;s&&!m.isBlock(s);)s=(u=s).previousSibling;if(u&&e.schema.isValidChild(l,d.toLowerCase())){for(i=m.create(d),sx(e,i),u.parentNode.insertBefore(i,u),s=u;s&&!m.isBlock(s);)c=s.nextSibling,i.appendChild(s),s=c;n.setStart(r,o),n.setEnd(r,o)}}return r}(a,m,E,s,i)),c=x.getParent(s,x.isBlock),l=c?x.getParent(c.parentNode,x.isBlock):null,f=c?c.nodeName.toUpperCase():"","LI"!==(d=l?l.nodeName.toUpperCase():"")||e.ctrlKey||(l=(c=l).parentNode,f=d),/^(LI|DT|DD)$/.test(f)&&x.isEmpty(c)?nx(a,S,l,c,m):m&&c===a.getBody()||(m=m||"P",va(c)?(r=ka(c),x.isEmpty(c)&&rx(c),KC(a,r)):k()?T():k(!0)?(r=c.parentNode.insertBefore(S(),c),KC(a,ox(c,"HR")?r:c)):((t=(y=E,C=y.cloneRange(),C.setStart(y.startContainer,ax(!0,y.startContainer,y.startOffset)),C.setEnd(y.endContainer,ax(!1,y.endContainer,y.endOffset)),C).cloneRange()).setEndAfter(c),o=t.extractContents(),b=o,U(qi(ir.fromDom(b),dr),function(e){var t=e.dom();t.nodeValue=ga(t.nodeValue)}),function(e){for(;Po.isText(e)&&(e.nodeValue=e.nodeValue.replace(/^[\r\n]+/,"")),e=e.firstChild;);}(o),r=o.firstChild,x.insertAfter(o,c),function(e,t,n){var r,o=n,i=[];if(o){for(;o=o.firstChild;){if(e.isBlock(o))return;Po.isElement(o)&&!t[o.nodeName.toLowerCase()]&&i.push(o)}for(r=i.length;r--;)!(o=i[r]).hasChildNodes()||o.firstChild===o.lastChild&&""===o.firstChild.nodeValue?e.remove(o):(a=e,(u=o)&&"A"===u.nodeName&&a.isEmpty(u)&&e.remove(o));var a,u}}(x,N,r),p=x,(h=c).normalize(),(v=h.lastChild)&&!/^(left|right)$/gi.test(p.getStyle(v,"float",!0))||p.add(h,"br"),x.isEmpty(c)&&rx(c),r.normalize(),x.isEmpty(r)?(x.remove(r),T()):KC(a,r)),x.setAttrib(r,"id",""),a.fire("NewBlock",{newBlock:r})))},lx=function(e,t){return YC(e).filter(function(e){return 0<t.length&&Or(ir.fromDom(e),t)}).isSome()},fx=function(e){return lx(e,yl(e))},dx=function(e){return lx(e,Cl(e))},mx=pf([{br:[]},{block:[]},{none:[]}]),gx=function(e,t){return dx(e)},px=function(n){return function(e,t){return""===vl(e)===n}},hx=function(n){return function(e,t){return JC(e)===n}},vx=function(n,r){return function(e,t){return GC(e)===n.toUpperCase()===r}},bx=function(e){return vx("pre",e)},yx=function(n){return function(e,t){return hl(e)===n}},Cx=function(e,t){return fx(e)},xx=function(e,t){return t},wx=function(e){var t=vl(e),n=XC(e.dom,e.selection.getStart());return n&&e.schema.isValidChild(n.nodeName,t||"P")},Nx=function(e,t){return function(n,r){return X(e,function(e,t){return e&&t(n,r)},!0)?A.some(t):A.none()}},Ex=function(e,t){return md([Nx([gx],mx.none()),Nx([vx("summary",!0)],mx.br()),Nx([bx(!0),yx(!1),xx],mx.br()),Nx([bx(!0),yx(!1)],mx.block()),Nx([bx(!0),yx(!0),xx],mx.block()),Nx([bx(!0),yx(!0)],mx.br()),Nx([hx(!0),xx],mx.br()),Nx([hx(!0)],mx.block()),Nx([px(!0),xx,wx],mx.block()),Nx([px(!0)],mx.br()),Nx([Cx],mx.br()),Nx([px(!1),xx],mx.br()),Nx([wx],mx.block())],[e,t.shiftKey]).getOr(mx.none())},Sx=function(e,t){Ex(e,t).fold(function(){Bg(e,t)},function(){cx(e,t)},o)},kx=function(o){o.on("keydown",function(e){var t,n,r;e.keyCode===xv.ENTER&&(t=o,(n=e).isDefaultPrevented()||(n.preventDefault(),(r=t.undoManager).typing&&(r.typing=!1,r.add()),t.undoManager.transact(function(){!1===t.selection.isCollapsed()&&t.execCommand("Delete"),Sx(t,n)})))})},Tx=function(n,r){var e=r.container(),t=r.offset();return Po.isText(e)?(e.insertData(t,n),A.some(vu(e,t+n.length))):Ts(r).map(function(e){var t=ir.fromText(n);return r.isAtEnd()?Ai(e,t):Ti(e,t),vu(t.dom(),n.length)})},Ax=d(Tx,"\xa0"),Rx=d(Tx," "),_x=function(e,t,n){return nc.navigateIgnore(e,t,n,Af)},Dx=function(e,t){return Y(ef(ir.fromDom(t.container()),e),mo)},Bx=function(e,n,r){return _x(e,n.dom(),r).forall(function(t){return Dx(n,r).fold(function(){return!1===bs(t,r,n.dom())},function(e){return!1===bs(t,r,n.dom())&&Lr(e,ir.fromDom(t.container()))})})},Ox=function(t,n,r){return Dx(n,r).fold(function(){return _x(t,n.dom(),r).forall(function(e){return!1===bs(e,r,n.dom())})},function(e){return _x(t,e.dom(),r).isNone()})},Px=d(Ox,!1),Ix=d(Ox,!0),Lx=d(Bx,!1),Mx=d(Bx,!0),Fx=function(e){return vu.isTextPosition(e)&&!e.isAtStart()&&!e.isAtEnd()},zx=function(e,t){var n=V(ef(ir.fromDom(t.container()),e),mo);return ne(n).getOr(e)},Ux=function(e,t){return Fx(t)?Tf(t):Tf(t)||nc.prevPosition(zx(e,t).dom(),t).exists(Tf)},Vx=function(e,t){return Fx(t)?kf(t):kf(t)||nc.nextPosition(zx(e,t).dom(),t).exists(kf)},Hx=function(e){return Ts(e).bind(function(e){return Yi(e,fr)}).exists(function(e){return t=Er(e,"white-space"),F(["pre","pre-wrap"],t);var t})},jx=function(e,t){return o=e,i=t,nc.prevPosition(o.dom(),i).isNone()||(n=e,r=t,nc.nextPosition(n.dom(),r).isNone())||Px(e,t)||Ix(e,t)||yf(e,t)||bf(e,t);var n,r,o,i},qx=function(e,t){var n,r,o,i=(r=(n=t).container(),o=n.offset(),Po.isText(r)&&o<r.data.length?vu(r,o+1):n);return!Hx(i)&&(Ix(e,i)||Mx(e,i)||bf(e,i)||Vx(e,i))},$x=function(e,t){return n=e,!Hx(r=t)&&(Px(n,r)||Lx(n,r)||yf(n,r)||Ux(n,r))||qx(e,t);var n,r},Wx=function(e,t){return wf(e.charAt(t))},Kx=function(e){var t=e.container();return Po.isText(t)&&Xn(t.data,"\xa0")},Xx=function(e){var t=e.data,n=W(t.split(""),function(e,t,n){return wf(e)&&0<t&&t<n.length-1&&Ef(n[t-1])&&Ef(n[t+1])?" ":e}).join("");return n!==t&&(e.data=n,!0)},Yx=function(l,e){return A.some(e).filter(Kx).bind(function(e){var t,n,r,o,i,a,u,s,c=e.container();return i=l,u=(a=c).data,s=vu(a,0),Wx(u,0)&&!$x(i,s)&&(a.data=" "+u.slice(1),1)||Xx(c)||(t=l,r=(n=c).data,o=vu(n,r.length-1),Wx(r,r.length-1)&&!$x(t,o)&&(n.data=r.slice(0,-1)+" ",1))?A.some(e):A.none()})},Gx=function(t){var e=ir.fromDom(t.getBody());t.selection.isCollapsed()&&Yx(e,vu.fromRangeStart(t.selection.getRng())).each(function(e){t.selection.setRng(e.toRange())})},Jx=function(r,o){return function(e){return t=r,!Hx(n=e)&&(jx(t,n)||Ux(t,n)||Vx(t,n))?Ax(o):Rx(o);var t,n}},Qx=function(e){var t,n,r=Cu.fromRangeStart(e.selection.getRng()),o=ir.fromDom(e.getBody());if(e.selection.isCollapsed()){var i=d(Ll.isInlineTarget,e),a=Cu.fromRangeStart(e.selection.getRng());return Dd(i,e.getBody(),a).bind((n=o,function(e){return e.fold(function(e){return nc.prevPosition(n.dom(),Cu.before(e))},function(e){return nc.firstPositionIn(e)},function(e){return nc.lastPositionIn(e)},function(e){return nc.nextPosition(n.dom(),Cu.after(e))})})).bind(Jx(o,r)).exists((t=e,function(e){return t.selection.setRng(e.toRange()),t.nodeChanged(),!0}))}return!1},Zx=function(r){r.on("keydown",function(e){var t,n;!1===e.isDefaultPrevented()&&(t=r,n=e,jC([{keyCode:xv.SPACEBAR,action:HC(Qx,t)}],n).each(function(e){n.preventDefault()}))})},ew=function(e,t){var n;t.hasAttribute("data-mce-caret")&&(ka(t),(n=e).selection.setRng(n.selection.getRng()),e.selection.scrollIntoView(t))},tw=function(e,t){var n,r=(n=e,Ji(ir.fromDom(n.getBody()),"*[data-mce-caret]").fold(q(null),function(e){return e.dom()}));if(r)return"compositionstart"===t.type?(t.preventDefault(),t.stopPropagation(),void ew(e,r)):void(Ca(r)&&(ew(e,r),e.undoManager.add()))},nw=function(e){e.on("keyup compositionstart",d(tw,e))},rw=rr.detect().browser,ow=function(t){var e,n;e=t,n=Ii(function(){e.composing||Gx(e)},0),rw.isIE()&&(e.on("keypress",function(e){n.throttle()}),e.on("remove",function(e){n.cancel()})),t.on("input",function(e){!1===e.isComposing&&Gx(t)})},iw=function(r){r.on("keydown",function(e){var t,n;!1===e.isDefaultPrevented()&&(t=r,n=e,jC([{keyCode:xv.END,action:bC(t,!0)},{keyCode:xv.HOME,action:bC(t,!1)}],n).each(function(e){n.preventDefault()}))})},aw=function(e){var t=jd.setupSelectedState(e);nw(e),qC(e,t),$C(e,t),kx(e),Zx(e),ow(e),iw(e)};function uw(u){var s,n,r,o=Jt.each,c=xv.BACKSPACE,l=xv.DELETE,f=u.dom,d=u.selection,e=u.settings,t=u.parser,i=ge.gecko,a=ge.ie,m=ge.webkit,g="data:text/mce-internal,",p=a?"Text":"URL",h=function(e,t){try{u.getDoc().execCommand(e,!1,t)}catch(n){}},v=function(e){return e.isDefaultPrevented()},b=function(){u.shortcuts.add("meta+a",null,"SelectAll")},y=function(){u.on("keydown",function(e){if(!v(e)&&e.keyCode===c&&d.isCollapsed()&&0===d.getRng().startOffset){var t=d.getNode().previousSibling;if(t&&t.nodeName&&"table"===t.nodeName.toLowerCase())return e.preventDefault(),!1}})},C=function(){u.inline||(u.contentStyles.push("body {min-height: 150px}"),u.on("click",function(e){var t;if("HTML"===e.target.nodeName){if(11<ge.ie)return void u.getBody().focus();t=u.selection.getRng(),u.getBody().focus(),u.selection.setRng(t),u.selection.normalize(),u.nodeChanged()}}))};return u.on("keydown",function(e){var t,n,r,o,i;if(!v(e)&&e.keyCode===xv.BACKSPACE&&(n=(t=d.getRng()).startContainer,r=t.startOffset,o=f.getRoot(),i=n,t.collapsed&&0===r)){for(;i&&i.parentNode&&i.parentNode.firstChild===i&&i.parentNode!==o;)i=i.parentNode;"BLOCKQUOTE"===i.tagName&&(u.formatter.toggle("blockquote",null,i),(t=f.createRng()).setStart(n,0),t.setEnd(n,0),d.setRng(t))}}),s=function(e){var t=f.create("body"),n=e.cloneContents();return t.appendChild(n),d.serializer.serialize(t,{format:"html"})},u.on("keydown",function(e){var t,n,r,o,i,a=e.keyCode;if(!v(e)&&(a===l||a===c)){if(t=u.selection.isCollapsed(),n=u.getBody(),t&&!f.isEmpty(n))return;if(!t&&(r=u.selection.getRng(),o=s(r),(i=f.createRng()).selectNode(u.getBody()),o!==s(i)))return;e.preventDefault(),u.setContent(""),n.firstChild&&f.isBlock(n.firstChild)?u.selection.setCursorLocation(n.firstChild,0):u.selection.setCursorLocation(n,0),u.nodeChanged()}}),ge.windowsPhone||u.on("keyup focusin mouseup",function(e){xv.modifierPressed(e)||d.normalize()},!0),m&&(u.settings.content_editable||f.bind(u.getDoc(),"mousedown mouseup",function(e){var t;if(e.target===u.getDoc().documentElement)if(t=d.getRng(),u.getBody().focus(),"mousedown"===e.type){if(ya(t.startContainer))return;d.placeCaretAt(e.clientX,e.clientY)}else d.setRng(t)}),u.on("click",function(e){var t=e.target;/^(IMG|HR)$/.test(t.nodeName)&&"false"!==f.getContentEditableParent(t)&&(e.preventDefault(),u.selection.select(t),u.nodeChanged()),"A"===t.nodeName&&f.hasClass(t,"mce-item-anchor")&&(e.preventDefault(),d.select(t))}),e.forced_root_block&&u.on("init",function(){h("DefaultParagraphSeparator",e.forced_root_block)}),u.on("init",function(){u.dom.bind(u.getBody(),"submit",function(e){e.preventDefault()})}),y(),t.addNodeFilter("br",function(e){for(var t=e.length;t--;)"Apple-interchange-newline"===e[t].attr("class")&&e[t].remove()}),ge.iOS?(u.inline||u.on("keydown",function(){H.document.activeElement===H.document.body&&u.getWin().focus()}),C(),u.on("click",function(e){var t=e.target;do{if("A"===t.tagName)return void e.preventDefault()}while(t=t.parentNode)}),u.contentStyles.push(".mce-content-body {-webkit-touch-callout: none}")):b()),11<=ge.ie&&(C(),y()),ge.ie&&(b(),h("AutoUrlDetect",!1),u.on("dragstart",function(e){var t,n,r;(t=e).dataTransfer&&(u.selection.isCollapsed()&&"IMG"===t.target.tagName&&d.select(t.target),0<(n=u.selection.getContent()).length&&(r=g+escape(u.id)+","+escape(n),t.dataTransfer.setData(p,r)))}),u.on("drop",function(e){if(!v(e)){var t=(i=e).dataTransfer&&(a=i.dataTransfer.getData(p))&&0<=a.indexOf(g)?(a=a.substr(g.length).split(","),{id:unescape(a[0]),html:unescape(a[1])}):null;if(t&&t.id!==u.id){e.preventDefault();var n=Ty(e.x,e.y,u.getDoc());d.setRng(n),r=t.html,o=!0,u.queryCommandSupported("mceInsertClipboardContent")?u.execCommand("mceInsertClipboardContent",!1,{content:r,internal:o}):u.execCommand("mceInsertContent",!1,r)}}var r,o,i,a})),i&&(u.on("keydown",function(e){if(!v(e)&&e.keyCode===c){if(!u.getBody().getElementsByTagName("hr").length)return;if(d.isCollapsed()&&0===d.getRng().startOffset){var t=d.getNode(),n=t.previousSibling;if("HR"===t.nodeName)return f.remove(t),void e.preventDefault();n&&n.nodeName&&"hr"===n.nodeName.toLowerCase()&&(f.remove(n),e.preventDefault())}}}),H.Range.prototype.getClientRects||u.on("mousedown",function(e){if(!v(e)&&"HTML"===e.target.nodeName){var t=u.getBody();t.blur(),ye.setEditorTimeout(u,function(){t.focus()})}}),n=function(){var e=f.getAttribs(d.getStart().cloneNode(!1));return function(){var t=d.getStart();t!==u.getBody()&&(f.setAttrib(t,"style",null),o(e,function(e){t.setAttributeNode(e.cloneNode(!0))}))}},r=function(){return!d.isCollapsed()&&f.getParent(d.getStart(),f.isBlock)!==f.getParent(d.getEnd(),f.isBlock)},u.on("keypress",function(e){var t;if(!v(e)&&(8===e.keyCode||46===e.keyCode)&&r())return t=n(),u.getDoc().execCommand("delete",!1,null),t(),e.preventDefault(),!1}),f.bind(u.getDoc(),"cut",function(e){var t;!v(e)&&r()&&(t=n(),ye.setEditorTimeout(u,function(){t()}))}),e.readonly||u.on("BeforeExecCommand MouseDown",function(){h("StyleWithCSS",!1),h("enableInlineTableEditing",!1),e.object_resizing||h("enableObjectResizing",!1)}),u.on("SetContent ExecCommand",function(e){"setcontent"!==e.type&&"mceInsertLink"!==e.command||o(f.select("a"),function(e){var t=e.parentNode,n=f.getRoot();if(t.lastChild===e){for(;t&&!f.isBlock(t);){if(t.parentNode.lastChild!==t||t===n)return;t=t.parentNode}f.add(t,"br",{"data-mce-bogus":1})}})}),u.contentStyles.push("img:-moz-broken {-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}"),ge.mac&&u.on("keydown",function(e){!xv.metaKeyPressed(e)||e.shiftKey||37!==e.keyCode&&39!==e.keyCode||(e.preventDefault(),u.selection.getSel().modify("move",37===e.keyCode?"backward":"forward","lineboundary"))}),y()),{refreshContentEditable:function(){},isHidden:function(){var e;return!i||u.removed?0:!(e=u.selection.getSel())||!e.rangeCount||0===e.rangeCount}}}var sw=function(e){return Po.isElement(e)&&ho(ir.fromDom(e))},cw=function(t){t.on("click",function(e){3<=e.detail&&function(e){var t=e.selection.getRng(),n=vu.fromRangeStart(t),r=vu.fromRangeEnd(t);if(vu.isElementPosition(n)){var o=n.container();sw(o)&&nc.firstPositionIn(o).each(function(e){return t.setStart(e.container(),e.offset())})}vu.isElementPosition(r)&&(o=n.container(),sw(o)&&nc.lastPositionIn(o).each(function(e){return t.setEnd(e.container(),e.offset())})),e.selection.setRng(rl(t))}(t)})},lw=function(e){var t,n;(t=e).on("click",function(e){t.dom.getParent(e.target,"details")&&e.preventDefault()}),(n=e).parser.addNodeFilter("details",function(e){U(e,function(e){e.attr("data-mce-open",e.attr("open")),e.attr("open","open")})}),n.serializer.addNodeFilter("details",function(e){U(e,function(e){var t=e.attr("data-mce-open");e.attr("open",R(t)?t:null),e.attr("data-mce-open",null)})})},fw=bi.DOM,dw=function(e){var t;e.bindPendingEventDelegates(),e.initialized=!0,e.fire("init"),e.focus(!0),e.nodeChanged({initial:!0}),e.execCallback("init_instance_callback",e),(t=e).settings.auto_focus&&ye.setEditorTimeout(t,function(){var e;(e=!0===t.settings.auto_focus?t:t.editorManager.get(t.settings.auto_focus)).destroyed||e.focus()},100)},mw=function(t,e){var n,r,u,o,i,a,s,c,l,f=t.settings,d=t.getElement(),m=t.getDoc();f.inline||(t.getElement().style.visibility=t.orgVisibility),e||f.content_editable||(m.open(),m.write(t.iframeHTML),m.close()),f.content_editable&&(t.on("remove",function(){var e=this.getBody();fw.removeClass(e,"mce-content-body"),fw.removeClass(e,"mce-edit-focus"),fw.setAttrib(e,"contentEditable",null)}),fw.addClass(d,"mce-content-body"),t.contentDocument=m=f.content_document||H.document,t.contentWindow=f.content_window||H.window,t.bodyElement=d,f.content_document=f.content_window=null,f.root_name=d.nodeName.toLowerCase()),(n=t.getBody()).disabled=!0,t.readonly=f.readonly,t.readonly||(t.inline&&"static"===fw.getStyle(n,"position",!0)&&(n.style.position="relative"),n.contentEditable=t.getParam("content_editable_state",!0)),n.disabled=!1,t.editorUpload=Lh(t),t.schema=ii(f),t.dom=bi(m,{keep_values:!0,url_converter:t.convertURL,url_converter_scope:t,hex_colors:f.force_hex_style_colors,class_filter:f.class_filter,update_styles:!0,root_element:t.inline?t.getBody():null,collect:f.content_editable,schema:t.schema,contentCssCors:Ol(t),onSetAttrib:function(e){t.fire("SetAttrib",e)}}),t.parser=((o=ly((u=t).settings,u.schema)).addAttributeFilter("src,href,style,tabindex",function(e,t){for(var n,r,o,i=e.length,a=u.dom;i--;)if(r=(n=e[i]).attr(t),o="data-mce-"+t,!n.attributes.map[o]){if(0===r.indexOf("data:")||0===r.indexOf("blob:"))continue;"style"===t?((r=a.serializeStyle(a.parseStyle(r),n.name)).length||(r=null),n.attr(o,r),n.attr(t,r)):"tabindex"===t?(n.attr(o,r),n.attr(t,null)):n.attr(o,u.convertURL(r,t,n.name))}}),o.addNodeFilter("script",function(e){for(var t,n,r=e.length;r--;)0!==(n=(t=e[r]).attr("type")||"no/type").indexOf("mce-")&&t.attr("type","mce-"+n)}),o.addNodeFilter("#cdata",function(e){for(var t,n=e.length;n--;)(t=e[n]).type=8,t.name="#comment",t.value="[CDATA["+t.value+"]]"}),o.addNodeFilter("p,h1,h2,h3,h4,h5,h6,div",function(e){for(var t,n=e.length,r=u.schema.getNonEmptyElements();n--;)(t=e[n]).isEmpty(r)&&0===t.getAll("br").length&&(t.append(new Zb("br",1)).shortEnded=!0)}),o),t.serializer=py(f,t),t.selection=tC(t.dom,t.getWin(),t.serializer,t),t.annotator=Mc(t),t.formatter=Vb(t),t.undoManager=Xv(t),t._nodeChangeDispatcher=new Wh(t),t._selectionOverrides=Sv(t),lw(t),cw(t),aw(t),Uh(t),t.fire("PreInit"),f.browser_spellcheck||f.gecko_spellcheck||(m.body.spellcheck=!1,fw.setAttrib(n,"spellcheck","false")),t.quirks=uw(t),t.fire("PostRender"),f.directionality&&(n.dir=f.directionality),f.nowrap&&(n.style.whiteSpace="nowrap"),f.protect&&t.on("BeforeSetContent",function(t){Jt.each(f.protect,function(e){t.content=t.content.replace(e,function(e){return"\x3c!--mce:protected "+escape(e)+"--\x3e"})})}),t.on("SetContent",function(){t.addVisual(t.getBody())}),t.load({initial:!0,format:"html"}),t.startContent=t.getContent({format:"raw"}),t.on("compositionstart compositionend",function(e){t.composing="compositionstart"===e.type}),0<t.contentStyles.length&&(r="",Jt.each(t.contentStyles,function(e){r+=e+"\r\n"}),t.dom.addStyle(r)),(i=t,i.inline?fw.styleSheetLoader:i.dom.styleSheetLoader).loadAll(t.contentCSS,function(e){dw(t)},function(e){dw(t)}),f.content_style&&(a=t,s=f.content_style,c=ir.fromDom(a.getDoc().head),l=ir.fromTag("style"),yr(l,"type","text/css"),_i(l,ir.fromText(s)),_i(c,l))},gw=bi.DOM,pw=function(e,t){var n,r,o,i,a,u,s,c=e.editorManager.translate("Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help"),l=(n=e.id,r=c,o=t.height,i=ll(e),s=ir.fromTag("iframe"),Cr(s,i),Cr(s,{id:n+"_ifr",frameBorder:"0",allowTransparency:"true",title:r}),Nr(s,{width:"100%",height:(a=o,u="number"==typeof a?a+"px":a,u||""),display:"block"}),s).dom();l.onload=function(){l.onload=null,e.fire("load")};var f,d,m,g,p=function(e,t){if(H.document.domain!==H.window.location.hostname&&ge.ie&&ge.ie<12){var n=Ih.uuid("mce");e[n]=function(){mw(e)};var r='javascript:(function(){document.open();document.domain="'+H.document.domain+'";var ed = window.parent.tinymce.get("'+e.id+'");document.write(ed.iframeHTML);document.close();ed.'+n+"(true);})()";return gw.setAttrib(t,"src",r),!0}return!1}(e,l);return e.contentAreaContainer=t.iframeContainer,e.iframeElement=l,e.iframeHTML=(g=fl(f=e)+"<html><head>",dl(f)!==f.documentBaseUrl&&(g+='<base href="'+f.documentBaseURI.getURI()+'" />'),g+='<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />',d=ml(f),m=gl(f),pl(f)&&(g+='<meta http-equiv="Content-Security-Policy" content="'+pl(f)+'" />'),g+='</head><body id="'+d+'" class="mce-content-body '+m+'" data-id="'+f.id+'"><br></body></html>'),gw.add(t.iframeContainer,l),p},hw=function(e,t){var n=pw(e,t);t.editorContainer&&(gw.get(t.editorContainer).style.display=e.orgDisplay,e.hidden=gw.isHidden(t.editorContainer)),e.getElement().style.display="none",gw.setAttrib(e.id,"aria-hidden","true"),n||mw(e)},vw=bi.DOM,bw=function(t,n,e){var r=Ch.get(e),o=Ch.urls[e]||t.documentBaseUrl.replace(/\/$/,"");if(e=Jt.trim(e),r&&-1===Jt.inArray(n,e)){if(Jt.each(Ch.dependencies(e),function(e){bw(t,n,e)}),t.plugins[e])return;try{var i=new r(t,o,t.$);(t.plugins[e]=i).init&&(i.init(t,o),n.push(e))}catch(ZN){yh.pluginInitError(t,e,ZN)}}},yw=function(e){return e.replace(/^\-/,"")},Cw=function(e){return{editorContainer:e,iframeContainer:e}},xw=function(e){var t,n,r=e.getElement();return e.inline?Cw(null):(t=r,n=vw.create("div"),vw.insertAfter(n,t),Cw(n))},ww=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=e.settings,m=e.getElement();return e.orgDisplay=m.style.display,R(d.theme)?(l=(o=e).settings,f=o.getElement(),i=l.width||vw.getStyle(f,"width")||"100%",a=l.height||vw.getStyle(f,"height")||f.offsetHeight,u=l.min_height||100,(s=/^[0-9\.]+(|px)$/i).test(""+i)&&(i=Math.max(parseInt(i,10),100)),s.test(""+a)&&(a=Math.max(parseInt(a,10),u)),c=o.theme.renderUI({targetNode:f,width:i,height:a,deltaWidth:l.delta_width,deltaHeight:l.delta_height}),l.content_editable||(a=(c.iframeHeight||a)+("number"==typeof a?c.deltaHeight||0:""))<u&&(a=u),c.height=a,c):P(d.theme)?(r=(t=e).getElement(),(n=t.settings.theme(t,r)).editorContainer.nodeType&&(n.editorContainer.id=n.editorContainer.id||t.id+"_parent"),n.iframeContainer&&n.iframeContainer.nodeType&&(n.iframeContainer.id=n.iframeContainer.id||t.id+"_iframecontainer"),n.height=n.iframeHeight?n.iframeHeight:r.offsetHeight,n):xw(e)},Nw=function(t){var e,n,r,o,i,a,u=t.settings,s=t.getElement();return t.rtl=u.rtl_ui||t.editorManager.i18n.rtl,t.editorManager.i18n.setCode(u.language),u.aria_label=u.aria_label||vw.getAttrib(s,"aria-label",t.getLang("aria.rich_text_area")),t.fire("ScriptsLoaded"),o=(n=t).settings.theme,R(o)?(n.settings.theme=yw(o),r=xh.get(o),n.theme=new r(n,xh.urls[o]),n.theme.init&&n.theme.init(n,xh.urls[o]||n.documentBaseUrl.replace(/\/$/,""),n.$)):n.theme={},i=t,a=[],Jt.each(i.settings.plugins.split(/[ ,]/),function(e){bw(i,a,yw(e))}),e=ww(t),t.editorContainer=e.editorContainer?e.editorContainer:null,u.content_css&&Jt.each(Jt.explode(u.content_css),function(e){t.contentCSS.push(t.documentBaseURI.toAbsolute(e))}),u.content_editable?mw(t):hw(t,e)},Ew=bi.DOM,Sw=function(e){return"-"===e.charAt(0)},kw=function(i,a){var u=Ni.ScriptLoader;!function(e,t,n,r){var o=t.settings,i=o.theme;if(R(i)){if(!Sw(i)&&!xh.urls.hasOwnProperty(i)){var a=o.theme_url;a?xh.load(i,t.documentBaseURI.toAbsolute(a)):xh.load(i,"themes/"+i+"/theme"+n+".js")}e.loadQueue(function(){xh.waitFor(i,r)})}else r()}(u,i,a,function(){var e,t,n,r,o;e=u,(n=(t=i).settings).language&&"en"!==n.language&&!n.language_url&&(n.language_url=t.editorManager.baseURL+"/langs/"+n.language+".js"),n.language_url&&!t.editorManager.i18n.data[n.language]&&e.add(n.language_url),r=i.settings,o=a,Jt.isArray(r.plugins)&&(r.plugins=r.plugins.join(" ")),Jt.each(r.external_plugins,function(e,t){Ch.load(t,e),r.plugins+=" "+t}),Jt.each(r.plugins.split(/[ ,]/),function(e){if((e=Jt.trim(e))&&!Ch.urls[e])if(Sw(e)){e=e.substr(1,e.length);var t=Ch.dependencies(e);Jt.each(t,function(e){var t={prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"};e=Ch.createUrl(t,e),Ch.load(e.resource,e)})}else Ch.load(e,{prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"})}),u.loadQueue(function(){i.removed||Nw(i)},i,function(e){yh.pluginLoadError(i,e[0]),i.removed||Nw(i)})})},Tw=function(t){var e=t.settings,n=t.id,r=function(){Ew.unbind(H.window,"ready",r),t.render()};if(Ae.Event.domLoaded){if(t.getElement()&&ge.contentEditable){e.inline?t.inline=!0:(t.orgVisibility=t.getElement().style.visibility,t.getElement().style.visibility="hidden");var o=t.getElement().form||Ew.getParent(n,"form");o&&(t.formElement=o,e.hidden_input&&!/TEXTAREA|INPUT/i.test(t.getElement().nodeName)&&(Ew.insertAfter(Ew.create("input",{type:"hidden",name:n}),n),t.hasHiddenInput=!0),t.formEventDelegate=function(e){t.fire(e.type,e)},Ew.bind(o,"submit reset",t.formEventDelegate),t.on("reset",function(){t.setContent(t.startContent,{format:"raw"})}),!e.submit_patch||o.submit.nodeType||o.submit.length||o._mceOldSubmit||(o._mceOldSubmit=o.submit,o.submit=function(){return t.editorManager.triggerSave(),t.setDirty(!1),o._mceOldSubmit(o)})),t.windowManager=lh(t),t.notificationManager=ch(t),"xml"===e.encoding&&t.on("GetContent",function(e){e.save&&(e.content=Ew.encode(e.content))}),e.add_form_submit_trigger&&t.on("submit",function(){t.initialized&&t.save()}),e.add_unload_trigger&&(t._beforeUnload=function(){!t.initialized||t.destroyed||t.isHidden()||t.save({format:"raw",no_events:!0,set_dirty:!1})},t.editorManager.on("BeforeUnload",t._beforeUnload)),t.editorManager.add(t),kw(t,t.suffix)}}else Ew.bind(H.window,"ready",r)},Aw=function(e,t,n){var r=e.sidebars?e.sidebars:[];r.push({name:t,settings:n}),e.sidebars=r},Rw=Jt.each,_w=Jt.trim,Dw="source protocol authority userInfo user password host port relative path directory file query anchor".split(" "),Bw={ftp:21,http:80,https:443,mailto:25},Ow=function(r,e){var t,n,o=this;if(r=_w(r),t=(e=o.settings=e||{}).base_uri,/^([\w\-]+):([^\/]{2})/i.test(r)||/^\s*#/.test(r))o.source=r;else{var i=0===r.indexOf("//");0!==r.indexOf("/")||i||(r=(t&&t.protocol||"http")+"://mce_host"+r),/^[\w\-]*:?\/\//.test(r)||(n=e.base_uri?e.base_uri.path:new Ow(H.document.location.href).directory,""==e.base_uri.protocol?r="//mce_host"+o.toAbsPath(n,r):(r=/([^#?]*)([#?]?.*)/.exec(r),r=(t&&t.protocol||"http")+"://mce_host"+o.toAbsPath(n,r[1])+r[2])),r=r.replace(/@@/g,"(mce_at)"),r=/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(r),Rw(Dw,function(e,t){var n=r[t];n&&(n=n.replace(/\(mce_at\)/g,"@@")),o[e]=n}),t&&(o.protocol||(o.protocol=t.protocol),o.userInfo||(o.userInfo=t.userInfo),o.port||"mce_host"!==o.host||(o.port=t.port),o.host&&"mce_host"!==o.host||(o.host=t.host),o.source=""),i&&(o.protocol="")}};Ow.prototype={setPath:function(e){e=/^(.*?)\/?(\w+)?$/.exec(e),this.path=e[0],this.directory=e[1],this.file=e[2],this.source="",this.getURI()},toRelative:function(e){var t;if("./"===e)return e;if("mce_host"!==(e=new Ow(e,{base_uri:this})).host&&this.host!==e.host&&e.host||this.port!==e.port||this.protocol!==e.protocol&&""!==e.protocol)return e.getURI();var n=this.getURI(),r=e.getURI();return n===r||"/"===n.charAt(n.length-1)&&n.substr(0,n.length-1)===r?n:(t=this.toRelPath(this.path,e.path),e.query&&(t+="?"+e.query),e.anchor&&(t+="#"+e.anchor),t)},toAbsolute:function(e,t){return(e=new Ow(e,{base_uri:this})).getURI(t&&this.isSameOrigin(e))},isSameOrigin:function(e){if(this.host==e.host&&this.protocol==e.protocol){if(this.port==e.port)return!0;var t=Bw[this.protocol];if(t&&(this.port||t)==(e.port||t))return!0}return!1},toRelPath:function(e,t){var n,r,o,i=0,a="";if(e=(e=e.substring(0,e.lastIndexOf("/"))).split("/"),n=t.split("/"),e.length>=n.length)for(r=0,o=e.length;r<o;r++)if(r>=n.length||e[r]!==n[r]){i=r+1;break}if(e.length<n.length)for(r=0,o=n.length;r<o;r++)if(r>=e.length||e[r]!==n[r]){i=r+1;break}if(1===i)return t;for(r=0,o=e.length-(i-1);r<o;r++)a+="../";for(r=i-1,o=n.length;r<o;r++)a+=r!==i-1?"/"+n[r]:n[r];return a},toAbsPath:function(e,t){var n,r,o,i=0,a=[];for(r=/\/$/.test(t)?"/":"",e=e.split("/"),t=t.split("/"),Rw(e,function(e){e&&a.push(e)}),e=a,n=t.length-1,a=[];0<=n;n--)0!==t[n].length&&"."!==t[n]&&(".."!==t[n]?0<i?i--:a.push(t[n]):i++);return 0!==(o=(n=e.length-i)<=0?a.reverse().join("/"):e.slice(0,n).join("/")+"/"+a.reverse().join("/")).indexOf("/")&&(o="/"+o),r&&o.lastIndexOf("/")!==o.length-1&&(o+=r),o},getURI:function(e){var t,n=this;return n.source&&!e||(t="",e||(n.protocol?t+=n.protocol+"://":t+="//",n.userInfo&&(t+=n.userInfo+"@"),n.host&&(t+=n.host),n.port&&(t+=":"+n.port)),n.path&&(t+=n.path),n.query&&(t+="?"+n.query),n.anchor&&(t+="#"+n.anchor),n.source=t),n.source}},Ow.parseDataUri=function(e){var t,n;return e=decodeURIComponent(e).split(","),(n=/data:([^;]+)/.exec(e[0]))&&(t=n[1]),{type:t,data:e[1]}},Ow.getDocumentBaseUrl=function(e){var t;return t=0!==e.protocol.indexOf("http")&&"file:"!==e.protocol?e.href:e.protocol+"//"+e.host+e.pathname,/^[^:]+:\/\/\/?[^\/]+\//.test(t)&&(t=t.replace(/[\?#].*$/,"").replace(/[\/\\][^\/]+$/,""),/[\/\\]$/.test(t)||(t+="/")),t};var Pw=function(e,t,n){var r,o,i,a,u;if(t.format=t.format?t.format:"html",t.get=!0,t.getInner=!0,t.no_events||e.fire("BeforeGetContent",t),"raw"===t.format)r=Jt.trim(Dv.trimExternal(e.serializer,n.innerHTML));else if("text"===t.format)r=ga(n.innerText||n.textContent);else{if("tree"===t.format)return e.serializer.serialize(n,t);i=(o=e).serializer.serialize(n,t),a=vl(o),u=new RegExp("^(<"+a+"[^>]*>( | |\\s|\xa0|<br \\/>|)<\\/"+a+">[\r\n]*|<br \\/>[\r\n]*)$"),r=i.replace(u,"")}return"text"===t.format||wo(ir.fromDom(n))?t.content=r:t.content=Jt.trim(r),t.no_events||e.fire("GetContent",t),t.content},Iw=function(e,t){t(e),e.firstChild&&Iw(e.firstChild,t),e.next&&Iw(e.next,t)},Lw=function(e,t,n){var r=function(e,n,t){var r={},o={},i=[];for(var a in t.firstChild&&Iw(t.firstChild,function(t){U(e,function(e){e.name===t.name&&(r[e.name]?r[e.name].nodes.push(t):r[e.name]={filter:e,nodes:[t]})}),U(n,function(e){"string"==typeof t.attr(e.name)&&(o[e.name]?o[e.name].nodes.push(t):o[e.name]={filter:e,nodes:[t]})})}),r)r.hasOwnProperty(a)&&i.push(r[a]);for(var a in o)o.hasOwnProperty(a)&&i.push(o[a]);return i}(e,t,n);U(r,function(t){U(t.filter.callbacks,function(e){e(t.nodes,t.filter.name,{})})})},Mw=function(e){return e instanceof Zb},Fw=function(e,t){var r;e.dom.setHTML(e.getBody(),t),eh(r=e)&&nc.firstPositionIn(r.getBody()).each(function(e){var t=e.getNode(),n=Po.isTable(t)?nc.firstPositionIn(t).getOr(e):e;r.selection.setRng(n.toRange())})},zw=function(u,s,c){return void 0===c&&(c={}),c.format=c.format?c.format:"html",c.set=!0,c.content=Mw(s)?"":s,Mw(s)||c.no_events||(u.fire("BeforeSetContent",c),s=c.content),A.from(u.getBody()).fold(q(s),function(e){return Mw(s)?function(e,t,n,r){Lw(e.parser.getNodeFilters(),e.parser.getAttributeFilters(),n);var o=el({validate:e.validate},e.schema).serialize(n);return r.content=wo(ir.fromDom(t))?o:Jt.trim(o),Fw(e,r.content),r.no_events||e.fire("SetContent",r),n}(u,e,s,c):(t=u,n=e,o=c,0===(r=s).length||/^\s+$/.test(r)?(a='<br data-mce-bogus="1">',"TABLE"===n.nodeName?r="<tr><td>"+a+"</td></tr>":/^(UL|OL)$/.test(n.nodeName)&&(r="<li>"+a+"</li>"),(i=vl(t))&&t.schema.isValidChild(n.nodeName.toLowerCase(),i.toLowerCase())?(r=a,r=t.dom.createHTML(i,t.settings.forced_root_block_attrs,r)):r||(r='<br data-mce-bogus="1">'),Fw(t,r),t.fire("SetContent",o)):("raw"!==o.format&&(r=el({validate:t.validate},t.schema).serialize(t.parser.parse(r,{isRootContent:!0,insert:!0}))),o.content=wo(ir.fromDom(n))?r:Jt.trim(r),Fw(t,o.content),o.no_events||t.fire("SetContent",o)),o.content);var t,n,r,o,i,a})},Uw=bi.DOM,Vw=function(e){return A.from(e).each(function(e){return e.destroy()})},Hw=function(e){if(!e.removed){var t=e._selectionOverrides,n=e.editorUpload,r=e.getBody(),o=e.getElement();r&&e.save({is_removing:!0}),e.removed=!0,e.unbindAllNativeEvents(),e.hasHiddenInput&&o&&Uw.remove(o.nextSibling),gp(e),e.editorManager.remove(e),!e.inline&&r&&(i=e,Uw.setStyle(i.id,"display",i.orgDisplay)),pp(e),Uw.remove(e.getContainer()),Vw(t),Vw(n),e.destroy()}var i},jw=function(e,t){var n,r,o,i=e.selection,a=e.dom;e.destroyed||(t||e.removed?(t||(e.editorManager.off("beforeunload",e._beforeUnload),e.theme&&e.theme.destroy&&e.theme.destroy(),Vw(i),Vw(a)),(r=(n=e).formElement)&&(r._mceOldSubmit&&(r.submit=r._mceOldSubmit,r._mceOldSubmit=null),Uw.unbind(r,"submit reset",n.formEventDelegate)),(o=e).contentAreaContainer=o.formElement=o.container=o.editorContainer=null,o.bodyElement=o.contentDocument=o.contentWindow=null,o.iframeElement=o.targetElm=null,o.selection&&(o.selection=o.selection.win=o.selection.dom=o.selection.dom.doc=null),e.destroyed=!0):e.remove())},qw=bi.DOM,$w=Jt.extend,Ww=Jt.each,Kw=Jt.resolve,Xw=ge.ie,Yw=function(e,t,n){var r,o,i,a,u,s,c,l=this,f=l.documentBaseUrl=n.documentBaseURL,d=n.baseURI;r=l,o=e,i=f,a=n.defaultSettings,u=t,c={id:o,theme:"modern",delta_width:0,delta_height:0,popup_css:"",plugins:"",document_base_url:i,add_form_submit_trigger:!0,submit_patch:!0,add_unload_trigger:!0,convert_urls:!0,relative_urls:!0,remove_script_host:!0,object_resizing:!0,doctype:"<!DOCTYPE html>",visual:!0,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",font_size_legacy_values:"xx-small,small,medium,large,x-large,xx-large,300%",forced_root_block:"p",hidden_input:!0,render_ui:!0,indentation:"40px",inline_styles:!0,convert_fonts_to_spans:!0,indent:"simple",indent_before:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",indent_after:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",entity_encoding:"named",url_converter:(s=r).convertURL,url_converter_scope:s,ie7_compat:!0},t=Mp(_p,c,a,u),l.settings=t,ki.language=t.language||"en",ki.languageLoad=t.language_load,ki.baseURL=n.baseURL,l.id=e,l.setDirty(!1),l.plugins={},l.documentBaseURI=new Ow(t.document_base_url,{base_uri:d}),l.baseURI=d,l.contentCSS=[],l.contentStyles=[],l.shortcuts=new qp(l),l.loadedCSS={},l.editorCommands=new ap(l),l.suffix=n.suffix,l.editorManager=n,l.inline=t.inline,l.buttons={},l.menuItems={},t.cache_suffix&&(ge.cacheSuffix=t.cache_suffix.replace(/^[\?\&]+/,"")),!1===t.override_viewport&&(ge.overrideViewPort=!1),n.fire("SetupEditor",{editor:l}),l.execCallback("setup",l),l.$=vn.overrideDefaults(function(){return{context:l.inline?l.getBody():l.getDoc(),element:l.getBody()}})};$w(Yw.prototype={render:function(){Tw(this)},focus:function(e){Zp(this,e)},hasFocus:function(){return eh(this)},execCallback:function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r,o=this.settings[e];if(o)return this.callbackLookup&&(r=this.callbackLookup[e])&&(o=r.func,r=r.scope),"string"==typeof o&&(r=(r=o.replace(/\.\w+$/,""))?Kw(r):0,o=Kw(o),this.callbackLookup=this.callbackLookup||{},this.callbackLookup[e]={func:o,scope:r}),o.apply(r||this,Array.prototype.slice.call(arguments,1))},translate:function(e){if(e&&Jt.is(e,"string")){var n=this.settings.language||"en",r=this.editorManager.i18n;e=r.data[n+"."+e]||e.replace(/\{\#([^\}]+)\}/g,function(e,t){return r.data[n+"."+t]||"{#"+t+"}"})}return this.editorManager.translate(e)},getLang:function(e,t){return this.editorManager.i18n.data[(this.settings.language||"en")+"."+e]||(t!==undefined?t:"{#"+e+"}")},getParam:function(e,t,n){return zp(this,e,t,n)},nodeChanged:function(e){this._nodeChangeDispatcher.nodeChanged(e)},addButton:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),t.stateSelector&&"undefined"==typeof t.active&&(t.active=!1),t.text||t.icon||(t.icon=e),t.tooltip=t.tooltip||t.title,n.buttons[e]=t},addSidebar:function(e,t){return Aw(this,e,t)},addMenuItem:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),n.menuItems[e]=t},addContextToolbar:function(e,t){var n,r=this;r.contextToolbars=r.contextToolbars||[],"string"==typeof e&&(n=e,e=function(e){return r.dom.is(e,n)}),r.contextToolbars.push({id:Ih.uuid("mcet"),predicate:e,items:t})},addCommand:function(e,t,n){this.editorCommands.addCommand(e,t,n)},addQueryStateHandler:function(e,t,n){this.editorCommands.addQueryStateHandler(e,t,n)},addQueryValueHandler:function(e,t,n){this.editorCommands.addQueryValueHandler(e,t,n)},addShortcut:function(e,t,n,r){this.shortcuts.add(e,t,n,r)},execCommand:function(e,t,n,r){return this.editorCommands.execCommand(e,t,n,r)},queryCommandState:function(e){return this.editorCommands.queryCommandState(e)},queryCommandValue:function(e){return this.editorCommands.queryCommandValue(e)},queryCommandSupported:function(e){return this.editorCommands.queryCommandSupported(e)},show:function(){this.hidden&&(this.hidden=!1,this.inline?this.getBody().contentEditable=!0:(qw.show(this.getContainer()),qw.hide(this.id)),this.load(),this.fire("show"))},hide:function(){var e=this,t=e.getDoc();e.hidden||(Xw&&t&&!e.inline&&t.execCommand("SelectAll"),e.save(),e.inline?(e.getBody().contentEditable=!1,e===e.editorManager.focusedEditor&&(e.editorManager.focusedEditor=null)):(qw.hide(e.getContainer()),qw.setStyle(e.id,"display",e.orgDisplay)),e.hidden=!0,e.fire("hide"))},isHidden:function(){return!!this.hidden},setProgressState:function(e,t){this.fire("ProgressState",{state:e,time:t})},load:function(e){var t,n=this.getElement();return this.removed?"":n?((e=e||{}).load=!0,t=this.setContent(n.value!==undefined?n.value:n.innerHTML,e),e.element=n,e.no_events||this.fire("LoadContent",e),e.element=n=null,t):void 0},save:function(e){var t,n,r=this,o=r.getElement();if(o&&r.initialized&&!r.removed)return(e=e||{}).save=!0,e.element=o,e.content=r.getContent(e),e.no_events||r.fire("SaveContent",e),"raw"===e.format&&r.fire("RawSaveContent",e),t=e.content,/TEXTAREA|INPUT/i.test(o.nodeName)?o.value=t:(!e.is_removing&&r.inline||(o.innerHTML=t),(n=qw.getParent(r.id,"form"))&&Ww(n.elements,function(e){if(e.name===r.id)return e.value=t,!1})),e.element=o=null,!1!==e.set_dirty&&r.setDirty(!1),t},setContent:function(e,t){return zw(this,e,t)},getContent:function(e){return t=this,void 0===(n=e)&&(n={}),A.from(t.getBody()).fold(q("tree"===n.format?new Zb("body",11):""),function(e){return Pw(t,n,e)});var t,n},insertContent:function(e,t){t&&(e=$w({content:e},t)),this.execCommand("mceInsertContent",!1,e)},isDirty:function(){return!this.isNotDirty},setDirty:function(e){var t=!this.isNotDirty;this.isNotDirty=!e,e&&e!==t&&this.fire("dirty")},setMode:function(e){var t,n;(n=e)!==wp(t=this)&&(t.initialized?xp(t,"readonly"===n):t.on("init",function(){xp(t,"readonly"===n)}),hp(t,n))},getContainer:function(){return this.container||(this.container=qw.get(this.editorContainer||this.id+"_parent")),this.container},getContentAreaContainer:function(){return this.contentAreaContainer},getElement:function(){return this.targetElm||(this.targetElm=qw.get(this.id)),this.targetElm},getWin:function(){var e;return this.contentWindow||(e=this.iframeElement)&&(this.contentWindow=e.contentWindow),this.contentWindow},getDoc:function(){var e;return this.contentDocument||(e=this.getWin())&&(this.contentDocument=e.document),this.contentDocument},getBody:function(){var e=this.getDoc();return this.bodyElement||(e?e.body:null)},convertURL:function(e,t,n){var r=this.settings;return r.urlconverter_callback?this.execCallback("urlconverter_callback",e,n,!0,t):!r.convert_urls||n&&"LINK"===n.nodeName||0===e.indexOf("file:")||0===e.length?e:r.relative_urls?this.documentBaseURI.toRelative(e):e=this.documentBaseURI.toAbsolute(e,r.remove_script_host)},addVisual:function(e){var n,r=this,o=r.settings,i=r.dom;e=e||r.getBody(),r.hasVisual===undefined&&(r.hasVisual=o.visual),Ww(i.select("table,a",e),function(e){var t;switch(e.nodeName){case"TABLE":return n=o.visual_table_class||"mce-item-table",void((t=i.getAttrib(e,"border"))&&"0"!==t||!r.hasVisual?i.removeClass(e,n):i.addClass(e,n));case"A":return void(i.getAttrib(e,"href")||(t=i.getAttrib(e,"name")||e.id,n=o.visual_anchor_class||"mce-item-anchor",t&&r.hasVisual?i.addClass(e,n):i.removeClass(e,n)))}}),r.fire("VisualAid",{element:e,hasVisual:r.hasVisual})},remove:function(){Hw(this)},destroy:function(e){jw(this,e)},uploadImages:function(e){return this.editorUpload.uploadImages(e)},_scanForImages:function(){return this.editorUpload.scanForImages()}},Ap);var Gw,Jw,Qw,Zw={isEditorUIElement:function(e){return-1!==e.className.toString().indexOf("mce-")}},eN=function(n,e){var t,r;rr.detect().browser.isIE()?(r=n).on("focusout",function(){Gg(r)}):(t=e,n.on("mouseup touchend",function(e){t.throttle()})),n.on("keyup nodechange",function(e){var t;"nodechange"===(t=e).type&&t.selectionChange||Gg(n)})},tN=function(e){var t,n,r,o=Ii(function(){Gg(e)},0);e.inline&&(t=e,n=o,r=function(){n.throttle()},bi.DOM.bind(H.document,"mouseup",r),t.on("remove",function(){bi.DOM.unbind(H.document,"mouseup",r)})),e.on("init",function(){eN(e,o)}),e.on("remove",function(){o.cancel()})},nN=bi.DOM,rN=function(e){return Zw.isEditorUIElement(e)},oN=function(t,e){var n=t?t.settings.custom_ui_selector:"";return null!==nN.getParent(e,function(e){return rN(e)||!!n&&t.dom.is(e,n)})},iN=function(r,e){var t=e.editor;tN(t),t.on("focusin",function(){var e=r.focusedEditor;e!==this&&(e&&e.fire("blur",{focusedEditor:this}),r.setActive(this),(r.focusedEditor=this).fire("focus",{blurredEditor:e}),this.focus(!0))}),t.on("focusout",function(){var t=this;ye.setEditorTimeout(t,function(){var e=r.focusedEditor;oN(t,function(){try{return H.document.activeElement}catch(e){return H.document.body}}())||e!==t||(t.fire("blur",{focusedEditor:null}),r.focusedEditor=null)})}),Gw||(Gw=function(e){var t,n=r.activeEditor;t=e.target,n&&t.ownerDocument===H.document&&(t===H.document.body||oN(n,t)||r.focusedEditor!==n||(n.fire("blur",{focusedEditor:null}),r.focusedEditor=null))},nN.bind(H.document,"focusin",Gw))},aN=function(e,t){e.focusedEditor===t.editor&&(e.focusedEditor=null),e.activeEditor||(nN.unbind(H.document,"focusin",Gw),Gw=null)},uN=function(e){e.on("AddEditor",d(iN,e)),e.on("RemoveEditor",d(aN,e))},sN=bi.DOM,cN=Jt.explode,lN=Jt.each,fN=Jt.extend,dN=0,mN=!1,gN=[],pN=[],hN=function(t){var n=t.type;lN(Qw.get(),function(e){switch(n){case"scroll":e.fire("ScrollWindow",t);break;case"resize":e.fire("ResizeWindow",t)}})},vN=function(e){e!==mN&&(e?vn(window).on("resize scroll",hN):vn(window).off("resize scroll",hN),mN=e)},bN=function(t){var e=pN;delete gN[t.id];for(var n=0;n<gN.length;n++)if(gN[n]===t){gN.splice(n,1);break}return pN=V(pN,function(e){return t!==e}),Qw.activeEditor===t&&(Qw.activeEditor=0<pN.length?pN[0]:null),Qw.focusedEditor===t&&(Qw.focusedEditor=null),e.length!==pN.length};fN(Qw={defaultSettings:{},$:vn,majorVersion:"4",minorVersion:"9.5",releaseDate:"2019-07-02",editors:gN,i18n:mh,activeEditor:null,settings:{},setup:function(){var e,t,n,r,o="";if(t=Ow.getDocumentBaseUrl(H.document.location),/^[^:]+:\/\/\/?[^\/]+\//.test(t)&&(t=t.replace(/[\?#].*$/,"").replace(/[\/\\][^\/]+$/,""),/[\/\\]$/.test(t)||(t+="/")),n=window.tinymce||window.tinyMCEPreInit)e=n.base||n.baseURL,o=n.suffix;else{for(var i=H.document.getElementsByTagName("script"),a=0;a<i.length;a++){var u=(r=i[a].src).substring(r.lastIndexOf("/"));if(/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(r)){-1!==u.indexOf(".min")&&(o=".min"),e=r.substring(0,r.lastIndexOf("/"));break}}!e&&H.document.currentScript&&(-1!==(r=H.document.currentScript.src).indexOf(".min")&&(o=".min"),e=r.substring(0,r.lastIndexOf("/")))}this.baseURL=new Ow(t).toAbsolute(e),this.documentBaseURL=t,this.baseURI=new Ow(this.baseURL),this.suffix=o,uN(this)},overrideDefaults:function(e){var t,n;(t=e.base_url)&&(this.baseURL=new Ow(this.documentBaseURL).toAbsolute(t.replace(/\/+$/,"")),this.baseURI=new Ow(this.baseURL)),n=e.suffix,e.suffix&&(this.suffix=n);var r=(this.defaultSettings=e).plugin_base_urls;for(var o in r)ki.PluginManager.urls[o]=r[o]},init:function(r){var n,u,s=this;u=Jt.makeMap("area base basefont br col frame hr img input isindex link meta param embed source wbr track colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu"," ");var c=function(e){var t=e.id;return t||(t=(t=e.name)&&!sN.get(t)?e.name:sN.uniqueId(),e.setAttribute("id",t)),t},l=function(e,t){return t.constructor===RegExp?t.test(e.className):sN.hasClass(e,t)},f=function(e){n=e},e=function(){var o,i=0,a=[],n=function(e,t,n){var r=new Yw(e,t,s);a.push(r),r.on("init",function(){++i===o.length&&f(a)}),r.targetElm=r.targetElm||n,r.render()};sN.unbind(window,"ready",e),function(e){var t=r[e];t&&t.apply(s,Array.prototype.slice.call(arguments,2))}("onpageload"),o=vn.unique(function(t){var e,n=[];if(ge.ie&&ge.ie<11)return yh.initError("TinyMCE does not support the browser you are using. For a list of supported browsers please see: https://www.tinymce.com/docs/get-started/system-requirements/"),[];if(t.types)return lN(t.types,function(e){n=n.concat(sN.select(e.selector))}),n;if(t.selector)return sN.select(t.selector);if(t.target)return[t.target];switch(t.mode){case"exact":0<(e=t.elements||"").length&&lN(cN(e),function(t){var e;(e=sN.get(t))?n.push(e):lN(H.document.forms,function(e){lN(e.elements,function(e){e.name===t&&(t="mce_editor_"+dN++,sN.setAttrib(e,"id",t),n.push(e))})})});break;case"textareas":case"specific_textareas":lN(sN.select("textarea"),function(e){t.editor_deselector&&l(e,t.editor_deselector)||t.editor_selector&&!l(e,t.editor_selector)||n.push(e)})}return n}(r)),r.types?lN(r.types,function(t){Jt.each(o,function(e){return!sN.is(e,t.selector)||(n(c(e),fN({},r,t),e),!1)})}):(Jt.each(o,function(e){var t;(t=s.get(e.id))&&t.initialized&&!(t.getContainer()||t.getBody()).parentNode&&(bN(t),t.unbindAllNativeEvents(),t.destroy(!0),t.removed=!0,t=null)}),0===(o=Jt.grep(o,function(e){return!s.get(e.id)})).length?f([]):lN(o,function(e){var t;t=e,r.inline&&t.tagName.toLowerCase()in u?yh.initError("Could not initialize inline editor on invalid inline target element",e):n(c(e),r,e)}))};return s.settings=r,sN.bind(window,"ready",e),new pe(function(t){n?t(n):f=function(e){t(e)}})},get:function(t){return 0===arguments.length?pN.slice(0):R(t)?Y(pN,function(e){return e.id===t}).getOr(null):I(t)&&pN[t]?pN[t]:null},add:function(e){var t=this;return gN[e.id]===e||(null===t.get(e.id)&&("length"!==e.id&&(gN[e.id]=e),gN.push(e),pN.push(e)),vN(!0),t.activeEditor=e,t.fire("AddEditor",{editor:e}),Jw||(Jw=function(){t.fire("BeforeUnload")},sN.bind(window,"beforeunload",Jw))),e},createEditor:function(e,t){return this.add(new Yw(e,t,this))},remove:function(e){var t,n,r=this;if(e){if(!R(e))return n=e,B(r.get(n.id))?null:(bN(n)&&r.fire("RemoveEditor",{editor:n}),0===pN.length&&sN.unbind(window,"beforeunload",Jw),n.remove(),vN(0<pN.length),n);lN(sN.select(e),function(e){(n=r.get(e.id))&&r.remove(n)})}else for(t=pN.length-1;0<=t;t--)r.remove(pN[t])},execCommand:function(e,t,n){var r=this.get(n);switch(e){case"mceAddEditor":return this.get(n)||new Yw(n,this.settings,this).render(),!0;case"mceRemoveEditor":return r&&r.remove(),!0;case"mceToggleEditor":return r?r.isHidden()?r.show():r.hide():this.execCommand("mceAddEditor",0,n),!0}return!!this.activeEditor&&this.activeEditor.execCommand(e,t,n)},triggerSave:function(){lN(pN,function(e){e.save()})},addI18n:function(e,t){mh.add(e,t)},translate:function(e){return mh.translate(e)},setActive:function(e){var t=this.activeEditor;this.activeEditor!==e&&(t&&t.fire("deactivate",{relatedTarget:e}),e.fire("activate",{relatedTarget:t})),this.activeEditor=e}},fp),Qw.setup();var yN,CN=Qw;function xN(n){return{walk:function(e,t){return _c(n,e,t)},split:Im,normalize:function(t){return wg(n,t).fold(q(!1),function(e){return t.setStart(e.startContainer,e.startOffset),t.setEnd(e.endContainer,e.endOffset),!0})}}}(yN=xN||(xN={})).compareRanges=pg,yN.getCaretRangeFromPoint=Ty,yN.getSelectedNode=$a,yN.getNode=Wa;var wN,NN,EN=xN,SN=Math.min,kN=Math.max,TN=Math.round,AN=function(e,t,n){var r,o,i,a,u,s;return r=t.x,o=t.y,i=e.w,a=e.h,u=t.w,s=t.h,"b"===(n=(n||"").split(""))[0]&&(o+=s),"r"===n[1]&&(r+=u),"c"===n[0]&&(o+=TN(s/2)),"c"===n[1]&&(r+=TN(u/2)),"b"===n[3]&&(o-=a),"r"===n[4]&&(r-=i),"c"===n[3]&&(o-=TN(a/2)),"c"===n[4]&&(r-=TN(i/2)),RN(r,o,i,a)},RN=function(e,t,n,r){return{x:e,y:t,w:n,h:r}},_N={inflate:function(e,t,n){return RN(e.x-t,e.y-n,e.w+2*t,e.h+2*n)},relativePosition:AN,findBestRelativePosition:function(e,t,n,r){var o,i;for(i=0;i<r.length;i++)if((o=AN(e,t,r[i])).x>=n.x&&o.x+o.w<=n.w+n.x&&o.y>=n.y&&o.y+o.h<=n.h+n.y)return r[i];return null},intersect:function(e,t){var n,r,o,i;return n=kN(e.x,t.x),r=kN(e.y,t.y),o=SN(e.x+e.w,t.x+t.w),i=SN(e.y+e.h,t.y+t.h),o-n<0||i-r<0?null:RN(n,r,o-n,i-r)},clamp:function(e,t,n){var r,o,i,a,u,s,c,l,f,d;return u=e.x,s=e.y,c=e.x+e.w,l=e.y+e.h,f=t.x+t.w,d=t.y+t.h,r=kN(0,t.x-u),o=kN(0,t.y-s),i=kN(0,c-f),a=kN(0,l-d),u+=r,s+=o,n&&(c+=r,l+=o,u-=i,s-=a),RN(u,s,(c-=i)-u,(l-=a)-s)},create:RN,fromClientRect:function(e){return RN(e.left,e.top,e.width,e.height)}},DN={},BN={add:function(e,t){DN[e.toLowerCase()]=t},has:function(e){return!!DN[e.toLowerCase()]},get:function(e){var t=e.toLowerCase(),n=DN.hasOwnProperty(t)?DN[t]:null;if(null===n)throw new Error("Could not find module for type: "+e);return n},create:function(e,t){var n;if("string"==typeof e?(t=t||{}).type=e:e=(t=e).type,e=e.toLowerCase(),!(n=DN[e]))throw new Error("Could not find control by type: "+e);return(n=new n(t)).type=e,n}},ON=Jt.each,PN=Jt.extend,IN=function(){};IN.extend=wN=function(n){var e,t,r,o=this.prototype,i=function(){var e,t,n;if(!NN&&(this.init&&this.init.apply(this,arguments),t=this.Mixins))for(e=t.length;e--;)(n=t[e]).init&&n.init.apply(this,arguments)},a=function(){return this},u=function(n,r){return function(){var e,t=this._super;return this._super=o[n],e=r.apply(this,arguments),this._super=t,e}};for(t in NN=!0,e=new this,NN=!1,n.Mixins&&(ON(n.Mixins,function(e){for(var t in e)"init"!==t&&(n[t]=e[t])}),o.Mixins&&(n.Mixins=o.Mixins.concat(n.Mixins))),n.Methods&&ON(n.Methods.split(","),function(e){n[e]=a}),n.Properties&&ON(n.Properties.split(","),function(e){var t="_"+e;n[e]=function(e){return e!==undefined?(this[t]=e,this):this[t]}}),n.Statics&&ON(n.Statics,function(e,t){i[t]=e}),n.Defaults&&o.Defaults&&(n.Defaults=PN({},o.Defaults,n.Defaults)),n)"function"==typeof(r=n[t])&&o[t]?e[t]=u(t,r):e[t]=r;return i.prototype=e,(i.constructor=i).extend=wN,i};var LN=Math.min,MN=Math.max,FN=Math.round,zN=function(e,n){var r,o,t,i;if(n=n||'"',null===e)return"null";if("string"==(t=typeof e))return o="\bb\tt\nn\ff\rr\"\"''\\\\",n+e.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g,function(e,t){return'"'===n&&"'"===e?e:(r=o.indexOf(t))+1?"\\"+o.charAt(r+1):(e=t.charCodeAt().toString(16),"\\u"+"0000".substring(e.length)+e)})+n;if("object"===t){if(e.hasOwnProperty&&"[object Array]"===Object.prototype.toString.call(e)){for(r=0,o="[";r<e.length;r++)o+=(0<r?",":"")+zN(e[r],n);return o+"]"}for(i in o="{",e)e.hasOwnProperty(i)&&(o+="function"!=typeof e[i]?(1<o.length?","+n:n)+i+n+":"+zN(e[i],n):"");return o+"}"}return""+e},UN={serialize:zN,parse:function(e){try{return JSON.parse(e)}catch(t){}}},VN={callbacks:{},count:0,send:function(t){var n=this,r=bi.DOM,o=t.count!==undefined?t.count:n.count,i="tinymce_jsonp_"+o;n.callbacks[o]=function(e){r.remove(i),delete n.callbacks[o],t.callback(e)},r.add(r.doc.body,"script",{id:i,src:t.url,type:"text/javascript"}),n.count++}},HN={send:function(e){var t,n=0,r=function(){!e.async||4===t.readyState||1e4<n++?(e.success&&n<1e4&&200===t.status?e.success.call(e.success_scope,""+t.responseText,t,e):e.error&&e.error.call(e.error_scope,1e4<n?"TIMED_OUT":"GENERAL",t,e),t=null):setTimeout(r,10)};if(e.scope=e.scope||this,e.success_scope=e.success_scope||e.scope,e.error_scope=e.error_scope||e.scope,e.async=!1!==e.async,e.data=e.data||"",HN.fire("beforeInitialize",{settings:e}),t=wh()){if(t.overrideMimeType&&t.overrideMimeType(e.content_type),t.open(e.type||(e.data?"POST":"GET"),e.url,e.async),e.crossDomain&&(t.withCredentials=!0),e.content_type&&t.setRequestHeader("Content-Type",e.content_type),e.requestheaders&&Jt.each(e.requestheaders,function(e){t.setRequestHeader(e.key,e.value)}),t.setRequestHeader("X-Requested-With","XMLHttpRequest"),(t=HN.fire("beforeSend",{xhr:t,settings:e}).xhr).send(e.data),!e.async)return r();setTimeout(r,10)}}};Jt.extend(HN,fp);var jN,qN,$N,WN,KN=Jt.extend,XN=function(e){this.settings=KN({},e),this.count=0};XN.sendRPC=function(e){return(new XN).send(e)},XN.prototype={send:function(n){var r=n.error,o=n.success;(n=KN(this.settings,n)).success=function(e,t){void 0===(e=UN.parse(e))&&(e={error:"JSON Parse error."}),e.error?r.call(n.error_scope||n.scope,e.error,t):o.call(n.success_scope||n.scope,e.result)},n.error=function(e,t){r&&r.call(n.error_scope||n.scope,e,t)},n.data=UN.serialize({id:n.id||"c"+this.count++,method:n.method,params:n.params}),n.content_type="application/json",HN.send(n)}};try{jN=H.window.localStorage}catch(ZN){qN={},$N=[],WN={getItem:function(e){var t=qN[e];return t||null},setItem:function(e,t){$N.push(e),qN[e]=String(t)},key:function(e){return $N[e]},removeItem:function(t){$N=$N.filter(function(e){return e===t}),delete qN[t]},clear:function(){$N=[],qN={}},length:0},Object.defineProperty(WN,"length",{get:function(){return $N.length},configurable:!1,enumerable:!1}),jN=WN}var YN,GN=CN,JN={geom:{Rect:_N},util:{Promise:pe,Delay:ye,Tools:Jt,VK:xv,URI:Ow,Class:IN,EventDispatcher:sp,Observable:fp,I18n:mh,XHR:HN,JSON:UN,JSONRequest:XN,JSONP:VN,LocalStorage:jN,Color:function(e){var n={},u=0,s=0,c=0,t=function(e){var t;return"object"==typeof e?"r"in e?(u=e.r,s=e.g,c=e.b):"v"in e&&function(e,t,n){var r,o,i,a;if(e=(parseInt(e,10)||0)%360,t=parseInt(t,10)/100,n=parseInt(n,10)/100,t=MN(0,LN(t,1)),n=MN(0,LN(n,1)),0!==t){switch(r=e/60,i=(o=n*t)*(1-Math.abs(r%2-1)),a=n-o,Math.floor(r)){case 0:u=o,s=i,c=0;break;case 1:u=i,s=o,c=0;break;case 2:u=0,s=o,c=i;break;case 3:u=0,s=i,c=o;break;case 4:u=i,s=0,c=o;break;case 5:u=o,s=0,c=i;break;default:u=s=c=0}u=FN(255*(u+a)),s=FN(255*(s+a)),c=FN(255*(c+a))}else u=s=c=FN(255*n)}(e.h,e.s,e.v):(t=/rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(e))?(u=parseInt(t[1],10),s=parseInt(t[2],10),c=parseInt(t[3],10)):(t=/#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(e))?(u=parseInt(t[1],16),s=parseInt(t[2],16),c=parseInt(t[3],16)):(t=/#([0-F])([0-F])([0-F])/gi.exec(e))&&(u=parseInt(t[1]+t[1],16),s=parseInt(t[2]+t[2],16),c=parseInt(t[3]+t[3],16)),u=u<0?0:255<u?255:u,s=s<0?0:255<s?255:s,c=c<0?0:255<c?255:c,n};return e&&t(e),n.toRgb=function(){return{r:u,g:s,b:c}},n.toHsv=function(){return e=u,t=s,n=c,o=0,(i=LN(e/=255,LN(t/=255,n/=255)))===(a=MN(e,MN(t,n)))?{h:0,s:0,v:100*(o=i)}:(r=(a-i)/a,{h:FN(60*((e===i?3:n===i?1:5)-(e===i?t-n:n===i?e-t:n-e)/((o=a)-i))),s:FN(100*r),v:FN(100*o)});var e,t,n,r,o,i,a},n.toHex=function(){var e=function(e){return 1<(e=parseInt(e,10).toString(16)).length?e:"0"+e};return"#"+e(u)+e(s)+e(c)},n.parse=t,n}},dom:{EventUtils:Ae,Sizzle:At,DomQuery:vn,TreeWalker:ao,DOMUtils:bi,ScriptLoader:Ni,RangeUtils:EN,Serializer:py,ControlSelection:wy,BookmarkManager:yy,Selection:tC,Event:Ae.Event},html:{Styles:ui,Entities:Xo,Node:Zb,Schema:ii,SaxParser:Rv,DomParser:ly,Writer:Zc,Serializer:el},ui:{Factory:BN},Env:ge,AddOnManager:ki,Annotator:Mc,Formatter:Vb,UndoManager:Xv,EditorCommands:ap,WindowManager:lh,NotificationManager:ch,EditorObservable:Ap,Shortcuts:qp,Editor:Yw,FocusManager:Zw,EditorManager:CN,DOM:bi.DOM,ScriptLoader:Ni.ScriptLoader,PluginManager:ki.PluginManager,ThemeManager:ki.ThemeManager,trim:Jt.trim,isArray:Jt.isArray,is:Jt.is,toArray:Jt.toArray,makeMap:Jt.makeMap,each:Jt.each,map:Jt.map,grep:Jt.grep,inArray:Jt.inArray,extend:Jt.extend,create:Jt.create,walk:Jt.walk,createNS:Jt.createNS,resolve:Jt.resolve,explode:Jt.explode,_addCacheSuffix:Jt._addCacheSuffix,isOpera:ge.opera,isWebKit:ge.webkit,isIE:ge.ie,isGecko:ge.gecko,isMac:ge.mac},QN=GN=Jt.extend(GN,JN);YN=QN,window.tinymce=YN,window.tinyMCE=YN,function(e){if("object"==typeof module)try{module.exports=e}catch(t){}}(QN)}(window); \ No newline at end of file +// 4.9.10 (2020-04-23) +!function(V){"use strict";var o=function(){},H=function(n,r){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return n(r.apply(null,e))}},q=function(e){return function(){return e}},$=function(e){return e};function d(r){for(var o=[],e=1;e<arguments.length;e++)o[e-1]=arguments[e];return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=o.concat(e);return r.apply(null,n)}}var e,t,n,r,i,a,u,s,c,l,f,m,g,p,h,v,y=function(n){return function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return!n.apply(null,e)}},b=q(!1),C=q(!0),x=function(){return w},w=(e=function(e){return e.isNone()},r={fold:function(e,t){return e()},is:b,isSome:b,isNone:C,getOr:n=function(e){return e},getOrThunk:t=function(e){return e()},getOrDie:function(e){throw new Error(e||"error: getOrDie called on none.")},getOrNull:q(null),getOrUndefined:q(undefined),or:n,orThunk:t,map:x,each:o,bind:x,exists:b,forall:C,filter:x,equals:e,equals_:e,toArray:function(){return[]},toString:q("none()")},Object.freeze&&Object.freeze(r),r),N=function(n){var e=q(n),t=function(){return o},r=function(e){return e(n)},o={fold:function(e,t){return t(n)},is:function(e){return n===e},isSome:C,isNone:b,getOr:e,getOrThunk:e,getOrDie:e,getOrNull:e,getOrUndefined:e,or:t,orThunk:t,map:function(e){return N(e(n))},each:function(e){e(n)},bind:r,exists:r,forall:r,filter:function(e){return e(n)?o:w},toArray:function(){return[n]},toString:function(){return"some("+n+")"},equals:function(e){return e.is(n)},equals_:function(e,t){return e.fold(b,function(e){return t(n,e)})}};return o},_={some:N,none:x,from:function(e){return null===e||e===undefined?w:N(e)}},E=function(t){return function(e){return function(e){if(null===e)return"null";var t=typeof e;return"object"===t&&(Array.prototype.isPrototypeOf(e)||e.constructor&&"Array"===e.constructor.name)?"array":"object"===t&&(String.prototype.isPrototypeOf(e)||e.constructor&&"String"===e.constructor.name)?"string":t}(e)===t}},S=E("string"),T=E("object"),k=E("array"),A=E("null"),R=E("boolean"),D=E("function"),O=E("number"),B=Array.prototype.slice,P=Array.prototype.indexOf,I=Array.prototype.push,L=function(e,t){return P.call(e,t)},F=function(e,t){return-1<L(e,t)},M=function(e,t){for(var n=0,r=e.length;n<r;n++)if(t(e[n],n))return!0;return!1},W=function(e,t){for(var n=e.length,r=new Array(n),o=0;o<n;o++){var i=e[o];r[o]=t(i,o)}return r},z=function(e,t){for(var n=0,r=e.length;n<r;n++)t(e[n],n)},K=function(e,t){for(var n=[],r=[],o=0,i=e.length;o<i;o++){var a=e[o];(t(a,o)?n:r).push(a)}return{pass:n,fail:r}},U=function(e,t){for(var n=[],r=0,o=e.length;r<o;r++){var i=e[r];t(i,r)&&n.push(i)}return n},j=function(e,t,n){return z(e,function(e){n=t(n,e)}),n},X=function(e,t){for(var n=0,r=e.length;n<r;n++){var o=e[n];if(t(o,n))return _.some(o)}return _.none()},Y=function(e,t){for(var n=0,r=e.length;n<r;n++)if(t(e[n],n))return _.some(n);return _.none()},G=function(e,t){return function(e){for(var t=[],n=0,r=e.length;n<r;++n){if(!k(e[n]))throw new Error("Arr.flatten item "+n+" was not an array, input: "+e);I.apply(t,e[n])}return t}(W(e,t))},J=function(e,t){for(var n=0,r=e.length;n<r;++n)if(!0!==t(e[n],n))return!1;return!0},Q=function(e,t){return U(e,function(e){return!F(t,e)})},Z=function(e){return 0===e.length?_.none():_.some(e[0])},ee=function(e){return 0===e.length?_.none():_.some(e[e.length-1])},te=D(Array.from)?Array.from:function(e){return B.call(e)},ne="undefined"!=typeof V.window?V.window:Function("return this;")(),re=function(e,t){return function(e,t){for(var n=t!==undefined&&null!==t?t:ne,r=0;r<e.length&&n!==undefined&&null!==n;++r)n=n[e[r]];return n}(e.split("."),t)},oe={getOrDie:function(e,t){var n=re(e,t);if(n===undefined||null===n)throw new Error(e+" not available on this browser");return n}},ie=function(){return oe.getOrDie("URL")},ae={createObjectURL:function(e){return ie().createObjectURL(e)},revokeObjectURL:function(e){ie().revokeObjectURL(e)}},ue=V.navigator,se=ue.userAgent,ce=function(e){return"matchMedia"in V.window&&V.matchMedia(e).matches};m=/Android/.test(se),a=(a=!(i=/WebKit/.test(se))&&/MSIE/gi.test(se)&&/Explorer/gi.test(ue.appName))&&/MSIE (\w+)\./.exec(se)[1],u=-1!==se.indexOf("Trident/")&&(-1!==se.indexOf("rv:")||-1!==ue.appName.indexOf("Netscape"))&&11,s=-1!==se.indexOf("Edge/")&&!a&&!u&&12,a=a||u||s,c=!i&&!u&&/Gecko/.test(se),l=-1!==se.indexOf("Mac"),f=/(iPad|iPhone)/.test(se),g="FormData"in V.window&&"FileReader"in V.window&&"URL"in V.window&&!!ae.createObjectURL,p=ce("only screen and (max-device-width: 480px)")&&(m||f),h=ce("only screen and (min-width: 800px)")&&(m||f),v=-1!==se.indexOf("Windows Phone"),s&&(i=!1);var le,fe={opera:!1,webkit:i,ie:a,gecko:c,mac:l,iOS:f,android:m,contentEditable:!f||g||534<=parseInt(se.match(/AppleWebKit\/(\d*)/)[1],10),transparentSrc:"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7",caretAfter:8!==a,range:V.window.getSelection&&"Range"in V.window,documentMode:a&&!s?V.document.documentMode||7:10,fileApi:g,ceFalse:!1===a||8<a,cacheSuffix:null,container:null,overrideViewPort:null,experimentalShadowDom:!1,canHaveCSP:!1===a||11<a,desktop:!p&&!h,windowsPhone:v},de=window.Promise?window.Promise:function(){function r(e,t){return function(){e.apply(t,arguments)}}var e=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)},i=function(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=null,this._value=null,this._deferreds=[],l(e,r(o,this),r(u,this))},t=i.immediateFn||"function"==typeof setImmediate&&setImmediate||function(e){setTimeout(e,1)};function a(r){var o=this;null!==this._state?t(function(){var e=o._state?r.onFulfilled:r.onRejected;if(null!==e){var t;try{t=e(o._value)}catch(n){return void r.reject(n)}r.resolve(t)}else(o._state?r.resolve:r.reject)(o._value)}):this._deferreds.push(r)}function o(e){try{if(e===this)throw new TypeError("A promise cannot be resolved with itself.");if(e&&("object"==typeof e||"function"==typeof e)){var t=e.then;if("function"==typeof t)return void l(r(t,e),r(o,this),r(u,this))}this._state=!0,this._value=e,s.call(this)}catch(n){u.call(this,n)}}function u(e){this._state=!1,this._value=e,s.call(this)}function s(){for(var e=0,t=this._deferreds.length;e<t;e++)a.call(this,this._deferreds[e]);this._deferreds=null}function c(e,t,n,r){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof t?t:null,this.resolve=n,this.reject=r}function l(e,t,n){var r=!1;try{e(function(e){r||(r=!0,t(e))},function(e){r||(r=!0,n(e))})}catch(o){if(r)return;r=!0,n(o)}}return i.prototype["catch"]=function(e){return this.then(null,e)},i.prototype.then=function(n,r){var o=this;return new i(function(e,t){a.call(o,new c(n,r,e,t))})},i.all=function(){var s=Array.prototype.slice.call(1===arguments.length&&e(arguments[0])?arguments[0]:arguments);return new i(function(o,i){if(0===s.length)return o([]);var a=s.length;function u(t,e){try{if(e&&("object"==typeof e||"function"==typeof e)){var n=e.then;if("function"==typeof n)return void n.call(e,function(e){u(t,e)},i)}s[t]=e,0==--a&&o(s)}catch(r){i(r)}}for(var e=0;e<s.length;e++)u(e,s[e])})},i.resolve=function(t){return t&&"object"==typeof t&&t.constructor===i?t:new i(function(e){e(t)})},i.reject=function(n){return new i(function(e,t){t(n)})},i.race=function(o){return new i(function(e,t){for(var n=0,r=o.length;n<r;n++)o[n].then(e,t)})},i}(),me=function(e,t){return"number"!=typeof t&&(t=0),setTimeout(e,t)},ge=function(e,t){return"number"!=typeof t&&(t=1),setInterval(e,t)},pe=function(t,n){var r,e;return(e=function(){var e=arguments;clearTimeout(r),r=me(function(){t.apply(this,e)},n)}).stop=function(){clearTimeout(r)},e},he={requestAnimationFrame:function(e,t){le?le.then(e):le=new de(function(e){t||(t=V.document.body),function(e,t){var n,r=V.window.requestAnimationFrame,o=["ms","moz","webkit"];for(n=0;n<o.length&&!r;n++)r=V.window[o[n]+"RequestAnimationFrame"];r||(r=function(e){V.window.setTimeout(e,0)}),r(e,t)}(e,t)}).then(e)},setTimeout:me,setInterval:ge,setEditorTimeout:function(e,t,n){return me(function(){e.removed||t()},n)},setEditorInterval:function(e,t,n){var r;return r=ge(function(){e.removed?clearInterval(r):t()},n)},debounce:pe,throttle:pe,clearInterval:function(e){return clearInterval(e)},clearTimeout:function(e){return clearTimeout(e)}},ve=/^(?:mouse|contextmenu)|click/,ye={keyLocation:1,layerX:1,layerY:1,returnValue:1,webkitMovementX:1,webkitMovementY:1,keyIdentifier:1},be=function(){return!1},Ce=function(){return!0},xe=function(e,t,n,r){e.addEventListener?e.addEventListener(t,n,r||!1):e.attachEvent&&e.attachEvent("on"+t,n)},we=function(e,t,n,r){e.removeEventListener?e.removeEventListener(t,n,r||!1):e.detachEvent&&e.detachEvent("on"+t,n)},Ne=function(e,t){var n,r,o=t||{};for(n in e)ye[n]||(o[n]=e[n]);if(o.target||(o.target=o.srcElement||V.document),fe.experimentalShadowDom&&(o.target=function(e,t){if(e.composedPath){var n=e.composedPath();if(n&&0<n.length)return n[0]}return t}(e,o.target)),e&&ve.test(e.type)&&e.pageX===undefined&&e.clientX!==undefined){var i=o.target.ownerDocument||V.document,a=i.documentElement,u=i.body;o.pageX=e.clientX+(a&&a.scrollLeft||u&&u.scrollLeft||0)-(a&&a.clientLeft||u&&u.clientLeft||0),o.pageY=e.clientY+(a&&a.scrollTop||u&&u.scrollTop||0)-(a&&a.clientTop||u&&u.clientTop||0)}return o.preventDefault=function(){o.isDefaultPrevented=Ce,e&&(e.preventDefault?e.preventDefault():e.returnValue=!1)},o.stopPropagation=function(){o.isPropagationStopped=Ce,e&&(e.stopPropagation?e.stopPropagation():e.cancelBubble=!0)},!(o.stopImmediatePropagation=function(){o.isImmediatePropagationStopped=Ce,o.stopPropagation()})==((r=o).isDefaultPrevented===Ce||r.isDefaultPrevented===be)&&(o.isDefaultPrevented=be,o.isPropagationStopped=be,o.isImmediatePropagationStopped=be),"undefined"==typeof o.metaKey&&(o.metaKey=!1),o},Ee=function(e,t,n){var r=e.document,o={type:"ready"};if(n.domLoaded)t(o);else{var i=function(){return"complete"===r.readyState||"interactive"===r.readyState&&r.body},a=function(){n.domLoaded||(n.domLoaded=!0,t(o))},u=function(){i()&&(we(r,"readystatechange",u),a())},s=function(){try{r.documentElement.doScroll("left")}catch(e){return void he.setTimeout(s)}a()};!r.addEventListener||fe.ie&&fe.ie<11?(xe(r,"readystatechange",u),r.documentElement.doScroll&&e.self===e.top&&s()):i()?a():xe(e,"DOMContentLoaded",a),xe(e,"load",a)}},Se=function(){var m,g,p,h,v,y=this,b={};g="mce-data-"+(+new Date).toString(32),h="onmouseenter"in V.document.documentElement,p="onfocusin"in V.document.documentElement,v={mouseenter:"mouseover",mouseleave:"mouseout"},m=1,y.domLoaded=!1,y.events=b;var C=function(e,t){var n,r,o,i,a=b[t];if(n=a&&a[e.type])for(r=0,o=n.length;r<o;r++)if((i=n[r])&&!1===i.func.call(i.scope,e)&&e.preventDefault(),e.isImmediatePropagationStopped())return};y.bind=function(e,t,n,r){var o,i,a,u,s,c,l,f=V.window,d=function(e){C(Ne(e||f.event),o)};if(e&&3!==e.nodeType&&8!==e.nodeType){for(e[g]?o=e[g]:(o=m++,e[g]=o,b[o]={}),r=r||e,a=(t=t.split(" ")).length;a--;)c=d,s=l=!1,"DOMContentLoaded"===(u=t[a])&&(u="ready"),y.domLoaded&&"ready"===u&&"complete"===e.readyState?n.call(r,Ne({type:u})):(h||(s=v[u])&&(c=function(e){var t,n;if(t=e.currentTarget,(n=e.relatedTarget)&&t.contains)n=t.contains(n);else for(;n&&n!==t;)n=n.parentNode;n||((e=Ne(e||f.event)).type="mouseout"===e.type?"mouseleave":"mouseenter",e.target=t,C(e,o))}),p||"focusin"!==u&&"focusout"!==u||(l=!0,s="focusin"===u?"focus":"blur",c=function(e){(e=Ne(e||f.event)).type="focus"===e.type?"focusin":"focusout",C(e,o)}),(i=b[o][u])?"ready"===u&&y.domLoaded?n({type:u}):i.push({func:n,scope:r}):(b[o][u]=i=[{func:n,scope:r}],i.fakeName=s,i.capture=l,i.nativeHandler=c,"ready"===u?Ee(e,c,y):xe(e,s||u,c,l)));return e=i=0,n}},y.unbind=function(e,t,n){var r,o,i,a,u,s;if(!e||3===e.nodeType||8===e.nodeType)return y;if(r=e[g]){if(s=b[r],t){for(i=(t=t.split(" ")).length;i--;)if(o=s[u=t[i]]){if(n)for(a=o.length;a--;)if(o[a].func===n){var c=o.nativeHandler,l=o.fakeName,f=o.capture;(o=o.slice(0,a).concat(o.slice(a+1))).nativeHandler=c,o.fakeName=l,o.capture=f,s[u]=o}n&&0!==o.length||(delete s[u],we(e,o.fakeName||u,o.nativeHandler,o.capture))}}else{for(u in s)o=s[u],we(e,o.fakeName||u,o.nativeHandler,o.capture);s={}}for(u in s)return y;delete b[r];try{delete e[g]}catch(d){e[g]=null}}return y},y.fire=function(e,t,n){var r;if(!e||3===e.nodeType||8===e.nodeType)return y;for((n=Ne(null,n)).type=t,n.target=e;(r=e[g])&&C(n,r),(e=e.parentNode||e.ownerDocument||e.defaultView||e.parentWindow)&&!n.isPropagationStopped(););return y},y.clean=function(e){var t,n,r=y.unbind;if(!e||3===e.nodeType||8===e.nodeType)return y;if(e[g]&&r(e),e.getElementsByTagName||(e=e.document),e&&e.getElementsByTagName)for(r(e),t=(n=e.getElementsByTagName("*")).length;t--;)(e=n[t])[g]&&r(e);return y},y.destroy=function(){b={}},y.cancel=function(e){return e&&(e.preventDefault(),e.stopImmediatePropagation()),!1}};Se.Event=new Se,Se.Event.bind(V.window,"ready",function(){});var Te,ke,_e,Ae,Re,De,Oe,Be,Pe,Ie,Le,Fe,Me,ze,Ue,je,Ve,He,qe="sizzle"+-new Date,$e=V.window.document,We=0,Ke=0,Xe=Tt(),Ye=Tt(),Ge=Tt(),Je=function(e,t){return e===t&&(Le=!0),0},Qe=typeof undefined,Ze={}.hasOwnProperty,et=[],tt=et.pop,nt=et.push,rt=et.push,ot=et.slice,it=et.indexOf||function(e){for(var t=0,n=this.length;t<n;t++)if(this[t]===e)return t;return-1},at="[\\x20\\t\\r\\n\\f]",ut="(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",st="\\["+at+"*("+ut+")(?:"+at+"*([*^$|!~]?=)"+at+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+ut+"))|)"+at+"*\\]",ct=":("+ut+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+st+")*)|.*)\\)|)",lt=new RegExp("^"+at+"+|((?:^|[^\\\\])(?:\\\\.)*)"+at+"+$","g"),ft=new RegExp("^"+at+"*,"+at+"*"),dt=new RegExp("^"+at+"*([>+~]|"+at+")"+at+"*"),mt=new RegExp("="+at+"*([^\\]'\"]*?)"+at+"*\\]","g"),gt=new RegExp(ct),pt=new RegExp("^"+ut+"$"),ht={ID:new RegExp("^#("+ut+")"),CLASS:new RegExp("^\\.("+ut+")"),TAG:new RegExp("^("+ut+"|[*])"),ATTR:new RegExp("^"+st),PSEUDO:new RegExp("^"+ct),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+at+"*(even|odd|(([+-]|)(\\d*)n|)"+at+"*(?:([+-]|)"+at+"*(\\d+)|))"+at+"*\\)|)","i"),bool:new RegExp("^(?:checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped)$","i"),needsContext:new RegExp("^"+at+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+at+"*((?:-\\d)?\\d*)"+at+"*\\)|)(?=[^-]|$)","i")},vt=/^(?:input|select|textarea|button)$/i,yt=/^h\d$/i,bt=/^[^{]+\{\s*\[native \w/,Ct=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,xt=/[+~]/,wt=/'|\\/g,Nt=new RegExp("\\\\([\\da-f]{1,6}"+at+"?|("+at+")|.)","ig"),Et=function(e,t,n){var r="0x"+t-65536;return r!=r||n?t:r<0?String.fromCharCode(r+65536):String.fromCharCode(r>>10|55296,1023&r|56320)};try{rt.apply(et=ot.call($e.childNodes),$e.childNodes),et[$e.childNodes.length].nodeType}catch(iE){rt={apply:et.length?function(e,t){nt.apply(e,ot.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}var St=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m;if((t?t.ownerDocument||t:$e)!==Me&&Fe(t),n=n||[],!e||"string"!=typeof e)return n;if(1!==(u=(t=t||Me).nodeType)&&9!==u)return[];if(Ue&&!r){if(o=Ct.exec(e))if(a=o[1]){if(9===u){if(!(i=t.getElementById(a))||!i.parentNode)return n;if(i.id===a)return n.push(i),n}else if(t.ownerDocument&&(i=t.ownerDocument.getElementById(a))&&He(t,i)&&i.id===a)return n.push(i),n}else{if(o[2])return rt.apply(n,t.getElementsByTagName(e)),n;if((a=o[3])&&ke.getElementsByClassName)return rt.apply(n,t.getElementsByClassName(a)),n}if(ke.qsa&&(!je||!je.test(e))){if(f=l=qe,d=t,m=9===u&&e,1===u&&"object"!==t.nodeName.toLowerCase()){for(c=De(e),(l=t.getAttribute("id"))?f=l.replace(wt,"\\$&"):t.setAttribute("id",f),f="[id='"+f+"'] ",s=c.length;s--;)c[s]=f+Pt(c[s]);d=xt.test(e)&&Ot(t.parentNode)||t,m=c.join(",")}if(m)try{return rt.apply(n,d.querySelectorAll(m)),n}catch(g){}finally{l||t.removeAttribute("id")}}}return Be(e.replace(lt,"$1"),t,n,r)};function Tt(){var r=[];return function e(t,n){return r.push(t+" ")>_e.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function kt(e){return e[qe]=!0,e}function _t(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&(~t.sourceIndex||1<<31)-(~e.sourceIndex||1<<31);if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function At(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function Rt(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function Dt(a){return kt(function(i){return i=+i,kt(function(e,t){for(var n,r=a([],e.length,i),o=r.length;o--;)e[n=r[o]]&&(e[n]=!(t[n]=e[n]))})})}function Ot(e){return e&&typeof e.getElementsByTagName!==Qe&&e}for(Te in ke=St.support={},Re=St.isXML=function(e){var t=e&&(e.ownerDocument||e).documentElement;return!!t&&"HTML"!==t.nodeName},Fe=St.setDocument=function(e){var t,s=e?e.ownerDocument||e:$e,n=s.defaultView;return s!==Me&&9===s.nodeType&&s.documentElement?(ze=(Me=s).documentElement,Ue=!Re(s),n&&n!==function(e){try{return e.top}catch(t){}return null}(n)&&(n.addEventListener?n.addEventListener("unload",function(){Fe()},!1):n.attachEvent&&n.attachEvent("onunload",function(){Fe()})),ke.attributes=!0,ke.getElementsByTagName=!0,ke.getElementsByClassName=bt.test(s.getElementsByClassName),ke.getById=!0,_e.find.ID=function(e,t){if(typeof t.getElementById!==Qe&&Ue){var n=t.getElementById(e);return n&&n.parentNode?[n]:[]}},_e.filter.ID=function(e){var t=e.replace(Nt,Et);return function(e){return e.getAttribute("id")===t}},_e.find.TAG=ke.getElementsByTagName?function(e,t){if(typeof t.getElementsByTagName!==Qe)return t.getElementsByTagName(e)}:function(e,t){var n,r=[],o=0,i=t.getElementsByTagName(e);if("*"===e){for(;n=i[o++];)1===n.nodeType&&r.push(n);return r}return i},_e.find.CLASS=ke.getElementsByClassName&&function(e,t){if(Ue)return t.getElementsByClassName(e)},Ve=[],je=[],ke.disconnectedMatch=!0,je=je.length&&new RegExp(je.join("|")),Ve=Ve.length&&new RegExp(Ve.join("|")),t=bt.test(ze.compareDocumentPosition),He=t||bt.test(ze.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},Je=t?function(e,t){if(e===t)return Le=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)===(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!ke.sortDetached&&t.compareDocumentPosition(e)===n?e===s||e.ownerDocument===$e&&He($e,e)?-1:t===s||t.ownerDocument===$e&&He($e,t)?1:Ie?it.call(Ie,e)-it.call(Ie,t):0:4&n?-1:1)}:function(e,t){if(e===t)return Le=!0,0;var n,r=0,o=e.parentNode,i=t.parentNode,a=[e],u=[t];if(!o||!i)return e===s?-1:t===s?1:o?-1:i?1:Ie?it.call(Ie,e)-it.call(Ie,t):0;if(o===i)return _t(e,t);for(n=e;n=n.parentNode;)a.unshift(n);for(n=t;n=n.parentNode;)u.unshift(n);for(;a[r]===u[r];)r++;return r?_t(a[r],u[r]):a[r]===$e?-1:u[r]===$e?1:0},s):Me},St.matches=function(e,t){return St(e,null,null,t)},St.matchesSelector=function(e,t){if((e.ownerDocument||e)!==Me&&Fe(e),t=t.replace(mt,"='$1']"),ke.matchesSelector&&Ue&&(!Ve||!Ve.test(t))&&(!je||!je.test(t)))try{var n=(void 0).call(e,t);if(n||ke.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(iE){}return 0<St(t,Me,null,[e]).length},St.contains=function(e,t){return(e.ownerDocument||e)!==Me&&Fe(e),He(e,t)},St.attr=function(e,t){(e.ownerDocument||e)!==Me&&Fe(e);var n=_e.attrHandle[t.toLowerCase()],r=n&&Ze.call(_e.attrHandle,t.toLowerCase())?n(e,t,!Ue):undefined;return r!==undefined?r:ke.attributes||!Ue?e.getAttribute(t):(r=e.getAttributeNode(t))&&r.specified?r.value:null},St.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},St.uniqueSort=function(e){var t,n=[],r=0,o=0;if(Le=!ke.detectDuplicates,Ie=!ke.sortStable&&e.slice(0),e.sort(Je),Le){for(;t=e[o++];)t===e[o]&&(r=n.push(o));for(;r--;)e.splice(n[r],1)}return Ie=null,e},Ae=St.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=Ae(e)}else if(3===o||4===o)return e.nodeValue}else for(;t=e[r++];)n+=Ae(t);return n},(_e=St.selectors={cacheLength:50,createPseudo:kt,match:ht,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(Nt,Et),e[3]=(e[3]||e[4]||e[5]||"").replace(Nt,Et),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||St.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&St.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return ht.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&>.test(n)&&(t=De(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(Nt,Et).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=Xe[e+" "];return t||(t=new RegExp("(^|"+at+")"+e+"("+at+"|$)"))&&Xe(e,function(e){return t.test("string"==typeof e.className&&e.className||typeof e.getAttribute!==Qe&&e.getAttribute("class")||"")})},ATTR:function(n,r,o){return function(e){var t=St.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===o:"!="===r?t!==o:"^="===r?o&&0===t.indexOf(o):"*="===r?o&&-1<t.indexOf(o):"$="===r?o&&t.slice(-o.length)===o:"~="===r?-1<(" "+t+" ").indexOf(o):"|="===r&&(t===o||t.slice(0,o.length+1)===o+"-"))}},CHILD:function(m,e,t,g,p){var h="nth"!==m.slice(0,3),v="last"!==m.slice(-4),y="of-type"===e;return 1===g&&0===p?function(e){return!!e.parentNode}:function(e,t,n){var r,o,i,a,u,s,c=h!==v?"nextSibling":"previousSibling",l=e.parentNode,f=y&&e.nodeName.toLowerCase(),d=!n&&!y;if(l){if(h){for(;c;){for(i=e;i=i[c];)if(y?i.nodeName.toLowerCase()===f:1===i.nodeType)return!1;s=c="only"===m&&!s&&"nextSibling"}return!0}if(s=[v?l.firstChild:l.lastChild],v&&d){for(u=(r=(o=l[qe]||(l[qe]={}))[m]||[])[0]===We&&r[1],a=r[0]===We&&r[2],i=u&&l.childNodes[u];i=++u&&i&&i[c]||(a=u=0)||s.pop();)if(1===i.nodeType&&++a&&i===e){o[m]=[We,u,a];break}}else if(d&&(r=(e[qe]||(e[qe]={}))[m])&&r[0]===We)a=r[1];else for(;(i=++u&&i&&i[c]||(a=u=0)||s.pop())&&((y?i.nodeName.toLowerCase()!==f:1!==i.nodeType)||!++a||(d&&((i[qe]||(i[qe]={}))[m]=[We,a]),i!==e)););return(a-=p)===g||a%g==0&&0<=a/g}}},PSEUDO:function(e,i){var t,a=_e.pseudos[e]||_e.setFilters[e.toLowerCase()]||St.error("unsupported pseudo: "+e);return a[qe]?a(i):1<a.length?(t=[e,e,"",i],_e.setFilters.hasOwnProperty(e.toLowerCase())?kt(function(e,t){for(var n,r=a(e,i),o=r.length;o--;)e[n=it.call(e,r[o])]=!(t[n]=r[o])}):function(e){return a(e,0,t)}):a}},pseudos:{not:kt(function(e){var r=[],o=[],u=Oe(e.replace(lt,"$1"));return u[qe]?kt(function(e,t,n,r){for(var o,i=u(e,null,r,[]),a=e.length;a--;)(o=i[a])&&(e[a]=!(t[a]=o))}):function(e,t,n){return r[0]=e,u(r,null,n,o),!o.pop()}}),has:kt(function(t){return function(e){return 0<St(t,e).length}}),contains:kt(function(t){return t=t.replace(Nt,Et),function(e){return-1<(e.textContent||e.innerText||Ae(e)).indexOf(t)}}),lang:kt(function(n){return pt.test(n||"")||St.error("unsupported lang: "+n),n=n.replace(Nt,Et).toLowerCase(),function(e){var t;do{if(t=Ue?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(t=t.toLowerCase())===n||0===t.indexOf(n+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}}),target:function(e){var t=V.window.location&&V.window.location.hash;return t&&t.slice(1)===e.id},root:function(e){return e===ze},focus:function(e){return e===Me.activeElement&&(!Me.hasFocus||Me.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:function(e){return!1===e.disabled},disabled:function(e){return!0===e.disabled},checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!_e.pseudos.empty(e)},header:function(e){return yt.test(e.nodeName)},input:function(e){return vt.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:Dt(function(){return[0]}),last:Dt(function(e,t){return[t-1]}),eq:Dt(function(e,t,n){return[n<0?n+t:n]}),even:Dt(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:Dt(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:Dt(function(e,t,n){for(var r=n<0?n+t:n;0<=--r;)e.push(r);return e}),gt:Dt(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=_e.pseudos.eq,{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})_e.pseudos[Te]=At(Te);for(Te in{submit:!0,reset:!0})_e.pseudos[Te]=Rt(Te);function Bt(){}function Pt(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function It(a,e,t){var u=e.dir,s=t&&"parentNode"===u,c=Ke++;return e.first?function(e,t,n){for(;e=e[u];)if(1===e.nodeType||s)return a(e,t,n)}:function(e,t,n){var r,o,i=[We,c];if(n){for(;e=e[u];)if((1===e.nodeType||s)&&a(e,t,n))return!0}else for(;e=e[u];)if(1===e.nodeType||s){if((r=(o=e[qe]||(e[qe]={}))[u])&&r[0]===We&&r[1]===c)return i[2]=r[2];if((o[u]=i)[2]=a(e,t,n))return!0}}}function Lt(o){return 1<o.length?function(e,t,n){for(var r=o.length;r--;)if(!o[r](e,t,n))return!1;return!0}:o[0]}function Ft(e,t,n,r,o){for(var i,a=[],u=0,s=e.length,c=null!=t;u<s;u++)(i=e[u])&&(n&&!n(i,r,o)||(a.push(i),c&&t.push(u)));return a}function Mt(m,g,p,h,v,e){return h&&!h[qe]&&(h=Mt(h)),v&&!v[qe]&&(v=Mt(v,e)),kt(function(e,t,n,r){var o,i,a,u=[],s=[],c=t.length,l=e||function(e,t,n){for(var r=0,o=t.length;r<o;r++)St(e,t[r],n);return n}(g||"*",n.nodeType?[n]:n,[]),f=!m||!e&&g?l:Ft(l,u,m,n,r),d=p?v||(e?m:c||h)?[]:t:f;if(p&&p(f,d,n,r),h)for(o=Ft(d,s),h(o,[],n,r),i=o.length;i--;)(a=o[i])&&(d[s[i]]=!(f[s[i]]=a));if(e){if(v||m){if(v){for(o=[],i=d.length;i--;)(a=d[i])&&o.push(f[i]=a);v(null,d=[],o,r)}for(i=d.length;i--;)(a=d[i])&&-1<(o=v?it.call(e,a):u[i])&&(e[o]=!(t[o]=a))}}else d=Ft(d===t?d.splice(c,d.length):d),v?v(null,t,d,r):rt.apply(t,d)})}function zt(e){for(var r,t,n,o=e.length,i=_e.relative[e[0].type],a=i||_e.relative[" "],u=i?1:0,s=It(function(e){return e===r},a,!0),c=It(function(e){return-1<it.call(r,e)},a,!0),l=[function(e,t,n){return!i&&(n||t!==Pe)||((r=t).nodeType?s(e,t,n):c(e,t,n))}];u<o;u++)if(t=_e.relative[e[u].type])l=[It(Lt(l),t)];else{if((t=_e.filter[e[u].type].apply(null,e[u].matches))[qe]){for(n=++u;n<o&&!_e.relative[e[n].type];n++);return Mt(1<u&&Lt(l),1<u&&Pt(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(lt,"$1"),t,u<n&&zt(e.slice(u,n)),n<o&&zt(e=e.slice(n)),n<o&&Pt(e))}l.push(t)}return Lt(l)}Bt.prototype=_e.filters=_e.pseudos,_e.setFilters=new Bt,De=St.tokenize=function(e,t){var n,r,o,i,a,u,s,c=Ye[e+" "];if(c)return t?0:c.slice(0);for(a=e,u=[],s=_e.preFilter;a;){for(i in n&&!(r=ft.exec(a))||(r&&(a=a.slice(r[0].length)||a),u.push(o=[])),n=!1,(r=dt.exec(a))&&(n=r.shift(),o.push({value:n,type:r[0].replace(lt," ")}),a=a.slice(n.length)),_e.filter)!(r=ht[i].exec(a))||s[i]&&!(r=s[i](r))||(n=r.shift(),o.push({value:n,type:i,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?St.error(e):Ye(e,u).slice(0)},Oe=St.compile=function(e,t){var n,h,v,y,b,r,o=[],i=[],a=Ge[e+" "];if(!a){for(t||(t=De(e)),n=t.length;n--;)(a=zt(t[n]))[qe]?o.push(a):i.push(a);(a=Ge(e,(h=i,y=0<(v=o).length,b=0<h.length,r=function(e,t,n,r,o){var i,a,u,s=0,c="0",l=e&&[],f=[],d=Pe,m=e||b&&_e.find.TAG("*",o),g=We+=null==d?1:Math.random()||.1,p=m.length;for(o&&(Pe=t!==Me&&t);c!==p&&null!=(i=m[c]);c++){if(b&&i){for(a=0;u=h[a++];)if(u(i,t,n)){r.push(i);break}o&&(We=g)}y&&((i=!u&&i)&&s--,e&&l.push(i))}if(s+=c,y&&c!==s){for(a=0;u=v[a++];)u(l,f,t,n);if(e){if(0<s)for(;c--;)l[c]||f[c]||(f[c]=tt.call(r));f=Ft(f)}rt.apply(r,f),o&&!e&&0<f.length&&1<s+v.length&&St.uniqueSort(r)}return o&&(We=g,Pe=d),l},y?kt(r):r))).selector=e}return a},Be=St.select=function(e,t,n,r){var o,i,a,u,s,c="function"==typeof e&&e,l=!r&&De(e=c.selector||e);if(n=n||[],1===l.length){if(2<(i=l[0]=l[0].slice(0)).length&&"ID"===(a=i[0]).type&&ke.getById&&9===t.nodeType&&Ue&&_e.relative[i[1].type]){if(!(t=(_e.find.ID(a.matches[0].replace(Nt,Et),t)||[])[0]))return n;c&&(t=t.parentNode),e=e.slice(i.shift().value.length)}for(o=ht.needsContext.test(e)?0:i.length;o--&&(a=i[o],!_e.relative[u=a.type]);)if((s=_e.find[u])&&(r=s(a.matches[0].replace(Nt,Et),xt.test(i[0].type)&&Ot(t.parentNode)||t))){if(i.splice(o,1),!(e=r.length&&Pt(i)))return rt.apply(n,r),n;break}}return(c||Oe(e,l))(r,t,!Ue,n,xt.test(e)&&Ot(t.parentNode)||t),n},ke.sortStable=qe.split("").sort(Je).join("")===qe,ke.detectDuplicates=!!Le,Fe(),ke.sortDetached=!0;var Ut=Array.isArray,jt=function(e,t,n){var r,o;if(!e)return 0;if(n=n||e,e.length!==undefined){for(r=0,o=e.length;r<o;r++)if(!1===t.call(n,e[r],r,e))return 0}else for(r in e)if(e.hasOwnProperty(r)&&!1===t.call(n,e[r],r,e))return 0;return 1},Vt=function(e,t,n){var r,o;for(r=0,o=e.length;r<o;r++)if(t.call(n,e[r],r,e))return r;return-1},Ht={isArray:Ut,toArray:function(e){var t,n,r=e;if(!Ut(e))for(r=[],t=0,n=e.length;t<n;t++)r[t]=e[t];return r},each:jt,map:function(n,r){var o=[];return jt(n,function(e,t){o.push(r(e,t,n))}),o},filter:function(n,r){var o=[];return jt(n,function(e,t){r&&!r(e,t,n)||o.push(e)}),o},indexOf:function(e,t){var n,r;if(e)for(n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},reduce:function(e,t,n,r){var o=0;for(arguments.length<3&&(n=e[0]);o<e.length;o++)n=t.call(r,n,e[o],o);return n},findIndex:Vt,find:function(e,t,n){var r=Vt(e,t,n);return-1!==r?e[r]:undefined},last:function(e){return e[e.length-1]}},qt=/^\s*|\s*$/g,$t=function(e){return null===e||e===undefined?"":(""+e).replace(qt,"")},Wt=function(e,t){return t?!("array"!==t||!Ht.isArray(e))||typeof e===t:e!==undefined},Kt=function(e,n,r,o){o=o||this,e&&(r&&(e=e[r]),Ht.each(e,function(e,t){if(!1===n.call(o,e,t,r))return!1;Kt(e,n,r,o)}))},Xt={trim:$t,isArray:Ht.isArray,is:Wt,toArray:Ht.toArray,makeMap:function(e,t,n){var r;for(t=t||",","string"==typeof(e=e||[])&&(e=e.split(t)),n=n||{},r=e.length;r--;)n[e[r]]={};return n},each:Ht.each,map:Ht.map,grep:Ht.filter,inArray:Ht.indexOf,hasOwn:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},extend:function(e,t){for(var n,r,o,i=[],a=2;a<arguments.length;a++)i[a-2]=arguments[a];var u,s=arguments;for(n=1,r=s.length;n<r;n++)for(o in t=s[n])t.hasOwnProperty(o)&&(u=t[o])!==undefined&&(e[o]=u);return e},create:function(e,t,n){var r,o,i,a,u,s=this,c=0;if(e=/^((static) )?([\w.]+)(:([\w.]+))?/.exec(e),i=e[3].match(/(^|\.)(\w+)$/i)[2],!(o=s.createNS(e[3].replace(/\.\w+$/,""),n))[i]){if("static"===e[2])return o[i]=t,void(this.onCreate&&this.onCreate(e[2],e[3],o[i]));t[i]||(t[i]=function(){},c=1),o[i]=t[i],s.extend(o[i].prototype,t),e[5]&&(r=s.resolve(e[5]).prototype,a=e[5].match(/\.(\w+)$/i)[1],u=o[i],o[i]=c?function(){return r[a].apply(this,arguments)}:function(){return this.parent=r[a],u.apply(this,arguments)},o[i].prototype[i]=o[i],s.each(r,function(e,t){o[i].prototype[t]=r[t]}),s.each(t,function(e,t){r[t]?o[i].prototype[t]=function(){return this.parent=r[t],e.apply(this,arguments)}:t!==i&&(o[i].prototype[t]=e)})),s.each(t["static"],function(e,t){o[i][t]=e})}},walk:Kt,createNS:function(e,t){var n,r;for(t=t||V.window,e=e.split("."),n=0;n<e.length;n++)t[r=e[n]]||(t[r]={}),t=t[r];return t},resolve:function(e,t){var n,r;for(t=t||V.window,n=0,r=(e=e.split(".")).length;n<r&&(t=t[e[n]]);n++);return t},explode:function(e,t){return!e||Wt(e,"array")?e:Ht.map(e.split(t||","),$t)},_addCacheSuffix:function(e){var t=fe.cacheSuffix;return t&&(e+=(-1===e.indexOf("?")?"?":"&")+t),e}},Yt=V.document,Gt=Array.prototype.push,Jt=Array.prototype.slice,Qt=/^(?:[^#<]*(<[\w\W]+>)[^>]*$|#([\w\-]*)$)/,Zt=Se.Event,en=Xt.makeMap("children,contents,next,prev"),tn=function(e){return void 0!==e},nn=function(e){return"string"==typeof e},rn=function(e,t){var n,r,o;for(o=(t=t||Yt).createElement("div"),n=t.createDocumentFragment(),o.innerHTML=e;r=o.firstChild;)n.appendChild(r);return n},on=function(e,t,n,r){var o;if(nn(t))t=rn(t,bn(e[0]));else if(t.length&&!t.nodeType){if(t=gn.makeArray(t),r)for(o=t.length-1;0<=o;o--)on(e,t[o],n,r);else for(o=0;o<t.length;o++)on(e,t[o],n,r);return e}if(t.nodeType)for(o=e.length;o--;)n.call(e[o],t);return e},an=function(e,t){return e&&t&&-1!==(" "+e.className+" ").indexOf(" "+t+" ")},un=function(e,t,n){var r,o;return t=gn(t)[0],e.each(function(){var e=this;n&&r===e.parentNode||(r=e.parentNode,o=t.cloneNode(!1),e.parentNode.insertBefore(o,e)),o.appendChild(e)}),e},sn=Xt.makeMap("fillOpacity fontWeight lineHeight opacity orphans widows zIndex zoom"," "),cn=Xt.makeMap("checked compact declare defer disabled ismap multiple nohref noshade nowrap readonly selected"," "),ln={"for":"htmlFor","class":"className",readonly:"readOnly"},fn={"float":"cssFloat"},dn={},mn={},gn=function(e,t){return new gn.fn.init(e,t)},pn=/^\s*|\s*$/g,hn=function(e){return null===e||e===undefined?"":(""+e).replace(pn,"")},vn=function(e,t){var n,r,o,i;if(e)if((n=e.length)===undefined){for(r in e)if(e.hasOwnProperty(r)&&(i=e[r],!1===t.call(i,r,i)))break}else for(o=0;o<n&&(i=e[o],!1!==t.call(i,o,i));o++);return e},yn=function(e,n){var r=[];return vn(e,function(e,t){n(t,e)&&r.push(t)}),r},bn=function(e){return e?9===e.nodeType?e:e.ownerDocument:Yt};gn.fn=gn.prototype={constructor:gn,selector:"",context:null,length:0,init:function(e,t){var n,r,o=this;if(!e)return o;if(e.nodeType)return o.context=o[0]=e,o.length=1,o;if(t&&t.nodeType)o.context=t;else{if(t)return gn(e).attr(t);o.context=t=V.document}if(nn(e)){if(!(n="<"===(o.selector=e).charAt(0)&&">"===e.charAt(e.length-1)&&3<=e.length?[null,e,null]:Qt.exec(e)))return gn(t).find(e);if(n[1])for(r=rn(e,bn(t)).firstChild;r;)Gt.call(o,r),r=r.nextSibling;else{if(!(r=bn(t).getElementById(n[2])))return o;if(r.id!==n[2])return o.find(e);o.length=1,o[0]=r}}else this.add(e,!1);return o},toArray:function(){return Xt.toArray(this)},add:function(e,t){var n,r,o=this;if(nn(e))return o.add(gn(e));if(!1!==t)for(n=gn.unique(o.toArray().concat(gn.makeArray(e))),o.length=n.length,r=0;r<n.length;r++)o[r]=n[r];else Gt.apply(o,gn.makeArray(e));return o},attr:function(t,n){var e,r=this;if("object"==typeof t)vn(t,function(e,t){r.attr(e,t)});else{if(!tn(n)){if(r[0]&&1===r[0].nodeType){if((e=dn[t])&&e.get)return e.get(r[0],t);if(cn[t])return r.prop(t)?t:undefined;null===(n=r[0].getAttribute(t,2))&&(n=undefined)}return n}this.each(function(){var e;if(1===this.nodeType){if((e=dn[t])&&e.set)return void e.set(this,n);null===n?this.removeAttribute(t,2):this.setAttribute(t,n,2)}})}return r},removeAttr:function(e){return this.attr(e,null)},prop:function(e,t){var n=this;if("object"==typeof(e=ln[e]||e))vn(e,function(e,t){n.prop(e,t)});else{if(!tn(t))return n[0]&&n[0].nodeType&&e in n[0]?n[0][e]:t;this.each(function(){1===this.nodeType&&(this[e]=t)})}return n},css:function(n,r){var e,o,i=this,t=function(e){return e.replace(/-(\D)/g,function(e,t){return t.toUpperCase()})},a=function(e){return e.replace(/[A-Z]/g,function(e){return"-"+e})};if("object"==typeof n)vn(n,function(e,t){i.css(e,t)});else if(tn(r))n=t(n),"number"!=typeof r||sn[n]||(r=r.toString()+"px"),i.each(function(){var e=this.style;if((o=mn[n])&&o.set)o.set(this,r);else{try{this.style[fn[n]||n]=r}catch(t){}null!==r&&""!==r||(e.removeProperty?e.removeProperty(a(n)):e.removeAttribute(n))}});else{if(e=i[0],(o=mn[n])&&o.get)return o.get(e);if(!e.ownerDocument.defaultView)return e.currentStyle?e.currentStyle[t(n)]:"";try{return e.ownerDocument.defaultView.getComputedStyle(e,null).getPropertyValue(a(n))}catch(u){return undefined}}return i},remove:function(){for(var e,t=this.length;t--;)e=this[t],Zt.clean(e),e.parentNode&&e.parentNode.removeChild(e);return this},empty:function(){for(var e,t=this.length;t--;)for(e=this[t];e.firstChild;)e.removeChild(e.firstChild);return this},html:function(e){var t,n=this;if(tn(e)){t=n.length;try{for(;t--;)n[t].innerHTML=e}catch(r){gn(n[t]).empty().append(e)}return n}return n[0]?n[0].innerHTML:""},text:function(e){var t,n=this;if(tn(e)){for(t=n.length;t--;)"innerText"in n[t]?n[t].innerText=e:n[0].textContent=e;return n}return n[0]?n[0].innerText||n[0].textContent:""},append:function(){return on(this,arguments,function(e){(1===this.nodeType||this.host&&1===this.host.nodeType)&&this.appendChild(e)})},prepend:function(){return on(this,arguments,function(e){(1===this.nodeType||this.host&&1===this.host.nodeType)&&this.insertBefore(e,this.firstChild)},!0)},before:function(){return this[0]&&this[0].parentNode?on(this,arguments,function(e){this.parentNode.insertBefore(e,this)}):this},after:function(){return this[0]&&this[0].parentNode?on(this,arguments,function(e){this.parentNode.insertBefore(e,this.nextSibling)},!0):this},appendTo:function(e){return gn(e).append(this),this},prependTo:function(e){return gn(e).prepend(this),this},replaceWith:function(e){return this.before(e).remove()},wrap:function(e){return un(this,e)},wrapAll:function(e){return un(this,e,!0)},wrapInner:function(e){return this.each(function(){gn(this).contents().wrapAll(e)}),this},unwrap:function(){return this.parent().each(function(){gn(this).replaceWith(this.childNodes)})},clone:function(){var e=[];return this.each(function(){e.push(this.cloneNode(!0))}),gn(e)},addClass:function(e){return this.toggleClass(e,!0)},removeClass:function(e){return this.toggleClass(e,!1)},toggleClass:function(o,i){var e=this;return"string"!=typeof o||(-1!==o.indexOf(" ")?vn(o.split(" "),function(){e.toggleClass(this,i)}):e.each(function(e,t){var n,r;(r=an(t,o))!==i&&(n=t.className,r?t.className=hn((" "+n+" ").replace(" "+o+" "," ")):t.className+=n?" "+o:o)})),e},hasClass:function(e){return an(this[0],e)},each:function(e){return vn(this,e)},on:function(e,t){return this.each(function(){Zt.bind(this,e,t)})},off:function(e,t){return this.each(function(){Zt.unbind(this,e,t)})},trigger:function(e){return this.each(function(){"object"==typeof e?Zt.fire(this,e.type,e):Zt.fire(this,e)})},show:function(){return this.css("display","")},hide:function(){return this.css("display","none")},slice:function(){return new gn(Jt.apply(this,arguments))},eq:function(e){return-1===e?this.slice(e):this.slice(e,+e+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},find:function(e){var t,n,r=[];for(t=0,n=this.length;t<n;t++)gn.find(e,this[t],r);return gn(r)},filter:function(n){return gn("function"==typeof n?yn(this.toArray(),function(e,t){return n(t,e)}):gn.filter(n,this.toArray()))},closest:function(n){var r=[];return n instanceof gn&&(n=n[0]),this.each(function(e,t){for(;t;){if("string"==typeof n&&gn(t).is(n)){r.push(t);break}if(t===n){r.push(t);break}t=t.parentNode}}),gn(r)},offset:function(e){var t,n,r,o,i=0,a=0;return e?this.css(e):((t=this[0])&&(r=(n=t.ownerDocument).documentElement,t.getBoundingClientRect&&(i=(o=t.getBoundingClientRect()).left+(r.scrollLeft||n.body.scrollLeft)-r.clientLeft,a=o.top+(r.scrollTop||n.body.scrollTop)-r.clientTop)),{left:i,top:a})},push:Gt,sort:[].sort,splice:[].splice},Xt.extend(gn,{extend:Xt.extend,makeArray:function(e){return(t=e)&&t===t.window||e.nodeType?[e]:Xt.toArray(e);var t},inArray:function(e,t){var n;if(t.indexOf)return t.indexOf(e);for(n=t.length;n--;)if(t[n]===e)return n;return-1},isArray:Xt.isArray,each:vn,trim:hn,grep:yn,find:St,expr:St.selectors,unique:St.uniqueSort,text:St.getText,contains:St.contains,filter:function(e,t,n){var r=t.length;for(n&&(e=":not("+e+")");r--;)1!==t[r].nodeType&&t.splice(r,1);return t=1===t.length?gn.find.matchesSelector(t[0],e)?[t[0]]:[]:gn.find.matches(e,t)}});var Cn=function(e,t,n){var r=[],o=e[t];for("string"!=typeof n&&n instanceof gn&&(n=n[0]);o&&9!==o.nodeType;){if(n!==undefined){if(o===n)break;if("string"==typeof n&&gn(o).is(n))break}1===o.nodeType&&r.push(o),o=o[t]}return r},xn=function(e,t,n,r){var o=[];for(r instanceof gn&&(r=r[0]);e;e=e[t])if(!n||e.nodeType===n){if(r!==undefined){if(e===r)break;if("string"==typeof r&&gn(e).is(r))break}o.push(e)}return o},wn=function(e,t,n){for(e=e[t];e;e=e[t])if(e.nodeType===n)return e;return null};vn({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return Cn(e,"parentNode")},next:function(e){return wn(e,"nextSibling",1)},prev:function(e){return wn(e,"previousSibling",1)},children:function(e){return xn(e.firstChild,"nextSibling",1)},contents:function(e){return Xt.toArray(("iframe"===e.nodeName?e.contentDocument||e.contentWindow.document:e).childNodes)}},function(e,r){gn.fn[e]=function(t){var n=[];return this.each(function(){var e=r.call(n,this,t,n);e&&(gn.isArray(e)?n.push.apply(n,e):n.push(e))}),1<this.length&&(en[e]||(n=gn.unique(n)),0===e.indexOf("parents")&&(n=n.reverse())),n=gn(n),t?n.filter(t):n}}),vn({parentsUntil:function(e,t){return Cn(e,"parentNode",t)},nextUntil:function(e,t){return xn(e,"nextSibling",1,t).slice(1)},prevUntil:function(e,t){return xn(e,"previousSibling",1,t).slice(1)}},function(r,o){gn.fn[r]=function(t,e){var n=[];return this.each(function(){var e=o.call(n,this,t,n);e&&(gn.isArray(e)?n.push.apply(n,e):n.push(e))}),1<this.length&&(n=gn.unique(n),0!==r.indexOf("parents")&&"prevUntil"!==r||(n=n.reverse())),n=gn(n),e?n.filter(e):n}}),gn.fn.is=function(e){return!!e&&0<this.filter(e).length},gn.fn.init.prototype=gn.fn,gn.overrideDefaults=function(n){var r,o=function(e,t){return r=r||n(),0===arguments.length&&(e=r.element),t||(t=r.context),new o.fn.init(e,t)};return gn.extend(o,this),o};var Nn=function(n,r,e){vn(e,function(e,t){n[e]=n[e]||{},n[e][r]=t})};fe.ie&&fe.ie<8&&(Nn(dn,"get",{maxlength:function(e){var t=e.maxLength;return 2147483647===t?undefined:t},size:function(e){var t=e.size;return 20===t?undefined:t},"class":function(e){return e.className},style:function(e){var t=e.style.cssText;return 0===t.length?undefined:t}}),Nn(dn,"set",{"class":function(e,t){e.className=t},style:function(e,t){e.style.cssText=t}})),fe.ie&&fe.ie<9&&(fn["float"]="styleFloat",Nn(mn,"set",{opacity:function(e,t){var n=e.style;null===t||""===t?n.removeAttribute("filter"):(n.zoom=1,n.filter="alpha(opacity="+100*t+")")}})),gn.attrHooks=dn,gn.cssHooks=mn;var En,Sn,Tn,kn,_n,An,Rn,Dn=function(e,t){var n=function(e,t){for(var n=0;n<e.length;n++){var r=e[n];if(r.test(t))return r}return undefined}(e,t);if(!n)return{major:0,minor:0};var r=function(e){return Number(t.replace(n,"$"+e))};return Bn(r(1),r(2))},On=function(){return Bn(0,0)},Bn=function(e,t){return{major:e,minor:t}},Pn={nu:Bn,detect:function(e,t){var n=String(t).toLowerCase();return 0===e.length?On():Dn(e,n)},unknown:On},In="Firefox",Ln=function(e,t){return function(){return t===e}},Fn=function(e){var t=e.current;return{current:t,version:e.version,isEdge:Ln("Edge",t),isChrome:Ln("Chrome",t),isIE:Ln("IE",t),isOpera:Ln("Opera",t),isFirefox:Ln(In,t),isSafari:Ln("Safari",t)}},Mn={unknown:function(){return Fn({current:undefined,version:Pn.unknown()})},nu:Fn,edge:q("Edge"),chrome:q("Chrome"),ie:q("IE"),opera:q("Opera"),firefox:q(In),safari:q("Safari")},zn="Windows",Un="Android",jn="Solaris",Vn="FreeBSD",Hn=function(e,t){return function(){return t===e}},qn=function(e){var t=e.current;return{current:t,version:e.version,isWindows:Hn(zn,t),isiOS:Hn("iOS",t),isAndroid:Hn(Un,t),isOSX:Hn("OSX",t),isLinux:Hn("Linux",t),isSolaris:Hn(jn,t),isFreeBSD:Hn(Vn,t)}},$n={unknown:function(){return qn({current:undefined,version:Pn.unknown()})},nu:qn,windows:q(zn),ios:q("iOS"),android:q(Un),linux:q("Linux"),osx:q("OSX"),solaris:q(jn),freebsd:q(Vn)},Wn=function(e,t){var n=String(t).toLowerCase();return X(e,function(e){return e.search(n)})},Kn=function(e,n){return Wn(e,n).map(function(e){var t=Pn.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Xn=function(e,n){return Wn(e,n).map(function(e){var t=Pn.detect(e.versionRegexes,n);return{current:e.name,version:t}})},Yn=function(e,t){return-1!==e.indexOf(t)},Gn=function(e){return e.replace(/^\s+|\s+$/g,"")},Jn=function(e){return e.replace(/\s+$/g,"")},Qn=/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,Zn=function(t){return function(e){return Yn(e,t)}},er=[{name:"Edge",versionRegexes:[/.*?edge\/ ?([0-9]+)\.([0-9]+)$/],search:function(e){return Yn(e,"edge/")&&Yn(e,"chrome")&&Yn(e,"safari")&&Yn(e,"applewebkit")}},{name:"Chrome",versionRegexes:[/.*?chrome\/([0-9]+)\.([0-9]+).*/,Qn],search:function(e){return Yn(e,"chrome")&&!Yn(e,"chromeframe")}},{name:"IE",versionRegexes:[/.*?msie\ ?([0-9]+)\.([0-9]+).*/,/.*?rv:([0-9]+)\.([0-9]+).*/],search:function(e){return Yn(e,"msie")||Yn(e,"trident")}},{name:"Opera",versionRegexes:[Qn,/.*?opera\/([0-9]+)\.([0-9]+).*/],search:Zn("opera")},{name:"Firefox",versionRegexes:[/.*?firefox\/\ ?([0-9]+)\.([0-9]+).*/],search:Zn("firefox")},{name:"Safari",versionRegexes:[Qn,/.*?cpu os ([0-9]+)_([0-9]+).*/],search:function(e){return(Yn(e,"safari")||Yn(e,"mobile/"))&&Yn(e,"applewebkit")}}],tr=[{name:"Windows",search:Zn("win"),versionRegexes:[/.*?windows\ nt\ ?([0-9]+)\.([0-9]+).*/]},{name:"iOS",search:function(e){return Yn(e,"iphone")||Yn(e,"ipad")},versionRegexes:[/.*?version\/\ ?([0-9]+)\.([0-9]+).*/,/.*cpu os ([0-9]+)_([0-9]+).*/,/.*cpu iphone os ([0-9]+)_([0-9]+).*/]},{name:"Android",search:Zn("android"),versionRegexes:[/.*?android\ ?([0-9]+)\.([0-9]+).*/]},{name:"OSX",search:Zn("os x"),versionRegexes:[/.*?os\ x\ ?([0-9]+)_([0-9]+).*/]},{name:"Linux",search:Zn("linux"),versionRegexes:[]},{name:"Solaris",search:Zn("sunos"),versionRegexes:[]},{name:"FreeBSD",search:Zn("freebsd"),versionRegexes:[]}],nr={browsers:q(er),oses:q(tr)},rr=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=nr.browsers(),m=nr.oses(),g=Kn(d,e).fold(Mn.unknown,Mn.nu),p=Xn(m,e).fold($n.unknown,$n.nu);return{browser:g,os:p,deviceType:(n=g,r=e,o=(t=p).isiOS()&&!0===/ipad/i.test(r),i=t.isiOS()&&!o,a=t.isAndroid()&&3===t.version.major,u=t.isAndroid()&&4===t.version.major,s=o||a||u&&!0===/mobile/i.test(r),c=t.isiOS()||t.isAndroid(),l=c&&!s,f=n.isSafari()&&t.isiOS()&&!1===/safari/i.test(r),{isiPad:q(o),isiPhone:q(i),isTablet:q(s),isPhone:q(l),isTouch:q(c),isAndroid:t.isAndroid,isiOS:t.isiOS,isWebView:q(f)})}},or={detect:(En=function(){var e=V.navigator.userAgent;return rr(e)},Tn=!1,function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];return Tn||(Tn=!0,Sn=En.apply(null,e)),Sn})},ir=function(e){if(null===e||e===undefined)throw new Error("Node cannot be null or undefined");return{dom:q(e)}},ar={fromHtml:function(e,t){var n=(t||V.document).createElement("div");if(n.innerHTML=e,!n.hasChildNodes()||1<n.childNodes.length)throw V.console.error("HTML does not have a single root node",e),new Error("HTML must have a single root node");return ir(n.childNodes[0])},fromTag:function(e,t){var n=(t||V.document).createElement(e);return ir(n)},fromText:function(e,t){var n=(t||V.document).createTextNode(e);return ir(n)},fromDom:ir,fromPoint:function(e,t,n){var r=e.dom();return _.from(r.elementFromPoint(t,n)).map(ir)}},ur=(V.Node.ATTRIBUTE_NODE,V.Node.CDATA_SECTION_NODE,V.Node.COMMENT_NODE,V.Node.DOCUMENT_NODE),sr=(V.Node.DOCUMENT_TYPE_NODE,V.Node.DOCUMENT_FRAGMENT_NODE,V.Node.ELEMENT_NODE),cr=V.Node.TEXT_NODE,lr=(V.Node.PROCESSING_INSTRUCTION_NODE,V.Node.ENTITY_REFERENCE_NODE,V.Node.ENTITY_NODE,V.Node.NOTATION_NODE,function(e){return e.dom().nodeName.toLowerCase()}),fr=function(t){return function(e){return e.dom().nodeType===t}},dr=fr(sr),mr=fr(cr),gr=Object.keys,pr=Object.hasOwnProperty,hr=function(e,t){for(var n=gr(e),r=0,o=n.length;r<o;r++){var i=n[r];t(e[i],i)}},vr=function(e,r){var o={};return hr(e,function(e,t){var n=r(e,t);o[n.k]=n.v}),o},yr=function(e,n){var r={},o={};return hr(e,function(e,t){(n(e,t)?r:o)[t]=e}),{t:r,f:o}},br=function(e,t){return pr.call(e,t)},Cr=function(e){return e.style!==undefined&&D(e.style.getPropertyValue)},xr=function(e,t,n){if(!(S(n)||R(n)||O(n)))throw V.console.error("Invalid call to Attr.set. Key ",t,":: Value ",n,":: Element ",e),new Error("Attribute value was not simple");e.setAttribute(t,n+"")},wr=function(e,t,n){xr(e.dom(),t,n)},Nr=function(e,t){var n=e.dom();hr(t,function(e,t){xr(n,t,e)})},Er=function(e,t){var n=e.dom().getAttribute(t);return null===n?undefined:n},Sr=function(e,t){e.dom().removeAttribute(t)},Tr=function(e,t){var n=e.dom();hr(t,function(e,t){!function(e,t,n){if(!S(n))throw V.console.error("Invalid call to CSS.set. Property ",t,":: Value ",n,":: Element ",e),new Error("CSS value must be a string: "+n);Cr(e)&&e.style.setProperty(t,n)}(n,t,e)})},kr=function(e,t){var n,r,o=e.dom(),i=V.window.getComputedStyle(o).getPropertyValue(t),a=""!==i||(r=mr(n=e)?n.dom().parentNode:n.dom())!==undefined&&null!==r&&r.ownerDocument.body.contains(r)?i:_r(o,t);return null===a?undefined:a},_r=function(e,t){return Cr(e)?e.style.getPropertyValue(t):""},Ar=function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];return function(){for(var n=[],e=0;e<arguments.length;e++)n[e]=arguments[e];if(t.length!==n.length)throw new Error('Wrong number of arguments to struct. Expected "['+t.length+']", got '+n.length+" arguments");var r={};return z(t,function(e,t){r[e]=q(n[t])}),r}},Rr=function(e,t){for(var n=[],r=function(e){return n.push(e),t(e)},o=t(e);(o=o.bind(r)).isSome(););return n},Dr=function(){return oe.getOrDie("Node")},Or=function(e,t,n){return 0!=(e.compareDocumentPosition(t)&n)},Br=function(e,t){return Or(e,t,Dr().DOCUMENT_POSITION_CONTAINED_BY)},Pr=sr,Ir=ur,Lr=function(e,t){var n=e.dom();if(n.nodeType!==Pr)return!1;var r=n;if(r.matches!==undefined)return r.matches(t);if(r.msMatchesSelector!==undefined)return r.msMatchesSelector(t);if(r.webkitMatchesSelector!==undefined)return r.webkitMatchesSelector(t);if(r.mozMatchesSelector!==undefined)return r.mozMatchesSelector(t);throw new Error("Browser lacks native selectors")},Fr=function(e){return e.nodeType!==Pr&&e.nodeType!==Ir||0===e.childElementCount},Mr=function(e,t){return e.dom()===t.dom()},zr=or.detect().browser.isIE()?function(e,t){return Br(e.dom(),t.dom())}:function(e,t){var n=e.dom(),r=t.dom();return n!==r&&n.contains(r)},Ur=function(e){return ar.fromDom(e.dom().ownerDocument)},jr=function(e){return ar.fromDom(e.dom().ownerDocument.defaultView)},Vr=function(e){return _.from(e.dom().parentNode).map(ar.fromDom)},Hr=function(e){return _.from(e.dom().previousSibling).map(ar.fromDom)},qr=function(e){return _.from(e.dom().nextSibling).map(ar.fromDom)},$r=function(e){return t=Rr(e,Hr),(n=B.call(t,0)).reverse(),n;var t,n},Wr=function(e){return Rr(e,qr)},Kr=function(e){return W(e.dom().childNodes,ar.fromDom)},Xr=function(e,t){var n=e.dom().childNodes;return _.from(n[t]).map(ar.fromDom)},Yr=function(e){return Xr(e,0)},Gr=function(e){return Xr(e,e.dom().childNodes.length-1)},Jr=(Ar("element","offset"),or.detect().browser),Qr=function(e){return X(e,dr)},Zr={getPos:function(e,t,n){var r,o,i,a=0,u=0,s=e.ownerDocument;if(n=n||e,t){if(n===e&&t.getBoundingClientRect&&"static"===kr(ar.fromDom(e),"position"))return{x:a=(o=t.getBoundingClientRect()).left+(s.documentElement.scrollLeft||e.scrollLeft)-s.documentElement.clientLeft,y:u=o.top+(s.documentElement.scrollTop||e.scrollTop)-s.documentElement.clientTop};for(r=t;r&&r!==n&&r.nodeType;)a+=r.offsetLeft||0,u+=r.offsetTop||0,r=r.offsetParent;for(r=t.parentNode;r&&r!==n&&r.nodeType;)a-=r.scrollLeft||0,u-=r.scrollTop||0,r=r.parentNode;u+=(i=ar.fromDom(t),Jr.isFirefox()&&"table"===lr(i)?Qr(Kr(i)).filter(function(e){return"caption"===lr(e)}).bind(function(o){return Qr(Wr(o)).map(function(e){var t=e.dom().offsetTop,n=o.dom().offsetTop,r=o.dom().offsetHeight;return t<=n?-r:0})}).getOr(0):0)}return{x:a,y:u}}},eo={},to={exports:eo};kn=undefined,_n=eo,An=to,Rn=undefined,function(e){"object"==typeof _n&&void 0!==An?An.exports=e():"function"==typeof kn&&kn.amd?kn([],e):("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).EphoxContactWrapper=e()}(function(){return function i(a,u,s){function c(t,e){if(!u[t]){if(!a[t]){var n="function"==typeof Rn&&Rn;if(!e&&n)return n(t,!0);if(l)return l(t,!0);var r=new Error("Cannot find module '"+t+"'");throw r.code="MODULE_NOT_FOUND",r}var o=u[t]={exports:{}};a[t][0].call(o.exports,function(e){return c(a[t][1][e]||e)},o,o.exports,i,a,u,s)}return u[t].exports}for(var l="function"==typeof Rn&&Rn,e=0;e<s.length;e++)c(s[e]);return c}({1:[function(e,t,n){var r,o,i=t.exports={};function a(){throw new Error("setTimeout has not been defined")}function u(){throw new Error("clearTimeout has not been defined")}function s(e){if(r===setTimeout)return setTimeout(e,0);if((r===a||!r)&&setTimeout)return r=setTimeout,setTimeout(e,0);try{return r(e,0)}catch(iE){try{return r.call(null,e,0)}catch(iE){return r.call(this,e,0)}}}!function(){try{r="function"==typeof setTimeout?setTimeout:a}catch(iE){r=a}try{o="function"==typeof clearTimeout?clearTimeout:u}catch(iE){o=u}}();var c,l=[],f=!1,d=-1;function m(){f&&c&&(f=!1,c.length?l=c.concat(l):d=-1,l.length&&g())}function g(){if(!f){var e=s(m);f=!0;for(var t=l.length;t;){for(c=l,l=[];++d<t;)c&&c[d].run();d=-1,t=l.length}c=null,f=!1,function(e){if(o===clearTimeout)return clearTimeout(e);if((o===u||!o)&&clearTimeout)return o=clearTimeout,clearTimeout(e);try{o(e)}catch(iE){try{return o.call(null,e)}catch(iE){return o.call(this,e)}}}(e)}}function p(e,t){this.fun=e,this.array=t}function h(){}i.nextTick=function(e){var t=new Array(arguments.length-1);if(1<arguments.length)for(var n=1;n<arguments.length;n++)t[n-1]=arguments[n];l.push(new p(e,t)),1!==l.length||f||s(g)},p.prototype.run=function(){this.fun.apply(null,this.array)},i.title="browser",i.browser=!0,i.env={},i.argv=[],i.version="",i.versions={},i.on=h,i.addListener=h,i.once=h,i.off=h,i.removeListener=h,i.removeAllListeners=h,i.emit=h,i.prependListener=h,i.prependOnceListener=h,i.listeners=function(e){return[]},i.binding=function(e){throw new Error("process.binding is not supported")},i.cwd=function(){return"/"},i.chdir=function(e){throw new Error("process.chdir is not supported")},i.umask=function(){return 0}},{}],2:[function(e,f,t){(function(n){!function(e){var t=setTimeout;function r(){}function i(e){if("object"!=typeof this)throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=undefined,this._deferreds=[],l(e,this)}function o(n,r){for(;3===n._state;)n=n._value;0!==n._state?(n._handled=!0,i._immediateFn(function(){var e=1===n._state?r.onFulfilled:r.onRejected;if(null!==e){var t;try{t=e(n._value)}catch(iE){return void u(r.promise,iE)}a(r.promise,t)}else(1===n._state?a:u)(r.promise,n._value)})):n._deferreds.push(r)}function a(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if(t instanceof i)return e._state=3,e._value=t,void s(e);if("function"==typeof n)return void l((r=n,o=t,function(){r.apply(o,arguments)}),e)}e._state=1,e._value=t,s(e)}catch(iE){u(e,iE)}var r,o}function u(e,t){e._state=2,e._value=t,s(e)}function s(e){2===e._state&&0===e._deferreds.length&&i._immediateFn(function(){e._handled||i._unhandledRejectionFn(e._value)});for(var t=0,n=e._deferreds.length;t<n;t++)o(e,e._deferreds[t]);e._deferreds=null}function c(e,t,n){this.onFulfilled="function"==typeof e?e:null,this.onRejected="function"==typeof t?t:null,this.promise=n}function l(e,t){var n=!1;try{e(function(e){n||(n=!0,a(t,e))},function(e){n||(n=!0,u(t,e))})}catch(r){if(n)return;n=!0,u(t,r)}}i.prototype["catch"]=function(e){return this.then(null,e)},i.prototype.then=function(e,t){var n=new this.constructor(r);return o(this,new c(e,t,n)),n},i.all=function(e){var s=Array.prototype.slice.call(e);return new i(function(o,i){if(0===s.length)return o([]);var a=s.length;function u(t,e){try{if(e&&("object"==typeof e||"function"==typeof e)){var n=e.then;if("function"==typeof n)return void n.call(e,function(e){u(t,e)},i)}s[t]=e,0==--a&&o(s)}catch(r){i(r)}}for(var e=0;e<s.length;e++)u(e,s[e])})},i.resolve=function(t){return t&&"object"==typeof t&&t.constructor===i?t:new i(function(e){e(t)})},i.reject=function(n){return new i(function(e,t){t(n)})},i.race=function(o){return new i(function(e,t){for(var n=0,r=o.length;n<r;n++)o[n].then(e,t)})},i._immediateFn="function"==typeof n?function(e){n(e)}:function(e){t(e,0)},i._unhandledRejectionFn=function(e){"undefined"!=typeof console&&console&&console.warn("Possible Unhandled Promise Rejection:",e)},i._setImmediateFn=function(e){i._immediateFn=e},i._setUnhandledRejectionFn=function(e){i._unhandledRejectionFn=e},void 0!==f&&f.exports?f.exports=i:e.Promise||(e.Promise=i)}(this)}).call(this,e("timers").setImmediate)},{timers:3}],3:[function(s,e,c){(function(e,t){var r=s("process/browser.js").nextTick,n=Function.prototype.apply,o=Array.prototype.slice,i={},a=0;function u(e,t){this._id=e,this._clearFn=t}c.setTimeout=function(){return new u(n.call(setTimeout,window,arguments),clearTimeout)},c.setInterval=function(){return new u(n.call(setInterval,window,arguments),clearInterval)},c.clearTimeout=c.clearInterval=function(e){e.close()},u.prototype.unref=u.prototype.ref=function(){},u.prototype.close=function(){this._clearFn.call(window,this._id)},c.enroll=function(e,t){clearTimeout(e._idleTimeoutId),e._idleTimeout=t},c.unenroll=function(e){clearTimeout(e._idleTimeoutId),e._idleTimeout=-1},c._unrefActive=c.active=function(e){clearTimeout(e._idleTimeoutId);var t=e._idleTimeout;0<=t&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},c.setImmediate="function"==typeof e?e:function(e){var t=a++,n=!(arguments.length<2)&&o.call(arguments,1);return i[t]=!0,r(function(){i[t]&&(n?e.apply(null,n):e.call(null),c.clearImmediate(t))}),t},c.clearImmediate="function"==typeof t?t:function(e){delete i[e]}}).call(this,s("timers").setImmediate,s("timers").clearImmediate)},{"process/browser.js":1,timers:3}],4:[function(e,t,n){var r=e("promise-polyfill"),o="undefined"!=typeof window?window:Function("return this;")();t.exports={boltExport:o.Promise||r}},{"promise-polyfill":2}]},{},[4])(4)});var no=to.exports.boltExport,ro=function(e){var n=_.none(),t=[],r=function(e){o()?a(e):t.push(e)},o=function(){return n.isSome()},i=function(e){z(e,a)},a=function(t){n.each(function(e){V.setTimeout(function(){t(e)},0)})};return e(function(e){n=_.some(e),i(t),t=[]}),{get:r,map:function(n){return ro(function(t){r(function(e){t(n(e))})})},isReady:o}},oo={nu:ro,pure:function(t){return ro(function(e){e(t)})}},io=function(e){V.setTimeout(function(){throw e},0)},ao=function(n){var e=function(e){n().then(e,io)};return{map:function(e){return ao(function(){return n().then(e)})},bind:function(t){return ao(function(){return n().then(function(e){return t(e).toPromise()})})},anonBind:function(e){return ao(function(){return n().then(function(){return e.toPromise()})})},toLazy:function(){return oo.nu(e)},toCached:function(){var e=null;return ao(function(){return null===e&&(e=n()),e})},toPromise:n,get:e}},uo={nu:function(e){return ao(function(){return new no(e)})},pure:function(e){return ao(function(){return no.resolve(e)})}},so=function(a,e){return e(function(r){var o=[],i=0;0===a.length?r([]):z(a,function(e,t){var n;e.get((n=t,function(e){o[n]=e,++i>=a.length&&r(o)}))})})},co=function(e){return so(e,uo.nu)},lo=function(n){return{is:function(e){return n===e},isValue:C,isError:b,getOr:q(n),getOrThunk:q(n),getOrDie:q(n),or:function(e){return lo(n)},orThunk:function(e){return lo(n)},fold:function(e,t){return t(n)},map:function(e){return lo(e(n))},mapError:function(e){return lo(n)},each:function(e){e(n)},bind:function(e){return e(n)},exists:function(e){return e(n)},forall:function(e){return e(n)},toOption:function(){return _.some(n)}}},fo=function(n){return{is:b,isValue:b,isError:C,getOr:$,getOrThunk:function(e){return e()},getOrDie:function(){return e=String(n),function(){throw new Error(e)}();var e},or:function(e){return e},orThunk:function(e){return e()},fold:function(e,t){return e(n)},map:function(e){return fo(n)},mapError:function(e){return fo(e(n))},each:o,bind:function(e){return fo(n)},exists:b,forall:C,toOption:_.none}},mo={value:lo,error:fo,fromOption:function(e,t){return e.fold(function(){return fo(t)},lo)}};function go(e,u){var t=e,n=function(e,t,n,r){var o,i;if(e){if(!r&&e[t])return e[t];if(e!==u){if(o=e[n])return o;for(i=e.parentNode;i&&i!==u;i=i.parentNode)if(o=i[n])return o}}};this.current=function(){return t},this.next=function(e){return t=n(t,"firstChild","nextSibling",e)},this.prev=function(e){return t=n(t,"lastChild","previousSibling",e)},this.prev2=function(e){return t=function(e,t,n,r){var o,i,a;if(e){if(o=e[n],u&&o===u)return;if(o){if(!r)for(a=o[t];a;a=a[t])if(!a[t])return a;return o}if((i=e.parentNode)&&i!==u)return i}}(t,"lastChild","previousSibling",e)}}var po,ho,vo,yo=function(t){var n;return function(e){return(n=n||function(e,t){for(var n={},r=0,o=e.length;r<o;r++){var i=e[r];n[String(i)]=t(i,r)}return n}(t,q(!0))).hasOwnProperty(lr(e))}},bo=yo(["h1","h2","h3","h4","h5","h6"]),Co=yo(["article","aside","details","div","dt","figcaption","footer","form","fieldset","header","hgroup","html","main","nav","section","summary","body","p","dl","multicol","dd","figure","address","center","blockquote","h1","h2","h3","h4","h5","h6","listing","xmp","pre","plaintext","menu","dir","ul","ol","li","hr","table","tbody","thead","tfoot","th","tr","td","caption"]),xo=function(e){return dr(e)&&!Co(e)},wo=function(e){return dr(e)&&"br"===lr(e)},No=yo(["h1","h2","h3","h4","h5","h6","p","div","address","pre","form","blockquote","center","dir","fieldset","header","footer","article","section","hgroup","aside","nav","figure"]),Eo=yo(["ul","ol","dl"]),So=yo(["li","dd","dt"]),To=yo(["area","base","basefont","br","col","frame","hr","img","input","isindex","link","meta","param","embed","source","wbr","track"]),ko=yo(["thead","tbody","tfoot"]),_o=yo(["td","th"]),Ao=yo(["pre","script","textarea","style"]),Ro=function(t){return function(e){return!!e&&e.nodeType===t}},Do=Ro(1),Oo=function(e){var r=e.toLowerCase().split(" ");return function(e){var t,n;if(e&&e.nodeType)for(n=e.nodeName.toLowerCase(),t=0;t<r.length;t++)if(n===r[t])return!0;return!1}},Bo=function(t){return function(e){if(Do(e)){if(e.contentEditable===t)return!0;if(e.getAttribute("data-mce-contenteditable")===t)return!0}return!1}},Po=Ro(3),Io=Ro(8),Lo=Ro(9),Fo=Ro(11),Mo=Oo("br"),zo=Bo("true"),Uo=Bo("false"),jo={isText:Po,isElement:Do,isComment:Io,isDocument:Lo,isDocumentFragment:Fo,isBr:Mo,isContentEditableTrue:zo,isContentEditableFalse:Uo,isRestrictedNode:function(e){return!!e&&!Object.getPrototypeOf(e)},matchNodeNames:Oo,hasPropValue:function(t,n){return function(e){return Do(e)&&e[t]===n}},hasAttribute:function(t,e){return function(e){return Do(e)&&e.hasAttribute(t)}},hasAttributeValue:function(t,n){return function(e){return Do(e)&&e.getAttribute(t)===n}},matchStyleValues:function(r,e){var o=e.toLowerCase().split(" ");return function(e){var t;if(Do(e))for(t=0;t<o.length;t++){var n=e.ownerDocument.defaultView.getComputedStyle(e,null);if((n?n.getPropertyValue(r):null)===o[t])return!0}return!1}},isBogus:function(e){return Do(e)&&e.hasAttribute("data-mce-bogus")},isBogusAll:function(e){return Do(e)&&"all"===e.getAttribute("data-mce-bogus")},isTable:function(e){return Do(e)&&"TABLE"===e.tagName}},Vo=function(e){return e&&"SPAN"===e.tagName&&"bookmark"===e.getAttribute("data-mce-type")},Ho=function(e,t){var n,r=t.childNodes;if(!jo.isElement(t)||!Vo(t)){for(n=r.length-1;0<=n;n--)Ho(e,r[n]);if(!1===jo.isDocument(t)){if(jo.isText(t)&&0<t.nodeValue.length){var o=Xt.trim(t.nodeValue).length;if(e.isBlock(t.parentNode)||0<o)return;if(0===o&&(a=(i=t).previousSibling&&"SPAN"===i.previousSibling.nodeName,u=i.nextSibling&&"SPAN"===i.nextSibling.nodeName,a&&u))return}else if(jo.isElement(t)&&(1===(r=t.childNodes).length&&Vo(r[0])&&t.parentNode.insertBefore(r[0],t),r.length||To(ar.fromDom(t))))return;e.remove(t)}var i,a,u;return t}},qo={trimNode:Ho},$o=Xt.makeMap,Wo=/[&<>\"\u0060\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Ko=/[<>&\u007E-\uD7FF\uE000-\uFFEF]|[\uD800-\uDBFF][\uDC00-\uDFFF]/g,Xo=/[<>&\"\']/g,Yo=/&#([a-z0-9]+);?|&([a-z0-9]+);/gi,Go={128:"\u20ac",130:"\u201a",131:"\u0192",132:"\u201e",133:"\u2026",134:"\u2020",135:"\u2021",136:"\u02c6",137:"\u2030",138:"\u0160",139:"\u2039",140:"\u0152",142:"\u017d",145:"\u2018",146:"\u2019",147:"\u201c",148:"\u201d",149:"\u2022",150:"\u2013",151:"\u2014",152:"\u02dc",153:"\u2122",154:"\u0161",155:"\u203a",156:"\u0153",158:"\u017e",159:"\u0178"};ho={'"':""","'":"'","<":"<",">":">","&":"&","`":"`"},vo={"<":"<",">":">","&":"&",""":'"',"'":"'"};var Jo=function(e,t){var n,r,o,i={};if(e){for(e=e.split(","),t=t||10,n=0;n<e.length;n+=2)r=String.fromCharCode(parseInt(e[n],t)),ho[r]||(o="&"+e[n+1]+";",i[r]=o,i[o]=r);return i}};po=Jo("50,nbsp,51,iexcl,52,cent,53,pound,54,curren,55,yen,56,brvbar,57,sect,58,uml,59,copy,5a,ordf,5b,laquo,5c,not,5d,shy,5e,reg,5f,macr,5g,deg,5h,plusmn,5i,sup2,5j,sup3,5k,acute,5l,micro,5m,para,5n,middot,5o,cedil,5p,sup1,5q,ordm,5r,raquo,5s,frac14,5t,frac12,5u,frac34,5v,iquest,60,Agrave,61,Aacute,62,Acirc,63,Atilde,64,Auml,65,Aring,66,AElig,67,Ccedil,68,Egrave,69,Eacute,6a,Ecirc,6b,Euml,6c,Igrave,6d,Iacute,6e,Icirc,6f,Iuml,6g,ETH,6h,Ntilde,6i,Ograve,6j,Oacute,6k,Ocirc,6l,Otilde,6m,Ouml,6n,times,6o,Oslash,6p,Ugrave,6q,Uacute,6r,Ucirc,6s,Uuml,6t,Yacute,6u,THORN,6v,szlig,70,agrave,71,aacute,72,acirc,73,atilde,74,auml,75,aring,76,aelig,77,ccedil,78,egrave,79,eacute,7a,ecirc,7b,euml,7c,igrave,7d,iacute,7e,icirc,7f,iuml,7g,eth,7h,ntilde,7i,ograve,7j,oacute,7k,ocirc,7l,otilde,7m,ouml,7n,divide,7o,oslash,7p,ugrave,7q,uacute,7r,ucirc,7s,uuml,7t,yacute,7u,thorn,7v,yuml,ci,fnof,sh,Alpha,si,Beta,sj,Gamma,sk,Delta,sl,Epsilon,sm,Zeta,sn,Eta,so,Theta,sp,Iota,sq,Kappa,sr,Lambda,ss,Mu,st,Nu,su,Xi,sv,Omicron,t0,Pi,t1,Rho,t3,Sigma,t4,Tau,t5,Upsilon,t6,Phi,t7,Chi,t8,Psi,t9,Omega,th,alpha,ti,beta,tj,gamma,tk,delta,tl,epsilon,tm,zeta,tn,eta,to,theta,tp,iota,tq,kappa,tr,lambda,ts,mu,tt,nu,tu,xi,tv,omicron,u0,pi,u1,rho,u2,sigmaf,u3,sigma,u4,tau,u5,upsilon,u6,phi,u7,chi,u8,psi,u9,omega,uh,thetasym,ui,upsih,um,piv,812,bull,816,hellip,81i,prime,81j,Prime,81u,oline,824,frasl,88o,weierp,88h,image,88s,real,892,trade,89l,alefsym,8cg,larr,8ch,uarr,8ci,rarr,8cj,darr,8ck,harr,8dl,crarr,8eg,lArr,8eh,uArr,8ei,rArr,8ej,dArr,8ek,hArr,8g0,forall,8g2,part,8g3,exist,8g5,empty,8g7,nabla,8g8,isin,8g9,notin,8gb,ni,8gf,prod,8gh,sum,8gi,minus,8gn,lowast,8gq,radic,8gt,prop,8gu,infin,8h0,ang,8h7,and,8h8,or,8h9,cap,8ha,cup,8hb,int,8hk,there4,8hs,sim,8i5,cong,8i8,asymp,8j0,ne,8j1,equiv,8j4,le,8j5,ge,8k2,sub,8k3,sup,8k4,nsub,8k6,sube,8k7,supe,8kl,oplus,8kn,otimes,8l5,perp,8m5,sdot,8o8,lceil,8o9,rceil,8oa,lfloor,8ob,rfloor,8p9,lang,8pa,rang,9ea,loz,9j0,spades,9j3,clubs,9j5,hearts,9j6,diams,ai,OElig,aj,oelig,b0,Scaron,b1,scaron,bo,Yuml,m6,circ,ms,tilde,802,ensp,803,emsp,809,thinsp,80c,zwnj,80d,zwj,80e,lrm,80f,rlm,80j,ndash,80k,mdash,80o,lsquo,80p,rsquo,80q,sbquo,80s,ldquo,80t,rdquo,80u,bdquo,810,dagger,811,Dagger,81g,permil,81p,lsaquo,81q,rsaquo,85c,euro",32);var Qo=function(e,t){return e.replace(t?Wo:Ko,function(e){return ho[e]||e})},Zo=function(e,t){return e.replace(t?Wo:Ko,function(e){return 1<e.length?"&#"+(1024*(e.charCodeAt(0)-55296)+(e.charCodeAt(1)-56320)+65536)+";":ho[e]||"&#"+e.charCodeAt(0)+";"})},ei=function(e,t,n){return n=n||po,e.replace(t?Wo:Ko,function(e){return ho[e]||n[e]||e})},ti={encodeRaw:Qo,encodeAllRaw:function(e){return(""+e).replace(Xo,function(e){return ho[e]||e})},encodeNumeric:Zo,encodeNamed:ei,getEncodeFunc:function(e,t){var n=Jo(t)||po,r=$o(e.replace(/\+/g,","));return r.named&&r.numeric?function(e,t){return e.replace(t?Wo:Ko,function(e){return ho[e]!==undefined?ho[e]:n[e]!==undefined?n[e]:1<e.length?"&#"+(1024*(e.charCodeAt(0)-55296)+(e.charCodeAt(1)-56320)+65536)+";":"&#"+e.charCodeAt(0)+";"})}:r.named?t?function(e,t){return ei(e,t,n)}:ei:r.numeric?Zo:Qo},decode:function(e){return e.replace(Yo,function(e,t){return t?65535<(t="x"===t.charAt(0).toLowerCase()?parseInt(t.substr(1),16):parseInt(t,10))?(t-=65536,String.fromCharCode(55296+(t>>10),56320+(1023&t))):Go[t]||String.fromCharCode(t):vo[e]||po[e]||(n=e,(r=ar.fromTag("div").dom()).innerHTML=n,r.textContent||r.innerText||n);var n,r})}},ni={},ri={},oi=Xt.makeMap,ii=Xt.each,ai=Xt.extend,ui=Xt.explode,si=Xt.inArray,ci=function(e,t){return(e=Xt.trim(e))?e.split(t||" "):[]},li=function(e){var u,t,n,r,o,i,s={},a=function(e,t,n){var r,o,i,a=function(e,t){var n,r,o={};for(n=0,r=e.length;n<r;n++)o[e[n]]=t||{};return o};for(t=t||"","string"==typeof(n=n||[])&&(n=ci(n)),r=(e=ci(e)).length;r--;)i={attributes:a(o=ci([u,t].join(" "))),attributesOrder:o,children:a(n,ri)},s[e[r]]=i},c=function(e,t){var n,r,o,i;for(n=(e=ci(e)).length,t=ci(t);n--;)for(r=s[e[n]],o=0,i=t.length;o<i;o++)r.attributes[t[o]]={},r.attributesOrder.push(t[o])};return ni[e]?ni[e]:(u="id accesskey class dir lang style tabindex title role",t="address blockquote div dl fieldset form h1 h2 h3 h4 h5 h6 hr menu ol p pre table ul",n="a abbr b bdo br button cite code del dfn em embed i iframe img input ins kbd label map noscript object q s samp script select small span strong sub sup textarea u var #text #comment","html4"!==e&&(u+=" contenteditable contextmenu draggable dropzone hidden spellcheck translate",t+=" article aside details dialog figure main header footer hgroup section nav",n+=" audio canvas command datalist mark meter output picture progress time wbr video ruby bdi keygen"),"html5-strict"!==e&&(u+=" xml:lang",n=[n,i="acronym applet basefont big font strike tt"].join(" "),ii(ci(i),function(e){a(e,"",n)}),t=[t,o="center dir isindex noframes"].join(" "),r=[t,n].join(" "),ii(ci(o),function(e){a(e,"",r)})),r=r||[t,n].join(" "),a("html","manifest","head body"),a("head","","base command link meta noscript script style title"),a("title hr noscript br"),a("base","href target"),a("link","href rel media hreflang type sizes hreflang"),a("meta","name http-equiv content charset"),a("style","media type scoped"),a("script","src async defer type charset"),a("body","onafterprint onbeforeprint onbeforeunload onblur onerror onfocus onhashchange onload onmessage onoffline ononline onpagehide onpageshow onpopstate onresize onscroll onstorage onunload",r),a("address dt dd div caption","",r),a("h1 h2 h3 h4 h5 h6 pre p abbr code var samp kbd sub sup i b u bdo span legend em strong small s cite dfn","",n),a("blockquote","cite",r),a("ol","reversed start type","li"),a("ul","","li"),a("li","value",r),a("dl","","dt dd"),a("a","href target rel media hreflang type",n),a("q","cite",n),a("ins del","cite datetime",r),a("img","src sizes srcset alt usemap ismap width height"),a("iframe","src name width height",r),a("embed","src type width height"),a("object","data type typemustmatch name usemap form width height",[r,"param"].join(" ")),a("param","name value"),a("map","name",[r,"area"].join(" ")),a("area","alt coords shape href target rel media hreflang type"),a("table","border","caption colgroup thead tfoot tbody tr"+("html4"===e?" col":"")),a("colgroup","span","col"),a("col","span"),a("tbody thead tfoot","","tr"),a("tr","","td th"),a("td","colspan rowspan headers",r),a("th","colspan rowspan headers scope abbr",r),a("form","accept-charset action autocomplete enctype method name novalidate target",r),a("fieldset","disabled form name",[r,"legend"].join(" ")),a("label","form for",n),a("input","accept alt autocomplete checked dirname disabled form formaction formenctype formmethod formnovalidate formtarget height list max maxlength min multiple name pattern readonly required size src step type value width"),a("button","disabled form formaction formenctype formmethod formnovalidate formtarget name type value","html4"===e?r:n),a("select","disabled form multiple name required size","option optgroup"),a("optgroup","disabled label","option"),a("option","disabled label selected value"),a("textarea","cols dirname disabled form maxlength name readonly required rows wrap"),a("menu","type label",[r,"li"].join(" ")),a("noscript","",r),"html4"!==e&&(a("wbr"),a("ruby","",[n,"rt rp"].join(" ")),a("figcaption","",r),a("mark rt rp summary bdi","",n),a("canvas","width height",r),a("video","src crossorigin poster preload autoplay mediagroup loop muted controls width height buffered",[r,"track source"].join(" ")),a("audio","src crossorigin preload autoplay mediagroup loop muted controls buffered volume",[r,"track source"].join(" ")),a("picture","","img source"),a("source","src srcset type media sizes"),a("track","kind src srclang label default"),a("datalist","",[n,"option"].join(" ")),a("article section nav aside main header footer","",r),a("hgroup","","h1 h2 h3 h4 h5 h6"),a("figure","",[r,"figcaption"].join(" ")),a("time","datetime",n),a("dialog","open",r),a("command","type label icon disabled checked radiogroup command"),a("output","for form name",n),a("progress","value max",n),a("meter","value min max low high optimum",n),a("details","open",[r,"summary"].join(" ")),a("keygen","autofocus challenge disabled form keytype name")),"html5-strict"!==e&&(c("script","language xml:space"),c("style","xml:space"),c("object","declare classid code codebase codetype archive standby align border hspace vspace"),c("embed","align name hspace vspace"),c("param","valuetype type"),c("a","charset name rev shape coords"),c("br","clear"),c("applet","codebase archive code object alt name width height align hspace vspace"),c("img","name longdesc align border hspace vspace"),c("iframe","longdesc frameborder marginwidth marginheight scrolling align"),c("font basefont","size color face"),c("input","usemap align"),c("select","onchange"),c("textarea"),c("h1 h2 h3 h4 h5 h6 div p legend caption","align"),c("ul","type compact"),c("li","type"),c("ol dl menu dir","compact"),c("pre","width xml:space"),c("hr","align noshade size width"),c("isindex","prompt"),c("table","summary width frame rules cellspacing cellpadding align bgcolor"),c("col","width align char charoff valign"),c("colgroup","width align char charoff valign"),c("thead","align char charoff valign"),c("tr","align char charoff valign bgcolor"),c("th","axis align char charoff valign nowrap bgcolor width height"),c("form","accept"),c("td","abbr axis scope align char charoff valign nowrap bgcolor width height"),c("tfoot","align char charoff valign"),c("tbody","align char charoff valign"),c("area","nohref"),c("body","background bgcolor text link vlink alink")),"html4"!==e&&(c("input button select textarea","autofocus"),c("input textarea","placeholder"),c("a","download"),c("link script img","crossorigin"),c("iframe","sandbox seamless allowfullscreen")),ii(ci("a form meter progress dfn"),function(e){s[e]&&delete s[e].children[e]}),delete s.caption.children.table,delete s.script,ni[e]=s)},fi=function(e,n){var r;return e&&(r={},"string"==typeof e&&(e={"*":e}),ii(e,function(e,t){r[t]=r[t.toUpperCase()]="map"===n?oi(e,/[, ]/):ui(e,/[, ]/)})),r};function di(i){var e,t,n,r,o,a,u,s,c,l,f,d,m,N={},g={},E=[],p={},h={},v=function(e,t,n){var r=i[e];return r?r=oi(r,/[, ]/,oi(r.toUpperCase(),/[, ]/)):(r=ni[e])||(r=oi(t," ",oi(t.toUpperCase()," ")),r=ai(r,n),ni[e]=r),r};n=li((i=i||{}).schema),!1===i.verify_html&&(i.valid_elements="*[*]"),e=fi(i.valid_styles),t=fi(i.invalid_styles,"map"),s=fi(i.valid_classes,"map"),r=v("whitespace_elements","pre script noscript style textarea video audio iframe object code"),o=v("self_closing_elements","colgroup dd dt li option p td tfoot th thead tr"),a=v("short_ended_elements","area base basefont br col frame hr img input isindex link meta param embed source wbr track"),u=v("boolean_attributes","checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls"),l=v("non_empty_elements","td th iframe video audio object script pre code",a),f=v("move_caret_before_on_enter_elements","table",l),d=v("text_block_elements","h1 h2 h3 h4 h5 h6 p div address pre form blockquote center dir fieldset header footer article section hgroup aside main nav figure"),c=v("block_elements","hr table tbody thead tfoot th tr td li ol ul caption dl dt dd noscript menu isindex option datalist select optgroup figcaption details summary",d),m=v("text_inline_elements","span strong b em i font strike u var cite dfn code mark q sup sub samp"),ii((i.special||"script noscript noframes noembed title style textarea xmp").split(" "),function(e){h[e]=new RegExp("</"+e+"[^>]*>","gi")});var S=function(e){return new RegExp("^"+e.replace(/([?+*])/g,".$1")+"$")},y=function(e){var t,n,r,o,i,a,u,s,c,l,f,d,m,g,p,h,v,y,b,C=/^([#+\-])?([^\[!\/]+)(?:\/([^\[!]+))?(?:(!?)\[([^\]]+)\])?$/,x=/^([!\-])?(\w+[\\:]:\w+|[^=:<]+)?(?:([=:<])(.*))?$/,w=/[*?+]/;if(e)for(e=ci(e,","),N["@"]&&(h=N["@"].attributes,v=N["@"].attributesOrder),t=0,n=e.length;t<n;t++)if(i=C.exec(e[t])){if(g=i[1],c=i[2],p=i[3],s=i[5],a={attributes:d={},attributesOrder:m=[]},"#"===g&&(a.paddEmpty=!0),"-"===g&&(a.removeEmpty=!0),"!"===i[4]&&(a.removeEmptyAttrs=!0),h){for(y in h)d[y]=h[y];m.push.apply(m,v)}if(s)for(r=0,o=(s=ci(s,"|")).length;r<o;r++)if(i=x.exec(s[r])){if(u={},f=i[1],l=i[2].replace(/[\\:]:/g,":"),g=i[3],b=i[4],"!"===f&&(a.attributesRequired=a.attributesRequired||[],a.attributesRequired.push(l),u.required=!0),"-"===f){delete d[l],m.splice(si(m,l),1);continue}g&&("="===g&&(a.attributesDefault=a.attributesDefault||[],a.attributesDefault.push({name:l,value:b}),u.defaultValue=b),":"===g&&(a.attributesForced=a.attributesForced||[],a.attributesForced.push({name:l,value:b}),u.forcedValue=b),"<"===g&&(u.validValues=oi(b,"?"))),w.test(l)?(a.attributePatterns=a.attributePatterns||[],u.pattern=S(l),a.attributePatterns.push(u)):(d[l]||m.push(l),d[l]=u)}h||"@"!==c||(h=d,v=m),p&&(a.outputName=c,N[p]=a),w.test(c)?(a.pattern=S(c),E.push(a)):N[c]=a}},b=function(e){N={},E=[],y(e),ii(n,function(e,t){g[t]=e.children})},C=function(e){var a=/^(~)?(.+)$/;e&&(ni.text_block_elements=ni.block_elements=null,ii(ci(e,","),function(e){var t=a.exec(e),n="~"===t[1],r=n?"span":"div",o=t[2];if(g[o]=g[r],p[o]=r,n||(c[o.toUpperCase()]={},c[o]={}),!N[o]){var i=N[r];delete(i=ai({},i)).removeEmptyAttrs,delete i.removeEmpty,N[o]=i}ii(g,function(e,t){e[r]&&(g[t]=e=ai({},g[t]),e[o]=e[r])})}))},x=function(e){var o=/^([+\-]?)(\w+)\[([^\]]+)\]$/;ni[i.schema]=null,e&&ii(ci(e,","),function(e){var t,n,r=o.exec(e);r&&(n=r[1],t=n?g[r[2]]:g[r[2]]={"#comment":{}},t=g[r[2]],ii(ci(r[3],"|"),function(e){"-"===n?delete t[e]:t[e]={}}))})},w=function(e){var t,n=N[e];if(n)return n;for(t=E.length;t--;)if((n=E[t]).pattern.test(e))return n};return i.valid_elements?b(i.valid_elements):(ii(n,function(e,t){N[t]={attributes:e.attributes,attributesOrder:e.attributesOrder},g[t]=e.children}),"html5"!==i.schema&&ii(ci("strong/b em/i"),function(e){e=ci(e,"/"),N[e[1]].outputName=e[0]}),ii(ci("ol ul sub sup blockquote span font a table tbody tr strong em b i"),function(e){N[e]&&(N[e].removeEmpty=!0)}),ii(ci("p h1 h2 h3 h4 h5 h6 th td pre div address caption li"),function(e){N[e].paddEmpty=!0}),ii(ci("span"),function(e){N[e].removeEmptyAttrs=!0})),C(i.custom_elements),x(i.valid_children),y(i.extended_valid_elements),x("+ol[ul|ol],+ul[ul|ol]"),ii({dd:"dl",dt:"dl",li:"ul ol",td:"tr",th:"tr",tr:"tbody thead tfoot",tbody:"table",thead:"table",tfoot:"table",legend:"fieldset",area:"map",param:"video audio object"},function(e,t){N[t]&&(N[t].parentsRequired=ci(e))}),i.invalid_elements&&ii(ui(i.invalid_elements),function(e){N[e]&&delete N[e]}),w("span")||y("span[!data-mce-type|*]"),{children:g,elements:N,getValidStyles:function(){return e},getValidClasses:function(){return s},getBlockElements:function(){return c},getInvalidStyles:function(){return t},getShortEndedElements:function(){return a},getTextBlockElements:function(){return d},getTextInlineElements:function(){return m},getBoolAttrs:function(){return u},getElementRule:w,getSelfClosingElements:function(){return o},getNonEmptyElements:function(){return l},getMoveCaretBeforeOnEnterElements:function(){return f},getWhiteSpaceElements:function(){return r},getSpecialElements:function(){return h},isValidChild:function(e,t){var n=g[e.toLowerCase()];return!(!n||!n[t.toLowerCase()])},isValid:function(e,t){var n,r,o=w(e);if(o){if(!t)return!0;if(o.attributes[t])return!0;if(n=o.attributePatterns)for(r=n.length;r--;)if(n[r].pattern.test(e))return!0}return!1},getCustomElements:function(){return p},addValidElements:y,setValidElements:b,addCustomElements:C,addValidChildren:x}}var mi=function(e,t,n,r){var o=function(e){return 1<(e=parseInt(e,10).toString(16)).length?e:"0"+e};return"#"+o(t)+o(n)+o(r)};function gi(b,e){var C,t,c,l,x=/rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)/gi,w=/(?:url(?:(?:\(\s*\"([^\"]+)\"\s*\))|(?:\(\s*\'([^\']+)\'\s*\))|(?:\(\s*([^)\s]+)\s*\))))|(?:\'([^\']+)\')|(?:\"([^\"]+)\")/gi,N=/\s*([^:]+):\s*([^;]+);?/g,E=/\s+$/,S={},T="\ufeff";for(b=b||{},e&&(c=e.getValidStyles(),l=e.getInvalidStyles()),t=("\\\" \\' \\; \\: ; : "+T).split(" "),C=0;C<t.length;C++)S[t[C]]=T+C,S[T+C]=t[C];return{toHex:function(e){return e.replace(x,mi)},parse:function(e){var t,n,r,o,i,a,u,s,c={},l=b.url_converter,f=b.url_converter_scope||this,d=function(e,t,n){var r,o,i,a;if((r=c[e+"-top"+t])&&(o=c[e+"-right"+t])&&(i=c[e+"-bottom"+t])&&(a=c[e+"-left"+t])){var u=[r,o,i,a];for(C=u.length-1;C--&&u[C]===u[C+1];);-1<C&&n||(c[e+t]=-1===C?u[0]:u.join(" "),delete c[e+"-top"+t],delete c[e+"-right"+t],delete c[e+"-bottom"+t],delete c[e+"-left"+t])}},m=function(e){var t,n=c[e];if(n){for(t=(n=n.split(" ")).length;t--;)if(n[t]!==n[0])return!1;return c[e]=n[0],!0}},g=function(e){return o=!0,S[e]},p=function(e,t){return o&&(e=e.replace(/\uFEFF[0-9]/g,function(e){return S[e]})),t||(e=e.replace(/\\([\'\";:])/g,"$1")),e},h=function(e){return String.fromCharCode(parseInt(e.slice(1),16))},v=function(e){return e.replace(/\\[0-9a-f]+/gi,h)},y=function(e,t,n,r,o,i){if(o=o||i)return"'"+(o=p(o)).replace(/\'/g,"\\'")+"'";if(t=p(t||n||r),!b.allow_script_urls){var a=t.replace(/[\s\r\n]+/g,"");if(/(java|vb)script:/i.test(a))return"";if(!b.allow_svg_data_urls&&/^data:image\/svg/i.test(a))return""}return l&&(t=l.call(f,t,"style")),"url('"+t.replace(/\'/g,"\\'")+"')"};if(e){for(e=(e=e.replace(/[\u0000-\u001F]/g,"")).replace(/\\[\"\';:\uFEFF]/g,g).replace(/\"[^\"]+\"|\'[^\']+\'/g,function(e){return e.replace(/[;:]/g,g)});t=N.exec(e);)if(N.lastIndex=t.index+t[0].length,n=t[1].replace(E,"").toLowerCase(),r=t[2].replace(E,""),n&&r){if(n=v(n),r=v(r),-1!==n.indexOf(T)||-1!==n.indexOf('"'))continue;if(!b.allow_script_urls&&("behavior"===n||/expression\s*\(|\/\*|\*\//.test(r)))continue;"font-weight"===n&&"700"===r?r="bold":"color"!==n&&"background-color"!==n||(r=r.toLowerCase()),r=(r=r.replace(x,mi)).replace(w,y),c[n]=o?p(r,!0):r}d("border","",!0),d("border","-width"),d("border","-color"),d("border","-style"),d("padding",""),d("margin",""),i="border",u="border-style",s="border-color",m(a="border-width")&&m(u)&&m(s)&&(c[i]=c[a]+" "+c[u]+" "+c[s],delete c[a],delete c[u],delete c[s]),"medium none"===c.border&&delete c.border,"none"===c["border-image"]&&delete c["border-image"]}return c},serialize:function(i,e){var t,n,r,o,a,u="",s=function(e){var t,n,r,o;if(t=c[e])for(n=0,r=t.length;n<r;n++)e=t[n],(o=i[e])&&(u+=(0<u.length?" ":"")+e+": "+o+";")};if(e&&c)s("*"),s(e);else for(t in i)!(n=i[t])||l&&(r=t,o=e,a=void 0,(a=l["*"])&&a[r]||(a=l[o])&&a[r])||(u+=(0<u.length?" ":"")+t+": "+n+";");return u}}}var pi,hi=Xt.each,vi=Xt.grep,yi=fe.ie,bi=/^([a-z0-9],?)+$/i,Ci=/^[ \t\r\n]*$/,xi=function(n,r,o){var e={},i=r.keep_values,t={set:function(e,t,n){r.url_converter&&(t=r.url_converter.call(r.url_converter_scope||o(),t,n,e[0])),e.attr("data-mce-"+n,t).attr(n,t)},get:function(e,t){return e.attr("data-mce-"+t)||e.attr(t)}};return e={style:{set:function(e,t){null===t||"object"!=typeof t?(i&&e.attr("data-mce-style",t),e.attr("style",t)):e.css(t)},get:function(e){var t=e.attr("data-mce-style")||e.attr("style");return t=n.serialize(n.parse(t),e[0].nodeName)}}},i&&(e.href=e.src=t),e},wi=function(e,t){var n=t.attr("style"),r=e.serialize(e.parse(n),t[0].nodeName);r||(r=null),t.attr("data-mce-style",r)},Ni=function(e,t){var n,r,o=0;if(e)for(n=e.nodeType,e=e.previousSibling;e;e=e.previousSibling)r=e.nodeType,(!t||3!==r||r!==n&&e.nodeValue.length)&&(o++,n=r);return o};function Ei(a,u){var s,c=this;void 0===u&&(u={});var r={},i=V.window,o={},t=0,e=function(m,g){void 0===g&&(g={});var p,h=0,v={};p=g.maxLoadTime||5e3;var y=function(e){m.getElementsByTagName("head")[0].appendChild(e)},n=function(e,t,n){var o,r,i,a,u=function(){for(var e=a.passed,t=e.length;t--;)e[t]();a.status=2,a.passed=[],a.failed=[]},s=function(){for(var e=a.failed,t=e.length;t--;)e[t]();a.status=3,a.passed=[],a.failed=[]},c=function(e,t){e()||((new Date).getTime()-i<p?he.setTimeout(t):s())},l=function(){c(function(){for(var e,t,n=m.styleSheets,r=n.length;r--;)if((t=(e=n[r]).ownerNode?e.ownerNode:e.owningElement)&&t.id===o.id)return u(),!0},l)},f=function(){c(function(){try{var e=r.sheet.cssRules;return u(),!!e}catch(t){}},f)};if(e=Xt._addCacheSuffix(e),v[e]?a=v[e]:(a={passed:[],failed:[]},v[e]=a),t&&a.passed.push(t),n&&a.failed.push(n),1!==a.status)if(2!==a.status)if(3!==a.status){if(a.status=1,(o=m.createElement("link")).rel="stylesheet",o.type="text/css",o.id="u"+h++,o.async=!1,o.defer=!1,i=(new Date).getTime(),g.contentCssCors&&(o.crossOrigin="anonymous"),"onload"in o&&!((d=V.navigator.userAgent.match(/WebKit\/(\d*)/))&&parseInt(d[1],10)<536))o.onload=l,o.onerror=s;else{if(0<V.navigator.userAgent.indexOf("Firefox"))return(r=m.createElement("style")).textContent='@import "'+e+'"',f(),void y(r);l()}var d;y(o),o.href=e}else s();else u()},t=function(t){return uo.nu(function(e){n(t,H(e,q(mo.value(t))),H(e,q(mo.error(t))))})},o=function(e){return e.fold($,$)};return{load:n,loadAll:function(e,n,r){co(W(e,t)).get(function(e){var t=K(e,function(e){return e.isValue()});0<t.fail.length?r(t.fail.map(o)):n(t.pass.map(o))})}}}(a,{contentCssCors:u.contentCssCors}),l=[],f=u.schema?u.schema:di({}),d=gi({url_converter:u.url_converter,url_converter_scope:u.url_converter_scope},u.schema),m=u.ownEvents?new Se(u.proxy):Se.Event,n=f.getBlockElements(),g=gn.overrideDefaults(function(){return{context:a,element:j.getRoot()}}),p=function(e){if(e&&a&&"string"==typeof e){var t=a.getElementById(e);return t&&t.id!==e?a.getElementsByName(e)[1]:t}return e},h=function(e){return"string"==typeof e&&(e=p(e)),g(e)},v=function(e,t,n){var r,o,i=h(e);return i.length&&(o=(r=s[t])&&r.get?r.get(i,t):i.attr(t)),void 0===o&&(o=n||""),o},y=function(e){var t=p(e);return t?t.attributes:[]},b=function(e,t,n){var r,o;""===n&&(n=null);var i=h(e);r=i.attr(t),i.length&&((o=s[t])&&o.set?o.set(i,n,t):i.attr(t,n),r!==n&&u.onSetAttrib&&u.onSetAttrib({attrElm:i,attrName:t,attrValue:n}))},C=function(){return u.root_element||a.body},x=function(e,t){return Zr.getPos(a.body,p(e),t)},w=function(e,t,n){var r=h(e);return n?r.css(t):("float"===(t=t.replace(/-(\D)/g,function(e,t){return t.toUpperCase()}))&&(t=fe.ie&&fe.ie<12?"styleFloat":"cssFloat"),r[0]&&r[0].style?r[0].style[t]:undefined)},N=function(e){var t,n;return e=p(e),t=w(e,"width"),n=w(e,"height"),-1===t.indexOf("px")&&(t=0),-1===n.indexOf("px")&&(n=0),{w:parseInt(t,10)||e.offsetWidth||e.clientWidth,h:parseInt(n,10)||e.offsetHeight||e.clientHeight}},E=function(e,t){var n;if(!e)return!1;if(!Array.isArray(e)){if("*"===t)return 1===e.nodeType;if(bi.test(t)){var r=t.toLowerCase().split(/,/),o=e.nodeName.toLowerCase();for(n=r.length-1;0<=n;n--)if(r[n]===o)return!0;return!1}if(e.nodeType&&1!==e.nodeType)return!1}var i=Array.isArray(e)?e:[e];return 0<St(t,i[0].ownerDocument||i[0],null,i).length},S=function(e,t,n,r){var o,i=[],a=p(e);for(r=r===undefined,n=n||("BODY"!==C().nodeName?C().parentNode:null),Xt.is(t,"string")&&(t="*"===(o=t)?function(e){return 1===e.nodeType}:function(e){return E(e,o)});a&&a!==n&&a.nodeType&&9!==a.nodeType;){if(!t||"function"==typeof t&&t(a)){if(!r)return[a];i.push(a)}a=a.parentNode}return r?i:null},T=function(e,t,n){var r=t;if(e)for("string"==typeof t&&(r=function(e){return E(e,t)}),e=e[n];e;e=e[n])if("function"==typeof r&&r(e))return e;return null},k=function(e,n,r){var o,t="string"==typeof e?p(e):e;if(!t)return!1;if(Xt.isArray(t)&&(t.length||0===t.length))return o=[],hi(t,function(e,t){e&&("string"==typeof e&&(e=p(e)),o.push(n.call(r,e,t)))}),o;var i=r||c;return n.call(i,t)},_=function(e,t){h(e).each(function(e,n){hi(t,function(e,t){b(n,t,e)})})},A=function(e,r){var t=h(e);yi?t.each(function(e,t){if(!1!==t.canHaveHTML){for(;t.firstChild;)t.removeChild(t.firstChild);try{t.innerHTML="<br>"+r,t.removeChild(t.firstChild)}catch(n){gn("<div></div>").html("<br>"+r).contents().slice(1).appendTo(t)}return r}}):t.html(r)},R=function(e,n,r,o,i){return k(e,function(e){var t="string"==typeof n?a.createElement(n):n;return _(t,r),o&&("string"!=typeof o&&o.nodeType?t.appendChild(o):"string"==typeof o&&A(t,o)),i?t:e.appendChild(t)})},D=function(e,t,n){return R(a.createElement(e),e,t,n,!0)},O=ti.decode,B=ti.encodeAllRaw,P=function(e,t){var n=h(e);return t?n.each(function(){for(var e;e=this.firstChild;)3===e.nodeType&&0===e.data.length?this.removeChild(e):this.parentNode.insertBefore(e,this)}).remove():n.remove(),1<n.length?n.toArray():n[0]},I=function(e,t,n){h(e).toggleClass(t,n).each(function(){""===this.className&&gn(this).attr("class",null)})},L=function(t,e,n){return k(e,function(e){return Xt.is(e,"array")&&(t=t.cloneNode(!0)),n&&hi(vi(e.childNodes),function(e){t.appendChild(e)}),e.parentNode.replaceChild(t,e)})},F=function(){return a.createRange()},M=function(e,t,n,r){if(Xt.isArray(e)){for(var o=e.length;o--;)e[o]=M(e[o],t,n,r);return e}return!u.collect||e!==a&&e!==i||l.push([e,t,n,r]),m.bind(e,t,n,r||j)},z=function(e,t,n){var r;if(Xt.isArray(e)){for(r=e.length;r--;)e[r]=z(e[r],t,n);return e}if(l&&(e===a||e===i))for(r=l.length;r--;){var o=l[r];e!==o[0]||t&&t!==o[1]||n&&n!==o[2]||m.unbind(o[0],o[1],o[2])}return m.unbind(e,t,n)},U=function(e){if(e&&jo.isElement(e)){var t=e.getAttribute("data-mce-contenteditable");return t&&"inherit"!==t?t:"inherit"!==e.contentEditable?e.contentEditable:null}return null},j={doc:a,settings:u,win:i,files:o,stdMode:!0,boxModel:!0,styleSheetLoader:e,boundEvents:l,styles:d,schema:f,events:m,isBlock:function(e){if("string"==typeof e)return!!n[e];if(e){var t=e.nodeType;if(t)return!(1!==t||!n[e.nodeName])}return!1},$:g,$$:h,root:null,clone:function(t,e){if(!yi||1!==t.nodeType||e)return t.cloneNode(e);if(!e){var n=a.createElement(t.nodeName);return hi(y(t),function(e){b(n,e.nodeName,v(t,e.nodeName))}),n}return null},getRoot:C,getViewPort:function(e){var t=e||i,n=t.document.documentElement;return{x:t.pageXOffset||n.scrollLeft,y:t.pageYOffset||n.scrollTop,w:t.innerWidth||n.clientWidth,h:t.innerHeight||n.clientHeight}},getRect:function(e){var t,n;return e=p(e),t=x(e),n=N(e),{x:t.x,y:t.y,w:n.w,h:n.h}},getSize:N,getParent:function(e,t,n){var r=S(e,t,n,!1);return r&&0<r.length?r[0]:null},getParents:S,get:p,getNext:function(e,t){return T(e,t,"nextSibling")},getPrev:function(e,t){return T(e,t,"previousSibling")},select:function(e,t){return St(e,p(t)||u.root_element||a,[])},is:E,add:R,create:D,createHTML:function(e,t,n){var r,o="";for(r in o+="<"+e,t)t.hasOwnProperty(r)&&null!==t[r]&&"undefined"!=typeof t[r]&&(o+=" "+r+'="'+B(t[r])+'"');return void 0!==n?o+">"+n+"</"+e+">":o+" />"},createFragment:function(e){var t,n=a.createElement("div"),r=a.createDocumentFragment();for(r.appendChild(n),e&&(n.innerHTML=e);t=n.firstChild;)r.appendChild(t);return r.removeChild(n),r},remove:P,setStyle:function(e,t,n){var r=h(e).css(t,n);u.update_styles&&wi(d,r)},getStyle:w,setStyles:function(e,t){var n=h(e).css(t);u.update_styles&&wi(d,n)},removeAllAttribs:function(e){return k(e,function(e){var t,n=e.attributes;for(t=n.length-1;0<=t;t--)e.removeAttributeNode(n.item(t))})},setAttrib:b,setAttribs:_,getAttrib:v,getPos:x,parseStyle:function(e){return d.parse(e)},serializeStyle:function(e,t){return d.serialize(e,t)},addStyle:function(e){var t,n;if(j!==Ei.DOM&&a===V.document){if(r[e])return;r[e]=!0}(n=a.getElementById("mceDefaultStyles"))||((n=a.createElement("style")).id="mceDefaultStyles",n.type="text/css",(t=a.getElementsByTagName("head")[0]).firstChild?t.insertBefore(n,t.firstChild):t.appendChild(n)),n.styleSheet?n.styleSheet.cssText+=e:n.appendChild(a.createTextNode(e))},loadCSS:function(e){var n;j===Ei.DOM||a!==V.document?(e||(e=""),n=a.getElementsByTagName("head")[0],hi(e.split(","),function(e){var t;e=Xt._addCacheSuffix(e),o[e]||(o[e]=!0,t=D("link",{rel:"stylesheet",href:e}),n.appendChild(t))})):Ei.DOM.loadCSS(e)},addClass:function(e,t){h(e).addClass(t)},removeClass:function(e,t){I(e,t,!1)},hasClass:function(e,t){return h(e).hasClass(t)},toggleClass:I,show:function(e){h(e).show()},hide:function(e){h(e).hide()},isHidden:function(e){return"none"===h(e).css("display")},uniqueId:function(e){return(e||"mce_")+t++},setHTML:A,getOuterHTML:function(e){var t="string"==typeof e?p(e):e;return jo.isElement(t)?t.outerHTML:gn("<div></div>").append(gn(t).clone()).html()},setOuterHTML:function(e,t){h(e).each(function(){try{if("outerHTML"in this)return void(this.outerHTML=t)}catch(e){}P(gn(this).html(t),!0)})},decode:O,encode:B,insertAfter:function(e,t){var r=p(t);return k(e,function(e){var t,n;return t=r.parentNode,(n=r.nextSibling)?t.insertBefore(e,n):t.appendChild(e),e})},replace:L,rename:function(t,e){var n;return t.nodeName!==e.toUpperCase()&&(n=D(e),hi(y(t),function(e){b(n,e.nodeName,v(t,e.nodeName))}),L(n,t,!0)),n||t},findCommonAncestor:function(e,t){for(var n,r=e;r;){for(n=t;n&&r!==n;)n=n.parentNode;if(r===n)break;r=r.parentNode}return!r&&e.ownerDocument?e.ownerDocument.documentElement:r},toHex:function(e){return d.toHex(Xt.trim(e))},run:k,getAttribs:y,isEmpty:function(e,t){var n,r,o,i,a,u,s=0;if(e=e.firstChild){a=new go(e,e.parentNode),t=t||(f?f.getNonEmptyElements():null),i=f?f.getWhiteSpaceElements():{};do{if(o=e.nodeType,jo.isElement(e)){var c=e.getAttribute("data-mce-bogus");if(c){e=a.next("all"===c);continue}if(u=e.nodeName.toLowerCase(),t&&t[u]){if("br"===u){s++,e=a.next();continue}return!1}for(n=(r=y(e)).length;n--;)if("name"===(u=r[n].nodeName)||"data-mce-bookmark"===u)return!1}if(8===o)return!1;if(3===o&&!Ci.test(e.nodeValue))return!1;if(3===o&&e.parentNode&&i[e.parentNode.nodeName]&&Ci.test(e.nodeValue))return!1;e=a.next()}while(e)}return s<=1},createRng:F,nodeIndex:Ni,split:function(e,t,n){var r,o,i,a=F();if(e&&t)return a.setStart(e.parentNode,Ni(e)),a.setEnd(t.parentNode,Ni(t)),r=a.extractContents(),(a=F()).setStart(t.parentNode,Ni(t)+1),a.setEnd(e.parentNode,Ni(e)+1),o=a.extractContents(),(i=e.parentNode).insertBefore(qo.trimNode(j,r),e),n?i.insertBefore(n,e):i.insertBefore(t,e),i.insertBefore(qo.trimNode(j,o),e),P(e),n||t},bind:M,unbind:z,fire:function(e,t,n){return m.fire(e,t,n)},getContentEditable:U,getContentEditableParent:function(e){for(var t=C(),n=null;e&&e!==t&&null===(n=U(e));e=e.parentNode);return n},destroy:function(){if(l)for(var e=l.length;e--;){var t=l[e];m.unbind(t[0],t[1],t[2])}St.setDocument&&St.setDocument()},isChildOf:function(e,t){for(;e;){if(t===e)return!0;e=e.parentNode}return!1},dumpRng:function(e){return"startContainer: "+e.startContainer.nodeName+", startOffset: "+e.startOffset+", endContainer: "+e.endContainer.nodeName+", endOffset: "+e.endOffset}};return s=xi(d,u,function(){return j}),j}(pi=Ei||(Ei={})).DOM=pi(V.document),pi.nodeIndex=Ni;var Si=Ei,Ti=Si.DOM,ki=Xt.each,_i=Xt.grep,Ai=function(e){return"function"==typeof e},Ri=function(){var l={},o=[],i={},a=[],f=0;this.isDone=function(e){return 2===l[e]},this.markDone=function(e){l[e]=2},this.add=this.load=function(e,t,n,r){l[e]===undefined&&(o.push(e),l[e]=0),t&&(i[e]||(i[e]=[]),i[e].push({success:t,failure:r,scope:n||this}))},this.remove=function(e){delete l[e],delete i[e]},this.loadQueue=function(e,t,n){this.loadScripts(o,e,t,n)},this.loadScripts=function(n,e,t,r){var u,s=[],c=function(t,e){ki(i[e],function(e){Ai(e[t])&&e[t].call(e.scope)}),i[e]=undefined};a.push({success:e,failure:r,scope:t||this}),(u=function(){var e=_i(n);if(n.length=0,ki(e,function(e){var t,n,r,o,i,a;2!==l[e]?3!==l[e]?1!==l[e]&&(l[e]=1,f++,t=e,n=function(){l[e]=2,f--,c("success",e),u()},r=function(){l[e]=3,f--,s.push(e),c("failure",e),u()},i=(a=Ti).uniqueId(),(o=V.document.createElement("script")).id=i,o.type="text/javascript",o.src=Xt._addCacheSuffix(t),o.onload=function(){a.remove(i),o&&(o.onreadystatechange=o.onload=o=null),n()},o.onerror=function(){Ai(r)?r():"undefined"!=typeof console&&console.log&&console.log("Failed to load script: "+t)},(V.document.getElementsByTagName("head")[0]||V.document.body).appendChild(o)):c("failure",e):c("success",e)}),!f){var t=a.slice(0);a.length=0,ki(t,function(e){0===s.length?Ai(e.success)&&e.success.call(e.scope):Ai(e.failure)&&e.failure.call(e.scope,s)})}})()}};Ri.ScriptLoader=new Ri;var Di,Oi=Xt.each;function Bi(){var r=this,o=[],a={},u={},i=[],s=function(e){var t;return u[e]&&(t=u[e].dependencies),t||[]},c=function(e,t){return"object"==typeof t?t:"string"==typeof e?{prefix:"",resource:t,suffix:""}:{prefix:e.prefix,resource:t,suffix:e.suffix}},l=function(e,n,t,r){var o=s(e);Oi(o,function(e){var t=c(n,e);f(t.resource,t,undefined,undefined)}),t&&(r?t.call(r):t.call(Ri))},f=function(e,t,n,r,o){if(!a[e]){var i="string"==typeof t?t:t.prefix+t.resource+t.suffix;0!==i.indexOf("/")&&-1===i.indexOf("://")&&(i=Bi.baseURL+"/"+i),a[e]=i.substring(0,i.lastIndexOf("/")),u[e]?l(e,t,n,r):Ri.ScriptLoader.add(i,function(){return l(e,t,n,r)},r,o)}};return{items:o,urls:a,lookup:u,_listeners:i,get:function(e){return u[e]?u[e].instance:undefined},dependencies:s,requireLangPack:function(e,t){var n=Bi.language;if(n&&!1!==Bi.languageLoad){if(t)if(-1!==(t=","+t+",").indexOf(","+n.substr(0,2)+","))n=n.substr(0,2);else if(-1===t.indexOf(","+n+","))return;Ri.ScriptLoader.add(a[e]+"/langs/"+n+".js")}},add:function(t,e,n){o.push(e),u[t]={instance:e,dependencies:n};var r=K(i,function(e){return e.name===t});return i=r.fail,Oi(r.pass,function(e){e.callback()}),e},remove:function(e){delete a[e],delete u[e]},createUrl:c,addComponents:function(e,t){var n=r.urls[e];Oi(t,function(e){Ri.ScriptLoader.add(n+"/"+e)})},load:f,waitFor:function(e,t){u.hasOwnProperty(e)?t():i.push({name:e,callback:t})}}}(Di=Bi||(Bi={})).PluginManager=Di(),Di.ThemeManager=Di();var Pi=function(t,n){Vr(t).each(function(e){e.dom().insertBefore(n.dom(),t.dom())})},Ii=function(e,t){qr(e).fold(function(){Vr(e).each(function(e){Fi(e,t)})},function(e){Pi(e,t)})},Li=function(t,n){Yr(t).fold(function(){Fi(t,n)},function(e){t.dom().insertBefore(n.dom(),e.dom())})},Fi=function(e,t){e.dom().appendChild(t.dom())},Mi=function(t,e){z(e,function(e){Fi(t,e)})},zi=function(e){e.dom().textContent="",z(Kr(e),function(e){Ui(e)})},Ui=function(e){var t=e.dom();null!==t.parentNode&&t.parentNode.removeChild(t)},ji=function(e){var t,n=Kr(e);0<n.length&&(t=e,z(n,function(e){Pi(t,e)})),Ui(e)},Vi=function(n,r){var o=null;return{cancel:function(){null!==o&&(V.clearTimeout(o),o=null)},throttle:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];null===o&&(o=V.setTimeout(function(){n.apply(null,e),o=null},r))}}},Hi=function(e){var t=e,n=function(){return t};return{get:n,set:function(e){t=e},clone:function(){return Hi(n())}}},qi=function(e,t){var n=Er(e,t);return n===undefined||""===n?[]:n.split(" ")},$i=function(e){return e.dom().classList!==undefined},Wi=function(e,t){return o=t,i=qi(n=e,r="class").concat([o]),wr(n,r,i.join(" ")),!0;var n,r,o,i},Ki=function(e,t){return o=t,0<(i=U(qi(n=e,r="class"),function(e){return e!==o})).length?wr(n,r,i.join(" ")):Sr(n,r),!1;var n,r,o,i},Xi=function(e,t){$i(e)?e.dom().classList.add(t):Wi(e,t)},Yi=function(e){0===($i(e)?e.dom().classList:qi(e,"class")).length&&Sr(e,"class")},Gi=function(e,t){return $i(e)&&e.dom().classList.contains(t)},Ji=function(e,t){var n=[];return z(Kr(e),function(e){t(e)&&(n=n.concat([e])),n=n.concat(Ji(e,t))}),n},Qi=function(e,t){return n=t,o=(r=e)===undefined?V.document:r.dom(),Fr(o)?[]:W(o.querySelectorAll(n),ar.fromDom);var n,r,o};function Zi(e,t,n,r,o){return e(n,r)?_.some(n):D(o)&&o(n)?_.none():t(n,r,o)}var ea,ta=function(e,t,n){for(var r=e.dom(),o=D(n)?n:q(!1);r.parentNode;){r=r.parentNode;var i=ar.fromDom(r);if(t(i))return _.some(i);if(o(i))break}return _.none()},na=function(e,t,n){return Zi(function(e,t){return t(e)},ta,e,t,n)},ra=function(e,t,n){return ta(e,function(e){return Lr(e,t)},n)},oa=function(e,t){return n=t,o=(r=e)===undefined?V.document:r.dom(),Fr(o)?_.none():_.from(o.querySelector(n)).map(ar.fromDom);var n,r,o},ia=function(e,t,n){return Zi(Lr,ra,e,t,n)},aa=q("mce-annotation"),ua=q("data-mce-annotation"),sa=q("data-mce-annotation-uid"),ca=function(r,e){var t=r.selection.getRng(),n=ar.fromDom(t.startContainer),o=ar.fromDom(r.getBody()),i=e.fold(function(){return"."+aa()},function(e){return"["+ua()+'="'+e+'"]'}),a=Xr(n,t.startOffset).getOr(n),u=ia(a,i,function(e){return Mr(e,o)}),s=function(e,t){return n=t,(r=e.dom())&&r.hasAttribute&&r.hasAttribute(n)?_.some(Er(e,t)):_.none();var n,r};return u.bind(function(e){return s(e,""+sa()).bind(function(n){return s(e,""+ua()).map(function(e){var t=la(r,n);return{uid:n,name:e,elements:t}})})})},la=function(e,t){var n=ar.fromDom(e.getBody());return Qi(n,"["+sa()+'="'+t+'"]')},fa=function(i,e){var n,r,o,a=Hi({}),c=function(e,t){u(e,function(e){return t(e),e})},u=function(e,t){var n=a.get(),r=t(n.hasOwnProperty(e)?n[e]:{listeners:[],previous:Hi(_.none())});n[e]=r,a.set(n)},t=(n=function(){var e,t,n,r=a.get(),o=(e=gr(r),(n=B.call(e,0)).sort(t),n);z(o,function(e){u(e,function(u){var s=u.previous.get();return ca(i,_.some(e)).fold(function(){var t;s.isSome()&&(c(t=e,function(e){z(e.listeners,function(e){return e(!1,t)})}),u.previous.set(_.none()))},function(e){var t,n,r,o=e.uid,i=e.name,a=e.elements;s.is(o)||(n=o,r=a,c(t=i,function(e){z(e.listeners,function(e){return e(!0,t,{uid:n,nodes:W(r,function(e){return e.dom()})})})}),u.previous.set(_.some(o)))}),{previous:u.previous,listeners:u.listeners}})})},r=30,o=null,{cancel:function(){null!==o&&(V.clearTimeout(o),o=null)},throttle:function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];null!==o&&V.clearTimeout(o),o=V.setTimeout(function(){n.apply(null,e),o=null},r)}});return i.on("remove",function(){t.cancel()}),i.on("nodeChange",function(){t.throttle()}),{addListener:function(e,t){u(e,function(e){return{previous:e.previous,listeners:e.listeners.concat([t])}})}}},da=function(e,n){e.on("init",function(){e.serializer.addNodeFilter("span",function(e){z(e,function(t){var e;(e=t,_.from(e.attributes.map[ua()]).bind(n.lookup)).each(function(e){!1===e.persistent&&t.unwrap()})})})})},ma=function(){return(ma=Object.assign||function(e){for(var t,n=1,r=arguments.length;n<r;n++)for(var o in t=arguments[n])Object.prototype.hasOwnProperty.call(t,o)&&(e[o]=t[o]);return e}).apply(this,arguments)},ga=0,pa=function(e,t){return ar.fromDom(e.dom().cloneNode(t))},ha=function(e){return pa(e,!1)},va=function(e){return pa(e,!0)},ya=function(e,t){var n,r,o=Ur(e).dom(),i=ar.fromDom(o.createDocumentFragment()),a=(n=t,(r=(o||V.document).createElement("div")).innerHTML=n,Kr(ar.fromDom(r)));Mi(i,a),zi(e),Fi(e,i)},ba="\ufeff",Ca=function(e){return e===ba},xa=ba,wa=function(e){return e.replace(new RegExp(ba,"g"),"")},Na=jo.isElement,Ea=jo.isText,Sa=function(e){return Ea(e)&&(e=e.parentNode),Na(e)&&e.hasAttribute("data-mce-caret")},Ta=function(e){return Ea(e)&&Ca(e.data)},ka=function(e){return Sa(e)||Ta(e)},_a=function(e){return e.firstChild!==e.lastChild||!jo.isBr(e.firstChild)},Aa=function(e){var t=e.container();return!(!e||!jo.isText(t))&&(t.data.charAt(e.offset())===xa||e.isAtStart()&&Ta(t.previousSibling))},Ra=function(e){var t=e.container();return!(!e||!jo.isText(t))&&(t.data.charAt(e.offset()-1)===xa||e.isAtEnd()&&Ta(t.nextSibling))},Da=function(e,t,n){var r,o,i;return(r=t.ownerDocument.createElement(e)).setAttribute("data-mce-caret",n?"before":"after"),r.setAttribute("data-mce-bogus","all"),r.appendChild(((i=V.document.createElement("br")).setAttribute("data-mce-bogus","1"),i)),o=t.parentNode,n?o.insertBefore(r,t):t.nextSibling?o.insertBefore(r,t.nextSibling):o.appendChild(r),r},Oa=function(e){return Ea(e)&&e.data[0]===xa},Ba=function(e){return Ea(e)&&e.data[e.data.length-1]===xa},Pa=function(e){return e&&e.hasAttribute("data-mce-caret")?(t=e.getElementsByTagName("br"),n=t[t.length-1],jo.isBogus(n)&&n.parentNode.removeChild(n),e.removeAttribute("data-mce-caret"),e.removeAttribute("data-mce-bogus"),e.removeAttribute("style"),e.removeAttribute("_moz_abspos"),e):null;var t,n},Ia=jo.isContentEditableTrue,La=jo.isContentEditableFalse,Fa=jo.isBr,Ma=jo.isText,za=jo.matchNodeNames("script style textarea"),Ua=jo.matchNodeNames("img input textarea hr iframe video audio object"),ja=jo.matchNodeNames("table"),Va=ka,Ha=function(e){return!Va(e)&&(Ma(e)?!za(e.parentNode):Ua(e)||Fa(e)||ja(e)||qa(e))},qa=function(e){return!1===(t=e,jo.isElement(t)&&"true"===t.getAttribute("unselectable"))&&La(e);var t},$a=function(e,t){return Ha(e)&&function(e,t){for(e=e.parentNode;e&&e!==t;e=e.parentNode){if(qa(e))return!1;if(Ia(e))return!0}return!0}(e,t)},Wa=Math.round,Ka=function(e){return e?{left:Wa(e.left),top:Wa(e.top),bottom:Wa(e.bottom),right:Wa(e.right),width:Wa(e.width),height:Wa(e.height)}:{left:0,top:0,bottom:0,right:0,width:0,height:0}},Xa=function(e,t){return e=Ka(e),t||(e.left=e.left+e.width),e.right=e.left,e.width=0,e},Ya=function(e,t,n){return 0<=e&&e<=Math.min(t.height,n.height)/2},Ga=function(e,t){return e.bottom-e.height/2<t.top||!(e.top>t.bottom)&&Ya(t.top-e.bottom,e,t)},Ja=function(e,t){return e.top>t.bottom||!(e.bottom<t.top)&&Ya(t.bottom-e.top,e,t)},Qa=function(e,t,n){return t>=e.left&&t<=e.right&&n>=e.top&&n<=e.bottom},Za=function(e){var t=e.startContainer,n=e.startOffset;return t.hasChildNodes()&&e.endOffset===n+1?t.childNodes[n]:null},eu=function(e,t){return 1===e.nodeType&&e.hasChildNodes()&&(t>=e.childNodes.length&&(t=e.childNodes.length-1),e=e.childNodes[t]),e},tu=new RegExp("[\u0300-\u036f\u0483-\u0487\u0488-\u0489\u0591-\u05bd\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7-\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u08e3-\u0902\u093a\u093c\u0941-\u0948\u094d\u0951-\u0957\u0962-\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2-\u09e3\u0a01-\u0a02\u0a3c\u0a41-\u0a42\u0a47-\u0a48\u0a4b-\u0a4d\u0a51\u0a70-\u0a71\u0a75\u0a81-\u0a82\u0abc\u0ac1-\u0ac5\u0ac7-\u0ac8\u0acd\u0ae2-\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62-\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c00\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55-\u0c56\u0c62-\u0c63\u0c81\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc-\u0ccd\u0cd5-\u0cd6\u0ce2-\u0ce3\u0d01\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62-\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb-\u0ebc\u0ec8-\u0ecd\u0f18-\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86-\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039-\u103a\u103d-\u103e\u1058-\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108d\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17b4-\u17b5\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193b\u1a17-\u1a18\u1a1b\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1ab0-\u1abd\u1abe\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80-\u1b81\u1ba2-\u1ba5\u1ba8-\u1ba9\u1bab-\u1bad\u1be6\u1be8-\u1be9\u1bed\u1bef-\u1bf1\u1c2c-\u1c33\u1c36-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1cf4\u1cf8-\u1cf9\u1dc0-\u1df5\u1dfc-\u1dff\u200c-\u200d\u20d0-\u20dc\u20dd-\u20e0\u20e1\u20e2-\u20e4\u20e5-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302d\u302e-\u302f\u3099-\u309a\ua66f\ua670-\ua672\ua674-\ua67d\ua69e-\ua69f\ua6f0-\ua6f1\ua802\ua806\ua80b\ua825-\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\ua9e5\uaa29-\uaa2e\uaa31-\uaa32\uaa35-\uaa36\uaa43\uaa4c\uaa7c\uaab0\uaab2-\uaab4\uaab7-\uaab8\uaabe-\uaabf\uaac1\uaaec-\uaaed\uaaf6\uabe5\uabe8\uabed\ufb1e\ufe00-\ufe0f\ufe20-\ufe2f\uff9e-\uff9f]"),nu=function(e){return"string"==typeof e&&768<=e.charCodeAt(0)&&tu.test(e)},ru=function(e,t,n){return e.isSome()&&t.isSome()?_.some(n(e.getOrDie(),t.getOrDie())):_.none()},ou=[].slice,iu=function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=ou.call(arguments);return function(e){for(var t=0;t<n.length;t++)if(!n[t](e))return!1;return!0}},au=function(){for(var e=[],t=0;t<arguments.length;t++)e[t]=arguments[t];var n=ou.call(arguments);return function(e){for(var t=0;t<n.length;t++)if(n[t](e))return!0;return!1}},uu=jo.isElement,su=Ha,cu=jo.matchStyleValues("display","block table"),lu=jo.matchStyleValues("float","left right"),fu=iu(uu,su,y(lu)),du=y(jo.matchStyleValues("white-space","pre pre-line pre-wrap")),mu=jo.isText,gu=jo.isBr,pu=Si.nodeIndex,hu=eu,vu=function(e){return"createRange"in e?e.createRange():Si.DOM.createRng()},yu=function(e){return e&&/[\r\n\t ]/.test(e)},bu=function(e){return!!e.setStart&&!!e.setEnd},Cu=function(e){var t,n=e.startContainer,r=e.startOffset;return!!(yu(e.toString())&&du(n.parentNode)&&jo.isText(n)&&(t=n.data,yu(t[r-1])||yu(t[r+1])))},xu=function(e){return 0===e.left&&0===e.right&&0===e.top&&0===e.bottom},wu=function(e){var t,n,r,o,i,a,u,s;return t=0<(n=e.getClientRects()).length?Ka(n[0]):Ka(e.getBoundingClientRect()),!bu(e)&&gu(e)&&xu(t)?(i=(r=e).ownerDocument,a=vu(i),u=i.createTextNode("\xa0"),(s=r.parentNode).insertBefore(u,r),a.setStart(u,0),a.setEnd(u,1),o=Ka(a.getBoundingClientRect()),s.removeChild(u),o):xu(t)&&bu(e)?function(e){var t=e.startContainer,n=e.endContainer,r=e.startOffset,o=e.endOffset;if(t===n&&jo.isText(n)&&0===r&&1===o){var i=e.cloneRange();return i.setEndAfter(n),wu(i)}return null}(e):t},Nu=function(e,t){var n=Xa(e,t);return n.width=1,n.right=n.left+1,n},Eu=function(e){var t,n,r=[],o=function(e){var t,n;0!==e.height&&(0<r.length&&(t=e,n=r[r.length-1],t.left===n.left&&t.top===n.top&&t.bottom===n.bottom&&t.right===n.right)||r.push(e))},i=function(e,t){var n=vu(e.ownerDocument);if(t<e.data.length){if(nu(e.data[t]))return r;if(nu(e.data[t-1])&&(n.setStart(e,t),n.setEnd(e,t+1),!Cu(n)))return o(Nu(wu(n),!1)),r}0<t&&(n.setStart(e,t-1),n.setEnd(e,t),Cu(n)||o(Nu(wu(n),!1))),t<e.data.length&&(n.setStart(e,t),n.setEnd(e,t+1),Cu(n)||o(Nu(wu(n),!0)))};if(mu(e.container()))return i(e.container(),e.offset()),r;if(uu(e.container()))if(e.isAtEnd())n=hu(e.container(),e.offset()),mu(n)&&i(n,n.data.length),fu(n)&&!gu(n)&&o(Nu(wu(n),!1));else{if(n=hu(e.container(),e.offset()),mu(n)&&i(n,0),fu(n)&&e.isAtEnd())return o(Nu(wu(n),!1)),r;t=hu(e.container(),e.offset()-1),fu(t)&&!gu(t)&&(cu(t)||cu(n)||!fu(n))&&o(Nu(wu(t),!1)),fu(n)&&o(Nu(wu(n),!0))}return r};function Su(t,n,e){var r=function(){return e||(e=Eu(Su(t,n))),e};return{container:q(t),offset:q(n),toRange:function(){var e;return(e=vu(t.ownerDocument)).setStart(t,n),e.setEnd(t,n),e},getClientRects:r,isVisible:function(){return 0<r().length},isAtStart:function(){return mu(t),0===n},isAtEnd:function(){return mu(t)?n>=t.data.length:n>=t.childNodes.length},isEqual:function(e){return e&&t===e.container()&&n===e.offset()},getNode:function(e){return hu(t,e?n-1:n)}}}(ea=Su||(Su={})).fromRangeStart=function(e){return ea(e.startContainer,e.startOffset)},ea.fromRangeEnd=function(e){return ea(e.endContainer,e.endOffset)},ea.after=function(e){return ea(e.parentNode,pu(e)+1)},ea.before=function(e){return ea(e.parentNode,pu(e))},ea.isAbove=function(e,t){return ru(Z(t.getClientRects()),ee(e.getClientRects()),Ga).getOr(!1)},ea.isBelow=function(e,t){return ru(ee(t.getClientRects()),Z(e.getClientRects()),Ja).getOr(!1)},ea.isAtStart=function(e){return!!e&&e.isAtStart()},ea.isAtEnd=function(e){return!!e&&e.isAtEnd()},ea.isTextPosition=function(e){return!!e&&jo.isText(e.container())},ea.isElementPosition=function(e){return!1===ea.isTextPosition(e)};var Tu,ku,_u=Su,Au=jo.isText,Ru=jo.isBogus,Du=Si.nodeIndex,Ou=function(e){var t=e.parentNode;return Ru(t)?Ou(t):t},Bu=function(e){return e?Ht.reduce(e.childNodes,function(e,t){return Ru(t)&&"BR"!==t.nodeName?e=e.concat(Bu(t)):e.push(t),e},[]):[]},Pu=function(t){return function(e){return t===e}},Iu=function(e){var t,r,n,o;return(Au(e)?"text()":e.nodeName.toLowerCase())+"["+(r=Bu(Ou(t=e)),n=Ht.findIndex(r,Pu(t),t),r=r.slice(0,n+1),o=Ht.reduce(r,function(e,t,n){return Au(t)&&Au(r[n-1])&&e++,e},0),r=Ht.filter(r,jo.matchNodeNames(t.nodeName)),(n=Ht.findIndex(r,Pu(t),t))-o)+"]"},Lu=function(e,t){var n,r,o,i,a,u=[];return n=t.container(),r=t.offset(),Au(n)?o=function(e,t){for(;(e=e.previousSibling)&&Au(e);)t+=e.data.length;return t}(n,r):(r>=(i=n.childNodes).length?(o="after",r=i.length-1):o="before",n=i[r]),u.push(Iu(n)),a=function(e,t,n){var r=[];for(t=t.parentNode;!(t===e||n&&n(t));t=t.parentNode)r.push(t);return r}(e,n),a=Ht.filter(a,y(jo.isBogus)),(u=u.concat(Ht.map(a,function(e){return Iu(e)}))).reverse().join("/")+","+o},Fu=function(e,t){var n,r,o;return t?(t=(n=t.split(","))[0].split("/"),o=1<n.length?n[1]:"before",(r=Ht.reduce(t,function(e,t){return(t=/([\w\-\(\)]+)\[([0-9]+)\]/.exec(t))?("text()"===t[1]&&(t[1]="#text"),n=e,r=t[1],o=parseInt(t[2],10),i=Bu(n),i=Ht.filter(i,function(e,t){return!Au(e)||!Au(i[t-1])}),(i=Ht.filter(i,jo.matchNodeNames(r)))[o]):null;var n,r,o,i},e))?Au(r)?function(e,t){for(var n,r=e,o=0;Au(r);){if(n=r.data.length,o<=t&&t<=o+n){e=r,t-=o;break}if(!Au(r.nextSibling)){e=r,t=n;break}o+=n,r=r.nextSibling}return Au(e)&&t>e.data.length&&(t=e.data.length),_u(e,t)}(r,parseInt(o,10)):(o="after"===o?Du(r)+1:Du(r),_u(r.parentNode,o)):null):null},Mu=function(e,t){jo.isText(t)&&0===t.data.length&&e.remove(t)},zu=function(e,t,n){var r,o,i,a,u,s,c;jo.isDocumentFragment(n)?(i=e,a=t,u=n,s=_.from(u.firstChild),c=_.from(u.lastChild),a.insertNode(u),s.each(function(e){return Mu(i,e.previousSibling)}),c.each(function(e){return Mu(i,e.nextSibling)})):(r=e,o=n,t.insertNode(o),Mu(r,o.previousSibling),Mu(r,o.nextSibling))},Uu=jo.isContentEditableFalse,ju=function(e,t,n,r,o){var i,a=r[o?"startContainer":"endContainer"],u=r[o?"startOffset":"endOffset"],s=[],c=0,l=e.getRoot();for(jo.isText(a)?s.push(n?function(e,t,n){var r,o;for(o=e(t.data.slice(0,n)).length,r=t.previousSibling;r&&jo.isText(r);r=r.previousSibling)o+=e(r.data).length;return o}(t,a,u):u):(u>=(i=a.childNodes).length&&i.length&&(c=1,u=Math.max(0,i.length-1)),s.push(e.nodeIndex(i[u],n)+c));a&&a!==l;a=a.parentNode)s.push(e.nodeIndex(a,n));return s},Vu=function(e,t,n){var r=0;return Xt.each(e.select(t),function(e){if("all"!==e.getAttribute("data-mce-bogus"))return e!==n&&void r++}),r},Hu=function(e,t){var n,r,o,i=t?"start":"end";n=e[i+"Container"],r=e[i+"Offset"],jo.isElement(n)&&"TR"===n.nodeName&&(n=(o=n.childNodes)[Math.min(t?r:r-1,o.length-1)])&&(r=t?0:n.childNodes.length,e["set"+(t?"Start":"End")](n,r))},qu=function(e){return Hu(e,!0),Hu(e,!1),e},$u=function(e,t){var n;if(jo.isElement(e)&&(e=eu(e,t),Uu(e)))return e;if(ka(e)){if(jo.isText(e)&&Sa(e)&&(e=e.parentNode),n=e.previousSibling,Uu(n))return n;if(n=e.nextSibling,Uu(n))return n}},Wu=function(e,t,n){var r=n.getNode(),o=r?r.nodeName:null,i=n.getRng();if(Uu(r)||"IMG"===o)return{name:o,index:Vu(n.dom,o,r)};var a,u,s,c,l,f,d,m=$u((a=i).startContainer,a.startOffset)||$u(a.endContainer,a.endOffset);return m?{name:o=m.tagName,index:Vu(n.dom,o,m)}:(u=e,c=t,l=i,f=(s=n).dom,(d={}).start=ju(f,u,c,l,!0),s.isCollapsed()||(d.end=ju(f,u,c,l,!1)),d)},Ku=function(e,t,n){var r={"data-mce-type":"bookmark",id:t,style:"overflow:hidden;line-height:0px"};return n?e.create("span",r,""):e.create("span",r)},Xu=function(e,t){var n=e.dom,r=e.getRng(),o=n.uniqueId(),i=e.isCollapsed(),a=e.getNode(),u=a.nodeName;if("IMG"===u)return{name:u,index:Vu(n,u,a)};var s=qu(r.cloneRange());if(!i){s.collapse(!1);var c=Ku(n,o+"_end",t);zu(n,s,c)}(r=qu(r)).collapse(!0);var l=Ku(n,o+"_start",t);return zu(n,r,l),e.moveToBookmark({id:o,keep:1}),{id:o}},Yu={getBookmark:function(e,t,n){return 2===t?Wu(wa,n,e):3===t?(o=(r=e).getRng(),{start:Lu(r.dom.getRoot(),_u.fromRangeStart(o)),end:Lu(r.dom.getRoot(),_u.fromRangeEnd(o))}):t?{rng:e.getRng()}:Xu(e,!1);var r,o},getUndoBookmark:d(Wu,$,!0),getPersistentBookmark:Xu},Gu="_mce_caret",Ju=function(e){return jo.isElement(e)&&e.id===Gu},Qu=function(e,t){for(;t&&t!==e;){if(t.id===Gu)return t;t=t.parentNode}return null},Zu=jo.isElement,es=jo.isText,ts=function(e){var t=e.parentNode;t&&t.removeChild(e)},ns=function(e,t){0===t.length?ts(e):e.nodeValue=t},rs=function(e){var t=wa(e);return{count:e.length-t.length,text:t}},os=function(e,t){return us(e),t},is=function(e,t){var n,r,o,i=t.container(),a=(n=te(i.childNodes),r=e,o=L(n,r),-1===o?_.none():_.some(o)).map(function(e){return e<t.offset()?_u(i,t.offset()-1):t}).getOr(t);return us(e),a},as=function(e,t){return es(e)&&t.container()===e?(r=t,o=rs((n=e).data.substr(0,r.offset())),i=rs(n.data.substr(r.offset())),0<(a=o.text+i.text).length?(ns(n,a),_u(n,r.offset()-o.count)):r):os(e,t);var n,r,o,i,a},us=function(e){if(Zu(e)&&ka(e)&&(_a(e)?e.removeAttribute("data-mce-caret"):ts(e)),es(e)){var t=wa(function(e){try{return e.nodeValue}catch(t){return""}}(e));ns(e,t)}},ss={removeAndReposition:function(e,t){return _u.isTextPosition(t)?as(e,t):(n=e,(r=t).container()===n.parentNode?is(n,r):os(n,r));var n,r},remove:us},cs=or.detect().browser,ls=jo.isContentEditableFalse,fs=function(e,t,n){var r,o,i,a,u,s=Xa(t.getBoundingClientRect(),n);return"BODY"===e.tagName?(r=e.ownerDocument.documentElement,o=e.scrollLeft||r.scrollLeft,i=e.scrollTop||r.scrollTop):(u=e.getBoundingClientRect(),o=e.scrollLeft-u.left,i=e.scrollTop-u.top),s.left+=o,s.right+=o,s.top+=i,s.bottom+=i,s.width=1,0<(a=t.offsetWidth-t.clientWidth)&&(n&&(a*=-1),s.left+=a,s.right+=a),s},ds=function(a,u,e){var t,s,c=Hi(_.none()),l=function(){!function(e){var t,n,r,o,i;for(t=gn("*[contentEditable=false]",e),o=0;o<t.length;o++)r=(n=t[o]).previousSibling,Ba(r)&&(1===(i=r.data).length?r.parentNode.removeChild(r):r.deleteData(i.length-1,1)),r=n.nextSibling,Oa(r)&&(1===(i=r.data).length?r.parentNode.removeChild(r):r.deleteData(0,1))}(a),s&&(ss.remove(s),s=null),c.get().each(function(e){gn(e.caret).remove(),c.set(_.none())}),clearInterval(t)},f=function(){t=he.setInterval(function(){e()?gn("div.mce-visual-caret",a).toggleClass("mce-visual-caret-hidden"):gn("div.mce-visual-caret",a).addClass("mce-visual-caret-hidden")},500)};return{show:function(t,e){var n,r,o;if(l(),o=e,jo.isElement(o)&&/^(TD|TH)$/i.test(o.tagName))return null;if(!u(e))return s=function(e,t){var n,r,o;if(r=e.ownerDocument.createTextNode(xa),o=e.parentNode,t){if(n=e.previousSibling,Ea(n)){if(ka(n))return n;if(Ba(n))return n.splitText(n.data.length-1)}o.insertBefore(r,e)}else{if(n=e.nextSibling,Ea(n)){if(ka(n))return n;if(Oa(n))return n.splitText(1),n}e.nextSibling?o.insertBefore(r,e.nextSibling):o.appendChild(r)}return r}(e,t),r=e.ownerDocument.createRange(),ls(s.nextSibling)?(r.setStart(s,0),r.setEnd(s,0)):(r.setStart(s,1),r.setEnd(s,1)),r;s=Da("p",e,t),n=fs(a,e,t),gn(s).css("top",n.top);var i=gn('<div class="mce-visual-caret" data-mce-bogus="all"></div>').css(n).appendTo(a)[0];return c.set(_.some({caret:i,element:e,before:t})),c.get().each(function(e){t&&gn(e.caret).addClass("mce-visual-caret-before")}),f(),(r=e.ownerDocument.createRange()).setStart(s,0),r.setEnd(s,0),r},hide:l,getCss:function(){return".mce-visual-caret {position: absolute;background-color: black;background-color: currentcolor;}.mce-visual-caret-hidden {display: none;}*[data-mce-caret] {position: absolute;left: -1000px;right: auto;top: 0;margin: 0;padding: 0;}"},reposition:function(){c.get().each(function(e){var t=fs(a,e.element,e.before);gn(e.caret).css(t)})},destroy:function(){return he.clearInterval(t)}}},ms=function(){return cs.isIE()||cs.isEdge()||cs.isFirefox()},gs=function(e){return ls(e)||jo.isTable(e)&&ms()},ps=jo.isContentEditableFalse,hs=jo.matchStyleValues("display","block table table-cell table-caption list-item"),vs=ka,ys=Sa,bs=jo.isElement,Cs=Ha,xs=function(e){return 0<e},ws=function(e){return e<0},Ns=function(e,t){for(var n;n=e(t);)if(!ys(n))return n;return null},Es=function(e,t,n,r,o){var i=new go(e,r);if(ws(t)){if((ps(e)||ys(e))&&n(e=Ns(i.prev,!0)))return e;for(;e=Ns(i.prev,o);)if(n(e))return e}if(xs(t)){if((ps(e)||ys(e))&&n(e=Ns(i.next,!0)))return e;for(;e=Ns(i.next,o);)if(n(e))return e}return null},Ss=function(e,t){for(;e&&e!==t;){if(hs(e))return e;e=e.parentNode}return null},Ts=function(e,t,n){return Ss(e.container(),n)===Ss(t.container(),n)},ks=function(e,t){var n,r;return t?(n=t.container(),r=t.offset(),bs(n)?n.childNodes[r+e]:null):null},_s=function(e,t){var n=t.ownerDocument.createRange();return e?(n.setStartBefore(t),n.setEndBefore(t)):(n.setStartAfter(t),n.setEndAfter(t)),n},As=function(e,t,n){var r,o,i,a;for(o=e?"previousSibling":"nextSibling";n&&n!==t;){if(r=n[o],vs(r)&&(r=r[o]),ps(r)){if(a=n,Ss(r,i=t)===Ss(a,i))return r;break}if(Cs(r))break;n=n.parentNode}return null},Rs=d(_s,!0),Ds=d(_s,!1),Os=function(e,t,n){var r,o,i,a,u=d(As,!0,t),s=d(As,!1,t);if(o=n.startContainer,i=n.startOffset,Sa(o)){if(bs(o)||(o=o.parentNode),"before"===(a=o.getAttribute("data-mce-caret"))&&(r=o.nextSibling,gs(r)))return Rs(r);if("after"===a&&(r=o.previousSibling,gs(r)))return Ds(r)}if(!n.collapsed)return n;if(jo.isText(o)){if(vs(o)){if(1===e){if(r=s(o))return Rs(r);if(r=u(o))return Ds(r)}if(-1===e){if(r=u(o))return Ds(r);if(r=s(o))return Rs(r)}return n}if(Ba(o)&&i>=o.data.length-1)return 1===e&&(r=s(o))?Rs(r):n;if(Oa(o)&&i<=1)return-1===e&&(r=u(o))?Ds(r):n;if(i===o.data.length)return(r=s(o))?Rs(r):n;if(0===i)return(r=u(o))?Ds(r):n}return n},Bs=function(e,t){return _.from(ks(e?0:-1,t)).filter(ps)},Ps=function(e,t,n){var r=Os(e,t,n);return-1===e?Su.fromRangeStart(r):Su.fromRangeEnd(r)},Is=function(e){return _.from(e.getNode()).map(ar.fromDom)},Ls=function(e,t){for(;t=e(t);)if(t.isVisible())return t;return t},Fs=function(e,t){var n=Ts(e,t);return!(n||!jo.isBr(e.getNode()))||n};(ku=Tu||(Tu={}))[ku.Backwards=-1]="Backwards",ku[ku.Forwards=1]="Forwards";var Ms,zs,Us,js=jo.isContentEditableFalse,Vs=jo.isText,Hs=jo.isElement,qs=jo.isBr,$s=Ha,Ws=function(e){return Ua(e)||!!qa(t=e)&&!0!==j(te(t.getElementsByTagName("*")),function(e,t){return e||Ia(t)},!1);var t},Ks=$a,Xs=function(e,t){return e.hasChildNodes()&&t<e.childNodes.length?e.childNodes[t]:null},Ys=function(e,t){if(xs(e)){if($s(t.previousSibling)&&!Vs(t.previousSibling))return _u.before(t);if(Vs(t))return _u(t,0)}if(ws(e)){if($s(t.nextSibling)&&!Vs(t.nextSibling))return _u.after(t);if(Vs(t))return _u(t,t.data.length)}return ws(e)?qs(t)?_u.before(t):_u.after(t):_u.before(t)},Gs=function(e,t,n){var r,o,i,a,u;if(!Hs(n)||!t)return null;if(t.isEqual(_u.after(n))&&n.lastChild){if(u=_u.after(n.lastChild),ws(e)&&$s(n.lastChild)&&Hs(n.lastChild))return qs(n.lastChild)?_u.before(n.lastChild):u}else u=t;var s,c,l,f=u.container(),d=u.offset();if(Vs(f)){if(ws(e)&&0<d)return _u(f,--d);if(xs(e)&&d<f.length)return _u(f,++d);r=f}else{if(ws(e)&&0<d&&(o=Xs(f,d-1),$s(o)))return!Ws(o)&&(i=Es(o,e,Ks,o))?Vs(i)?_u(i,i.data.length):_u.after(i):Vs(o)?_u(o,o.data.length):_u.before(o);if(xs(e)&&d<f.childNodes.length&&(o=Xs(f,d),$s(o)))return qs(o)?(s=n,(l=(c=o).nextSibling)&&$s(l)?Vs(l)?_u(l,0):_u.before(l):Gs(Tu.Forwards,_u.after(c),s)):!Ws(o)&&(i=Es(o,e,Ks,o))?Vs(i)?_u(i,0):_u.before(i):Vs(o)?_u(o,0):_u.after(o);r=o||u.getNode()}return(xs(e)&&u.isAtEnd()||ws(e)&&u.isAtStart())&&(r=Es(r,e,q(!0),n,!0),Ks(r,n))?Ys(e,r):(o=Es(r,e,Ks,n),!(a=Ht.last(U(function(e,t){for(var n=[];e&&e!==t;)n.push(e),e=e.parentNode;return n}(f,n),js)))||o&&a.contains(o)?o?Ys(e,o):null:u=xs(e)?_u.after(a):_u.before(a))},Js=function(t){return{next:function(e){return Gs(Tu.Forwards,e,t)},prev:function(e){return Gs(Tu.Backwards,e,t)}}},Qs=function(e){return _u.isTextPosition(e)?0===e.offset():Ha(e.getNode())},Zs=function(e){if(_u.isTextPosition(e)){var t=e.container();return e.offset()===t.data.length}return Ha(e.getNode(!0))},ec=function(e,t){return!_u.isTextPosition(e)&&!_u.isTextPosition(t)&&e.getNode()===t.getNode(!0)},tc=function(e,t,n){return e?!ec(t,n)&&(r=t,!(!_u.isTextPosition(r)&&jo.isBr(r.getNode())))&&Zs(t)&&Qs(n):!ec(n,t)&&Qs(t)&&Zs(n);var r},nc=function(e,t,n){var r=Js(t);return _.from(e?r.next(n):r.prev(n))},rc=function(t,n,r){return nc(t,n,r).bind(function(e){return Ts(r,e,n)&&tc(t,r,e)?nc(t,n,e):_.some(e)})},oc=function(t,n,e,r){return rc(t,n,e).bind(function(e){return r(e)?oc(t,n,e,r):_.some(e)})},ic=function(e,t){var n,r,o,i,a,u=e?t.firstChild:t.lastChild;return jo.isText(u)?_.some(_u(u,e?0:u.data.length)):u?Ha(u)?_.some(e?_u.before(u):(a=u,jo.isBr(a)?_u.before(a):_u.after(a))):(r=t,o=u,i=(n=e)?_u.before(o):_u.after(o),nc(n,r,i)):_.none()},ac=d(nc,!0),uc=d(nc,!1),sc={fromPosition:nc,nextPosition:ac,prevPosition:uc,navigate:rc,navigateIgnore:oc,positionIn:ic,firstPositionIn:d(ic,!0),lastPositionIn:d(ic,!1)},cc=function(e,t){return jo.isElement(t)&&e.isBlock(t)&&!t.innerHTML&&!fe.ie&&(t.innerHTML='<br data-mce-bogus="1" />'),t},lc=function(e,t){return sc.lastPositionIn(e).fold(function(){return!1},function(e){return t.setStart(e.container(),e.offset()),t.setEnd(e.container(),e.offset()),!0})},fc=function(e,t,n){return!(!1!==t.hasChildNodes()||!Qu(e,t)||(o=n,i=(r=t).ownerDocument.createTextNode(xa),r.appendChild(i),o.setStart(i,0),o.setEnd(i,0),0));var r,o,i},dc=function(e,t,n,r){var o,i,a,u,s=n[t?"start":"end"],c=e.getRoot();if(s){for(a=s[0],i=c,o=s.length-1;1<=o;o--){if(u=i.childNodes,fc(c,i,r))return!0;if(s[o]>u.length-1)return!!fc(c,i,r)||lc(i,r);i=u[s[o]]}3===i.nodeType&&(a=Math.min(s[0],i.nodeValue.length)),1===i.nodeType&&(a=Math.min(s[0],i.childNodes.length)),t?r.setStart(i,a):r.setEnd(i,a)}return!0},mc=function(e){return jo.isText(e)&&0<e.data.length},gc=function(e,t,n){var r,o,i,a,u,s,c=e.get(n.id+"_"+t),l=n.keep;if(c){if(r=c.parentNode,"start"===t?l?c.hasChildNodes()?(r=c.firstChild,o=1):mc(c.nextSibling)?(r=c.nextSibling,o=0):mc(c.previousSibling)?(r=c.previousSibling,o=c.previousSibling.data.length):(r=c.parentNode,o=e.nodeIndex(c)+1):o=e.nodeIndex(c):l?c.hasChildNodes()?(r=c.firstChild,o=1):mc(c.previousSibling)?(r=c.previousSibling,o=c.previousSibling.data.length):(r=c.parentNode,o=e.nodeIndex(c)):o=e.nodeIndex(c),u=r,s=o,!l){for(a=c.previousSibling,i=c.nextSibling,Xt.each(Xt.grep(c.childNodes),function(e){jo.isText(e)&&(e.nodeValue=e.nodeValue.replace(/\uFEFF/g,""))});c=e.get(n.id+"_"+t);)e.remove(c,!0);a&&i&&a.nodeType===i.nodeType&&jo.isText(a)&&!fe.opera&&(o=a.nodeValue.length,a.appendData(i.nodeValue),e.remove(i),u=a,s=o)}return _.some(_u(u,s))}return _.none()},pc=function(e,t){var n,r,o,i,a,u,s,c,l,f,d,m,g,p,h,v,y=e.dom;if(t){if(v=t,Xt.isArray(v.start))return p=t,h=(g=y).createRng(),dc(g,!0,p,h)&&dc(g,!1,p,h)?_.some(h):_.none();if("string"==typeof t.start)return _.some((f=t,d=(l=y).createRng(),m=Fu(l.getRoot(),f.start),d.setStart(m.container(),m.offset()),m=Fu(l.getRoot(),f.end),d.setEnd(m.container(),m.offset()),d));if(t.hasOwnProperty("id"))return s=gc(o=y,"start",i=t),c=gc(o,"end",i),ru(s,(u=s,(a=c).isSome()?a:u),function(e,t){var n=o.createRng();return n.setStart(cc(o,e.container()),e.offset()),n.setEnd(cc(o,t.container()),t.offset()),n});if(t.hasOwnProperty("name"))return n=y,r=t,_.from(n.select(r.name)[r.index]).map(function(e){var t=n.createRng();return t.selectNode(e),t});if(t.hasOwnProperty("rng"))return _.some(t.rng)}return _.none()},hc=function(e,t,n){return Yu.getBookmark(e,t,n)},vc=function(t,e){pc(t,e).each(function(e){t.setRng(e)})},yc=function(e){return jo.isElement(e)&&"SPAN"===e.tagName&&"bookmark"===e.getAttribute("data-mce-type")},bc=function(e){return e&&/^(IMG)$/.test(e.nodeName)},Cc=function(e){return e&&3===e.nodeType&&/^([\t \r\n]+|)$/.test(e.nodeValue)},xc=function(e,t,n){return"color"!==n&&"backgroundColor"!==n||(t=e.toHex(t)),"fontWeight"===n&&700===t&&(t="bold"),"fontFamily"===n&&(t=t.replace(/[\'\"]/g,"").replace(/,\s+/g,",")),""+t},wc={isInlineBlock:bc,moveStart:function(e,t,n){var r,o,i,a=n.startOffset,u=n.startContainer;if((n.startContainer!==n.endContainer||!bc(n.startContainer.childNodes[n.startOffset]))&&1===u.nodeType)for(a<(i=u.childNodes).length?r=new go(u=i[a],e.getParent(u,e.isBlock)):(r=new go(u=i[i.length-1],e.getParent(u,e.isBlock))).next(!0),o=r.current();o;o=r.next())if(3===o.nodeType&&!Cc(o))return n.setStart(o,0),void t.setRng(n)},getNonWhiteSpaceSibling:function(e,t,n){if(e)for(t=t?"nextSibling":"previousSibling",e=n?e:e[t];e;e=e[t])if(1===e.nodeType||!Cc(e))return e},isTextBlock:function(e,t){return t.nodeType&&(t=t.nodeName),!!e.schema.getTextBlockElements()[t.toLowerCase()]},isValid:function(e,t,n){return e.schema.isValidChild(t,n)},isWhiteSpaceNode:Cc,replaceVars:function(e,n){return"string"!=typeof e?e=e(n):n&&(e=e.replace(/%(\w+)/g,function(e,t){return n[t]||e})),e},isEq:function(e,t){return t=t||"",e=""+((e=e||"").nodeName||e),t=""+(t.nodeName||t),e.toLowerCase()===t.toLowerCase()},normalizeStyleValue:xc,getStyle:function(e,t,n){return xc(e,e.getStyle(t,n),n)},getTextDecoration:function(t,e){var n;return t.getParent(e,function(e){return(n=t.getStyle(e,"text-decoration"))&&"none"!==n}),n},getParents:function(e,t,n){return e.getParents(t,n,e.getRoot())}},Nc=yc,Ec=wc.getParents,Sc=wc.isWhiteSpaceNode,Tc=wc.isTextBlock,kc=function(e,t){for(void 0===t&&(t=3===e.nodeType?e.length:e.childNodes.length);e&&e.hasChildNodes();)(e=e.childNodes[t])&&(t=3===e.nodeType?e.length:e.childNodes.length);return{node:e,offset:t}},_c=function(e,t){for(var n=t;n;){if(1===n.nodeType&&e.getContentEditable(n))return"false"===e.getContentEditable(n)?n:t;n=n.parentNode}return t},Ac=function(e,t,n,r){var o,i,a=n.nodeValue;return void 0===r&&(r=e?a.length:0),e?(o=a.lastIndexOf(" ",r),-1!==(o=(i=a.lastIndexOf("\xa0",r))<o?o:i)&&!t&&(o<r||!e)&&o<=a.length&&o++):(o=a.indexOf(" ",r),i=a.indexOf("\xa0",r),o=-1!==o&&(-1===i||o<i)?o:i),o},Rc=function(e,t,n,r,o,i){var a,u,s,c;if(3===n.nodeType){if(-1!==(s=Ac(o,i,n,r)))return{container:n,offset:s};c=n}for(a=new go(n,e.getParent(n,e.isBlock)||t);u=a[o?"prev":"next"]();)if(3!==u.nodeType||Nc(u.parentNode)){if(e.isBlock(u)||wc.isEq(u,"BR"))break}else if(-1!==(s=Ac(o,i,c=u)))return{container:u,offset:s};if(c)return{container:c,offset:r=o?0:c.length}},Dc=function(e,t,n,r,o){var i,a,u,s;for(3===r.nodeType&&0===r.nodeValue.length&&r[o]&&(r=r[o]),i=Ec(e,r),a=0;a<i.length;a++)for(u=0;u<t.length;u++)if(!("collapsed"in(s=t[u])&&s.collapsed!==n.collapsed)&&e.is(i[a],s.selector))return i[a];return r},Oc=function(t,e,n,r){var o,i=t.dom,a=i.getRoot();if(e[0].wrapper||(o=i.getParent(n,e[0].block,a)),!o){var u=i.getParent(n,"LI,TD,TH");o=i.getParent(3===n.nodeType?n.parentNode:n,function(e){return e!==a&&Tc(t,e)},u)}if(o&&e[0].wrapper&&(o=Ec(i,o,"ul,ol").reverse()[0]||o),!o)for(o=n;o[r]&&!i.isBlock(o[r])&&(o=o[r],!wc.isEq(o,"br")););return o||n},Bc=function(e,t,n,r,o,i,a){var u,s,c,l,f,d;if(u=s=a?n:o,l=a?"previousSibling":"nextSibling",f=e.getRoot(),3===u.nodeType&&!Sc(u)&&(a?0<r:i<u.nodeValue.length))return u;for(;;){if(!t[0].block_expand&&e.isBlock(s))return s;for(c=s[l];c;c=c[l])if(!Nc(c)&&!Sc(c)&&("BR"!==(d=c).nodeName||!d.getAttribute("data-mce-bogus")||d.nextSibling))return s;if(s===f||s.parentNode===f){u=s;break}s=s.parentNode}return u},Pc=function(e,t,n,r){var o,i=t.startContainer,a=t.startOffset,u=t.endContainer,s=t.endOffset,c=e.dom;return 1===i.nodeType&&i.hasChildNodes()&&3===(i=eu(i,a)).nodeType&&(a=0),1===u.nodeType&&u.hasChildNodes()&&3===(u=eu(u,t.collapsed?s:s-1)).nodeType&&(s=u.nodeValue.length),i=_c(c,i),u=_c(c,u),(Nc(i.parentNode)||Nc(i))&&(i=Nc(i)?i:i.parentNode,3===(i=t.collapsed?i.previousSibling||i:i.nextSibling||i).nodeType&&(a=t.collapsed?i.length:0)),(Nc(u.parentNode)||Nc(u))&&(u=Nc(u)?u:u.parentNode,3===(u=t.collapsed?u.nextSibling||u:u.previousSibling||u).nodeType&&(s=t.collapsed?0:u.length)),t.collapsed&&((o=Rc(c,e.getBody(),i,a,!0,r))&&(i=o.container,a=o.offset),(o=Rc(c,e.getBody(),u,s,!1,r))&&(u=o.container,s=o.offset)),n[0].inline&&(u=r?u:function(e,t){var n=kc(e,t);if(n.node){for(;n.node&&0===n.offset&&n.node.previousSibling;)n=kc(n.node.previousSibling);n.node&&0<n.offset&&3===n.node.nodeType&&" "===n.node.nodeValue.charAt(n.offset-1)&&1<n.offset&&(e=n.node).splitText(n.offset-1)}return e}(u,s)),(n[0].inline||n[0].block_expand)&&(n[0].inline&&3===i.nodeType&&0!==a||(i=Bc(c,n,i,a,u,s,!0)),n[0].inline&&3===u.nodeType&&s!==u.nodeValue.length||(u=Bc(c,n,i,a,u,s,!1))),n[0].selector&&!1!==n[0].expand&&!n[0].inline&&(i=Dc(c,n,t,i,"previousSibling"),u=Dc(c,n,t,u,"nextSibling")),(n[0].block||n[0].selector)&&(i=Oc(e,n,i,"previousSibling"),u=Oc(e,n,u,"nextSibling"),n[0].block&&(c.isBlock(i)||(i=Bc(c,n,i,a,u,s,!0)),c.isBlock(u)||(u=Bc(c,n,i,a,u,s,!1)))),1===i.nodeType&&(a=c.nodeIndex(i),i=i.parentNode),1===u.nodeType&&(s=c.nodeIndex(u)+1,u=u.parentNode),{startContainer:i,startOffset:a,endContainer:u,endOffset:s}},Ic=Xt.each,Lc=function(e,t,o){var n,r,i,a,u,s,c,l=t.startContainer,f=t.startOffset,d=t.endContainer,m=t.endOffset;if(0<(c=e.select("td[data-mce-selected],th[data-mce-selected]")).length)Ic(c,function(e){o([e])});else{var g,p,h,v=function(e){var t;return 3===(t=e[0]).nodeType&&t===l&&f>=t.nodeValue.length&&e.splice(0,1),t=e[e.length-1],0===m&&0<e.length&&t===d&&3===t.nodeType&&e.splice(e.length-1,1),e},y=function(e,t,n){for(var r=[];e&&e!==n;e=e[t])r.push(e);return r},b=function(e,t){do{if(e.parentNode===t)return e;e=e.parentNode}while(e)},C=function(e,t,n){var r=n?"nextSibling":"previousSibling";for(u=(a=e).parentNode;a&&a!==t;a=u)u=a.parentNode,(s=y(a===e?a:a[r],r)).length&&(n||s.reverse(),o(v(s)))};if(1===l.nodeType&&l.hasChildNodes()&&(l=l.childNodes[f]),1===d.nodeType&&d.hasChildNodes()&&(p=m,h=(g=d).childNodes,--p>h.length-1?p=h.length-1:p<0&&(p=0),d=h[p]||g),l===d)return o(v([l]));for(n=e.findCommonAncestor(l,d),a=l;a;a=a.parentNode){if(a===d)return C(l,n,!0);if(a===n)break}for(a=d;a;a=a.parentNode){if(a===l)return C(d,n);if(a===n)break}r=b(l,n)||l,i=b(d,n)||d,C(l,r,!0),(s=y(r===l?r:r.nextSibling,"nextSibling",i===d?i.nextSibling:i)).length&&o(v(s)),C(d,i)}},Fc=(Ms=mr,zs="text",{get:function(e){if(!Ms(e))throw new Error("Can only get "+zs+" value of a "+zs+" node");return Us(e).getOr("")},getOption:Us=function(e){return Ms(e)?_.from(e.dom().nodeValue):_.none()},set:function(e,t){if(!Ms(e))throw new Error("Can only set raw "+zs+" value of a "+zs+" node");e.dom().nodeValue=t}}),Mc=function(e){return Fc.get(e)},zc=function(r,o,i,a){return Vr(o).fold(function(){return"skipping"},function(e){return"br"===a||mr(n=o)&&"\ufeff"===Mc(n)?"valid":dr(t=o)&&Gi(t,aa())?"existing":Ju(o)?"caret":wc.isValid(r,i,a)&&wc.isValid(r,lr(e),i)?"valid":"invalid-child";var t,n})},Uc=function(e,t,n,r){var o,i,a=t.uid,u=void 0===a?(o="mce-annotation",i=(new Date).getTime(),o+"_"+Math.floor(1e9*Math.random())+ ++ga+String(i)):a,s=function(e,t){var n={};for(var r in e)Object.prototype.hasOwnProperty.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&"function"==typeof Object.getOwnPropertySymbols){var o=0;for(r=Object.getOwnPropertySymbols(e);o<r.length;o++)t.indexOf(r[o])<0&&Object.prototype.propertyIsEnumerable.call(e,r[o])&&(n[r[o]]=e[r[o]])}return n}(t,["uid"]),c=ar.fromTag("span",e);Xi(c,aa()),wr(c,""+sa(),u),wr(c,""+ua(),n);var l,f=r(u,s),d=f.attributes,m=void 0===d?{}:d,g=f.classes,p=void 0===g?[]:g;return Nr(c,m),l=c,z(p,function(e){Xi(l,e)}),c},jc=function(i,e,t,n,r){var a=[],u=Uc(i.getDoc(),r,t,n),s=Hi(_.none()),c=function(){s.set(_.none())},l=function(e){z(e,o)},o=function(e){var t,n;switch(zc(i,e,"span",lr(e))){case"invalid-child":c();var r=Kr(e);l(r),c();break;case"valid":var o=s.get().getOrThunk(function(){var e=ha(u);return a.push(e),s.set(_.some(e)),e});Pi(t=e,n=o),Fi(n,t)}};return Lc(i.dom,e,function(e){var t;c(),t=W(e,ar.fromDom),l(t)}),a},Vc=function(s,c,l,f){s.undoManager.transact(function(){var e,t,n,r,o=s.selection.getRng();if(o.collapsed&&(r=Pc(e=s,t=o,[{inline:!0}],3===(n=t).startContainer.nodeType&&n.startContainer.nodeValue.length>=n.startOffset&&"\xa0"===n.startContainer.nodeValue[n.startOffset]),t.setStart(r.startContainer,r.startOffset),t.setEnd(r.endContainer,r.endOffset),e.selection.setRng(t)),s.selection.getRng().collapsed){var i=Uc(s.getDoc(),f,c,l.decorate);ya(i,"\xa0"),s.selection.getRng().insertNode(i.dom()),s.selection.select(i.dom())}else{var a=Yu.getPersistentBookmark(s.selection,!1),u=s.selection.getRng();jc(s,u,c,l.decorate,f),s.selection.moveToBookmark(a)}})};function Hc(s){var n,r=(n={},{register:function(e,t){n[e]={name:e,settings:t}},lookup:function(e){return n.hasOwnProperty(e)?_.from(n[e]).map(function(e){return e.settings}):_.none()}});da(s,r);var o=fa(s);return{register:function(e,t){r.register(e,t)},annotate:function(t,n){r.lookup(t).each(function(e){Vc(s,t,e,n)})},annotationChanged:function(e,t){o.addListener(e,t)},remove:function(e){ca(s,_.some(e)).each(function(e){var t=e.elements;z(t,ji)})},getAll:function(e){var t,n,r,o,i,a,u=(t=s,n=e,r=ar.fromDom(t.getBody()),o=Qi(r,"["+ua()+'="'+n+'"]'),i={},z(o,function(e){var t=Er(e,sa()),n=i.hasOwnProperty(t)?i[t]:[];i[t]=n.concat([e])}),i);return a=function(e){return W(e,function(e){return e.dom()})},vr(u,function(e,t){return{k:t,v:a(e,t)}})}}}var qc=function(e){return Xt.grep(e.childNodes,function(e){return"LI"===e.nodeName})},$c=function(e){return e&&e.firstChild&&e.firstChild===e.lastChild&&("\xa0"===(t=e.firstChild).data||jo.isBr(t));var t},Wc=function(e){return 0<e.length&&(!(t=e[e.length-1]).firstChild||$c(t))?e.slice(0,-1):e;var t},Kc=function(e,t){var n=e.getParent(t,e.isBlock);return n&&"LI"===n.nodeName?n:null},Xc=function(e,t){var n=_u.after(e),r=Js(t).prev(n);return r?r.toRange():null},Yc=function(t,e,n){var r,o,i,a,u=t.parentNode;return Xt.each(e,function(e){u.insertBefore(e,t)}),r=t,o=n,i=_u.before(r),(a=Js(o).next(i))?a.toRange():null},Gc=function(e,t){var n,r,o,i,a,u,s=t.firstChild,c=t.lastChild;return s&&"meta"===s.name&&(s=s.next),c&&"mce_marker"===c.attr("id")&&(c=c.prev),r=c,u=(n=e).getNonEmptyElements(),r&&(r.isEmpty(u)||(o=r,n.getBlockElements()[o.name]&&(a=o).firstChild&&a.firstChild===a.lastChild&&("br"===(i=o.firstChild).name||"\xa0"===i.value)))&&(c=c.prev),!(!s||s!==c||"ul"!==s.name&&"ol"!==s.name)},Jc=function(e,o,i,t){var n,r,a,u,s,c,l,f,d,m,g,p,h,v,y,b,C,x,w,N=(n=o,r=t,c=e.serialize(r),l=n.createFragment(c),u=(a=l).firstChild,s=a.lastChild,u&&"META"===u.nodeName&&u.parentNode.removeChild(u),s&&"mce_marker"===s.id&&s.parentNode.removeChild(s),a),E=Kc(o,i.startContainer),S=Wc(qc(N.firstChild)),T=o.getRoot(),k=function(e){var t=_u.fromRangeStart(i),n=Js(o.getRoot()),r=1===e?n.prev(t):n.next(t);return!r||Kc(o,r.getNode())!==E};return k(1)?Yc(E,S,T):k(2)?(f=E,d=S,m=T,o.insertAfter(d.reverse(),f),Xc(d[0],m)):(p=S,h=T,v=g=E,b=(y=i).cloneRange(),C=y.cloneRange(),b.setStartBefore(v),C.setEndAfter(v),x=[b.cloneContents(),C.cloneContents()],(w=g.parentNode).insertBefore(x[0],g),Xt.each(p,function(e){w.insertBefore(e,g)}),w.insertBefore(x[1],g),w.removeChild(g),Xc(p[p.length-1],h))},Qc=function(e,t){return!!Kc(e,t)},Zc=Xt.each,el=function(o){this.compare=function(e,t){if(e.nodeName!==t.nodeName)return!1;var n=function(n){var r={};return Zc(o.getAttribs(n),function(e){var t=e.nodeName.toLowerCase();0!==t.indexOf("_")&&"style"!==t&&0!==t.indexOf("data-")&&(r[t]=o.getAttrib(n,t))}),r},r=function(e,t){var n,r;for(r in e)if(e.hasOwnProperty(r)){if(void 0===(n=t[r]))return!1;if(e[r]!==n)return!1;delete t[r]}for(r in t)if(t.hasOwnProperty(r))return!1;return!0};return!(!r(n(e),n(t))||!r(o.parseStyle(o.getAttrib(e,"style")),o.parseStyle(o.getAttrib(t,"style")))||yc(e)||yc(t))}},tl=function(e){var t=Qi(e,"br"),n=U(function(e){for(var t=[],n=e.dom();n;)t.push(ar.fromDom(n)),n=n.lastChild;return t}(e).slice(-1),wo);t.length===n.length&&z(n,Ui)},nl=function(e){zi(e),Fi(e,ar.fromHtml('<br data-mce-bogus="1">'))},rl=function(n){Gr(n).each(function(t){Hr(t).each(function(e){Co(n)&&wo(t)&&Co(e)&&Ui(t)})})},ol=Xt.makeMap;function il(e){var u,s,c,l,f,d=[];return u=(e=e||{}).indent,s=ol(e.indent_before||""),c=ol(e.indent_after||""),l=ti.getEncodeFunc(e.entity_encoding||"raw",e.entities),f="html"===e.element_format,{start:function(e,t,n){var r,o,i,a;if(u&&s[e]&&0<d.length&&0<(a=d[d.length-1]).length&&"\n"!==a&&d.push("\n"),d.push("<",e),t)for(r=0,o=t.length;r<o;r++)i=t[r],d.push(" ",i.name,'="',l(i.value,!0),'"');d[d.length]=!n||f?">":" />",n&&u&&c[e]&&0<d.length&&0<(a=d[d.length-1]).length&&"\n"!==a&&d.push("\n")},end:function(e){var t;d.push("</",e,">"),u&&c[e]&&0<d.length&&0<(t=d[d.length-1]).length&&"\n"!==t&&d.push("\n")},text:function(e,t){0<e.length&&(d[d.length]=t?e:l(e))},cdata:function(e){d.push("<![CDATA[",e,"]]>")},comment:function(e){d.push("\x3c!--",e,"--\x3e")},pi:function(e,t){t?d.push("<?",e," ",l(t),"?>"):d.push("<?",e,"?>"),u&&d.push("\n")},doctype:function(e){d.push("<!DOCTYPE",e,">",u?"\n":"")},reset:function(){d.length=0},getContent:function(){return d.join("").replace(/\n$/,"")}}}function al(t,g){void 0===g&&(g=di());var p=il(t);return(t=t||{}).validate=!("validate"in t)||t.validate,{serialize:function(e){var f,d;d=t.validate,f={3:function(e){p.text(e.value,e.raw)},8:function(e){p.comment(e.value)},7:function(e){p.pi(e.name,e.value)},10:function(e){p.doctype(e.value)},4:function(e){p.cdata(e.value)},11:function(e){if(e=e.firstChild)for(;m(e),e=e.next;);}},p.reset();var m=function(e){var t,n,r,o,i,a,u,s,c,l=f[e.type];if(l)l(e);else{if(t=e.name,n=e.shortEnded,r=e.attributes,d&&r&&1<r.length&&((a=[]).map={},c=g.getElementRule(e.name))){for(u=0,s=c.attributesOrder.length;u<s;u++)(o=c.attributesOrder[u])in r.map&&(i=r.map[o],a.map[o]=i,a.push({name:o,value:i}));for(u=0,s=r.length;u<s;u++)(o=r[u].name)in a.map||(i=r.map[o],a.map[o]=i,a.push({name:o,value:i}));r=a}if(p.start(e.name,r,n),!n){if(e=e.firstChild)for(;m(e),e=e.next;);p.end(t)}}};return 1!==e.type||t.inner?f[11](e):m(e),p.getContent()}}}var ul,sl=function(a){var u=_u.fromRangeStart(a),s=_u.fromRangeEnd(a),c=a.commonAncestorContainer;return sc.fromPosition(!1,c,s).map(function(e){return!Ts(u,s,c)&&Ts(u,e,c)?(t=u.container(),n=u.offset(),r=e.container(),o=e.offset(),(i=V.document.createRange()).setStart(t,n),i.setEnd(r,o),i):a;var t,n,r,o,i}).getOr(a)},cl=function(e){return e.collapsed?e:sl(e)},ll=jo.matchNodeNames("td th"),fl=function(e,t){var n,r,o=e.selection.getRng(),i=o.startContainer,a=o.startOffset;o.collapsed&&(n=i,r=a,jo.isText(n)&&"\xa0"===n.nodeValue[r-1])&&jo.isText(i)&&(i.insertData(a-1," "),i.deleteData(a,1),o.setStart(i,a),o.setEnd(i,a),e.selection.setRng(o)),e.selection.setContent(t)},dl=function(e,t,n){var r,o,i,a,u,s,c,l,f,d,m,g=e.selection,p=e.dom;if(/^ | $/.test(t)&&(t=function(e,t){var n,r;n=e.startContainer,r=e.startOffset;var o=function(e){return n[e]&&3===n[e].nodeType};return 3===n.nodeType&&(0<r?t=t.replace(/^ /," "):o("previousSibling")||(t=t.replace(/^ /," ")),r<n.length?t=t.replace(/ (<br>|)$/," "):o("nextSibling")||(t=t.replace(/( | )(<br>|)$/," "))),t}(g.getRng(),t)),r=e.parser,m=n.merge,o=al({validate:e.settings.validate},e.schema),d='<span id="mce_marker" data-mce-type="bookmark">​</span>',s={content:t,format:"html",selection:!0,paste:n.paste},(s=e.fire("BeforeSetContent",s)).isDefaultPrevented())e.fire("SetContent",{content:s.content,format:"html",selection:!0,paste:n.paste});else{-1===(t=s.content).indexOf("{$caret}")&&(t+="{$caret}"),t=t.replace(/\{\$caret\}/,d);var h,v,y,b,C,x,w=(l=g.getRng()).startContainer||(l.parentElement?l.parentElement():null),N=e.getBody();w===N&&g.isCollapsed()&&p.isBlock(N.firstChild)&&(h=e,(v=N.firstChild)&&!h.schema.getShortEndedElements()[v.nodeName])&&p.isEmpty(N.firstChild)&&((l=p.createRng()).setStart(N.firstChild,0),l.setEnd(N.firstChild,0),g.setRng(l)),g.isCollapsed()||(e.selection.setRng(cl(e.selection.getRng())),e.getDoc().execCommand("Delete",!1,null),y=e.selection.getRng(),b=t,C=y.startContainer,x=y.startOffset,3===C.nodeType&&y.collapsed&&("\xa0"===C.data[x]?(C.deleteData(x,1),/[\u00a0| ]$/.test(b)||(b+=" ")):"\xa0"===C.data[x-1]&&(C.deleteData(x-1,1),/[\u00a0| ]$/.test(b)||(b=" "+b))),t=b);var E,S,T,k={context:(i=g.getNode()).nodeName.toLowerCase(),data:n.data,insert:!0};if(u=r.parse(t,k),!0===n.paste&&Gc(e.schema,u)&&Qc(p,i))return l=Jc(o,p,e.selection.getRng(),u),e.selection.setRng(l),void e.fire("SetContent",s);if(function(e){for(var t=e;t=t.walk();)1===t.type&&t.attr("data-mce-fragment","1")}(u),"mce_marker"===(f=u.lastChild).attr("id"))for(f=(c=f).prev;f;f=f.walk(!0))if(3===f.type||!p.isBlock(f.name)){e.schema.isValidChild(f.parent.name,"span")&&f.parent.insert(c,f,"br"===f.name);break}if(e._selectionOverrides.showBlockCaretContainer(i),k.invalid){for(fl(e,d),i=g.getNode(),a=e.getBody(),9===i.nodeType?i=f=a:f=i;f!==a;)f=(i=f).parentNode;t=i===a?a.innerHTML:p.getOuterHTML(i),t=o.serialize(r.parse(t.replace(/<span (id="mce_marker"|id=mce_marker).+?<\/span>/i,function(){return o.serialize(u)}))),i===a?p.setHTML(a,t):p.setOuterHTML(i,t)}else!function(e,t,n){if("all"===n.getAttribute("data-mce-bogus"))n.parentNode.insertBefore(e.dom.createFragment(t),n);else{var r=n.firstChild,o=n.lastChild;!r||r===o&&"BR"===r.nodeName?e.dom.setHTML(n,t):fl(e,t)}}(e,t=o.serialize(u),i);!function(e,t){var n=e.schema.getTextInlineElements(),r=e.dom;if(t){var o=e.getBody(),i=new el(r);Xt.each(r.select("*[data-mce-fragment]"),function(e){for(var t=e.parentNode;t&&t!==o;t=t.parentNode)n[e.nodeName.toLowerCase()]&&i.compare(t,e)&&r.remove(e,!0)})}}(e,m),function(n,e){var t,r,o,i,a,u=n.dom,s=n.selection;if(e){if(n.selection.scrollIntoView(e),t=function(e){for(var t=n.getBody();e&&e!==t;e=e.parentNode)if("false"===n.dom.getContentEditable(e))return e;return null}(e))return u.remove(e),s.select(t);var c=u.createRng();(i=e.previousSibling)&&3===i.nodeType?(c.setStart(i,i.nodeValue.length),fe.ie||(a=e.nextSibling)&&3===a.nodeType&&(i.appendData(a.data),a.parentNode.removeChild(a))):(c.setStartBefore(e),c.setEndBefore(e)),r=u.getParent(e,u.isBlock),u.remove(e),r&&u.isEmpty(r)&&(n.$(r).empty(),c.setStart(r,0),c.setEnd(r,0),ll(r)||r.getAttribute("data-mce-fragment")||!(o=function(e){var t=_u.fromRangeStart(e);if(t=Js(n.getBody()).next(t))return t.toRange()}(c))?u.add(r,u.create("br",{"data-mce-bogus":"1"})):(c=o,u.remove(r))),s.setRng(c)}}(e,p.get("mce_marker")),E=e.getBody(),Xt.each(E.getElementsByTagName("*"),function(e){e.removeAttribute("data-mce-fragment")}),S=e.dom,T=e.selection.getStart(),_.from(S.getParent(T,"td,th")).map(ar.fromDom).each(rl),e.fire("SetContent",s),e.addVisual()}},ml=function(e,t){var n,r,o="string"!=typeof(n=t)?(r=Xt.extend({paste:n.paste,data:{paste:n.paste}},n),{content:n.content,details:r}):{content:n,details:{}};dl(e,o.content,o.details)},gl=/[\u0591-\u07FF\uFB1D-\uFDFF\uFE70-\uFEFC]/,pl=function(e,t,n){var r=e.getParam(t,n);if(-1!==r.indexOf("=")){var o=e.getParam(t,"","hash");return o.hasOwnProperty(e.id)?o[e.id]:n}return r},hl=function(e){return e.getParam("iframe_attrs",{})},vl=function(e){return e.getParam("doctype","<!DOCTYPE html>")},yl=function(e){return e.getParam("document_base_url","")},bl=function(e){return pl(e,"body_id","tinymce")},Cl=function(e){return pl(e,"body_class","")},xl=function(e){return e.getParam("content_security_policy","")},wl=function(e){return e.getParam("br_in_pre",!0)},Nl=function(e){if(e.getParam("force_p_newlines",!1))return"p";var t=e.getParam("forced_root_block","p");return!1===t?"":t},El=function(e){return e.getParam("forced_root_block_attrs",{})},Sl=function(e){return e.getParam("br_newline_selector",".mce-toc h2,figcaption,caption")},Tl=function(e){return e.getParam("no_newline_selector","")},kl=function(e){return e.getParam("keep_styles",!0)},_l=function(e){return e.getParam("end_container_on_empty_block",!1)},Al=function(e){return Xt.explode(e.getParam("font_size_style_values",""))},Rl=function(e){return Xt.explode(e.getParam("font_size_classes",""))},Dl=function(e){return e.getParam("images_dataimg_filter",q(!0),"function")},Ol=function(e){return e.getParam("automatic_uploads",!0,"boolean")},Bl=function(e){return e.getParam("images_reuse_filename",!1,"boolean")},Pl=function(e){return e.getParam("images_replace_blob_uris",!0,"boolean")},Il=function(e){return e.getParam("images_upload_url","","string")},Ll=function(e){return e.getParam("images_upload_base_path","","string")},Fl=function(e){return e.getParam("images_upload_credentials",!1,"boolean")},Ml=function(e){return e.getParam("images_upload_handler",null,"function")},zl=function(e){return e.getParam("content_css_cors",!1,"boolean")},Ul=function(e){return e.getParam("inline_boundaries_selector","a[href],code,.mce-annotation","string")},jl=function(e,t){if(!t)return t;var n=t.container(),r=t.offset();return e?Ta(n)?jo.isText(n.nextSibling)?_u(n.nextSibling,0):_u.after(n):Aa(t)?_u(n,r+1):t:Ta(n)?jo.isText(n.previousSibling)?_u(n.previousSibling,n.previousSibling.data.length):_u.before(n):Ra(t)?_u(n,r-1):t},Vl={isInlineTarget:function(e,t){return Lr(ar.fromDom(t),Ul(e))},findRootInline:function(e,t,n){var r,o,i,a=(r=e,o=t,i=n,U(Si.DOM.getParents(i.container(),"*",o),r));return _.from(a[a.length-1])},isRtl:function(e){return"rtl"===Si.DOM.getStyle(e,"direction",!0)||(t=e.textContent,gl.test(t));var t},isAtZwsp:function(e){return Aa(e)||Ra(e)},normalizePosition:jl,normalizeForwards:d(jl,!0),normalizeBackwards:d(jl,!1),hasSameParentBlock:function(e,t,n){var r=Ss(t,e),o=Ss(n,e);return r&&r===o}},Hl=function(e,t){return zr(e,t)?na(t,function(e){return No(e)||So(e)},(n=e,function(e){return Mr(n,ar.fromDom(e.dom().parentNode))})):_.none();var n},ql=function(e){var t,n,r;e.dom.isEmpty(e.getBody())&&(e.setContent(""),n=(t=e).getBody(),r=n.firstChild&&t.dom.isBlock(n.firstChild)?n.firstChild:n,t.selection.setCursorLocation(r,0))},$l=function(i,a,u){return ru(sc.firstPositionIn(u),sc.lastPositionIn(u),function(e,t){var n=Vl.normalizePosition(!0,e),r=Vl.normalizePosition(!1,t),o=Vl.normalizePosition(!1,a);return i?sc.nextPosition(u,o).map(function(e){return e.isEqual(r)&&a.isEqual(n)}).getOr(!1):sc.prevPosition(u,o).map(function(e){return e.isEqual(n)&&a.isEqual(r)}).getOr(!1)}).getOr(!0)},Wl=function(e,t){var n,r,o,i=ar.fromDom(e),a=ar.fromDom(t);return n=a,r="pre,code",o=d(Mr,i),ra(n,r,o).isSome()},Kl=function(e,t){return Ha(t)&&!1===(r=e,o=t,jo.isText(o)&&/^[ \t\r\n]*$/.test(o.data)&&!1===Wl(r,o))||(n=t,jo.isElement(n)&&"A"===n.nodeName&&n.hasAttribute("name"))||Xl(t);var n,r,o},Xl=jo.hasAttribute("data-mce-bookmark"),Yl=jo.hasAttribute("data-mce-bogus"),Gl=jo.hasAttributeValue("data-mce-bogus","all"),Jl=function(e){return function(e){var t,n,r=0;if(Kl(e,e))return!1;if(!(n=e.firstChild))return!0;t=new go(n,e);do{if(Gl(n))n=t.next(!0);else if(Yl(n))n=t.next();else if(jo.isBr(n))r++,n=t.next();else{if(Kl(e,n))return!1;n=t.next()}}while(n);return r<=1}(e.dom())},Ql=Ar("block","position"),Zl=Ar("from","to"),ef=function(e,t){var n=ar.fromDom(e),r=ar.fromDom(t.container());return Hl(n,r).map(function(e){return Ql(e,t)})},tf=function(o,i,e){var t=ef(o,_u.fromRangeStart(e)),n=t.bind(function(e){return sc.fromPosition(i,o,e.position()).bind(function(e){return ef(o,e).map(function(e){return t=o,n=i,r=e,jo.isBr(r.position().getNode())&&!1===Jl(r.block())?sc.positionIn(!1,r.block().dom()).bind(function(e){return e.isEqual(r.position())?sc.fromPosition(n,t,e).bind(function(e){return ef(t,e)}):_.some(r)}).getOr(r):r;var t,n,r})})});return ru(t,n,Zl).filter(function(e){return!1===Mr((r=e).from().block(),r.to().block())&&Vr((n=e).from().block()).bind(function(t){return Vr(n.to().block()).filter(function(e){return Mr(t,e)})}).isSome()&&(t=e,!1===jo.isContentEditableFalse(t.from().block().dom())&&!1===jo.isContentEditableFalse(t.to().block().dom()));var t,n,r})},nf=function(e,t,n){return n.collapsed?tf(e,t,n):_.none()},rf=function(e,t,n){return zr(t,e)?function(e,t){for(var n=D(t)?t:b,r=e.dom(),o=[];null!==r.parentNode&&r.parentNode!==undefined;){var i=r.parentNode,a=ar.fromDom(i);if(o.push(a),!0===n(a))break;r=i}return o}(e,function(e){return n(e)||Mr(e,t)}).slice(0,-1):[]},of=function(e,t){return rf(e,t,q(!1))},af=of,uf=function(e,t){return[e].concat(of(e,t))},sf=function(e){var t,n=(t=Kr(e),Y(t,Co).fold(function(){return t},function(e){return t.slice(0,e)}));return z(n,Ui),n},cf=function(e,t){var n=uf(t,e);return X(n.reverse(),Jl).each(Ui)},lf=function(e,t,n,r){if(Jl(n))return nl(n),sc.firstPositionIn(n.dom());0===U($r(r),function(e){return!Jl(e)}).length&&Jl(t)&&Pi(r,ar.fromTag("br"));var o=sc.prevPosition(n.dom(),_u.before(r.dom()));return z(sf(t),function(e){Pi(r,e)}),cf(e,t),o},ff=function(e,t,n){if(Jl(n))return Ui(n),Jl(t)&&nl(t),sc.firstPositionIn(t.dom());var r=sc.lastPositionIn(n.dom());return z(sf(t),function(e){Fi(n,e)}),cf(e,t),r},df=function(e,t){return zr(t,e)?(n=uf(e,t),_.from(n[n.length-1])):_.none();var n},mf=function(e,t){sc.positionIn(e,t.dom()).map(function(e){return e.getNode()}).map(ar.fromDom).filter(wo).each(Ui)},gf=function(e,t,n){return mf(!0,t),mf(!1,n),df(t,n).fold(d(ff,e,t,n),d(lf,e,t,n))},pf=function(e,t,n,r){return t?gf(e,r,n):gf(e,n,r)},hf=function(t,n){var e,r=ar.fromDom(t.getBody());return(e=nf(r.dom(),n,t.selection.getRng()).bind(function(e){return pf(r,n,e.from().block(),e.to().block())})).each(function(e){t.selection.setRng(e.toRange())}),e.isSome()},vf=function(e,t){var n=ar.fromDom(t),r=d(Mr,e);return ta(n,_o,r).isSome()},yf=function(e,t){var n,r,o=sc.prevPosition(e.dom(),_u.fromRangeStart(t)).isNone(),i=sc.nextPosition(e.dom(),_u.fromRangeEnd(t)).isNone();return!(vf(n=e,(r=t).startContainer)||vf(n,r.endContainer))&&o&&i},bf=function(e){var n,r,o,t,i=ar.fromDom(e.getBody()),a=e.selection.getRng();return yf(i,a)?((t=e).setContent(""),t.selection.setCursorLocation(),!0):(n=i,r=e.selection,o=r.getRng(),ru(Hl(n,ar.fromDom(o.startContainer)),Hl(n,ar.fromDom(o.endContainer)),function(e,t){return!1===Mr(e,t)&&(o.deleteContents(),pf(n,!0,e,t).each(function(e){r.setRng(e.toRange())}),!0)}).getOr(!1))},Cf=function(e,t){return!e.selection.isCollapsed()&&bf(e)},xf=function(a){if(!k(a))throw new Error("cases must be an array");if(0===a.length)throw new Error("there must be at least one case");var u=[],n={};return z(a,function(e,r){var t=gr(e);if(1!==t.length)throw new Error("one and only one name per case");var o=t[0],i=e[o];if(n[o]!==undefined)throw new Error("duplicate key detected:"+o);if("cata"===o)throw new Error("cannot have a case named cata (sorry)");if(!k(i))throw new Error("case arguments must be an array");u.push(o),n[o]=function(){var e=arguments.length;if(e!==i.length)throw new Error("Wrong number of arguments to case "+o+". Expected "+i.length+" ("+i+"), got "+e);for(var n=new Array(e),t=0;t<n.length;t++)n[t]=arguments[t];return{fold:function(){if(arguments.length!==a.length)throw new Error("Wrong number of arguments to fold. Expected "+a.length+", got "+arguments.length);return arguments[r].apply(null,n)},match:function(e){var t=gr(e);if(u.length!==t.length)throw new Error("Wrong number of arguments to match. Expected: "+u.join(",")+"\nActual: "+t.join(","));if(!J(u,function(e){return F(t,e)}))throw new Error("Not all branches were specified when using match. Specified: "+t.join(", ")+"\nRequired: "+u.join(", "));return e[o].apply(null,n)},log:function(e){V.console.log(e,{constructors:u,constructor:o,params:n})}}}}),n},wf=function(e){return Is(e).exists(wo)},Nf=function(e,t,n){var r=U(uf(ar.fromDom(n.container()),t),Co),o=Z(r).getOr(t);return sc.fromPosition(e,o.dom(),n).filter(wf)},Ef=function(e,t){return Is(t).exists(wo)||Nf(!0,e,t).isSome()},Sf=function(e,t){return(n=t,_.from(n.getNode(!0)).map(ar.fromDom)).exists(wo)||Nf(!1,e,t).isSome();var n},Tf=d(Nf,!1),kf=d(Nf,!0),_f=(ul="\xa0",function(e){return ul===e}),Af=function(e){return/^[\r\n\t ]$/.test(e)},Rf=function(e){return!Af(e)&&!_f(e)},Df=function(n,r,o){return _.from(o.container()).filter(jo.isText).exists(function(e){var t=n?0:-1;return r(e.data.charAt(o.offset()+t))})},Of=d(Df,!0,Af),Bf=d(Df,!1,Af),Pf=function(e){var t=e.container();return jo.isText(t)&&0===t.data.length},If=function(e,t){var n=ks(e,t);return jo.isContentEditableFalse(n)&&!jo.isBogusAll(n)},Lf=d(If,0),Ff=d(If,-1),Mf=function(e,t){return jo.isTable(ks(e,t))},zf=d(Mf,0),Uf=d(Mf,-1),jf=xf([{remove:["element"]},{moveToElement:["element"]},{moveToPosition:["position"]}]),Vf=function(e,t,n,r){var o=r.getNode(!1===t);return Hl(ar.fromDom(e),ar.fromDom(n.getNode())).map(function(e){return Jl(e)?jf.remove(e.dom()):jf.moveToElement(o)}).orThunk(function(){return _.some(jf.moveToElement(o))})},Hf=function(u,s,c){return sc.fromPosition(s,u,c).bind(function(e){return a=e.getNode(),_o(ar.fromDom(a))||So(ar.fromDom(a))?_.none():(t=u,o=e,i=function(e){return xo(ar.fromDom(e))&&!Ts(r,o,t)},Bs(!(n=s),r=c).fold(function(){return Bs(n,o).fold(q(!1),i)},i)?_.none():s&&jo.isContentEditableFalse(e.getNode())?Vf(u,s,c,e):!1===s&&jo.isContentEditableFalse(e.getNode(!0))?Vf(u,s,c,e):s&&Ff(c)?_.some(jf.moveToPosition(e)):!1===s&&Lf(c)?_.some(jf.moveToPosition(e)):_.none());var t,n,r,o,i,a})},qf=function(r,e,o){return i=e,a=o.getNode(!1===i),u=i?"after":"before",jo.isElement(a)&&a.getAttribute("data-mce-caret")===u?(t=e,n=o.getNode(!1===e),t&&jo.isContentEditableFalse(n.nextSibling)?_.some(jf.moveToElement(n.nextSibling)):!1===t&&jo.isContentEditableFalse(n.previousSibling)?_.some(jf.moveToElement(n.previousSibling)):_.none()).fold(function(){return Hf(r,e,o)},_.some):Hf(r,e,o).bind(function(e){return t=r,n=o,e.fold(function(e){return _.some(jf.remove(e))},function(e){return _.some(jf.moveToElement(e))},function(e){return Ts(n,e,t)?_.none():_.some(jf.moveToPosition(e))});var t,n});var t,n,i,a,u},$f=function(e,t,n){if(0!==n){var r,o,i,a=e.data.slice(t,t+n),u=t+n>=e.data.length,s=0===t;e.replaceData(t,n,(o=s,i=u,j((r=a).split(""),function(e,t){return-1!==" \f\n\r\t\x0B".indexOf(t)||"\xa0"===t?e.previousCharIsSpace||""===e.str&&o||e.str.length===r.length-1&&i?{previousCharIsSpace:!1,str:e.str+"\xa0"}:{previousCharIsSpace:!0,str:e.str+" "}:{previousCharIsSpace:!1,str:e.str+t}},{previousCharIsSpace:!1,str:""}).str))}},Wf=function(e,t){var n,r=e.data.slice(t),o=r.length-(n=r,n.replace(/^\s+/g,"")).length;return $f(e,t,o)},Kf=function(e,t){return r=e,o=(n=t).container(),i=n.offset(),!1===_u.isTextPosition(n)&&o===r.parentNode&&i>_u.before(r).offset()?_u(t.container(),t.offset()-1):t;var n,r,o,i},Xf=function(e){return Ha(e.previousSibling)?_.some((t=e.previousSibling,jo.isText(t)?_u(t,t.data.length):_u.after(t))):e.previousSibling?sc.lastPositionIn(e.previousSibling):_.none();var t},Yf=function(e){return Ha(e.nextSibling)?_.some((t=e.nextSibling,jo.isText(t)?_u(t,0):_u.before(t))):e.nextSibling?sc.firstPositionIn(e.nextSibling):_.none();var t},Gf=function(r,o){return Xf(o).orThunk(function(){return Yf(o)}).orThunk(function(){return e=r,t=o,n=_u.before(t.previousSibling?t.previousSibling:t.parentNode),sc.prevPosition(e,n).fold(function(){return sc.nextPosition(e,_u.after(t))},_.some);var e,t,n})},Jf=function(n,r){return Yf(r).orThunk(function(){return Xf(r)}).orThunk(function(){return e=n,t=r,sc.nextPosition(e,_u.after(t)).fold(function(){return sc.prevPosition(e,_u.before(t))},_.some);var e,t})},Qf=function(e,t,n){return(r=e,o=t,i=n,r?Jf(o,i):Gf(o,i)).map(d(Kf,n));var r,o,i},Zf=function(t,n,e){e.fold(function(){t.focus()},function(e){t.selection.setRng(e.toRange(),n)})},ed=function(e,t){return t&&e.schema.getBlockElements().hasOwnProperty(lr(t))},td=function(e){if(Jl(e)){var t=ar.fromHtml('<br data-mce-bogus="1">');return zi(e),Fi(e,t),_.some(_u.before(t.dom()))}return _.none()},nd=function(e,t,l){var n,r,o,i,a=Hr(e).filter(mr),u=qr(e).filter(mr);return Ui(e),(n=a,r=u,o=t,i=function(e,t,n){var r,o,i,a,u=e.dom(),s=t.dom(),c=u.data.length;return o=s,i=l,a=Jn((r=u).data).length,r.appendData(o.data),Ui(ar.fromDom(o)),i&&Wf(r,a),n.container()===s?_u(u,c):n},n.isSome()&&r.isSome()&&o.isSome()?_.some(i(n.getOrDie(),r.getOrDie(),o.getOrDie())):_.none()).orThunk(function(){return l&&(a.each(function(e){return t=e.dom(),n=e.dom().length,r=t.data.slice(0,n),o=r.length-Jn(r).length,$f(t,n-o,o);var t,n,r,o}),u.each(function(e){return Wf(e.dom(),0)})),t})},rd=function(t,n,e,r){void 0===r&&(r=!0);var o,i,a=Qf(n,t.getBody(),e.dom()),u=ta(e,d(ed,t),(o=t.getBody(),function(e){return e.dom()===o})),s=nd(e,a,(i=e,br(t.schema.getTextInlineElements(),lr(i))));t.dom.isEmpty(t.getBody())?(t.setContent(""),t.selection.setCursorLocation()):u.bind(td).fold(function(){r&&Zf(t,n,s)},function(e){r&&Zf(t,n,_.some(e))})},od=function(a,u){var e,t,n,r,o,i;return(e=a.getBody(),t=u,n=a.selection.getRng(),r=Os(t?1:-1,e,n),o=_u.fromRangeStart(r),i=ar.fromDom(e),!1===t&&Ff(o)?_.some(jf.remove(o.getNode(!0))):t&&Lf(o)?_.some(jf.remove(o.getNode())):!1===t&&Lf(o)&&Sf(i,o)?Tf(i,o).map(function(e){return jf.remove(e.getNode())}):t&&Ff(o)&&Ef(i,o)?kf(i,o).map(function(e){return jf.remove(e.getNode())}):qf(e,t,o)).map(function(e){return e.fold((o=a,i=u,function(e){return o._selectionOverrides.hideFakeCaret(),rd(o,i,ar.fromDom(e)),!0}),(n=a,r=u,function(e){var t=r?_u.before(e):_u.after(e);return n.selection.setRng(t.toRange()),!0}),(t=a,function(e){return t.selection.setRng(e.toRange()),!0}));var t,n,r,o,i}).getOr(!1)},id=function(e,t){var n,r=e.selection.getNode();return!!jo.isContentEditableFalse(r)&&(n=ar.fromDom(e.getBody()),z(Qi(n,".mce-offscreen-selection"),Ui),rd(e,t,ar.fromDom(e.selection.getNode())),ql(e),!0)},ad=function(e,t){return e.selection.isCollapsed()?od(e,t):id(e,t)},ud=function(e){var t,n=function(e,t){for(;t&&t!==e;){if(jo.isContentEditableTrue(t)||jo.isContentEditableFalse(t))return t;t=t.parentNode}return null}(e.getBody(),e.selection.getNode());return jo.isContentEditableTrue(n)&&e.dom.isBlock(n)&&e.dom.isEmpty(n)&&(t=e.dom.create("br",{"data-mce-bogus":"1"}),e.dom.setHTML(n,""),n.appendChild(t),e.selection.setRng(_u.before(t).toRange())),!0},sd=jo.isText,cd=function(e){return sd(e)&&e.data[0]===xa},ld=function(e){return sd(e)&&e.data[e.data.length-1]===xa},fd=function(e){return e.ownerDocument.createTextNode(xa)},dd=function(e,t){return e?function(e){if(sd(e.previousSibling))return ld(e.previousSibling)||e.previousSibling.appendData(xa),e.previousSibling;if(sd(e))return cd(e)||e.insertData(0,xa),e;var t=fd(e);return e.parentNode.insertBefore(t,e),t}(t):function(e){if(sd(e.nextSibling))return cd(e.nextSibling)||e.nextSibling.insertData(0,xa),e.nextSibling;if(sd(e))return ld(e)||e.appendData(xa),e;var t=fd(e);return e.nextSibling?e.parentNode.insertBefore(t,e.nextSibling):e.parentNode.appendChild(t),t}(t)},md=d(dd,!0),gd=d(dd,!1),pd=function(e,t){return jo.isText(e.container())?dd(t,e.container()):dd(t,e.getNode())},hd=function(e,t){var n=t.get();return n&&e.container()===n&&Ta(n)},vd=function(n,e){return e.fold(function(e){ss.remove(n.get());var t=md(e);return n.set(t),_.some(_u(t,t.length-1))},function(e){return sc.firstPositionIn(e).map(function(e){if(hd(e,n))return _u(n.get(),1);ss.remove(n.get());var t=pd(e,!0);return n.set(t),_u(t,1)})},function(e){return sc.lastPositionIn(e).map(function(e){if(hd(e,n))return _u(n.get(),n.get().length-1);ss.remove(n.get());var t=pd(e,!1);return n.set(t),_u(t,t.length-1)})},function(e){ss.remove(n.get());var t=gd(e);return n.set(t),_.some(_u(t,1))})},yd=function(e,t){for(var n=0;n<e.length;n++){var r=e[n].apply(null,t);if(r.isSome())return r}return _.none()},bd=xf([{before:["element"]},{start:["element"]},{end:["element"]},{after:["element"]}]),Cd=function(e,t){var n=Ss(t,e);return n||e},xd=function(e,t,n){var r=Vl.normalizeForwards(n),o=Cd(t,r.container());return Vl.findRootInline(e,o,r).fold(function(){return sc.nextPosition(o,r).bind(d(Vl.findRootInline,e,o)).map(function(e){return bd.before(e)})},_.none)},wd=function(e,t){return null===Qu(e,t)},Nd=function(e,t,n){return Vl.findRootInline(e,t,n).filter(d(wd,t))},Ed=function(e,t,n){var r=Vl.normalizeBackwards(n);return Nd(e,t,r).bind(function(e){return sc.prevPosition(e,r).isNone()?_.some(bd.start(e)):_.none()})},Sd=function(e,t,n){var r=Vl.normalizeForwards(n);return Nd(e,t,r).bind(function(e){return sc.nextPosition(e,r).isNone()?_.some(bd.end(e)):_.none()})},Td=function(e,t,n){var r=Vl.normalizeBackwards(n),o=Cd(t,r.container());return Vl.findRootInline(e,o,r).fold(function(){return sc.prevPosition(o,r).bind(d(Vl.findRootInline,e,o)).map(function(e){return bd.after(e)})},_.none)},kd=function(e){return!1===Vl.isRtl(Ad(e))},_d=function(e,t,n){return yd([xd,Ed,Sd,Td],[e,t,n]).filter(kd)},Ad=function(e){return e.fold($,$,$,$)},Rd=function(e){return e.fold(q("before"),q("start"),q("end"),q("after"))},Dd=function(e){return e.fold(bd.before,bd.before,bd.after,bd.after)},Od=function(n,e,r,t,o,i){return ru(Vl.findRootInline(e,r,t),Vl.findRootInline(e,r,o),function(e,t){return e!==t&&Vl.hasSameParentBlock(r,e,t)?bd.after(n?e:t):i}).getOr(i)},Bd=function(e,r){return e.fold(q(!0),function(e){return n=r,!(Rd(t=e)===Rd(n)&&Ad(t)===Ad(n));var t,n})},Pd=function(e,t){return e?t.fold(H(_.some,bd.start),_.none,H(_.some,bd.after),_.none):t.fold(_.none,H(_.some,bd.before),_.none,H(_.some,bd.end))},Id=function(a,u,s,c){var e=Vl.normalizePosition(a,c),l=_d(u,s,e);return _d(u,s,e).bind(d(Pd,a)).orThunk(function(){return t=a,n=u,r=s,o=l,e=c,i=Vl.normalizePosition(t,e),sc.fromPosition(t,r,i).map(d(Vl.normalizePosition,t)).fold(function(){return o.map(Dd)},function(e){return _d(n,r,e).map(d(Od,t,n,r,i,e)).filter(d(Bd,o))}).filter(kd);var t,n,r,o,e,i})},Ld=_d,Fd=Id,Md=(d(Id,!1),d(Id,!0),Dd),zd=function(e){return e.fold(bd.start,bd.start,bd.end,bd.end)},Ud=function(e){return D(e.selection.getSel().modify)},jd=function(e,t,n){var r=e?1:-1;return t.setRng(_u(n.container(),n.offset()+r).toRange()),t.getSel().modify("move",e?"forward":"backward","word"),!0},Vd=function(e,t){var n=t.selection.getRng(),r=e?_u.fromRangeEnd(n):_u.fromRangeStart(n);return!!Ud(t)&&(e&&Aa(r)?jd(!0,t.selection,r):!(e||!Ra(r))&&jd(!1,t.selection,r))},Hd=function(e,t){var n=e.dom.createRng();n.setStart(t.container(),t.offset()),n.setEnd(t.container(),t.offset()),e.selection.setRng(n)},qd=function(e){return!1!==e.settings.inline_boundaries},$d=function(e,t){e?t.setAttribute("data-mce-selected","inline-boundary"):t.removeAttribute("data-mce-selected")},Wd=function(t,e,n){return vd(e,n).map(function(e){return Hd(t,e),n})},Kd=function(e,t,n){return function(){return!!qd(t)&&Vd(e,t)}},Xd={move:function(a,u,s){return function(){return!!qd(a)&&(t=a,n=u,e=s,r=t.getBody(),o=_u.fromRangeStart(t.selection.getRng()),i=d(Vl.isInlineTarget,t),Fd(e,i,r,o).bind(function(e){return Wd(t,n,e)})).isSome();var t,n,e,r,o,i}},moveNextWord:d(Kd,!0),movePrevWord:d(Kd,!1),setupSelectedState:function(a){var u=Hi(null),s=d(Vl.isInlineTarget,a);return a.on("NodeChange",function(e){var t,n,r,o,i;qd(a)&&(t=s,n=a.dom,r=e.parents,o=U(n.select('*[data-mce-selected="inline-boundary"]'),t),i=U(r,t),z(Q(o,i),d($d,!1)),z(Q(i,o),d($d,!0)),function(e,t){if(e.selection.isCollapsed()&&!0!==e.composing&&t.get()){var n=_u.fromRangeStart(e.selection.getRng());_u.isTextPosition(n)&&!1===Vl.isAtZwsp(n)&&(Hd(e,ss.removeAndReposition(t.get(),n)),t.set(null))}}(a,u),function(n,r,o,e){if(r.selection.isCollapsed()){var t=U(e,n);z(t,function(e){var t=_u.fromRangeStart(r.selection.getRng());Ld(n,r.getBody(),t).bind(function(e){return Wd(r,o,e)})})}}(s,a,u,e.parents))}),u},setCaretPosition:Hd},Yd=function(t,n){return function(e){return vd(n,e).map(function(e){return Xd.setCaretPosition(t,e),!0}).getOr(!1)}},Gd=function(r,o,i,a){var u=r.getBody(),s=d(Vl.isInlineTarget,r);r.undoManager.ignore(function(){var e,t,n;r.selection.setRng((e=i,t=a,(n=V.document.createRange()).setStart(e.container(),e.offset()),n.setEnd(t.container(),t.offset()),n)),r.execCommand("Delete"),Ld(s,u,_u.fromRangeStart(r.selection.getRng())).map(zd).map(Yd(r,o))}),r.nodeChanged()},Jd=function(n,r,i,o){var e,t,a=(e=n.getBody(),t=o.container(),Ss(t,e)||e),u=d(Vl.isInlineTarget,n),s=Ld(u,a,o);return s.bind(function(e){return i?e.fold(q(_.some(zd(e))),_.none,q(_.some(Md(e))),_.none):e.fold(_.none,q(_.some(Md(e))),_.none,q(_.some(zd(e))))}).map(Yd(n,r)).getOrThunk(function(){var t=sc.navigate(i,a,o),e=t.bind(function(e){return Ld(u,a,e)});return s.isSome()&&e.isSome()?Vl.findRootInline(u,a,o).map(function(e){return o=e,!!ru(sc.firstPositionIn(o),sc.lastPositionIn(o),function(e,t){var n=Vl.normalizePosition(!0,e),r=Vl.normalizePosition(!1,t);return sc.nextPosition(o,n).map(function(e){return e.isEqual(r)}).getOr(!0)}).getOr(!0)&&(rd(n,i,ar.fromDom(e)),!0);var o}).getOr(!1):e.bind(function(e){return t.map(function(e){return i?Gd(n,r,o,e):Gd(n,r,e,o),!0})}).getOr(!1)})},Qd=function(e,t,n){if(e.selection.isCollapsed()&&!1!==e.settings.inline_boundaries){var r=_u.fromRangeStart(e.selection.getRng());return Jd(e,t,n,r)}return!1},Zd=Ar("start","end"),em=Ar("rng","table","cells"),tm=xf([{removeTable:["element"]},{emptyCells:["cells"]}]),nm=function(e,t){return ia(ar.fromDom(e),"td,th",t)},rm=function(e,t){return ra(e,"table",t)},om=function(e){return!1===Mr(e.start(),e.end())},im=function(e,n){return rm(e.start(),n).bind(function(t){return rm(e.end(),n).bind(function(e){return Mr(t,e)?_.some(t):_.none()})})},am=function(e){return Qi(e,"td,th")},um=function(r,e){var t=nm(e.startContainer,r),n=nm(e.endContainer,r);return e.collapsed?_.none():ru(t,n,Zd).fold(function(){return t.fold(function(){return n.bind(function(t){return rm(t,r).bind(function(e){return Z(am(e)).map(function(e){return Zd(e,t)})})})},function(t){return rm(t,r).bind(function(e){return ee(am(e)).map(function(e){return Zd(t,e)})})})},function(e){return sm(r,e)?_.none():(n=r,rm((t=e).start(),n).bind(function(e){return ee(am(e)).map(function(e){return Zd(t.start(),e)})}));var t,n})},sm=function(e,t){return im(t,e).isSome()},cm=function(e,t){var n,r,o,i,a=d(Mr,e);return(n=t,r=a,o=nm(n.startContainer,r),i=nm(n.endContainer,r),ru(o,i,Zd).filter(om).filter(function(e){return sm(r,e)}).orThunk(function(){return um(r,n)})).bind(function(e){return im(t=e,a).map(function(e){return em(t,e,am(e))});var t})},lm=function(e,t){return Y(e,function(e){return Mr(e,t)})},fm=function(n){return(r=n,ru(lm(r.cells(),r.rng().start()),lm(r.cells(),r.rng().end()),function(e,t){return r.cells().slice(e,t+1)})).map(function(e){var t=n.cells();return e.length===t.length?tm.removeTable(n.table()):tm.emptyCells(e)});var r},dm=function(e,t){return cm(e,t).bind(fm)},mm=function(e){var t=[];if(e)for(var n=0;n<e.rangeCount;n++)t.push(e.getRangeAt(n));return t},gm=mm,pm=function(e){return G(e,function(e){var t=Za(e);return t?[ar.fromDom(t)]:[]})},hm=function(e){return 1<mm(e).length},vm=function(e){return U(pm(e),_o)},ym=function(e){return Qi(e,"td[data-mce-selected],th[data-mce-selected]")},bm=function(e,t){var n=ym(t),r=vm(e);return 0<n.length?n:r},Cm=bm,xm=function(e){return bm(gm(e.selection.getSel()),ar.fromDom(e.getBody()))},wm=function(e,t){return z(t,nl),e.selection.setCursorLocation(t[0].dom(),0),!0},Nm=function(e,t){return rd(e,!1,t),!0},Em=function(n,e,r,t){return Tm(e,t).fold(function(){return t=n,dm(e,r).map(function(e){return e.fold(d(Nm,t),d(wm,t))});var t},function(e){return km(n,e)}).getOr(!1)},Sm=function(e,t){return X(uf(t,e),_o)},Tm=function(e,t){return X(uf(t,e),function(e){return"caption"===lr(e)})},km=function(e,t){return nl(t),e.selection.setCursorLocation(t.dom(),0),_.some(!0)},_m=function(u,s,c,l,f){return sc.navigate(c,u.getBody(),f).bind(function(e){return r=l,o=c,i=f,a=e,sc.firstPositionIn(r.dom()).bind(function(t){return sc.lastPositionIn(r.dom()).map(function(e){return o?i.isEqual(t)&&a.isEqual(e):i.isEqual(e)&&a.isEqual(t)})}).getOr(!0)?km(u,l):(t=l,n=e,Tm(s,ar.fromDom(n.getNode())).map(function(e){return!1===Mr(e,t)}));var t,n,r,o,i,a}).or(_.some(!0))},Am=function(a,u,s,e){var c=_u.fromRangeStart(a.selection.getRng());return Sm(s,e).bind(function(e){return Jl(e)?km(a,e):(t=a,n=s,r=u,o=e,i=c,sc.navigate(r,t.getBody(),i).bind(function(e){return Sm(n,ar.fromDom(e.getNode())).map(function(e){return!1===Mr(e,o)})}));var t,n,r,o,i})},Rm=function(a,u,e){var s=ar.fromDom(a.getBody());return Tm(s,e).fold(function(){return Am(a,u,s,e)},function(e){return t=a,n=u,r=s,o=e,i=_u.fromRangeStart(t.selection.getRng()),Jl(o)?km(t,o):_m(t,r,n,o,i);var t,n,r,o,i}).getOr(!1)},Dm=function(e,t){var n,r,o,i,a,u=ar.fromDom(e.selection.getStart(!0)),s=xm(e);return e.selection.isCollapsed()&&0===s.length?Rm(e,t,u):(n=e,r=u,o=ar.fromDom(n.getBody()),i=n.selection.getRng(),0!==(a=xm(n)).length?wm(n,a):Em(n,o,i,r))},Om=wc.isEq,Bm=function(e,t,n){var r=e.formatter.get(n);if(r)for(var o=0;o<r.length;o++)if(!1===r[o].inherit&&e.dom.is(t,r[o].selector))return!0;return!1},Pm=function(t,e,n,r){var o=t.dom.getRoot();return e!==o&&(e=t.dom.getParent(e,function(e){return!!Bm(t,e,n)||e.parentNode===o||!!Fm(t,e,n,r,!0)}),Fm(t,e,n,r))},Im=function(e,t,n){return!!Om(t,n.inline)||!!Om(t,n.block)||(n.selector?1===t.nodeType&&e.is(t,n.selector):void 0)},Lm=function(e,t,n,r,o,i){var a,u,s,c=n[r];if(n.onmatch)return n.onmatch(t,n,r);if(c)if("undefined"==typeof c.length){for(a in c)if(c.hasOwnProperty(a)){if(u="attributes"===r?e.getAttrib(t,a):wc.getStyle(e,t,a),o&&!u&&!n.exact)return;if((!o||n.exact)&&!Om(u,wc.normalizeStyleValue(e,wc.replaceVars(c[a],i),a)))return}}else for(s=0;s<c.length;s++)if("attributes"===r?e.getAttrib(t,c[s]):wc.getStyle(e,t,c[s]))return n;return n},Fm=function(e,t,n,r,o){var i,a,u,s,c=e.formatter.get(n),l=e.dom;if(c&&t)for(a=0;a<c.length;a++)if(i=c[a],Im(e.dom,t,i)&&Lm(l,t,i,"attributes",o,r)&&Lm(l,t,i,"styles",o,r)){if(s=i.classes)for(u=0;u<s.length;u++)if(!e.dom.hasClass(t,s[u]))return;return i}},Mm={matchNode:Fm,matchName:Im,match:function(e,t,n,r){var o;return r?Pm(e,r,t,n):(r=e.selection.getNode(),!!Pm(e,r,t,n)||!((o=e.selection.getStart())===r||!Pm(e,o,t,n)))},matchAll:function(r,o,i){var e,a=[],u={};return e=r.selection.getStart(),r.dom.getParent(e,function(e){var t,n;for(t=0;t<o.length;t++)n=o[t],!u[n]&&Fm(r,e,n,i)&&(u[n]=!0,a.push(n))},r.dom.getRoot()),a},canApply:function(e,t){var n,r,o,i,a,u=e.formatter.get(t),s=e.dom;if(u)for(n=e.selection.getStart(),r=wc.getParents(s,n),i=u.length-1;0<=i;i--){if(!(a=u[i].selector)||u[i].defaultBlock)return!0;for(o=r.length-1;0<=o;o--)if(s.is(r[o],a))return!0}return!1},matchesUnInheritedFormatSelector:Bm},zm=function(e,t){return e.splitText(t)},Um=function(e){var t=e.startContainer,n=e.startOffset,r=e.endContainer,o=e.endOffset;return t===r&&jo.isText(t)?0<n&&n<t.nodeValue.length&&(t=(r=zm(t,n)).previousSibling,n<o?(t=r=zm(r,o-=n).previousSibling,o=r.nodeValue.length,n=0):o=0):(jo.isText(t)&&0<n&&n<t.nodeValue.length&&(t=zm(t,n),n=0),jo.isText(r)&&0<o&&o<r.nodeValue.length&&(o=(r=zm(r,o).previousSibling).nodeValue.length)),{startContainer:t,startOffset:n,endContainer:r,endOffset:o}},jm=xa,Vm="_mce_caret",Hm=function(e){return 0<function(e){for(var t=[];e;){if(3===e.nodeType&&e.nodeValue!==jm||1<e.childNodes.length)return[];1===e.nodeType&&t.push(e),e=e.firstChild}return t}(e).length},qm=function(e){var t;if(e)for(e=(t=new go(e,e)).current();e;e=t.next())if(3===e.nodeType)return e;return null},$m=function(e){var t=ar.fromTag("span");return Nr(t,{id:Vm,"data-mce-bogus":"1","data-mce-type":"format-caret"}),e&&Fi(t,ar.fromText(jm)),t},Wm=function(e,t,n){void 0===n&&(n=!0);var r,o=e.dom,i=e.selection;if(Hm(t))rd(e,!1,ar.fromDom(t),n);else{var a=i.getRng(),u=o.getParent(t,o.isBlock),s=((r=qm(t))&&r.nodeValue.charAt(0)===jm&&r.deleteData(0,1),r);a.startContainer===s&&0<a.startOffset&&a.setStart(s,a.startOffset-1),a.endContainer===s&&0<a.endOffset&&a.setEnd(s,a.endOffset-1),o.remove(t,!0),u&&o.isEmpty(u)&&nl(ar.fromDom(u)),i.setRng(a)}},Km=function(e,t,n){void 0===n&&(n=!0);var r=e.dom,o=e.selection;if(t)Wm(e,t,n);else if(!(t=Qu(e.getBody(),o.getStart())))for(;t=r.get(Vm);)Wm(e,t,!1)},Xm=function(e,t,n){var r=e.dom,o=r.getParent(n,d(wc.isTextBlock,e));o&&r.isEmpty(o)?n.parentNode.replaceChild(t,n):(tl(ar.fromDom(n)),r.isEmpty(n)?n.parentNode.replaceChild(t,n):r.insertAfter(t,n))},Ym=function(e,t){return e.appendChild(t),t},Gm=function(e,t){var n,r,o=(n=function(e,t){return Ym(e,t.cloneNode(!1))},r=t,function(e,t){for(var n=e.length-1;0<=n;n--)t(e[n],n)}(e,function(e){r=n(r,e)}),r);return Ym(o,o.ownerDocument.createTextNode(jm))},Jm=function(i){i.on("mouseup keydown",function(e){var t,n,r,o;t=i,n=e.keyCode,r=t.selection,o=t.getBody(),Km(t,null,!1),8!==n&&46!==n||!r.isCollapsed()||r.getStart().innerHTML!==jm||Km(t,Qu(o,r.getStart())),37!==n&&39!==n||Km(t,Qu(o,r.getStart()))})},Qm=function(e,t){return e.schema.getTextInlineElements().hasOwnProperty(lr(t))&&!Ju(t.dom())&&!jo.isBogus(t.dom())},Zm=function(e){return 1===Kr(e).length},eg=function(e,t,n,r){var o,i,a,u,s=d(Qm,t),c=W(U(r,s),function(e){return e.dom()});if(0===c.length)rd(t,e,n);else{var l=(o=n.dom(),i=c,a=$m(!1),u=Gm(i,a.dom()),Pi(ar.fromDom(o),a),Ui(ar.fromDom(o)),_u(u,0));t.selection.setRng(l.toRange())}},tg=function(r,o){var t,e=ar.fromDom(r.getBody()),n=ar.fromDom(r.selection.getStart()),i=U((t=uf(n,e),Y(t,Co).fold(q(t),function(e){return t.slice(0,e)})),Zm);return ee(i).map(function(e){var t,n=_u.fromRangeStart(r.selection.getRng());return!(!$l(o,n,e.dom())||Ju((t=e).dom())&&Hm(t.dom())||(eg(o,r,e,i),0))}).getOr(!1)},ng=function(e,t){return!!e.selection.isCollapsed()&&tg(e,t)},rg=jo.isContentEditableTrue,og=jo.isContentEditableFalse,ig=function(e,t,n,r,o){return t._selectionOverrides.showCaret(e,n,r,o)},ag=function(e,t){var n,r;return e.fire("BeforeObjectSelected",{target:t}).isDefaultPrevented()?null:((r=(n=t).ownerDocument.createRange()).selectNode(n),r)},ug=function(e,t,n){var r=Os(1,e.getBody(),t),o=_u.fromRangeStart(r),i=o.getNode();if(og(i))return ig(1,e,i,!o.isAtEnd(),!1);var a=o.getNode(!0);if(og(a))return ig(1,e,a,!1,!1);var u=e.dom.getParent(o.getNode(),function(e){return og(e)||rg(e)});return og(u)?ig(1,e,u,!1,n):null},sg=function(e,t,n){if(!t||!t.collapsed)return t;var r=ug(e,t,n);return r||t},cg=function(e,t,n,r,o,i){var a,u,s=ig(r,e,i.getNode(!o),o,!0);if(t.collapsed){var c=t.cloneRange();o?c.setEnd(s.startContainer,s.startOffset):c.setStart(s.endContainer,s.endOffset),c.deleteContents()}else t.deleteContents();return e.selection.setRng(s),a=e.dom,u=n,jo.isText(u)&&0===u.data.length&&a.remove(u),!0},lg=function(e,t){return function(e,t){var n=e.selection.getRng();if(!jo.isText(n.commonAncestorContainer))return!1;var r=t?Tu.Forwards:Tu.Backwards,o=Js(e.getBody()),i=d(Ls,o.next),a=d(Ls,o.prev),u=t?i:a,s=t?Lf:Ff,c=Ps(r,e.getBody(),n),l=Vl.normalizePosition(t,u(c));if(!l)return!1;if(s(l))return cg(e,n,c.getNode(),r,t,l);var f=u(l);return!!(f&&s(f)&&Fs(l,f))&&cg(e,n,c.getNode(),r,t,f)}(e,t)},fg=function(e,t){e.getDoc().execCommand(t,!1,null)},dg=function(e){ad(e,!1)||lg(e,!1)||Qd(e,!1)||hf(e,!1)||Dm(e)||Cf(e,!1)||ng(e,!1)||(fg(e,"Delete"),ql(e))},mg=function(e){ad(e,!0)||lg(e,!0)||Qd(e,!0)||hf(e,!0)||Dm(e)||Cf(e,!0)||ng(e,!0)||fg(e,"ForwardDelete")},gg=function(o,t,e){var n=function(e){return t=o,n=e.dom(),r=_r(n,t),_.from(r).filter(function(e){return 0<e.length});var t,n,r};return na(ar.fromDom(e),function(e){return n(e).isSome()},function(e){return Mr(ar.fromDom(t),e)}).bind(n)},pg=function(o){return function(r,e){return _.from(e).map(ar.fromDom).filter(dr).bind(function(e){return gg(o,r,e.dom()).or((t=o,n=e.dom(),_.from(Si.DOM.getStyle(n,t,!0))));var t,n}).getOr("")}},hg={getFontSize:pg("font-size"),getFontFamily:H(function(e){return e.replace(/[\'\"\\]/g,"").replace(/,\s+/g,",")},pg("font-family")),toPt:function(e,t){return/[0-9.]+px$/.test(e)?(n=72*parseInt(e,10)/96,r=t||0,o=Math.pow(10,r),Math.round(n*o)/o+"pt"):e;var n,r,o}},vg=function(e){return sc.firstPositionIn(e.getBody()).map(function(e){var t=e.container();return jo.isText(t)?t.parentNode:t})},yg=function(o){return _.from(o.selection.getRng()).bind(function(e){var t,n,r=o.getBody();return n=r,(t=e).startContainer===n&&0===t.startOffset?_.none():_.from(o.selection.getStart(!0))})},bg=function(e,t){if(/^[0-9\.]+$/.test(t)){var n=parseInt(t,10);if(1<=n&&n<=7){var r=Al(e),o=Rl(e);return o?o[n-1]||t:r[n-1]||t}return t}return t},Cg=function(e,t){return e&&t&&e.startContainer===t.startContainer&&e.startOffset===t.startOffset&&e.endContainer===t.endContainer&&e.endOffset===t.endOffset},xg=function(e,t,n){return null!==function(e,t,n){for(;e&&e!==t;){if(n(e))return e;e=e.parentNode}return null}(e,t,n)},wg=function(e,t,n){return xg(e,t,function(e){return e.nodeName===n})},Ng=function(e){return e&&"TABLE"===e.nodeName},Eg=function(e,t,n){for(var r=new go(t,e.getParent(t.parentNode,e.isBlock)||e.getRoot());t=r[n?"prev":"next"]();)if(jo.isBr(t))return!0},Sg=function(e,t,n,r,o){var i,a,u,s,c,l,f=e.getRoot(),d=e.schema.getNonEmptyElements();if(u=e.getParent(o.parentNode,e.isBlock)||f,r&&jo.isBr(o)&&t&&e.isEmpty(u))return _.some(Su(o.parentNode,e.nodeIndex(o)));for(i=new go(o,u);s=i[r?"prev":"next"]();){if("false"===e.getContentEditableParent(s)||(l=f,ka(c=s)&&!1===xg(c,l,Ju)))return _.none();if(jo.isText(s)&&0<s.nodeValue.length)return!1===wg(s,f,"A")?_.some(Su(s,r?s.nodeValue.length:0)):_.none();if(e.isBlock(s)||d[s.nodeName.toLowerCase()])return _.none();a=s}return n&&a?_.some(Su(a,0)):_.none()},Tg=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m,g=e.getRoot(),p=!1;if(o=r[(n?"start":"end")+"Container"],i=r[(n?"start":"end")+"Offset"],l=jo.isElement(o)&&i===o.childNodes.length,s=e.schema.getNonEmptyElements(),c=n,ka(o))return _.none();if(jo.isElement(o)&&i>o.childNodes.length-1&&(c=!1),jo.isDocument(o)&&(o=g,i=0),o===g){if(c&&(u=o.childNodes[0<i?i-1:0])){if(ka(u))return _.none();if(s[u.nodeName]||Ng(u))return _.none()}if(o.hasChildNodes()){if(i=Math.min(!c&&0<i?i-1:i,o.childNodes.length-1),o=o.childNodes[i],i=jo.isText(o)&&l?o.data.length:0,!t&&o===g.lastChild&&Ng(o))return _.none();if(function(e,t){for(;t&&t!==e;){if(jo.isContentEditableFalse(t))return!0;t=t.parentNode}return!1}(g,o)||ka(o))return _.none();if(o.hasChildNodes()&&!1===Ng(o)){a=new go(u=o,g);do{if(jo.isContentEditableFalse(u)||ka(u)){p=!1;break}if(jo.isText(u)&&0<u.nodeValue.length){i=c?0:u.nodeValue.length,o=u,p=!0;break}if(s[u.nodeName.toLowerCase()]&&(!(f=u)||!/^(TD|TH|CAPTION)$/.test(f.nodeName))){i=e.nodeIndex(u),o=u.parentNode,c||i++,p=!0;break}}while(u=c?a.next():a.prev())}}}return t&&(jo.isText(o)&&0===i&&Sg(e,l,t,!0,o).each(function(e){o=e.container(),i=e.offset(),p=!0}),jo.isElement(o)&&((u=o.childNodes[i])||(u=o.childNodes[i-1]),!u||!jo.isBr(u)||(m="A",(d=u).previousSibling&&d.previousSibling.nodeName===m)||Eg(e,u,!1)||Eg(e,u,!0)||Sg(e,l,t,!0,u).each(function(e){o=e.container(),i=e.offset(),p=!0}))),c&&!t&&jo.isText(o)&&i===o.nodeValue.length&&Sg(e,l,t,!1,o).each(function(e){o=e.container(),i=e.offset(),p=!0}),p?_.some(Su(o,i)):_.none()},kg=function(e,t){var n=t.collapsed,r=t.cloneRange(),o=Su.fromRangeStart(t);return Tg(e,n,!0,r).each(function(e){n&&Su.isAbove(o,e)||r.setStart(e.container(),e.offset())}),n||Tg(e,n,!1,r).each(function(e){r.setEnd(e.container(),e.offset())}),n&&r.collapse(!0),Cg(t,r)?_.none():_.some(r)},_g=function(e,t,n){var r=e.create("span",{}," ");n.parentNode.insertBefore(r,n),t.scrollIntoView(r),e.remove(r)},Ag=function(e,t,n,r){var o=e.createRng();r?(o.setStartBefore(n),o.setEndBefore(n)):(o.setStartAfter(n),o.setEndAfter(n)),t.setRng(o)},Rg=function(e,t){var n,r,o=e.selection,i=e.dom,a=o.getRng();kg(i,a).each(function(e){a.setStart(e.startContainer,e.startOffset),a.setEnd(e.endContainer,e.endOffset)});var u=a.startOffset,s=a.startContainer;if(1===s.nodeType&&s.hasChildNodes()){var c=u>s.childNodes.length-1;s=s.childNodes[Math.min(u,s.childNodes.length-1)]||s,u=c&&3===s.nodeType?s.nodeValue.length:0}var l=i.getParent(s,i.isBlock),f=l?i.getParent(l.parentNode,i.isBlock):null,d=f?f.nodeName.toUpperCase():"",m=t&&t.ctrlKey;"LI"!==d||m||(l=f),s&&3===s.nodeType&&u>=s.nodeValue.length&&(function(e,t,n){for(var r,o=new go(t,n),i=e.getNonEmptyElements();r=o.next();)if(i[r.nodeName.toLowerCase()]||0<r.length)return!0}(e.schema,s,l)||(n=i.create("br"),a.insertNode(n),a.setStartAfter(n),a.setEndAfter(n),r=!0)),n=i.create("br"),zu(i,a,n),_g(i,o,n),Ag(i,o,n,r),e.undoManager.add()},Dg=function(e,t){var n=ar.fromTag("br");Pi(ar.fromDom(t),n),e.undoManager.add()},Og=function(e,t){Bg(e.getBody(),t)||Ii(ar.fromDom(t),ar.fromTag("br"));var n=ar.fromTag("br");Ii(ar.fromDom(t),n),_g(e.dom,e.selection,n.dom()),Ag(e.dom,e.selection,n.dom(),!1),e.undoManager.add()},Bg=function(e,t){return n=_u.after(t),!!jo.isBr(n.getNode())||sc.nextPosition(e,_u.after(t)).map(function(e){return jo.isBr(e.getNode())}).getOr(!1);var n},Pg=function(e){return e&&"A"===e.nodeName&&"href"in e},Ig=function(e){return e.fold(q(!1),Pg,Pg,q(!1))},Lg=function(e,t){t.fold(o,d(Dg,e),d(Og,e),o)},Fg=function(e,t){var n,r,o,i=(n=e,r=d(Vl.isInlineTarget,n),o=_u.fromRangeStart(n.selection.getRng()),Ld(r,n.getBody(),o).filter(Ig));i.isSome()?i.each(d(Lg,e)):Rg(e,t)},Mg={create:Ar("start","soffset","finish","foffset")},zg=xf([{before:["element"]},{on:["element","offset"]},{after:["element"]}]),Ug=(zg.before,zg.on,zg.after,function(e){return e.fold($,$,$)}),jg=xf([{domRange:["rng"]},{relative:["startSitu","finishSitu"]},{exact:["start","soffset","finish","foffset"]}]),Vg={domRange:jg.domRange,relative:jg.relative,exact:jg.exact,exactFromRange:function(e){return jg.exact(e.start(),e.soffset(),e.finish(),e.foffset())},getWin:function(e){var t=e.match({domRange:function(e){return ar.fromDom(e.startContainer)},relative:function(e,t){return Ug(e)},exact:function(e,t,n,r){return e}});return jr(t)},range:Mg.create},Hg=or.detect().browser,qg=function(e,t){var n=mr(t)?Mc(t).length:Kr(t).length+1;return n<e?n:e<0?0:e},$g=function(e){return Vg.range(e.start(),qg(e.soffset(),e.start()),e.finish(),qg(e.foffset(),e.finish()))},Wg=function(e,t){return!jo.isRestrictedNode(t.dom())&&(zr(e,t)||Mr(e,t))},Kg=function(t){return function(e){return Wg(t,e.start())&&Wg(t,e.finish())}},Xg=function(e){return!0===e.inline||Hg.isIE()},Yg=function(e){return Vg.range(ar.fromDom(e.startContainer),e.startOffset,ar.fromDom(e.endContainer),e.endOffset)},Gg=function(e){var t=e.getSelection();return(t&&0!==t.rangeCount?_.from(t.getRangeAt(0)):_.none()).map(Yg)},Jg=function(e){var t=jr(e);return Gg(t.dom()).filter(Kg(e))},Qg=function(e,t){return _.from(t).filter(Kg(e)).map($g)},Zg=function(e){var t=V.document.createRange();try{return t.setStart(e.start().dom(),e.soffset()),t.setEnd(e.finish().dom(),e.foffset()),_.some(t)}catch(n){return _.none()}},ep=function(e){return(e.bookmark?e.bookmark:_.none()).bind(d(Qg,ar.fromDom(e.getBody()))).bind(Zg)},tp=function(e){var t=Xg(e)?Jg(ar.fromDom(e.getBody())):_.none();e.bookmark=t.isSome()?t:e.bookmark},np=function(t){ep(t).each(function(e){t.selection.setRng(e)})},rp=ep,op=function(e){return Eo(e)||So(e)},ip=function(e){return U(W(e.selection.getSelectedBlocks(),ar.fromDom),function(e){return!op(e)&&!Vr(e).map(op).getOr(!1)})},ap=function(e,t){var n=e.settings,r=e.dom,o=e.selection,i=e.formatter,a=/[a-z%]+$/i.exec(n.indentation)[0],u=parseInt(n.indentation,10),s=e.getParam("indent_use_margin",!1);e.queryCommandState("InsertUnorderedList")||e.queryCommandState("InsertOrderedList")||n.forced_root_block||r.getParent(o.getNode(),r.isBlock)||i.apply("div"),z(ip(e),function(e){!function(e,t,n,r,o,i){if("false"!==e.getContentEditable(i)){var a=n?"margin":"padding";if(a="TABLE"===i.nodeName?"margin":a,a+="rtl"===e.getStyle(i,"direction",!0)?"Right":"Left","outdent"===t){var u=Math.max(0,parseInt(i.style[a]||0,10)-r);e.setStyle(i,a,u?u+o:"")}else u=parseInt(i.style[a]||0,10)+r+o,e.setStyle(i,a,u)}}(r,t,s,u,a,e.dom())})},up=Xt.each,sp=Xt.extend,cp=Xt.map,lp=Xt.inArray;function fp(s){var o,i,a,t,c={state:{},exec:{},value:{}},n=s.settings;s.on("PreInit",function(){o=s.dom,i=s.selection,n=s.settings,a=s.formatter});var r=function(e){var t;if(!s.quirks.isHidden()&&!s.removed){if(e=e.toLowerCase(),t=c.state[e])return t(e);try{return s.getDoc().queryCommandState(e)}catch(n){}return!1}},e=function(e,n){n=n||"exec",up(e,function(t,e){up(e.toLowerCase().split(","),function(e){c[n][e]=t})})},u=function(e,t,n){e=e.toLowerCase(),c.value[e]=function(){return t.call(n||s)}};sp(this,{execCommand:function(t,n,r,e){var o,i,a=!1;if(!s.removed){if(/^(mceAddUndoLevel|mceEndUndoLevel|mceBeginUndoLevel|mceRepaint)$/.test(t)||e&&e.skip_focus?np(s):s.focus(),(e=s.fire("BeforeExecCommand",{command:t,ui:n,value:r})).isDefaultPrevented())return!1;if(i=t.toLowerCase(),o=c.exec[i])return o(i,n,r),s.fire("ExecCommand",{command:t,ui:n,value:r}),!0;if(up(s.plugins,function(e){if(e.execCommand&&e.execCommand(t,n,r))return s.fire("ExecCommand",{command:t,ui:n,value:r}),!(a=!0)}),a)return a;if(s.theme&&s.theme.execCommand&&s.theme.execCommand(t,n,r))return s.fire("ExecCommand",{command:t,ui:n,value:r}),!0;try{a=s.getDoc().execCommand(t,n,r)}catch(u){}return!!a&&(s.fire("ExecCommand",{command:t,ui:n,value:r}),!0)}},queryCommandState:r,queryCommandValue:function(e){var t;if(!s.quirks.isHidden()&&!s.removed){if(e=e.toLowerCase(),t=c.value[e])return t(e);try{return s.getDoc().queryCommandValue(e)}catch(n){}}},queryCommandSupported:function(e){if(e=e.toLowerCase(),c.exec[e])return!0;try{return s.getDoc().queryCommandSupported(e)}catch(t){}return!1},addCommands:e,addCommand:function(e,o,i){e=e.toLowerCase(),c.exec[e]=function(e,t,n,r){return o.call(i||s,t,n,r)}},addQueryStateHandler:function(e,t,n){e=e.toLowerCase(),c.state[e]=function(){return t.call(n||s)}},addQueryValueHandler:u,hasCustomCommand:function(e){return e=e.toLowerCase(),!!c.exec[e]}});var l=function(e,t,n){return t===undefined&&(t=!1),n===undefined&&(n=null),s.getDoc().execCommand(e,t,n)},f=function(e){return a.match(e)},d=function(e,t){a.toggle(e,t?{value:t}:undefined),s.nodeChanged()},m=function(e){t=i.getBookmark(e)},g=function(){i.moveToBookmark(t)};e({"mceResetDesignMode,mceBeginUndoLevel":function(){},"mceEndUndoLevel,mceAddUndoLevel":function(){s.undoManager.add()},"Cut,Copy,Paste":function(e){var t,n=s.getDoc();try{l(e)}catch(o){t=!0}if("paste"!==e||n.queryCommandEnabled(e)||(t=!0),t||!n.queryCommandSupported(e)){var r=s.translate("Your browser doesn't support direct access to the clipboard. Please use the Ctrl+X/C/V keyboard shortcuts instead.");fe.mac&&(r=r.replace(/Ctrl\+/g,"\u2318+")),s.notificationManager.open({text:r,type:"error"})}},unlink:function(){if(i.isCollapsed()){var e=s.dom.getParent(s.selection.getStart(),"a");e&&s.dom.remove(e,!0)}else a.remove("link")},"JustifyLeft,JustifyCenter,JustifyRight,JustifyFull,JustifyNone":function(e){var t=e.substring(7);"full"===t&&(t="justify"),up("left,center,right,justify".split(","),function(e){t!==e&&a.remove("align"+e)}),"none"!==t&&d("align"+t)},"InsertUnorderedList,InsertOrderedList":function(e){var t,n;l(e),(t=o.getParent(i.getNode(),"ol,ul"))&&(n=t.parentNode,/^(H[1-6]|P|ADDRESS|PRE)$/.test(n.nodeName)&&(m(),o.split(n,t),g()))},"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){d(e)},"ForeColor,HiliteColor":function(e,t,n){d(e,n)},FontName:function(e,t,n){var r,o;o=n,(r=s).formatter.toggle("fontname",{value:bg(r,o)}),r.nodeChanged()},FontSize:function(e,t,n){var r,o;o=n,(r=s).formatter.toggle("fontsize",{value:bg(r,o)}),r.nodeChanged()},RemoveFormat:function(e){a.remove(e)},mceBlockQuote:function(){d("blockquote")},FormatBlock:function(e,t,n){return d(n||"p")},mceCleanup:function(){var e=i.getBookmark();s.setContent(s.getContent()),i.moveToBookmark(e)},mceRemoveNode:function(e,t,n){var r=n||i.getNode();r!==s.getBody()&&(m(),s.dom.remove(r,!0),g())},mceSelectNodeDepth:function(e,t,n){var r=0;o.getParent(i.getNode(),function(e){if(1===e.nodeType&&r++===n)return i.select(e),!1},s.getBody())},mceSelectNode:function(e,t,n){i.select(n)},mceInsertContent:function(e,t,n){ml(s,n)},mceInsertRawHTML:function(e,t,n){i.setContent("tiny_mce_marker");var r=s.getContent();s.setContent(r.replace(/tiny_mce_marker/g,function(){return n}))},mceToggleFormat:function(e,t,n){d(n)},mceSetContent:function(e,t,n){s.setContent(n)},"Indent,Outdent":function(e){ap(s,e)},mceRepaint:function(){},InsertHorizontalRule:function(){s.execCommand("mceInsertContent",!1,"<hr />")},mceToggleVisualAid:function(){s.hasVisual=!s.hasVisual,s.addVisual()},mceReplaceContent:function(e,t,n){s.execCommand("mceInsertContent",!1,n.replace(/\{\$selection\}/g,i.getContent({format:"text"})))},mceInsertLink:function(e,t,n){var r;"string"==typeof n&&(n={href:n}),r=o.getParent(i.getNode(),"a"),n.href=n.href.replace(" ","%20"),r&&n.href||a.remove("link"),n.href&&a.apply("link",n,r)},selectAll:function(){var e=o.getParent(i.getStart(),jo.isContentEditableTrue);if(e){var t=o.createRng();t.selectNodeContents(e),i.setRng(t)}},"delete":function(){dg(s)},forwardDelete:function(){mg(s)},mceNewDocument:function(){s.setContent("")},InsertLineBreak:function(e,t,n){return Fg(s,n),!0}});var p=function(n){return function(){var e=i.isCollapsed()?[o.getParent(i.getNode(),o.isBlock)]:i.getSelectedBlocks(),t=cp(e,function(e){return!!a.matchNode(e,n)});return-1!==lp(t,!0)}};e({JustifyLeft:p("alignleft"),JustifyCenter:p("aligncenter"),JustifyRight:p("alignright"),JustifyFull:p("alignjustify"),"Bold,Italic,Underline,Strikethrough,Superscript,Subscript":function(e){return f(e)},mceBlockQuote:function(){return f("blockquote")},Outdent:function(){var e;if(n.inline_styles){if((e=o.getParent(i.getStart(),o.isBlock))&&0<parseInt(e.style.paddingLeft,10))return!0;if((e=o.getParent(i.getEnd(),o.isBlock))&&0<parseInt(e.style.paddingLeft,10))return!0}return r("InsertUnorderedList")||r("InsertOrderedList")||!n.inline_styles&&!!o.getParent(i.getNode(),"BLOCKQUOTE")},"InsertUnorderedList,InsertOrderedList":function(e){var t=o.getParent(i.getNode(),"ul,ol");return t&&("insertunorderedlist"===e&&"UL"===t.tagName||"insertorderedlist"===e&&"OL"===t.tagName)}},"state"),e({Undo:function(){s.undoManager.undo()},Redo:function(){s.undoManager.redo()}}),u("FontName",function(){return yg(t=s).fold(function(){return vg(t).map(function(e){return hg.getFontFamily(t.getBody(),e)}).getOr("")},function(e){return hg.getFontFamily(t.getBody(),e)});var t},this),u("FontSize",function(){return yg(t=s).fold(function(){return vg(t).map(function(e){return hg.getFontSize(t.getBody(),e)}).getOr("")},function(e){return hg.getFontSize(t.getBody(),e)});var t},this)}var dp=Xt.makeMap("focus blur focusin focusout click dblclick mousedown mouseup mousemove mouseover beforepaste paste cut copy selectionchange mouseout mouseenter mouseleave wheel keydown keypress keyup input contextmenu dragstart dragend dragover draggesture dragdrop drop drag submit compositionstart compositionend compositionupdate touchstart touchmove touchend"," "),mp=function(a){var u,s,c=this,l={},f=function(){return!1},d=function(){return!0};u=(a=a||{}).scope||c,s=a.toggleEvent||f;var r=function(e,t,n,r){var o,i,a;if(!1===t&&(t=f),t)for(t={func:t},r&&Xt.extend(t,r),a=(i=e.toLowerCase().split(" ")).length;a--;)e=i[a],(o=l[e])||(o=l[e]=[],s(e,!0)),n?o.unshift(t):o.push(t);return c},m=function(e,t){var n,r,o,i,a;if(e)for(n=(i=e.toLowerCase().split(" ")).length;n--;){if(e=i[n],r=l[e],!e){for(o in l)s(o,!1),delete l[o];return c}if(r){if(t)for(a=r.length;a--;)r[a].func===t&&(r=r.slice(0,a).concat(r.slice(a+1)),l[e]=r);else r.length=0;r.length||(s(e,!1),delete l[e])}}else{for(e in l)s(e,!1);l={}}return c};c.fire=function(e,t){var n,r,o,i;if(e=e.toLowerCase(),(t=t||{}).type=e,t.target||(t.target=u),t.preventDefault||(t.preventDefault=function(){t.isDefaultPrevented=d},t.stopPropagation=function(){t.isPropagationStopped=d},t.stopImmediatePropagation=function(){t.isImmediatePropagationStopped=d},t.isDefaultPrevented=f,t.isPropagationStopped=f,t.isImmediatePropagationStopped=f),a.beforeFire&&a.beforeFire(t),n=l[e])for(r=0,o=n.length;r<o;r++){if((i=n[r]).once&&m(e,i.func),t.isImmediatePropagationStopped())return t.stopPropagation(),t;if(!1===i.func.call(u,t))return t.preventDefault(),t}return t},c.on=r,c.off=m,c.once=function(e,t,n){return r(e,t,n,{once:!0})},c.has=function(e){return e=e.toLowerCase(),!(!l[e]||0===l[e].length)}};mp.isNative=function(e){return!!dp[e.toLowerCase()]};var gp,pp=function(n){return n._eventDispatcher||(n._eventDispatcher=new mp({scope:n,toggleEvent:function(e,t){mp.isNative(e)&&n.toggleNativeEvent&&n.toggleNativeEvent(e,t)}})),n._eventDispatcher},hp={fire:function(e,t,n){if(this.removed&&"remove"!==e&&"detach"!==e)return t;if(t=pp(this).fire(e,t,n),!1!==n&&this.parent)for(var r=this.parent();r&&!t.isPropagationStopped();)r.fire(e,t,!1),r=r.parent();return t},on:function(e,t,n){return pp(this).on(e,t,n)},off:function(e,t){return pp(this).off(e,t)},once:function(e,t){return pp(this).once(e,t)},hasEventListeners:function(e){return pp(this).has(e)}},vp=function(e,t){return e.fire("PreProcess",t)},yp=function(e,t){return e.fire("PostProcess",t)},bp=function(e){return e.fire("remove")},Cp=function(e){return e.fire("detach")},xp=function(e,t){return e.fire("SwitchMode",{mode:t})},wp=function(e,t,n,r){e.fire("ObjectResizeStart",{target:t,width:n,height:r})},Np=function(e,t,n,r){e.fire("ObjectResized",{target:t,width:n,height:r})},Ep=function(e,t,n){try{e.getDoc().execCommand(t,!1,n)}catch(r){}},Sp=function(e,t,n){var r,o;Gi(e,t)&&!1===n?(o=t,$i(r=e)?r.dom().classList.remove(o):Ki(r,o),Yi(r)):n&&Xi(e,t)},Tp=function(e,t){Sp(ar.fromDom(e.getBody()),"mce-content-readonly",t),t?(e.selection.controlSelection.hideResizeRect(),e.readonly=!0,e.getBody().contentEditable="false"):(e.readonly=!1,e.getBody().contentEditable="true",Ep(e,"StyleWithCSS",!1),Ep(e,"enableInlineTableEditing",!1),Ep(e,"enableObjectResizing",!1),e.focus(),e.nodeChanged())},kp=function(e){return e.readonly?"readonly":"design"},_p=Si.DOM,Ap=function(e,t){return"selectionchange"===t?e.getDoc():!e.inline&&/^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(t)?e.getDoc().documentElement:e.settings.event_root?(e.eventRoot||(e.eventRoot=_p.select(e.settings.event_root)[0]),e.eventRoot):e.getBody()},Rp=function(e,t,n){var r;(r=e).hidden||r.readonly?!0===e.readonly&&n.preventDefault():e.fire(t,n)},Dp=function(i,a){var e,t;if(i.delegates||(i.delegates={}),!i.delegates[a]&&!i.removed)if(e=Ap(i,a),i.settings.event_root){if(gp||(gp={},i.editorManager.on("removeEditor",function(){var e;if(!i.editorManager.activeEditor&&gp){for(e in gp)i.dom.unbind(Ap(i,e));gp=null}})),gp[a])return;t=function(e){for(var t=e.target,n=i.editorManager.get(),r=n.length;r--;){var o=n[r].getBody();(o===t||_p.isChildOf(t,o))&&Rp(n[r],a,e)}},gp[a]=t,_p.bind(e,a,t)}else t=function(e){Rp(i,a,e)},_p.bind(e,a,t),i.delegates[a]=t},Op={bindPendingEventDelegates:function(){var t=this;Xt.each(t._pendingNativeEvents,function(e){Dp(t,e)})},toggleNativeEvent:function(e,t){var n=this;"focus"!==e&&"blur"!==e&&(t?n.initialized?Dp(n,e):n._pendingNativeEvents?n._pendingNativeEvents.push(e):n._pendingNativeEvents=[e]:n.initialized&&(n.dom.unbind(Ap(n,e),e,n.delegates[e]),delete n.delegates[e]))},unbindAllNativeEvents:function(){var e,t=this,n=t.getBody(),r=t.dom;if(t.delegates){for(e in t.delegates)t.dom.unbind(Ap(t,e),e,t.delegates[e]);delete t.delegates}!t.inline&&n&&r&&(n.onload=null,r.unbind(t.getWin()),r.unbind(t.getDoc())),r&&(r.unbind(n),r.unbind(t.getContainer()))}},Bp=Op=Xt.extend({},hp,Op),Pp=Ar("sections","settings"),Ip=or.detect().deviceType.isTouch(),Lp=["lists","autolink","autosave"],Fp={theme:"mobile"},Mp=function(e){var t=k(e)?e.join(" "):e,n=W(S(t)?t.split(" "):[],Gn);return U(n,function(e){return 0<e.length})},zp=function(e,t){return e.sections().hasOwnProperty(t)},Up=function(e,t,n,r){var o,i=Mp(n.forced_plugins),a=Mp(r.plugins),u=e&&zp(t,"mobile")?U(a,d(F,Lp)):a,s=(o=u,[].concat(Mp(i)).concat(Mp(o)));return Xt.extend(r,{plugins:s.join(" ")})},jp=function(e,t,n,r){var o,i,a,u,s,c,l,f,d,m,g,p,h=(o=["mobile"],i=yr(r,function(e,t){return F(o,t)}),Pp(i.t,i.f)),v=Xt.extend(t,n,h.settings(),(m=e,p=(g=h).settings().inline,m&&zp(g,"mobile")&&!p?(c="mobile",l=Fp,f=h.sections(),d=f.hasOwnProperty(c)?f[c]:{},Xt.extend({},l,d)):{}),{validate:!0,content_editable:h.settings().inline,external_plugins:(a=n,u=h.settings(),s=u.external_plugins?u.external_plugins:{},a&&a.external_plugins?Xt.extend({},a.external_plugins,s):s)});return Up(e,h,n,v)},Vp=function(e,t,n){return _.from(t.settings[n]).filter(e)},Hp=function(e,t,n,r){var o,i,a,u=t in e.settings?e.settings[t]:n;return"hash"===r?(a={},"string"==typeof(i=u)?z(0<i.indexOf("=")?i.split(/[;,](?![^=;,]*(?:[;,]|$))/):i.split(","),function(e){var t=e.split("=");1<t.length?a[Xt.trim(t[0])]=Xt.trim(t[1]):a[Xt.trim(t[0])]=Xt.trim(t)}):a=i,a):"string"===r?Vp(S,e,t).getOr(n):"number"===r?Vp(O,e,t).getOr(n):"boolean"===r?Vp(R,e,t).getOr(n):"object"===r?Vp(T,e,t).getOr(n):"array"===r?Vp(k,e,t).getOr(n):"string[]"===r?Vp((o=S,function(e){return k(e)&&J(e,o)}),e,t).getOr(n):"function"===r?Vp(D,e,t).getOr(n):u},qp=Xt.each,$p=Xt.explode,Wp={f1:112,f2:113,f3:114,f4:115,f5:116,f6:117,f7:118,f8:119,f9:120,f10:121,f11:122,f12:123},Kp=Xt.makeMap("alt,ctrl,shift,meta,access");function Xp(i){var a={},r=[],u=function(e){var t,n,r={};for(n in qp($p(e,"+"),function(e){e in Kp?r[e]=!0:/^[0-9]{2,}$/.test(e)?r.keyCode=parseInt(e,10):(r.charCode=e.charCodeAt(0),r.keyCode=Wp[e]||e.toUpperCase().charCodeAt(0))}),t=[r.keyCode],Kp)r[n]?t.push(n):r[n]=!1;return r.id=t.join(","),r.access&&(r.alt=!0,fe.mac?r.ctrl=!0:r.shift=!0),r.meta&&(fe.mac?r.meta=!0:(r.ctrl=!0,r.meta=!1)),r},s=function(e,t,n,r){var o;return(o=Xt.map($p(e,">"),u))[o.length-1]=Xt.extend(o[o.length-1],{func:n,scope:r||i}),Xt.extend(o[0],{desc:i.translate(t),subpatterns:o.slice(1)})},o=function(e,t){return!!t&&t.ctrl===e.ctrlKey&&t.meta===e.metaKey&&t.alt===e.altKey&&t.shift===e.shiftKey&&!!(e.keyCode===t.keyCode||e.charCode&&e.charCode===t.charCode)&&(e.preventDefault(),!0)},c=function(e){return e.func?e.func.call(e.scope):null};i.on("keyup keypress keydown",function(t){var e,n;((n=t).altKey||n.ctrlKey||n.metaKey||"keydown"===(e=t).type&&112<=e.keyCode&&e.keyCode<=123)&&!t.isDefaultPrevented()&&(qp(a,function(e){if(o(t,e))return r=e.subpatterns.slice(0),"keydown"===t.type&&c(e),!0}),o(t,r[0])&&(1===r.length&&"keydown"===t.type&&c(r[0]),r.shift()))}),this.add=function(e,n,r,o){var t;return"string"==typeof(t=r)?r=function(){i.execCommand(t,!1,null)}:Xt.isArray(t)&&(r=function(){i.execCommand(t[0],t[1],t[2])}),qp($p(Xt.trim(e.toLowerCase())),function(e){var t=s(e,n,r,o);a[t.id]=t}),!0},this.remove=function(e){var t=s(e);return!!a[t.id]&&(delete a[t.id],!0)}}var Yp=function(e){var t=Ur(e).dom();return e.dom()===t.activeElement},Gp=function(t){return(e=Ur(t),n=e!==undefined?e.dom():V.document,_.from(n.activeElement).map(ar.fromDom)).filter(function(e){return t.dom().contains(e.dom())});var e,n},Jp=function(t,e){return(n=e,n.collapsed?_.from(eu(n.startContainer,n.startOffset)).map(ar.fromDom):_.none()).bind(function(e){return ko(e)?_.some(e):!1===zr(t,e)?_.some(t):_.none()});var n},Qp=function(t,e){Jp(ar.fromDom(t.getBody()),e).bind(function(e){return sc.firstPositionIn(e.dom())}).fold(function(){t.selection.normalize()},function(e){return t.selection.setRng(e.toRange())})},Zp=function(e){if(e.setActive)try{e.setActive()}catch(t){e.focus()}else e.focus()},eh=function(e){var t,n=e.getBody();return n&&(t=ar.fromDom(n),Yp(t)||Gp(t).isSome())},th=function(e){return e.inline?eh(e):(t=e).iframeElement&&Yp(ar.fromDom(t.iframeElement));var t},nh=function(e){return e.editorManager.setActive(e)},rh=function(e,t){e.removed||(t?nh(e):function(t){var e=t.selection,n=t.settings.content_editable,r=t.getBody(),o=e.getRng();t.quirks.refreshContentEditable();var i,a,u=(i=t,a=e.getNode(),i.dom.getParent(a,function(e){return"true"===i.dom.getContentEditable(e)}));if(t.$.contains(r,u))return Zp(u),Qp(t,o),nh(t);t.bookmark!==undefined&&!1===th(t)&&rp(t).each(function(e){t.selection.setRng(e),o=e}),n||(fe.opera||Zp(r),t.getWin().focus()),(fe.gecko||n)&&(Zp(r),Qp(t,o)),nh(t)}(e))},oh=th,ih=function(e,t){return t.dom()[e]},ah=function(e,t){return parseInt(kr(t,e),10)},uh=d(ih,"clientWidth"),sh=d(ih,"clientHeight"),ch=d(ah,"margin-top"),lh=d(ah,"margin-left"),fh=function(e,t,n){var r,o,i,a,u,s,c,l,f,d,m,g=ar.fromDom(e.getBody()),p=e.inline?g:(r=g,ar.fromDom(r.dom().ownerDocument.documentElement)),h=(o=e.inline,a=t,u=n,s=(i=p).dom().getBoundingClientRect(),{x:a-(o?s.left+i.dom().clientLeft+lh(i):0),y:u-(o?s.top+i.dom().clientTop+ch(i):0)});return l=h.x,f=h.y,d=uh(c=p),m=sh(c),0<=l&&0<=f&&l<=d&&f<=m},dh=function(e){var t,n=e.inline?e.getBody():e.getContentAreaContainer();return(t=n,_.from(t).map(ar.fromDom)).map(function(e){return zr(Ur(e),e)}).getOr(!1)};function mh(n){var t,o=[],i=function(){var e,t=n.theme;return t&&t.getNotificationManagerImpl?t.getNotificationManagerImpl():{open:e=function(){throw new Error("Theme did not provide a NotificationManager implementation.")},close:e,reposition:e,getArgs:e}},a=function(){0<o.length&&i().reposition(o)},u=function(t){Y(o,function(e){return e===t}).each(function(e){o.splice(e,1)})},r=function(r){if(!n.removed&&dh(n))return X(o,function(e){return t=i().getArgs(e),n=r,!(t.type!==n.type||t.text!==n.text||t.progressBar||t.timeout||n.progressBar||n.timeout);var t,n}).getOrThunk(function(){n.editorManager.setActive(n);var e,t=i().open(r,function(){u(t),a()});return e=t,o.push(e),a(),t})};return(t=n).on("SkinLoaded",function(){var e=t.settings.service_message;e&&r({text:e,type:"warning",timeout:0,icon:""})}),t.on("ResizeEditor ResizeWindow",function(){he.requestAnimationFrame(a)}),t.on("remove",function(){z(o.slice(),function(e){i().close(e)})}),{open:r,close:function(){_.from(o[0]).each(function(e){i().close(e),u(e),a()})},getNotifications:function(){return o}}}function gh(r){var o=[],i=function(){var e,t=r.theme;return t&&t.getWindowManagerImpl?t.getWindowManagerImpl():{open:e=function(){throw new Error("Theme did not provide a WindowManager implementation.")},alert:e,confirm:e,close:e,getParams:e,setParams:e}},a=function(e,t){return function(){return t?t.apply(e,arguments):undefined}},u=function(e){var t;o.push(e),t=e,r.fire("OpenWindow",{win:t})},s=function(n){Y(o,function(e){return e===n}).each(function(e){var t;o.splice(e,1),t=n,r.fire("CloseWindow",{win:t}),0===o.length&&r.focus()})},e=function(){return _.from(o[o.length-1])};return r.on("remove",function(){z(o.slice(0),function(e){i().close(e)})}),{windows:o,open:function(e,t){r.editorManager.setActive(r),tp(r);var n=i().open(e,t,s);return u(n),n},alert:function(e,t,n){var r=i().alert(e,a(n||this,t),s);u(r)},confirm:function(e,t,n){var r=i().confirm(e,a(n||this,t),s);u(r)},close:function(){e().each(function(e){i().close(e),s(e)})},getParams:function(){return e().map(i().getParams).getOr(null)},setParams:function(t){e().each(function(e){i().setParams(e,t)})},getWindows:function(){return o}}}var ph={},hh="en",vh={setCode:function(e){e&&(hh=e,this.rtl=!!this.data[e]&&"rtl"===this.data[e]._dir)},getCode:function(){return hh},rtl:!1,add:function(e,t){var n=ph[e];for(var r in n||(ph[e]=n={}),t)n[r]=t[r];this.setCode(e)},translate:function(e){var t=ph[hh]||{},n=function(e){return Xt.is(e,"function")?Object.prototype.toString.call(e):r(e)?"":""+e},r=function(e){return""===e||null===e||Xt.is(e,"undefined")},o=function(e){return e=n(e),Xt.hasOwn(t,e)?n(t[e]):e};if(r(e))return"";if(Xt.is(e,"object")&&Xt.hasOwn(e,"raw"))return n(e.raw);if(Xt.is(e,"array")){var i=e.slice(1);e=o(e[0]).replace(/\{([0-9]+)\}/g,function(e,t){return Xt.hasOwn(i,t)?n(i[t]):e})}return o(e).replace(/{context:\w+}$/,"")},data:ph},yh=Bi.PluginManager,bh=function(e,t){var n=function(e,t){for(var n in yh.urls)if(yh.urls[n]+"/plugin"+t+".js"===e)return n;return null}(t,e.suffix);return n?vh.translate(["Failed to load plugin: {0} from url {1}",n,t]):vh.translate(["Failed to load plugin url: {0}",t])},Ch=function(e,t){e.notificationManager.open({type:"error",text:t})},xh=function(e,t){e._skinLoaded?Ch(e,t):e.on("SkinLoaded",function(){Ch(e,t)})},wh=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r=V.window.console;r&&(r.error?r.error.apply(r,arguments):r.log.apply(r,arguments))},Nh={pluginLoadError:function(e,t){xh(e,bh(e,t))},pluginInitError:function(e,t,n){var r=vh.translate(["Failed to initialize plugin: {0}",t]);wh(r,n),xh(e,r)},uploadError:function(e,t){xh(e,vh.translate(["Failed to upload image: {0}",t]))},displayError:xh,initError:wh},Eh=Bi.PluginManager,Sh=Bi.ThemeManager;function Th(){return new(oe.getOrDie("XMLHttpRequest"))}function kh(u,s){var r={},n=function(e,r,o,t){var i,n;(i=Th()).open("POST",s.url),i.withCredentials=s.credentials,i.upload.onprogress=function(e){t(e.loaded/e.total*100)},i.onerror=function(){o("Image upload failed due to a XHR Transport error. Code: "+i.status)},i.onload=function(){var e,t,n;i.status<200||300<=i.status?o("HTTP Error: "+i.status):(e=JSON.parse(i.responseText))&&"string"==typeof e.location?r((t=s.basePath,n=e.location,t?t.replace(/\/$/,"")+"/"+n.replace(/^\//,""):n)):o("Invalid JSON: "+i.responseText)},(n=new V.FormData).append("file",e.blob(),e.filename()),i.send(n)},c=function(e,t){return{url:t,blobInfo:e,status:!0}},l=function(e,t){return{url:"",blobInfo:e,status:!1,error:t}},f=function(e,t){Xt.each(r[e],function(e){e(t)}),delete r[e]},o=function(e,n){return e=Xt.grep(e,function(e){return!u.isUploaded(e.blobUri())}),de.all(Xt.map(e,function(e){return u.isPending(e.blobUri())?(t=e.blobUri(),new de(function(e){r[t]=r[t]||[],r[t].push(e)})):(o=e,i=s.handler,a=n,u.markPending(o.blobUri()),new de(function(t){var n;try{var r=function(){n&&n.close()};i(o,function(e){r(),u.markUploaded(o.blobUri(),e),f(o.blobUri(),c(o,e)),t(c(o,e))},function(e){r(),u.removeFailed(o.blobUri()),f(o.blobUri(),l(o,e)),t(l(o,e))},function(e){e<0||100<e||(n||(n=a()),n.progressBar.value(e))})}catch(e){t(l(o,e.message))}}));var o,i,a,t}))};return!1===D(s.handler)&&(s.handler=n),{upload:function(e,t){return s.url||s.handler!==n?o(e,t):new de(function(e){e([])})}}}var _h=function(e){return oe.getOrDie("atob")(e)},Ah=function(e){var t,n,r=decodeURIComponent(e).split(",");return(n=/data:([^;]+)/.exec(r[0]))&&(t=n[1]),{type:t,data:r[1]}},Rh=function(a){return new de(function(e){var t,n,r,o,i=Ah(a);try{t=_h(i.data)}catch(iE){return void e(new V.Blob([]))}for(o=t.length,n=new(oe.getOrDie("Uint8Array"))(o),r=0;r<n.length;r++)n[r]=t.charCodeAt(r);e(new V.Blob([n],{type:i.type}))})},Dh=function(e){return 0===e.indexOf("blob:")?(i=e,new de(function(e,t){var n=function(){t("Cannot convert "+i+" to Blob. Resource might not exist or is inaccessible.")};try{var r=Th();r.open("GET",i,!0),r.responseType="blob",r.onload=function(){200===this.status?e(this.response):n()},r.onerror=n,r.send()}catch(o){n()}})):0===e.indexOf("data:")?Rh(e):null;var i},Oh=function(n){return new de(function(e){var t=new(oe.getOrDie("FileReader"));t.onloadend=function(){e(t.result)},t.readAsDataURL(n)})},Bh=Ah,Ph=0,Ih=function(e){return(e||"blobid")+Ph++},Lh=function(n,r,o,t){var i,a;0!==r.src.indexOf("blob:")?(i=Bh(r.src).data,(a=n.findFirst(function(e){return e.base64()===i}))?o({image:r,blobInfo:a}):Dh(r.src).then(function(e){a=n.create(Ih(),e,i),n.add(a),o({image:r,blobInfo:a})},function(e){t(e)})):(a=n.getByUri(r.src))?o({image:r,blobInfo:a}):Dh(r.src).then(function(t){Oh(t).then(function(e){i=Bh(e).data,a=n.create(Ih(),t,i),n.add(a),o({image:r,blobInfo:a})})},function(e){t(e)})},Fh=function(e){return e?te(e.getElementsByTagName("img")):[]},Mh=0,zh={uuid:function(e){return e+Mh+++(t=function(){return Math.round(4294967295*Math.random()).toString(36)},"s"+(new Date).getTime().toString(36)+t()+t()+t());var t}};function Uh(u){var n,o,t,e,i,r,a,s,c,l=(n=[],o=function(e){var t,n,r;if(!e.blob||!e.base64)throw new Error("blob and base64 representations of the image are required for BlobInfo to be created");return t=e.id||zh.uuid("blobid"),n=e.name||t,{id:q(t),name:q(n),filename:q(n+"."+(r=e.blob.type,{"image/jpeg":"jpg","image/jpg":"jpg","image/gif":"gif","image/png":"png"}[r.toLowerCase()]||"dat")),blob:q(e.blob),base64:q(e.base64),blobUri:q(e.blobUri||ae.createObjectURL(e.blob)),uri:q(e.uri)}},{create:function(e,t,n,r){if(S(e))return o({id:e,name:r,blob:t,base64:n});if(T(e))return o(e);throw new Error("Unknown input type")},add:function(e){t(e.id())||n.push(e)},get:t=function(t){return e(function(e){return e.id()===t})},getByUri:function(t){return e(function(e){return e.blobUri()===t})},findFirst:e=function(e){return U(n,e)[0]},removeByUri:function(t){n=U(n,function(e){return e.blobUri()!==t||(ae.revokeObjectURL(e.blobUri()),!1)})},destroy:function(){z(n,function(e){ae.revokeObjectURL(e.blobUri())}),n=[]}}),f=(a={},s=function(e,t){return{status:e,resultUri:t}},{hasBlobUri:c=function(e){return e in a},getResultUri:function(e){var t=a[e];return t?t.resultUri:null},isPending:function(e){return!!c(e)&&1===a[e].status},isUploaded:function(e){return!!c(e)&&2===a[e].status},markPending:function(e){a[e]=s(1,null)},markUploaded:function(e,t){a[e]=s(2,t)},removeFailed:function(e){delete a[e]},destroy:function(){a={}}}),d=[],m=function(t){return function(e){return u.selection?t(e):[]}},g=function(e,t,n){for(var r=0;-1!==(r=e.indexOf(t,r))&&(e=e.substring(0,r)+n+e.substr(r+t.length),r+=n.length-t.length+1),-1!==r;);return e},p=function(e,t,n){return e=g(e,'src="'+t+'"','src="'+n+'"'),e=g(e,'data-mce-src="'+t+'"','data-mce-src="'+n+'"')},h=function(t,n){z(u.undoManager.data,function(e){"fragmented"===e.type?e.fragments=W(e.fragments,function(e){return p(e,t,n)}):e.content=p(e.content,t,n)})},v=function(){return u.notificationManager.open({text:u.translate("Image uploading..."),type:"info",timeout:-1,progressBar:!0})},y=function(e,t){l.removeByUri(e.src),h(e.src,t),u.$(e).attr({src:Bl(u)?t+"?"+(new Date).getTime():t,"data-mce-src":u.convertURL(t,"src")})},b=function(n){return i||(i=kh(f,{url:Il(u),basePath:Ll(u),credentials:Fl(u),handler:Ml(u)})),w().then(m(function(r){var e;return e=W(r,function(e){return e.blobInfo}),i.upload(e,v).then(m(function(e){var t=W(e,function(e,t){var n=r[t].image;return e.status&&Pl(u)?y(n,e.url):e.error&&Nh.uploadError(u,e.error),{element:n,status:e.status}});return n&&n(t),t}))}))},C=function(e){if(Ol(u))return b(e)},x=function(t){return!1!==J(d,function(e){return e(t)})&&(0!==t.getAttribute("src").indexOf("data:")||Dl(u)(t))},w=function(){var o,i,a;return r||(o=f,i=l,a={},r={findAll:function(e,n){var t;n||(n=q(!0)),t=U(Fh(e),function(e){var t=e.src;return!!fe.fileApi&&!e.hasAttribute("data-mce-bogus")&&!e.hasAttribute("data-mce-placeholder")&&!(!t||t===fe.transparentSrc)&&(0===t.indexOf("blob:")?!o.isUploaded(t)&&n(e):0===t.indexOf("data:")&&n(e))});var r=W(t,function(n){if(a[n.src])return new de(function(t){a[n.src].then(function(e){if("string"==typeof e)return e;t({image:n,blobInfo:e.blobInfo})})});var e=new de(function(e,t){Lh(i,n,e,t)}).then(function(e){return delete a[e.image.src],e})["catch"](function(e){return delete a[n.src],e});return a[n.src]=e});return de.all(r)}}),r.findAll(u.getBody(),x).then(m(function(e){return e=U(e,function(e){return"string"!=typeof e||(Nh.displayError(u,e),!1)}),z(e,function(e){h(e.image.src,e.blobInfo.blobUri()),e.image.src=e.blobInfo.blobUri(),e.image.removeAttribute("data-mce-src")}),e}))},N=function(e){return e.replace(/src="(blob:[^"]+)"/g,function(e,n){var t=f.getResultUri(n);if(t)return'src="'+t+'"';var r=l.getByUri(n);return r||(r=j(u.editorManager.get(),function(e,t){return e||t.editorUpload&&t.editorUpload.blobCache.getByUri(n)},null)),r?'src="data:'+r.blob().type+";base64,"+r.base64()+'"':e})};return u.on("setContent",function(){Ol(u)?C():w()}),u.on("RawSaveContent",function(e){e.content=N(e.content)}),u.on("getContent",function(e){e.source_view||"raw"===e.format||(e.content=N(e.content))}),u.on("PostRender",function(){u.parser.addNodeFilter("img",function(e){z(e,function(e){var t=e.attr("src");if(!l.getByUri(t)){var n=f.getResultUri(t);n&&e.attr("src",n)}})})}),{blobCache:l,addFilter:function(e){d.push(e)},uploadImages:b,uploadImagesAuto:C,scanForImages:w,destroy:function(){l.destroy(),f.destroy(),r=i=null}}}var jh=function(e,t){return e.hasOwnProperty(t.nodeName)},Vh=function(e,t){if(jo.isText(t)){if(0===t.nodeValue.length)return!0;if(/^\s+$/.test(t.nodeValue)&&(!t.nextSibling||jh(e,t.nextSibling)))return!0}return!1},Hh=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=e.settings,m=e.dom,g=e.selection,p=e.schema,h=p.getBlockElements(),v=g.getStart(),y=e.getBody();if(f=d.forced_root_block,v&&jo.isElement(v)&&f&&(l=y.nodeName.toLowerCase(),p.isValidChild(l,f.toLowerCase())&&(b=h,C=y,x=v,!M(af(ar.fromDom(x),ar.fromDom(C)),function(e){return jh(b,e.dom())})))){var b,C,x,w,N;for(n=(t=g.getRng()).startContainer,r=t.startOffset,o=t.endContainer,i=t.endOffset,c=oh(e),v=y.firstChild;v;)if(w=h,N=v,jo.isText(N)||jo.isElement(N)&&!jh(w,N)&&!yc(N)){if(Vh(h,v)){v=(u=v).nextSibling,m.remove(u);continue}a||(a=m.create(f,e.settings.forced_root_block_attrs),v.parentNode.insertBefore(a,v),s=!0),v=(u=v).nextSibling,a.appendChild(u)}else a=null,v=v.nextSibling;s&&c&&(t.setStart(n,r),t.setEnd(o,i),g.setRng(t),e.nodeChanged())}},qh=function(e){e.settings.forced_root_block&&e.on("NodeChange",d(Hh,e))},$h=function(t){return Yr(t).fold(q([t]),function(e){return[t].concat($h(e))})},Wh=function(t){return Gr(t).fold(q([t]),function(e){return"br"===lr(e)?Hr(e).map(function(e){return[t].concat(Wh(e))}).getOr([]):[t].concat(Wh(e))})},Kh=function(o,e){return ru((i=e,a=i.startContainer,u=i.startOffset,jo.isText(a)?0===u?_.some(ar.fromDom(a)):_.none():_.from(a.childNodes[u]).map(ar.fromDom)),(t=e,n=t.endContainer,r=t.endOffset,jo.isText(n)?r===n.data.length?_.some(ar.fromDom(n)):_.none():_.from(n.childNodes[r-1]).map(ar.fromDom)),function(e,t){var n=X($h(o),d(Mr,e)),r=X(Wh(o),d(Mr,t));return n.isSome()&&r.isSome()}).getOr(!1);var t,n,r,i,a,u},Xh=function(e,t,n,r){var o=n,i=new go(n,o),a=e.schema.getNonEmptyElements();do{if(3===n.nodeType&&0!==Xt.trim(n.nodeValue).length)return void(r?t.setStart(n,0):t.setEnd(n,n.nodeValue.length));if(a[n.nodeName]&&!/^(TD|TH)$/.test(n.nodeName))return void(r?t.setStartBefore(n):"BR"===n.nodeName?t.setEndBefore(n):t.setEndAfter(n));if(fe.ie&&fe.ie<11&&e.isBlock(n)&&e.isEmpty(n))return void(r?t.setStart(n,0):t.setEnd(n,0))}while(n=r?i.next():i.prev());"BODY"===o.nodeName&&(r?t.setStart(o,0):t.setEnd(o,o.childNodes.length))},Yh=function(e){var t=e.selection.getSel();return t&&0<t.rangeCount};function Gh(i){var r,o=[];"onselectionchange"in i.getDoc()||i.on("NodeChange Click MouseUp KeyUp Focus",function(e){var t,n;n={startContainer:(t=i.selection.getRng()).startContainer,startOffset:t.startOffset,endContainer:t.endContainer,endOffset:t.endOffset},"nodechange"!==e.type&&Cg(n,r)||i.fire("SelectionChange"),r=n}),i.on("contextmenu",function(){i.fire("SelectionChange")}),i.on("SelectionChange",function(){var e=i.selection.getStart(!0);!e||!fe.range&&i.selection.isCollapsed()||Yh(i)&&!function(e){var t,n;if((n=i.$(e).parentsUntil(i.getBody()).add(e)).length===o.length){for(t=n.length;0<=t&&n[t]===o[t];t--);if(-1===t)return o=n,!0}return o=n,!1}(e)&&i.dom.isChildOf(e,i.getBody())&&i.nodeChanged({selectionChange:!0})}),i.on("MouseUp",function(e){!e.isDefaultPrevented()&&Yh(i)&&("IMG"===i.selection.getNode().nodeName?he.setEditorTimeout(i,function(){i.nodeChanged()}):i.nodeChanged())}),this.nodeChanged=function(e){var t,n,r,o=i.selection;i.initialized&&o&&!i.settings.disable_nodechange&&!i.readonly&&(r=i.getBody(),(t=o.getStart(!0)||r).ownerDocument===i.getDoc()&&i.dom.isChildOf(t,r)||(t=r),n=[],i.dom.getParent(t,function(e){if(e===r)return!0;n.push(e)}),(e=e||{}).element=t,e.parents=n,i.fire("NodeChange",e))}}var Jh,Qh,Zh={BACKSPACE:8,DELETE:46,DOWN:40,ENTER:13,LEFT:37,RIGHT:39,SPACEBAR:32,TAB:9,UP:38,END:35,HOME:36,modifierPressed:function(e){return e.shiftKey||e.ctrlKey||e.altKey||this.metaKeyPressed(e)},metaKeyPressed:function(e){return fe.mac?e.metaKey:e.ctrlKey&&!e.altKey}},ev=function(e){return j(e,function(e,t){return e.concat(function(t){var e=function(e){return W(e,function(e){return(e=Ka(e)).node=t,e})};if(jo.isElement(t))return e(t.getClientRects());if(jo.isText(t)){var n=t.ownerDocument.createRange();return n.setStart(t,0),n.setEnd(t,t.data.length),e(n.getClientRects())}}(t))},[])};(Qh=Jh||(Jh={}))[Qh.Up=-1]="Up",Qh[Qh.Down=1]="Down";var tv=function(o,i,a,e,u,t){var n,s,c=0,l=[],r=function(e){var t,n,r;for(r=ev([e]),-1===o&&(r=r.reverse()),t=0;t<r.length;t++)if(n=r[t],!a(n,s)){if(0<l.length&&i(n,Ht.last(l))&&c++,n.line=c,u(n))return!0;l.push(n)}};return(s=Ht.last(t.getClientRects()))&&(r(n=t.getNode()),function(e,t,n,r){for(;r=Es(r,e,$a,t);)if(n(r))return}(o,e,r,n)),l},nv=d(tv,Jh.Up,Ga,Ja),rv=d(tv,Jh.Down,Ja,Ga),ov=function(n){return function(e){return t=n,e.line>t;var t}},iv=function(n){return function(e){return t=n,e.line===t;var t}},av=jo.isContentEditableFalse,uv=Es,sv=function(e,t){return Math.abs(e.left-t)},cv=function(e,t){return Math.abs(e.right-t)},lv=function(e,t){return e>=t.left&&e<=t.right},fv=function(e,o){return Ht.reduce(e,function(e,t){var n,r;return n=Math.min(sv(e,o),cv(e,o)),r=Math.min(sv(t,o),cv(t,o)),lv(o,t)?t:lv(o,e)?e:r===n&&av(t.node)?t:r<n?t:e})},dv=function(e,t,n,r){for(;r=uv(r,e,$a,t);)if(n(r))return},mv=function(e,t,n){var r,o,i,a,u,s,c,l=ev(U(te(e.getElementsByTagName("*")),gs)),f=U(l,function(e){return n>=e.top&&n<=e.bottom});return(r=fv(f,t))&&(r=fv((a=e,c=function(t,e){var n;return n=U(ev([e]),function(e){return!t(e,u)}),s=s.concat(n),0===n.length},(s=[]).push(u=r),dv(Jh.Up,a,d(c,Ga),u.node),dv(Jh.Down,a,d(c,Ja),u.node),s),t))&&gs(r.node)?(i=t,{node:(o=r).node,before:sv(o,i)<cv(o,i)}):null},gv=function(t,n,e){if(e.collapsed)return!1;if(fe.ie&&fe.ie<=11&&e.startOffset===e.endOffset-1&&e.startContainer===e.endContainer){var r=e.startContainer.childNodes[e.startOffset];if(jo.isElement(r))return M(r.getClientRects(),function(e){return Qa(e,t,n)})}return M(e.getClientRects(),function(e){return Qa(e,t,n)})},pv=function(e){var t,n,r,o;return o=e.getBoundingClientRect(),n=(t=e.ownerDocument).documentElement,r=t.defaultView,{top:o.top+r.pageYOffset-n.clientTop,left:o.left+r.pageXOffset-n.clientLeft}},hv=function(e,t){return n=(u=e).inline?pv(u.getBody()):{left:0,top:0},a=(i=e).getBody(),r=i.inline?{left:a.scrollLeft,top:a.scrollTop}:{left:0,top:0},{pageX:(o=function(e,t){if(t.target.ownerDocument!==e.getDoc()){var n=pv(e.getContentAreaContainer()),r=(i=(o=e).getBody(),a=o.getDoc().documentElement,u={left:i.scrollLeft,top:i.scrollTop},s={left:i.scrollLeft||a.scrollLeft,top:i.scrollTop||a.scrollTop},o.inline?u:s);return{left:t.pageX-n.left+r.left,top:t.pageY-n.top+r.top}}var o,i,a,u,s;return{left:t.pageX,top:t.pageY}}(e,t)).left-n.left+r.left,pageY:o.top-n.top+r.top};var n,r,o,i,a,u},vv=jo.isContentEditableFalse,yv=jo.isContentEditableTrue,bv=function(e){e&&e.parentNode&&e.parentNode.removeChild(e)},Cv=function(u,s){return function(e){if(0===e.button){var t=X(s.dom.getParents(e.target),au(vv,yv)).getOr(null);if(i=s.getBody(),vv(a=t)&&a!==i){var n=s.dom.getPos(t),r=s.getBody(),o=s.getDoc().documentElement;u.element=t,u.screenX=e.screenX,u.screenY=e.screenY,u.maxX=(s.inline?r.scrollWidth:o.offsetWidth)-2,u.maxY=(s.inline?r.scrollHeight:o.offsetHeight)-2,u.relX=e.pageX-n.x,u.relY=e.pageY-n.y,u.width=t.offsetWidth,u.height=t.offsetHeight,u.ghost=function(e,t,n,r){var o=t.cloneNode(!0);e.dom.setStyles(o,{width:n,height:r}),e.dom.setAttrib(o,"data-mce-selected",null);var i=e.dom.create("div",{"class":"mce-drag-container","data-mce-bogus":"all",unselectable:"on",contenteditable:"false"});return e.dom.setStyles(i,{position:"absolute",opacity:.5,overflow:"hidden",border:0,padding:0,margin:0,width:n,height:r}),e.dom.setStyles(o,{margin:0,boxSizing:"border-box"}),i.appendChild(o),i}(s,t,u.width,u.height)}}var i,a}},xv=function(l,f){return function(e){if(l.dragging&&(s=(i=f).selection,c=s.getSel().getRangeAt(0).startContainer,a=3===c.nodeType?c.parentNode:c,u=l.element,a!==u&&!i.dom.isChildOf(a,u)&&!vv(a))){var t=(r=l.element,(o=r.cloneNode(!0)).removeAttribute("data-mce-selected"),o),n=f.fire("drop",{targetClone:t,clientX:e.clientX,clientY:e.clientY});n.isDefaultPrevented()||(t=n.targetClone,f.undoManager.transact(function(){bv(l.element),f.insertContent(f.dom.getOuterHTML(t)),f._selectionOverrides.hideFakeCaret()}))}var r,o,i,a,u,s,c;wv(l)}},wv=function(e){e.dragging=!1,e.element=null,bv(e.ghost)},Nv=function(e){var t,n,r,o,i,a,p,h,v,u,s,c={};t=Si.DOM,a=V.document,n=Cv(c,e),p=c,h=e,v=he.throttle(function(e,t){h._selectionOverrides.hideFakeCaret(),h.selection.placeCaretAt(e,t)},0),r=function(e){var t,n,r,o,i,a,u,s,c,l,f,d,m=Math.max(Math.abs(e.screenX-p.screenX),Math.abs(e.screenY-p.screenY));if(p.element&&!p.dragging&&10<m){if(h.fire("dragstart",{target:p.element}).isDefaultPrevented())return;p.dragging=!0,h.focus()}if(p.dragging){var g=(f=p,{pageX:(d=hv(h,e)).pageX-f.relX,pageY:d.pageY+5});c=p.ghost,l=h.getBody(),c.parentNode!==l&&l.appendChild(c),t=p.ghost,n=g,r=p.width,o=p.height,i=p.maxX,a=p.maxY,s=u=0,t.style.left=n.pageX+"px",t.style.top=n.pageY+"px",n.pageX+r>i&&(u=n.pageX+r-i),n.pageY+o>a&&(s=n.pageY+o-a),t.style.width=r-u+"px",t.style.height=o-s+"px",v(e.clientX,e.clientY)}},o=xv(c,e),u=c,i=function(){u.dragging&&s.fire("dragend"),wv(u)},(s=e).on("mousedown",n),e.on("mousemove",r),e.on("mouseup",o),t.bind(a,"mousemove",r),t.bind(a,"mouseup",i),e.on("remove",function(){t.unbind(a,"mousemove",r),t.unbind(a,"mouseup",i)})},Ev=function(e){var n;Nv(e),(n=e).on("drop",function(e){var t="undefined"!=typeof e.clientX?n.getDoc().elementFromPoint(e.clientX,e.clientY):null;(vv(t)||vv(n.dom.getContentEditableParent(t)))&&e.preventDefault()})},Sv=function(t){var e=Vi(function(){if(!t.removed&&t.selection.getRng().collapsed){var e=sg(t,t.selection.getRng(),!1);t.selection.setRng(e)}},0);t.on("focus",function(){e.throttle()}),t.on("blur",function(){e.cancel()})},Tv=jo.isContentEditableTrue,kv=jo.isContentEditableFalse,_v=function(e,t){for(var n=e.getBody();t&&t!==n;){if(Tv(t)||kv(t))return t;t=t.parentNode}return null},Av=function(g){var p,e,t,a=g.getBody(),o=ds(g.getBody(),function(e){return g.dom.isBlock(e)},function(){return oh(g)}),h="sel-"+g.dom.uniqueId(),u=function(e){e&&g.selection.setRng(e)},s=function(){return g.selection.getRng()},v=function(e,t,n,r){return void 0===r&&(r=!0),g.fire("ShowCaret",{target:t,direction:e,before:n}).isDefaultPrevented()?null:(r&&g.selection.scrollIntoView(t,-1===e),o.show(n,t))},y=function(e,t){return t=Os(e,a,t),-1===e?_u.fromRangeStart(t):_u.fromRangeEnd(t)},n=function(e){return ka(e)||Oa(e)||Ba(e)},b=function(e){return n(e.startContainer)||n(e.endContainer)},c=function(e){var t=g.schema.getShortEndedElements(),n=g.dom.createRng(),r=e.startContainer,o=e.startOffset,i=e.endContainer,a=e.endOffset;return br(t,r.nodeName.toLowerCase())?0===o?n.setStartBefore(r):n.setStartAfter(r):n.setStart(r,o),br(t,i.nodeName.toLowerCase())?0===a?n.setEndBefore(i):n.setEndAfter(i):n.setEnd(i,a),n},l=function(e,t){var n,r,o,i,a,u,s,c,l,f,d=g.$,m=g.dom;if(!e)return null;if(e.collapsed){if(!b(e))if(!1===t){if(c=y(-1,e),gs(c.getNode(!0)))return v(-1,c.getNode(!0),!1,!1);if(gs(c.getNode()))return v(-1,c.getNode(),!c.isAtEnd(),!1)}else{if(c=y(1,e),gs(c.getNode()))return v(1,c.getNode(),!c.isAtEnd(),!1);if(gs(c.getNode(!0)))return v(1,c.getNode(!0),!1,!1)}return null}return i=e.startContainer,a=e.startOffset,u=e.endOffset,3===i.nodeType&&0===a&&kv(i.parentNode)&&(i=i.parentNode,a=m.nodeIndex(i),i=i.parentNode),1!==i.nodeType?null:(u===a+1&&i===e.endContainer&&(n=i.childNodes[a]),kv(n)?(l=f=n.cloneNode(!0),(s=g.fire("ObjectSelected",{target:n,targetClone:l})).isDefaultPrevented()?null:(r=oa(ar.fromDom(g.getBody()),"#"+h).fold(function(){return d([])},function(e){return d([e.dom()])}),l=s.targetClone,0===r.length&&(r=d('<div data-mce-bogus="all" class="mce-offscreen-selection"></div>').attr("id",h)).appendTo(g.getBody()),e=g.dom.createRng(),l===f&&fe.ie?(r.empty().append('<p style="font-size: 0" data-mce-bogus="all">\xa0</p>').append(l),e.setStartAfter(r[0].firstChild.firstChild),e.setEndAfter(l)):(r.empty().append("\xa0").append(l).append("\xa0"),e.setStart(r[0].firstChild,1),e.setEnd(r[0].lastChild,0)),r.css({top:m.getPos(n,g.getBody()).y}),r[0].focus(),(o=g.selection.getSel()).removeAllRanges(),o.addRange(e),z(Qi(ar.fromDom(g.getBody()),"*[data-mce-selected]"),function(e){Sr(e,"data-mce-selected")}),n.setAttribute("data-mce-selected","1"),p=n,C(),e)):null)},f=function(){p&&(p.removeAttribute("data-mce-selected"),oa(ar.fromDom(g.getBody()),"#"+h).each(Ui),p=null),oa(ar.fromDom(g.getBody()),"#"+h).each(Ui),p=null},C=function(){o.hide()};return fe.ceFalse&&(function(){g.on("mouseup",function(e){var t=s();t.collapsed&&fh(g,e.clientX,e.clientY)&&u(ug(g,t,!1))}),g.on("click",function(e){var t;(t=_v(g,e.target))&&(kv(t)&&(e.preventDefault(),g.focus()),Tv(t)&&g.dom.isChildOf(t,g.selection.getNode())&&f())}),g.on("blur NewBlock",function(){f()}),g.on("ResizeWindow FullscreenStateChanged",function(){return o.reposition()});var n,r,i=function(e,t){var n,r,o=g.dom.getParent(e,g.dom.isBlock),i=g.dom.getParent(t,g.dom.isBlock);return!(!o||!g.dom.isChildOf(o,i)||!1!==kv(_v(g,o)))||o&&(n=o,r=i,!(g.dom.getParent(n,g.dom.isBlock)===g.dom.getParent(r,g.dom.isBlock)))&&function(e){var t=Js(e);if(!e.firstChild)return!1;var n=_u.before(e.firstChild),r=t.next(n);return r&&!Lf(r)&&!Ff(r)}(o)};r=!1,(n=g).on("touchstart",function(){r=!1}),n.on("touchmove",function(){r=!0}),n.on("touchend",function(e){var t=_v(n,e.target);kv(t)&&(r||(e.preventDefault(),l(ag(n,t))))}),g.on("mousedown",function(e){var t,n=e.target;if((n===a||"HTML"===n.nodeName||g.dom.isChildOf(n,a))&&!1!==fh(g,e.clientX,e.clientY))if(t=_v(g,n))kv(t)?(e.preventDefault(),l(ag(g,t))):(f(),Tv(t)&&e.shiftKey||gv(e.clientX,e.clientY,g.selection.getRng())||(C(),g.selection.placeCaretAt(e.clientX,e.clientY)));else if(!1===gs(n)){f(),C();var r=mv(a,e.clientX,e.clientY);if(r&&!i(e.target,r.node)){e.preventDefault();var o=v(1,r.node,r.before,!1);g.getBody().focus(),u(o)}}}),g.on("keypress",function(e){Zh.modifierPressed(e)||(e.keyCode,kv(g.selection.getNode())&&e.preventDefault())}),g.on("getSelectionRange",function(e){var t=e.range;if(p){if(!p.parentNode)return void(p=null);(t=t.cloneRange()).selectNode(p),e.range=t}}),g.on("setSelectionRange",function(e){e.range=c(e.range);var t=l(e.range,e.forward);t&&(e.range=t)}),g.on("AfterSetSelectionRange",function(e){var t,n=e.range;b(n)||"mcepastebin"===n.startContainer.parentNode.id||C(),t=n.startContainer.parentNode,g.dom.hasClass(t,"mce-offscreen-selection")||f()}),g.on("copy",function(e){var t,n=e.clipboardData;if(!e.isDefaultPrevented()&&e.clipboardData&&!fe.ie){var r=(t=g.dom.get(h))?t.getElementsByTagName("*")[0]:t;r&&(e.preventDefault(),n.clearData(),n.setData("text/html",r.outerHTML),n.setData("text/plain",r.outerText))}}),Ev(g),Sv(g)}(),e=g.contentStyles,t=".mce-content-body",e.push(o.getCss()),e.push(t+" .mce-offscreen-selection {position: absolute;left: -9999999999px;max-width: 1000000px;}"+t+" *[contentEditable=false] {cursor: default;}"+t+" *[contentEditable=true] {cursor: text;}")),{showCaret:v,showBlockCaretContainer:function(e){e.hasAttribute("data-mce-caret")&&(Pa(e),u(s()),g.selection.scrollIntoView(e[0]))},hideFakeCaret:C,destroy:function(){o.destroy(),p=null}}},Rv=function(e){for(var t=e;/<!--|--!?>/g.test(t);)t=t.replace(/<!--|--!?>/g,"");return t},Dv=function(e,t,n){var r,o,i,a,u=1;for(a=e.getShortEndedElements(),(i=/<([!?\/])?([A-Za-z0-9\-_\:\.]+)((?:\s+[^"\'>]+(?:(?:"[^"]*")|(?:\'[^\']*\')|[^>]*))*|\/|\s+)>/g).lastIndex=r=n;o=i.exec(t);){if(r=i.lastIndex,"/"===o[1])u--;else if(!o[1]){if(o[2]in a)continue;u++}if(0===u)break}return r},Ov=function(e,t){var n=e.exec(t);if(n){var r=n[1],o=n[2];return"string"==typeof r&&"data-mce-bogus"===r.toLowerCase()?o:null}return null};function Bv(z,U){void 0===U&&(U=di());var e=function(){};!1!==(z=z||{}).fix_self_closing&&(z.fix_self_closing=!0);var j=z.comment?z.comment:e,V=z.cdata?z.cdata:e,H=z.text?z.text:e,q=z.start?z.start:e,$=z.end?z.end:e,W=z.pi?z.pi:e,K=z.doctype?z.doctype:e;return{parse:function(e){var t,n,r,d,o,i,a,m,u,s,g,c,p,l,f,h,v,y,b,C,x,w,N,E,S,T,k,_,A,R=0,D=[],O=0,B=ti.decode,P=Xt.makeMap("src,href,data,background,formaction,poster,xlink:href"),I=/((java|vb)script|mhtml):/i,L=function(e){var t,n;for(t=D.length;t--&&D[t].name!==e;);if(0<=t){for(n=D.length-1;t<=n;n--)(e=D[n]).valid&&$(e.name);D.length=t}},F=function(e,t,n,r,o){var i,a,u,s,c;if(n=(t=t.toLowerCase())in g?t:B(n||r||o||""),p&&!m&&0==(0===(u=t).indexOf("data-")||0===u.indexOf("aria-"))){if(!(i=y[t])&&b){for(a=b.length;a--&&!(i=b[a]).pattern.test(t););-1===a&&(i=null)}if(!i)return;if(i.validValues&&!(n in i.validValues))return}if(P[t]&&!z.allow_script_urls){var l=n.replace(/[\s\u0000-\u001F]+/g,"");try{l=decodeURIComponent(l)}catch(f){l=unescape(l)}if(I.test(l))return;if(c=l,!(s=z).allow_html_data_urls&&(/^data:image\//i.test(c)?!1===s.allow_svg_data_urls&&/^data:image\/svg\+xml/i.test(c):/^data:/i.test(c)))return}m&&(t in P||0===t.indexOf("on"))||(d.map[t]=n,d.push({name:t,value:n}))};for(S=new RegExp("<(?:(?:!--([\\w\\W]*?)--!?>)|(?:!\\[CDATA\\[([\\w\\W]*?)\\]\\]>)|(?:!DOCTYPE([\\w\\W]*?)>)|(?:\\?([^\\s\\/<>]+) ?([\\w\\W]*?)[?/]>)|(?:\\/([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)>)|(?:([A-Za-z][A-Za-z0-9\\-_\\:\\.]*)((?:\\s+[^\"'>]+(?:(?:\"[^\"]*\")|(?:'[^']*')|[^>]*))*|\\/|\\s+)>))","g"),T=/([\w:\-]+)(?:\s*=\s*(?:(?:\"((?:[^\"])*)\")|(?:\'((?:[^\'])*)\')|([^>\s]+)))?/g,s=U.getShortEndedElements(),E=z.self_closing_elements||U.getSelfClosingElements(),g=U.getBoolAttrs(),p=z.validate,u=z.remove_internals,A=z.fix_self_closing,k=U.getSpecialElements(),N=e+">";t=S.exec(N);){if(R<t.index&&H(B(e.substr(R,t.index-R))),n=t[6])":"===(n=n.toLowerCase()).charAt(0)&&(n=n.substr(1)),L(n);else if(n=t[7]){if(t.index+t[0].length>e.length){H(B(e.substr(t.index))),R=t.index+t[0].length;continue}":"===(n=n.toLowerCase()).charAt(0)&&(n=n.substr(1)),c=n in s,A&&E[n]&&0<D.length&&D[D.length-1].name===n&&L(n);var M=Ov(T,t[8]);if(null!==M){if("all"===M){R=Dv(U,e,S.lastIndex),S.lastIndex=R;continue}f=!1}if(!p||(l=U.getElementRule(n))){if(f=!0,p&&(y=l.attributes,b=l.attributePatterns),(v=t[8])?((m=-1!==v.indexOf("data-mce-type"))&&u&&(f=!1),(d=[]).map={},v.replace(T,F)):(d=[]).map={},p&&!m){if(C=l.attributesRequired,x=l.attributesDefault,w=l.attributesForced,l.removeEmptyAttrs&&!d.length&&(f=!1),w)for(o=w.length;o--;)a=(h=w[o]).name,"{$uid}"===(_=h.value)&&(_="mce_"+O++),d.map[a]=_,d.push({name:a,value:_});if(x)for(o=x.length;o--;)(a=(h=x[o]).name)in d.map||("{$uid}"===(_=h.value)&&(_="mce_"+O++),d.map[a]=_,d.push({name:a,value:_}));if(C){for(o=C.length;o--&&!(C[o]in d.map););-1===o&&(f=!1)}if(h=d.map["data-mce-bogus"]){if("all"===h){R=Dv(U,e,S.lastIndex),S.lastIndex=R;continue}f=!1}}f&&q(n,d,c)}else f=!1;if(r=k[n]){r.lastIndex=R=t.index+t[0].length,(t=r.exec(e))?(f&&(i=e.substr(R,t.index-R)),R=t.index+t[0].length):(i=e.substr(R),R=e.length),f&&(0<i.length&&H(i,!0),$(n)),S.lastIndex=R;continue}c||(v&&v.indexOf("/")===v.length-1?f&&$(n):D.push({name:n,valid:f}))}else(n=t[1])?(">"===n.charAt(0)&&(n=" "+n),z.allow_conditional_comments||"[if"!==n.substr(0,3).toLowerCase()||(n=" "+n),j(n)):(n=t[2])?V(Rv(n)):(n=t[3])?K(n):(n=t[4])&&W(n,t[5]);R=t.index+t[0].length}for(R<e.length&&H(B(e.substr(R))),o=D.length-1;0<=o;o--)(n=D[o]).valid&&$(n.name)}}}(Bv||(Bv={})).findEndTag=Dv;var Pv=Bv,Iv=function(e,t){var n,r,o,i,a,u,s,c,l=t,f=/<(\w+) [^>]*data-mce-bogus="all"[^>]*>/g,d=e.schema;for(u=e.getTempAttrs(),s=l,c=new RegExp(["\\s?("+u.join("|")+')="[^"]+"'].join("|"),"gi"),l=s.replace(c,""),a=d.getShortEndedElements();i=f.exec(l);)r=f.lastIndex,o=i[0].length,n=a[i[1]]?r:Pv.findEndTag(d,l,r),l=l.substring(0,r-o)+l.substring(n),f.lastIndex=r-o;return wa(l)},Lv={trimExternal:Iv,trimInternal:Iv},Fv=0,Mv=2,zv=1,Uv=function(g,p){var e=g.length+p.length+2,h=new Array(e),v=new Array(e),c=function(e,t,n,r,o){var i=l(e,t,n,r);if(null===i||i.start===t&&i.diag===t-r||i.end===e&&i.diag===e-n)for(var a=e,u=n;a<t||u<r;)a<t&&u<r&&g[a]===p[u]?(o.push([0,g[a]]),++a,++u):r-n<t-e?(o.push([2,g[a]]),++a):(o.push([1,p[u]]),++u);else{c(e,i.start,n,i.start-i.diag,o);for(var s=i.start;s<i.end;++s)o.push([0,g[s]]);c(i.end,t,i.end-i.diag,r,o)}},y=function(e,t,n,r){for(var o=e;o-t<r&&o<n&&g[o]===p[o-t];)++o;return{start:e,end:o,diag:t}},l=function(e,t,n,r){var o=t-e,i=r-n;if(0===o||0===i)return null;var a,u,s,c,l,f=o-i,d=i+o,m=(d%2==0?d:d+1)/2;for(h[1+m]=e,v[1+m]=t+1,a=0;a<=m;++a){for(u=-a;u<=a;u+=2){for(s=u+m,u===-a||u!==a&&h[s-1]<h[s+1]?h[s]=h[s+1]:h[s]=h[s-1]+1,l=(c=h[s])-e+n-u;c<t&&l<r&&g[c]===p[l];)h[s]=++c,++l;if(f%2!=0&&f-a<=u&&u<=f+a&&v[s-f]<=h[s])return y(v[s-f],u+e-n,t,r)}for(u=f-a;u<=f+a;u+=2){for(s=u+m-f,u===f-a||u!==f+a&&v[s+1]<=v[s-1]?v[s]=v[s+1]-1:v[s]=v[s-1],l=(c=v[s]-1)-e+n-u;e<=c&&n<=l&&g[c]===p[l];)v[s]=c--,l--;if(f%2==0&&-a<=u&&u<=a&&v[s]<=h[s+f])return y(v[s],u+e-n,t,r)}}},t=[];return c(0,g.length,0,p.length,t),t},jv=function(e){return jo.isElement(e)?e.outerHTML:jo.isText(e)?ti.encodeRaw(e.data,!1):jo.isComment(e)?"\x3c!--"+e.data+"--\x3e":""},Vv=function(e,t,n){var r=function(e){var t,n,r;for(r=V.document.createElement("div"),t=V.document.createDocumentFragment(),e&&(r.innerHTML=e);n=r.firstChild;)t.appendChild(n);return t}(t);if(e.hasChildNodes()&&n<e.childNodes.length){var o=e.childNodes[n];o.parentNode.insertBefore(r,o)}else e.appendChild(r)},Hv=function(e){return U(W(te(e.childNodes),jv),function(e){return 0<e.length})},qv=function(e,t){var n,r,o,i=W(te(t.childNodes),jv);return n=Uv(i,e),r=t,o=0,z(n,function(e){e[0]===Fv?o++:e[0]===zv?(Vv(r,e[1],o),o++):e[0]===Mv&&function(e,t){if(e.hasChildNodes()&&t<e.childNodes.length){var n=e.childNodes[t];n.parentNode.removeChild(n)}}(r,o)}),t},$v=Hi(_.none()),Wv=function(e){return{type:"fragmented",fragments:e,content:"",bookmark:null,beforeBookmark:null}},Kv=function(e){return{type:"complete",fragments:null,content:e,bookmark:null,beforeBookmark:null}},Xv=function(e){return"fragmented"===e.type?e.fragments.join(""):e.content},Yv=function(e){var t=ar.fromTag("body",$v.get().getOrThunk(function(){var e=V.document.implementation.createHTMLDocument("undo");return $v.set(_.some(e)),e}));return ya(t,Xv(e)),z(Qi(t,"*[data-mce-bogus]"),ji),t.dom().innerHTML},Gv=function(n){var e,t,r;return e=Hv(n.getBody()),-1!==(t=(r=G(e,function(e){var t=Lv.trimInternal(n.serializer,e);return 0<t.length?[t]:[]})).join("")).indexOf("</iframe>")?Wv(r):Kv(t)},Jv=function(e,t,n){"fragmented"===t.type?qv(t.fragments,e.getBody()):e.setContent(t.content,{format:"raw"}),e.selection.moveToBookmark(n?t.beforeBookmark:t.bookmark)},Qv=function(e,t){return!(!e||!t)&&(r=t,Xv(e)===Xv(r)||(n=t,Yv(e)===Yv(n)));var n,r};function Zv(u){var s,r,o=this,c=0,l=[],t=0,f=function(){return 0===t},i=function(e){f()&&(o.typing=e)},d=function(e){u.setDirty(e)},a=function(e){i(!1),o.add({},e)},n=function(){o.typing&&(i(!1),o.add())};return u.on("init",function(){o.add()}),u.on("BeforeExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&(n(),o.beforeChange())}),u.on("ExecCommand",function(e){var t=e.command;"Undo"!==t&&"Redo"!==t&&"mceRepaint"!==t&&a(e)}),u.on("ObjectResizeStart Cut",function(){o.beforeChange()}),u.on("SaveContent ObjectResized blur",a),u.on("DragEnd",a),u.on("KeyUp",function(e){var t=e.keyCode;e.isDefaultPrevented()||((33<=t&&t<=36||37<=t&&t<=40||45===t||e.ctrlKey)&&(a(),u.nodeChanged()),46!==t&&8!==t||u.nodeChanged(),r&&o.typing&&!1===Qv(Gv(u),l[0])&&(!1===u.isDirty()&&(d(!0),u.fire("change",{level:l[0],lastLevel:null})),u.fire("TypingUndo"),r=!1,u.nodeChanged()))}),u.on("KeyDown",function(e){var t=e.keyCode;if(!e.isDefaultPrevented())if(33<=t&&t<=36||37<=t&&t<=40||45===t)o.typing&&a(e);else{var n=e.ctrlKey&&!e.altKey||e.metaKey;!(t<16||20<t)||224===t||91===t||o.typing||n||(o.beforeChange(),i(!0),o.add({},e),r=!0)}}),u.on("MouseDown",function(e){o.typing&&a(e)}),u.on("input",function(e){var t;e.inputType&&("insertReplacementText"===e.inputType||"insertText"===(t=e).inputType&&null===t.data)&&a(e)}),u.addShortcut("meta+z","","Undo"),u.addShortcut("meta+y,meta+shift+z","","Redo"),u.on("AddUndo Undo Redo ClearUndos",function(e){e.isDefaultPrevented()||u.nodeChanged()}),o={data:l,typing:!1,beforeChange:function(){f()&&(s=Yu.getUndoBookmark(u.selection))},add:function(e,t){var n,r,o,i=u.settings;if(o=Gv(u),e=e||{},e=Xt.extend(e,o),!1===f()||u.removed)return null;if(r=l[c],u.fire("BeforeAddUndo",{level:e,lastLevel:r,originalEvent:t}).isDefaultPrevented())return null;if(r&&Qv(r,e))return null;if(l[c]&&(l[c].beforeBookmark=s),i.custom_undo_redo_levels&&l.length>i.custom_undo_redo_levels){for(n=0;n<l.length-1;n++)l[n]=l[n+1];l.length--,c=l.length}e.bookmark=Yu.getUndoBookmark(u.selection),c<l.length-1&&(l.length=c+1),l.push(e),c=l.length-1;var a={level:e,lastLevel:r,originalEvent:t};return u.fire("AddUndo",a),0<c&&(d(!0),u.fire("change",a)),e},undo:function(){var e;return o.typing&&(o.add(),o.typing=!1,i(!1)),0<c&&(e=l[--c],Jv(u,e,!0),d(!0),u.fire("undo",{level:e})),e},redo:function(){var e;return c<l.length-1&&(e=l[++c],Jv(u,e,!1),d(!0),u.fire("redo",{level:e})),e},clear:function(){l=[],c=0,o.typing=!1,o.data=l,u.fire("ClearUndos")},hasUndo:function(){return 0<c||o.typing&&l[0]&&!Qv(Gv(u),l[0])},hasRedo:function(){return c<l.length-1&&!o.typing},transact:function(e){return n(),o.beforeChange(),o.ignore(e),o.add()},ignore:function(e){try{t++,e()}finally{t--}},extra:function(e,t){var n,r;o.transact(e)&&(r=l[c].bookmark,n=l[c-1],Jv(u,n,!0),o.transact(t)&&(l[c-1].beforeBookmark=r))}}}var ey,ty,ny={},ry=Ht.filter,oy=Ht.each;ty=function(e){var t,n,r=e.selection.getRng();t=jo.matchNodeNames("pre"),r.collapsed||(n=e.selection.getSelectedBlocks(),oy(ry(ry(n,t),function(e){return t(e.previousSibling)&&-1!==Ht.indexOf(n,e.previousSibling)}),function(e){var t,n;t=e.previousSibling,gn(n=e).remove(),gn(t).append("<br><br>").append(n.childNodes)}))},ny[ey="pre"]||(ny[ey]=[]),ny[ey].push(ty);var iy=function(e,t){oy(ny[e],function(e){e(t)})},ay=/^(src|href|style)$/,uy=Xt.each,sy=wc.isEq,cy=function(e,t,n){return e.isChildOf(t,n)&&t!==n&&!e.isBlock(n)},ly=function(e,t,n){var r,o,i;return r=t[n?"startContainer":"endContainer"],o=t[n?"startOffset":"endOffset"],jo.isElement(r)&&(i=r.childNodes.length-1,!n&&o&&o--,r=r.childNodes[i<o?i:o]),jo.isText(r)&&n&&o>=r.nodeValue.length&&(r=new go(r,e.getBody()).next()||r),jo.isText(r)&&!n&&0===o&&(r=new go(r,e.getBody()).prev()||r),r},fy=function(e,t,n,r){var o=e.create(n,r);return t.parentNode.insertBefore(o,t),o.appendChild(t),o},dy=function(e,t,n,r,o){var i=ar.fromDom(t),a=ar.fromDom(e.create(r,o)),u=n?Wr(i):$r(i);return Mi(a,u),n?(Pi(i,a),Li(a,i)):(Ii(i,a),Fi(a,i)),a.dom()},my=function(e,t,n,r){return!(t=wc.getNonWhiteSpaceSibling(t,n,r))||"BR"===t.nodeName||e.isBlock(t)},gy=function(e,n,r,o,i){var t,a,u,s,c,l,f,d,m,g,p,h,v,y,b=e.dom;if(c=b,!(sy(l=o,(f=n).inline)||sy(l,f.block)||(f.selector?jo.isElement(l)&&c.is(l,f.selector):void 0)||(s=o,n.links&&"A"===s.tagName)))return!1;if("all"!==n.remove)for(uy(n.styles,function(e,t){e=wc.normalizeStyleValue(b,wc.replaceVars(e,r),t),"number"==typeof t&&(t=e,i=0),(n.remove_similar||!i||sy(wc.getStyle(b,i,t),e))&&b.setStyle(o,t,""),u=1}),u&&""===b.getAttrib(o,"style")&&(o.removeAttribute("style"),o.removeAttribute("data-mce-style")),uy(n.attributes,function(e,t){var n;if(e=wc.replaceVars(e,r),"number"==typeof t&&(t=e,i=0),!i||sy(b.getAttrib(i,t),e)){if("class"===t&&(e=b.getAttrib(o,t))&&(n="",uy(e.split(/\s+/),function(e){/mce\-\w+/.test(e)&&(n+=(n?" ":"")+e)}),n))return void b.setAttrib(o,t,n);"class"===t&&o.removeAttribute("className"),ay.test(t)&&o.removeAttribute("data-mce-"+t),o.removeAttribute(t)}}),uy(n.classes,function(e){e=wc.replaceVars(e,r),i&&!b.hasClass(i,e)||b.removeClass(o,e)}),a=b.getAttribs(o),t=0;t<a.length;t++){var C=a[t].nodeName;if(0!==C.indexOf("_")&&0!==C.indexOf("data-"))return!1}return"none"!==n.remove?(d=e,g=n,h=(m=o).parentNode,v=d.dom,y=d.settings.forced_root_block,g.block&&(y?h===v.getRoot()&&(g.list_block&&sy(m,g.list_block)||uy(Xt.grep(m.childNodes),function(e){wc.isValid(d,y,e.nodeName.toLowerCase())?p?p.appendChild(e):(p=fy(v,e,y),v.setAttribs(p,d.settings.forced_root_block_attrs)):p=0})):v.isBlock(m)&&!v.isBlock(h)&&(my(v,m,!1)||my(v,m.firstChild,!0,1)||m.insertBefore(v.create("br"),m.firstChild),my(v,m,!0)||my(v,m.lastChild,!1,1)||m.appendChild(v.create("br")))),g.selector&&g.inline&&!sy(g.inline,m)||v.remove(m,1),!0):void 0},py=gy,hy=function(s,c,l,e,f){var t,n,d=s.formatter.get(c),m=d[0],a=!0,u=s.dom,r=s.selection,i=function(e){var n,t,r,o,i,a,u=(n=s,t=e,r=c,o=l,i=f,uy(wc.getParents(n.dom,t.parentNode).reverse(),function(e){var t;a||"_start"===e.id||"_end"===e.id||(t=Mm.matchNode(n,e,r,o,i))&&!1!==t.split&&(a=e)}),a);return function(e,t,n,r,o,i,a,u){var s,c,l,f,d,m,g=e.dom;if(n){for(m=n.parentNode,s=r.parentNode;s&&s!==m;s=s.parentNode){for(c=g.clone(s,!1),d=0;d<t.length;d++)if(gy(e,t[d],u,c,c)){c=0;break}c&&(l&&c.appendChild(l),f||(f=c),l=c)}!i||a.mixed&&g.isBlock(n)||(r=g.split(n,r)),l&&(o.parentNode.insertBefore(l,o),f.appendChild(o))}return r}(s,d,u,e,e,!0,m,l)},g=function(e){var t,n,r,o,i;if(jo.isElement(e)&&u.getContentEditable(e)&&(o=a,a="true"===u.getContentEditable(e),i=!0),t=Xt.grep(e.childNodes),a&&!i)for(n=0,r=d.length;n<r&&!gy(s,d[n],l,e,e);n++);if(m.deep&&t.length){for(n=0,r=t.length;n<r;n++)g(t[n]);i&&(a=o)}},p=function(e){var t,n=u.get(e?"_start":"_end"),r=n[e?"firstChild":"lastChild"];return yc(t=r)&&jo.isElement(t)&&("_start"===t.id||"_end"===t.id)&&(r=r[e?"firstChild":"lastChild"]),jo.isText(r)&&0===r.data.length&&(r=e?n.previousSibling||n.nextSibling:n.nextSibling||n.previousSibling),u.remove(n,!0),r},o=function(e){var t,n,r=e.commonAncestorContainer;if(e=Pc(s,e,d,!0),m.split){if(e=Um(e),(t=ly(s,e,!0))!==(n=ly(s,e))){if(/^(TR|TH|TD)$/.test(t.nodeName)&&t.firstChild&&(t="TR"===t.nodeName?t.firstChild.firstChild||t:t.firstChild||t),r&&/^T(HEAD|BODY|FOOT|R)$/.test(r.nodeName)&&/^(TH|TD)$/.test(n.nodeName)&&n.firstChild&&(n=n.firstChild||n),cy(u,t,n)){var o=_.from(t.firstChild).getOr(t);return i(dy(u,o,!0,"span",{id:"_start","data-mce-type":"bookmark"})),void p(!0)}if(cy(u,n,t))return o=_.from(n.lastChild).getOr(n),i(dy(u,o,!1,"span",{id:"_end","data-mce-type":"bookmark"})),void p(!1);t=fy(u,t,"span",{id:"_start","data-mce-type":"bookmark"}),n=fy(u,n,"span",{id:"_end","data-mce-type":"bookmark"}),i(t),i(n),t=p(!0),n=p()}else t=n=i(t);e.startContainer=t.parentNode?t.parentNode:t,e.startOffset=u.nodeIndex(t),e.endContainer=n.parentNode?n.parentNode:n,e.endOffset=u.nodeIndex(n)+1}Lc(u,e,function(e){uy(e,function(e){g(e),jo.isElement(e)&&"underline"===s.dom.getStyle(e,"text-decoration")&&e.parentNode&&"underline"===wc.getTextDecoration(u,e.parentNode)&&gy(s,{deep:!1,exact:!0,inline:"span",styles:{textDecoration:"underline"}},null,e)})})};if(e)e.nodeType?((n=u.createRng()).setStartBefore(e),n.setEndAfter(e),o(n)):o(e);else if("false"!==u.getContentEditable(r.getNode()))r.isCollapsed()&&m.inline&&!u.select("td[data-mce-selected],th[data-mce-selected]").length?function(e,t,n,r){var o,i,a,u,s,c,l,f=e.dom,d=e.selection,m=[],g=d.getRng();for(o=g.startContainer,i=g.startOffset,3===(s=o).nodeType&&(i!==o.nodeValue.length&&(u=!0),s=s.parentNode);s;){if(Mm.matchNode(e,s,t,n,r)){c=s;break}s.nextSibling&&(u=!0),m.push(s),s=s.parentNode}if(c)if(u){a=d.getBookmark(),g.collapse(!0);var p=Pc(e,g,e.formatter.get(t),!0);p=Um(p),e.formatter.remove(t,n,p),d.moveToBookmark(a)}else{l=Qu(e.getBody(),c);var h=$m(!1).dom(),v=Gm(m,h);Xm(e,h,l||c),Wm(e,l,!1),d.setCursorLocation(v,1),f.isEmpty(c)&&f.remove(c)}}(s,c,l,f):(t=Yu.getPersistentBookmark(s.selection,!0),o(r.getRng()),r.moveToBookmark(t),m.inline&&Mm.match(s,c,l,r.getStart())&&wc.moveStart(u,r,r.getRng()),s.nodeChanged());else{e=r.getNode();for(var h=0,v=d.length;h<v&&(!d[h].ceFalseOverride||!gy(s,d[h],l,e,e));h++);}},vy=Xt.each,yy=function(e){return e&&1===e.nodeType&&!yc(e)&&!Ju(e)&&!jo.isBogus(e)},by=function(e,t){var n;for(n=e;n;n=n[t]){if(3===n.nodeType&&0!==n.nodeValue.length)return e;if(1===n.nodeType&&!yc(n))return n}return e},Cy=function(e,t,n){var r,o,i=new el(e);if(t&&n&&(t=by(t,"previousSibling"),n=by(n,"nextSibling"),i.compare(t,n))){for(r=t.nextSibling;r&&r!==n;)r=(o=r).nextSibling,t.appendChild(o);return e.remove(n),Xt.each(Xt.grep(n.childNodes),function(e){t.appendChild(e)}),t}return n},xy=function(e,t,n){vy(e.childNodes,function(e){yy(e)&&(t(e)&&n(e),e.hasChildNodes()&&xy(e,t,n))})},wy=function(n,e){return d(function(e,t){return!(!t||!wc.getStyle(n,t,e))},e)},Ny=function(r,e,t){return d(function(e,t,n){r.setStyle(n,e,t),""===n.getAttribute("style")&&n.removeAttribute("style"),Ey(r,n)},e,t)},Ey=function(e,t){"SPAN"===t.nodeName&&0===e.getAttribs(t).length&&e.remove(t,!0)},Sy=function(e,t){var n;1===t.nodeType&&t.parentNode&&1===t.parentNode.nodeType&&(n=wc.getTextDecoration(e,t.parentNode),e.getStyle(t,"color")&&n?e.setStyle(t,"text-decoration",n):e.getStyle(t,"text-decoration")===n&&e.setStyle(t,"text-decoration",null))},Ty=function(n,e,r,o){vy(e,function(t){vy(n.dom.select(t.inline,o),function(e){yy(e)&&py(n,t,r,e,t.exact?e:null)}),function(r,e,t){if(e.clear_child_styles){var n=e.links?"*:not(a)":"*";vy(r.select(n,t),function(n){yy(n)&&vy(e.styles,function(e,t){r.setStyle(n,t,"")})})}}(n.dom,t,o)})},ky=function(e,t,n,r){(t.styles.color||t.styles.textDecoration)&&(Xt.walk(r,d(Sy,e),"childNodes"),Sy(e,r))},_y=function(e,t,n,r){t.styles&&t.styles.backgroundColor&&xy(r,wy(e,"fontSize"),Ny(e,"backgroundColor",wc.replaceVars(t.styles.backgroundColor,n)))},Ay=function(e,t,n,r){"sub"!==t.inline&&"sup"!==t.inline||(xy(r,wy(e,"fontSize"),Ny(e,"fontSize","")),e.remove(e.select("sup"===t.inline?"sub":"sup",r),!0))},Ry=function(e,t,n,r){r&&!1!==t.merge_siblings&&(r=Cy(e,wc.getNonWhiteSpaceSibling(r),r),r=Cy(e,r,wc.getNonWhiteSpaceSibling(r,!0)))},Dy=function(t,n,r,o,i){Mm.matchNode(t,i.parentNode,r,o)&&py(t,n,o,i)||n.merge_with_parents&&t.dom.getParent(i.parentNode,function(e){if(Mm.matchNode(t,e,r,o))return py(t,n,o,i),!0})},Oy=Xt.each,By=function(g,p,h,r){var e,t,v=g.formatter.get(p),y=v[0],o=!r&&g.selection.isCollapsed(),i=g.dom,n=g.selection,b=function(n,e){if(e=e||y,n){if(e.onformat&&e.onformat(n,e,h,r),Oy(e.styles,function(e,t){i.setStyle(n,t,wc.replaceVars(e,h))}),e.styles){var t=i.getAttrib(n,"style");t&&n.setAttribute("data-mce-style",t)}Oy(e.attributes,function(e,t){i.setAttrib(n,t,wc.replaceVars(e,h))}),Oy(e.classes,function(e){e=wc.replaceVars(e,h),i.hasClass(n,e)||i.addClass(n,e)})}},C=function(e,t){var n=!1;return!!y.selector&&(Oy(e,function(e){if(!("collapsed"in e&&e.collapsed!==o))return i.is(t,e.selector)&&!Ju(t)?(b(t,e),!(n=!0)):void 0}),n)},a=function(s,e,t,c){var l,f,d=[],m=!0;l=y.inline||y.block,f=s.create(l),b(f),Lc(s,e,function(e){var a,u=function(e){var t,n,r,o;if(o=m,t=e.nodeName.toLowerCase(),n=e.parentNode.nodeName.toLowerCase(),1===e.nodeType&&s.getContentEditable(e)&&(o=m,m="true"===s.getContentEditable(e),r=!0),wc.isEq(t,"br"))return a=0,void(y.block&&s.remove(e));if(y.wrapper&&Mm.matchNode(g,e,p,h))a=0;else{if(m&&!r&&y.block&&!y.wrapper&&wc.isTextBlock(g,t)&&wc.isValid(g,n,l))return e=s.rename(e,l),b(e),d.push(e),void(a=0);if(y.selector){var i=C(v,e);if(!y.inline||i)return void(a=0)}!m||r||!wc.isValid(g,l,t)||!wc.isValid(g,n,l)||!c&&3===e.nodeType&&1===e.nodeValue.length&&65279===e.nodeValue.charCodeAt(0)||Ju(e)||y.inline&&s.isBlock(e)?(a=0,Oy(Xt.grep(e.childNodes),u),r&&(m=o),a=0):(a||(a=s.clone(f,!1),e.parentNode.insertBefore(a,e),d.push(a)),a.appendChild(e))}};Oy(e,u)}),!0===y.links&&Oy(d,function(e){var t=function(e){"A"===e.nodeName&&b(e,y),Oy(Xt.grep(e.childNodes),t)};t(e)}),Oy(d,function(e){var t,n,r,o,i,a=function(e){var n=!1;return Oy(e.childNodes,function(e){if((t=e)&&1===t.nodeType&&!yc(t)&&!Ju(t)&&!jo.isBogus(t))return n=e,!1;var t}),n};n=0,Oy(e.childNodes,function(e){wc.isWhiteSpaceNode(e)||yc(e)||n++}),t=n,!(1<d.length)&&s.isBlock(e)||0!==t?(y.inline||y.wrapper)&&(y.exact||1!==t||((o=a(r=e))&&!yc(o)&&Mm.matchName(s,o,y)&&(i=s.clone(o,!1),b(i),s.replace(i,r,!0),s.remove(o,1)),e=i||r),Ty(g,v,h,e),Dy(g,y,p,h,e),_y(s,y,h,e),Ay(s,y,h,e),Ry(s,y,h,e)):s.remove(e,1)})};if("false"!==i.getContentEditable(n.getNode())){if(y){if(r)r.nodeType?C(v,r)||((t=i.createRng()).setStartBefore(r),t.setEndAfter(r),a(i,Pc(g,t,v),0,!0)):a(i,r,0,!0);else if(o&&y.inline&&!i.select("td[data-mce-selected],th[data-mce-selected]").length)!function(e,t,n){var r,o,i,a,u,s,c=e.selection;a=(r=c.getRng(!0)).startOffset,s=r.startContainer.nodeValue,(o=Qu(e.getBody(),c.getStart()))&&(i=qm(o));var l,f,d=/[^\s\u00a0\u00ad\u200b\ufeff]/;s&&0<a&&a<s.length&&d.test(s.charAt(a))&&d.test(s.charAt(a-1))?(u=c.getBookmark(),r.collapse(!0),r=Pc(e,r,e.formatter.get(t)),r=Um(r),e.formatter.apply(t,n,r),c.moveToBookmark(u)):(o&&i.nodeValue===jm||(l=e.getDoc(),f=$m(!0).dom(),i=(o=l.importNode(f,!0)).firstChild,r.insertNode(o),a=1),e.formatter.apply(t,n,o),c.setCursorLocation(i,a))}(g,p,h);else{var u=g.selection.getNode();g.settings.forced_root_block||!v[0].defaultBlock||i.getParent(u,i.isBlock)||By(g,v[0].defaultBlock),g.selection.setRng(cl(g.selection.getRng())),e=Yu.getPersistentBookmark(g.selection,!0),a(i,Pc(g,n.getRng(),v)),y.styles&&ky(i,y,h,u),n.moveToBookmark(e),wc.moveStart(i,n,n.getRng()),g.nodeChanged()}iy(p,g)}}else{r=n.getNode();for(var s=0,c=v.length;s<c;s++)if(v[s].ceFalseOverride&&i.is(r,v[s].selector))return void b(r,v[s])}},Py={applyFormat:By},Iy=Xt.each,Ly=function(e,t,n,r,o){var i,a,u,s,c,l,f,d;null===t.get()&&(a=e,u={},(i=t).set({}),a.on("NodeChange",function(n){var r=wc.getParents(a.dom,n.element),o={};r=Xt.grep(r,function(e){return 1===e.nodeType&&!e.getAttribute("data-mce-bogus")}),Iy(i.get(),function(e,n){Iy(r,function(t){return a.formatter.matchNode(t,n,{},e.similar)?(u[n]||(Iy(e,function(e){e(!0,{node:t,format:n,parents:r})}),u[n]=e),o[n]=e,!1):!Mm.matchesUnInheritedFormatSelector(a,t,n)&&void 0})}),Iy(u,function(e,t){o[t]||(delete u[t],Iy(e,function(e){e(!1,{node:n.element,format:t,parents:r})}))})})),c=n,l=r,f=o,d=(s=t).get(),Iy(c.split(","),function(e){d[e]||(d[e]=[],d[e].similar=f),d[e].push(l)}),s.set(d)},Fy={get:function(r){var t={valigntop:[{selector:"td,th",styles:{verticalAlign:"top"}}],valignmiddle:[{selector:"td,th",styles:{verticalAlign:"middle"}}],valignbottom:[{selector:"td,th",styles:{verticalAlign:"bottom"}}],alignleft:[{selector:"figure.image",collapsed:!1,classes:"align-left",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"left"},inherit:!1,preview:!1,defaultBlock:"div"},{selector:"img,table",collapsed:!1,styles:{"float":"left"},preview:"font-family font-size"}],aligncenter:[{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"center"},inherit:!1,preview:"font-family font-size",defaultBlock:"div"},{selector:"figure.image",collapsed:!1,classes:"align-center",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"img",collapsed:!1,styles:{display:"block",marginLeft:"auto",marginRight:"auto"},preview:!1},{selector:"table",collapsed:!1,styles:{marginLeft:"auto",marginRight:"auto"},preview:"font-family font-size"}],alignright:[{selector:"figure.image",collapsed:!1,classes:"align-right",ceFalseOverride:!0,preview:"font-family font-size"},{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"right"},inherit:!1,preview:"font-family font-size",defaultBlock:"div"},{selector:"img,table",collapsed:!1,styles:{"float":"right"},preview:"font-family font-size"}],alignjustify:[{selector:"figure,p,h1,h2,h3,h4,h5,h6,td,th,tr,div,ul,ol,li",styles:{textAlign:"justify"},inherit:!1,defaultBlock:"div",preview:"font-family font-size"}],bold:[{inline:"strong",remove:"all"},{inline:"span",styles:{fontWeight:"bold"}},{inline:"b",remove:"all"}],italic:[{inline:"em",remove:"all"},{inline:"span",styles:{fontStyle:"italic"}},{inline:"i",remove:"all"}],underline:[{inline:"span",styles:{textDecoration:"underline"},exact:!0},{inline:"u",remove:"all"}],strikethrough:[{inline:"span",styles:{textDecoration:"line-through"},exact:!0},{inline:"strike",remove:"all"}],forecolor:{inline:"span",styles:{color:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0},hilitecolor:{inline:"span",styles:{backgroundColor:"%value"},links:!0,remove_similar:!0,clear_child_styles:!0},fontname:{inline:"span",toggle:!1,styles:{fontFamily:"%value"},clear_child_styles:!0},fontsize:{inline:"span",toggle:!1,styles:{fontSize:"%value"},clear_child_styles:!0},fontsize_class:{inline:"span",attributes:{"class":"%value"}},blockquote:{block:"blockquote",wrapper:1,remove:"all"},subscript:{inline:"sub"},superscript:{inline:"sup"},code:{inline:"code"},link:{inline:"a",selector:"a",remove:"all",split:!0,deep:!0,onmatch:function(){return!0},onformat:function(n,e,t){Xt.each(t,function(e,t){r.setAttrib(n,t,e)})}},removeformat:[{selector:"b,strong,em,i,font,u,strike,sub,sup,dfn,code,samp,kbd,var,cite,mark,q,del,ins",remove:"all",split:!0,expand:!1,block_expand:!0,deep:!0},{selector:"span",attributes:["style","class"],remove:"empty",split:!0,expand:!1,deep:!0},{selector:"*",attributes:["style","class"],split:!1,expand:!1,deep:!0}]};return Xt.each("p h1 h2 h3 h4 h5 h6 div address pre div dt dd samp".split(/\s/),function(e){t[e]={block:e,remove:"all"}}),t}},My=Xt.each,zy=Si.DOM,Uy=function(e,t){var n,o,r,m=t&&t.schema||di({}),g=function(e){var t,n,r;return o="string"==typeof e?{name:e,classes:[],attrs:{}}:e,t=zy.create(o.name),n=t,(r=o).classes.length&&zy.addClass(n,r.classes.join(" ")),zy.setAttribs(n,r.attrs),t},p=function(n,e,t){var r,o,i,a,u,s,c,l,f=0<e.length&&e[0],d=f&&f.name;if(u=d,s="string"!=typeof(a=n)?a.nodeName.toLowerCase():a,c=m.getElementRule(s),i=!(!(l=c&&c.parentsRequired)||!l.length)&&(u&&-1!==Xt.inArray(l,u)?u:l[0]))d===i?(o=e[0],e=e.slice(1)):o=i;else if(f)o=e[0],e=e.slice(1);else if(!t)return n;return o&&(r=g(o)).appendChild(n),t&&(r||(r=zy.create("div")).appendChild(n),Xt.each(t,function(e){var t=g(e);r.insertBefore(t,n)})),p(r,e,o&&o.siblings)};return e&&e.length?(o=e[0],n=g(o),(r=zy.create("div")).appendChild(p(n,e.slice(1),o.siblings)),r):""},jy=function(e){var t,a={classes:[],attrs:{}};return"*"!==(e=a.selector=Xt.trim(e))&&(t=e.replace(/(?:([#\.]|::?)([\w\-]+)|(\[)([^\]]+)\]?)/g,function(e,t,n,r,o){switch(t){case"#":a.attrs.id=n;break;case".":a.classes.push(n);break;case":":-1!==Xt.inArray("checked disabled enabled read-only required".split(" "),n)&&(a.attrs[n]=n)}if("["===r){var i=o.match(/([\w\-]+)(?:\=\"([^\"]+))?/);i&&(a.attrs[i[1]]=i[2])}return""})),a.name=t||"div",a},Vy=function(e){return e&&"string"==typeof e?(e=(e=e.split(/\s*,\s*/)[0]).replace(/\s*(~\+|~|\+|>)\s*/g,"$1"),Xt.map(e.split(/(?:>|\s+(?![^\[\]]+\]))/),function(e){var t=Xt.map(e.split(/(?:~\+|~|\+)/),jy),n=t.pop();return t.length&&(n.siblings=t),n}).reverse()):[]},Hy=function(n,e){var t,r,o,i,a,u,s="";if(!1===(u=n.settings.preview_styles))return"";"string"!=typeof u&&(u="font-family font-size font-weight font-style text-decoration text-transform color background-color border border-radius outline text-shadow");var c=function(e){return e.replace(/%(\w+)/g,"")};if("string"==typeof e){if(!(e=n.formatter.get(e)))return;e=e[0]}return"preview"in e&&!1===(u=e.preview)?"":(t=e.block||e.inline||"span",(i=Vy(e.selector)).length?(i[0].name||(i[0].name=t),t=e.selector,r=Uy(i,n)):r=Uy([t],n),o=zy.select(t,r)[0]||r.firstChild,My(e.styles,function(e,t){(e=c(e))&&zy.setStyle(o,t,e)}),My(e.attributes,function(e,t){(e=c(e))&&zy.setAttrib(o,t,e)}),My(e.classes,function(e){e=c(e),zy.hasClass(o,e)||zy.addClass(o,e)}),n.fire("PreviewFormats"),zy.setStyles(r,{position:"absolute",left:-65535}),n.getBody().appendChild(r),a=zy.getStyle(n.getBody(),"fontSize",!0),a=/px$/.test(a)?parseInt(a,10):0,My(u.split(" "),function(e){var t=zy.getStyle(o,e,!0);if(!("background-color"===e&&/transparent|rgba\s*\([^)]+,\s*0\)/.test(t)&&(t=zy.getStyle(n.getBody(),e,!0),"#ffffff"===zy.toHex(t).toLowerCase())||"color"===e&&"#000000"===zy.toHex(t).toLowerCase())){if("font-size"===e&&/em|%$/.test(t)){if(0===a)return;t=parseFloat(t)/(/%$/.test(t)?100:1)*a+"px"}"border"===e&&t&&(s+="padding:0 2px;"),s+=e+":"+t+";"}}),n.fire("AfterPreviewFormats"),zy.remove(r),s)},qy=function(e,t,n,r,o){var i=t.get(n);!Mm.match(e,n,r,o)||"toggle"in i[0]&&!i[0].toggle?Py.applyFormat(e,n,r,o):hy(e,n,r,o)},$y=function(e){e.addShortcut("meta+b","","Bold"),e.addShortcut("meta+i","","Italic"),e.addShortcut("meta+u","","Underline");for(var t=1;t<=6;t++)e.addShortcut("access+"+t,"",["FormatBlock",!1,"h"+t]);e.addShortcut("access+7","",["FormatBlock",!1,"p"]),e.addShortcut("access+8","",["FormatBlock",!1,"div"]),e.addShortcut("access+9","",["FormatBlock",!1,"address"])};function Wy(e){var t,n,r,o=(t=e,n={},(r=function(e,t){e&&("string"!=typeof e?Xt.each(e,function(e,t){r(t,e)}):(t=t.length?t:[t],Xt.each(t,function(e){"undefined"==typeof e.deep&&(e.deep=!e.selector),"undefined"==typeof e.split&&(e.split=!e.selector||e.inline),"undefined"==typeof e.remove&&e.selector&&!e.inline&&(e.remove="none"),e.selector&&e.inline&&(e.mixed=!0,e.block_expand=!0),"string"==typeof e.classes&&(e.classes=e.classes.split(/\s+/))}),n[e]=t))})(Fy.get(t.dom)),r(t.settings.formats),{get:function(e){return e?n[e]:n},register:r,unregister:function(e){return e&&n[e]&&delete n[e],n}}),i=Hi(null);return $y(e),Jm(e),{get:o.get,register:o.register,unregister:o.unregister,apply:d(Py.applyFormat,e),remove:d(hy,e),toggle:d(qy,e,o),match:d(Mm.match,e),matchAll:d(Mm.matchAll,e),matchNode:d(Mm.matchNode,e),canApply:d(Mm.canApply,e),formatChanged:d(Ly,e,i),getCssText:d(Hy,e)}}var Ky,Xy=Object.prototype.hasOwnProperty,Yy=(Ky=function(e,t){return t},function(){for(var e=new Array(arguments.length),t=0;t<e.length;t++)e[t]=arguments[t];if(0===e.length)throw new Error("Can't merge zero objects");for(var n={},r=0;r<e.length;r++){var o=e[r];for(var i in o)Xy.call(o,i)&&(n[i]=Ky(n[i],o[i]))}return n}),Gy={register:function(t,s,c){t.addAttributeFilter("data-mce-tabindex",function(e,t){for(var n,r=e.length;r--;)(n=e[r]).attr("tabindex",n.attributes.map["data-mce-tabindex"]),n.attr(t,null)}),t.addAttributeFilter("src,href,style",function(e,t){for(var n,r,o=e.length,i="data-mce-"+t,a=s.url_converter,u=s.url_converter_scope;o--;)(r=(n=e[o]).attributes.map[i])!==undefined?(n.attr(t,0<r.length?r:null),n.attr(i,null)):(r=n.attributes.map[t],"style"===t?r=c.serializeStyle(c.parseStyle(r),n.name):a&&(r=a.call(u,r,t,n.name)),n.attr(t,0<r.length?r:null))}),t.addAttributeFilter("class",function(e){for(var t,n,r=e.length;r--;)(n=(t=e[r]).attr("class"))&&(n=t.attr("class").replace(/(?:^|\s)mce-item-\w+(?!\S)/g,""),t.attr("class",0<n.length?n:null))}),t.addAttributeFilter("data-mce-type",function(e,t,n){for(var r,o=e.length;o--;)"bookmark"!==(r=e[o]).attributes.map["data-mce-type"]||n.cleanup||(_.from(r.firstChild).exists(function(e){return!Ca(e.value)})?r.unwrap():r.remove())}),t.addNodeFilter("noscript",function(e){for(var t,n=e.length;n--;)(t=e[n].firstChild)&&(t.value=ti.decode(t.value))}),t.addNodeFilter("script,style",function(e,t){for(var n,r,o,i=e.length,a=function(e){return e.replace(/(<!--\[CDATA\[|\]\]-->)/g,"\n").replace(/^[\r\n]*|[\r\n]*$/g,"").replace(/^\s*((<!--)?(\s*\/\/)?\s*<!\[CDATA\[|(<!--\s*)?\/\*\s*<!\[CDATA\[\s*\*\/|(\/\/)?\s*<!--|\/\*\s*<!--\s*\*\/)\s*[\r\n]*/gi,"").replace(/\s*(\/\*\s*\]\]>\s*\*\/(-->)?|\s*\/\/\s*\]\]>(-->)?|\/\/\s*(-->)?|\]\]>|\/\*\s*-->\s*\*\/|\s*-->\s*)\s*$/g,"")};i--;)r=(n=e[i]).firstChild?n.firstChild.value:"","script"===t?((o=n.attr("type"))&&n.attr("type","mce-no/type"===o?null:o.replace(/^mce\-/,"")),"xhtml"===s.element_format&&0<r.length&&(n.firstChild.value="// <![CDATA[\n"+a(r)+"\n// ]]>")):"xhtml"===s.element_format&&0<r.length&&(n.firstChild.value="\x3c!--\n"+a(r)+"\n--\x3e")}),t.addNodeFilter("#comment",function(e){for(var t,n=e.length;n--;)0===(t=e[n]).value.indexOf("[CDATA[")?(t.name="#cdata",t.type=4,t.value=t.value.replace(/^\[CDATA\[|\]\]$/g,"")):0===t.value.indexOf("mce:protected ")&&(t.name="#text",t.type=3,t.raw=!0,t.value=unescape(t.value).substr(14))}),t.addNodeFilter("xml:namespace,input",function(e,t){for(var n,r=e.length;r--;)7===(n=e[r]).type?n.remove():1===n.type&&("input"!==t||"type"in n.attributes.map||n.attr("type","text"))}),t.addAttributeFilter("data-mce-type",function(e){z(e,function(e){"format-caret"===e.attr("data-mce-type")&&(e.isEmpty(t.schema.getNonEmptyElements())?e.remove():e.unwrap())})}),t.addAttributeFilter("data-mce-src,data-mce-href,data-mce-style,data-mce-selected,data-mce-expando,data-mce-type,data-mce-resize",function(e,t){for(var n=e.length;n--;)e[n].attr(t,null)})},trimTrailingBr:function(e){var t,n,r=function(e){return e&&"br"===e.name};r(t=e.lastChild)&&r(n=t.prev)&&(t.remove(),n.remove())}},Jy={process:function(e,t,n){return f=n,(l=e)&&l.hasEventListeners("PreProcess")&&!f.no_events?(o=t,i=n,c=(r=e).dom,o=o.cloneNode(!0),(a=V.document.implementation).createHTMLDocument&&(u=a.createHTMLDocument(""),Xt.each("BODY"===o.nodeName?o.childNodes:[o],function(e){u.body.appendChild(u.importNode(e,!0))}),o="BODY"!==o.nodeName?u.body.firstChild:u.body,s=c.doc,c.doc=u),vp(r,Yy(i,{node:o})),s&&(c.doc=s),o):t;var r,o,i,a,u,s,c,l,f}},Qy=function(e,a,u){e.addNodeFilter("font",function(e){z(e,function(e){var t,n=a.parse(e.attr("style")),r=e.attr("color"),o=e.attr("face"),i=e.attr("size");r&&(n.color=r),o&&(n["font-family"]=o),i&&(n["font-size"]=u[parseInt(e.attr("size"),10)-1]),e.name="span",e.attr("style",a.serialize(n)),t=e,z(["color","face","size"],function(e){t.attr(e,null)})})})},Zy=function(e,t){var n,r=gi();t.convert_fonts_to_spans&&Qy(e,r,Xt.explode(t.font_size_legacy_values)),n=r,e.addNodeFilter("strike",function(e){z(e,function(e){var t=n.parse(e.attr("style"));t["text-decoration"]="line-through",e.name="span",e.attr("style",n.serialize(t))})})},eb={register:function(e,t){t.inline_styles&&Zy(e,t)}},tb=/^[ \t\r\n]*$/,nb={"#text":3,"#comment":8,"#cdata":4,"#pi":7,"#doctype":10,"#document-fragment":11},rb=function(e,t,n){var r,o,i=n?"lastChild":"firstChild",a=n?"prev":"next";if(e[i])return e[i];if(e!==t){if(r=e[a])return r;for(o=e.parent;o&&o!==t;o=o.parent)if(r=o[a])return r}},ob=function(){function a(e,t){this.name=e,1===(this.type=t)&&(this.attributes=[],this.attributes.map={})}return a.create=function(e,t){var n,r;if(n=new a(e,nb[e]||1),t)for(r in t)n.attr(r,t[r]);return n},a.prototype.replace=function(e){return e.parent&&e.remove(),this.insert(e,this),this.remove(),this},a.prototype.attr=function(e,t){var n,r;if("string"!=typeof e){for(r in e)this.attr(r,e[r]);return this}if(n=this.attributes){if(t!==undefined){if(null===t){if(e in n.map)for(delete n.map[e],r=n.length;r--;)if(n[r].name===e)return n=n.splice(r,1),this;return this}if(e in n.map){for(r=n.length;r--;)if(n[r].name===e){n[r].value=t;break}}else n.push({name:e,value:t});return n.map[e]=t,this}return n.map[e]}},a.prototype.clone=function(){var e,t,n,r,o,i=new a(this.name,this.type);if(n=this.attributes){for((o=[]).map={},e=0,t=n.length;e<t;e++)"id"!==(r=n[e]).name&&(o[o.length]={name:r.name,value:r.value},o.map[r.name]=r.value);i.attributes=o}return i.value=this.value,i.shortEnded=this.shortEnded,i},a.prototype.wrap=function(e){return this.parent.insert(e,this),e.append(this),this},a.prototype.unwrap=function(){var e,t;for(e=this.firstChild;e;)t=e.next,this.insert(e,this,!0),e=t;this.remove()},a.prototype.remove=function(){var e=this.parent,t=this.next,n=this.prev;return e&&(e.firstChild===this?(e.firstChild=t)&&(t.prev=null):n.next=t,e.lastChild===this?(e.lastChild=n)&&(n.next=null):t.prev=n,this.parent=this.next=this.prev=null),this},a.prototype.append=function(e){var t;return e.parent&&e.remove(),(t=this.lastChild)?((t.next=e).prev=t,this.lastChild=e):this.lastChild=this.firstChild=e,e.parent=this,e},a.prototype.insert=function(e,t,n){var r;return e.parent&&e.remove(),r=t.parent||this,n?(t===r.firstChild?r.firstChild=e:t.prev.next=e,e.prev=t.prev,(e.next=t).prev=e):(t===r.lastChild?r.lastChild=e:t.next.prev=e,e.next=t.next,(e.prev=t).next=e),e.parent=r,e},a.prototype.getAll=function(e){var t,n=[];for(t=this.firstChild;t;t=rb(t,this))t.name===e&&n.push(t);return n},a.prototype.empty=function(){var e,t,n;if(this.firstChild){for(e=[],n=this.firstChild;n;n=rb(n,this))e.push(n);for(t=e.length;t--;)(n=e[t]).parent=n.firstChild=n.lastChild=n.next=n.prev=null}return this.firstChild=this.lastChild=null,this},a.prototype.isEmpty=function(e,t,n){var r,o,i=this.firstChild;if(t=t||{},i)do{if(1===i.type){if(i.attributes.map["data-mce-bogus"])continue;if(e[i.name])return!1;for(r=i.attributes.length;r--;)if("name"===(o=i.attributes[r].name)||0===o.indexOf("data-mce-bookmark"))return!1}if(8===i.type)return!1;if(3===i.type&&!tb.test(i.value))return!1;if(3===i.type&&i.parent&&t[i.parent.name]&&tb.test(i.value))return!1;if(n&&n(i))return!1}while(i=rb(i,this));return!0},a.prototype.walk=function(e){return rb(this,null,e)},a}(),ib=function(e,t,n,r){(e.padd_empty_with_br||t.insert)&&n[r.name]?r.empty().append(new ob("br",1)).shortEnded=!0:r.empty().append(new ob("#text",3)).value="\xa0"},ab=function(e){return ub(e,"#text")&&"\xa0"===e.firstChild.value},ub=function(e,t){return e&&e.firstChild&&e.firstChild===e.lastChild&&e.firstChild.name===t},sb=function(r,e,t,n){return n.isEmpty(e,t,function(e){return t=e,(n=r.getElementRule(t.name))&&n.paddEmpty;var t,n})},cb=function(e,t){return e&&(t[e.name]||"br"===e.name)},lb=function(e,p){var h=e.schema;p.remove_trailing_brs&&e.addNodeFilter("br",function(e,t,n){var r,o,i,a,u,s,c,l,f=e.length,d=Xt.extend({},h.getBlockElements()),m=h.getNonEmptyElements(),g=h.getNonEmptyElements();for(d.body=1,r=0;r<f;r++)if(i=(o=e[r]).parent,d[o.parent.name]&&o===i.lastChild){for(u=o.prev;u;){if("span"!==(s=u.name)||"bookmark"!==u.attr("data-mce-type")){if("br"!==s)break;if("br"===s){o=null;break}}u=u.prev}o&&(o.remove(),sb(h,m,g,i)&&(c=h.getElementRule(i.name))&&(c.removeEmpty?i.remove():c.paddEmpty&&ib(p,n,d,i)))}else{for(a=o;i&&i.firstChild===a&&i.lastChild===a&&!d[(a=i).name];)i=i.parent;a===i&&!0!==p.padd_empty_with_br&&((l=new ob("#text",3)).value="\xa0",o.replace(l))}}),e.addAttributeFilter("href",function(e){var t,n,r,o=e.length;if(!p.allow_unsafe_link_target)for(;o--;)"a"===(t=e[o]).name&&"_blank"===t.attr("target")&&t.attr("rel",(n=t.attr("rel"),r=n?Xt.trim(n):"",/\b(noopener)\b/g.test(r)?r:r.split(" ").filter(function(e){return 0<e.length}).concat(["noopener"]).sort().join(" ")))}),p.allow_html_in_named_anchor||e.addAttributeFilter("id,name",function(e){for(var t,n,r,o,i=e.length;i--;)if("a"===(o=e[i]).name&&o.firstChild&&!o.attr("href"))for(r=o.parent,t=o.lastChild;n=t.prev,r.insert(t,o),t=n;);}),p.fix_list_elements&&e.addNodeFilter("ul,ol",function(e){for(var t,n,r=e.length;r--;)if("ul"===(n=(t=e[r]).parent).name||"ol"===n.name)if(t.prev&&"li"===t.prev.name)t.prev.append(t);else{var o=new ob("li",1);o.attr("style","list-style-type: none"),t.wrap(o)}}),p.validate&&h.getValidClasses()&&e.addAttributeFilter("class",function(e){for(var t,n,r,o,i,a,u,s=e.length,c=h.getValidClasses();s--;){for(n=(t=e[s]).attr("class").split(" "),i="",r=0;r<n.length;r++)o=n[r],u=!1,(a=c["*"])&&a[o]&&(u=!0),a=c[t.name],!u&&a&&a[o]&&(u=!0),u&&(i&&(i+=" "),i+=o);i.length||(i=null),t.attr("class",i)}})},fb=Xt.makeMap,db=Xt.each,mb=Xt.explode,gb=Xt.extend;function pb(T,k){void 0===k&&(k=di());var _={},A=[],R={},D={};(T=T||{}).validate=!("validate"in T)||T.validate,T.root_name=T.root_name||"body";var O=function(e){var t,n,r;(n=e.name)in _&&((r=R[n])?r.push(e):R[n]=[e]),t=A.length;for(;t--;)(n=A[t].name)in e.attributes.map&&((r=D[n])?r.push(e):D[n]=[e]);return e},e={schema:k,addAttributeFilter:function(e,n){db(mb(e),function(e){var t;for(t=0;t<A.length;t++)if(A[t].name===e)return void A[t].callbacks.push(n);A.push({name:e,callbacks:[n]})})},getAttributeFilters:function(){return[].concat(A)},addNodeFilter:function(e,n){db(mb(e),function(e){var t=_[e];t||(_[e]=t=[]),t.push(n)})},getNodeFilters:function(){var e=[];for(var t in _)_.hasOwnProperty(t)&&e.push({name:t,callbacks:_[t]});return e},filterNode:O,parse:function(e,a){var t,n,r,o,i,u,s,c,l,f,d,m=[];a=a||{},R={},D={},l=gb(fb("script,style,head,html,body,title,meta,param"),k.getBlockElements());var g=k.getNonEmptyElements(),p=k.children,h=T.validate,v="forced_root_block"in a?a.forced_root_block:T.forced_root_block,y=k.getWhiteSpaceElements(),b=/^[ \t\r\n]+/,C=/[ \t\r\n]+$/,x=/[ \t\r\n]+/g,w=/^[ \t\r\n]+$/;f=y.hasOwnProperty(a.context)||y.hasOwnProperty(T.root_name);var N=function(e,t){var n,r=new ob(e,t);return e in _&&((n=R[e])?n.push(r):R[e]=[r]),r},E=function(e){var t,n,r,o,i=k.getBlockElements();for(t=e.prev;t&&3===t.type;){if(0<(r=t.value.replace(C,"")).length)return void(t.value=r);if(n=t.next){if(3===n.type&&n.value.length){t=t.prev;continue}if(!i[n.name]&&"script"!==n.name&&"style"!==n.name){t=t.prev;continue}}o=t.prev,t.remove(),t=o}};t=Pv({validate:h,allow_script_urls:T.allow_script_urls,allow_conditional_comments:T.allow_conditional_comments,self_closing_elements:function(e){var t,n={};for(t in e)"li"!==t&&"p"!==t&&(n[t]=e[t]);return n}(k.getSelfClosingElements()),cdata:function(e){d.append(N("#cdata",4)).value=e},text:function(e,t){var n;f||(e=e.replace(x," "),cb(d.lastChild,l)&&(e=e.replace(b,""))),0!==e.length&&((n=N("#text",3)).raw=!!t,d.append(n).value=e)},comment:function(e){d.append(N("#comment",8)).value=e},pi:function(e,t){d.append(N(e,7)).value=t,E(d)},doctype:function(e){d.append(N("#doctype",10)).value=e,E(d)},start:function(e,t,n){var r,o,i,a,u;if(i=h?k.getElementRule(e):{}){for((r=N(i.outputName||e,1)).attributes=t,r.shortEnded=n,d.append(r),(u=p[d.name])&&p[r.name]&&!u[r.name]&&m.push(r),o=A.length;o--;)(a=A[o].name)in t.map&&((s=D[a])?s.push(r):D[a]=[r]);l[e]&&E(r),n||(d=r),!f&&y[e]&&(f=!0)}},end:function(e){var t,n,r,o,i;if(n=h?k.getElementRule(e):{}){if(l[e]&&!f){if((t=d.firstChild)&&3===t.type)if(0<(r=t.value.replace(b,"")).length)t.value=r,t=t.next;else for(o=t.next,t.remove(),t=o;t&&3===t.type;)r=t.value,o=t.next,(0===r.length||w.test(r))&&(t.remove(),t=o),t=o;if((t=d.lastChild)&&3===t.type)if(0<(r=t.value.replace(C,"")).length)t.value=r,t=t.prev;else for(o=t.prev,t.remove(),t=o;t&&3===t.type;)r=t.value,o=t.prev,(0===r.length||w.test(r))&&(t.remove(),t=o),t=o}if(f&&y[e]&&(f=!1),n.removeEmpty&&sb(k,g,y,d)&&!d.attributes.map.name&&!d.attr("id"))return i=d.parent,l[d.name]?d.empty().remove():d.unwrap(),void(d=i);n.paddEmpty&&(ab(d)||sb(k,g,y,d))&&ib(T,a,l,d),d=d.parent}}},k);var S=d=new ob(a.context||T.root_name,11);if(t.parse(e),h&&m.length&&(a.context?a.invalid=!0:function(e){var t,n,r,o,i,a,u,s,c,l,f,d,m,g,p,h;for(d=fb("tr,td,th,tbody,thead,tfoot,table"),l=k.getNonEmptyElements(),f=k.getWhiteSpaceElements(),m=k.getTextBlockElements(),g=k.getSpecialElements(),t=0;t<e.length;t++)if((n=e[t]).parent&&!n.fixed)if(m[n.name]&&"li"===n.parent.name){for(p=n.next;p&&m[p.name];)p.name="li",p.fixed=!0,n.parent.insert(p,n.parent),p=p.next;n.unwrap(n)}else{for(o=[n],r=n.parent;r&&!k.isValidChild(r.name,n.name)&&!d[r.name];r=r.parent)o.push(r);if(r&&1<o.length){for(o.reverse(),i=a=O(o[0].clone()),c=0;c<o.length-1;c++){for(k.isValidChild(a.name,o[c].name)?(u=O(o[c].clone()),a.append(u)):u=a,s=o[c].firstChild;s&&s!==o[c+1];)h=s.next,u.append(s),s=h;a=u}sb(k,l,f,i)?r.insert(n,o[0],!0):(r.insert(i,o[0],!0),r.insert(n,i)),r=o[0],(sb(k,l,f,r)||ub(r,"br"))&&r.empty().remove()}else if(n.parent){if("li"===n.name){if((p=n.prev)&&("ul"===p.name||"ul"===p.name)){p.append(n);continue}if((p=n.next)&&("ul"===p.name||"ul"===p.name)){p.insert(n,p.firstChild,!0);continue}n.wrap(O(new ob("ul",1)));continue}k.isValidChild(n.parent.name,"div")&&k.isValidChild("div",n.name)?n.wrap(O(new ob("div",1))):g[n.name]?n.empty().remove():n.unwrap()}}}(m)),v&&("body"===S.name||a.isRootContent)&&function(){var e,t,n=S.firstChild,r=function(e){e&&((n=e.firstChild)&&3===n.type&&(n.value=n.value.replace(b,"")),(n=e.lastChild)&&3===n.type&&(n.value=n.value.replace(C,"")))};if(k.isValidChild(S.name,v.toLowerCase())){for(;n;)e=n.next,3===n.type||1===n.type&&"p"!==n.name&&!l[n.name]&&!n.attr("data-mce-type")?(t||((t=N(v,1)).attr(T.forced_root_block_attrs),S.insert(t,n)),t.append(n)):(r(t),t=null),n=e;r(t)}}(),!a.invalid){for(c in R){for(s=_[c],i=(n=R[c]).length;i--;)n[i].parent||n.splice(i,1);for(r=0,o=s.length;r<o;r++)s[r](n,c,a)}for(r=0,o=A.length;r<o;r++)if((s=A[r]).name in D){for(i=(n=D[s.name]).length;i--;)n[i].parent||n.splice(i,1);for(i=0,u=s.callbacks.length;i<u;i++)s.callbacks[i](n,s.name,a)}}return S}};return lb(e,T),eb.register(e,T),e}var hb=function(e,t,n){-1===Xt.inArray(t,n)&&(e.addAttributeFilter(n,function(e,t){for(var n=e.length;n--;)e[n].attr(t,null)}),t.push(n))},vb=function(e,t,n){var r=wa(n.getInner?t.innerHTML:e.getOuterHTML(t));return n.selection||Ao(ar.fromDom(t))?r:Xt.trim(r)},yb=function(e,t,n){var r=n.selection?Yy({forced_root_block:!1},n):n,o=e.parse(t,r);return Gy.trimTrailingBr(o),o},bb=function(e,t,n,r,o){var i,a,u,s,c=(i=r,al(t,n).serialize(i));return a=e,s=c,!(u=o).no_events&&a?yp(a,Yy(u,{content:s})).content:s};function Cb(e,t){var a,u,s,c,l,n,r=(a=e,n=["data-mce-selected"],s=(u=t)&&u.dom?u.dom:Si.DOM,c=u&&u.schema?u.schema:di(a),a.entity_encoding=a.entity_encoding||"named",a.remove_trailing_brs=!("remove_trailing_brs"in a)||a.remove_trailing_brs,l=pb(a,c),Gy.register(l,a,s),{schema:c,addNodeFilter:l.addNodeFilter,addAttributeFilter:l.addAttributeFilter,serialize:function(e,t){var n=Yy({format:"html"},t||{}),r=Jy.process(u,e,n),o=vb(s,r,n),i=yb(l,o,n);return"tree"===n.format?i:bb(u,a,c,i,n)},addRules:function(e){c.addValidElements(e)},setRules:function(e){c.setValidElements(e)},addTempAttr:d(hb,l,n),getTempAttrs:function(){return n}});return{schema:r.schema,addNodeFilter:r.addNodeFilter,addAttributeFilter:r.addAttributeFilter,serialize:r.serialize,addRules:r.addRules,setRules:r.setRules,addTempAttr:r.addTempAttr,getTempAttrs:r.getTempAttrs}}function xb(e){return{getBookmark:d(hc,e),moveToBookmark:d(vc,e)}}(xb||(xb={})).isBookmarkNode=yc;var wb,Nb,Eb=xb,Sb=jo.isContentEditableFalse,Tb=jo.isContentEditableTrue,kb=function(r,a){var u,s,c,l,f,d,m,g,p,h,v,y,i,b,C,x,w,N=a.dom,E=Xt.each,S=a.getDoc(),T=V.document,k=Math.abs,_=Math.round,A=a.getBody();l={nw:[0,0,-1,-1],ne:[1,0,1,-1],se:[1,1,1,1],sw:[0,1,-1,1]};var e=".mce-content-body";a.contentStyles.push(e+" div.mce-resizehandle {position: absolute;border: 1px solid black;box-sizing: content-box;background: #FFF;width: 7px;height: 7px;z-index: 10000}"+e+" .mce-resizehandle:hover {background: #000}"+e+" img[data-mce-selected],"+e+" hr[data-mce-selected] {outline: 1px solid black;resize: none}"+e+" .mce-clonedresizable {position: absolute;"+(fe.gecko?"":"outline: 1px dashed black;")+"opacity: .5;filter: alpha(opacity=50);z-index: 10000}"+e+" .mce-resize-helper {background: #555;background: rgba(0,0,0,0.75);border-radius: 3px;border: 1px;color: white;display: none;font-family: sans-serif;font-size: 12px;white-space: nowrap;line-height: 14px;margin: 5px 10px;padding: 5px;position: absolute;z-index: 10001}");var R=function(e){return e&&("IMG"===e.nodeName||a.dom.is(e,"figure.image"))},n=function(e){var t,n,r=e.target;t=e,n=a.selection.getRng(),!R(t.target)||gv(t.clientX,t.clientY,n)||e.isDefaultPrevented()||a.selection.select(r)},D=function(e){return a.dom.is(e,"figure.image")?e.querySelector("img"):e},O=function(e){var t=a.settings.object_resizing;return!1!==t&&!fe.iOS&&("string"!=typeof t&&(t="table,img,figure.image,div"),"false"!==e.getAttribute("data-mce-resize")&&e!==a.getBody()&&Lr(ar.fromDom(e),t))},B=function(e){var t,n,r,o;t=e.screenX-d,n=e.screenY-m,b=t*f[2]+h,C=n*f[3]+v,b=b<5?5:b,C=C<5?5:C,(R(u)&&!1!==a.settings.resize_img_proportional?!Zh.modifierPressed(e):Zh.modifierPressed(e)||R(u)&&f[2]*f[3]!=0)&&(k(t)>k(n)?(C=_(b*y),b=_(C/y)):(b=_(C/y),C=_(b*y))),N.setStyles(D(s),{width:b,height:C}),r=0<(r=f.startPos.x+t)?r:0,o=0<(o=f.startPos.y+n)?o:0,N.setStyles(c,{left:r,top:o,display:"block"}),c.innerHTML=b+" × "+C,f[2]<0&&s.clientWidth<=b&&N.setStyle(s,"left",g+(h-b)),f[3]<0&&s.clientHeight<=C&&N.setStyle(s,"top",p+(v-C)),(t=A.scrollWidth-x)+(n=A.scrollHeight-w)!=0&&N.setStyles(c,{left:r-t,top:o-n}),i||(wp(a,u,h,v),i=!0)},P=function(){i=!1;var e=function(e,t){t&&(u.style[e]||!a.schema.isValid(u.nodeName.toLowerCase(),e)?N.setStyle(D(u),e,t):N.setAttrib(D(u),e,t))};e("width",b),e("height",C),N.unbind(S,"mousemove",B),N.unbind(S,"mouseup",P),T!==S&&(N.unbind(T,"mousemove",B),N.unbind(T,"mouseup",P)),N.remove(s),N.remove(c),o(u),Np(a,u,b,C),N.setAttrib(u,"style",N.getAttrib(u,"style")),a.nodeChanged()},o=function(e){var t,r,o,n,i;I(),M(),t=N.getPos(e,A),g=t.x,p=t.y,i=e.getBoundingClientRect(),r=i.width||i.right-i.left,o=i.height||i.bottom-i.top,u!==e&&(u=e,b=C=0),n=a.fire("ObjectSelected",{target:e}),O(e)&&!n.isDefaultPrevented()?E(l,function(n,e){var t;(t=N.get("mceResizeHandle"+e))&&N.remove(t),t=N.add(A,"div",{id:"mceResizeHandle"+e,"data-mce-bogus":"all","class":"mce-resizehandle",unselectable:!0,style:"cursor:"+e+"-resize; margin:0; padding:0"}),11===fe.ie&&(t.contentEditable=!1),N.bind(t,"mousedown",function(e){var t;e.stopImmediatePropagation(),e.preventDefault(),d=(t=e).screenX,m=t.screenY,h=D(u).clientWidth,v=D(u).clientHeight,y=v/h,(f=n).startPos={x:r*n[0]+g,y:o*n[1]+p},x=A.scrollWidth,w=A.scrollHeight,s=u.cloneNode(!0),N.addClass(s,"mce-clonedresizable"),N.setAttrib(s,"data-mce-bogus","all"),s.contentEditable=!1,s.unSelectabe=!0,N.setStyles(s,{left:g,top:p,margin:0}),s.removeAttribute("data-mce-selected"),A.appendChild(s),N.bind(S,"mousemove",B),N.bind(S,"mouseup",P),T!==S&&(N.bind(T,"mousemove",B),N.bind(T,"mouseup",P)),c=N.add(A,"div",{"class":"mce-resize-helper","data-mce-bogus":"all"},h+" × "+v)}),n.elm=t,N.setStyles(t,{left:r*n[0]+g-t.offsetWidth/2,top:o*n[1]+p-t.offsetHeight/2})}):I(),u.setAttribute("data-mce-selected","1")},I=function(){var e,t;for(e in M(),u&&u.removeAttribute("data-mce-selected"),l)(t=N.get("mceResizeHandle"+e))&&(N.unbind(t),N.remove(t))},L=function(e){var t,n=function(e,t){if(e)do{if(e===t)return!0}while(e=e.parentNode)};i||a.removed||(E(N.select("img[data-mce-selected],hr[data-mce-selected]"),function(e){e.removeAttribute("data-mce-selected")}),t="mousedown"===e.type?e.target:r.getNode(),n(t=N.$(t).closest("table,img,figure.image,hr")[0],A)&&(z(),n(r.getStart(!0),t)&&n(r.getEnd(!0),t))?o(t):I())},F=function(e){return Sb(function(e,t){for(;t&&t!==e;){if(Tb(t)||Sb(t))return t;t=t.parentNode}return null}(a.getBody(),e))},M=function(){for(var e in l){var t=l[e];t.elm&&(N.unbind(t.elm),delete t.elm)}},z=function(){try{a.getDoc().execCommand("enableObjectResizing",!1,!1)}catch(e){}};return a.on("init",function(){z(),fe.ie&&11<=fe.ie&&(a.on("mousedown click",function(e){var t=e.target,n=t.nodeName;i||!/^(TABLE|IMG|HR)$/.test(n)||F(t)||(2!==e.button&&a.selection.select(t,"TABLE"===n),"mousedown"===e.type&&a.nodeChanged())}),a.dom.bind(A,"mscontrolselect",function(e){var t=function(e){he.setEditorTimeout(a,function(){a.selection.select(e)})};if(F(e.target))return e.preventDefault(),void t(e.target);/^(TABLE|IMG|HR)$/.test(e.target.nodeName)&&(e.preventDefault(),"IMG"===e.target.tagName&&t(e.target))}));var t=he.throttle(function(e){a.composing||L(e)});a.on("nodechange ResizeEditor ResizeWindow drop FullscreenStateChanged",t),a.on("keyup compositionend",function(e){u&&"TABLE"===u.nodeName&&t(e)}),a.on("hide blur",I),a.on("contextmenu",n)}),a.on("remove",M),{isResizable:O,showResizeRect:o,hideResizeRect:I,updateResizeRect:L,destroy:function(){u=s=null}}},_b=function(e){for(var t=0,n=0,r=e;r&&r.nodeType;)t+=r.offsetLeft||0,n+=r.offsetTop||0,r=r.offsetParent;return{x:t,y:n}},Ab=function(e,t,n){var r,o,i,a,u,s=e.dom,c=s.getRoot(),l=0;if(u={elm:t,alignToTop:n},e.fire("scrollIntoView",u),!u.isDefaultPrevented()&&jo.isElement(t)){if(!1===n&&(l=t.offsetHeight),"BODY"!==c.nodeName){var f=e.selection.getScrollContainer();if(f)return r=_b(t).y-_b(f).y+l,a=f.clientHeight,void((r<(i=f.scrollTop)||i+a<r+25)&&(f.scrollTop=r<i?r:r-a+25))}o=s.getViewPort(e.getWin()),r=s.getPos(t).y+l,i=o.y,a=o.h,(r<o.y||i+a<r+25)&&e.getWin().scrollTo(0,r<i?r:r-a+25)}},Rb=function(d,e){Z(Su.fromRangeStart(e).getClientRects()).each(function(e){var t,n,r,o,i,a,u,s,c,l=function(e){if(e.inline)return e.getBody().getBoundingClientRect();var t=e.getWin();return{left:0,right:t.innerWidth,top:0,bottom:t.innerHeight,width:t.innerWidth,height:t.innerHeight}}(d),f={x:(i=t=l,a=n=e,a.left>i.left&&a.right<i.right?0:a.left<i.left?a.left-i.left:a.right-i.right),y:(r=t,o=n,o.top>r.top&&o.bottom<r.bottom?0:o.top<r.top?o.top-r.top:o.bottom-r.bottom)};s=0!==f.x?0<f.x?f.x+4:f.x-4:0,c=0!==f.y?0<f.y?f.y+4:f.y-4:0,(u=d).inline?(u.getBody().scrollLeft+=s,u.getBody().scrollTop+=c):u.getWin().scrollBy(s,c)})},Db=function(e){return jo.isContentEditableTrue(e)||jo.isContentEditableFalse(e)},Ob=function(e,t,n){var r,o,i,a,u,s=n;if(s.caretPositionFromPoint)(o=s.caretPositionFromPoint(e,t))&&((r=n.createRange()).setStart(o.offsetNode,o.offset),r.collapse(!0));else if(n.caretRangeFromPoint)r=n.caretRangeFromPoint(e,t);else if(s.body.createTextRange){r=s.body.createTextRange();try{r.moveToPoint(e,t),r.collapse(!0)}catch(c){r=function(e,n,t){var r,o,i;if(r=t.elementFromPoint(e,n),o=t.body.createTextRange(),r&&"HTML"!==r.tagName||(r=t.body),o.moveToElementText(r),0<(i=(i=Xt.toArray(o.getClientRects())).sort(function(e,t){return(e=Math.abs(Math.max(e.top-n,e.bottom-n)))-(t=Math.abs(Math.max(t.top-n,t.bottom-n)))})).length){n=(i[0].bottom+i[0].top)/2;try{return o.moveToPoint(e,n),o.collapse(!0),o}catch(a){}}return null}(e,t,n)}return i=r,a=n.body,u=i&&i.parentElement?i.parentElement():null,jo.isContentEditableFalse(function(e,t,n){for(;e&&e!==t;){if(n(e))return e;e=e.parentNode}return null}(u,a,Db))?null:i}return r},Bb=function(n,e){return W(e,function(e){var t=n.fire("GetSelectionRange",{range:e});return t.range!==e?t.range:e})},Pb=function(e,t){var n=(t||V.document).createDocumentFragment();return z(e,function(e){n.appendChild(e.dom())}),ar.fromDom(n)},Ib=Ar("element","width","rows"),Lb=Ar("element","cells"),Fb=Ar("x","y"),Mb=function(e,t){var n=parseInt(Er(e,t),10);return isNaN(n)?1:n},zb=function(e){return j(e,function(e,t){return t.cells().length>e?t.cells().length:e},0)},Ub=function(e,t){for(var n=e.rows(),r=0;r<n.length;r++)for(var o=n[r].cells(),i=0;i<o.length;i++)if(Mr(o[i],t))return _.some(Fb(i,r));return _.none()},jb=function(e,t,n,r,o){for(var i=[],a=e.rows(),u=n;u<=o;u++){var s=a[u].cells(),c=t<r?s.slice(t,r+1):s.slice(r,t+1);i.push(Lb(a[u].element(),c))}return i},Vb=function(e){var o=Ib(ha(e),0,[]);return z(Qi(e,"tr"),function(n,r){z(Qi(n,"td,th"),function(e,t){!function(e,t,n,r,o){for(var i=Mb(o,"rowspan"),a=Mb(o,"colspan"),u=e.rows(),s=n;s<n+i;s++){u[s]||(u[s]=Lb(va(r),[]));for(var c=t;c<t+a;c++)u[s].cells()[c]=s===n&&c===t?o:ha(o)}}(o,function(e,t,n){for(;r=t,o=n,i=void 0,((i=e.rows())[o]?i[o].cells():[])[r];)t++;var r,o,i;return t}(o,t,r),r,n,e)})}),Ib(o.element(),zb(o.rows()),o.rows())},Hb=function(e){return n=W((t=e).rows(),function(e){var t=W(e.cells(),function(e){var t=va(e);return Sr(t,"colspan"),Sr(t,"rowspan"),t}),n=ha(e.element());return Mi(n,t),n}),r=ha(t.element()),o=ar.fromTag("tbody"),Mi(o,n),Fi(r,o),r;var t,n,r,o},qb=function(l,e,t){return Ub(l,e).bind(function(c){return Ub(l,t).map(function(e){return t=l,r=e,o=(n=c).x(),i=n.y(),a=r.x(),u=r.y(),s=i<u?jb(t,o,i,a,u):jb(t,o,u,a,i),Ib(t.element(),zb(s),s);var t,n,r,o,i,a,u,s})})},$b=function(n,t){return X(n,function(e){return"li"===lr(e)&&Kh(e,t)}).fold(q([]),function(e){return(t=n,X(t,function(e){return"ul"===lr(e)||"ol"===lr(e)})).map(function(e){return[ar.fromTag("li"),ar.fromTag(lr(e))]}).getOr([]);var t})},Wb=function(e,t){var n,r=ar.fromDom(t.commonAncestorContainer),o=uf(r,e),i=U(o,function(e){return xo(e)||bo(e)}),a=$b(o,t),u=i.concat(a.length?a:So(n=r)?Vr(n).filter(Eo).fold(q([]),function(e){return[n,e]}):Eo(n)?[n]:[]);return W(u,ha)},Kb=function(){return Pb([])},Xb=function(e,t){return n=ar.fromDom(t.cloneContents()),r=Wb(e,t),o=j(r,function(e,t){return Fi(t,e),t},n),0<r.length?Pb([o]):o;var n,r,o},Yb=function(e,o){return(t=e,n=o[0],ra(n,"table",d(Mr,t))).bind(function(e){var t=o[0],n=o[o.length-1],r=Vb(e);return qb(r,t,n).map(function(e){return Pb([Hb(e)])})}).getOrThunk(Kb);var t,n},Gb=function(e,t){var n,r,o=Cm(t,e);return 0<o.length?Yb(e,o):(n=e,0<(r=t).length&&r[0].collapsed?Kb():Xb(n,r[0]))},Jb=function(e,t){if(void 0===t&&(t={}),t.get=!0,t.format=t.format||"html",t.selection=!0,(t=e.fire("BeforeGetContent",t)).isDefaultPrevented())return e.fire("GetContent",t),t.content;if("text"===t.format)return c=e,_.from(c.selection.getRng()).map(function(e){var t=c.dom.add(c.getBody(),"div",{"data-mce-bogus":"all",style:"overflow: hidden; opacity: 0;"},e.cloneContents()),n=wa(t.innerText);return c.dom.remove(t),n}).getOr("");t.getInner=!0;var n,r,o,i,a,u,s,c,l=(r=t,i=(n=e).selection.getRng(),a=n.dom.create("body"),u=n.selection.getSel(),s=Bb(n,gm(u)),(o=r.contextual?Gb(ar.fromDom(n.getBody()),s).dom():i.cloneContents())&&a.appendChild(o),n.selection.serializer.serialize(a,r));return"tree"===t.format?l:(t.content=e.selection.isCollapsed()?"":l,e.fire("GetContent",t),t.content)},Qb=function(e,t,n){var r,o,i,a=e.selection.getRng(),u=e.getDoc();if((n=n||{format:"html"}).set=!0,n.selection=!0,n.content=t,n.no_events||!(n=e.fire("BeforeSetContent",n)).isDefaultPrevented()){if(t=n.content,a.insertNode){t+='<span id="__caret">_</span>',a.startContainer===u&&a.endContainer===u?u.body.innerHTML=t:(a.deleteContents(),0===u.body.childNodes.length?u.body.innerHTML=t:a.createContextualFragment?a.insertNode(a.createContextualFragment(t)):(o=u.createDocumentFragment(),i=u.createElement("div"),o.appendChild(i),i.outerHTML=t,a.insertNode(o))),r=e.dom.get("__caret"),(a=u.createRange()).setStartBefore(r),a.setEndBefore(r),e.selection.setRng(a),e.dom.remove("__caret");try{e.selection.setRng(a)}catch(s){}}else a.item&&(u.execCommand("Delete",!1,null),a=e.getRng()),/^\s+/.test(t)?(a.pasteHTML('<span id="__mce_tmp">_</span>'+t),e.dom.remove("__mce_tmp")):a.pasteHTML(t);n.no_events||e.fire("SetContent",n)}else e.fire("SetContent",n)},Zb=function(e,t,n,r,o){var i=n?t.startContainer:t.endContainer,a=n?t.startOffset:t.endOffset;return _.from(i).map(ar.fromDom).map(function(e){return r&&t.collapsed?e:Xr(e,o(e,a)).getOr(e)}).bind(function(e){return dr(e)?_.some(e):Vr(e)}).map(function(e){return e.dom()}).getOr(e)},eC=function(e,t,n){return Zb(e,t,!0,n,function(e,t){return Math.min(e.dom().childNodes.length,t)})},tC=function(e,t,n){return Zb(e,t,!1,n,function(e,t){return 0<t?t-1:t})},nC=function(e,t){for(var n=e;e&&jo.isText(e)&&0===e.length;)e=t?e.nextSibling:e.previousSibling;return e||n},rC=Xt.each,oC=function(e){return!!e.select},iC=function(e){return!(!e||!e.ownerDocument)&&zr(ar.fromDom(e.ownerDocument),ar.fromDom(e))},aC=function(u,s,e,c){var n,t,l,f,a,r=function(e,t){return Qb(c,e,t)},o=function(e){var t=m();t.collapse(!!e),i(t)},d=function(){return s.getSelection?s.getSelection():s.document.selection},m=function(){var e,t,n,r,o=function(e,t,n){try{return t.compareBoundaryPoints(e,n)}catch(r){return-1}};if(!s)return null;if(null==(r=s.document))return null;if(c.bookmark!==undefined&&!1===oh(c)){var i=rp(c);if(i.isSome())return i.map(function(e){return Bb(c,[e])[0]}).getOr(r.createRange())}try{(e=d())&&!jo.isRestrictedNode(e.anchorNode)&&(t=0<e.rangeCount?e.getRangeAt(0):e.createRange?e.createRange():r.createRange())}catch(a){}return(t=Bb(c,[t])[0])||(t=r.createRange?r.createRange():r.body.createTextRange()),t.setStart&&9===t.startContainer.nodeType&&t.collapsed&&(n=u.getRoot(),t.setStart(n,0),t.setEnd(n,0)),l&&f&&(0===o(t.START_TO_START,t,l)&&0===o(t.END_TO_END,t,l)?t=f:f=l=null),t},i=function(e,t){var n,r;if((o=e)&&(oC(o)||iC(o.startContainer)&&iC(o.endContainer))){var o,i=oC(e)?e:null;if(i){f=null;try{i.select()}catch(a){}}else{if(n=d(),e=c.fire("SetSelectionRange",{range:e,forward:t}).range,n){f=e;try{n.removeAllRanges(),n.addRange(e)}catch(a){}!1===t&&n.extend&&(n.collapse(e.endContainer,e.endOffset),n.extend(e.startContainer,e.startOffset)),l=0<n.rangeCount?n.getRangeAt(0):null}e.collapsed||e.startContainer!==e.endContainer||!n.setBaseAndExtent||fe.ie||e.endOffset-e.startOffset<2&&e.startContainer.hasChildNodes()&&(r=e.startContainer.childNodes[e.startOffset])&&"IMG"===r.tagName&&(n.setBaseAndExtent(e.startContainer,e.startOffset,e.endContainer,e.endOffset),n.anchorNode===e.startContainer&&n.focusNode===e.endContainer||n.setBaseAndExtent(r,0,r,1)),c.fire("AfterSetSelectionRange",{range:e,forward:t})}}},g=function(){var e,t,n=d();return!(n&&n.anchorNode&&n.focusNode)||((e=u.createRng()).setStart(n.anchorNode,n.anchorOffset),e.collapse(!0),(t=u.createRng()).setStart(n.focusNode,n.focusOffset),t.collapse(!0),e.compareBoundaryPoints(e.START_TO_START,t)<=0)},p={bookmarkManager:null,controlSelection:null,dom:u,win:s,serializer:e,editor:c,collapse:o,setCursorLocation:function(e,t){var n=u.createRng();e?(n.setStart(e,t),n.setEnd(e,t),i(n),o(!1)):(Xh(u,n,c.getBody(),!0),i(n))},getContent:function(e){return Jb(c,e)},setContent:r,getBookmark:function(e,t){return n.getBookmark(e,t)},moveToBookmark:function(e){return n.moveToBookmark(e)},select:function(e,t){var r,n,o;return(r=u,n=e,o=t,_.from(n).map(function(e){var t=r.nodeIndex(e),n=r.createRng();return n.setStart(e.parentNode,t),n.setEnd(e.parentNode,t+1),o&&(Xh(r,n,e,!0),Xh(r,n,e,!1)),n})).each(i),e},isCollapsed:function(){var e=m(),t=d();return!(!e||e.item)&&(e.compareEndPoints?0===e.compareEndPoints("StartToEnd",e):!t||e.collapsed)},isForward:g,setNode:function(e){return r(u.getOuterHTML(e)),e},getNode:function(){return e=c.getBody(),(t=m())?(r=t.startContainer,o=t.endContainer,i=t.startOffset,a=t.endOffset,n=t.commonAncestorContainer,!t.collapsed&&(r===o&&a-i<2&&r.hasChildNodes()&&(n=r.childNodes[i]),3===r.nodeType&&3===o.nodeType&&(r=r.length===i?nC(r.nextSibling,!0):r.parentNode,o=0===a?nC(o.previousSibling,!1):o.parentNode,r&&r===o))?r:n&&3===n.nodeType?n.parentNode:n):e;var e,t,n,r,o,i,a},getSel:d,setRng:i,getRng:m,getStart:function(e){return eC(c.getBody(),m(),e)},getEnd:function(e){return tC(c.getBody(),m(),e)},getSelectedBlocks:function(e,t){return function(e,t,n,r){var o,i,a=[];if(i=e.getRoot(),n=e.getParent(n||eC(i,t,t.collapsed),e.isBlock),r=e.getParent(r||tC(i,t,t.collapsed),e.isBlock),n&&n!==i&&a.push(n),n&&r&&n!==r)for(var u=new go(o=n,i);(o=u.next())&&o!==r;)e.isBlock(o)&&a.push(o);return r&&n!==r&&r!==i&&a.push(r),a}(u,m(),e,t)},normalize:function(){var e=m(),t=d();if(!hm(t)&&Yh(c)){var n=kg(u,e);return n.each(function(e){i(e,g())}),n.getOr(e)}return e},selectorChanged:function(e,t){var i;return a||(a={},i={},c.on("NodeChange",function(e){var n=e.element,r=u.getParents(n,null,u.getRoot()),o={};rC(a,function(e,n){rC(r,function(t){if(u.is(t,n))return i[n]||(rC(e,function(e){e(!0,{node:t,selector:n,parents:r})}),i[n]=e),o[n]=e,!1})}),rC(i,function(e,t){o[t]||(delete i[t],rC(e,function(e){e(!1,{node:n,selector:t,parents:r})}))})})),a[e]||(a[e]=[]),a[e].push(t),p},getScrollContainer:function(){for(var e,t=u.getRoot();t&&"BODY"!==t.nodeName;){if(t.scrollHeight>t.clientHeight){e=t;break}t=t.parentNode}return e},scrollIntoView:function(e,t){return Ab(c,e,t)},placeCaretAt:function(e,t){return i(Ob(e,t,c.getDoc()))},getBoundingClientRect:function(){var e=m();return e.collapsed?_u.fromRangeStart(e).getClientRects()[0]:e.getBoundingClientRect()},destroy:function(){s=l=f=null,t.destroy()}};return n=Eb(p),t=kb(p,c),p.bookmarkManager=n,p.controlSelection=t,p};(Nb=wb||(wb={}))[Nb.Br=0]="Br",Nb[Nb.Block=1]="Block",Nb[Nb.Wrap=2]="Wrap",Nb[Nb.Eol=3]="Eol";var uC=function(e,t){return e===Tu.Backwards?t.reverse():t},sC=function(e,t,n,r){for(var o,i,a,u,s,c,l=Js(n),f=r,d=[];f&&(s=l,c=f,o=t===Tu.Forwards?s.next(c):s.prev(c));){if(jo.isBr(o.getNode(!1)))return t===Tu.Forwards?{positions:uC(t,d).concat([o]),breakType:wb.Br,breakAt:_.some(o)}:{positions:uC(t,d),breakType:wb.Br,breakAt:_.some(o)};if(o.isVisible()){if(e(f,o)){var m=(i=t,a=f,u=o,jo.isBr(u.getNode(i===Tu.Forwards))?wb.Br:!1===Ts(a,u)?wb.Block:wb.Wrap);return{positions:uC(t,d),breakType:m,breakAt:_.some(o)}}d.push(o),f=o}else f=o}return{positions:uC(t,d),breakType:wb.Eol,breakAt:_.none()}},cC=function(n,r,o,e){return r(o,e).breakAt.map(function(e){var t=r(o,e).positions;return n===Tu.Backwards?t.concat(e):[e].concat(t)}).getOr([])},lC=function(e,i){return j(e,function(e,o){return e.fold(function(){return _.some(o)},function(r){return ru(Z(r.getClientRects()),Z(o.getClientRects()),function(e,t){var n=Math.abs(i-e.left);return Math.abs(i-t.left)<=n?o:r}).or(e)})},_.none())},fC=function(t,e){return Z(e.getClientRects()).bind(function(e){return lC(t,e.left)})},dC=d(sC,Su.isAbove,-1),mC=d(sC,Su.isBelow,1),gC=d(cC,-1,dC),pC=d(cC,1,mC),hC=jo.isContentEditableFalse,vC=Za,yC=function(e,t,n,r){var o=e===Tu.Forwards,i=o?Lf:Ff;if(!r.collapsed){var a=vC(r);if(hC(a))return ig(e,t,a,e===Tu.Backwards,!0)}var u=Sa(r.startContainer),s=Ps(e,t.getBody(),r);if(i(s))return ag(t,s.getNode(!o));var c=Vl.normalizePosition(o,n(s));if(!c)return u?r:null;if(i(c))return ig(e,t,c.getNode(!o),o,!0);var l=n(c);return l&&i(l)&&Fs(c,l)?ig(e,t,l.getNode(!o),o,!0):u?sg(t,c.toRange(),!0):null},bC=function(e,t,n,r){var o,i,a,u,s,c,l,f,d;if(d=vC(r),o=Ps(e,t.getBody(),r),i=n(t.getBody(),ov(1),o),a=U(i,iv(1)),s=Ht.last(o.getClientRects()),(Lf(o)||zf(o))&&(d=o.getNode()),(Ff(o)||Uf(o))&&(d=o.getNode(!0)),!s)return null;if(c=s.left,(u=fv(a,c))&&hC(u.node))return l=Math.abs(c-u.left),f=Math.abs(c-u.right),ig(e,t,u.node,l<f,!0);if(d){var m=function(e,t,n,r){var o,i,a,u,s,c,l=Js(t),f=[],d=0,m=function(e){return Ht.last(e.getClientRects())};1===e?(o=l.next,i=Ja,a=Ga,u=_u.after(r)):(o=l.prev,i=Ga,a=Ja,u=_u.before(r)),c=m(u);do{if(u.isVisible()&&!a(s=m(u),c)){if(0<f.length&&i(s,Ht.last(f))&&d++,(s=Ka(s)).position=u,s.line=d,n(s))return f;f.push(s)}}while(u=o(u));return f}(e,t.getBody(),ov(1),d);if(u=fv(U(m,iv(1)),c))return sg(t,u.position.toRange(),!0);if(u=Ht.last(U(m,iv(0))))return sg(t,u.position.toRange(),!0)}},CC=function(e,t,n){var r,o,i,a,u=Js(e.getBody()),s=d(Ls,u.next),c=d(Ls,u.prev);if(n.collapsed&&e.settings.forced_root_block){if(!(r=e.dom.getParent(n.startContainer,"PRE")))return;(1===t?s(_u.fromRangeStart(n)):c(_u.fromRangeStart(n)))||(a=(i=e).dom.create(Nl(i)),(!fe.ie||11<=fe.ie)&&(a.innerHTML='<br data-mce-bogus="1">'),o=a,1===t?e.$(r).after(o):e.$(r).before(o),e.selection.select(o,!0),e.selection.collapse())}},xC=function(l,f){return function(){var e,t,n,r,o,i,a,u,s,c=(t=f,r=Js((e=l).getBody()),o=d(Ls,r.next),i=d(Ls,r.prev),a=t?Tu.Forwards:Tu.Backwards,u=t?o:i,s=e.selection.getRng(),(n=yC(a,e,u,s))?n:(n=CC(e,a,s))||null);return!!c&&(l.selection.setRng(c),!0)}},wC=function(u,s){return function(){var e,t,n,r,o,i,a=(r=(t=s)?1:-1,o=t?rv:nv,i=(e=u).selection.getRng(),(n=bC(r,e,o,i))?n:(n=CC(e,r,i))||null);return!!a&&(u.selection.setRng(a),!0)}},NC=function(r,o){return function(){var t,e=o?_u.fromRangeEnd(r.selection.getRng()):_u.fromRangeStart(r.selection.getRng()),n=o?mC(r.getBody(),e):dC(r.getBody(),e);return(o?ee(n.positions):Z(n.positions)).filter((t=o,function(e){return t?Ff(e):Lf(e)})).fold(q(!1),function(e){return r.selection.setRng(e.toRange()),!0})}},EC=function(e,t,n,r,o){var i,a,u,s,c=Qi(ar.fromDom(n),"td,th,caption").map(function(e){return e.dom()}),l=U((i=e,G(c,function(e){var t,n,r=(t=Ka(e.getBoundingClientRect()),n=-1,{left:t.left-n,top:t.top-n,right:t.right+2*n,bottom:t.bottom+2*n,width:t.width+n,height:t.height+n});return[{x:r.left,y:i(r),cell:e},{x:r.right,y:i(r),cell:e}]})),function(e){return t(e,o)});return(a=l,u=r,s=o,j(a,function(e,r){return e.fold(function(){return _.some(r)},function(e){var t=Math.sqrt(Math.abs(e.x-u)+Math.abs(e.y-s)),n=Math.sqrt(Math.abs(r.x-u)+Math.abs(r.y-s));return _.some(n<t?r:e)})},_.none())).map(function(e){return e.cell})},SC=d(EC,function(e){return e.bottom},function(e,t){return e.y<t}),TC=d(EC,function(e){return e.top},function(e,t){return e.y>t}),kC=function(t,n){return Z(n.getClientRects()).bind(function(e){return SC(t,e.left,e.top)}).bind(function(e){return fC((t=e,sc.lastPositionIn(t).map(function(e){return dC(t,e).positions.concat(e)}).getOr([])),n);var t})},_C=function(t,n){return ee(n.getClientRects()).bind(function(e){return TC(t,e.left,e.top)}).bind(function(e){return fC((t=e,sc.firstPositionIn(t).map(function(e){return[e].concat(mC(t,e).positions)}).getOr([])),n);var t})},AC=function(e,t){e.selection.setRng(t),Rb(e,t)},RC=function(e,t,n){var r,o,i,a,u=e(t,n);return(a=u).breakType===wb.Wrap&&0===a.positions.length||!jo.isBr(n.getNode())&&(i=u).breakType===wb.Br&&1===i.positions.length?(r=e,o=t,!u.breakAt.map(function(e){return r(o,e).breakAt.isSome()}).getOr(!1)):u.breakAt.isNone()},DC=d(RC,dC),OC=d(RC,mC),BC=function(e,t,n,r){var o,i,a,u,s=e.selection.getRng(),c=t?1:-1;if(ms()&&(o=t,i=s,a=n,u=_u.fromRangeStart(i),sc.positionIn(!o,a).map(function(e){return e.isEqual(u)}).getOr(!1))){var l=ig(c,e,n,!t,!0);return AC(e,l),!0}return!1},PC=function(e,t){var n=t.getNode(e);return jo.isElement(n)&&"TABLE"===n.nodeName?_.some(n):_.none()},IC=function(u,s,c){var e=PC(!!s,c),t=!1===s;e.fold(function(){return AC(u,c.toRange())},function(a){return sc.positionIn(t,u.getBody()).filter(function(e){return e.isEqual(c)}).fold(function(){return AC(u,c.toRange())},function(e){return n=s,o=a,t=c,void((i=Nl(r=u))?r.undoManager.transact(function(){var e=ar.fromTag(i);Nr(e,El(r)),Fi(e,ar.fromTag("br")),n?Ii(ar.fromDom(o),e):Pi(ar.fromDom(o),e);var t=r.dom.createRng();t.setStart(e.dom(),0),t.setEnd(e.dom(),0),AC(r,t)}):AC(r,t.toRange()));var n,r,o,t,i})})},LC=function(e,t,n,r){var o,i,a,u,s,c,l=e.selection.getRng(),f=_u.fromRangeStart(l),d=e.getBody();if(!t&&DC(r,f)){var m=(u=d,kC(s=n,c=f).orThunk(function(){return Z(c.getClientRects()).bind(function(e){return lC(gC(u,_u.before(s)),e.left)})}).getOr(_u.before(s)));return IC(e,t,m),!0}return!(!t||!OC(r,f))&&(o=d,m=_C(i=n,a=f).orThunk(function(){return Z(a.getClientRects()).bind(function(e){return lC(pC(o,_u.after(i)),e.left)})}).getOr(_u.after(i)),IC(e,t,m),!0)},FC=function(t,n){return function(){return _.from(t.dom.getParent(t.selection.getNode(),"td,th")).bind(function(e){return _.from(t.dom.getParent(e,"table")).map(function(e){return BC(t,n,e)})}).getOr(!1)}},MC=function(n,r){return function(){return _.from(n.dom.getParent(n.selection.getNode(),"td,th")).bind(function(t){return _.from(n.dom.getParent(t,"table")).map(function(e){return LC(n,r,e,t)})}).getOr(!1)}},zC=function(e){return F(["figcaption"],lr(e))},UC=function(e){var t=V.document.createRange();return t.setStartBefore(e.dom()),t.setEndBefore(e.dom()),t},jC=function(e,t,n){n?Fi(e,t):Li(e,t)},VC=function(e,t,n,r){return""===t?(l=e,f=r,d=ar.fromTag("br"),jC(l,d,f),UC(d)):(o=e,i=r,a=t,u=n,s=ar.fromTag(a),c=ar.fromTag("br"),Nr(s,u),Fi(s,c),jC(o,s,i),UC(c));var o,i,a,u,s,c,l,f,d},HC=function(e,t,n){return t?(o=e.dom(),mC(o,n).breakAt.isNone()):(r=e.dom(),dC(r,n).breakAt.isNone());var r,o},qC=function(t,n){var e,r,o,i=ar.fromDom(t.getBody()),a=_u.fromRangeStart(t.selection.getRng()),u=Nl(t),s=El(t);return(e=a,r=i,o=d(Mr,r),na(ar.fromDom(e.container()),Co,o).filter(zC)).exists(function(){if(HC(i,n,a)){var e=VC(i,u,s,n);return t.selection.setRng(e),!0}return!1})},$C=function(e,t){return function(){return!!e.selection.isCollapsed()&&qC(e,t)}},WC=function(e,r){return G(W(e,function(e){return Yy({shiftKey:!1,altKey:!1,ctrlKey:!1,metaKey:!1,keyCode:0,action:o},e)}),function(e){return t=e,(n=r).keyCode===t.keyCode&&n.shiftKey===t.shiftKey&&n.altKey===t.altKey&&n.ctrlKey===t.ctrlKey&&n.metaKey===t.metaKey?[e]:[];var t,n})},KC=function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r=Array.prototype.slice.call(arguments,1);return function(){return e.apply(null,r)}},XC=function(e,t){return X(WC(e,t),function(e){return e.action()})},YC=function(i,a){i.on("keydown",function(e){var t,n,r,o;!1===e.isDefaultPrevented()&&(t=i,n=a,r=e,o=or.detect().os,XC([{keyCode:Zh.RIGHT,action:xC(t,!0)},{keyCode:Zh.LEFT,action:xC(t,!1)},{keyCode:Zh.UP,action:wC(t,!1)},{keyCode:Zh.DOWN,action:wC(t,!0)},{keyCode:Zh.RIGHT,action:FC(t,!0)},{keyCode:Zh.LEFT,action:FC(t,!1)},{keyCode:Zh.UP,action:MC(t,!1)},{keyCode:Zh.DOWN,action:MC(t,!0)},{keyCode:Zh.RIGHT,action:Xd.move(t,n,!0)},{keyCode:Zh.LEFT,action:Xd.move(t,n,!1)},{keyCode:Zh.RIGHT,ctrlKey:!o.isOSX(),altKey:o.isOSX(),action:Xd.moveNextWord(t,n)},{keyCode:Zh.LEFT,ctrlKey:!o.isOSX(),altKey:o.isOSX(),action:Xd.movePrevWord(t,n)},{keyCode:Zh.UP,action:$C(t,!1)},{keyCode:Zh.DOWN,action:$C(t,!0)}],r).each(function(e){r.preventDefault()}))})},GC=function(o,i){o.on("keydown",function(e){var t,n,r;!1===e.isDefaultPrevented()&&(t=o,n=i,r=e,XC([{keyCode:Zh.BACKSPACE,action:KC(ad,t,!1)},{keyCode:Zh.DELETE,action:KC(ad,t,!0)},{keyCode:Zh.BACKSPACE,action:KC(lg,t,!1)},{keyCode:Zh.DELETE,action:KC(lg,t,!0)},{keyCode:Zh.BACKSPACE,action:KC(Qd,t,n,!1)},{keyCode:Zh.DELETE,action:KC(Qd,t,n,!0)},{keyCode:Zh.BACKSPACE,action:KC(Dm,t,!1)},{keyCode:Zh.DELETE,action:KC(Dm,t,!0)},{keyCode:Zh.BACKSPACE,action:KC(Cf,t,!1)},{keyCode:Zh.DELETE,action:KC(Cf,t,!0)},{keyCode:Zh.BACKSPACE,action:KC(hf,t,!1)},{keyCode:Zh.DELETE,action:KC(hf,t,!0)},{keyCode:Zh.BACKSPACE,action:KC(ng,t,!1)},{keyCode:Zh.DELETE,action:KC(ng,t,!0)}],r).each(function(e){r.preventDefault()}))}),o.on("keyup",function(e){var t,n;!1===e.isDefaultPrevented()&&(t=o,n=e,XC([{keyCode:Zh.BACKSPACE,action:KC(ud,t)},{keyCode:Zh.DELETE,action:KC(ud,t)}],n))})},JC=function(e){return _.from(e.dom.getParent(e.selection.getStart(!0),e.dom.isBlock))},QC=function(e,t){var n,r,o,i=t,a=e.dom,u=e.schema.getMoveCaretBeforeOnEnterElements();if(t){if(/^(LI|DT|DD)$/.test(t.nodeName)){var s=function(e){for(;e;){if(1===e.nodeType||3===e.nodeType&&e.data&&/[\r\n\s]/.test(e.data))return e;e=e.nextSibling}}(t.firstChild);s&&/^(UL|OL|DL)$/.test(s.nodeName)&&t.insertBefore(a.doc.createTextNode("\xa0"),t.firstChild)}if(o=a.createRng(),t.normalize(),t.hasChildNodes()){for(n=new go(t,t);r=n.current();){if(jo.isText(r)){o.setStart(r,0),o.setEnd(r,0);break}if(u[r.nodeName.toLowerCase()]){o.setStartBefore(r),o.setEndBefore(r);break}i=r,r=n.next()}r||(o.setStart(i,0),o.setEnd(i,0))}else jo.isBr(t)?t.nextSibling&&a.isBlock(t.nextSibling)?(o.setStartBefore(t),o.setEndBefore(t)):(o.setStartAfter(t),o.setEndAfter(t)):(o.setStart(t,0),o.setEnd(t,0));e.selection.setRng(o),a.remove(void 0),e.selection.scrollIntoView(t)}},ZC=function(e,t){var n,r,o=e.getRoot();for(n=t;n!==o&&"false"!==e.getContentEditable(n);)"true"===e.getContentEditable(n)&&(r=n),n=n.parentNode;return n!==o?r:o},ex=JC,tx=function(e){return JC(e).fold(q(""),function(e){return e.nodeName.toUpperCase()})},nx=function(e){return JC(e).filter(function(e){return So(ar.fromDom(e))}).isSome()},rx=function(e,t){return e&&e.parentNode&&e.parentNode.nodeName===t},ox=function(e){return e&&/^(OL|UL|LI)$/.test(e.nodeName)},ix=function(e){var t=e.parentNode;return/^(LI|DT|DD)$/.test(t.nodeName)?t:e},ax=function(e,t,n){for(var r=e[n?"firstChild":"lastChild"];r&&!jo.isElement(r);)r=r[n?"nextSibling":"previousSibling"];return r===t},ux=function(e,t,n,r,o){var i=e.dom,a=e.selection.getRng();if(n!==e.getBody()){var u;ox(u=n)&&ox(u.parentNode)&&(o="LI");var s,c,l=o?t(o):i.create("BR");if(ax(n,r,!0)&&ax(n,r,!1))rx(n,"LI")?i.insertAfter(l,ix(n)):i.replace(l,n);else if(ax(n,r,!0))rx(n,"LI")?(i.insertAfter(l,ix(n)),l.appendChild(i.doc.createTextNode(" ")),l.appendChild(n)):n.parentNode.insertBefore(l,n);else if(ax(n,r,!1))i.insertAfter(l,ix(n));else{n=ix(n);var f=a.cloneRange();f.setStartAfter(r),f.setEndAfter(n);var d=f.extractContents();"LI"===o&&(c="LI",(s=d).firstChild&&s.firstChild.nodeName===c)?(l=d.firstChild,i.insertAfter(d,n)):(i.insertAfter(d,n),i.insertAfter(l,n))}i.remove(r),QC(e,l)}},sx=function(e){e.innerHTML='<br data-mce-bogus="1">'},cx=function(e,t){return e.nodeName===t||e.previousSibling&&e.previousSibling.nodeName===t},lx=function(e,t){return t&&e.isBlock(t)&&!/^(TD|TH|CAPTION|FORM)$/.test(t.nodeName)&&!/^(fixed|absolute)/i.test(t.style.position)&&"true"!==e.getContentEditable(t)},fx=function(e,t,n){return!1===jo.isText(t)?n:e?1===n&&t.data.charAt(n-1)===xa?0:n:n===t.data.length-1&&t.data.charAt(n)===xa?t.data.length:n},dx=function(e,t){var n,r,o=e.getRoot();for(n=t;n!==o&&"false"!==e.getContentEditable(n);)"true"===e.getContentEditable(n)&&(r=n),n=n.parentNode;return n!==o?r:o},mx=function(o,i,e){_.from(e.style).map(o.dom.parseStyle).each(function(e){var t=function(e){var t={},n=e.dom();if(Cr(n))for(var r=0;r<n.style.length;r++){var o=n.style.item(r);t[o]=n.style[o]}return t}(ar.fromDom(i)),n=ma(ma({},t),e);o.dom.setStyles(i,n)});var t=_.from(e["class"]).map(function(e){return e.split(/\s+/)}),n=_.from(i.className).map(function(e){return U(e.split(/\s+/),function(e){return""!==e})});ru(t,n,function(t,e){var n=U(e,function(e){return!F(t,e)}),r=function(){for(var e=0,t=0,n=arguments.length;t<n;t++)e+=arguments[t].length;var r=Array(e),o=0;for(t=0;t<n;t++)for(var i=arguments[t],a=0,u=i.length;a<u;a++,o++)r[o]=i[a];return r}(t,n);o.dom.setAttrib(i,"class",r.join(" "))});var r=["style","class"],a=yr(e,function(e,t){return!F(r,t)}).t;o.dom.setAttribs(i,a)},gx=function(e,t){var n=Nl(e);if(n&&n.toLowerCase()===t.tagName.toLowerCase()){var r=El(e);mx(e,t,r)}},px=function(a,e){var t,u,s,i,c,n,r,o,l,f,d,m,g,p,h,v,y,b,C,x=a.dom,w=a.schema,N=w.getNonEmptyElements(),E=a.selection.getRng(),S=function(e){var t,n,r,o=s,i=w.getTextInlineElements();if(r=t=e||"TABLE"===f||"HR"===f?x.create(e||m):c.cloneNode(!1),!1===kl(a))x.setAttrib(t,"style",null),x.setAttrib(t,"class",null);else do{if(i[o.nodeName]){if(Ju(o)||yc(o))continue;n=o.cloneNode(!1),x.setAttrib(n,"id",""),t.hasChildNodes()?n.appendChild(t.firstChild):r=n,t.appendChild(n)}}while((o=o.parentNode)&&o!==u);return gx(a,t),sx(r),t},T=function(e){var t,n,r,o;if(o=fx(e,s,i),jo.isText(s)&&(e?0<o:o<s.nodeValue.length))return!1;if(s.parentNode===c&&g&&!e)return!0;if(e&&jo.isElement(s)&&s===c.firstChild)return!0;if(cx(s,"TABLE")||cx(s,"HR"))return g&&!e||!g&&e;for(t=new go(s,c),jo.isText(s)&&(e&&0===o?t.prev():e||o!==s.nodeValue.length||t.next());n=t.current();){if(jo.isElement(n)){if(!n.getAttribute("data-mce-bogus")&&(r=n.nodeName.toLowerCase(),N[r]&&"br"!==r))return!1}else if(jo.isText(n)&&!/^[ \t\r\n]*$/.test(n.nodeValue))return!1;e?t.prev():t.next()}return!0},k=function(){r=/^(H[1-6]|PRE|FIGURE)$/.test(f)&&"HGROUP"!==d?S(m):S(),_l(a)&&lx(x,l)&&x.isEmpty(c)?r=x.split(l,c):x.insertAfter(r,c),QC(a,r)};kg(x,E).each(function(e){E.setStart(e.startContainer,e.startOffset),E.setEnd(e.endContainer,e.endOffset)}),s=E.startContainer,i=E.startOffset,m=Nl(a),n=e.shiftKey,jo.isElement(s)&&s.hasChildNodes()&&(g=i>s.childNodes.length-1,s=s.childNodes[Math.min(i,s.childNodes.length-1)]||s,i=g&&jo.isText(s)?s.nodeValue.length:0),(u=dx(x,s))&&((m&&!n||!m&&n)&&(s=function(e,t,n,r,o){var i,a,u,s,c,l,f,d=t||"P",m=e.dom,g=dx(m,r);if(!(a=m.getParent(r,m.isBlock))||!lx(m,a)){if(l=(a=a||g)===e.getBody()||(f=a)&&/^(TD|TH|CAPTION)$/.test(f.nodeName)?a.nodeName.toLowerCase():a.parentNode.nodeName.toLowerCase(),!a.hasChildNodes())return i=m.create(d),gx(e,i),a.appendChild(i),n.setStart(i,0),n.setEnd(i,0),i;for(s=r;s.parentNode!==a;)s=s.parentNode;for(;s&&!m.isBlock(s);)s=(u=s).previousSibling;if(u&&e.schema.isValidChild(l,d.toLowerCase())){for(i=m.create(d),gx(e,i),u.parentNode.insertBefore(i,u),s=u;s&&!m.isBlock(s);)c=s.nextSibling,i.appendChild(s),s=c;n.setStart(r,o),n.setEnd(r,o)}}return r}(a,m,E,s,i)),c=x.getParent(s,x.isBlock),l=c?x.getParent(c.parentNode,x.isBlock):null,f=c?c.nodeName.toUpperCase():"","LI"!==(d=l?l.nodeName.toUpperCase():"")||e.ctrlKey||(l=(c=l).parentNode,f=d),/^(LI|DT|DD)$/.test(f)&&x.isEmpty(c)?ux(a,S,l,c,m):m&&c===a.getBody()||(m=m||"P",Sa(c)?(r=Pa(c),x.isEmpty(c)&&sx(c),gx(a,r),QC(a,r)):T()?k():T(!0)?(r=c.parentNode.insertBefore(S(),c),QC(a,cx(c,"HR")?r:c)):((t=(b=E,C=b.cloneRange(),C.setStart(b.startContainer,fx(!0,b.startContainer,b.startOffset)),C.setEnd(b.endContainer,fx(!1,b.endContainer,b.endOffset)),C).cloneRange()).setEndAfter(c),o=t.extractContents(),y=o,z(Ji(ar.fromDom(y),mr),function(e){var t=e.dom();t.nodeValue=wa(t.nodeValue)}),function(e){for(;jo.isText(e)&&(e.nodeValue=e.nodeValue.replace(/^[\r\n]+/,"")),e=e.firstChild;);}(o),r=o.firstChild,x.insertAfter(o,c),function(e,t,n){var r,o=n,i=[];if(o){for(;o=o.firstChild;){if(e.isBlock(o))return;jo.isElement(o)&&!t[o.nodeName.toLowerCase()]&&i.push(o)}for(r=i.length;r--;)!(o=i[r]).hasChildNodes()||o.firstChild===o.lastChild&&""===o.firstChild.nodeValue?e.remove(o):(a=e,(u=o)&&"A"===u.nodeName&&a.isEmpty(u)&&e.remove(o));var a,u}}(x,N,r),p=x,(h=c).normalize(),(v=h.lastChild)&&!/^(left|right)$/gi.test(p.getStyle(v,"float",!0))||p.add(h,"br"),x.isEmpty(c)&&sx(c),r.normalize(),x.isEmpty(r)?(x.remove(r),k()):(gx(a,r),QC(a,r))),x.setAttrib(r,"id",""),a.fire("NewBlock",{newBlock:r})))},hx=function(e,t){return ex(e).filter(function(e){return 0<t.length&&Lr(ar.fromDom(e),t)}).isSome()},vx=function(e){return hx(e,Sl(e))},yx=function(e){return hx(e,Tl(e))},bx=xf([{br:[]},{block:[]},{none:[]}]),Cx=function(e,t){return yx(e)},xx=function(n){return function(e,t){return""===Nl(e)===n}},wx=function(n){return function(e,t){return nx(e)===n}},Nx=function(n,r){return function(e,t){return tx(e)===n.toUpperCase()===r}},Ex=function(e){return Nx("pre",e)},Sx=function(n){return function(e,t){return wl(e)===n}},Tx=function(e,t){return vx(e)},kx=function(e,t){return t},_x=function(e){var t=Nl(e),n=ZC(e.dom,e.selection.getStart());return n&&e.schema.isValidChild(n.nodeName,t||"P")},Ax=function(e,t){return function(n,r){return j(e,function(e,t){return e&&t(n,r)},!0)?_.some(t):_.none()}},Rx=function(e,t){return yd([Ax([Cx],bx.none()),Ax([Nx("summary",!0)],bx.br()),Ax([Ex(!0),Sx(!1),kx],bx.br()),Ax([Ex(!0),Sx(!1)],bx.block()),Ax([Ex(!0),Sx(!0),kx],bx.block()),Ax([Ex(!0),Sx(!0)],bx.br()),Ax([wx(!0),kx],bx.br()),Ax([wx(!0)],bx.block()),Ax([xx(!0),kx,_x],bx.block()),Ax([xx(!0)],bx.br()),Ax([Tx],bx.br()),Ax([xx(!1),kx],bx.br()),Ax([_x],bx.block())],[e,t.shiftKey]).getOr(bx.none())},Dx=function(e,t){Rx(e,t).fold(function(){Fg(e,t)},function(){px(e,t)},o)},Ox=function(o){o.on("keydown",function(e){var t,n,r;e.keyCode===Zh.ENTER&&(t=o,(n=e).isDefaultPrevented()||(n.preventDefault(),(r=t.undoManager).typing&&(r.typing=!1,r.add()),t.undoManager.transact(function(){!1===t.selection.isCollapsed()&&t.execCommand("Delete"),Dx(t,n)})))})},Bx=function(n,r){var e=r.container(),t=r.offset();return jo.isText(e)?(e.insertData(t,n),_.some(Su(e,t+n.length))):Is(r).map(function(e){var t=ar.fromText(n);return r.isAtEnd()?Ii(e,t):Pi(e,t),Su(t.dom(),n.length)})},Px=d(Bx,"\xa0"),Ix=d(Bx," "),Lx=function(e,t,n){return sc.navigateIgnore(e,t,n,Pf)},Fx=function(e,t){return X(uf(ar.fromDom(t.container()),e),Co)},Mx=function(e,n,r){return Lx(e,n.dom(),r).forall(function(t){return Fx(n,r).fold(function(){return!1===Ts(t,r,n.dom())},function(e){return!1===Ts(t,r,n.dom())&&zr(e,ar.fromDom(t.container()))})})},zx=function(t,n,r){return Fx(n,r).fold(function(){return Lx(t,n.dom(),r).forall(function(e){return!1===Ts(e,r,n.dom())})},function(e){return Lx(t,e.dom(),r).isNone()})},Ux=d(zx,!1),jx=d(zx,!0),Vx=d(Mx,!1),Hx=d(Mx,!0),qx=function(e){return Su.isTextPosition(e)&&!e.isAtStart()&&!e.isAtEnd()},$x=function(e,t){var n=U(uf(ar.fromDom(t.container()),e),Co);return Z(n).getOr(e)},Wx=function(e,t){return qx(t)?Bf(t):Bf(t)||sc.prevPosition($x(e,t).dom(),t).exists(Bf)},Kx=function(e,t){return qx(t)?Of(t):Of(t)||sc.nextPosition($x(e,t).dom(),t).exists(Of)},Xx=function(e){return Is(e).bind(function(e){return na(e,dr)}).exists(function(e){return t=kr(e,"white-space"),F(["pre","pre-wrap"],t);var t})},Yx=function(e,t){return o=e,i=t,sc.prevPosition(o.dom(),i).isNone()||(n=e,r=t,sc.nextPosition(n.dom(),r).isNone())||Ux(e,t)||jx(e,t)||Sf(e,t)||Ef(e,t);var n,r,o,i},Gx=function(e,t){var n,r,o,i=(r=(n=t).container(),o=n.offset(),jo.isText(r)&&o<r.data.length?Su(r,o+1):n);return!Xx(i)&&(jx(e,i)||Hx(e,i)||Ef(e,i)||Kx(e,i))},Jx=function(e,t){return n=e,!Xx(r=t)&&(Ux(n,r)||Vx(n,r)||Sf(n,r)||Wx(n,r))||Gx(e,t);var n,r},Qx=function(e,t){return _f(e.charAt(t))},Zx=function(e){var t=e.container();return jo.isText(t)&&Yn(t.data,"\xa0")},ew=function(e){var n,t=e.data,r=(n=t.split(""),W(n,function(e,t){return _f(e)&&0<t&&t<n.length-1&&Rf(n[t-1])&&Rf(n[t+1])?" ":e}).join(""));return r!==t&&(e.data=r,!0)},tw=function(l,e){return _.some(e).filter(Zx).bind(function(e){var t,n,r,o,i,a,u,s,c=e.container();return i=l,u=(a=c).data,s=Su(a,0),Qx(u,0)&&!Jx(i,s)&&(a.data=" "+u.slice(1),1)||ew(c)||(t=l,r=(n=c).data,o=Su(n,r.length-1),Qx(r,r.length-1)&&!Jx(t,o)&&(n.data=r.slice(0,-1)+" ",1))?_.some(e):_.none()})},nw=function(t){var e=ar.fromDom(t.getBody());t.selection.isCollapsed()&&tw(e,Su.fromRangeStart(t.selection.getRng())).each(function(e){t.selection.setRng(e.toRange())})},rw=function(r,o){return function(e){return t=r,!Xx(n=e)&&(Yx(t,n)||Wx(t,n)||Kx(t,n))?Px(o):Ix(o);var t,n}},ow=function(e){var t,n,r=_u.fromRangeStart(e.selection.getRng()),o=ar.fromDom(e.getBody());if(e.selection.isCollapsed()){var i=d(Vl.isInlineTarget,e),a=_u.fromRangeStart(e.selection.getRng());return Ld(i,e.getBody(),a).bind((n=o,function(e){return e.fold(function(e){return sc.prevPosition(n.dom(),_u.before(e))},function(e){return sc.firstPositionIn(e)},function(e){return sc.lastPositionIn(e)},function(e){return sc.nextPosition(n.dom(),_u.after(e))})})).bind(rw(o,r)).exists((t=e,function(e){return t.selection.setRng(e.toRange()),t.nodeChanged(),!0}))}return!1},iw=function(r){r.on("keydown",function(e){var t,n;!1===e.isDefaultPrevented()&&(t=r,n=e,XC([{keyCode:Zh.SPACEBAR,action:KC(ow,t)}],n).each(function(e){n.preventDefault()}))})},aw=function(e,t){var n;t.hasAttribute("data-mce-caret")&&(Pa(t),(n=e).selection.setRng(n.selection.getRng()),e.selection.scrollIntoView(t))},uw=function(e,t){var n,r=(n=e,oa(ar.fromDom(n.getBody()),"*[data-mce-caret]").fold(q(null),function(e){return e.dom()}));if(r)return"compositionstart"===t.type?(t.preventDefault(),t.stopPropagation(),void aw(e,r)):void(_a(r)&&(aw(e,r),e.undoManager.add()))},sw=function(e){e.on("keyup compositionstart",d(uw,e))},cw=or.detect().browser,lw=function(t){var e,n;e=t,n=Vi(function(){e.composing||nw(e)},0),cw.isIE()&&(e.on("keypress",function(e){n.throttle()}),e.on("remove",function(e){n.cancel()})),t.on("input",function(e){!1===e.isComposing&&nw(t)})},fw=function(r){r.on("keydown",function(e){var t,n;!1===e.isDefaultPrevented()&&(t=r,n=e,XC([{keyCode:Zh.END,action:NC(t,!0)},{keyCode:Zh.HOME,action:NC(t,!1)}],n).each(function(e){n.preventDefault()}))})},dw=function(e){var t=Xd.setupSelectedState(e);sw(e),YC(e,t),GC(e,t),Ox(e),iw(e),lw(e),fw(e)};function mw(u){var s,n,r,o=Xt.each,c=Zh.BACKSPACE,l=Zh.DELETE,f=u.dom,d=u.selection,e=u.settings,t=u.parser,i=fe.gecko,a=fe.ie,m=fe.webkit,g="data:text/mce-internal,",p=a?"Text":"URL",h=function(e,t){try{u.getDoc().execCommand(e,!1,t)}catch(n){}},v=function(e){return e.isDefaultPrevented()},y=function(){u.shortcuts.add("meta+a",null,"SelectAll")},b=function(){u.on("keydown",function(e){if(!v(e)&&e.keyCode===c&&d.isCollapsed()&&0===d.getRng().startOffset){var t=d.getNode().previousSibling;if(t&&t.nodeName&&"table"===t.nodeName.toLowerCase())return e.preventDefault(),!1}})},C=function(){u.inline||(u.contentStyles.push("body {min-height: 150px}"),u.on("click",function(e){var t;if("HTML"===e.target.nodeName){if(11<fe.ie)return void u.getBody().focus();t=u.selection.getRng(),u.getBody().focus(),u.selection.setRng(t),u.selection.normalize(),u.nodeChanged()}}))};return u.on("keydown",function(e){var t,n,r,o,i;if(!v(e)&&e.keyCode===Zh.BACKSPACE&&(n=(t=d.getRng()).startContainer,r=t.startOffset,o=f.getRoot(),i=n,t.collapsed&&0===r)){for(;i&&i.parentNode&&i.parentNode.firstChild===i&&i.parentNode!==o;)i=i.parentNode;"BLOCKQUOTE"===i.tagName&&(u.formatter.toggle("blockquote",null,i),(t=f.createRng()).setStart(n,0),t.setEnd(n,0),d.setRng(t))}}),s=function(e){var t=f.create("body"),n=e.cloneContents();return t.appendChild(n),d.serializer.serialize(t,{format:"html"})},u.on("keydown",function(e){var t,n,r,o,i,a=e.keyCode;if(!v(e)&&(a===l||a===c)){if(t=u.selection.isCollapsed(),n=u.getBody(),t&&!f.isEmpty(n))return;if(!t&&(r=u.selection.getRng(),o=s(r),(i=f.createRng()).selectNode(u.getBody()),o!==s(i)))return;e.preventDefault(),u.setContent(""),n.firstChild&&f.isBlock(n.firstChild)?u.selection.setCursorLocation(n.firstChild,0):u.selection.setCursorLocation(n,0),u.nodeChanged()}}),fe.windowsPhone||u.on("keyup focusin mouseup",function(e){Zh.modifierPressed(e)||d.normalize()},!0),m&&(u.settings.content_editable||f.bind(u.getDoc(),"mousedown mouseup",function(e){var t;if(e.target===u.getDoc().documentElement)if(t=d.getRng(),u.getBody().focus(),"mousedown"===e.type){if(ka(t.startContainer))return;d.placeCaretAt(e.clientX,e.clientY)}else d.setRng(t)}),u.on("click",function(e){var t=e.target;/^(IMG|HR)$/.test(t.nodeName)&&"false"!==f.getContentEditableParent(t)&&(e.preventDefault(),u.selection.select(t),u.nodeChanged()),"A"===t.nodeName&&f.hasClass(t,"mce-item-anchor")&&(e.preventDefault(),d.select(t))}),e.forced_root_block&&u.on("init",function(){h("DefaultParagraphSeparator",e.forced_root_block)}),u.on("init",function(){u.dom.bind(u.getBody(),"submit",function(e){e.preventDefault()})}),b(),t.addNodeFilter("br",function(e){for(var t=e.length;t--;)"Apple-interchange-newline"===e[t].attr("class")&&e[t].remove()}),fe.iOS?(u.inline||u.on("keydown",function(){V.document.activeElement===V.document.body&&u.getWin().focus()}),C(),u.on("click",function(e){var t=e.target;do{if("A"===t.tagName)return void e.preventDefault()}while(t=t.parentNode)}),u.contentStyles.push(".mce-content-body {-webkit-touch-callout: none}")):y()),11<=fe.ie&&(C(),b()),fe.ie&&(y(),h("AutoUrlDetect",!1),u.on("dragstart",function(e){var t,n,r;(t=e).dataTransfer&&(u.selection.isCollapsed()&&"IMG"===t.target.tagName&&d.select(t.target),0<(n=u.selection.getContent()).length&&(r=g+escape(u.id)+","+escape(n),t.dataTransfer.setData(p,r)))}),u.on("drop",function(e){if(!v(e)){var t=(i=e).dataTransfer&&(a=i.dataTransfer.getData(p))&&0<=a.indexOf(g)?(a=a.substr(g.length).split(","),{id:unescape(a[0]),html:unescape(a[1])}):null;if(t&&t.id!==u.id){e.preventDefault();var n=Ob(e.x,e.y,u.getDoc());d.setRng(n),r=t.html,o=!0,u.queryCommandSupported("mceInsertClipboardContent")?u.execCommand("mceInsertClipboardContent",!1,{content:r,internal:o}):u.execCommand("mceInsertContent",!1,r)}}var r,o,i,a})),i&&(u.on("keydown",function(e){if(!v(e)&&e.keyCode===c){if(!u.getBody().getElementsByTagName("hr").length)return;if(d.isCollapsed()&&0===d.getRng().startOffset){var t=d.getNode(),n=t.previousSibling;if("HR"===t.nodeName)return f.remove(t),void e.preventDefault();n&&n.nodeName&&"hr"===n.nodeName.toLowerCase()&&(f.remove(n),e.preventDefault())}}}),V.Range.prototype.getClientRects||u.on("mousedown",function(e){if(!v(e)&&"HTML"===e.target.nodeName){var t=u.getBody();t.blur(),he.setEditorTimeout(u,function(){t.focus()})}}),n=function(){var e=f.getAttribs(d.getStart().cloneNode(!1));return function(){var t=d.getStart();t!==u.getBody()&&(f.setAttrib(t,"style",null),o(e,function(e){t.setAttributeNode(e.cloneNode(!0))}))}},r=function(){return!d.isCollapsed()&&f.getParent(d.getStart(),f.isBlock)!==f.getParent(d.getEnd(),f.isBlock)},u.on("keypress",function(e){var t;if(!v(e)&&(8===e.keyCode||46===e.keyCode)&&r())return t=n(),u.getDoc().execCommand("delete",!1,null),t(),e.preventDefault(),!1}),f.bind(u.getDoc(),"cut",function(e){var t;!v(e)&&r()&&(t=n(),he.setEditorTimeout(u,function(){t()}))}),e.readonly||u.on("BeforeExecCommand MouseDown",function(){h("StyleWithCSS",!1),h("enableInlineTableEditing",!1),e.object_resizing||h("enableObjectResizing",!1)}),u.on("SetContent ExecCommand",function(e){"setcontent"!==e.type&&"mceInsertLink"!==e.command||o(f.select("a"),function(e){var t=e.parentNode,n=f.getRoot();if(t.lastChild===e){for(;t&&!f.isBlock(t);){if(t.parentNode.lastChild!==t||t===n)return;t=t.parentNode}f.add(t,"br",{"data-mce-bogus":1})}})}),u.contentStyles.push("img:-moz-broken {-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}"),fe.mac&&u.on("keydown",function(e){!Zh.metaKeyPressed(e)||e.shiftKey||37!==e.keyCode&&39!==e.keyCode||(e.preventDefault(),u.selection.getSel().modify("move",37===e.keyCode?"backward":"forward","lineboundary"))}),b()),{refreshContentEditable:function(){},isHidden:function(){var e;return!i||u.removed?0:!(e=u.selection.getSel())||!e.rangeCount||0===e.rangeCount}}}var gw=function(e){return jo.isElement(e)&&No(ar.fromDom(e))},pw=function(t){t.on("click",function(e){3<=e.detail&&function(e){var t=e.selection.getRng(),n=Su.fromRangeStart(t),r=Su.fromRangeEnd(t);if(Su.isElementPosition(n)){var o=n.container();gw(o)&&sc.firstPositionIn(o).each(function(e){return t.setStart(e.container(),e.offset())})}Su.isElementPosition(r)&&(o=n.container(),gw(o)&&sc.lastPositionIn(o).each(function(e){return t.setEnd(e.container(),e.offset())})),e.selection.setRng(cl(t))}(t)})},hw=function(e){var t,n;(t=e).on("click",function(e){t.dom.getParent(e.target,"details")&&e.preventDefault()}),(n=e).parser.addNodeFilter("details",function(e){z(e,function(e){e.attr("data-mce-open",e.attr("open")),e.attr("open","open")})}),n.serializer.addNodeFilter("details",function(e){z(e,function(e){var t=e.attr("data-mce-open");e.attr("open",S(t)?t:null),e.attr("data-mce-open",null)})})},vw=Si.DOM,yw=function(e){var t;e.bindPendingEventDelegates(),e.initialized=!0,e.fire("init"),e.focus(!0),e.nodeChanged({initial:!0}),e.execCallback("init_instance_callback",e),(t=e).settings.auto_focus&&he.setEditorTimeout(t,function(){var e;(e=!0===t.settings.auto_focus?t:t.editorManager.get(t.settings.auto_focus)).destroyed||e.focus()},100)},bw=function(t,e){var n,r,u,o,i,a,s,c,l,f=t.settings,d=t.getElement(),m=t.getDoc();f.inline||(t.getElement().style.visibility=t.orgVisibility),e||f.content_editable||(m.open(),m.write(t.iframeHTML),m.close()),f.content_editable&&(t.on("remove",function(){var e=this.getBody();vw.removeClass(e,"mce-content-body"),vw.removeClass(e,"mce-edit-focus"),vw.setAttrib(e,"contentEditable",null)}),vw.addClass(d,"mce-content-body"),t.contentDocument=m=f.content_document||V.document,t.contentWindow=f.content_window||V.window,t.bodyElement=d,f.content_document=f.content_window=null,f.root_name=d.nodeName.toLowerCase()),(n=t.getBody()).disabled=!0,t.readonly=f.readonly,t.readonly||(t.inline&&"static"===vw.getStyle(n,"position",!0)&&(n.style.position="relative"),n.contentEditable=t.getParam("content_editable_state",!0)),n.disabled=!1,t.editorUpload=Uh(t),t.schema=di(f),t.dom=Si(m,{keep_values:!0,url_converter:t.convertURL,url_converter_scope:t,hex_colors:f.force_hex_style_colors,class_filter:f.class_filter,update_styles:!0,root_element:t.inline?t.getBody():null,collect:f.content_editable,schema:t.schema,contentCssCors:zl(t),onSetAttrib:function(e){t.fire("SetAttrib",e)}}),t.parser=((o=pb((u=t).settings,u.schema)).addAttributeFilter("src,href,style,tabindex",function(e,t){for(var n,r,o,i=e.length,a=u.dom;i--;)if(r=(n=e[i]).attr(t),o="data-mce-"+t,!n.attributes.map[o]){if(0===r.indexOf("data:")||0===r.indexOf("blob:"))continue;"style"===t?((r=a.serializeStyle(a.parseStyle(r),n.name)).length||(r=null),n.attr(o,r),n.attr(t,r)):"tabindex"===t?(n.attr(o,r),n.attr(t,null)):n.attr(o,u.convertURL(r,t,n.name))}}),o.addNodeFilter("script",function(e){for(var t,n,r=e.length;r--;)0!==(n=(t=e[r]).attr("type")||"no/type").indexOf("mce-")&&t.attr("type","mce-"+n)}),o.addNodeFilter("#cdata",function(e){for(var t,n=e.length;n--;)(t=e[n]).type=8,t.name="#comment",t.value="[CDATA["+t.value+"]]"}),o.addNodeFilter("p,h1,h2,h3,h4,h5,h6,div",function(e){for(var t,n=e.length,r=u.schema.getNonEmptyElements();n--;)(t=e[n]).isEmpty(r)&&0===t.getAll("br").length&&(t.append(new ob("br",1)).shortEnded=!0)}),o),t.serializer=Cb(f,t),t.selection=aC(t.dom,t.getWin(),t.serializer,t),t.annotator=Hc(t),t.formatter=Wy(t),t.undoManager=Zv(t),t._nodeChangeDispatcher=new Gh(t),t._selectionOverrides=Av(t),hw(t),pw(t),dw(t),qh(t),t.fire("PreInit"),f.browser_spellcheck||f.gecko_spellcheck||(m.body.spellcheck=!1,vw.setAttrib(n,"spellcheck","false")),t.quirks=mw(t),t.fire("PostRender"),f.directionality&&(n.dir=f.directionality),f.nowrap&&(n.style.whiteSpace="nowrap"),f.protect&&t.on("BeforeSetContent",function(t){Xt.each(f.protect,function(e){t.content=t.content.replace(e,function(e){return"\x3c!--mce:protected "+escape(e)+"--\x3e"})})}),t.on("SetContent",function(){t.addVisual(t.getBody())}),t.load({initial:!0,format:"html"}),t.startContent=t.getContent({format:"raw"}),t.on("compositionstart compositionend",function(e){t.composing="compositionstart"===e.type}),0<t.contentStyles.length&&(r="",Xt.each(t.contentStyles,function(e){r+=e+"\r\n"}),t.dom.addStyle(r)),(i=t,i.inline?vw.styleSheetLoader:i.dom.styleSheetLoader).loadAll(t.contentCSS,function(e){yw(t)},function(e){yw(t)}),f.content_style&&(a=t,s=f.content_style,c=ar.fromDom(a.getDoc().head),l=ar.fromTag("style"),wr(l,"type","text/css"),Fi(l,ar.fromText(s)),Fi(c,l))},Cw=Si.DOM,xw=function(e,t){var n,r,o,i,a,u,s,c=e.editorManager.translate("Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help"),l=(n=e.id,r=c,o=t.height,i=hl(e),s=ar.fromTag("iframe"),Nr(s,i),Nr(s,{id:n+"_ifr",frameBorder:"0",allowTransparency:"true",title:r}),Tr(s,{width:"100%",height:(a=o,u="number"==typeof a?a+"px":a,u||""),display:"block"}),s).dom();l.onload=function(){l.onload=null,e.fire("load")};var f,d,m,g,p=function(e,t){if(V.document.domain!==V.window.location.hostname&&fe.ie&&fe.ie<12){var n=zh.uuid("mce");e[n]=function(){bw(e)};var r='javascript:(function(){document.open();document.domain="'+V.document.domain+'";var ed = window.parent.tinymce.get("'+e.id+'");document.write(ed.iframeHTML);document.close();ed.'+n+"(true);})()";return Cw.setAttrib(t,"src",r),!0}return!1}(e,l);return e.contentAreaContainer=t.iframeContainer,e.iframeElement=l,e.iframeHTML=(g=vl(f=e)+"<html><head>",yl(f)!==f.documentBaseUrl&&(g+='<base href="'+f.documentBaseURI.getURI()+'" />'),g+='<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />',d=bl(f),m=Cl(f),xl(f)&&(g+='<meta http-equiv="Content-Security-Policy" content="'+xl(f)+'" />'),g+='</head><body id="'+d+'" class="mce-content-body '+m+'" data-id="'+f.id+'"><br></body></html>'),Cw.add(t.iframeContainer,l),p},ww=function(e,t){var n=xw(e,t);t.editorContainer&&(Cw.get(t.editorContainer).style.display=e.orgDisplay,e.hidden=Cw.isHidden(t.editorContainer)),e.getElement().style.display="none",Cw.setAttrib(e.id,"aria-hidden","true"),n||bw(e)},Nw=Si.DOM,Ew=function(t,n,e){var r=Eh.get(e),o=Eh.urls[e]||t.documentBaseUrl.replace(/\/$/,"");if(e=Xt.trim(e),r&&-1===Xt.inArray(n,e)){if(Xt.each(Eh.dependencies(e),function(e){Ew(t,n,e)}),t.plugins[e])return;try{var i=new r(t,o,t.$);(t.plugins[e]=i).init&&(i.init(t,o),n.push(e))}catch(iE){Nh.pluginInitError(t,e,iE)}}},Sw=function(e){return e.replace(/^\-/,"")},Tw=function(e){return{editorContainer:e,iframeContainer:e}},kw=function(e){var t,n,r=e.getElement();return e.inline?Tw(null):(t=r,n=Nw.create("div"),Nw.insertAfter(n,t),Tw(n))},_w=function(e){var t,n,r,o,i,a,u,s,c,l,f,d=e.settings,m=e.getElement();return e.orgDisplay=m.style.display,S(d.theme)?(l=(o=e).settings,f=o.getElement(),i=l.width||Nw.getStyle(f,"width")||"100%",a=l.height||Nw.getStyle(f,"height")||f.offsetHeight,u=l.min_height||100,(s=/^[0-9\.]+(|px)$/i).test(""+i)&&(i=Math.max(parseInt(i,10),100)),s.test(""+a)&&(a=Math.max(parseInt(a,10),u)),c=o.theme.renderUI({targetNode:f,width:i,height:a,deltaWidth:l.delta_width,deltaHeight:l.delta_height}),l.content_editable||(a=(c.iframeHeight||a)+("number"==typeof a?c.deltaHeight||0:""))<u&&(a=u),c.height=a,c):D(d.theme)?(r=(t=e).getElement(),(n=t.settings.theme(t,r)).editorContainer.nodeType&&(n.editorContainer.id=n.editorContainer.id||t.id+"_parent"),n.iframeContainer&&n.iframeContainer.nodeType&&(n.iframeContainer.id=n.iframeContainer.id||t.id+"_iframecontainer"),n.height=n.iframeHeight?n.iframeHeight:r.offsetHeight,n):kw(e)},Aw=function(t){var e,n,r,o,i,a,u=t.settings,s=t.getElement();return t.rtl=u.rtl_ui||t.editorManager.i18n.rtl,t.editorManager.i18n.setCode(u.language),u.aria_label=u.aria_label||Nw.getAttrib(s,"aria-label",t.getLang("aria.rich_text_area")),t.fire("ScriptsLoaded"),o=(n=t).settings.theme,S(o)?(n.settings.theme=Sw(o),r=Sh.get(o),n.theme=new r(n,Sh.urls[o]),n.theme.init&&n.theme.init(n,Sh.urls[o]||n.documentBaseUrl.replace(/\/$/,""),n.$)):n.theme={},i=t,a=[],Xt.each(i.settings.plugins.split(/[ ,]/),function(e){Ew(i,a,Sw(e))}),e=_w(t),t.editorContainer=e.editorContainer?e.editorContainer:null,u.content_css&&Xt.each(Xt.explode(u.content_css),function(e){t.contentCSS.push(t.documentBaseURI.toAbsolute(e))}),u.content_editable?bw(t):ww(t,e)},Rw=Si.DOM,Dw=function(e){return"-"===e.charAt(0)},Ow=function(i,a){var u=Ri.ScriptLoader;!function(e,t,n,r){var o=t.settings,i=o.theme;if(S(i)){if(!Dw(i)&&!Sh.urls.hasOwnProperty(i)){var a=o.theme_url;a?Sh.load(i,t.documentBaseURI.toAbsolute(a)):Sh.load(i,"themes/"+i+"/theme"+n+".js")}e.loadQueue(function(){Sh.waitFor(i,r)})}else r()}(u,i,a,function(){var e,t,n,r,o;e=u,(n=(t=i).settings).language&&"en"!==n.language&&!n.language_url&&(n.language_url=t.editorManager.baseURL+"/langs/"+n.language+".js"),n.language_url&&!t.editorManager.i18n.data[n.language]&&e.add(n.language_url),r=i.settings,o=a,Xt.isArray(r.plugins)&&(r.plugins=r.plugins.join(" ")),Xt.each(r.external_plugins,function(e,t){Eh.load(t,e),r.plugins+=" "+t}),Xt.each(r.plugins.split(/[ ,]/),function(e){if((e=Xt.trim(e))&&!Eh.urls[e])if(Dw(e)){e=e.substr(1,e.length);var t=Eh.dependencies(e);Xt.each(t,function(e){var t={prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"};e=Eh.createUrl(t,e),Eh.load(e.resource,e)})}else Eh.load(e,{prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"})}),u.loadQueue(function(){i.removed||Aw(i)},i,function(e){Nh.pluginLoadError(i,e[0]),i.removed||Aw(i)})})},Bw=function(t){var e=t.settings,n=t.id,r=function(){Rw.unbind(V.window,"ready",r),t.render()};if(Se.Event.domLoaded){if(t.getElement()&&fe.contentEditable){e.inline?t.inline=!0:(t.orgVisibility=t.getElement().style.visibility,t.getElement().style.visibility="hidden");var o=t.getElement().form||Rw.getParent(n,"form");o&&(t.formElement=o,e.hidden_input&&!/TEXTAREA|INPUT/i.test(t.getElement().nodeName)&&(Rw.insertAfter(Rw.create("input",{type:"hidden",name:n}),n),t.hasHiddenInput=!0),t.formEventDelegate=function(e){t.fire(e.type,e)},Rw.bind(o,"submit reset",t.formEventDelegate),t.on("reset",function(){t.setContent(t.startContent,{format:"raw"})}),!e.submit_patch||o.submit.nodeType||o.submit.length||o._mceOldSubmit||(o._mceOldSubmit=o.submit,o.submit=function(){return t.editorManager.triggerSave(),t.setDirty(!1),o._mceOldSubmit(o)})),t.windowManager=gh(t),t.notificationManager=mh(t),"xml"===e.encoding&&t.on("GetContent",function(e){e.save&&(e.content=Rw.encode(e.content))}),e.add_form_submit_trigger&&t.on("submit",function(){t.initialized&&t.save()}),e.add_unload_trigger&&(t._beforeUnload=function(){!t.initialized||t.destroyed||t.isHidden()||t.save({format:"raw",no_events:!0,set_dirty:!1})},t.editorManager.on("BeforeUnload",t._beforeUnload)),t.editorManager.add(t),Ow(t,t.suffix)}}else Rw.bind(V.window,"ready",r)},Pw=function(e,t,n){var r=e.sidebars?e.sidebars:[];r.push({name:t,settings:n}),e.sidebars=r},Iw=Xt.each,Lw=Xt.trim,Fw="source protocol authority userInfo user password host port relative path directory file query anchor".split(" "),Mw={ftp:21,http:80,https:443,mailto:25},zw=function(r,e){var t,n,o=this;if(r=Lw(r),t=(e=o.settings=e||{}).base_uri,/^([\w\-]+):([^\/]{2})/i.test(r)||/^\s*#/.test(r))o.source=r;else{var i=0===r.indexOf("//");0!==r.indexOf("/")||i||(r=(t&&t.protocol||"http")+"://mce_host"+r),/^[\w\-]*:?\/\//.test(r)||(n=e.base_uri?e.base_uri.path:new zw(V.document.location.href).directory,""==e.base_uri.protocol?r="//mce_host"+o.toAbsPath(n,r):(r=/([^#?]*)([#?]?.*)/.exec(r),r=(t&&t.protocol||"http")+"://mce_host"+o.toAbsPath(n,r[1])+r[2])),r=r.replace(/@@/g,"(mce_at)"),r=/^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@\/]*):?([^:@\/]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/.exec(r),Iw(Fw,function(e,t){var n=r[t];n&&(n=n.replace(/\(mce_at\)/g,"@@")),o[e]=n}),t&&(o.protocol||(o.protocol=t.protocol),o.userInfo||(o.userInfo=t.userInfo),o.port||"mce_host"!==o.host||(o.port=t.port),o.host&&"mce_host"!==o.host||(o.host=t.host),o.source=""),i&&(o.protocol="")}};zw.prototype={setPath:function(e){e=/^(.*?)\/?(\w+)?$/.exec(e),this.path=e[0],this.directory=e[1],this.file=e[2],this.source="",this.getURI()},toRelative:function(e){var t;if("./"===e)return e;if("mce_host"!==(e=new zw(e,{base_uri:this})).host&&this.host!==e.host&&e.host||this.port!==e.port||this.protocol!==e.protocol&&""!==e.protocol)return e.getURI();var n=this.getURI(),r=e.getURI();return n===r||"/"===n.charAt(n.length-1)&&n.substr(0,n.length-1)===r?n:(t=this.toRelPath(this.path,e.path),e.query&&(t+="?"+e.query),e.anchor&&(t+="#"+e.anchor),t)},toAbsolute:function(e,t){return(e=new zw(e,{base_uri:this})).getURI(t&&this.isSameOrigin(e))},isSameOrigin:function(e){if(this.host==e.host&&this.protocol==e.protocol){if(this.port==e.port)return!0;var t=Mw[this.protocol];if(t&&(this.port||t)==(e.port||t))return!0}return!1},toRelPath:function(e,t){var n,r,o,i=0,a="";if(e=(e=e.substring(0,e.lastIndexOf("/"))).split("/"),n=t.split("/"),e.length>=n.length)for(r=0,o=e.length;r<o;r++)if(r>=n.length||e[r]!==n[r]){i=r+1;break}if(e.length<n.length)for(r=0,o=n.length;r<o;r++)if(r>=e.length||e[r]!==n[r]){i=r+1;break}if(1===i)return t;for(r=0,o=e.length-(i-1);r<o;r++)a+="../";for(r=i-1,o=n.length;r<o;r++)a+=r!==i-1?"/"+n[r]:n[r];return a},toAbsPath:function(e,t){var n,r,o,i=0,a=[];for(r=/\/$/.test(t)?"/":"",e=e.split("/"),t=t.split("/"),Iw(e,function(e){e&&a.push(e)}),e=a,n=t.length-1,a=[];0<=n;n--)0!==t[n].length&&"."!==t[n]&&(".."!==t[n]?0<i?i--:a.push(t[n]):i++);return 0!==(o=(n=e.length-i)<=0?a.reverse().join("/"):e.slice(0,n).join("/")+"/"+a.reverse().join("/")).indexOf("/")&&(o="/"+o),r&&o.lastIndexOf("/")!==o.length-1&&(o+=r),o},getURI:function(e){var t,n=this;return n.source&&!e||(t="",e||(n.protocol?t+=n.protocol+"://":t+="//",n.userInfo&&(t+=n.userInfo+"@"),n.host&&(t+=n.host),n.port&&(t+=":"+n.port)),n.path&&(t+=n.path),n.query&&(t+="?"+n.query),n.anchor&&(t+="#"+n.anchor),n.source=t),n.source}},zw.parseDataUri=function(e){var t,n;return e=decodeURIComponent(e).split(","),(n=/data:([^;]+)/.exec(e[0]))&&(t=n[1]),{type:t,data:e[1]}},zw.getDocumentBaseUrl=function(e){var t;return t=0!==e.protocol.indexOf("http")&&"file:"!==e.protocol?e.href:e.protocol+"//"+e.host+e.pathname,/^[^:]+:\/\/\/?[^\/]+\//.test(t)&&(t=t.replace(/[\?#].*$/,"").replace(/[\/\\][^\/]+$/,""),/[\/\\]$/.test(t)||(t+="/")),t};var Uw=function(e,t,n){var r,o,i,a,u;if(t.format=t.format?t.format:"html",t.get=!0,t.getInner=!0,t.no_events||e.fire("BeforeGetContent",t),"raw"===t.format)r=Xt.trim(Lv.trimExternal(e.serializer,n.innerHTML));else if("text"===t.format)r=wa(n.innerText||n.textContent);else{if("tree"===t.format)return e.serializer.serialize(n,t);i=(o=e).serializer.serialize(n,t),a=Nl(o),u=new RegExp("^(<"+a+"[^>]*>( | |\\s|\xa0|<br \\/>|)<\\/"+a+">[\r\n]*|<br \\/>[\r\n]*)$"),r=i.replace(u,"")}return"text"===t.format||Ao(ar.fromDom(n))?t.content=r:t.content=Xt.trim(r),t.no_events||e.fire("GetContent",t),t.content},jw=function(e,t){t(e),e.firstChild&&jw(e.firstChild,t),e.next&&jw(e.next,t)},Vw=function(e,t,n){var r=function(e,n,t){var r={},o={},i=[];for(var a in t.firstChild&&jw(t.firstChild,function(t){z(e,function(e){e.name===t.name&&(r[e.name]?r[e.name].nodes.push(t):r[e.name]={filter:e,nodes:[t]})}),z(n,function(e){"string"==typeof t.attr(e.name)&&(o[e.name]?o[e.name].nodes.push(t):o[e.name]={filter:e,nodes:[t]})})}),r)r.hasOwnProperty(a)&&i.push(r[a]);for(var a in o)o.hasOwnProperty(a)&&i.push(o[a]);return i}(e,t,n);z(r,function(t){z(t.filter.callbacks,function(e){e(t.nodes,t.filter.name,{})})})},Hw=function(e){return e instanceof ob},qw=function(e,t){var r;e.dom.setHTML(e.getBody(),t),oh(r=e)&&sc.firstPositionIn(r.getBody()).each(function(e){var t=e.getNode(),n=jo.isTable(t)?sc.firstPositionIn(t).getOr(e):e;r.selection.setRng(n.toRange())})},$w=function(u,s,c){return void 0===c&&(c={}),c.format=c.format?c.format:"html",c.set=!0,c.content=Hw(s)?"":s,Hw(s)||c.no_events||(u.fire("BeforeSetContent",c),s=c.content),_.from(u.getBody()).fold(q(s),function(e){return Hw(s)?function(e,t,n,r){Vw(e.parser.getNodeFilters(),e.parser.getAttributeFilters(),n);var o=al({validate:e.validate},e.schema).serialize(n);return r.content=Ao(ar.fromDom(t))?o:Xt.trim(o),qw(e,r.content),r.no_events||e.fire("SetContent",r),n}(u,e,s,c):(t=u,n=e,o=c,0===(r=s).length||/^\s+$/.test(r)?(a='<br data-mce-bogus="1">',"TABLE"===n.nodeName?r="<tr><td>"+a+"</td></tr>":/^(UL|OL)$/.test(n.nodeName)&&(r="<li>"+a+"</li>"),(i=Nl(t))&&t.schema.isValidChild(n.nodeName.toLowerCase(),i.toLowerCase())?(r=a,r=t.dom.createHTML(i,t.settings.forced_root_block_attrs,r)):r||(r='<br data-mce-bogus="1">'),qw(t,r),t.fire("SetContent",o)):("raw"!==o.format&&(r=al({validate:t.validate},t.schema).serialize(t.parser.parse(r,{isRootContent:!0,insert:!0}))),o.content=Ao(ar.fromDom(n))?r:Xt.trim(r),qw(t,o.content),o.no_events||t.fire("SetContent",o)),o.content);var t,n,r,o,i,a})},Ww=Si.DOM,Kw=function(e){return _.from(e).each(function(e){return e.destroy()})},Xw=function(e){if(!e.removed){var t=e._selectionOverrides,n=e.editorUpload,r=e.getBody(),o=e.getElement();r&&e.save({is_removing:!0}),e.removed=!0,e.unbindAllNativeEvents(),e.hasHiddenInput&&o&&Ww.remove(o.nextSibling),bp(e),e.editorManager.remove(e),!e.inline&&r&&(i=e,Ww.setStyle(i.id,"display",i.orgDisplay)),Cp(e),Ww.remove(e.getContainer()),Kw(t),Kw(n),e.destroy()}var i},Yw=function(e,t){var n,r,o,i=e.selection,a=e.dom;e.destroyed||(t||e.removed?(t||(e.editorManager.off("beforeunload",e._beforeUnload),e.theme&&e.theme.destroy&&e.theme.destroy(),Kw(i),Kw(a)),(r=(n=e).formElement)&&(r._mceOldSubmit&&(r.submit=r._mceOldSubmit,r._mceOldSubmit=null),Ww.unbind(r,"submit reset",n.formEventDelegate)),(o=e).contentAreaContainer=o.formElement=o.container=o.editorContainer=null,o.bodyElement=o.contentDocument=o.contentWindow=null,o.iframeElement=o.targetElm=null,o.selection&&(o.selection=o.selection.win=o.selection.dom=o.selection.dom.doc=null),e.destroyed=!0):e.remove())},Gw=Si.DOM,Jw=Xt.extend,Qw=Xt.each,Zw=Xt.resolve,eN=fe.ie,tN=function(e,t,n){var r,o,i,a,u,s,c,l=this,f=l.documentBaseUrl=n.documentBaseURL,d=n.baseURI;r=l,o=e,i=f,a=n.defaultSettings,u=t,c={id:o,theme:"modern",delta_width:0,delta_height:0,popup_css:"",plugins:"",document_base_url:i,add_form_submit_trigger:!0,submit_patch:!0,add_unload_trigger:!0,convert_urls:!0,relative_urls:!0,remove_script_host:!0,object_resizing:!0,doctype:"<!DOCTYPE html>",visual:!0,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",font_size_legacy_values:"xx-small,small,medium,large,x-large,xx-large,300%",forced_root_block:"p",hidden_input:!0,render_ui:!0,indentation:"40px",inline_styles:!0,convert_fonts_to_spans:!0,indent:"simple",indent_before:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",indent_after:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,summary,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",entity_encoding:"named",url_converter:(s=r).convertURL,url_converter_scope:s,ie7_compat:!0},t=jp(Ip,c,a,u),l.settings=t,Bi.language=t.language||"en",Bi.languageLoad=t.language_load,Bi.baseURL=n.baseURL,l.id=e,l.setDirty(!1),l.plugins={},l.documentBaseURI=new zw(t.document_base_url,{base_uri:d}),l.baseURI=d,l.contentCSS=[],l.contentStyles=[],l.shortcuts=new Xp(l),l.loadedCSS={},l.editorCommands=new fp(l),l.suffix=n.suffix,l.editorManager=n,l.inline=t.inline,l.buttons={},l.menuItems={},t.cache_suffix&&(fe.cacheSuffix=t.cache_suffix.replace(/^[\?\&]+/,"")),!1===t.override_viewport&&(fe.overrideViewPort=!1),n.fire("SetupEditor",{editor:l}),l.execCallback("setup",l),l.$=gn.overrideDefaults(function(){return{context:l.inline?l.getBody():l.getDoc(),element:l.getBody()}})};Jw(tN.prototype={render:function(){Bw(this)},focus:function(e){rh(this,e)},hasFocus:function(){return oh(this)},execCallback:function(e){for(var t=[],n=1;n<arguments.length;n++)t[n-1]=arguments[n];var r,o=this.settings[e];if(o)return this.callbackLookup&&(r=this.callbackLookup[e])&&(o=r.func,r=r.scope),"string"==typeof o&&(r=(r=o.replace(/\.\w+$/,""))?Zw(r):0,o=Zw(o),this.callbackLookup=this.callbackLookup||{},this.callbackLookup[e]={func:o,scope:r}),o.apply(r||this,Array.prototype.slice.call(arguments,1))},translate:function(e){if(e&&Xt.is(e,"string")){var n=this.settings.language||"en",r=this.editorManager.i18n;e=r.data[n+"."+e]||e.replace(/\{\#([^\}]+)\}/g,function(e,t){return r.data[n+"."+t]||"{#"+t+"}"})}return this.editorManager.translate(e)},getLang:function(e,t){return this.editorManager.i18n.data[(this.settings.language||"en")+"."+e]||(t!==undefined?t:"{#"+e+"}")},getParam:function(e,t,n){return Hp(this,e,t,n)},nodeChanged:function(e){this._nodeChangeDispatcher.nodeChanged(e)},addButton:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),t.stateSelector&&"undefined"==typeof t.active&&(t.active=!1),t.text||t.icon||(t.icon=e),t.tooltip=t.tooltip||t.title,n.buttons[e]=t},addSidebar:function(e,t){return Pw(this,e,t)},addMenuItem:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),n.menuItems[e]=t},addContextToolbar:function(e,t){var n,r=this;r.contextToolbars=r.contextToolbars||[],"string"==typeof e&&(n=e,e=function(e){return r.dom.is(e,n)}),r.contextToolbars.push({id:zh.uuid("mcet"),predicate:e,items:t})},addCommand:function(e,t,n){this.editorCommands.addCommand(e,t,n)},addQueryStateHandler:function(e,t,n){this.editorCommands.addQueryStateHandler(e,t,n)},addQueryValueHandler:function(e,t,n){this.editorCommands.addQueryValueHandler(e,t,n)},addShortcut:function(e,t,n,r){this.shortcuts.add(e,t,n,r)},execCommand:function(e,t,n,r){return this.editorCommands.execCommand(e,t,n,r)},queryCommandState:function(e){return this.editorCommands.queryCommandState(e)},queryCommandValue:function(e){return this.editorCommands.queryCommandValue(e)},queryCommandSupported:function(e){return this.editorCommands.queryCommandSupported(e)},show:function(){this.hidden&&(this.hidden=!1,this.inline?this.getBody().contentEditable=!0:(Gw.show(this.getContainer()),Gw.hide(this.id)),this.load(),this.fire("show"))},hide:function(){var e=this,t=e.getDoc();e.hidden||(eN&&t&&!e.inline&&t.execCommand("SelectAll"),e.save(),e.inline?(e.getBody().contentEditable=!1,e===e.editorManager.focusedEditor&&(e.editorManager.focusedEditor=null)):(Gw.hide(e.getContainer()),Gw.setStyle(e.id,"display",e.orgDisplay)),e.hidden=!0,e.fire("hide"))},isHidden:function(){return!!this.hidden},setProgressState:function(e,t){this.fire("ProgressState",{state:e,time:t})},load:function(e){var t,n=this.getElement();return this.removed?"":n?((e=e||{}).load=!0,t=this.setContent(n.value!==undefined?n.value:n.innerHTML,e),e.element=n,e.no_events||this.fire("LoadContent",e),e.element=n=null,t):void 0},save:function(e){var t,n,r=this,o=r.getElement();if(o&&r.initialized&&!r.removed)return(e=e||{}).save=!0,e.element=o,e.content=r.getContent(e),e.no_events||r.fire("SaveContent",e),"raw"===e.format&&r.fire("RawSaveContent",e),t=e.content,/TEXTAREA|INPUT/i.test(o.nodeName)?o.value=t:(!e.is_removing&&r.inline||(o.innerHTML=t),(n=Gw.getParent(r.id,"form"))&&Qw(n.elements,function(e){if(e.name===r.id)return e.value=t,!1})),e.element=o=null,!1!==e.set_dirty&&r.setDirty(!1),t},setContent:function(e,t){return $w(this,e,t)},getContent:function(e){return t=this,void 0===(n=e)&&(n={}),_.from(t.getBody()).fold(q("tree"===n.format?new ob("body",11):""),function(e){return Uw(t,n,e)});var t,n},insertContent:function(e,t){t&&(e=Jw({content:e},t)),this.execCommand("mceInsertContent",!1,e)},isDirty:function(){return!this.isNotDirty},setDirty:function(e){var t=!this.isNotDirty;this.isNotDirty=!e,e&&e!==t&&this.fire("dirty")},setMode:function(e){var t,n;(n=e)!==kp(t=this)&&(t.initialized?Tp(t,"readonly"===n):t.on("init",function(){Tp(t,"readonly"===n)}),xp(t,n))},getContainer:function(){return this.container||(this.container=Gw.get(this.editorContainer||this.id+"_parent")),this.container},getContentAreaContainer:function(){return this.contentAreaContainer},getElement:function(){return this.targetElm||(this.targetElm=Gw.get(this.id)),this.targetElm},getWin:function(){var e;return this.contentWindow||(e=this.iframeElement)&&(this.contentWindow=e.contentWindow),this.contentWindow},getDoc:function(){var e;return this.contentDocument||(e=this.getWin())&&(this.contentDocument=e.document),this.contentDocument},getBody:function(){var e=this.getDoc();return this.bodyElement||(e?e.body:null)},convertURL:function(e,t,n){var r=this.settings;return r.urlconverter_callback?this.execCallback("urlconverter_callback",e,n,!0,t):!r.convert_urls||n&&"LINK"===n.nodeName||0===e.indexOf("file:")||0===e.length?e:r.relative_urls?this.documentBaseURI.toRelative(e):e=this.documentBaseURI.toAbsolute(e,r.remove_script_host)},addVisual:function(e){var n,r=this,o=r.settings,i=r.dom;e=e||r.getBody(),r.hasVisual===undefined&&(r.hasVisual=o.visual),Qw(i.select("table,a",e),function(e){var t;switch(e.nodeName){case"TABLE":return n=o.visual_table_class||"mce-item-table",void((t=i.getAttrib(e,"border"))&&"0"!==t||!r.hasVisual?i.removeClass(e,n):i.addClass(e,n));case"A":return void(i.getAttrib(e,"href")||(t=i.getAttrib(e,"name")||e.id,n=o.visual_anchor_class||"mce-item-anchor",t&&r.hasVisual?i.addClass(e,n):i.removeClass(e,n)))}}),r.fire("VisualAid",{element:e,hasVisual:r.hasVisual})},remove:function(){Xw(this)},destroy:function(e){Yw(this,e)},uploadImages:function(e){return this.editorUpload.uploadImages(e)},_scanForImages:function(){return this.editorUpload.scanForImages()}},Bp);var nN,rN,oN,iN={isEditorUIElement:function(e){return-1!==e.className.toString().indexOf("mce-")}},aN=function(n,e){var t,r;or.detect().browser.isIE()?(r=n).on("focusout",function(){tp(r)}):(t=e,n.on("mouseup touchend",function(e){t.throttle()})),n.on("keyup nodechange",function(e){var t;"nodechange"===(t=e).type&&t.selectionChange||tp(n)})},uN=function(e){var t,n,r,o=Vi(function(){tp(e)},0);e.inline&&(t=e,n=o,r=function(){n.throttle()},Si.DOM.bind(V.document,"mouseup",r),t.on("remove",function(){Si.DOM.unbind(V.document,"mouseup",r)})),e.on("init",function(){aN(e,o)}),e.on("remove",function(){o.cancel()})},sN=Si.DOM,cN=function(e){return iN.isEditorUIElement(e)},lN=function(t,e){var n=t?t.settings.custom_ui_selector:"";return null!==sN.getParent(e,function(e){return cN(e)||!!n&&t.dom.is(e,n)})},fN=function(r,e){var t=e.editor;uN(t),t.on("focusin",function(){var e=r.focusedEditor;e!==this&&(e&&e.fire("blur",{focusedEditor:this}),r.setActive(this),(r.focusedEditor=this).fire("focus",{blurredEditor:e}),this.focus(!0))}),t.on("focusout",function(){var t=this;he.setEditorTimeout(t,function(){var e=r.focusedEditor;lN(t,function(){try{return V.document.activeElement}catch(e){return V.document.body}}())||e!==t||(t.fire("blur",{focusedEditor:null}),r.focusedEditor=null)})}),nN||(nN=function(e){var t,n=r.activeEditor;t=e.target,n&&t.ownerDocument===V.document&&(t===V.document.body||lN(n,t)||r.focusedEditor!==n||(n.fire("blur",{focusedEditor:null}),r.focusedEditor=null))},sN.bind(V.document,"focusin",nN))},dN=function(e,t){e.focusedEditor===t.editor&&(e.focusedEditor=null),e.activeEditor||(sN.unbind(V.document,"focusin",nN),nN=null)},mN=function(e){e.on("AddEditor",d(fN,e)),e.on("RemoveEditor",d(dN,e))},gN=Si.DOM,pN=Xt.explode,hN=Xt.each,vN=Xt.extend,yN=0,bN=!1,CN=[],xN=[],wN=function(t){var n=t.type;hN(oN.get(),function(e){switch(n){case"scroll":e.fire("ScrollWindow",t);break;case"resize":e.fire("ResizeWindow",t)}})},NN=function(e){e!==bN&&(e?gn(window).on("resize scroll",wN):gn(window).off("resize scroll",wN),bN=e)},EN=function(t){var e=xN;delete CN[t.id];for(var n=0;n<CN.length;n++)if(CN[n]===t){CN.splice(n,1);break}return xN=U(xN,function(e){return t!==e}),oN.activeEditor===t&&(oN.activeEditor=0<xN.length?xN[0]:null),oN.focusedEditor===t&&(oN.focusedEditor=null),e.length!==xN.length};vN(oN={defaultSettings:{},$:gn,majorVersion:"4",minorVersion:"9.10",releaseDate:"2020-04-23",editors:CN,i18n:vh,activeEditor:null,settings:{},setup:function(){var e,t,n="";t=zw.getDocumentBaseUrl(V.document.location),/^[^:]+:\/\/\/?[^\/]+\//.test(t)&&(t=t.replace(/[\?#].*$/,"").replace(/[\/\\][^\/]+$/,""),/[\/\\]$/.test(t)||(t+="/"));var r=window.tinymce||window.tinyMCEPreInit;if(r)e=r.base||r.baseURL,n=r.suffix;else{for(var o=V.document.getElementsByTagName("script"),i=0;i<o.length;i++){var a;if(""!==(a=o[i].src||"")){var u=a.substring(a.lastIndexOf("/"));if(/tinymce(\.full|\.jquery|)(\.min|\.dev|)\.js/.test(a)){-1!==u.indexOf(".min")&&(n=".min"),e=a.substring(0,a.lastIndexOf("/"));break}}}!e&&V.document.currentScript&&(-1!==(a=V.document.currentScript.src).indexOf(".min")&&(n=".min"),e=a.substring(0,a.lastIndexOf("/")))}this.baseURL=new zw(t).toAbsolute(e),this.documentBaseURL=t,this.baseURI=new zw(this.baseURL),this.suffix=n,mN(this)},overrideDefaults:function(e){var t,n;(t=e.base_url)&&(this.baseURL=new zw(this.documentBaseURL).toAbsolute(t.replace(/\/+$/,"")),this.baseURI=new zw(this.baseURL)),n=e.suffix,e.suffix&&(this.suffix=n);var r=(this.defaultSettings=e).plugin_base_urls;for(var o in r)Bi.PluginManager.urls[o]=r[o]},init:function(r){var n,u,s=this;u=Xt.makeMap("area base basefont br col frame hr img input isindex link meta param embed source wbr track colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu"," ");var c=function(e){var t=e.id;return t||(t=(t=e.name)&&!gN.get(t)?e.name:gN.uniqueId(),e.setAttribute("id",t)),t},l=function(e,t){return t.constructor===RegExp?t.test(e.className):gN.hasClass(e,t)},f=function(e){n=e},e=function(){var o,i=0,a=[],n=function(e,t,n){var r=new tN(e,t,s);a.push(r),r.on("init",function(){++i===o.length&&f(a)}),r.targetElm=r.targetElm||n,r.render()};gN.unbind(window,"ready",e),function(e){var t=r[e];t&&t.apply(s,Array.prototype.slice.call(arguments,2))}("onpageload"),o=gn.unique(function(t){var e,n=[];if(fe.ie&&fe.ie<11)return Nh.initError("TinyMCE does not support the browser you are using. For a list of supported browsers please see: https://www.tinymce.com/docs/get-started/system-requirements/"),[];if(t.types)return hN(t.types,function(e){n=n.concat(gN.select(e.selector))}),n;if(t.selector)return gN.select(t.selector);if(t.target)return[t.target];switch(t.mode){case"exact":0<(e=t.elements||"").length&&hN(pN(e),function(t){var e;(e=gN.get(t))?n.push(e):hN(V.document.forms,function(e){hN(e.elements,function(e){e.name===t&&(t="mce_editor_"+yN++,gN.setAttrib(e,"id",t),n.push(e))})})});break;case"textareas":case"specific_textareas":hN(gN.select("textarea"),function(e){t.editor_deselector&&l(e,t.editor_deselector)||t.editor_selector&&!l(e,t.editor_selector)||n.push(e)})}return n}(r)),r.types?hN(r.types,function(t){Xt.each(o,function(e){return!gN.is(e,t.selector)||(n(c(e),vN({},r,t),e),!1)})}):(Xt.each(o,function(e){var t;(t=s.get(e.id))&&t.initialized&&!(t.getContainer()||t.getBody()).parentNode&&(EN(t),t.unbindAllNativeEvents(),t.destroy(!0),t.removed=!0,t=null)}),0===(o=Xt.grep(o,function(e){return!s.get(e.id)})).length?f([]):hN(o,function(e){var t;t=e,r.inline&&t.tagName.toLowerCase()in u?Nh.initError("Could not initialize inline editor on invalid inline target element",e):n(c(e),r,e)}))};return s.settings=r,gN.bind(window,"ready",e),new de(function(t){n?t(n):f=function(e){t(e)}})},get:function(t){return 0===arguments.length?xN.slice(0):S(t)?X(xN,function(e){return e.id===t}).getOr(null):O(t)&&xN[t]?xN[t]:null},add:function(e){var t=this;return CN[e.id]===e||(null===t.get(e.id)&&("length"!==e.id&&(CN[e.id]=e),CN.push(e),xN.push(e)),NN(!0),t.activeEditor=e,t.fire("AddEditor",{editor:e}),rN||(rN=function(){t.fire("BeforeUnload")},gN.bind(window,"beforeunload",rN))),e},createEditor:function(e,t){return this.add(new tN(e,t,this))},remove:function(e){var t,n,r=this;if(e){if(!S(e))return n=e,A(r.get(n.id))?null:(EN(n)&&r.fire("RemoveEditor",{editor:n}),0===xN.length&&gN.unbind(window,"beforeunload",rN),n.remove(),NN(0<xN.length),n);hN(gN.select(e),function(e){(n=r.get(e.id))&&r.remove(n)})}else for(t=xN.length-1;0<=t;t--)r.remove(xN[t])},execCommand:function(e,t,n){var r=this.get(n);switch(e){case"mceAddEditor":return this.get(n)||new tN(n,this.settings,this).render(),!0;case"mceRemoveEditor":return r&&r.remove(),!0;case"mceToggleEditor":return r?r.isHidden()?r.show():r.hide():this.execCommand("mceAddEditor",0,n),!0}return!!this.activeEditor&&this.activeEditor.execCommand(e,t,n)},triggerSave:function(){hN(xN,function(e){e.save()})},addI18n:function(e,t){vh.add(e,t)},translate:function(e){return vh.translate(e)},setActive:function(e){var t=this.activeEditor;this.activeEditor!==e&&(t&&t.fire("deactivate",{relatedTarget:e}),e.fire("activate",{relatedTarget:t})),this.activeEditor=e}},hp),oN.setup();var SN,TN=oN;function kN(n){return{walk:function(e,t){return Lc(n,e,t)},split:Um,normalize:function(t){return kg(n,t).fold(q(!1),function(e){return t.setStart(e.startContainer,e.startOffset),t.setEnd(e.endContainer,e.endOffset),!0})}}}(SN=kN||(kN={})).compareRanges=Cg,SN.getCaretRangeFromPoint=Ob,SN.getSelectedNode=Za,SN.getNode=eu;var _N,AN,RN=kN,DN=Math.min,ON=Math.max,BN=Math.round,PN=function(e,t,n){var r,o,i,a,u,s;return r=t.x,o=t.y,i=e.w,a=e.h,u=t.w,s=t.h,"b"===(n=(n||"").split(""))[0]&&(o+=s),"r"===n[1]&&(r+=u),"c"===n[0]&&(o+=BN(s/2)),"c"===n[1]&&(r+=BN(u/2)),"b"===n[3]&&(o-=a),"r"===n[4]&&(r-=i),"c"===n[3]&&(o-=BN(a/2)),"c"===n[4]&&(r-=BN(i/2)),IN(r,o,i,a)},IN=function(e,t,n,r){return{x:e,y:t,w:n,h:r}},LN={inflate:function(e,t,n){return IN(e.x-t,e.y-n,e.w+2*t,e.h+2*n)},relativePosition:PN,findBestRelativePosition:function(e,t,n,r){var o,i;for(i=0;i<r.length;i++)if((o=PN(e,t,r[i])).x>=n.x&&o.x+o.w<=n.w+n.x&&o.y>=n.y&&o.y+o.h<=n.h+n.y)return r[i];return null},intersect:function(e,t){var n,r,o,i;return n=ON(e.x,t.x),r=ON(e.y,t.y),o=DN(e.x+e.w,t.x+t.w),i=DN(e.y+e.h,t.y+t.h),o-n<0||i-r<0?null:IN(n,r,o-n,i-r)},clamp:function(e,t,n){var r,o,i,a,u,s,c,l,f,d;return u=e.x,s=e.y,c=e.x+e.w,l=e.y+e.h,f=t.x+t.w,d=t.y+t.h,r=ON(0,t.x-u),o=ON(0,t.y-s),i=ON(0,c-f),a=ON(0,l-d),u+=r,s+=o,n&&(c+=r,l+=o,u-=i,s-=a),IN(u,s,(c-=i)-u,(l-=a)-s)},create:IN,fromClientRect:function(e){return IN(e.left,e.top,e.width,e.height)}},FN={},MN={add:function(e,t){FN[e.toLowerCase()]=t},has:function(e){return!!FN[e.toLowerCase()]},get:function(e){var t=e.toLowerCase(),n=FN.hasOwnProperty(t)?FN[t]:null;if(null===n)throw new Error("Could not find module for type: "+e);return n},create:function(e,t){var n;if("string"==typeof e?(t=t||{}).type=e:e=(t=e).type,e=e.toLowerCase(),!(n=FN[e]))throw new Error("Could not find control by type: "+e);return(n=new n(t)).type=e,n}},zN=Xt.each,UN=Xt.extend,jN=function(){};jN.extend=_N=function(n){var e,t,r,o=this.prototype,i=function(){var e,t,n;if(!AN&&(this.init&&this.init.apply(this,arguments),t=this.Mixins))for(e=t.length;e--;)(n=t[e]).init&&n.init.apply(this,arguments)},a=function(){return this},u=function(n,r){return function(){var e,t=this._super;return this._super=o[n],e=r.apply(this,arguments),this._super=t,e}};for(t in AN=!0,e=new this,AN=!1,n.Mixins&&(zN(n.Mixins,function(e){for(var t in e)"init"!==t&&(n[t]=e[t])}),o.Mixins&&(n.Mixins=o.Mixins.concat(n.Mixins))),n.Methods&&zN(n.Methods.split(","),function(e){n[e]=a}),n.Properties&&zN(n.Properties.split(","),function(e){var t="_"+e;n[e]=function(e){return e!==undefined?(this[t]=e,this):this[t]}}),n.Statics&&zN(n.Statics,function(e,t){i[t]=e}),n.Defaults&&o.Defaults&&(n.Defaults=UN({},o.Defaults,n.Defaults)),n)"function"==typeof(r=n[t])&&o[t]?e[t]=u(t,r):e[t]=r;return i.prototype=e,(i.constructor=i).extend=_N,i};var VN=Math.min,HN=Math.max,qN=Math.round,$N=function(e,n){var r,o,t,i;if(n=n||'"',null===e)return"null";if("string"==(t=typeof e))return o="\bb\tt\nn\ff\rr\"\"''\\\\",n+e.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g,function(e,t){return'"'===n&&"'"===e?e:(r=o.indexOf(t))+1?"\\"+o.charAt(r+1):(e=t.charCodeAt().toString(16),"\\u"+"0000".substring(e.length)+e)})+n;if("object"===t){if(e.hasOwnProperty&&"[object Array]"===Object.prototype.toString.call(e)){for(r=0,o="[";r<e.length;r++)o+=(0<r?",":"")+$N(e[r],n);return o+"]"}for(i in o="{",e)e.hasOwnProperty(i)&&(o+="function"!=typeof e[i]?(1<o.length?","+n:n)+i+n+":"+$N(e[i],n):"");return o+"}"}return""+e},WN={serialize:$N,parse:function(e){try{return JSON.parse(e)}catch(t){}}},KN={callbacks:{},count:0,send:function(t){var n=this,r=Si.DOM,o=t.count!==undefined?t.count:n.count,i="tinymce_jsonp_"+o;n.callbacks[o]=function(e){r.remove(i),delete n.callbacks[o],t.callback(e)},r.add(r.doc.body,"script",{id:i,src:t.url,type:"text/javascript"}),n.count++}},XN={send:function(e){var t,n=0,r=function(){!e.async||4===t.readyState||1e4<n++?(e.success&&n<1e4&&200===t.status?e.success.call(e.success_scope,""+t.responseText,t,e):e.error&&e.error.call(e.error_scope,1e4<n?"TIMED_OUT":"GENERAL",t,e),t=null):setTimeout(r,10)};if(e.scope=e.scope||this,e.success_scope=e.success_scope||e.scope,e.error_scope=e.error_scope||e.scope,e.async=!1!==e.async,e.data=e.data||"",XN.fire("beforeInitialize",{settings:e}),t=Th()){if(t.overrideMimeType&&t.overrideMimeType(e.content_type),t.open(e.type||(e.data?"POST":"GET"),e.url,e.async),e.crossDomain&&(t.withCredentials=!0),e.content_type&&t.setRequestHeader("Content-Type",e.content_type),e.requestheaders&&Xt.each(e.requestheaders,function(e){t.setRequestHeader(e.key,e.value)}),t.setRequestHeader("X-Requested-With","XMLHttpRequest"),(t=XN.fire("beforeSend",{xhr:t,settings:e}).xhr).send(e.data),!e.async)return r();setTimeout(r,10)}}};Xt.extend(XN,hp);var YN,GN,JN,QN,ZN=Xt.extend,eE=function(e){this.settings=ZN({},e),this.count=0};eE.sendRPC=function(e){return(new eE).send(e)},eE.prototype={send:function(n){var r=n.error,o=n.success;(n=ZN(this.settings,n)).success=function(e,t){void 0===(e=WN.parse(e))&&(e={error:"JSON Parse error."}),e.error?r.call(n.error_scope||n.scope,e.error,t):o.call(n.success_scope||n.scope,e.result)},n.error=function(e,t){r&&r.call(n.error_scope||n.scope,e,t)},n.data=WN.serialize({id:n.id||"c"+this.count++,method:n.method,params:n.params}),n.content_type="application/json",XN.send(n)}};try{YN=V.window.localStorage}catch(iE){GN={},JN=[],QN={getItem:function(e){var t=GN[e];return t||null},setItem:function(e,t){JN.push(e),GN[e]=String(t)},key:function(e){return JN[e]},removeItem:function(t){JN=JN.filter(function(e){return e===t}),delete GN[t]},clear:function(){JN=[],GN={}},length:0},Object.defineProperty(QN,"length",{get:function(){return JN.length},configurable:!1,enumerable:!1}),YN=QN}var tE,nE=TN,rE={geom:{Rect:LN},util:{Promise:de,Delay:he,Tools:Xt,VK:Zh,URI:zw,Class:jN,EventDispatcher:mp,Observable:hp,I18n:vh,XHR:XN,JSON:WN,JSONRequest:eE,JSONP:KN,LocalStorage:YN,Color:function(e){var n={},u=0,s=0,c=0,t=function(e){var t;return"object"==typeof e?"r"in e?(u=e.r,s=e.g,c=e.b):"v"in e&&function(e,t,n){var r,o,i,a;if(e=(parseInt(e,10)||0)%360,t=parseInt(t,10)/100,n=parseInt(n,10)/100,t=HN(0,VN(t,1)),n=HN(0,VN(n,1)),0!==t){switch(r=e/60,i=(o=n*t)*(1-Math.abs(r%2-1)),a=n-o,Math.floor(r)){case 0:u=o,s=i,c=0;break;case 1:u=i,s=o,c=0;break;case 2:u=0,s=o,c=i;break;case 3:u=0,s=i,c=o;break;case 4:u=i,s=0,c=o;break;case 5:u=o,s=0,c=i;break;default:u=s=c=0}u=qN(255*(u+a)),s=qN(255*(s+a)),c=qN(255*(c+a))}else u=s=c=qN(255*n)}(e.h,e.s,e.v):(t=/rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(e))?(u=parseInt(t[1],10),s=parseInt(t[2],10),c=parseInt(t[3],10)):(t=/#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(e))?(u=parseInt(t[1],16),s=parseInt(t[2],16),c=parseInt(t[3],16)):(t=/#([0-F])([0-F])([0-F])/gi.exec(e))&&(u=parseInt(t[1]+t[1],16),s=parseInt(t[2]+t[2],16),c=parseInt(t[3]+t[3],16)),u=u<0?0:255<u?255:u,s=s<0?0:255<s?255:s,c=c<0?0:255<c?255:c,n};return e&&t(e),n.toRgb=function(){return{r:u,g:s,b:c}},n.toHsv=function(){return e=u,t=s,n=c,o=0,(i=VN(e/=255,VN(t/=255,n/=255)))===(a=HN(e,HN(t,n)))?{h:0,s:0,v:100*(o=i)}:(r=(a-i)/a,{h:qN(60*((e===i?3:n===i?1:5)-(e===i?t-n:n===i?e-t:n-e)/((o=a)-i))),s:qN(100*r),v:qN(100*o)});var e,t,n,r,o,i,a},n.toHex=function(){var e=function(e){return 1<(e=parseInt(e,10).toString(16)).length?e:"0"+e};return"#"+e(u)+e(s)+e(c)},n.parse=t,n}},dom:{EventUtils:Se,Sizzle:St,DomQuery:gn,TreeWalker:go,DOMUtils:Si,ScriptLoader:Ri,RangeUtils:RN,Serializer:Cb,ControlSelection:kb,BookmarkManager:Eb,Selection:aC,Event:Se.Event},html:{Styles:gi,Entities:ti,Node:ob,Schema:di,SaxParser:Pv,DomParser:pb,Writer:il,Serializer:al},ui:{Factory:MN},Env:fe,AddOnManager:Bi,Annotator:Hc,Formatter:Wy,UndoManager:Zv,EditorCommands:fp,WindowManager:gh,NotificationManager:mh,EditorObservable:Bp,Shortcuts:Xp,Editor:tN,FocusManager:iN,EditorManager:TN,DOM:Si.DOM,ScriptLoader:Ri.ScriptLoader,PluginManager:Bi.PluginManager,ThemeManager:Bi.ThemeManager,trim:Xt.trim,isArray:Xt.isArray,is:Xt.is,toArray:Xt.toArray,makeMap:Xt.makeMap,each:Xt.each,map:Xt.map,grep:Xt.grep,inArray:Xt.inArray,extend:Xt.extend,create:Xt.create,walk:Xt.walk,createNS:Xt.createNS,resolve:Xt.resolve,explode:Xt.explode,_addCacheSuffix:Xt._addCacheSuffix,isOpera:fe.opera,isWebKit:fe.webkit,isIE:fe.ie,isGecko:fe.gecko,isMac:fe.mac},oE=nE=Xt.extend(nE,rE);tE=oE,window.tinymce=tE,window.tinyMCE=tE,function(e){if("object"==typeof module)try{module.exports=e}catch(t){}}(oE)}(window); \ No newline at end of file diff --git a/nginx.conf.sample b/nginx.conf.sample index 9219400f6aacd..ead80ccb22ece 100644 --- a/nginx.conf.sample +++ b/nginx.conf.sample @@ -3,13 +3,13 @@ # # use tcp connection # # server 127.0.0.1:9000; # # or socket -# server unix:/var/run/php/php7.0-fpm.sock; +# server unix:/var/run/php/php7.4-fpm.sock; # } # server { # listen 80; # server_name mage.dev; # set $MAGE_ROOT /var/www/magento2; -# set $MAGE_DEBUG_SHOW_ARGS 1; +# set $MAGE_DEBUG_SHOW_ARGS 0; # include /vagrant/magento2/nginx.conf.sample; # } # @@ -93,7 +93,7 @@ location / { } location /pub/ { - location ~ ^/pub/media/(downloadable|customer|import|theme_customization/.*\.xml) { + location ~ ^/pub/media/(downloadable|customer|import|custom_options|theme_customization/.*\.xml) { deny all; } alias $MAGE_ROOT/pub/; @@ -166,6 +166,11 @@ location /media/downloadable/ { location /media/import/ { deny all; } + +location /media/custom_options/ { + deny all; +} + location /errors/ { location ~* \.xml$ { deny all; @@ -176,7 +181,8 @@ location /errors/ { location ~ ^/(index|get|static|errors/report|errors/404|errors/503|health_check)\.php$ { try_files $uri =404; fastcgi_pass fastcgi_backend; - fastcgi_buffers 1024 4k; + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; fastcgi_param PHP_FLAG "session.auto_start=off \n suhosin.session.cryptua=off"; fastcgi_param PHP_VALUE "memory_limit=756M \n max_execution_time=18000"; diff --git a/phpserver/README.md b/phpserver/README.md index 2e8c8f2efd317..a6f9cafaf023c 100644 --- a/phpserver/README.md +++ b/phpserver/README.md @@ -17,11 +17,12 @@ Without a router script, that is not possible via the php built-in server. Please read how to install Magento using the <a href="https://devdocs.magento.com/guides/v2.4/install-gde/install/cli/install-cli.html" target="_blank">command line</a>. An example follows: ``` -php bin/magento setup:install --base-url=http://127.0.0.1:8082 ---db-host=localhost --db-name=magento --db-user=magento --db-password=magento ---admin-firstname=Magento --admin-lastname=User --admin-email=user@example.com ---admin-user=admin --admin-password=admin123 --language=en_US ---currency=USD --timezone=America/Chicago --use-rewrites=1 +php bin/magento setup:install --base-url=http://127.0.0.1:8082 \ +--db-host=localhost --db-name=magento --db-user=magento --db-password=magento \ +--admin-firstname=Magento --admin-lastname=User --admin-email=user@example.com \ +--admin-user=admin --admin-password=admin123 --language=en_US \ +--currency=USD --timezone=America/Chicago --use-rewrites=1 \ +--search-engine=elasticsearch7 --elasticsearch-host=es-host.example.com --elasticsearch-port=9200 ``` Notes: diff --git a/pub/media/custom_options/.htaccess b/pub/media/custom_options/.htaccess new file mode 100644 index 0000000000000..87cd9785472ac --- /dev/null +++ b/pub/media/custom_options/.htaccess @@ -0,0 +1,7 @@ +<IfVersion < 2.4> + order deny,allow + deny from all +</IfVersion> +<IfVersion >= 2.4> + Require all denied +</IfVersion> diff --git a/setup/config/di.config.php b/setup/config/di.config.php index b9f2ebe2fa4e0..ccbf3b51fe1c2 100644 --- a/setup/config/di.config.php +++ b/setup/config/di.config.php @@ -4,20 +4,30 @@ * See COPYING.txt for license details. */ +use Laminas\EventManager\EventManagerInterface; +use Laminas\ServiceManager\ServiceLocatorInterface; +use Laminas\ServiceManager\ServiceManager; +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Component\ComponentRegistrarInterface; +use Magento\Framework\DB\Logger\Quiet; +use Magento\Framework\DB\LoggerInterface; +use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Locale\Config; +use Magento\Framework\Locale\ConfigInterface; +use Magento\Framework\Setup\Declaration\Schema\SchemaConfig; + return [ 'di' => [ 'instance' => [ 'preference' => [ - \Laminas\EventManager\EventManagerInterface::class => 'EventManager', - \Laminas\ServiceManager\ServiceLocatorInterface::class => \Laminas\ServiceManager\ServiceManager::class, - \Magento\Framework\DB\LoggerInterface::class => \Magento\Framework\DB\Logger\Quiet::class, - \Magento\Framework\Locale\ConfigInterface::class => \Magento\Framework\Locale\Config::class, - \Magento\Framework\Filesystem\DriverInterface::class => - \Magento\Framework\Filesystem\Driver\File::class, - \Magento\Framework\Component\ComponentRegistrarInterface::class => - \Magento\Framework\Component\ComponentRegistrar::class, + EventManagerInterface::class => 'EventManager', + ServiceLocatorInterface::class => ServiceManager::class, + LoggerInterface::class => Quiet::class, + ConfigInterface::class => Config::class, + DriverInterface::class => \Magento\Framework\Filesystem\Driver\File::class, + ComponentRegistrarInterface::class => ComponentRegistrar::class, ], - \Magento\Framework\Setup\Declaration\Schema\SchemaConfig::class => [ + SchemaConfig::class => [ 'parameters' => [ 'connectionScopes' => [ 'default', diff --git a/setup/src/Magento/Setup/Console/Command/DeployStaticContentCommand.php b/setup/src/Magento/Setup/Console/Command/DeployStaticContentCommand.php index 6a4231259a1ca..1afdd86cdff3f 100644 --- a/setup/src/Magento/Setup/Console/Command/DeployStaticContentCommand.php +++ b/setup/src/Magento/Setup/Console/Command/DeployStaticContentCommand.php @@ -9,6 +9,8 @@ use Magento\Deploy\Console\ConsoleLoggerFactory; use Magento\Deploy\Console\DeployStaticOptions as Options; use Magento\Framework\App\State; +use Magento\Framework\Console\Cli; +use Psr\Log\LogLevel; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -20,14 +22,14 @@ use Magento\Deploy\Service\DeployStaticContent; /** - * Deploy static content command + * Command to Deploy Static Content * * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class DeployStaticContentCommand extends Command { /** - * Default language value + * Default language value. Always used for adminhtml, fallback if no frontend locale is supplied. */ const DEFAULT_LANGUAGE_VALUE = 'en_US'; @@ -55,16 +57,16 @@ class DeployStaticContentCommand extends Command private $objectManager; /** - * @var \Magento\Framework\App\State + * @var State */ private $appState; /** * StaticContentCommand constructor * - * @param InputValidator $inputValidator - * @param ConsoleLoggerFactory $consoleLoggerFactory - * @param Options $options + * @param InputValidator $inputValidator + * @param ConsoleLoggerFactory $consoleLoggerFactory + * @param Options $options * @param ObjectManagerProvider $objectManagerProvider */ public function __construct( @@ -82,7 +84,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritdoc * @throws \InvalidArgumentException * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ @@ -96,7 +98,10 @@ protected function configure() } /** - * {@inheritdoc} + * @inheritdoc + * @param InputInterface $input + * @param OutputInterface $output + * * @throws \InvalidArgumentException * @throws LocalizedException */ @@ -104,22 +109,12 @@ protected function execute(InputInterface $input, OutputInterface $output) { $time = microtime(true); - if (!$input->getOption(Options::FORCE_RUN) && $this->getAppState()->getMode() !== State::MODE_PRODUCTION) { - throw new LocalizedException( - __( - 'NOTE: Manual static content deployment is not required in "default" and "developer" modes.' - . PHP_EOL . 'In "default" and "developer" modes static contents are being deployed ' - . 'automatically on demand.' - . PHP_EOL . 'If you still want to deploy in these modes, use -f option: ' - . "'bin/magento setup:static-content:deploy -f'" - ) - ); - } - + $this->checkAppMode($input); $this->inputValidator->validate($input); $options = $input->getOptions(); - $options[Options::LANGUAGE] = $input->getArgument(Options::LANGUAGES_ARGUMENT) ?: ['all']; + $languageOption = $options[Options::LANGUAGE] ?: ['all']; + $options[Options::LANGUAGE] = $input->getArgument(Options::LANGUAGES_ARGUMENT) ?: $languageOption; $refreshOnly = isset($options[Options::REFRESH_CONTENT_VERSION_ONLY]) && $options[Options::REFRESH_CONTENT_VERSION_ONLY]; @@ -132,18 +127,44 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->mockCache(); - /** @var DeployStaticContent $deployService */ - $deployService = $this->objectManager->create(DeployStaticContent::class, [ - 'logger' => $logger - ]); - - $deployService->deploy($options); + $exitCode = Cli::RETURN_SUCCESS; + try { + /** @var DeployStaticContent $deployService */ + $deployService = $this->objectManager->create(DeployStaticContent::class, [ + 'logger' => $logger + ]); + $deployService->deploy($options); + } catch (\Throwable $e) { + $logger->error('Error happened during deploy process: ' . $e->getMessage()); + $exitCode = Cli::RETURN_FAILURE; + } if (!$refreshOnly) { $logger->notice(PHP_EOL . "Execution time: " . (microtime(true) - $time)); } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + return $exitCode; + } + + /** + * Check application mode + * + * @param InputInterface $input + * @throws LocalizedException + */ + private function checkAppMode(InputInterface $input): void + { + if (!$input->getOption(Options::FORCE_RUN) && $this->getAppState()->getMode() !== State::MODE_PRODUCTION) { + throw new LocalizedException( + __( + 'NOTE: Manual static content deployment is not required in "default" and "developer" modes.' + . PHP_EOL . 'In "default" and "developer" modes static contents are being deployed ' + . 'automatically on demand.' + . PHP_EOL . 'If you still want to deploy in these modes, use -f option: ' + . "'bin/magento setup:static-content:deploy -f'" + ) + ); + } } /** @@ -161,6 +182,8 @@ private function mockCache() } /** + * Retrieve application state + * * @return State */ private function getAppState() diff --git a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php index 56a4a85b17d99..4c50a3de4fb31 100644 --- a/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php +++ b/setup/src/Magento/Setup/Console/Command/DiCompileCommand.php @@ -5,8 +5,9 @@ */ namespace Magento\Setup\Console\Command; -use Magento\Framework\ObjectManagerInterface; +use Magento\Framework\App\ObjectManager; use Magento\Framework\Filesystem\DriverInterface; +use Magento\Framework\Filesystem\Io\File; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Magento\Framework\Filesystem; @@ -72,6 +73,11 @@ class DiCompileCommand extends Command */ private $componentRegistrar; + /** + * @var File + */ + private $file; + /** * Constructor * @@ -82,6 +88,8 @@ class DiCompileCommand extends Command * @param Filesystem $filesystem * @param DriverInterface $fileDriver * @param \Magento\Framework\Component\ComponentRegistrar $componentRegistrar + * @param File|null $file + * @throws \Magento\Setup\Exception */ public function __construct( DeploymentConfig $deploymentConfig, @@ -90,7 +98,8 @@ public function __construct( ObjectManagerProvider $objectManagerProvider, Filesystem $filesystem, DriverInterface $fileDriver, - ComponentRegistrar $componentRegistrar + ComponentRegistrar $componentRegistrar, + File $file = null ) { $this->deploymentConfig = $deploymentConfig; $this->directoryList = $directoryList; @@ -99,6 +108,7 @@ public function __construct( $this->filesystem = $filesystem; $this->fileDriver = $fileDriver; $this->componentRegistrar = $componentRegistrar; + $this->file = $file ?: ObjectManager::getInstance()->get(File::class); parent::__construct(); } @@ -227,10 +237,10 @@ private function getExcludedModulePaths(array $modulePaths) { $modulesByBasePath = []; foreach ($modulePaths as $modulePath) { - $moduleDir = basename($modulePath); - $vendorPath = dirname($modulePath); - $vendorDir = basename($vendorPath); - $basePath = dirname($vendorPath); + $moduleDir = $this->file->getPathInfo($modulePath)['basename']; + $vendorPath = $this->fileDriver->getParentDirectory($modulePath); + $vendorDir = $this->file->getPathInfo($vendorPath)['basename']; + $basePath = $this->fileDriver->getParentDirectory($vendorPath); $modulesByBasePath[$basePath][$vendorDir][] = $moduleDir; } @@ -360,12 +370,9 @@ private function configureObjectManager(OutputInterface $output) private function getOperationsConfiguration( array $compiledPathsList ) { - $excludePatterns = []; - foreach ($this->excludedPathsList as $excludedPaths) { - $excludePatterns = array_merge($excludedPaths, $excludePatterns); - } + $excludePatterns = array_merge([], ...array_values($this->excludedPathsList)); - $operations = [ + return [ OperationFactory::PROXY_GENERATOR => [], OperationFactory::REPOSITORY_GENERATOR => [ 'paths' => $compiledPathsList['application'], @@ -400,8 +407,7 @@ private function getOperationsConfiguration( $compiledPathsList['generated_helpers'], ], OperationFactory::APPLICATION_ACTION_LIST_GENERATOR => [], + OperationFactory::PLUGIN_LIST_GENERATOR => [], ]; - - return $operations; } } diff --git a/setup/src/Magento/Setup/Console/Command/ModuleStatusCommand.php b/setup/src/Magento/Setup/Console/Command/ModuleStatusCommand.php index 65fc265a64ec8..4e5e73f53478e 100644 --- a/setup/src/Magento/Setup/Console/Command/ModuleStatusCommand.php +++ b/setup/src/Magento/Setup/Console/Command/ModuleStatusCommand.php @@ -8,13 +8,13 @@ namespace Magento\Setup\Console\Command; +use Magento\Framework\Console\Cli; use Magento\Framework\Module\FullModuleList; use Magento\Framework\Module\ModuleList; use Magento\Setup\Model\ObjectManagerProvider; -use Magento\Framework\Console\Cli; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Input\InputArgument; /** * Command for displaying status of modules @@ -22,15 +22,11 @@ class ModuleStatusCommand extends AbstractSetupCommand { /** - * Object manager provider - * * @var ObjectManagerProvider */ private $objectManagerProvider; /** - * Inject dependencies - * * @param ObjectManagerProvider $objectManagerProvider */ public function __construct(ObjectManagerProvider $objectManagerProvider) @@ -40,26 +36,33 @@ public function __construct(ObjectManagerProvider $objectManagerProvider) } /** - * {@inheritdoc} + * @inheritdoc */ protected function configure() { $this->setName('module:status') ->setDescription('Displays status of modules') - ->addArgument('module', InputArgument::OPTIONAL, 'Optional module name') + ->addArgument( + 'module-names', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'Optional module name' + ) ->addOption('enabled', null, null, 'Print only enabled modules') ->addOption('disabled', null, null, 'Print only disabled modules'); parent::configure(); } /** - * {@inheritdoc} + * @inheritdoc */ protected function execute(InputInterface $input, OutputInterface $output) { - $moduleName = (string)$input->getArgument('module'); - if ($moduleName) { - return $this->showSpecificModule($moduleName, $output); + $moduleNames = $input->getArgument('module-names'); + if (!empty($moduleNames)) { + foreach ($moduleNames as $moduleName) { + $this->showSpecificModule($moduleName, $output); + } + return Cli::RETURN_SUCCESS; } $onlyEnabled = $input->getOption('enabled'); @@ -79,34 +82,42 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln("<info>List of disabled modules:</info>"); $this->showDisabledModules($output); $output->writeln(''); + + return Cli::RETURN_SUCCESS; } /** + * Specific module show + * * @param string $moduleName * @param OutputInterface $output + * @return int */ - private function showSpecificModule(string $moduleName, OutputInterface $output) + private function showSpecificModule(string $moduleName, OutputInterface $output): int { $allModules = $this->getAllModules(); - if (!in_array($moduleName, $allModules->getNames())) { - $output->writeln('<error>Module does not exist</error>'); + if (!in_array($moduleName, $allModules->getNames(), true)) { + $output->writeln($moduleName . ' : <error>Module does not exist</error>'); return Cli::RETURN_FAILURE; } $enabledModules = $this->getEnabledModules(); - if (in_array($moduleName, $enabledModules->getNames())) { - $output->writeln('<info>Module is enabled</info>'); + if (in_array($moduleName, $enabledModules->getNames(), true)) { + $output->writeln($moduleName . ' : <info>Module is enabled</info>'); return Cli::RETURN_FAILURE; } - $output->writeln('<info>Module is disabled</info>'); - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + $output->writeln($moduleName . ' : <info> Module is disabled</info>'); + return Cli::RETURN_SUCCESS; } /** + * Enable modules show + * * @param OutputInterface $output + * @return int */ - private function showEnabledModules(OutputInterface $output) + private function showEnabledModules(OutputInterface $output): int { $enabledModules = $this->getEnabledModules(); $enabledModuleNames = $enabledModules->getNames(); @@ -116,13 +127,17 @@ private function showEnabledModules(OutputInterface $output) } $output->writeln(join("\n", $enabledModuleNames)); - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** + * Disabled modules show + * * @param OutputInterface $output + * @return int */ - private function showDisabledModules(OutputInterface $output) + private function showDisabledModules(OutputInterface $output): int { $disabledModuleNames = $this->getDisabledModuleNames(); if (count($disabledModuleNames) === 0) { @@ -131,32 +146,42 @@ private function showDisabledModules(OutputInterface $output) } $output->writeln(join("\n", $disabledModuleNames)); - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + + return Cli::RETURN_SUCCESS; } /** + * Returns all modules + * * @return FullModuleList */ private function getAllModules(): FullModuleList { - return $this->objectManagerProvider->get()->create(FullModuleList::class); + return $this->objectManagerProvider->get() + ->create(FullModuleList::class); } /** + * Returns enabled modules + * * @return ModuleList */ private function getEnabledModules(): ModuleList { - return $this->objectManagerProvider->get()->create(ModuleList::class); + return $this->objectManagerProvider->get() + ->create(ModuleList::class); } /** + * Returns disabled module names + * * @return array */ private function getDisabledModuleNames(): array { $fullModuleList = $this->getAllModules(); $enabledModules = $this->getEnabledModules(); + return array_diff($fullModuleList->getNames(), $enabledModules->getNames()); } } diff --git a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php index 4a0cd3bc9a69a..10a2ffa05a796 100644 --- a/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php +++ b/setup/src/Magento/Setup/Console/Command/UpgradeCommand.php @@ -6,9 +6,12 @@ namespace Magento\Setup\Console\Command; use Magento\Deploy\Console\Command\App\ConfigImportCommand; -use Magento\Framework\App\State as AppState; use Magento\Framework\App\DeploymentConfig; use Magento\Framework\App\ObjectManager; +use Magento\Framework\App\State as AppState; +use Magento\Framework\Console\Cli; +use Magento\Framework\Exception\RuntimeException; +use Magento\Framework\Config\CacheInterface; use Magento\Framework\Setup\ConsoleLogger; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\Declaration\Schema\OperationsExecutor; @@ -52,22 +55,30 @@ class UpgradeCommand extends AbstractSetupCommand */ private $searchConfigFactory; + /* + * @var CacheInterface + */ + private $cache; + /** * @param InstallerFactory $installerFactory * @param SearchConfigFactory $searchConfigFactory * @param DeploymentConfig $deploymentConfig * @param AppState|null $appState + * @param CacheInterface|null $cache */ public function __construct( InstallerFactory $installerFactory, SearchConfigFactory $searchConfigFactory, DeploymentConfig $deploymentConfig = null, - AppState $appState = null + AppState $appState = null, + CacheInterface $cache = null ) { $this->installerFactory = $installerFactory; $this->searchConfigFactory = $searchConfigFactory; $this->deploymentConfig = $deploymentConfig ?: ObjectManager::getInstance()->get(DeploymentConfig::class); $this->appState = $appState ?: ObjectManager::getInstance()->get(AppState::class); + $this->cache = $cache ?: ObjectManager::getInstance()->get(CacheInterface::class); parent::__construct(); } @@ -129,7 +140,9 @@ protected function execute(InputInterface $input, OutputInterface $output) $installer = $this->installerFactory->create(new ConsoleLogger($output)); $installer->updateModulesSequence($keepGenerated); $searchConfig = $this->searchConfigFactory->create(); + $this->cache->clean(); $searchConfig->validateSearchEngine(); + $installer->removeUnusedTriggers(); $installer->installSchema($request); $installer->installDataFixtures($request); @@ -138,8 +151,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $arrayInput = new ArrayInput([]); $arrayInput->setInteractive($input->isInteractive()); $result = $importConfigCommand->run($arrayInput, $output); - if ($result === \Magento\Framework\Console\Cli::RETURN_FAILURE) { - throw new \Magento\Framework\Exception\RuntimeException( + if ($result === Cli::RETURN_FAILURE) { + throw new RuntimeException( __('%1 failed. See previous output.', ConfigImportCommand::COMMAND_NAME) ); } @@ -152,9 +165,9 @@ protected function execute(InputInterface $input, OutputInterface $output) } } catch (\Exception $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); - return \Magento\Framework\Console\Cli::RETURN_FAILURE; + return Cli::RETURN_FAILURE; } - return \Magento\Framework\Console\Cli::RETURN_SUCCESS; + return Cli::RETURN_SUCCESS; } } diff --git a/setup/src/Magento/Setup/Console/CompilerPreparation.php b/setup/src/Magento/Setup/Console/CompilerPreparation.php index c83aa48636393..3cfaa16885a34 100644 --- a/setup/src/Magento/Setup/Console/CompilerPreparation.php +++ b/setup/src/Magento/Setup/Console/CompilerPreparation.php @@ -3,6 +3,8 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ +declare(strict_types=1); + namespace Magento\Setup\Console; use Magento\Framework\App\Bootstrap; @@ -65,13 +67,9 @@ public function __construct( */ public function handleCompilerEnvironment() { - $compilationCommands = $this->getCompilerInvalidationCommands(); - $cmdName = $this->input->getFirstArgument(); - $isHelpOption = $this->input->hasParameterOption('--help') || $this->input->hasParameterOption('-h'); - if (!in_array($cmdName, $compilationCommands) || $isHelpOption) { + if (!$this->shouldInvalidateCompiledDI()) { return; } - if (!$this->getGenerationDirectoryAccess()->check()) { throw new GenerationDirectoryAccessException(); } @@ -121,4 +119,39 @@ private function getGenerationDirectoryAccess() return $this->generationDirectoryAccess; } + + /** + * Checks if the command being executed should invalidate compiled DI. + * + * @return bool + */ + private function shouldInvalidateCompiledDI(): bool + { + $compilationCommands = $this->getCompilerInvalidationCommands(); + $cmdName = $this->input->getFirstArgument(); + $isHelpOption = $this->input->hasParameterOption('--help') || $this->input->hasParameterOption('-h'); + $invalidate = false; + if (!$isHelpOption) { + $invalidate = in_array($cmdName, $compilationCommands); + if (!$invalidate) { + // Check if it's an abbreviation of compilation commands. + $expr = preg_replace_callback( + '{([^:]+|)}', + function ($matches) { + return preg_quote($matches[1]) . '[^:]*'; + }, + $cmdName + ); + $commands = preg_grep('{^' . $expr . '$}', $compilationCommands); + if (empty($commands)) { + $commands = preg_grep('{^' . $expr . '$}i', $compilationCommands); + } + if (count($commands) === 1) { + $invalidate = true; + } + } + } + + return $invalidate; + } } diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList.php b/setup/src/Magento/Setup/Model/ConfigOptionsList.php index 7bc0853769217..a8d0a8591f539 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsList.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList.php @@ -53,6 +53,7 @@ class ConfigOptionsList implements ConfigOptionsListInterface \Magento\Setup\Model\ConfigOptionsList\Cache::class, \Magento\Setup\Model\ConfigOptionsList\PageCache::class, \Magento\Setup\Model\ConfigOptionsList\Lock::class, + \Magento\Setup\Model\ConfigOptionsList\Directory::class, ]; /** diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php new file mode 100644 index 0000000000000..e838dbee33603 --- /dev/null +++ b/setup/src/Magento/Setup/Model/ConfigOptionsList/Directory.php @@ -0,0 +1,90 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Setup\Model\ConfigOptionsList; + +use Magento\Framework\App\DeploymentConfig; +use Magento\Framework\Config\Data\ConfigData; +use Magento\Framework\Config\File\ConfigFilePool; +use Magento\Framework\Setup\ConfigOptionsListInterface; +use Magento\Framework\Setup\Option\SelectConfigOption; + +/** + * Deployment configuration options for the folders. + */ +class Directory implements ConfigOptionsListInterface +{ + /** + * Input key for config command. + */ + private const INPUT_KEY_DOCUMENT_ROOT_IS_PUB = 'document-root-is-pub'; + + /** + * Path in in configuration. + */ + const CONFIG_PATH_DOCUMENT_ROOT_IS_PUB = 'directories/document_root_is_pub'; + + /** + * The available configuration values. + * + * @var array + */ + private $selectOptions = [true, false]; + + /** + * Create config and update document root value according to provided options + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return ConfigData|ConfigData[] + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function createConfig(array $options, DeploymentConfig $deploymentConfig) + { + $configData = new ConfigData(ConfigFilePool::APP_ENV); + if (isset($options[self::INPUT_KEY_DOCUMENT_ROOT_IS_PUB])) { + $configData->set( + self::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, + \filter_var($options[self::INPUT_KEY_DOCUMENT_ROOT_IS_PUB], FILTER_VALIDATE_BOOLEAN) + ); + } + + return $configData; + } + + /** + * Return options from Directory configuration. + * + * @return \Magento\Framework\Setup\Option\AbstractConfigOption[]|SelectConfigOption[] + */ + public function getOptions() + { + return [ + new SelectConfigOption( + self::INPUT_KEY_DOCUMENT_ROOT_IS_PUB, + SelectConfigOption::FRONTEND_WIZARD_SELECT, + $this->selectOptions, + self::CONFIG_PATH_DOCUMENT_ROOT_IS_PUB, + 'Flag to show is Pub is on root, can be true or false only', + false + ), + ]; + } + + /** + * Validate options. + * + * @param array $options + * @param DeploymentConfig $deploymentConfig + * @return array|string[] + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function validate(array $options, DeploymentConfig $deploymentConfig) + { + return []; + } +} diff --git a/setup/src/Magento/Setup/Model/ConfigOptionsListCollector.php b/setup/src/Magento/Setup/Model/ConfigOptionsListCollector.php index a97685920f13a..ec81e49f1bf16 100644 --- a/setup/src/Magento/Setup/Model/ConfigOptionsListCollector.php +++ b/setup/src/Magento/Setup/Model/ConfigOptionsListCollector.php @@ -6,8 +6,9 @@ namespace Magento\Setup\Model; use Magento\Framework\App\Filesystem\DirectoryList; +use Magento\Framework\Component\ComponentRegistrar; +use Magento\Framework\Component\ComponentRegistrarInterface; use Magento\Framework\Filesystem; -use Magento\Framework\Module\FullModuleList; use Magento\Framework\Setup\ConfigOptionsListInterface; use Laminas\ServiceManager\ServiceLocatorInterface; @@ -30,13 +31,6 @@ class ConfigOptionsListCollector */ private $filesystem; - /** - * Module list including enabled and disabled modules - * - * @var FullModuleList - */ - private $fullModuleList; - /** * Object manager provider * @@ -51,27 +45,34 @@ class ConfigOptionsListCollector */ private $serviceLocator; + /** + * Component list + * + * @var ComponentRegistrarInterface + */ + private $componentRegistrar; + /** * Constructor * * @param DirectoryList $directoryList * @param Filesystem $filesystem - * @param FullModuleList $fullModuleList + * @param ComponentRegistrarInterface $componentRegistrar * @param ObjectManagerProvider $objectManagerProvider * @param ServiceLocatorInterface $serviceLocator */ public function __construct( DirectoryList $directoryList, Filesystem $filesystem, - FullModuleList $fullModuleList, + ComponentRegistrarInterface $componentRegistrar, ObjectManagerProvider $objectManagerProvider, ServiceLocatorInterface $serviceLocator ) { $this->directoryList = $directoryList; $this->filesystem = $filesystem; - $this->fullModuleList = $fullModuleList; $this->objectManagerProvider = $objectManagerProvider; $this->serviceLocator = $serviceLocator; + $this->componentRegistrar = $componentRegistrar; } /** @@ -86,8 +87,8 @@ public function collectOptionsLists() { $optionsList = []; - // go through modules - foreach ($this->fullModuleList->getNames() as $moduleName) { + $modulePaths = $this->componentRegistrar->getPaths(ComponentRegistrar::MODULE); + foreach (array_keys($modulePaths) as $moduleName) { $optionsClassName = str_replace('_', '\\', $moduleName) . '\Setup\ConfigOptionsList'; if (class_exists($optionsClassName)) { $optionsClass = $this->objectManagerProvider->get()->create($optionsClassName); diff --git a/setup/src/Magento/Setup/Model/Installer.php b/setup/src/Magento/Setup/Model/Installer.php index 061416fcfd9ea..296782c3873c0 100644 --- a/setup/src/Magento/Setup/Model/Installer.php +++ b/setup/src/Magento/Setup/Model/Installer.php @@ -7,7 +7,9 @@ namespace Magento\Setup\Model; use Magento\Backend\Setup\ConfigOptionsList as BackendConfigOptionsList; +use Magento\Framework\App\Cache\Manager; use Magento\Framework\App\Cache\Type\Block as BlockCache; +use Magento\Framework\App\Cache\Type\Config as ConfigCache; use Magento\Framework\App\Cache\Type\Layout as LayoutCache; use Magento\Framework\App\DeploymentConfig\Reader; use Magento\Framework\App\DeploymentConfig\Writer; @@ -21,10 +23,13 @@ use Magento\Framework\DB\Adapter\AdapterInterface; use Magento\Framework\DB\Adapter\Pdo\Mysql; use Magento\Framework\Exception\FileSystemException; +use Magento\Framework\Exception\LocalizedException; +use Magento\Framework\Exception\RuntimeException; use Magento\Framework\Filesystem; use Magento\Framework\Model\ResourceModel\Db\Context; use Magento\Framework\Module\ModuleList\Loader as ModuleLoader; use Magento\Framework\Module\ModuleListInterface; +use Magento\Framework\Mview\TriggerCleaner; use Magento\Framework\Setup\Declaration\Schema\DryRunLogger; use Magento\Framework\Setup\FilePermissions; use Magento\Framework\Setup\InstallDataInterface; @@ -33,13 +38,16 @@ use Magento\Framework\Setup\ModuleDataSetupInterface; use Magento\Framework\Setup\Patch\PatchApplier; use Magento\Framework\Setup\Patch\PatchApplierFactory; +use Magento\Framework\Setup\SampleData\State; use Magento\Framework\Setup\SchemaPersistor; use Magento\Framework\Setup\SchemaSetupInterface; use Magento\Framework\Setup\UpgradeDataInterface; use Magento\Framework\Setup\UpgradeSchemaInterface; +use Magento\Framework\Validation\ValidationException; use Magento\PageCache\Model\Cache\Type as PageCache; use Magento\Setup\Console\Command\InstallCommand; use Magento\Setup\Controller\ResponseTypeInterface; +use Magento\Setup\Exception; use Magento\Setup\Model\ConfigModel as SetupConfigModel; use Magento\Setup\Module\ConnectionFactory; use Magento\Setup\Module\DataSetupFactory; @@ -215,7 +223,7 @@ class Installer private $dataSetupFactory; /** - * @var \Magento\Framework\Setup\SampleData\State + * @var State */ protected $sampleDataState; @@ -246,6 +254,11 @@ class Installer */ private $patchApplierFactory; + /** + * @var TriggerCleaner + */ + private $triggerCleaner; + /** * Constructor * @@ -267,10 +280,10 @@ class Installer * @param DbValidator $dbValidator * @param SetupFactory $setupFactory * @param DataSetupFactory $dataSetupFactory - * @param \Magento\Framework\Setup\SampleData\State $sampleDataState + * @param State $sampleDataState * @param ComponentRegistrar $componentRegistrar * @param PhpReadinessCheck $phpReadinessCheck - * @throws \Magento\Setup\Exception + * @throws Exception * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -292,7 +305,7 @@ public function __construct( DbValidator $dbValidator, SetupFactory $setupFactory, DataSetupFactory $dataSetupFactory, - \Magento\Framework\Setup\SampleData\State $sampleDataState, + State $sampleDataState, ComponentRegistrar $componentRegistrar, PhpReadinessCheck $phpReadinessCheck ) { @@ -319,6 +332,7 @@ public function __construct( $this->componentRegistrar = $componentRegistrar; $this->phpReadinessCheck = $phpReadinessCheck; $this->schemaPersistor = $this->objectManagerProvider->get()->get(SchemaPersistor::class); + $this->triggerCleaner = $this->objectManagerProvider->get()->get(TriggerCleaner::class); } /** @@ -326,7 +340,9 @@ public function __construct( * * @param \ArrayObject|array $request * @return void - * @throws \LogicException + * @throws FileSystemException + * @throws LocalizedException + * @throws RuntimeException */ public function install($request) { @@ -390,6 +406,7 @@ public function install($request) * Get declaration installer. For upgrade process it must be created after deployment config update. * * @return DeclarationInstaller + * @throws Exception */ private function getDeclarationInstaller() { @@ -406,6 +423,7 @@ private function getDeclarationInstaller() * * @return void * @SuppressWarnings(PHPMD.UnusedPrivateMethod) Called by install() via callback. + * @throws FileSystemException */ private function writeInstallationDate() { @@ -421,7 +439,9 @@ private function writeInstallationDate() * @param \ArrayObject|array $request * @param bool $dryRun * @return array - * @throws \LogicException + * @throws FileSystemException + * @throws LocalizedException + * @throws RuntimeException */ private function createModulesConfig($request, $dryRun = false) { @@ -547,6 +567,9 @@ public function checkApplicationFilePermissions() * * @param \ArrayObject|array $data * @return void + * @throws FileSystemException + * @throws LocalizedException + * @throws RuntimeException */ public function installDeploymentConfig($data) { @@ -567,6 +590,7 @@ public function installDeploymentConfig($data) * * @param SchemaSetupInterface $setup * @return void + * @throws \Zend_Db_Exception */ private function setupModuleRegistry(SchemaSetupInterface $setup) { @@ -665,6 +689,7 @@ private function setupSessionTable( * @param SchemaSetupInterface $setup * @param AdapterInterface $connection * @return void + * @throws \Zend_Db_Exception */ private function setupCacheTable( SchemaSetupInterface $setup, @@ -719,6 +744,7 @@ private function setupCacheTable( * @param SchemaSetupInterface $setup * @param AdapterInterface $connection * @return void + * @throws \Zend_Db_Exception */ private function setupCacheTagTable( SchemaSetupInterface $setup, @@ -755,6 +781,7 @@ private function setupCacheTagTable( * @param SchemaSetupInterface $setup * @param AdapterInterface $connection * @return void + * @throws \Zend_Db_Exception */ private function setupFlagTable( SchemaSetupInterface $setup, @@ -811,6 +838,7 @@ private function setupFlagTable( * * @param array $request * @return void + * @throws Exception */ public function declarativeInstallSchema(array $request) { @@ -844,6 +872,9 @@ private function cleanMemoryTables(SchemaSetupInterface $setup) * * @param array $request * @return void + * @throws Exception + * @throws \Magento\Framework\Setup\Exception + * @throws \Zend_Db_Exception */ public function installSchema(array $request) { @@ -890,6 +921,8 @@ private function convertationOfOldScriptsIsAllowed(array $request) * * @param array $request * @return void + * @throws Exception + * @throws \Magento\Framework\Setup\Exception */ public function installDataFixtures(array $request = []) { @@ -955,7 +988,7 @@ private function throwExceptionForNotWritablePaths(array $paths) * @param array $request * @return void * @throws \Magento\Framework\Setup\Exception - * @throws \Magento\Setup\Exception + * @throws Exception * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) @@ -963,7 +996,8 @@ private function throwExceptionForNotWritablePaths(array $paths) private function handleDBSchemaData($setup, $type, array $request) { if (!($type === 'schema' || $type === 'data')) { - throw new \Magento\Setup\Exception("Unsupported operation type $type is requested"); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception("Unsupported operation type $type is requested"); } $resource = new \Magento\Framework\Module\ModuleResource($this->context); $verType = $type . '-version'; @@ -1077,13 +1111,16 @@ private function handleDBSchemaData($setup, $type, array $request) * Assert DbConfigExists * * @return void - * @throws \Magento\Setup\Exception + * @throws Exception + * @throws FileSystemException + * @throws RuntimeException */ private function assertDbConfigExists() { $config = $this->deploymentConfig->get(ConfigOptionsListConstants::CONFIG_PATH_DB_CONNECTION_DEFAULT); if (!$config) { - throw new \Magento\Setup\Exception( + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception( "Can't run this operation: configuration for DB connection is absent." ); } @@ -1106,6 +1143,8 @@ private function isDryRun(array $request) * * @param \ArrayObject|array $data * @return void + * @throws Exception + * @throws LocalizedException */ public function installUserConfig($data) { @@ -1135,8 +1174,8 @@ public function installUserConfig($data) * * @param \ArrayObject|array $data * @return void - * @throws \Magento\Framework\Validation\ValidationException - * @throws \Magento\Setup\Exception + * @throws ValidationException + * @throws Exception */ public function installSearchConfiguration($data) { @@ -1151,13 +1190,14 @@ public function installSearchConfiguration($data) * @param string $className * @param string $interfaceName * @return mixed|null - * @throws \Magento\Setup\Exception + * @throws Exception */ protected function createSchemaDataHandler($className, $interfaceName) { if (class_exists($className)) { if (!is_subclass_of($className, $interfaceName) && $className !== $interfaceName) { - throw new \Magento\Setup\Exception($className . ' must implement \\' . $interfaceName); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception($className . ' must implement \\' . $interfaceName); } else { return $this->objectManagerProvider->get()->create($className); } @@ -1214,6 +1254,9 @@ private function installOrderIncrementPrefix($orderIncrementPrefix) * * @param \ArrayObject|array $data * @return void + * @throws Exception + * @throws FileSystemException + * @throws RuntimeException */ public function installAdminUser($data) { @@ -1237,17 +1280,19 @@ public function installAdminUser($data) * * @param bool $keepGeneratedFiles Cleanup generated classes and view files and reset ObjectManager * @return void - * @throws \Magento\Setup\Exception + * @throws Exception */ public function updateModulesSequence($keepGeneratedFiles = false) { $config = $this->deploymentConfig->get(ConfigOptionsListConstants::KEY_MODULES); if (!$config) { - throw new \Magento\Setup\Exception( + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception( "Can't run this operation: deployment configuration is absent." . " Run 'magento setup:config:set --help' for options." ); } + $this->flushCaches([ConfigCache::TYPE_IDENTIFIER]); $this->cleanCaches(); if (!$keepGeneratedFiles) { $this->cleanupGeneratedFiles(); @@ -1308,11 +1353,12 @@ public function uninstall() * @param bool $isEnabled * @param array $types * @return void + * @throws Exception */ private function updateCaches($isEnabled, $types = []) { - /** @var \Magento\Framework\App\Cache\Manager $cacheManager */ - $cacheManager = $this->objectManagerProvider->get()->create(\Magento\Framework\App\Cache\Manager::class); + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->create(Manager::class); $availableTypes = $cacheManager->getAvailableTypes(); $types = empty($types) ? $availableTypes : array_intersect($availableTypes, $types); @@ -1331,8 +1377,9 @@ function (string $key) use ($types) { ); $this->log->log('Current status:'); - // phpcs:ignore Magento2.Functions.DiscouragedFunction - $this->log->log(print_r($cacheStatus, true)); + foreach ($cacheStatus as $cache => $status) { + $this->log->log(sprintf('%s: %d', $cache, $status)); + } } /** @@ -1341,16 +1388,34 @@ function (string $key) use ($types) { * @return void * * @SuppressWarnings(PHPMD.UnusedPrivateMethod) Called by install() via callback. + * @throws Exception */ private function cleanCaches() { - /** @var \Magento\Framework\App\Cache\Manager $cacheManager */ - $cacheManager = $this->objectManagerProvider->get()->get(\Magento\Framework\App\Cache\Manager::class); + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->get(Manager::class); $types = $cacheManager->getAvailableTypes(); $cacheManager->clean($types); $this->log->log('Cache cleared successfully'); } + /** + * Flush caches for specific types or all available types + * + * @param array $types + * @return void + * + * @throws Exception + */ + private function flushCaches($types = []) + { + /** @var Manager $cacheManager */ + $cacheManager = $this->objectManagerProvider->get()->get(Manager::class); + $types = empty($types) ? $cacheManager->getAvailableTypes() : $types; + $cacheManager->flush($types); + $this->log->log('Cache types ' . implode(',', $types) . ' flushed successfully'); + } + /** * Enables or disables maintenance mode for Magento application * @@ -1416,6 +1481,7 @@ public function cleanupDb() * Removes deployment configuration * * @return void + * @throws FileSystemException */ private function deleteDeploymentConfig() { @@ -1440,6 +1506,9 @@ private function deleteDeploymentConfig() * Validates that MySQL is accessible and MySQL version is supported * * @return void + * @throws Exception + * @throws FileSystemException + * @throws RuntimeException */ private function assertDbAccessible() { @@ -1502,7 +1571,7 @@ private function assertDbAccessible() * @param string $moduleName * @param string $type * @return InstallSchemaInterface | UpgradeSchemaInterface | InstallDataInterface | UpgradeDataInterface | null - * @throws \Magento\Setup\Exception + * @throws Exception */ private function getSchemaDataHandler($moduleName, $type) { @@ -1533,7 +1602,8 @@ private function getSchemaDataHandler($moduleName, $type) $interface = self::DATA_INSTALL; break; default: - throw new \Magento\Setup\Exception("$className does not exist"); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception("$className does not exist"); } return $this->createSchemaDataHandler($className, $interface); @@ -1545,7 +1615,7 @@ private function getSchemaDataHandler($moduleName, $type) * @param \Magento\Framework\Module\ModuleResource $resource * @param string $type * @return ModuleContext[] - * @throws \Magento\Setup\Exception + * @throws Exception */ private function generateListOfModuleContext($resource, $type) { @@ -1556,7 +1626,8 @@ private function generateListOfModuleContext($resource, $type) } elseif ($type === 'data-version') { $dbVer = $resource->getDataVersion($moduleName); } else { - throw new \Magento\Setup\Exception("Unsupported version type $type is requested"); + // phpcs:ignore Magento2.Exceptions.DirectThrow + throw new Exception("Unsupported version type $type is requested"); } if ($dbVer !== false) { $moduleContextList[$moduleName] = new ModuleContext($dbVer); @@ -1644,4 +1715,15 @@ private function updateColumnType( ); } } + + /** + * Remove unused triggers from db + * + * @throws \Exception + */ + public function removeUnusedTriggers(): void + { + $this->triggerCleaner->removeTriggers(); + $this->cleanCaches(); + } } diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/Operation/PluginListGenerator.php b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/PluginListGenerator.php new file mode 100644 index 0000000000000..c1314ec39c245 --- /dev/null +++ b/setup/src/Magento/Setup/Module/Di/App/Task/Operation/PluginListGenerator.php @@ -0,0 +1,60 @@ +<?php +/** + * Copyright © Magento, Inc. All rights reserved. + * See COPYING.txt for license details. + */ +declare(strict_types=1); + +namespace Magento\Setup\Module\Di\App\Task\Operation; + +use Magento\Framework\Config\ScopeInterface; +use Magento\Setup\Module\Di\App\Task\OperationInterface; +use Magento\Framework\Interception\ConfigWriterInterface; + +/** + * Writes plugin list configuration data per scope to generated metadata. + */ +class PluginListGenerator implements OperationInterface +{ + /** + * @var ScopeInterface + */ + private $scopeConfig; + + /** + * @var ConfigWriterInterface + */ + private $configWriter; + + /** + * @param ScopeInterface $scopeConfig + * @param ConfigWriterInterface $configWriter + */ + public function __construct( + ScopeInterface $scopeConfig, + ConfigWriterInterface $configWriter + ) { + $this->scopeConfig = $scopeConfig; + $this->configWriter = $configWriter; + } + + /** + * @inheritDoc + */ + public function doOperation() + { + $scopes = $this->scopeConfig->getAllScopes(); + // remove primary scope for production mode as it is only called in developer mode + $scopes = array_diff($scopes, ['primary']); + + $this->configWriter->write($scopes); + } + + /** + * @inheritDoc + */ + public function getName() + { + return 'Plugin list generation'; + } +} diff --git a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php index 607790e41421c..07ff60c367392 100644 --- a/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php +++ b/setup/src/Magento/Setup/Module/Di/App/Task/OperationFactory.php @@ -19,45 +19,50 @@ class OperationFactory private $objectManager; /** - * Area + * Area config generator operation definition */ const AREA_CONFIG_GENERATOR = 'area'; /** - * Interception + * Interception operation definition */ const INTERCEPTION = 'interception'; /** - * Interception cache + * Interception cache operation definition */ const INTERCEPTION_CACHE = 'interception_cache'; /** - * Repository generator + * Repository generator operation definition */ const REPOSITORY_GENERATOR = 'repository_generator'; /** - * Proxy generator + * Proxy generator operation definition */ const PROXY_GENERATOR = 'proxy_generator'; /** - * Service data attributes generator + * Service data attributes generator operation definition */ const DATA_ATTRIBUTES_GENERATOR = 'extension_attributes_generator'; /** - * Application code generator + * Application code generator operation definition */ const APPLICATION_CODE_GENERATOR = 'application_code_generator'; /** - * Application action list generator + * Application action list generator operation definition */ const APPLICATION_ACTION_LIST_GENERATOR = 'application_action_list_generator'; + /** + * Plugin list generator operation definition + */ + const PLUGIN_LIST_GENERATOR = 'plugin_list_generator'; + /** * Operations definitions * @@ -73,6 +78,7 @@ class OperationFactory self::REPOSITORY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\RepositoryGenerator::class, self::PROXY_GENERATOR => \Magento\Setup\Module\Di\App\Task\Operation\ProxyGenerator::class, self::APPLICATION_ACTION_LIST_GENERATOR => AppActionListGenerator::class, + self::PLUGIN_LIST_GENERATOR => PluginListGenerator::class, ]; /** diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php b/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php index f01c99ccc336f..f205f255abb2a 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Console/Command/DeployStaticContentCommandTest.php @@ -10,21 +10,24 @@ use Magento\Deploy\Console\ConsoleLogger; use Magento\Deploy\Console\ConsoleLoggerFactory; use Magento\Deploy\Console\DeployStaticOptions; - use Magento\Deploy\Console\InputValidator; +use Magento\Deploy\Process\TimeoutException; use Magento\Deploy\Service\DeployStaticContent; use Magento\Framework\App\State; +use Magento\Framework\Console\Cli; use Magento\Framework\ObjectManagerInterface; use Magento\Framework\TestFramework\Unit\Helper\ObjectManager; - use Magento\Setup\Console\Command\DeployStaticContentCommand; use Magento\Setup\Model\ObjectManagerProvider; use PHPUnit\Framework\MockObject\MockObject as Mock; - use PHPUnit\Framework\TestCase; - use Symfony\Component\Console\Tester\CommandTester; +/** + * Test for static content deploy command + * + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) + */ class DeployStaticContentCommandTest extends TestCase { /** @@ -111,7 +114,8 @@ public function testExecute($input) $this->deployService->expects($this->once())->method('deploy'); $tester = new CommandTester($this->command); - $tester->execute($input); + $exitCode = $tester->execute($input); + $this->assertEquals(Cli::RETURN_SUCCESS, $exitCode); } /** @@ -129,6 +133,36 @@ public function executeDataProvider() ]; } + /** + * @return void + */ + public function testExecuteWithError() + { + $this->appState->expects($this->once()) + ->method('getMode') + ->willReturn(State::MODE_PRODUCTION); + + $this->inputValidator->expects($this->once()) + ->method('validate'); + + $this->consoleLoggerFactory->expects($this->once()) + ->method('getLogger') + ->willReturn($this->logger); + $this->logger->expects($this->once()) + ->method('error'); + + $this->objectManager->expects($this->once()) + ->method('create') + ->willReturn($this->deployService); + $this->deployService->expects($this->once()) + ->method('deploy') + ->willThrowException(new TimeoutException()); + + $tester = new CommandTester($this->command); + $exitCode = $tester->execute([]); + $this->assertEquals(Cli::RETURN_FAILURE, $exitCode); + } + /** * @param string $mode * @return void diff --git a/setup/src/Magento/Setup/Test/Unit/Console/Command/DiCompileCommandTest.php b/setup/src/Magento/Setup/Test/Unit/Console/Command/DiCompileCommandTest.php index e269f89073dd7..0386353a0b2df 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/Command/DiCompileCommandTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Console/Command/DiCompileCommandTest.php @@ -65,6 +65,9 @@ class DiCompileCommandTest extends TestCase /** @var OutputFormatterInterface|MockObject */ private $outputFormatterMock; + /** @var Filesystem\Io\File|MockObject */ + private $fileMock; + protected function setUp(): void { $this->deploymentConfigMock = $this->createMock(DeploymentConfig::class); @@ -96,6 +99,14 @@ protected function setUp(): void $this->fileDriverMock = $this->getMockBuilder(File::class) ->disableOriginalConstructor() ->getMock(); + $this->fileDriverMock->method('getParentDirectory')->willReturnMap( + [ + ['/path/to/module/one', '/path/to/module'], + ['/path/to/module', '/path/to'], + ['/path (1)/to/module/two', '/path (1)/to/module'], + ['/path (1)/to/module', '/path (1)/to'], + ] + ); $this->componentRegistrarMock = $this->createMock(ComponentRegistrar::class); $this->componentRegistrarMock->expects($this->any())->method('getPaths')->willReturnMap([ [ComponentRegistrar::MODULE, ['/path/to/module/one', '/path (1)/to/module/two']], @@ -108,6 +119,17 @@ protected function setUp(): void $this->outputMock = $this->getMockForAbstractClass(OutputInterface::class); $this->outputMock->method('getFormatter') ->willReturn($this->outputFormatterMock); + $this->fileMock = $this->getMockBuilder(Filesystem\Io\File::class) + ->disableOriginalConstructor() + ->getMock(); + $this->fileMock->method('getPathInfo')->willReturnMap( + [ + ['/path/to/module/one', ['basename' => 'one']], + ['/path/to/module', ['basename' => 'module']], + ['/path (1)/to/module/two', ['basename' => 'two']], + ['/path (1)/to/module', ['basename' => 'module']], + ] + ); $this->command = new DiCompileCommand( $this->deploymentConfigMock, @@ -116,7 +138,8 @@ protected function setUp(): void $objectManagerProviderMock, $this->filesystemMock, $this->fileDriverMock, - $this->componentRegistrarMock + $this->componentRegistrarMock, + $this->fileMock ); } @@ -160,7 +183,7 @@ public function testExecute() ->with(ProgressBar::class) ->willReturn($progressBar); - $this->managerMock->expects($this->exactly(8))->method('addOperation') + $this->managerMock->expects($this->exactly(9))->method('addOperation') ->withConsecutive( [OperationFactory::PROXY_GENERATOR, []], [OperationFactory::REPOSITORY_GENERATOR, $this->anything()], @@ -178,7 +201,8 @@ public function testExecute() [OperationFactory::INTERCEPTION, $this->anything()], [OperationFactory::AREA_CONFIG_GENERATOR, $this->anything()], [OperationFactory::INTERCEPTION_CACHE, $this->anything()], - [OperationFactory::APPLICATION_ACTION_LIST_GENERATOR, $this->anything()] + [OperationFactory::APPLICATION_ACTION_LIST_GENERATOR, $this->anything()], + [OperationFactory::PLUGIN_LIST_GENERATOR, $this->anything()] ); $this->managerMock->expects($this->once())->method('process'); diff --git a/setup/src/Magento/Setup/Test/Unit/Console/CompilerPreparationTest.php b/setup/src/Magento/Setup/Test/Unit/Console/CompilerPreparationTest.php index cca3daadab73c..beb90950e2def 100644 --- a/setup/src/Magento/Setup/Test/Unit/Console/CompilerPreparationTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Console/CompilerPreparationTest.php @@ -139,7 +139,25 @@ public function commandNameDataProvider() 'commandName' => 'not:a:compiler', 'isCompileCommand' => false, 'isHelpOption' => false, - ] + ], + 'ST compiler, directory exists, abbreviation 1' => [ + 'commandName' => 's:d:c', + 'isCompileCommand' => true, + 'isHelpOption' => false, + 'dirExists' => true + ], + 'ST compiler, directory exists, abbreviation 2' => [ + 'commandName' => 'se:di:co', + 'isCompileCommand' => true, + 'isHelpOption' => false, + 'dirExists' => true + ], + 'ST compiler, directory exists, abbreviation ambiguous' => [ + 'commandName' => 'se:di', + 'isCompileCommand' => false, + 'isHelpOption' => false, + 'dirExists' => true + ], ]; } diff --git a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php index 0d12a73434355..48afa684bb9d2 100644 --- a/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php +++ b/setup/src/Magento/Setup/Test/Unit/Model/InstallerTest.php @@ -450,12 +450,12 @@ public function installDataProvider() ['Installing user configuration...'], ['Enabling caches:'], ['Current status:'], - [print_r(['foo' => 1, 'bar' => 1], true)], + ['foo: 1'], + ['bar: 1'], ['Installing data...'], ['Data install/update:'], ['Disabling caches:'], ['Current status:'], - [print_r([], true)], ['Module \'Foo_One\':'], ['Module \'Bar_Two\':'], ['Data post-updates:'], @@ -463,7 +463,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Caches clearing:'], ['Cache cleared successfully'], ['Disabling Maintenance Mode:'], @@ -502,12 +501,12 @@ public function installDataProvider() ['Installing user configuration...'], ['Enabling caches:'], ['Current status:'], - [print_r(['foo' => 1, 'bar' => 1], true)], + ['foo: 1'], + ['bar: 1'], ['Installing data...'], ['Data install/update:'], ['Disabling caches:'], ['Current status:'], - [print_r([], true)], ['Module \'Foo_One\':'], ['Module \'Bar_Two\':'], ['Data post-updates:'], @@ -515,7 +514,6 @@ public function installDataProvider() ['Module \'Bar_Two\':'], ['Enabling caches:'], ['Current status:'], - [print_r([], true)], ['Installing admin user...'], ['Caches clearing:'], ['Cache cleared successfully'], @@ -590,11 +588,12 @@ public function testUpdateModulesSequence() ); $installer = $this->prepareForUpdateModulesTests(); - $this->logger->expects($this->at(0))->method('log')->with('Cache cleared successfully'); - $this->logger->expects($this->at(1))->method('log')->with('File system cleanup:'); - $this->logger->expects($this->at(2))->method('log') + $this->logger->expects($this->at(0))->method('log')->with('Cache types config flushed successfully'); + $this->logger->expects($this->at(1))->method('log')->with('Cache cleared successfully'); + $this->logger->expects($this->at(2))->method('log')->with('File system cleanup:'); + $this->logger->expects($this->at(3))->method('log') ->with('The directory \'/generation\' doesn\'t exist - skipping cleanup'); - $this->logger->expects($this->at(3))->method('log')->with('Updating modules:'); + $this->logger->expects($this->at(4))->method('log')->with('Updating modules:'); $installer->updateModulesSequence(false); } @@ -604,8 +603,9 @@ public function testUpdateModulesSequenceKeepGenerated() $installer = $this->prepareForUpdateModulesTests(); - $this->logger->expects($this->at(0))->method('log')->with('Cache cleared successfully'); - $this->logger->expects($this->at(1))->method('log')->with('Updating modules:'); + $this->logger->expects($this->at(0))->method('log')->with('Cache types config flushed successfully'); + $this->logger->expects($this->at(1))->method('log')->with('Cache cleared successfully'); + $this->logger->expects($this->at(2))->method('log')->with('Updating modules:'); $installer->updateModulesSequence(true); }